@bezda/rhp-mcp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +20 -5
  2. package/bin/rhp-mcp.js +5 -1
  3. package/package.json +3 -3
  4. package/skill/SKILL.md +116 -26
  5. package/skill/recipes/area.html +6 -0
  6. package/skill/recipes/bar.html +9 -1
  7. package/skill/recipes/box-plot.html +9 -1
  8. package/skill/recipes/bubble.html +6 -0
  9. package/skill/recipes/bullet.html +6 -0
  10. package/skill/recipes/candlestick.html +9 -1
  11. package/skill/recipes/column.html +18 -5
  12. package/skill/recipes/dial.html +349 -0
  13. package/skill/recipes/diverging-bars.html +6 -0
  14. package/skill/recipes/donut.html +9 -1
  15. package/skill/recipes/dumbbell.html +6 -0
  16. package/skill/recipes/gantt.html +17 -5
  17. package/skill/recipes/grouped-bars.html +6 -0
  18. package/skill/recipes/heatmap.html +9 -1
  19. package/skill/recipes/histogram.html +9 -1
  20. package/skill/recipes/line.html +7 -1
  21. package/skill/recipes/live.html +6 -0
  22. package/skill/recipes/log-line.html +493 -0
  23. package/skill/recipes/lollipop.html +6 -0
  24. package/skill/recipes/multi-line.html +10 -5
  25. package/skill/recipes/pyramid.html +9 -1
  26. package/skill/recipes/race.html +6 -0
  27. package/skill/recipes/radial-bars.html +6 -0
  28. package/skill/recipes/scatter.html +23 -4
  29. package/skill/recipes/silhouettes.html +354 -0
  30. package/skill/recipes/slope.html +23 -4
  31. package/skill/recipes/sparklines.html +7 -1
  32. package/skill/recipes/stacked-100.html +6 -0
  33. package/skill/recipes/stacked-bars.html +6 -0
  34. package/skill/recipes/strip.html +9 -1
  35. package/skill/recipes/unit-stack.html +276 -0
  36. package/skill/recipes/violin.html +6 -0
  37. package/skill/recipes/waffle.html +6 -0
  38. package/skill/recipes/waterfall.html +6 -0
  39. package/skill/references/api.md +150 -22
  40. package/skill/references/design.md +303 -326
  41. package/skill/references/environments.md +11 -4
  42. package/skill/references/forms.md +41 -4
  43. package/skill/references/interaction.md +7 -2
  44. package/skill/references/looks.md +267 -0
  45. package/skill/references/performance.md +143 -0
  46. package/skill/references/pitfalls.md +52 -7
  47. package/src/check/README.md +33 -3
  48. package/src/check/browser.js +11 -5
  49. package/src/check/findings.js +15 -1
  50. package/src/check/index.js +182 -31
  51. package/src/check/inpage.js +82 -18
  52. package/src/check/interact.js +1 -19
  53. package/src/check/network.js +168 -0
  54. package/src/check/png.js +18 -0
  55. package/src/check/report.js +4 -1
  56. package/src/check/selection.js +123 -0
  57. package/src/guard/rhp.js +4 -0
  58. package/src/guard/rules.js +1 -1
  59. package/src/server.js +9 -8
