The deliberate rules behind every chart in this repo.
Code in lib/format.ts, lib/scale.ts, lib/colors.ts, and the chart components enforce these. When the rule and the code disagree, fix the code. When you change a rule, update this artifact first. Single source of truth: HOUSE_STYLE.md.
Tone and surface
- Calm, data-first, analyst-grade. Carta-esque, not editorial-magazine.
- Inter for every readable string — headlines, body, labels, captions.
- IBM Plex Mono as chart utility only — eyebrows, axis ticks, ticker symbols. Never for body or display.
- Sharp corners on every container. The only curved geometry on the page is the chart line itself.
- No editorial cream, no display serif. White card on a cool gray page.
Color
A single semantic palette. Tokens live in lib/colors.ts.
Surface and chrome
pagepaperborderpilleyebrowBlueText
inkbodymutemuteSoftcaptionData — Wong canon
Wong canon (WSJ, Beautiful Evidence-adjacent): vary tone within a single hue rather than introducing a contrasting one. A negative bar is a desaturated tint of the same slate, never salmon, never red. The dark/light contrast scans at thumbnail size without competing with the parenthetical numbers.
One hue. Three tones.
primaryprimaryTintprimarySoftIndicator — status palette
Indicator chips are the only place green and salmon appear. Never as chart fill.
positiveStatusnegativeStatusDiscipline
- One primary hue for data. The chart is not "Block blue" or "Coinbase rust"; it is the house color, applied to whatever series carries the story.
- Negative bars are tonal, not chromatic.
primaryTintlight slate, not salmon, not red. - Status accents (green / salmon) are reserved for indicator chips and arrows. They never appear as chart fill.
- Never use color as the only carrier of meaning. Sign is also conveyed by position, accounting parens, and arrow glyphs.
- Two colors on a chart means two semantic series —
primary+primarySoft, orinkfor a near-black contrast line.
Number formatting
Implemented in lib/format.ts.
unit: "B" or unit: "M". The unit reflects the context of the deck or memo, not the magnitude of one chart's data. No auto-detection.-$0.6B renders as ($0.6).Operating Income ($ in billions) — never just "Operating Income."step ≥ 1 → integer ($25, $0, ($5)). step < 1 → one decimal ($0.5, $1.0, ($0.5)).|value| ≥ 10 → integer ($24, ($25)). |value| < 10 → one decimal ($1.7, ($0.6)).$24.2B); integer beyond ($207B).Y-axis scale
Three rules, applied in order. Implemented in lib/scale.ts.
4a · Increments must be nice numbers
Acceptable values: 1, 2, 2.5, or 5 × any power of ten — $0.1, $0.2, $0.25, $0.5, $1, etc. Arbitrary divisions like $0.3 or $0.7 force readers to do mental math and read as amateur work. Mixed intervals are never acceptable.
4b · Tick count lands between four and six
Below four → the chart loses its sense of magnitude. Above six → the gridlines compete with the data. When you have more labels than you need, halve the increment count by bumping to the next nice step. Do not shrink the type.
4c · Range extends one increment past the data
Snap the floor and ceiling to the nearest nice number that gives one increment of headroom on each side. Data running from ($0.6) to $1.7 should produce an axis from ($1.0) to $2.0 — not ($0.7) to $1.8 (not nice numbers) and not ($5) to $10 (excessive padding crushes the data).
4d · Zero rule
When zero falls inside the data range, zero is always a labeled tick. An unlabeled zero line is a tell of careless work. When all data is positive and dataMin / dataMax ≥ 0.25, anchor the floor at floor(dataMin). Bar charts always show zero.
4e · Redundancy elimination
When every bar or point is direct-labeled, the y-axis is doing redundant work. Keep two or three reference labels. Drop the rest. Drop the y-axis title — three statements of unit on a single chart is two too many.
<BarChartCarta minimalAxis />
The bar chart below shows the rule applied — only $0 and $8 remain, every bar carries its $X.X value, no axis title.
Area fill
Default to lines. Area fill is only justified for ONE of three jobs:
- MASS — emphasize the size of a single metric over time (e.g., balance-sheet total).
- MIX — composition of a total via stacked areas.
- BACKDROP — render one primary series as a soft context layer behind a key line.
Strict rule: never use an area fill unless zero is visible AND meaningful.
Callouts
Short pieces of text on the chart that do editorial work the surrounding prose cannot. The difference between a chart that delivers a number and a chart that delivers an argument.
Three kinds that earn their keep
- Value labels — sit on data points. Above for positives, below for negatives.
- Event annotations — sit at a specific x-axis point: "FTX collapse · Nov 2022", "Q2 earnings miss".
- Range callouts — label a region rather than a point. Pair with a shaded region.
Formatting
- Smaller font than the chart title — typically 11.5px.
- Sans-serif italic, same family as editorial body. Italics signal "editorial commentary, not data."
- Color drops to tertiary grey (
colors.caption). Never the highlight color. - No box, no fill, no border. Floating text only.
Leader lines
- Thin (0.5–1pt), grey, as short as the geometry allows.
- Never cross other data. Never bend more than once.
- Never end with arrowheads. An arrowhead reads as clip art.
- A small dot at the data end is acceptable; nothing is cleaner.
Restraint
One callout per chart is the default. Two is sometimes warranted. Three is the upper limit. Four is almost always wrong — split the chart in two or let the prose do more work.
annotations={[
{ x: "FY22", text: "FTX collapse · Nov 2022", vAlign: "below" }
]}Shaded regions
Translucent fills behind the data marking some condition over a range of the x-axis. One of the clearest tells separating Bloomberg- or FT-quality charts from default software output.
Four jobs
- Recession bands — NBER-dated downturns, light grey at ~50% opacity. Labeled "Recession" in the corner of the first band, unlabeled thereafter.
- Forecast / projection regions — shade the right edge where actual transitions to estimated. Same accent at 15–25% opacity. Line switches solid → dashed at the boundary.
- Threshold bands — shade a horizontal range to show data position relative to a benchmark.
- Event windows — shade specific x-ranges to highlight a period.
Technical rules
- Opacity 10–25%, never above 30%. Shading lives behind data and must not compete with it.
- Always desaturated. Shading reads as context, not as a second data series.
- Labels go inside the region, in the same grey as the fill, set in the smallest type on the chart. Italic.
- No borders. The fill edge is sharp enough on its own.
- Render order: shaded regions first. Behind every other element.
Mistake to avoid
Never shade something that doesn't correspond to a meaningful change in the data on the chart's axes. A "high inflation period" shading on a chart of housing prices implies the data behaves differently inside the band than outside; if it doesn't, the shading is decoration, and decoration on a chart is chartjunk. Every shaded region should answer "what is different about the data inside this region?" If the answer is "nothing," the shading comes off.
shadedRegions={[
{ xStart: "FY22", xEnd: "FY23", label: "Crypto winter · FTX Nov '22" }
]}Live demo:
Revenue halved through the 2022–2023 crypto winter before a two-year rebound to $7.2B.
Chart-type selection
State the reader claim before picking the chart. Pick the relationship, then the simplest chart that makes it legible.
<StatTile><MetricCard><MultiLineChart><BarChartCarta><ComparisonCardSharp><Watchlist><LogoStrip>Card chrome
Every chart card, top to bottom:
CHART X.Yeyebrow — IBM Plex Mono, deep navy#1e3a8a, 11px, 0.18em tracking, weight 600.- Header pill — beige
#f3f4f6strip, centered title in mono with leading Lucide icon, share icon on the right. - Subtitle sentence — Inter, 14px, body color. One sentence stating the reader claim. Active voice. Numbers drop in naturally (
$24B,($0.6B)). - Chart body — SVG. Smooth Catmull-Rom for lines.
- Source caption — Inter italic, caption grey, 12px. "Captured from [issuer], Form 10-K, fiscal years 2021–2025" or equivalent.
Source-line discipline
- The source line is in the first draft, not the final pass.
- Cite the filing or dataset, not the URL.
- Illustrative figures must be flagged in the source line ("Illustrative — actuals from latest 10-K …").
Direct labels over legends
Three or fewer series → label each line at the right edge. Use a legend only when direct labeling would clutter the chart.
Type scale
4060010600226003260011600135001440011400114001160012400Tables
Tables behave like structured arguments, not spreadsheets. Every table needs one dominant reading path:
- Category → comparison → conclusion
- Company → metric → implication
- Period → value → change
A house-style table makes that argument visible before the reader reads every cell.
13a · When to use a table
Avoid tables when the story is movement over time. Use a chart.
13b · Anatomy
Top to bottom: chart number → title bar → subtitle → column header row (when ≥ 3 columns) → divider → body → divider → source note.
13c · Title and subtitle
Express the idea, not just the data. Weak: "Company Comparison." Better: "Same Vertical · Different Theory of Value." The subtitle answers one of three questions: What is being compared? What is the sample? What should the reader notice? Pattern: [Comparison], across [scope]. [Methodology or caveat].
13d · Column headers
Use whenever the table has 3+ columns. 10–11px, uppercase, letter-spaced, muted navy/slate. Aligned with body content. Functional, not clever — the title can be editorial; headers are functional.
13e · Row height
44–56px72–88px88–108px104–128pxHorizontal dividers only — 1px solid at low opacity. No vertical rules unless the table is dense financial data.
13f · Alignment
- Left: text labels, company names, logos + names.
- Right: numbers, percentages, multiples, rankings.
- Center: arrows, status tags.
- Never center-align large blocks of text in a table — it slows scanning.
13g · Logos
Primary company: 32×32px logo box (icon max 24×24px), name 16–18px semibold, gap 10–12px, opacity 100%.
Secondary / losing company: 28×28px logo box (icon max 22×22px), logo opacity 45–60%, name opacity 55–65%, optional 1px strikethrough in muted color, no bright brand colors. All logos sit in the same invisible box. Don't let one logo be 2× louder than another unless that hierarchy is intentional.
13h · Anchor stat sizing
For comparison cards where the rightmost column is the payoff: 26–32px bold value, 12–13px muted sentence-case label, right-aligned. Stat is the row's payoff, not the whole row.
13i · Emphasis system
Each table picks one primary emphasis system. Don't stack them.
- Bold winner
- Muted loser
- Highlighted row
- Large right-side stat
- Accent-colored status pill
- Filled background cell
For a comparison card: muted loser + bold winner + large anchor stat. That is enough.
13j · Patterns
Comparison table — A vs. B argument. Category | losing bet | → | winning bet | proof point. The arrow does important work: turns a list of logos into an argument.
Financial table — metric | year | year | year. Tabular numerals, tighter rows (44–56px), key row bold, totals separated by stronger rule, no logos.
Ranking table — rank | company | metric | note. Rank small and muted; rank already creates hierarchy. Don't overlay badges on top of it.
Matrix table — company × dimensions. Compact rows, short cells. Symbols only if explained in a legend.
Status table — workstream | status | owner | next milestone. Status pill: small, rounded, muted background, one-word label (Live, Scaling, Testing, Delayed, Watch). For editorial reports, use quieter language than red/yellow/green — RYG is operational dashboard territory.
13k · Decision framework
Decide the table's job before designing it.
Layouts (format · grid · pickable templates)
Every layout sits on the same underlying grid. The discipline of holding format and margins constant is what makes a sequence of different layouts read as a series rather than as one-off graphics.
14a · Format
1600×9001080×13501080×1080—14b · Grid
- Outer margins: 80px on a 1600-wide canvas; 60px on a 1080-wide canvas.
- Top header band: 80–100px tall, fixed.
- Bottom footer band: 80–100px tall, fixed.
- Section dividers: hairline rules at 0.5–0.75px stroke, 18–22% opacity.
14c · Stat-anchored layouts
Three-up. Three thesis statistics in equal-width columns, each with its own micro-visualization. Header → title + subtitle → three columns separated by hairline dividers → optional framework strip. Each column: small mono figure label, giant numeral at 140–180pt, small supporting viz, one-sentence caption. Strongest version uses three different visualization types, not three identical mini-charts. Best at 16:9. Failure mode: three numbers that don't support the same argument.
Single hero number. One massive numeral occupies the center. Numeral at 280–400pt, label above (mono), one-line caption directly below, baseline reference, source in footer. Optional trend chip ("+128% YoY") in the upper-right; optional faint sparkline running underneath the digits. Best at 1:1 or 4:5. Failure mode: using the layout for a generic stat — "73% of CMOs say content marketing is important" looks ridiculous at 400pt.
Versus split. Canvas split into two halves, separated by a thick vertical divider with a small "VS" or directional indicator at the dividing line. Discipline: visually symmetric — same label position, same number size, same caption length on both sides. Asymmetry is the content, never the layout. A small comparative line in the dividing strip ("3.2× difference") makes the relationship explicit. Optional: tint the thesis-supporting side with a barely-visible wash. Best at 16:9. Failure mode: comparing things that aren't comparable. The format implies parity.
14d · Chart-anchored layouts
Editorial chart. One large chart fills most of the frame. Chart sits at 65–75% of vertical canvas, title above, footer below. At least one annotation directly on the chart. A chart without an annotation is a default plot; a chart with one is journalism. Good types: line with one highlighted series, ranked horizontal bars with the top one or two color-accented, area charts with shaded eras, slope graphs comparing two time points. Avoid scatter and stacked bars at hero scale. Failure mode: too many series.
Small multiples grid. Regular grid of identical small charts. 2×3, 2×4, or 3×3 most common. Same axes, same chart type, same encoding in every panel — only the data differs. Shared title above the grid; each panel has its own small label and may carry one annotated value. Shared y-axis scale is non-negotiable for honest comparison. Variants: sparkline grids (9–16 subjects), area panels (cumulative metrics), choropleth maps (geographies). Failure mode: choosing this when the subjects don't deserve equal weight.
Before/after split. Horizontal canvas split into two charts: same metric or subject at two points in time. Identical chart type, axes, treatment. The only difference is the data and the small label above each panel. A transition arrow or callout in the middle narrates the change. Good fit: market-structure changes (pre/post regulation, acquisition, technology shift). Failure mode: two states that aren't fair comparisons.
14e · Comparison and benchmark
Good / Better / Best tier. Three-column table with tier labels at the top (BASELINE / STRONG / ELITE or GOOD / BETTER / BEST), 3–6 metric rows below. Bessemer canonical. Headers in mono small caps, hairline rule below, light zebra striping or thin row rules, rightmost column tinted. Numbers right-aligned with tabular figures. 3–5 rows is the sweet spot. Hard rule: every metric must run in the same direction. If one metric flips, the format breaks. Failure mode: non-credible benchmarks — readers know what's achievable in their industry.
2×2 matrix. Four-quadrant chart with axes labeled at endpoints; quadrants labeled by name; entities plotted within. Strongest variant: the thesis 2×2 — quadrant names make an argument ("Buyer-Builders" / "Outsider Builders" / "Buyer-Buyers" / "Outsider Buyers"). The four labels become the framework. Best at 1:1. Failure mode: putting all your portfolio companies in the upper-right quadrant — readers recognize the move and discount the chart.
League table. Ranked table of 8–15 entities with 3–5 metric columns. Header row in small caps; hairline rule below; subtle row tint for one or two rows of interest (your portfolio, the median, the leader). Numbers right-aligned, tabular figures. Optional delta column on the right with up/down arrows in a tinted box. Hard cap: 5 columns including the entity name. Anything more becomes a spreadsheet.
14f · Landscape and categorical
Market map. Rectangular canvas divided into segments and subsegments; logos populate each segment. CB Insights / a16z signature. Outer frame: 4–8 major categories, header bar at top of each segment. Logos rendered in one neutral color, not original brand colors. Segment headers in mono small caps with a count: PAYMENTS (14). The strongest market maps embed a thesis in the structure — relative segment size, what's labeled "emerging" vs. "established." Best at 16:9 or 4:5; never 1:1. Failure mode: the inventory map. Every company in alphabetical order, no editorial choices, no thesis. Wallpaper.
Funnel or pyramid. Stacked geometric form (trapezoid, triangle, or rectangular tier stack) showing hierarchy or conversion. 4–6 tiers. Variant: the iceberg — visible portion small, underwater portion (below a horizontal water line) much larger. Used for "the surface number is X but the underlying reality is Y" arguments. Variant: the layered stack — horizontal architecture diagram. Best at 4:5 or 9:16. Failure mode: forcing arbitrary data into a funnel when there's no real hierarchy. The format lies.
14g · Editorial and conceptual
Pull quote hero. A single quotation dominates the frame. Quote at 36–56pt, attribution in smaller body sans below. Length: under 25 words ideal, 40 words upper limit. Display serif works better than sans — the contrast against the body's sans creates a "moment." Best at 4:5. Failure mode: quotes that aren't actually quotable. Generic praise from a portfolio CEO is a testimonial, not a pull quote.
Framework definition. Vehicle for propagating new vocabulary (Buyer-Builder, Centaur, G.R.I.T.). Term name at the top, display weight, often heavier or italic. One-sentence definition directly below, supporting visual underneath: formula, four-stage process, 2×2, comparison strip. Variant: the term card — entire frame styled as a glossary entry. Best at 1:1 or 4:5. Strongest definitions are one concept, named in 1–3 words, defined in one sentence.
Timeline / process. Events on a horizontal axis. 4–7 marker points at key dates or stages, each marker labeled. Optional small chart underneath aligned to the same x-axis. Variant: the four-stage flow — four labels arranged horizontally with arrows between them (Pain → Prototype → Pull → Professionalize). Best at 16:9 and wider. Failure mode: too many marker points. Five is the sweet spot, seven is the upper limit. More than seven is illegible at thumbnail size.
14h · Picking from the menu
The right layout follows from the content, not the other way around.
- Three thesis statistics → three-up
- Single dominant finding → hero number
- Direct head-to-head → versus split or before/after
- Comparative benchmarks → Good / Better / Best
- Categorical structure → market map
- New vocabulary → framework definition
- "Surface vs. underwater" → iceberg pyramid
The test: write the headline and supporting facts as a paragraph in plain prose first. The shape of that prose — one big finding, three parallel findings, a contrast, a definition — is the layout the content wants. Let the content choose the shape.
Workflow for a new chart
- Write the one-sentence reader claim.
- Pick the relationship (change-over-time, ranking, deviation, etc.).
- Pick the chart type from §8; pick the layout shape from §14.
- State the unit (B or M) explicitly. Apply Rules 1–7.
- Decide on fill from §5. Default to no fill.
- Write the subtitle in active voice.
- Write the source line.
- Drop into
app/page.tsx. Don't extend or invent a new card type without first proposing it here.