Skip to main content

Chart Style Book

This is the canonical reference for how a "good" Blocklens chart looks. It distills the rules expressed across the ~70 built-in templates (frontend/src/lab/services/chartTemplates.ts), the DB-seeded templates (scripts/metrics/{etf,dat,funding}_chart_templates.sql), and the rendering behaviour of UnifiedChart.

Two audiences:

  1. Frontend / template authors — apply these rules when designing new templates or seed data.
  2. The Buddy lab-author skill — loads a condensed copy of these rules so AI-generated chart configs match the visual language of the curated catalog.

If a rule below contradicts what a template currently does, the template wins for legacy reasons but new work should follow this guide.


1. Axes

1.1 Price always on the right Y-axis, log scale

Every template that overlays BTC price puts price on a separate right axis with scale: "log". The primary metric goes on the left axis. Linking price visually but separating it numerically keeps fast-growing metrics (holdings, AUM, supply) and price both readable.

Exception: price-model templates (realized_price, transferred_price) put price on the same axis as the model so users can compare two prices on one scale. That axis is log.

1.2 Metrics that share a unit share an axis

  • lth_supply + sth_supply → one left axis (both BTC).
  • lth_mvrv + sth_mvrv → one left axis (both ratios).
  • Funding rate (%) on left, price (USD) on right — different units, different axes.

Do not stack two metrics with very different ranges on the same linear axis. The smaller series will be visually crushed.

1.3 Cycle comparison: allLeftAxis: true + log scale

Cycle-performance templates (e.g. cycle-perf-low, cycle-perf-ath, cycle-perf-halving) force allLeftAxis: true with yAxes: [{ side: "left", scale: "log" }]. Multiple cycles overlay on the same log axis so exponential growth is comparable across cycles.

1.4 Axis label color

  • Multiple differently-colored series sharing an axis → axis label is neutral dark gray #6b7280.
  • Single metric owning an axis → label may inherit that metric's color (used rarely; gray is the safe default).

2. Scale selection

Use linearUse log
Supplies, flows, counts, USD values, market capBTC price (always)
Ratios (MVRV, SOPR), percentages, funding ratesEquity indices when shown alone (SPY/QQQ/IWM)
Daily deltas, net changesPrice models (realized_price, transferred_price)
Anything that fluctuates in a bounded rangeCycle-performance overlays

If a metric spans more than ~2 orders of magnitude over the visible window, prefer log.


3. Chart type per metric

StyleWhenfillOpacity
areaHoldings, supplies, cumulative flows, AUM, stacked cohorts0.3
lineRatios (MVRV, SOPR), rates (funding), price overlays0.1
barDaily / 30-day deltas (net flows, supply 30D Δ)0.5
candlestickOHLC only

Bars on a log axis render unreadably — keep bars linear.


4. Color palette

4.1 By domain

DomainElementHex
Price overlayBTC price (neutral)#6b7280
SupplyLTH#2563eb
SupplySTH#dc2626
Supplythird cohort#fbbf24 / #FFBB28
ETFholdings#f59e0b
ETFAUM#3b82f6
ETFnet flow#10b981
ETFcumulative flow#059669
ETFdominance#8b5cf6
ETFUS holdings#3b82f6
ETFrealized price#dc2626 / #ea580c
DATtotal holdings#F7931A (Bitcoin brand)
DATpublic companies#0088FE
DATgovernments#FF4444
DATprivate companies#FFBB28
DATnet change#82ca9d
DATdominance#ff7300
Fundingrate#3b82f6
Fundingspread#f97316
Fundingoverheated zonergba(239,68,68,0.1)
Fundingcapitulation zonergba(34,197,94,0.1)
Macroyields (neutral)#6b7280
Macroinflation#f59e0b
MacroVIX / risk#ef4444
Cycle 1→4violet → blue → green → orange#8b5cf6#3b82f6#22c55e#f97316

4.2 Multi-metric coloring

When two related cohorts appear together (LTH/STH, US/non-US, public/private), use the established pair (blue + red for LTH/STH) so users learn one mental model. Do not reinvent palettes per chart.


