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:
- Frontend / template authors — apply these rules when designing new templates or seed data.
- The Buddy
lab-authorskill — 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 linear | Use log |
|---|---|
| Supplies, flows, counts, USD values, market cap | BTC price (always) |
| Ratios (MVRV, SOPR), percentages, funding rates | Equity indices when shown alone (SPY/QQQ/IWM) |
| Daily deltas, net changes | Price models (realized_price, transferred_price) |
| Anything that fluctuates in a bounded range | Cycle-performance overlays |
If a metric spans more than ~2 orders of magnitude over the visible window, prefer log.
3. Chart type per metric
| Style | When | fillOpacity |
|---|---|---|
area | Holdings, supplies, cumulative flows, AUM, stacked cohorts | 0.3 |
line | Ratios (MVRV, SOPR), rates (funding), price overlays | 0.1 |
bar | Daily / 30-day deltas (net flows, supply 30D Δ) | 0.5 |
candlestick | OHLC only | — |
Bars on a log axis render unreadably — keep bars linear.
4. Color palette
4.1 By domain
| Domain | Element | Hex |
|---|---|---|
| Price overlay | BTC price (neutral) | #6b7280 |
| Supply | LTH | #2563eb |
| Supply | STH | #dc2626 |
| Supply | third cohort | #fbbf24 / #FFBB28 |
| ETF | holdings | #f59e0b |
| ETF | AUM | #3b82f6 |
| ETF | net flow | #10b981 |
| ETF | cumulative flow | #059669 |
| ETF | dominance | #8b5cf6 |
| ETF | US holdings | #3b82f6 |
| ETF | realized price | #dc2626 / #ea580c |
| DAT | total holdings | #F7931A (Bitcoin brand) |
| DAT | public companies | #0088FE |
| DAT | governments | #FF4444 |
| DAT | private companies | #FFBB28 |
| DAT | net change | #82ca9d |
| DAT | dominance | #ff7300 |
| Funding | rate | #3b82f6 |
| Funding | spread | #f97316 |
| Funding | overheated zone | rgba(239,68,68,0.1) |
| Funding | capitulation zone | rgba(34,197,94,0.1) |
| Macro | yields (neutral) | #6b7280 |
| Macro | inflation | #f59e0b |
| Macro | VIX / risk | #ef4444 |
| Cycle 1→4 | violet → 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
| Preset | When |
|---|---|
ALL | Long-trend metrics — supply, valuation, cycle overlays, anything with a long history users want to see in full. |
1Y | Noisy metrics where multi-year views obscure the signal — daily net flows, DAT net change, funding spread. |
1W / 1M | Never default — these are user-driven zoom levels. |
7. Styling constants
Use these everywhere unless there is a chart-specific reason not to:
strokeWidth: 2for all lines.fillOpacity: 0.3for areas,0.1for line overlays,0.5for bars.instanceIdformat:{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)
- Good:
- 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.
- Good:
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:
- Resolve the metric IDs via
search_metrics/get_metric. Confirm each metric'sgradeis within the user's tier. - 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.
- Is BTC price one of the series? → price on right with
- Pick chart styles per §3.
- Pick colors per §4.1 — use the established palette for the domain.
- Pick scale per §2.
- Pick date range per §6.
- Add reference lines/areas only when the threshold has a clear meaning (§5).
- Apply styling constants (§7) and naming conventions (§8).
- 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_templatestable) — curated templates seeded byscripts/metrics/*_chart_templates.sqland surfaced viaGET /api/lab/templates. - User-created (
lab_user_chartstable) — what the rest of this guide is about. Saved via the Lab UI, by forking a template, or by Buddy'ssave_chartMCP 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.