feat(line): add ridgeline as a Line variant (#143)
* feat(line): add ridgeline as a Line variant One distribution per series, stacked at a fixed pitch on a single shared amplitude. Ships three static examples (light, dark, full editorial), the type-line.md spec, and a geometric gate. verify-ridgeline.py derives the figure's one amplitude and its baseline pitch from the figure itself, then holds every vertex to both. It also checks the drawn baseline rules, the shared bin scale, absolute M/L/Z only, the overlap ceiling, the focus pairing and every label binding. test-verify-ridgeline.py proves each check in both polarities and pins the scope treaty with the sibling Line gates. The contract binds data-bins on a <path>, which no other chart in the repo declares, so verify-slopegraph and this gate never claim the same file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(verify-ridgeline): read exponent coordinates as numbers, not commands parse_d scanned for [A-Za-z] independently of the numbers, so the `e` in a valid scientific-notation coordinate such as 4.4e2 was counted as a path command. The explicit-command rule then rejected a correct ridgeline with a message about command counts. Tokenise the d attribute left to right with the number branch tried first, so an exponent is consumed as part of its number. A letter that survives that branch is a real command and still faces the M/L/Z vocabulary check; anything else leaves the path unreadable, which stays a finding. Adds both polarities: a shipped coordinate rewritten as 4.4e2 must verify, and a falsified one written the same way must not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: 0xDarkMatter <0xDarkMatter@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
0xDarkMatter
Claude Fable 5
parent
5538b35116
commit
ddaeae936e
@@ -102,7 +102,7 @@ The pattern owns semantic primitives and its tighter budget; the type owns layou
|
||||
| Ranked hierarchy or conversion drop-off | **Pyramid / funnel** | [type-pyramid.md](references/type-pyramid.md) |
|
||||
| Quantitative comparison across categories | **Bar chart** | [type-bar.md](references/type-bar.md) |
|
||||
| Part-of-whole where the relative sizes are the story | **Treemap** | [type-treemap.md](references/type-treemap.md) |
|
||||
| Continuous trends over time, or change between exactly two states (slopegraph) | **Line chart** | [type-line.md](references/type-line.md) |
|
||||
| Continuous trends over time, change between exactly two states (slopegraph), or one distribution per series (ridgeline) | **Line chart** | [type-line.md](references/type-line.md) |
|
||||
| Tasks and phases on a timeline | **Gantt** | [type-gantt.md](references/type-gantt.md) |
|
||||
| Distribution and correlation between two variables | **Scatter plot** | [type-scatter.md](references/type-scatter.md) |
|
||||
| End-to-end data stack on a container cluster | **High-Level** | [type-high-level.md](references/type-high-level.md) |
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request latency by service · Ridgeline</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); color: var(--color-ink); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 3rem 2rem; }
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
.eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.15; margin-bottom: 1.5rem; }
|
||||
svg { width: 100%; min-width: 760px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Ridgeline · Diagram Design</p>
|
||||
<h1>Checkout is slow in two different ways</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ridgeline-dark-title ridgeline-dark-desc">
|
||||
<title id="ridgeline-dark-title">Checkout is slow in two different ways</title>
|
||||
<desc id="ridgeline-dark-desc">Ridgeline chart of request-latency distributions for five services over one week; each ridge is the share of that service's requests falling in 40-millisecond bins, drawn on one shared amplitude scale, and checkout-api carries a second peak past 300 milliseconds that none of the other four show.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(245,245,245,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- ONE amplitude on every ridge: y = baseline - 2.4 x (share in %). Per-ridge
|
||||
normalisation would let a rare, flat distribution wear the same silhouette
|
||||
as a tight one, which is the single lie this type is built to tell.
|
||||
scripts/verify-ridgeline.py derives the amplitude from the figure and then
|
||||
holds every vertex of every ridge to it. -->
|
||||
<text transform="rotate(-90 24 264)" x="24" y="264" fill="#bfc0c0" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">SHARE OF REQUESTS · 1% = 2.4PX ON EVERY RIDGE</text>
|
||||
|
||||
<!-- One baseline per ridge, 56px pitch. The baseline is where a bin of zero
|
||||
requests sits, so it can never be moved to give a ridge more room. -->
|
||||
<line data-ridge="static-cdn" data-role="baseline" x1="320" y1="152" x2="680" y2="152" stroke="rgba(245,245,245,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="auth-api" data-role="baseline" x1="320" y1="208" x2="680" y2="208" stroke="rgba(245,245,245,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="search-api" data-role="baseline" x1="320" y1="264" x2="680" y2="264" stroke="rgba(245,245,245,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="checkout-api" data-role="baseline" x1="320" y1="320" x2="680" y2="320" stroke="rgba(245,245,245,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="report-export" data-role="baseline" x1="320" y1="376" x2="680" y2="376" stroke="rgba(245,245,245,0.25)" stroke-width="1"/>
|
||||
|
||||
<!-- Shared x-scale: every ridge is sampled at the same 13 bins, 40ms wide,
|
||||
labelled at the lower edge. Straight segments between bins - no smoothing,
|
||||
because a spline through 13 bins invents extrema the sample never had. -->
|
||||
<path data-ridge="static-cdn" data-baseline="152" data-bins="0,26,34,21,11,5,2,1,0,0,0,0,0" d="M320,152 L350,89.6 L380,70.4 L410,101.6 L440,125.6 L470,140 L500,147.2 L530,149.6 L560,152 L590,152 L620,152 L650,152 L680,152 Z" fill="rgba(245,245,245,0.12)" stroke="rgba(245,245,245,0.80)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="static-cdn" data-role="name" x="304" y="155.5" fill="#f5f5f5" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">static-cdn</text>
|
||||
<text data-ridge="static-cdn" data-role="range" x="696" y="155.5" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">40–280 ms</text>
|
||||
<path data-ridge="auth-api" data-baseline="208" data-bins="0,6,28,36,18,7,3,1,1,0,0,0,0" d="M320,208 L350,193.6 L380,140.8 L410,121.6 L440,164.8 L470,191.2 L500,200.8 L530,205.6 L560,205.6 L590,208 L620,208 L650,208 L680,208 Z" fill="rgba(245,245,245,0.12)" stroke="rgba(245,245,245,0.74)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="auth-api" data-role="name" x="304" y="211.5" fill="#f5f5f5" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">auth-api</text>
|
||||
<text data-ridge="auth-api" data-role="range" x="696" y="211.5" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">40–320 ms</text>
|
||||
<path data-ridge="search-api" data-baseline="264" data-bins="0,1,9,22,27,19,11,6,3,1,1,0,0" d="M320,264 L350,261.6 L380,242.4 L410,211.2 L440,199.2 L470,218.4 L500,237.6 L530,249.6 L560,256.8 L590,261.6 L620,261.6 L650,264 L680,264 Z" fill="rgba(245,245,245,0.12)" stroke="rgba(245,245,245,0.68)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="search-api" data-role="name" x="304" y="267.5" fill="#f5f5f5" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">search-api</text>
|
||||
<text data-ridge="search-api" data-role="range" x="696" y="267.5" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">40–400 ms</text>
|
||||
|
||||
<!-- checkout-api FOCAL - and it is the ridge whose SHAPE is the story,
|
||||
not the slowest service. Drawn last so its second peak reads over the
|
||||
ridge below it rather than under. -->
|
||||
<path data-ridge="checkout-api" data-baseline="320" data-bins="0,1,6,17,21,14,8,6,7,9,7,4,0" d="M320,320 L350,317.6 L380,305.6 L410,279.2 L440,269.6 L470,286.4 L500,300.8 L530,305.6 L560,303.2 L590,298.4 L620,303.2 L650,310.4 L680,320 Z" fill="rgba(240,138,89,0.16)" stroke="#f08a59" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
<text data-ridge="checkout-api" data-role="name" x="304" y="323.5" fill="#f5f5f5" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">checkout-api</text>
|
||||
<text data-ridge="checkout-api" data-role="range" x="696" y="323.5" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">40–440 ms</text>
|
||||
<path data-ridge="report-export" data-baseline="376" data-bins="0,0,1,3,7,12,17,19,16,12,9,4,0" d="M320,376 L350,376 L380,373.6 L410,368.8 L440,359.2 L470,347.2 L500,335.2 L530,330.4 L560,337.6 L590,347.2 L620,354.4 L650,366.4 L680,376 Z" fill="rgba(245,245,245,0.12)" stroke="rgba(245,245,245,0.62)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="report-export" data-role="name" x="304" y="379.5" fill="#f5f5f5" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">report-export</text>
|
||||
<text data-ridge="report-export" data-role="range" x="696" y="379.5" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">80–440 ms</text>
|
||||
|
||||
<!-- Bin ticks on the shared latency scale. data-bin binds the printed number,
|
||||
so a caption cannot be edited without its binding. -->
|
||||
<text data-tick="0" data-bin="0" x="320" y="400" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">0</text>
|
||||
<text data-tick="1" data-bin="120" x="410" y="400" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">120</text>
|
||||
<text data-tick="2" data-bin="240" x="500" y="400" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">240</text>
|
||||
<text data-tick="3" data-bin="360" x="590" y="400" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">360</text>
|
||||
<text data-tick="4" data-bin="480" x="680" y="400" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">480</text>
|
||||
<text x="500" y="424" fill="#bfc0c0" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle">REQUEST LATENCY (MS), BIN LOWER EDGE</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="462" x2="960" y2="462" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="478" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
<text x="960" y="478" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.06em" text-anchor="end">SHARE OF EACH SERVICE'S OWN REQUESTS PER 40 MS BIN · 1% = 2.4PX ON EVERY RIDGE · UNSMOOTHED · ILLUSTRATIVE FIGURES</text>
|
||||
|
||||
<rect x="40" y="486" width="24" height="10" fill="rgba(240,138,89,0.16)"/>
|
||||
<line x1="40" y1="486" x2="64" y2="486" stroke="#f08a59" stroke-width="2.4"/>
|
||||
<text x="72" y="496" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">checkout-api · focal, second peak past 300 ms</text>
|
||||
<rect x="360" y="486" width="24" height="10" fill="rgba(245,245,245,0.12)"/>
|
||||
<line x1="360" y1="486" x2="384" y2="486" stroke="rgba(245,245,245,0.80)" stroke-width="1.2"/>
|
||||
<text x="392" y="496" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Other services · strongest tone is the fastest median</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request latency by service · Ridgeline</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 760px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 720px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink-strong { background: rgba(45,49,66,0.80); }
|
||||
.card-dot.ink-light { background: rgba(45,49,66,0.62); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-muted); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Ridgeline · Diagram Design</p>
|
||||
<h1>Checkout is slow in two different ways</h1>
|
||||
<p class="subtitle">Five services, one week of request latency, one amplitude scale. Each ridge is the share of that service's own requests in each 40 ms bin — so the ridges compare shape, not traffic. Four of them have a single hump and a thinning tail; checkout-api has a second, smaller peak past 300 ms, which is a different failure from simply being slow.</p>
|
||||
</div>
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ridgeline-full-title ridgeline-full-desc">
|
||||
<title id="ridgeline-full-title">Checkout is slow in two different ways</title>
|
||||
<desc id="ridgeline-full-desc">Ridgeline chart of request-latency distributions for five services over one week; each ridge is the share of that service's requests falling in 40-millisecond bins, drawn on one shared amplitude scale, and checkout-api carries a second peak past 300 milliseconds that none of the other four show.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- ONE amplitude on every ridge: y = baseline - 2.4 x (share in %). Per-ridge
|
||||
normalisation would let a rare, flat distribution wear the same silhouette
|
||||
as a tight one, which is the single lie this type is built to tell.
|
||||
scripts/verify-ridgeline.py derives the amplitude from the figure and then
|
||||
holds every vertex of every ridge to it. -->
|
||||
<text transform="rotate(-90 24 264)" x="24" y="264" fill="#4f5d75" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">SHARE OF REQUESTS · 1% = 2.4PX ON EVERY RIDGE</text>
|
||||
|
||||
<!-- One baseline per ridge, 56px pitch. The baseline is where a bin of zero
|
||||
requests sits, so it can never be moved to give a ridge more room. -->
|
||||
<line data-ridge="static-cdn" data-role="baseline" x1="320" y1="152" x2="680" y2="152" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="auth-api" data-role="baseline" x1="320" y1="208" x2="680" y2="208" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="search-api" data-role="baseline" x1="320" y1="264" x2="680" y2="264" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="checkout-api" data-role="baseline" x1="320" y1="320" x2="680" y2="320" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="report-export" data-role="baseline" x1="320" y1="376" x2="680" y2="376" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
|
||||
<!-- Shared x-scale: every ridge is sampled at the same 13 bins, 40ms wide,
|
||||
labelled at the lower edge. Straight segments between bins - no smoothing,
|
||||
because a spline through 13 bins invents extrema the sample never had. -->
|
||||
<path data-ridge="static-cdn" data-baseline="152" data-bins="0,26,34,21,11,5,2,1,0,0,0,0,0" d="M320,152 L350,89.6 L380,70.4 L410,101.6 L440,125.6 L470,140 L500,147.2 L530,149.6 L560,152 L590,152 L620,152 L650,152 L680,152 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.80)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="static-cdn" data-role="name" x="304" y="155.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">static-cdn</text>
|
||||
<text data-ridge="static-cdn" data-role="range" x="696" y="155.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–280 ms</text>
|
||||
<path data-ridge="auth-api" data-baseline="208" data-bins="0,6,28,36,18,7,3,1,1,0,0,0,0" d="M320,208 L350,193.6 L380,140.8 L410,121.6 L440,164.8 L470,191.2 L500,200.8 L530,205.6 L560,205.6 L590,208 L620,208 L650,208 L680,208 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.74)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="auth-api" data-role="name" x="304" y="211.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">auth-api</text>
|
||||
<text data-ridge="auth-api" data-role="range" x="696" y="211.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–320 ms</text>
|
||||
<path data-ridge="search-api" data-baseline="264" data-bins="0,1,9,22,27,19,11,6,3,1,1,0,0" d="M320,264 L350,261.6 L380,242.4 L410,211.2 L440,199.2 L470,218.4 L500,237.6 L530,249.6 L560,256.8 L590,261.6 L620,261.6 L650,264 L680,264 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.68)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="search-api" data-role="name" x="304" y="267.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">search-api</text>
|
||||
<text data-ridge="search-api" data-role="range" x="696" y="267.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–400 ms</text>
|
||||
|
||||
<!-- checkout-api FOCAL - and it is the ridge whose SHAPE is the story,
|
||||
not the slowest service. Drawn last so its second peak reads over the
|
||||
ridge below it rather than under. -->
|
||||
<path data-ridge="checkout-api" data-baseline="320" data-bins="0,1,6,17,21,14,8,6,7,9,7,4,0" d="M320,320 L350,317.6 L380,305.6 L410,279.2 L440,269.6 L470,286.4 L500,300.8 L530,305.6 L560,303.2 L590,298.4 L620,303.2 L650,310.4 L680,320 Z" fill="rgba(235,108,54,0.16)" stroke="#eb6c36" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
<text data-ridge="checkout-api" data-role="name" x="304" y="323.5" fill="#2d3142" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">checkout-api</text>
|
||||
<text data-ridge="checkout-api" data-role="range" x="696" y="323.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–440 ms</text>
|
||||
<path data-ridge="report-export" data-baseline="376" data-bins="0,0,1,3,7,12,17,19,16,12,9,4,0" d="M320,376 L350,376 L380,373.6 L410,368.8 L440,359.2 L470,347.2 L500,335.2 L530,330.4 L560,337.6 L590,347.2 L620,354.4 L650,366.4 L680,376 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.62)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="report-export" data-role="name" x="304" y="379.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">report-export</text>
|
||||
<text data-ridge="report-export" data-role="range" x="696" y="379.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">80–440 ms</text>
|
||||
|
||||
<!-- Bin ticks on the shared latency scale. data-bin binds the printed number,
|
||||
so a caption cannot be edited without its binding. -->
|
||||
<text data-tick="0" data-bin="0" x="320" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">0</text>
|
||||
<text data-tick="1" data-bin="120" x="410" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">120</text>
|
||||
<text data-tick="2" data-bin="240" x="500" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">240</text>
|
||||
<text data-tick="3" data-bin="360" x="590" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">360</text>
|
||||
<text data-tick="4" data-bin="480" x="680" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">480</text>
|
||||
<text x="500" y="424" fill="#4f5d75" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle">REQUEST LATENCY (MS), BIN LOWER EDGE</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="462" x2="960" y2="462" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="478" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
<text x="960" y="478" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.06em" text-anchor="end">SHARE OF EACH SERVICE'S OWN REQUESTS PER 40 MS BIN · 1% = 2.4PX ON EVERY RIDGE · UNSMOOTHED · ILLUSTRATIVE FIGURES</text>
|
||||
|
||||
<rect x="40" y="486" width="24" height="10" fill="rgba(235,108,54,0.16)"/>
|
||||
<line x1="40" y1="486" x2="64" y2="486" stroke="#eb6c36" stroke-width="2.4"/>
|
||||
<text x="72" y="496" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">checkout-api · focal, second peak past 300 ms</text>
|
||||
<rect x="360" y="486" width="24" height="10" fill="rgba(45,49,66,0.12)"/>
|
||||
<line x1="360" y1="486" x2="384" y2="486" stroke="rgba(45,49,66,0.80)" stroke-width="1.2"/>
|
||||
<text x="392" y="496" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Other services · strongest tone is the fastest median</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">FOCAL · CHECKOUT-API</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>A second peak is not a long tail</h3></div>
|
||||
<p>About a third of checkout's requests land past 280 ms, and they arrive as a distinct bump peaking near 360 ms rather than as a thinning tail. A tail says "sometimes slower"; a second peak says there are two paths through the code and one of them is always slow. Its p50 is 160 ms and its p99 is 400 ms — both true, and neither says the requests between them arrive in two clusters instead of one spread.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink-strong"></span><h3>Height is share, so the ridges compare shape</h3></div>
|
||||
<p>static-cdn is the tallest ridge because its requests are concentrated in three bins, not because it serves the most traffic. Each ridge is normalised to its own service's request count and then drawn on one shared amplitude — that is what makes the silhouettes comparable, and it is the one thing a ridgeline must never fudge.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink-light"></span><h3>Overlap is deliberate; occlusion would not be</h3></div>
|
||||
<p>Ridges sit 56 px apart and the tallest rises 86 px, so peaks cross the row above and the stack reads as depth. The rule is that no peak may reach the row two above it — past that a peak hides behind a peak, and the fix is more pitch, never a smaller amplitude on one ridge.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span>share of requests per 40 ms bin · one week · illustrative</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request latency by service · Ridgeline</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); color: var(--color-ink); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 3rem 2rem; }
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
.eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.15; margin-bottom: 1.5rem; }
|
||||
svg { width: 100%; min-width: 760px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Ridgeline · Diagram Design</p>
|
||||
<h1>Checkout is slow in two different ways</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ridgeline-title ridgeline-desc">
|
||||
<title id="ridgeline-title">Checkout is slow in two different ways</title>
|
||||
<desc id="ridgeline-desc">Ridgeline chart of request-latency distributions for five services over one week; each ridge is the share of that service's requests falling in 40-millisecond bins, drawn on one shared amplitude scale, and checkout-api carries a second peak past 300 milliseconds that none of the other four show.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- ONE amplitude on every ridge: y = baseline - 2.4 x (share in %). Per-ridge
|
||||
normalisation would let a rare, flat distribution wear the same silhouette
|
||||
as a tight one, which is the single lie this type is built to tell.
|
||||
scripts/verify-ridgeline.py derives the amplitude from the figure and then
|
||||
holds every vertex of every ridge to it. -->
|
||||
<text transform="rotate(-90 24 264)" x="24" y="264" fill="#4f5d75" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">SHARE OF REQUESTS · 1% = 2.4PX ON EVERY RIDGE</text>
|
||||
|
||||
<!-- One baseline per ridge, 56px pitch. The baseline is where a bin of zero
|
||||
requests sits, so it can never be moved to give a ridge more room. -->
|
||||
<line data-ridge="static-cdn" data-role="baseline" x1="320" y1="152" x2="680" y2="152" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="auth-api" data-role="baseline" x1="320" y1="208" x2="680" y2="208" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="search-api" data-role="baseline" x1="320" y1="264" x2="680" y2="264" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="checkout-api" data-role="baseline" x1="320" y1="320" x2="680" y2="320" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<line data-ridge="report-export" data-role="baseline" x1="320" y1="376" x2="680" y2="376" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
|
||||
<!-- Shared x-scale: every ridge is sampled at the same 13 bins, 40ms wide,
|
||||
labelled at the lower edge. Straight segments between bins - no smoothing,
|
||||
because a spline through 13 bins invents extrema the sample never had. -->
|
||||
<path data-ridge="static-cdn" data-baseline="152" data-bins="0,26,34,21,11,5,2,1,0,0,0,0,0" d="M320,152 L350,89.6 L380,70.4 L410,101.6 L440,125.6 L470,140 L500,147.2 L530,149.6 L560,152 L590,152 L620,152 L650,152 L680,152 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.80)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="static-cdn" data-role="name" x="304" y="155.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">static-cdn</text>
|
||||
<text data-ridge="static-cdn" data-role="range" x="696" y="155.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–280 ms</text>
|
||||
<path data-ridge="auth-api" data-baseline="208" data-bins="0,6,28,36,18,7,3,1,1,0,0,0,0" d="M320,208 L350,193.6 L380,140.8 L410,121.6 L440,164.8 L470,191.2 L500,200.8 L530,205.6 L560,205.6 L590,208 L620,208 L650,208 L680,208 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.74)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="auth-api" data-role="name" x="304" y="211.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">auth-api</text>
|
||||
<text data-ridge="auth-api" data-role="range" x="696" y="211.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–320 ms</text>
|
||||
<path data-ridge="search-api" data-baseline="264" data-bins="0,1,9,22,27,19,11,6,3,1,1,0,0" d="M320,264 L350,261.6 L380,242.4 L410,211.2 L440,199.2 L470,218.4 L500,237.6 L530,249.6 L560,256.8 L590,261.6 L620,261.6 L650,264 L680,264 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.68)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="search-api" data-role="name" x="304" y="267.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">search-api</text>
|
||||
<text data-ridge="search-api" data-role="range" x="696" y="267.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–400 ms</text>
|
||||
|
||||
<!-- checkout-api FOCAL - and it is the ridge whose SHAPE is the story,
|
||||
not the slowest service. Drawn last so its second peak reads over the
|
||||
ridge below it rather than under. -->
|
||||
<path data-ridge="checkout-api" data-baseline="320" data-bins="0,1,6,17,21,14,8,6,7,9,7,4,0" d="M320,320 L350,317.6 L380,305.6 L410,279.2 L440,269.6 L470,286.4 L500,300.8 L530,305.6 L560,303.2 L590,298.4 L620,303.2 L650,310.4 L680,320 Z" fill="rgba(235,108,54,0.16)" stroke="#eb6c36" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
<text data-ridge="checkout-api" data-role="name" x="304" y="323.5" fill="#2d3142" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">checkout-api</text>
|
||||
<text data-ridge="checkout-api" data-role="range" x="696" y="323.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–440 ms</text>
|
||||
<path data-ridge="report-export" data-baseline="376" data-bins="0,0,1,3,7,12,17,19,16,12,9,4,0" d="M320,376 L350,376 L380,373.6 L410,368.8 L440,359.2 L470,347.2 L500,335.2 L530,330.4 L560,337.6 L590,347.2 L620,354.4 L650,366.4 L680,376 Z" fill="rgba(45,49,66,0.12)" stroke="rgba(45,49,66,0.62)" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<text data-ridge="report-export" data-role="name" x="304" y="379.5" fill="#2d3142" font-size="11" font-weight="500" font-family="'Geist', sans-serif" text-anchor="end">report-export</text>
|
||||
<text data-ridge="report-export" data-role="range" x="696" y="379.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">80–440 ms</text>
|
||||
|
||||
<!-- Bin ticks on the shared latency scale. data-bin binds the printed number,
|
||||
so a caption cannot be edited without its binding. -->
|
||||
<text data-tick="0" data-bin="0" x="320" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">0</text>
|
||||
<text data-tick="1" data-bin="120" x="410" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">120</text>
|
||||
<text data-tick="2" data-bin="240" x="500" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">240</text>
|
||||
<text data-tick="3" data-bin="360" x="590" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">360</text>
|
||||
<text data-tick="4" data-bin="480" x="680" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">480</text>
|
||||
<text x="500" y="424" fill="#4f5d75" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle">REQUEST LATENCY (MS), BIN LOWER EDGE</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="462" x2="960" y2="462" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="478" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
<text x="960" y="478" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.06em" text-anchor="end">SHARE OF EACH SERVICE'S OWN REQUESTS PER 40 MS BIN · 1% = 2.4PX ON EVERY RIDGE · UNSMOOTHED · ILLUSTRATIVE FIGURES</text>
|
||||
|
||||
<rect x="40" y="486" width="24" height="10" fill="rgba(235,108,54,0.16)"/>
|
||||
<line x1="40" y1="486" x2="64" y2="486" stroke="#eb6c36" stroke-width="2.4"/>
|
||||
<text x="72" y="496" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">checkout-api · focal, second peak past 300 ms</text>
|
||||
<rect x="360" y="486" width="24" height="10" fill="rgba(45,49,66,0.12)"/>
|
||||
<line x1="360" y1="486" x2="384" y2="486" stroke="rgba(45,49,66,0.80)" stroke-width="1.2"/>
|
||||
<text x="392" y="496" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Other services · strongest tone is the fastest median</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -298,6 +298,9 @@
|
||||
<button class="tab new" data-type="slopegraph">
|
||||
<span class="eyebrow">30</span>Line · slopegraph
|
||||
</button>
|
||||
<button class="tab new" data-type="ridgeline">
|
||||
<span class="eyebrow">20</span>Line · ridgeline
|
||||
</button>
|
||||
<button class="tab new" data-type="dp-security-matrix">
|
||||
<span class="eyebrow">33</span>DP security matrix
|
||||
</button>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
## Variants
|
||||
|
||||
- **Slopegraph:** exactly two states, several series, read as slope and rank change. Full spec below.
|
||||
- **Ridgeline:** one distribution per series, stacked with deliberate overlap on one shared amplitude. Full spec below.
|
||||
|
||||
### Slopegraph
|
||||
|
||||
@@ -138,6 +139,70 @@ What each binding buys, and what it costs to omit:
|
||||
- An unbound visible string: a name, a value or a state caption with no attribute stating the same thing.
|
||||
- Curving the connector. There is nothing between the two points to curve through.
|
||||
|
||||
### Ridgeline
|
||||
|
||||
**Best for:** one **distribution** per series, stacked at a fixed pitch with deliberate overlap, when the shape of each distribution is the story and the series are directly comparable — request latency per service, build duration per pipeline, session length per cohort. The line chart above plots one value per x; a ridgeline plots a whole distribution per row, and the reading is the silhouette: where the mass sits, how tight it is, whether there is a second peak. A p50/p99 pair cannot show a bimodal service, and a box plot flattens it to a rectangle.
|
||||
|
||||
Not for: a single distribution (that is a histogram — one ridge is a ridgeline with the comparison removed); series measured in different units or on different x-ranges, which cannot share the one x-scale the type requires; time series, where each row is one value per moment rather than one distribution; or distributions so similar that the ridges are five copies of one shape, which a table of percentiles says shorter.
|
||||
|
||||
#### Layout conventions
|
||||
|
||||
- **One baseline per ridge at a fixed pitch**, inside the `0 0 1000 500` viewBox the other Line variants use. The shipped example runs five baselines at `y` 152/208/264/320/376, a 56px pitch.
|
||||
- **The x-run is the slopegraph's**, `x` 320 → 680, sampled at 13 bins 30px apart. The gutters are symmetric about it: names right-aligned ending at `x=304`, ranges starting at `x=696`, both 16px clear of the plot.
|
||||
- **Name left of the baseline, range right of it**, each on its ridge's own row (`y = baseline + 3.5`). The range is the span of the ridge's nonzero mass in x-axis units, which is the number a silhouette cannot give you.
|
||||
- **Bin ticks** in Geist Mono 9px at `y=400`, centred on bin positions and tracked `0.14em`, with the x-axis caption at `y=424`. The rotated amplitude caption sits at `x=24`, as in the parent chart.
|
||||
- **Ridge count 3–12, bins 8–40.** Below three ridges there is no family of shapes to compare and two distributions are a pair of small multiples; past twelve the stack is taller than a reader can hold one silhouette in mind across. Below eight bins the outline is a histogram wearing a curve's clothes; past forty the bins are narrower than the noise in them.
|
||||
- **Straight segments between bins, closed with `Z`.** Not splines: the source line states the drawing is unsmoothed, and a curve through binned counts puts extrema between bins that the sample never measured. The draft this variant came from permits Catmull-Rom with vertices on true values; the shipped grammar does not use it, because unsmoothed bins need no footnote about what the curve is allowed to invent.
|
||||
- **No gridlines.** The baselines are the rules, and every ridge prints its own range.
|
||||
|
||||
#### Colour
|
||||
|
||||
The slopegraph's colour section holds here unchanged, with one addition for the fill. One accent on the editorially focal ridge (stroke `accent`, fill the accent at `0.16`), an `ink` opacity ramp for the rest (`0.80 → 0.62`, floor `0.53`) ordered top row to bottom, and focus carried by stroke weight — 2.4px against 1.2px — rather than tone. Labels stay `ink` (names) and `muted` (ranges) on every ridge including the focal one.
|
||||
|
||||
- **Fill `ink` at `0.12` on every non-focal ridge**, low enough that two overlapping ridges read as depth rather than as a third tone. This is the one place the type needs a fill at all: the outline alone does not say which side of the curve is mass.
|
||||
- **Legend wording stays skin-neutral** — "strongest tone", never "darkest". The ramp is ink-at-opacity, so its top is the darkest line on light paper and the lightest on dark, and a legend that says "darker" ships false in one of the two skins while rendering perfectly in both.
|
||||
- **The accent marks the ridge the story is about, not the worst performer.** In the shipped example it marks the service whose *shape* is unusual, not the slowest one.
|
||||
|
||||
#### Honest-data rule
|
||||
|
||||
**One amplitude on every ridge, stated in the source line.** That is the entire claim of the type: the ridges are stacked so their silhouettes can be compared, and per-ridge normalisation destroys exactly that while rendering beautifully — a rare, flat distribution given its own scale wears the same shape as a tight one. `scripts/verify-ridgeline.py` derives the figure's single amplitude and holds every vertex of every ridge to it.
|
||||
|
||||
- **The baseline never lies.** Each ridge declares the row it rises from, and that row must sit on the stack's fixed pitch with its rule drawn across the full bin run. A baseline nudged up to give one ridge headroom is the same falsification as a private amplitude, told with different arithmetic — and it is the harder one to see, because the silhouette above it is untouched.
|
||||
- **Every ridge shares one x-scale.** A peak at one x means one latency on every row, or the column comparison the type exists for is fiction.
|
||||
- **State the overlap, and keep it overlap.** Ridges are meant to intrude on the row above — that is how the stack reads as depth. A peak that reaches the row *two* above is occluded by the peaks it passes, and the fix is more pitch, never a smaller amplitude on one ridge. The checker enforces the ceiling; the source line states the amplitude so the reader can convert a height back into a number.
|
||||
- **A ridge starts and ends on its own baseline.** The first and last bins are zero, so the outline closes along the baseline. A distribution clipped mid-mass draws a cliff, and a cliff reads as data.
|
||||
- **No smoothing beyond what the footnote states.** The shipped grammar smooths nothing at all. If a figure ever does smooth, the footnote names the method and the vertices stay on true values — a spline that invents a second peak is indistinguishable, on the page, from a service that has one.
|
||||
- **Height is share, not volume.** Each ridge is normalised to its own series' count before the shared amplitude is applied, so the tallest ridge is the most concentrated, not the busiest. Say so in the source line; a reader who assumes height is traffic reads the figure exactly backwards.
|
||||
|
||||
#### Declaring the values
|
||||
|
||||
The binding contract is the slopegraph's, applied to areas: the outline declares its bins and its baseline, and every visible string is bound to what it describes.
|
||||
|
||||
```svg
|
||||
<line data-ridge="checkout-api" data-role="baseline" x1="320" y1="320" x2="680" y2="320" stroke="rgba(45,49,66,0.25)" stroke-width="1"/>
|
||||
<path data-ridge="checkout-api" data-baseline="320" data-bins="0,1,6,17,21,14,8,6,7,9,7,4,0" d="M320,320 L350,317.6 L380,305.6 L410,279.2 L440,269.6 L470,286.4 L500,300.8 L530,305.6 L560,303.2 L590,298.4 L620,303.2 L650,310.4 L680,320 Z" fill="rgba(235,108,54,0.16)" stroke="#eb6c36" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
<text data-ridge="checkout-api" data-role="name" x="304" y="323.5" fill="#2d3142" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">checkout-api</text>
|
||||
<text data-ridge="checkout-api" data-role="range" x="696" y="323.5" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">40–440 ms</text>
|
||||
<text data-tick="2" data-bin="240" x="500" y="400" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.14em" text-anchor="middle">240</text>
|
||||
```
|
||||
|
||||
`data-bins` is the basis of every geometric check, and it is this contract's own vocabulary: the slopegraph above binds `data-series` on a `<line>`, this variant binds `data-bins` on a `<path>`, and neither gate reads the other's attribute, so neither claims the other's file. Any further Line variant should take its own attribute for the same reason — a shared name means two checkers holding one figure to two contracts, and the one that loses rejects it for lacking elements it never said it had. `data-baseline` is what makes a moved row detectable; without it the checker would have to infer the zero from the drawing, which is the very thing being falsified. The printed range is cross-checked against the first and last nonzero bin through the figure's own tick scale, so a range widened by a word is a finding. `scripts/verify-ridgeline.py` covers the amplitude, the pitch, the baseline rules, the shared bins, the segment grammar, the overlap ceiling, the focus pairing and every label binding; `scripts/test-verify-ridgeline.py` proves each check in both polarities and pins the scope treaty with the sibling gates.
|
||||
|
||||
**No `transform` on any of it**, for the reason the slopegraph section gives: the checker reads raw coordinates, so a transform on an outline, a baseline rule, a bound label, an ancestor `<g>` or a CSS rule moves the rendered mark away from the bin that was verified. The rotated amplitude caption is fine — it is neither verified geometry nor a bound label.
|
||||
|
||||
#### Anti-patterns
|
||||
|
||||
- Per-ridge normalisation, or any second amplitude anywhere in the figure — the type's one unforgivable error.
|
||||
- A baseline moved off the pitch to buy one ridge headroom, or a baseline rule drawn somewhere other than the row its ridge declares.
|
||||
- Ridges sampled on different x-ranges, or an x-scale that is not printed at all.
|
||||
- Smoothing that puts a peak between two bins, and any spline in a figure whose footnote says the drawing is unsmoothed.
|
||||
- A ridge clipped mid-mass so the outline closes as a cliff.
|
||||
- Pitch so tight that peaks hide behind peaks — increase the pitch, never shrink one ridge.
|
||||
- One hue per ridge instead of the ink ramp plus a single accent.
|
||||
- A legend that says "darker is faster", which is false on one of the two skins.
|
||||
- Fewer than 3 ridges (that is a histogram or a pair of small multiples) or more than 12.
|
||||
- Reading traffic off ridge height, or shipping a figure whose source line lets a reader do so.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-line.html` — minimal light
|
||||
@@ -146,3 +211,6 @@ What each binding buys, and what it costs to omit:
|
||||
- `assets/example-slopegraph.html` — slopegraph, minimal light
|
||||
- `assets/example-slopegraph-dark.html` — slopegraph, minimal dark
|
||||
- `assets/example-slopegraph-full.html` — slopegraph, full editorial
|
||||
- `assets/example-ridgeline.html` — ridgeline, minimal light
|
||||
- `assets/example-ridgeline-dark.html` — ridgeline, minimal dark
|
||||
- `assets/example-ridgeline-full.html` — ridgeline, full editorial
|
||||
|
||||
Reference in New Issue
Block a user