app/chart_templates.py
Rama main · réplica de lectura del repositorio original.
"""The default charts, written in the builder's own vocabulary.
Every chart the app ships is a spec here — nothing is drawn by hand-written
code any more. That is the point: if a default can't be expressed as a spec,
then the builder can't reproduce it, and somebody who edits or deletes it can
never get it back.
A new account is seeded with these. From that moment they are that person's
charts: editing one changes their copy, deleting one removes it, and the
template stays here to be re-added from the builder's "Start from a template"
list.
"""
from __future__ import annotations
# key → template. `key` is stable and recorded on the saved chart, so a chart
# that came from a template can still be told apart from one built by hand.
TEMPLATES: tuple[dict, ...] = (
{
"key": "today",
"name": "Percentage difference today",
"blurb": "Last close vs previous close, per open holding",
"width": "full",
"spec": {
"grain": "positions",
"x": "ticker",
"measures": ["pos_day_pct"],
"type": "bar",
},
},
{
"key": "portfolio",
"name": "Portfolio over time",
"blurb": "Invested, market value and net gain, daily",
"width": "full",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["value", "invested", "gain"],
"type": "line",
"bucket": "day",
},
},
{
"key": "periods",
"name": "Performance summary",
"blurb": "1 day to 20 years: what you put in, what it made, the return",
"width": "full",
"spec": {
"grain": "periods",
"x": "period",
"measures": ["p_from", "p_value_then", "p_contributed", "p_growth",
"p_on_money_in", "p_twr", "p_twr_annual"],
"type": "table",
},
},
{
"key": "alloc_invested",
"name": "Total invested %",
"blurb": "All-time outlay by asset class",
"width": "half",
"spec": {
"grain": "positions",
"x": "asset_class",
"measures": ["pos_cost"],
"type": "doughnut",
},
},
{
"key": "alloc_value",
"name": "Total position %",
"blurb": "Current value by asset class",
"width": "half",
"spec": {
"grain": "positions",
"x": "asset_class",
"measures": ["pos_value"],
"type": "doughnut",
},
},
{
"key": "yrpct",
"name": "Yearly performance",
"blurb": "Gain in each financial year as % of what was invested",
"width": "half",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["period_return"],
"type": "bar",
"bucket": "fy",
},
},
{
"key": "gainpct",
"name": "Overall performance by year",
"blurb": "Cumulative return at each financial year end",
"width": "half",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["gain_pct"],
"type": "bar",
"bucket": "fy",
},
},
{
"key": "cum",
"name": "Year-end growth",
"blurb": "Cumulative invested vs cumulative value gain",
"width": "half",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["invested", "value_gain"],
"type": "bar",
"bucket": "fy",
},
},
{
"key": "yearly",
"name": "Yearly invested & return",
"blurb": "What went in and what it made, per financial year",
"width": "half",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["invested_delta", "gain_delta"],
"type": "bar",
"bucket": "fy",
},
},
{
"key": "month_ends",
"name": "Month ends",
"blurb": "The daily series as a table, one row per month",
"width": "full",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["invested", "value", "gain", "gain_pct"],
"type": "table",
"bucket": "month",
},
},
# Beyond the originals — cheap to offer once everything is a spec, and the
# obvious next questions someone asks of a portfolio.
{
"key": "value_by_ticker",
"name": "Value by holding",
"blurb": "What each position is worth right now",
"width": "half",
"spec": {
"grain": "positions",
"x": "ticker",
"measures": ["pos_value"],
"type": "hbar",
},
},
{
"key": "gain_by_ticker",
"name": "Unrealised gain % by holding",
"blurb": "How each position's units held have done against their cost",
"width": "half",
"spec": {
"grain": "positions",
"x": "ticker",
"measures": ["pos_gain_pct"],
"type": "hbar",
},
},
{
"key": "value_over_time_by_holding",
"name": "Value over time, by holding",
"blurb": "One line per holding — how each has grown, not just the total",
"width": "full",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["value"],
"split": "ticker",
"type": "line",
"bucket": "month",
},
},
{
"key": "value_over_time_by_class",
"name": "Value over time, by asset class",
"blurb": "How the mix between ETFs, shares and crypto has shifted",
"width": "full",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["value"],
"split": "asset_class",
"type": "area",
"bucket": "month",
},
},
{
"key": "returns_by_period",
"name": "Return by period",
"blurb": "The time-weighted return over each window, as bars",
"width": "half",
"spec": {
"grain": "periods",
"x": "period",
"measures": ["p_twr"],
"type": "bar",
},
},
{
"key": "value_by_ticker_currency",
"name": "Value by holding and currency",
"blurb": "Each position stacked by the currency it trades in",
"width": "half",
"spec": {
"grain": "positions",
"x": "ticker",
"measures": ["pos_value"],
"split": "currency",
"type": "bar",
},
},
{
"key": "income",
"name": "Cash income by year",
"blurb": "Distributions taken as cash rather than reinvested",
"width": "half",
"spec": {
"grain": "timeseries",
"x": "date",
"measures": ["cash_div"],
"type": "bar",
"bucket": "fy",
},
},
)
BY_KEY = {t["key"]: t for t in TEMPLATES}
# What a new account starts with, in this order. The extras above are offered
# in the builder but not applied unasked — a first look at the page should be
# the set we designed, not everything we could think of.
DEFAULT_KEYS = (
"today",
"portfolio",
"periods",
"alloc_invested",
"alloc_value",
"yrpct",
"gainpct",
"cum",
"yearly",
"month_ends",
)