5. Reference lines and areas

Use these only when the threshold has a clear interpretation:

  • Funding rate > 20% → red shaded area, label "Overheated".
  • Funding rate < -10% → green shaded area, label "Capitulation".
  • SOPR = 1.0 → reference line at break-even (opportunity — not yet in templates).
  • MVRV = 1.0 → reference line at fair-value parity.

Avoid decorative lines without numeric meaning. One or two annotations per chart maximum.

"referenceAreas": [
{ "y1": 20, "y2": 100, "yAxisId": "axis-left", "fill": "rgba(239,68,68,0.1)", "label": "Overheated" },
{ "y1": -100, "y2": -10, "yAxisId": "axis-left", "fill": "rgba(34,197,94,0.1)", "label": "Capitulation" }
]

6. Date range defaults

PresetWhen
ALLLong-trend metrics — supply, valuation, cycle overlays, anything with a long history users want to see in full.
1YNoisy metrics where multi-year views obscure the signal — daily net flows, DAT net change, funding spread.
1W / 1MNever default — these are user-driven zoom levels.

7. Styling constants

Use these everywhere unless there is a chart-specific reason not to:

  • strokeWidth: 2 for all lines.
  • fillOpacity: 0.3 for areas, 0.1 for line overlays, 0.5 for bars.
  • instanceId format: {metricId}-{seq} (e.g. price-1, lth_supply-1). Stable across edits.

8. Naming conventions

  • Chart name — noun phrase, title-cased like a column header. ≤60 characters. No emojis.
    • Good: BTC LTH Supply with 30D Change, ETF Net Flow vs. Price
    • Bad: 📈 Bitcoin Long-Term Holders!!! (2024)
  • Description — one sentence describing what the chart shows and the insight it surfaces.
    • Good: Long-term holder supply with 30-day delta bars to surface accumulation and distribution waves.

9. Anti-patterns

Reject these when reviewing a template or AI-generated config:

  • Single metric on a two-axis layout — the empty axis is wasted real estate.
  • More than 4 series on one axis without stacking — visually unreadable.
  • Mixing very different ranges on a single linear axis (e.g. counts 1-2M with price 20k-100k) — the smaller series is invisible.
  • BTC price on a linear axis when paired with a high-growth metric — price will dominate visually.
  • Decorative reference lines without a numeric meaning — adds noise, not signal.
  • Bars on a log scale — render unreadably.
  • Per-chart bespoke palette — breaks the user's learned color associations.

10. Quick reference for the lab-author skill

When Buddy builds a chart config from natural language:

  1. Resolve the metric IDs via search_metrics / get_metric. Confirm each metric's grade is within the user's tier.
  2. Decide axis layout:
    • Is BTC price one of the series? → price on right with scale: "log", primary on left.
    • Do remaining metrics share a unit? → same axis. Different units? → different axes.
  3. Pick chart styles per §3.
  4. Pick colors per §4.1 — use the established palette for the domain.
  5. Pick scale per §2.
  6. Pick date range per §6.
  7. Add reference lines/areas only when the threshold has a clear meaning (§5).
  8. Apply styling constants (§7) and naming conventions (§8).
  9. Run the anti-pattern checklist (§9) before saving.

11. Where the templates live

  • Static (frontend)frontend/src/lab/services/chartTemplates.ts. ~70 templates loaded at app start.
  • Database (lab_chart_templates table) — curated templates seeded by scripts/metrics/*_chart_templates.sql and surfaced via GET /api/lab/templates.
  • User-created (lab_user_charts table) — what the rest of this guide is about. Saved via the Lab UI, by forking a template, or by Buddy's save_chart MCP tool.

The config JSON shape (metrics, formulas, yAxes, dateRange, referenceLines, referenceAreas, etc.) is identical across all three sources — see ChartConfigApi in frontend/src/lab/services/labApi.ts for the TypeScript definition and backend/api_app/app/lab/schemas.py for the Pydantic schema.