@@ -0,0 +1,276 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Monthly spending</title>
7
+ <meta name="description" content="Unit stack: stacked columns cut into counted units (coins), the picked part moved to the foot of every stack with its amount under the column, a picker of parts ranked by their total, and columns that lie down as rows on a phone. Use it for a total made of parts over periods, with one part to follow.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: stacked columns cut into units of one fixed amount by a mask (every full unit the same size, the last one of each part cut to what is left), a picked part moved to the foot of every stack with its amount printed under the column, a picker of parts ranked by their total, headline and figures computed from one data block, and columns that lie down as rows when they get too narrow.
11
+ Leave: the design. This poster was made for its own subject (a household's monthly spending as piles of coins on one counter: mint paper where coins are struck, copper for the picked category, milled edges top and bottom).
12
+ Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
13
+ -->
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Old+Standard+TT:wght@400;700&family=Hanken+Grotesk:wght@400;500;600;700&display=swap">
17
+ <script type="importmap">{ "imports": { "@bezda/rhp/standalone": "https://cdn.jsdelivr.net/npm/@bezda/rhp@2/dist/standalone.js" } }</script>
18
+ <style>
19
+ /* Tokens: mint paper (where coins are struck), deep green ink, copper for the coins of the picked category */
20
+ :root {
21
+ --page: #b9d3c1; --paper: #dcebdf; --ink: #11301f; --soft: #46634f; --rule: #b2ccb9;
22
+ --accent: #a03a14; --quiet: #5d8569;
23
+ --display: "Old Standard TT", "Iowan Old Style", Georgia, serif;
24
+ --text: "Hanken Grotesk", "Helvetica Neue", Arial, sans-serif;
25
+ }
26
+ * { box-sizing: border-box; }
27
+ body { margin: 0; padding: 48px 16px; background: var(--page, #b9d3c1); }
28
+
29
+ /* The poster, its top and bottom edges milled like the rim of a coin */
30
+ .poster { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); row-gap: 14px; max-width: 1080px; margin: 0 auto; padding: 50px 52px 34px;
31
+ background: var(--paper); color: var(--ink); font: 16px/1.5 var(--text);
32
+ grid-template-areas: "kicker" "headline" "dek" "rail" "stage" "average" "note"; }
33
+ .poster::before, .poster::after { content: ""; position: absolute; left: 0; right: 0; height: 7px;
34
+ background: repeating-linear-gradient(90deg, var(--quiet) 0 2px, transparent 2px 5px); }
35
+ .poster::before { top: 0; }
36
+ .poster::after { bottom: 0; }
37
+ .poster figcaption { display: contents; }
38
+ .poster .kicker { grid-area: kicker; font: 700 12px/1.3 var(--text); letter-spacing: .2em; text-transform: uppercase; color: var(--accent); }
39
+ .poster .headline { grid-area: headline; max-width: 17em; font: 700 clamp(31px, 4.3vw, 46px)/1.06 var(--display); letter-spacing: -.01em; text-wrap: balance; }
40
+ .poster .dek { grid-area: dek; max-width: 54ch; font-size: 17px; line-height: 1.5; color: var(--soft); }
41
+ .poster .note { grid-area: note; display: block; margin-top: 8px; padding-top: 12px; border-top: 1px solid var(--rule); font-size: 12px; line-height: 1.5; color: var(--soft); }
42
+ .stage { grid-area: stage; min-width: 0; }
43
+
44
+ /* The average month, set like the total on a statement */
45
+ .average { grid-area: average; display: grid; gap: 2px; margin: 0; padding-top: 14px; border-top: 2px solid var(--ink); }
46
+ .average b { font: 700 44px/1 var(--display); font-variant-numeric: tabular-nums; }
47
+ .average span { font-size: 14px; color: var(--soft); }
48
+ .average span:first-of-type { font-weight: 600; color: var(--ink); }
49
+
50
+ /* The rail: the categories ranked by their year, each a button that puts its coins at the foot of every month */
51
+ .rail { grid-area: rail; display: grid; align-content: start; gap: 10px; min-width: 0; }
52
+ .hint { margin: 0; font-size: 14px; font-weight: 600; }
53
+ .picker { display: flex; flex-wrap: wrap; gap: 6px; }
54
+ .picker button { display: inline-flex; align-items: center; gap: 8px; min-height: 34px; padding: 6px 12px 6px 9px; border: 1px solid var(--rule); border-radius: 3px;
55
+ background: none; color: var(--ink); font: 500 14px/1.2 var(--text); text-align: left; cursor: pointer; }
56
+ .picker .coin { flex: none; width: 14px; height: 14px; border-radius: 50%; background: var(--quiet); }
57
+ .picker .sum { display: none; margin-left: auto; padding-left: 12px; font-variant-numeric: tabular-nums; color: var(--soft); }
58
+ .picker button[aria-pressed="true"] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent); font-weight: 700; }
59
+ .picker button[aria-pressed="true"] .coin { background: var(--accent); }
60
+ .picker button[aria-pressed="true"] .sum { color: var(--accent); }
61
+ .poster :focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
62
+ .readout { display: grid; margin: 0; font-size: 14px; line-height: 1.45; color: var(--soft); font-variant-numeric: tabular-nums; }
63
+ .readout span { grid-area: 1 / 1; }
64
+ .readout .off { visibility: hidden; }
65
+ .readout b { color: var(--ink); }
66
+
67
+ @media (min-width: 700px) {
68
+ .picker .sum { display: inline; }
69
+ }
70
+
71
+ /* From 1120px: the words over the chart on the left, the average beside the headline, the categories in a rail */
72
+ @media (min-width: 1120px) {
73
+ .poster { grid-template-columns: minmax(0, 1fr) 268px; column-gap: 56px;
74
+ grid-template-areas: "kicker kicker" "headline average" "dek average" "stage rail" "note note"; }
75
+ .poster .dek { margin-bottom: 18px; }
76
+ .average { align-self: end; margin-bottom: 18px; }
77
+ .average b { font-size: 58px; }
78
+ .rail { padding-top: 4px; }
79
+ .picker { display: grid; gap: 0; border-bottom: 1px solid var(--rule); }
80
+ .picker button { min-height: 40px; padding: 8px 6px; border: 0; border-top: 1px solid var(--rule); border-radius: 0; font-size: 15px; }
81
+ .picker button[aria-pressed="true"] { box-shadow: none; background: color-mix(in oklab, var(--accent) 9%, var(--paper)); }
82
+ }
83
+
84
+ @media (max-width: 640px) {
85
+ body { padding: 16px 16px 32px; }
86
+ .poster { padding: 30px 18px 22px; row-gap: 12px; }
87
+ .poster .kicker { font-size: 11px; letter-spacing: .12em; }
88
+ .poster .dek { font-size: 15px; }
89
+ .rail { gap: 8px; }
90
+ .picker button { min-height: 30px; padding: 4px 10px 4px 8px; font-size: 13px; }
91
+ .picker .coin { width: 12px; height: 12px; }
92
+ .average b { font-size: 38px; }
93
+ }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div id="chart"></div>
98
+ <script type="module">
99
+ import { Chart, Plot, Bar, Label, Tick, Poster, slat, nice, html, render, createSignal, createMemo, onMount, onCleanup } from "@bezda/rhp/standalone";
100
+
101
+ // Your data
102
+ // Illustrative numbers for an invented household: put your own in their place. One line per month ("YYYY-MM"),
103
+ // then what you spent in each category, in the order of CATEGORIES. Any number of months and categories works,
104
+ // and every title and number on the page is worked out from these lines.
105
+ const CURRENCY = "USD";
106
+ const LOCALE = "en-US";
107
+ const CATEGORIES = ["Rent", "Bills", "Groceries", "Eating out", "Transport", "Shopping", "Travel"];
108
+ const MONTHS = [
109
+ ["2025-10", 1650, 196, 548, 286, 164, 212, 84],
110
+ ["2025-11", 1650, 238, 572, 254, 158, 468, 46],
111
+ ["2025-12", 1650, 284, 694, 412, 196, 836, 612],
112
+ ["2026-01", 1650, 302, 516, 168, 152, 124, 0],
113
+ ["2026-02", 1650, 288, 488, 236, 148, 146, 38],
114
+ ["2026-03", 1650, 246, 532, 248, 171, 198, 142],
115
+ ["2026-04", 1650, 208, 541, 274, 486, 236, 96],
116
+ ["2026-05", 1650, 182, 559, 318, 182, 184, 228],
117
+ ["2026-06", 1650, 176, 566, 342, 174, 252, 312],
118
+ ["2026-07", 1700, 214, 498, 296, 216, 208, 448],
119
+ ["2026-08", 1700, 226, 462, 188, 138, 316, 1864],
120
+ ["2026-09", 1700, 192, 551, 302, 168, 238, 72],
121
+ ];
122
+ const NOTE = "Illustrative data for an invented household.";
123
+
124
+ // What the page says, worked out from the data above
125
+ const money = new Intl.NumberFormat(LOCALE, { style: "currency", currency: CURRENCY, maximumFractionDigits: 0 });
126
+ const compact = new Intl.NumberFormat(LOCALE, { style: "currency", currency: CURRENCY, notation: "compact", maximumFractionDigits: 1 });
127
+ const percent = new Intl.NumberFormat(LOCALE, { style: "percent" });
128
+ const sum = (values) => values.reduce((a, b) => a + b, 0);
129
+
130
+ const DATES = MONTHS.map(([ym]) => new Date(+ym.slice(0, 4), +ym.slice(5, 7) - 1, 1));
131
+ const AMOUNTS = MONTHS.map(([, ...amounts]) => amounts);
132
+ const TOTALS = AMOUNTS.map(sum);
133
+ const ALL = sum(TOTALS);
134
+ const AVERAGE = ALL / MONTHS.length;
135
+ const short = new Intl.DateTimeFormat(LOCALE, { month: "short" });
136
+ const shortYear = new Intl.DateTimeFormat(LOCALE, { month: "short", year: "numeric" });
137
+ const long = new Intl.DateTimeFormat(LOCALE, { month: "long" });
138
+ const longYear = new Intl.DateTimeFormat(LOCALE, { month: "long", year: "numeric" });
139
+ const SHORT = DATES.map((d) => short.format(d));
140
+ // A month named in a sentence takes its year when the data has that month twice, and so do the months under the coins
141
+ const named = (i) => (SHORT.filter((m) => m === SHORT[i]).length > 1 ? longYear : long).format(DATES[i]);
142
+ const DATED = new Set(SHORT).size < SHORT.length;
143
+ const LABELS = DATED ? DATES.map((d) => new Intl.DateTimeFormat(LOCALE, { month: "short", year: "2-digit" }).format(d)) : SHORT;
144
+ // Amounts under the coins: whole units, or compact with one decimal once they need five digits
145
+ const BIG = Math.max(...TOTALS) >= 10000;
146
+ const tenths = new Intl.NumberFormat(LOCALE, { style: "currency", currency: CURRENCY, notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 });
147
+ const amount = (v) => (BIG ? tenths : money).format(v);
148
+
149
+ // Each category's year, ranked; and the one that swings most from month to month, which the headline is about
150
+ const YEAR = CATEGORIES.map((_, c) => sum(AMOUNTS.map((a) => a[c])));
151
+ const RANKED = CATEGORIES.map((_, c) => c).sort((a, b) => YEAR[b] - YEAR[a]);
152
+ const column = (c) => AMOUNTS.map((a) => a[c]);
153
+ const swing = (c) => Math.max(...column(c)) - Math.min(...column(c));
154
+ const SWING = CATEGORIES.map((_, c) => c).reduce((best, c) => (swing(c) > swing(best) ? c : best), 0);
155
+ // When a value came: "in August", or "in 3 months" when several months share it
156
+ const when = (values, v) => {
157
+ const months = values.flatMap((x, i) => (x === v ? [i] : []));
158
+ return months.length > 1 ? `in ${months.length} months` : `in ${named(months[0])}`;
159
+ };
160
+ const headline = (() => {
161
+ const values = column(SWING);
162
+ const [low, high] = [Math.min(...values), Math.max(...values)];
163
+ return `${CATEGORIES[SWING]} swings your spending most: ${money.format(high)} ${when(values, high)}, ${low === 0 ? "nothing" : "as little as " + money.format(low)} ${when(values, low)}`;
164
+ })();
165
+
166
+ // The scale, and what one coin is worth: a round sum that keeps the tallest month under about 30 coins
167
+ const scale = nice(0, Math.max(...TOTALS));
168
+ const COIN = (() => {
169
+ for (let p = 1; ; p *= 10) for (const s of [1, 2, 2.5, 5]) if (s * p * 30 >= Math.max(...TOTALS)) return s * p;
170
+ })();
171
+ // A coin as a share of the part it is in: tiles that size, laid from the part's foot, make every full coin worth COIN
172
+ // and cut the last one to what is left
173
+ const coin = (v) => (v > 0 ? COIN / v : 1);
174
+ const RANGE = `${longYear.format(DATES[0])} to ${longYear.format(DATES.at(-1))}`;
175
+
176
+ // The chart's font and colors, read from the page's tokens
177
+ const THEME = { font: "var(--text)", ink: "var(--ink)", muted: "var(--soft)", grid: "var(--rule)", surface: "var(--paper)", series: ["var(--accent)", "var(--quiet)"] };
178
+
179
+ // Slat types: a month is a pile of coins on the counter, the picked category's coins at its foot (series-1, copper) and
180
+ // the rest of the month above them (series-2). A mask cuts each part into coins from its foot: every full coin is worth
181
+ // the same and is the same size, and the last one is cut to what is left, so a part keeps its exact height and its coins
182
+ // can be counted. Each coin is bevelled at its corners and stands 2px clear of what is under it, so a part's cut coin
183
+ // never touches the next part's first one.
184
+ const Month = slat({
185
+ inset: { vertical: 0.2, horizontal: 0.18 },
186
+ thickness: { horizontal: 30 },
187
+ room: { vertical: { start: 40, end: 22 }, horizontal: { start: "auto", end: 62 } },
188
+ css: `
189
+ .coins { --rhp-radius: 0px; }
190
+ .coins:vertical {
191
+ mask:
192
+ linear-gradient(to top, #0000 3px, #000 0 calc(100% - 1px), #0000 0) 0 100% / 100% calc(100% * var(--coin)),
193
+ linear-gradient(to top, #0000 2px, #000 0) 50% 100% / calc(100% - 3px) calc(100% * var(--coin)) repeat-y;
194
+ }
195
+ .coins:horizontal {
196
+ mask:
197
+ linear-gradient(to right, #0000 3px, #000 0 calc(100% - 1px), #0000 0) 0 0 / calc(100% * var(--coin)) 100%,
198
+ linear-gradient(to right, #0000 2px, #000 0) 0 50% / calc(100% * var(--coin)) calc(100% - 3px) repeat-x;
199
+ }
200
+ .name { --rhp-label-size: 13px; font-weight: 500; }
201
+ .name .amt { font-weight: 700; color: var(--rhp-series-1); font-variant-numeric: tabular-nums; }
202
+ .name:vertical .m, .name:vertical .amt { display: block; text-align: center; line-height: 1.3; }
203
+ .name:horizontal .m { display: inline-block; width: 2.4em; }
204
+ .dated .name:horizontal .m { width: 3.8em; }
205
+ .name:horizontal .amt { display: inline-block; min-width: 4.6em; text-align: right; }
206
+ .total { font-size: 12px; font-weight: 600; color: var(--rhp-muted); font-variant-numeric: tabular-nums; }
207
+ .most .total { font-weight: 700; color: var(--rhp-ink); }
208
+ `,
209
+ }, (d) => html`
210
+ <div class=${() => ["month", d.most && "most", DATED && "dated"].filter(Boolean).join(" ")}
211
+ aria-label=${() => `${longYear.format(d.date)}: ${money.format(d.total)} in all. ` + RANKED.map((c) => `${CATEGORIES[c]} ${money.format(d.amounts[c])}`).join(", ")}>
212
+ <${Label} edge="start" class="name"><span class="m">${() => d.month}</span><span class="amt">${() => amount(d.lit)}</span><//>
213
+ <${Bar} to=${() => d.lit} color="series-1" class="coins" style=${() => ({ "--coin": coin(d.lit) })} />
214
+ <${Bar} from=${() => d.lit} to=${() => d.total} color="series-2" class="coins" style=${() => ({ "--coin": coin(d.total - d.lit) })} />
215
+ <${Label} at=${() => d.total} class="total">${() => amount(d.total)}<//>
216
+ </div>`);
217
+
218
+ // The counter the coins stand on: one thick line at zero across the plot
219
+ const Counter = slat({ room: {}, css: `.counter { --rhp-tick-width: 3px; }` }, () => html`
220
+ <div><${Tick} at=${0} thick=${1} class="counter" /></div>`);
221
+
222
+ // Chart component: the picked category is the app's state. The months stand as columns while each has 44px or more,
223
+ // and lie as rows when they do not (a phone, or many months).
224
+ function Spending() {
225
+ const [pick, setPick] = createSignal(SWING);
226
+ const lit = createMemo(() => AMOUNTS.map((a) => a[pick()]));
227
+ const [width, setWidth] = createSignal(0);
228
+ const upright = () => width() - 42 >= MONTHS.length * 44;
229
+ let stage;
230
+ onMount(() => {
231
+ setWidth(stage.clientWidth);
232
+ const watch = new ResizeObserver(() => setWidth(stage.clientWidth));
233
+ watch.observe(stage);
234
+ onCleanup(() => watch.disconnect());
235
+ });
236
+ const most = Math.max(...TOTALS);
237
+ // What the picked category came to. Every category's line is laid in one grid cell and only the picked one shows,
238
+ // so the cell is as tall as the longest line and the chart under it never moves.
239
+ const said = (c) => {
240
+ const high = Math.max(...column(c));
241
+ return `${money.format(YEAR[c])} in all (${percent.format(YEAR[c] / ALL)}), most ${when(column(c), high)}: ${money.format(high)}.`;
242
+ };
243
+ return html`
244
+ <${Poster}
245
+ kicker=${`Monthly spending · ${shortYear.format(DATES[0])} to ${shortYear.format(DATES.at(-1))}`}
246
+ title=${headline}
247
+ dek=${`What went out each month, ${RANGE}, piled in coins of ${money.format(COIN)} on one counter.`}
248
+ note=${NOTE}>
249
+ <p class="average"><b>${money.format(AVERAGE)}</b><span>an average month</span><span>${money.format(ALL)} over ${MONTHS.length} months</span></p>
250
+ <div class="rail">
251
+ <p class="hint" id="pick-hint">Pick a category: its coins go in copper at the foot of each pile</p>
252
+ <div class="picker" role="group" aria-labelledby="pick-hint">
253
+ ${RANKED.map((c) => html`
254
+ <button type="button" aria-pressed=${() => pick() === c} onClick=${() => setPick(c)}>
255
+ <i class="coin" aria-hidden="true"></i><span>${CATEGORIES[c]}</span><span class="sum">${money.format(YEAR[c])}</span>
256
+ </button>`)}
257
+ </div>
258
+ <p class="readout" aria-live="polite">
259
+ ${RANKED.map((c) => html`<span class=${() => (pick() === c ? "" : "off")}><b>${CATEGORIES[c]}</b>: ${said(c)}</span>`)}
260
+ </p>
261
+ </div>
262
+ <div class="stage" ref=${(el) => (stage = el)}>
263
+ <${Chart} orientation=${() => (upright() ? "vertical" : "horizontal")} height=${360}
264
+ scale=${[scale.min, scale.max]} ticks=${() => (width() < 360 ? scale.ticks.filter((t, i) => i % 2 === 0) : scale.ticks)} format=${(v) => compact.format(v)}
265
+ label=${`Spending each month, ${RANGE}, in ${CURRENCY}`} theme=${THEME}>
266
+ <${Plot} month=${LABELS} date=${DATES} amounts=${AMOUNTS} total=${TOTALS} lit=${lit} most=${(d) => d.total === most}>${Month}<//>
267
+ <${Plot} overlap=${true} slats=${1} style=${{ "pointer-events": "none" }}>${Counter}<//>
268
+ <//>
269
+ </div>
270
+ <//>`;
271
+ }
272
+
273
+ render(() => html`<${Spending} />`, document.getElementById("chart"));
274
+ </script>
275
+ </body>
276
+ </html>
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Teenagers sleep longer at weekends</title>
7
7
  <meta name="description" content="Violin plot of hours of sleep by age group made with density(): a mirrored Area per group with its median, over a band of the recommended range, and a switch between weeknights and weekends that morphs the shapes. Use it to compare the full distribution of several groups.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: a mirrored Area per group from density(), its median, a band for a recommended range, and a switch that morphs the shapes.
11
+ Leave: the design. This poster was made for its own subject (teenagers' sleep under a night sky, with faint stars and a crescent moon).
12
+ Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
13
+ -->
8
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@1,6..72,400..600&family=Albert+Sans:wght@400..800&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Blood types of 100 people</title>
7
7
  <meta name="description" content="Waffle chart: 100 units in a 10 by 10 grid, one per person, colored by group, with a picker that lights the units a chosen group relates to. Use it for counts out of 100 or shares of a population.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: ten slats of ten Cells with an inline SVG in each, and a picker that lights the units a chosen group relates to.
11
+ Leave: the design. This poster was made for its own subject (blood types as a donor card torn off along its perforation, each person a drop).
12
+ Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
13
+ -->
8
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400..800&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Profit bridge</title>
7
7
  <meta name="description" content="Waterfall chart from a starting value through gains and losses to a final total, each step linked to the next, with a switch between periods. Use it to show how one amount becomes another, step by step.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: Bars between running totals with a link from each step to the next, and a switch between periods.
11
+ Leave: the design. This poster was made for its own subject (a profit bridge as a page of an accountant's ledger, one ruled line per step and the bottom line double ruled).
12
+ Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
13
+ -->
8
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,400..700&family=DM+Serif+Display&display=swap">