@bezda/rhp-mcp 0.1.0 → 0.1.1

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 (53) hide show
  1. package/README.md +4 -3
  2. package/bin/rhp-mcp.js +5 -1
  3. package/package.json +3 -3
  4. package/skill/SKILL.md +112 -25
  5. package/skill/recipes/area.html +6 -0
  6. package/skill/recipes/bar.html +6 -0
  7. package/skill/recipes/box-plot.html +6 -0
  8. package/skill/recipes/bubble.html +6 -0
  9. package/skill/recipes/bullet.html +6 -0
  10. package/skill/recipes/candlestick.html +6 -0
  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 +6 -0
  15. package/skill/recipes/dumbbell.html +6 -0
  16. package/skill/recipes/gantt.html +14 -4
  17. package/skill/recipes/grouped-bars.html +6 -0
  18. package/skill/recipes/heatmap.html +6 -0
  19. package/skill/recipes/histogram.html +6 -0
  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 +6 -0
  26. package/skill/recipes/race.html +6 -0
  27. package/skill/recipes/radial-bars.html +6 -0
  28. package/skill/recipes/scatter.html +6 -0
  29. package/skill/recipes/silhouettes.html +352 -0
  30. package/skill/recipes/slope.html +6 -0
  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 +6 -0
  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 +3 -1
  44. package/skill/references/looks.md +267 -0
  45. package/skill/references/pitfalls.md +52 -7
  46. package/src/check/README.md +15 -2
  47. package/src/check/findings.js +1 -1
  48. package/src/check/index.js +103 -14
  49. package/src/check/inpage.js +80 -16
  50. package/src/check/report.js +4 -1
  51. package/src/guard/rhp.js +4 -0
  52. package/src/guard/rules.js +1 -1
  53. package/src/server.js +9 -8
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Make charts with [rhp](https://rhp.vercel.app) from any AI agent.
4
4
  rhp 2 came out in September 2026, after the training data of today's models, so an agent knows nothing about it until it reads rhp's guide.
5
- This package carries that guide (the workflow, the references and 30 tested recipes) and a checker that renders the agent's chart in a browser and reports what is wrong, with a fix for each problem and screenshots to look at.
5
+ This package carries that guide (the workflow, the references and the tested recipes) and a checker that renders the agent's chart in a browser and reports what is wrong, with a fix for each problem and screenshots to look at.
6
6
 
7
7
  ## Start here: the rhp skill
8
8
 
@@ -30,6 +30,7 @@ It checks an `.html` page, a Solid or React `.jsx` or `.tsx` component, or a `.j
30
30
  | `--widths 1280,390` | The page widths to check; the interactions are tried at the first, and slats are tapped at phone widths (600px or less). |
31
31
  | `--dark` | Renders with a dark color scheme. |
32
32
  | `--no-interact` | Skips the interactions. |
33
+ | `--settle-timeout <ms>` | How long the page may take to come to rest before it is measured, 100 to 30000 ms (10000 by default); raise it on a slow machine. |
33
34
  | `--format <format>` | `html`, `solid`, `react` or `module`, when the file's extension and code leave it unclear. |
34
35
  | `--json` | Prints the whole result as JSON. |
35
36
  | `--out <dir>` | Where the screenshots go (a folder in the system's temp folder by default). |
@@ -147,9 +148,9 @@ Without `--scope user` it goes to the project's `.gemini/settings.json`.
147
148
  | Tool | What it returns |
148
149
  |---|---|
149
150
  | `rhp_guide` | The workflow from a request to a checked chart, the rules that prevent most bugs, and the names of the references and recipes. The agent calls it first. |
150
- | `rhp_reference` | One reference by `name`: `api`, `design`, `environments`, `forms`, `interaction` or `pitfalls`. A long one comes back as its list of sections, and `section` (a number or a title) returns one of them. |
151
+ | `rhp_reference` | One reference by `name`: `api`, `design`, `environments`, `forms`, `interaction`, `looks` or `pitfalls`. A long one comes back as its list of sections, and `section` (a number or a title) returns one of them. |
151
152
  | `rhp_recipe` | With no `type`, the list of recipes with what each is for; with a `type` such as `bar`, that recipe's whole HTML file, a tested chart page to start from. |
152
- | `rhp_check` | Renders the chart at `file` (an absolute path) or the `code` given, and returns the report (errors and warnings, each with a fix, and the interactions it tried) and the screenshots as images. Options: `format`, `widths` (`[1280, 390]` by default), `dark`, `interact`. |
153
+ | `rhp_check` | Renders the chart at `file` (an absolute path) or the `code` given, and returns the report (errors and warnings, each with a fix, and the interactions it tried) and the screenshots as images. Options: `format`, `widths` (`[1280, 390]` by default, up to six), `dark`, `interact`, `settleTimeout`. |
153
154
 
154
155
  `rhp_check` takes absolute paths only: some clients (Codex) start the server in its own folder, where a relative path would point at the wrong file.
155
156
  The server also has one prompt, `chart`, which clients show as a slash command: it asks for a chart and tells the agent to follow `rhp_guide` and to check with `rhp_check` until the report is clean.
package/bin/rhp-mcp.js CHANGED
@@ -14,6 +14,7 @@ const USAGE = `rhp-mcp: make charts with rhp from an AI agent.
14
14
  --widths 1280,390 page widths to check (the interaction pass runs at the first)
15
15
  --dark emulate a dark color scheme
16
16
  --no-interact skip the interaction pass
17
+ --settle-timeout <ms> readiness wait budget, 100 to 30000 ms (default 10000)
17
18
  --format <format> html, solid, react or module (found from the file when left out)
18
19
  --json print the whole result as JSON
19
20
  --out <dir> where the screenshots go (left out: a folder in the system's temp dir, so nothing is
@@ -24,7 +25,7 @@ const USAGE = `rhp-mcp: make charts with rhp from an AI agent.
24
25
  `;
25
26
 
26
27
  const FLAGS = ["--dark", "--no-interact", "--json"];
27
- const VALUES = ["--widths", "--format", "--out"];
28
+ const VALUES = ["--widths", "--format", "--out", "--settle-timeout"];
28
29
  const FORMATS = ["html", "solid", "react", "module"];
29
30
 
30
31
  function usageError(message) {
@@ -57,9 +58,11 @@ async function runCheck(argv) {
57
58
  }
58
59
 
59
60
  const widths = value["--widths"]?.split(",").map(Number);
61
+ const settleTimeout = value["--settle-timeout"] === undefined ? undefined : Number(value["--settle-timeout"]);
60
62
  if (files.length !== 1) return usageError(files.length ? `check takes one file, not ${files.length}` : "check needs the chart's file");
61
63
  if (widths && !widths.every((w) => Number.isInteger(w) && w >= 200 && w <= 2560)) return usageError("--widths takes page widths in px from 200 to 2560, for example 1280,390");
62
64
  if (value["--format"] && !FORMATS.includes(value["--format"])) return usageError(`--format is one of ${FORMATS.join(", ")}`);
65
+ if (settleTimeout !== undefined && (!Number.isInteger(settleTimeout) || settleTimeout < 100 || settleTimeout > 30000)) return usageError("--settle-timeout takes milliseconds from 100 to 30000");
63
66
 
64
67
  const { check } = await import("../src/check/index.js");
65
68
  const result = await check({
@@ -68,6 +71,7 @@ async function runCheck(argv) {
68
71
  widths,
69
72
  dark: argv.includes("--dark"),
70
73
  interact: !argv.includes("--no-interact"),
74
+ settleTimeout,
71
75
  outDir: value["--out"],
72
76
  });
73
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bezda/rhp-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Make charts with rhp from any AI agent: an MCP server with rhp's guide, references and recipes, and a checker that renders a chart in a browser and reports what is wrong, with screenshots",
5
5
  "keywords": [
6
6
  "rhp",
@@ -47,11 +47,11 @@
47
47
  "scripts": {
48
48
  "build": "node scripts/build.js",
49
49
  "prepack": "npm run build",
50
- "test": "node test/mcp.js && node test/run.js"
50
+ "test": "node test/settle-regression.js && node test/mcp.js && node test/run.js"
51
51
  },
52
52
  "dependencies": {
53
53
  "@babel/core": "^7.29.7",
54
- "@bezda/rhp": "^2.0.1",
54
+ "@bezda/rhp": "^2.0.3",
55
55
  "@modelcontextprotocol/sdk": "^1.32.0",
56
56
  "babel-preset-solid": "^1.9.15",
57
57
  "esbuild": "^0.28.2",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rhp
3
- description: Makes charts, graphs, plots, infographics and data visualizations with rhp (@bezda/rhp, reactive html plots), charts built from HTML elements and CSS with SolidJS that run in a plain HTML page, React, Next.js, Vue, Svelte, Solid or Astro. Use it whenever the user asks to create, restyle, fix or extend a chart and rhp is the library to use, which is when they name rhp or @bezda/rhp, the project depends on @bezda/rhp, or the project has no chart library yet. Covers bar, column, grouped, stacked, 100% stacked, diverging, waterfall, line, area, scatter, bubble, slope, sparkline, histogram, box, violin, strip, dumbbell, gantt, candlestick, donut, radial, waffle, heatmap, lollipop, bullet and pyramid charts, bar chart races and live data, with magazine-style poster design, interaction, and a tool that renders the chart and reports problems.
3
+ description: Makes charts, graphs, plots, infographics and data visualizations with rhp (@bezda/rhp, reactive html plots), charts built from HTML elements and CSS with SolidJS that run in a plain HTML page, React, Next.js, Vue, Svelte, Solid or Astro. Use it whenever the user asks to create, restyle, fix or extend a chart and rhp is the library to use, which is when they name rhp or @bezda/rhp, the project depends on @bezda/rhp, or the project has no chart library yet. Covers bar, column, grouped, stacked, 100% stacked, diverging, waterfall, line, area, scatter, bubble, slope, sparkline, histogram, box, violin, strip, dumbbell, gantt, candlestick, donut, radial, waffle, heatmap, lollipop, bullet and pyramid charts, dials, log-scale lines, silhouettes to scale, unit stacks, bar chart races and live data, with magazine-style poster design, interaction, and a tool that renders the chart and reports problems.
4
4
  license: MIT
5
5
  metadata:
6
6
  author: Bezda (rhp)
@@ -12,6 +12,8 @@ metadata:
12
12
 
13
13
  rhp draws charts out of HTML elements placed by CSS.
14
14
  You describe one **slat** (the element drawn for one row of data) from a few **blocks** (Bar, Dot, Tick, Label, Cell, Place, Area, Line), and rhp draws a slat for every row of the data and keeps each one in its place.
15
+ For bulk scatter points, the experimental [`ManyDots` collection](references/api.md#manydots-experimental-point-collection) can draw plain HTML points directly inside a Chart with `cross`, without a Plot or a slat per point.
16
+ Use a repository build that exports it; a version-2 package or CDN URL alone does not establish prototype availability.
15
17
 
16
18
  **You do not know rhp yet.**
17
19
  rhp 2 was written in September 2026, after your training data.
@@ -32,11 +34,19 @@ Asked (must all be in the result):
32
34
  - "<the user's words>" -> what that means for the chart
33
35
  - ...
34
36
  Chosen (the user left these open):
35
- - form, environment and file, data, look, interaction, motion (CSS by default; JS for data that changes rapidly), size
37
+ - form, environment and file, data, interaction, motion (CSS by default; JS for data that changes rapidly), size
38
+ Design (from the subject; none of it the starting recipe's):
39
+ - Idea: the subject's own shape, object or way of drawing this data, and how it helps the reading
40
+ - Marks: how each mark is drawn to carry the idea
41
+ - Composition: where the headline, the chart and the other parts sit, on a desktop and on a phone
42
+ - Palette: the paper, the ink, the accent, and where each comes from
43
+ - Type: the faces, and why these
36
44
  ```
37
45
 
38
46
  - Put under **Asked** every explicit request, quoting the user's words: the data and its numbers, the chart type, titles and text, colors, fonts, size, orientation, sort order, labels, legend, interactions, animation, framework, file name, and anything they said not to do.
39
47
  - Everything else goes under **Chosen**, filled with the defaults below.
48
+ - The five **Design** lines are written at step 4, after you have looked at the subject (step 3) and at the starting recipe.
49
+ A line the user or the app has fixed says so ("Palette: theirs, exactly", "Type: the app's"); a "plain", "minimal" or "no poster" request has no Design lines.
40
50
  - A request is never dropped, swapped for something "better" or quietly reduced. If rhp cannot do something exactly, say so in the brief and do the closest thing.
41
51
  - A part of the data the user did not name (what is left when their parts do not reach 100%) is a quiet remainder: unlabeled, in the quiet color, with no key entry, and explained in the note, never a new series with its own color.
42
52
  Write it under **Asked**, beside the request it changes, as a deviation.
@@ -66,7 +76,13 @@ Look at the project before you write anything: read package.json (dependencies a
66
76
  Name a new file after its subject (`languages.html`, `RainfallChart.jsx`), and put it where the project keeps similar files.
67
77
  Install with the project's package manager (`npm install @bezda/rhp`) unless the chart is a plain HTML page, which loads rhp from jsDelivr.
68
78
 
69
- ### 3. Design the slat, then pick the recipes to start from
79
+ ### 3. Find the subject's own form, design the slat, pick recipes for technique
80
+
81
+ Look at the subject before you think of a chart type.
82
+ A reader already knows what it looks like: its shape (a week is seven columns, a river runs from source to mouth, a depth hangs below the surface), its objects (a ticket, a scoreboard, a gauge, a ledger), and the way people who work with it already draw this data (a tide table, a score sheet, a growth chart at the doctor's).
83
+ When one of those can hold the data honestly, it is the chart's form: [forms.md](references/forms.md) lists the shapes rhp draws and the recipe that has the technique for each.
84
+ When none can, the form comes from the data alone, and the subject shows in the marks and the composition (step 4).
85
+ The form the user named always wins.
70
86
 
71
87
  An rhp chart has no chart type to configure.
72
88
  It is one slat, the element drawn for each row of data, composed from blocks, so you design that slat:
@@ -76,11 +92,24 @@ It is one slat, the element drawn for each row of data, composed from blocks, so
76
92
  3. **What sits on top of the slats?** A second Plot over the first (`overlap`) draws layers: a scatter, a line, a crosshair, a marker for today.
77
93
  4. **What does each slat hold of its own?** A Plot inside a slat draws small multiples: a heatmap's cells, a grouped bar's bars, a strip of dots.
78
94
 
79
- [forms.md](references/forms.md) maps data and stories to forms and to these compositions; use the form the user named, if they named one.
95
+ [forms.md](references/forms.md) maps subjects, data and stories to forms and to these compositions; use the form the user named, if they named one.
80
96
  Then open the closest recipe in `recipes/` and read it whole.
81
- Every recipe is a complete, tested, designed chart page, a worked example of one composition: start from it, keep its structure and its techniques, and replace its data, text, look and details.
97
+
98
+ **A recipe gives technique, not a design.**
99
+ Every recipe is a complete, tested chart page, and its comment at the top says which part is which.
100
+ Take its technique: how the Plots and blocks are composed, the scale, the helpers, the interaction handlers, the accessibility, the phone rules.
101
+ Leave its design: its look, the order and place of its parts, its readout band and controls row, the way its marks are drawn, and its wording were made for another subject.
102
+ A chart that keeps them is that recipe with new data, and a reader can tell.
82
103
  When the story needs a slat no recipe has (a name, a bar, a sparkline and a target in one slat), combine the techniques of several recipes.
83
104
 
105
+ For a bulk scatter collection, consider `ManyDots` instead of creating one slat per point.
106
+ Keep `Dot` when you need rich point content or Plot's automatic keyboard navigation, selection, or readout behavior.
107
+ Both remain explicit choices at any count, with no automatic switch at 500 points.
108
+ Read the [ManyDots API and accessible example](references/api.md#manydots-experimental-point-collection) before using it.
109
+ It preserves per-point color, size, shape, classes, and appearance styles and supports native hover, click, touch, and pointer events through its host.
110
+ Point metadata uses the original `data-rhp-index`; drag behavior and keyboard-accessible controls are supplied by the application.
111
+ Its numeric sizes mean pixels, updates are immediate and scan the rows, and initial SSR hydration requires aligned row order when DOM identity matters.
112
+
84
113
  | Recipe | Use it for |
85
114
  |---|---|
86
115
  | `bar` | ranked categories, long names (horizontal bars) |
@@ -113,27 +142,46 @@ When the story needs a slat no recipe has (a name, a bar, a sparkline and a targ
113
142
  | `lollipop` | ranked values, lighter than bars |
114
143
  | `bullet` | actual against target, with bands |
115
144
  | `pyramid` | two sides of one population by age |
145
+ | `silhouettes` | things known by their shape (buildings, ships, bottles), each in its own outline to scale |
146
+ | `dial` | how a day, a week or another cycle splits into parts, around a dial |
147
+ | `log-line` | a quantity that grows by multiples over years, on a log scale, its falls measured |
148
+ | `unit-stack` | a total made of parts over periods, counted out in units, one part to follow |
116
149
 
117
150
  The rhp MCP server's `rhp_recipe` tool (`rhp:rhp_recipe`) returns the same recipes when the files are not on disk.
118
151
 
119
152
  ### 4. Design it
120
153
 
121
154
  Unless the user gave a style, the chart is an original **editorial poster**: a magazine, advertisement or feature-article infographic designed for this subject.
122
- Read [design.md](references/design.md) and follow its procedure; in short:
155
+ Original means that a reader could not guess which recipe it started from, and that with its words covered it still says what it is about.
156
+ Read [design.md](references/design.md) and make its five decisions, in the brief's Design lines, each from this subject and each different from the starting recipe's:
157
+
158
+ - **Idea.** The subject's own shape, object or way of drawing this data (step 3), in one sentence, and how it helps the reading.
159
+ It is the one thing a reader remembers, and it shows in the form or the marks, not only in the frame.
160
+ - **Marks.** Drawn to carry the idea: each item in its own outline, a bar counted out in units, a container and what fills it, the stroke of the subject's own instrument.
161
+ Flat and honest: the value is still read from a length, a position or a count.
162
+ - **Composition.** Where the headline, the chart, the key number and the note sit, taken from the idea and not from the recipe.
163
+ The idea must still be there at 390px.
164
+ - **Palette.** The subject's own ground as the paper (a colored paper built from the subject's own color when it has one; design.md section 4 says how), its ink, one accent the subject owns for the story, and context marks quiet without being a wall of gray.
165
+ - **Type.** A display face with the subject's voice and a plain text face, neither of them the recipe's.
166
+
167
+ What the user or the project has fixed stays fixed, and the rest is still designed:
168
+
169
+ | The request | Fixed | Yours, from the subject |
170
+ |---|---|---|
171
+ | no style given | nothing | all five |
172
+ | the user's colors, fonts, brand or "dark" | those, exactly | the idea, the marks, the composition, and what they left open |
173
+ | a chart inside an app with a look of its own | the app's fonts, colors, card and spacing | the idea, the marks, and the composition inside the card |
174
+ | "plain", "minimal", "no poster", "just the chart" | the plain chart of design.md section 9 | nothing: plain wins |
175
+
176
+ Unless the request is plain (design.md section 9 says what a plain or a minimal chart keeps):
123
177
 
124
- - The poster carries a **kicker** (the topic, a few words), a **headline that states the finding** ("Bananas outsell everything else", not "Fruit sales"), a **dek** (context and how to read the chart), the chart, and a **note** (source and year, or "Illustrative data"; no source line when the user gave the data and no source).
125
- - Derive the look from the subject: a material or setting, a palette, a type pairing, how the marks are drawn, and one ornament.
126
- Make it your own, not a copy of the recipe's look or of another chart you made: never reuse a recipe's headline formula, kicker wording or readout band as they are.
178
+ - The chart carries a **headline that states the finding** ("Bananas outsell everything else", not "Fruit sales"), and a poster also a **kicker** (the topic, a few words), a **dek** (context and how to read the chart) and a **note** (source and year, or "Illustrative data"; no source line when the user gave the data and no source).
179
+ Write them for this subject: never a recipe's headline formula or kicker wording.
127
180
  - Color has a purpose: one accent for the story, quieter colors for the rest. Direct labels beat legends.
128
181
  - The series or item the headline names leads at rest: lit, labeled and in the accent before the reader does anything.
129
182
  When the user's color for it is weak on the background, keep the color and outline or label its marks.
130
183
  - It must read on a phone (390px wide) as well as on a desktop, with the chart in the phone's first screen.
131
-
132
- When the user gives a style, a brand, colors or fonts, use exactly those: the palette and the type are theirs.
133
- The layout, the frame, the marks and one idea from the subject are still yours to design, with design.md's legibility rules.
134
-
135
- Inside an existing app with a look of its own (its fonts, CSS variables, Tailwind theme or component library), the chart takes the app's fonts and colors instead of a poster's.
136
- Keep the editorial habits that help any chart: a title that states the finding, direct labels, a source line.
184
+ - Legibility is never traded for the idea: design.md's contrast floors, text sizes and honesty rules hold for every design.
137
185
 
138
186
  ### 5. Give it an interaction
139
187
 
@@ -145,9 +193,10 @@ The hint for an interaction sits beside its control, in words that fit every dev
145
193
  When the user asks for a "static" chart, an image-like chart or no interaction, add none.
146
194
  A chart with fixed data and no interaction gets `static=${true}` on its Chart: rhp draws it once and keeps no signals.
147
195
 
148
- ### 6. Build it from the recipe
196
+ ### 6. Build it, with the recipe's technique
149
197
 
150
198
  - Keep the recipe's code format and section order: data, then slat types, then the chart component, then mounting.
199
+ With `ManyDots`, omit slat types and put the collection directly inside the Chart.
151
200
  - Use the user's data exactly, every value, in their units.
152
201
  With no data given: when the subject is factual, use real figures and name the source and year in the note; with network access, take them from the primary source (the agency or publisher that makes them) rather than from memory, and from memory use only figures you know well.
153
202
  Otherwise use plausible figures and the note says "Illustrative data".
@@ -158,6 +207,13 @@ A chart with fixed data and no interaction gets `static=${true}` on its Chart: r
158
207
  The handoff says exactly how to put their own numbers in, or offers to read a file they point to.
159
208
  - Compute the scale from the data: `nice(Math.min(0, ...values), Math.max(0, ...values))` (rule 2 below).
160
209
  - Compute the ranks, the leader and every number in the headline and the dek from a sorted copy of the data, never from its input order: a Plot's `order` sorts only what it draws.
210
+ - Text computed from the data (the headline, the dek, a readout, an `aria-label`) must stay true for any data it may get:
211
+ - A change from a zero or missing base prints "new" or "up from none", never ∞%, NaN% or "as many as"; a change in a rate is in percentage points ("down 0.8 points").
212
+ - A change computed from the first and last values names them ("22% above 12 weeks ago"), and "held steady" needs every value inside a narrow band, not two equal ends.
213
+ - A count over the chart's window ("led for eight years", "a record") names the window ("every year from 2015 to 2022") unless you checked it holds before the window too.
214
+ - Numbers in the text are rounded as the labels are, and labels use the fewest decimals that keep neighbors apart, with no trailing zeros.
215
+ - Real data from yearly releases takes each year from the latest release that covers it, never a mix of first estimates and revisions.
216
+ When the user asks for brands and the source counts companies, the note names every merge shown ("Lenovo includes Motorola").
161
217
  - Look up anything you are unsure of in [api.md](references/api.md) rather than guessing a prop.
162
218
  - Keep the code short and plain: comments at section heads and where a reader needs one.
163
219
 
@@ -169,12 +225,22 @@ Run the checker on the file after the first full draft and again after every fix
169
225
  - otherwise run `npx -y @bezda/rhp-mcp check <file>` (add `--dark` when the page has a dark mode).
170
226
 
171
227
  A chart component that takes its data from the app cannot be drawn on its own: check a small entry file that draws it with fixed data, as environments.md section 13 shows.
228
+ For the experimental `ManyDots` collection, use a build that exports it and also check its rendered point count, coordinates, per-point appearance, native interactions, and keyboard-accessible alternative directly in the browser.
229
+ A clean standard-chart checker report alone does not establish coverage of its bulk points.
172
230
 
173
231
  It builds the chart, renders it in a browser at 1280px and 390px, tries its interaction, and reports runtime errors, props rhp does not have, data the slat reads but the Plot lacks, values past the scale, overlapping or cut-off text, marks sticking out, sideways scrolling, low contrast and tiny text, with a fix for each.
174
232
  Its screenshots show the page with reduced motion asked for, so a chart that animates on load is pictured at rest; its interactions run with motion on.
175
233
  They go to a folder in the system's temp directory, and the report gives their paths; `--out <dir>` puts them elsewhere.
176
234
  Keep screenshots, and any test files of your own, out of the user's project.
177
235
  Then open the screenshots and look at them as a demanding art director would: hierarchy, spacing, alignment, color, legibility on the phone, and whether the headline is true for the data.
236
+ Unless the request was plain or minimal, put the design to two tests:
237
+
238
+ - **The recipe test.** Run the checker on the recipe you started from as well, and look at its 1280px screenshot beside yours.
239
+ If a reader could tell that yours started from it (the same parts in the same places, the same band above the chart, the same marks in new colors), change the composition or the marks; new colors and fonts do not pass this test.
240
+ - **The subject test.** Look at your screenshots at 1280px and at 390px as if the words were covered.
241
+ What is left must still say what the chart is about.
242
+ If it could hold any data, the idea lives only in the text and the frame: carry it into the form or the marks.
243
+
178
244
  Fix what you see, and check again.
179
245
 
180
246
  Never make a warning go away by removing something the user asked for.
@@ -185,8 +251,9 @@ If the checker cannot run here (no Node, no browser, a chatbot without tools), g
185
251
  - Go through every **Asked** item of the brief against the final code and screenshots, and fix anything missing.
186
252
  - Recompute every number in the headline, the dek and the note from the final data: a claim must be true for the numbers shown.
187
253
  - Try every control once more and compare before and after: nothing else on the page may move (pitfalls.md, "The layout jumps").
188
- - Check once more at four widths (`--widths 1280,1024,768,390`, or `widths` in `rhp_check`): layouts that change between 390px and 1280px (a breakpoint, a turned chart, a panel beside the chart) are where most layout bugs are.
189
- - When the chart will take other data (the user's own, live or generated), check a copy with the hardest data it may get: the longest names, values ten times larger, twice as many rows.
254
+ - Check once more at six widths (`--widths 1280,1024,768,390,360,320`): layouts that change between 320px and 1280px (a breakpoint, a turned chart, a panel beside the chart) are where most layout bugs are; 360px and 320px catch narrower phone layouts.
255
+ If `rhp_check` accepts only four widths per call, use two calls to cover all six.
256
+ - When the chart will take other data (the user's own, live, loaded or generated, or a component that takes its rows as a prop even if the app's data is fixed today), check a copy with the hardest data it may get: the longest real names of the domain, values ten times larger, twice as many rows, one row and none, a first value of 0 and all zeros, the smallest parts (1 to 5%), and dates across a year's end.
190
257
  - Then tell the user, briefly: what you made, where it is and how to open or run it (an HTML file opens with a double-click, and needs a connection for rhp), what you chose for them (form, look, interaction, data source or "illustrative"), and anything you could not do, with the reason.
191
258
  For a short request, those few lines are the whole handoff: no test logs, tool versions or local paths.
192
259
  To name the checker, give the command anyone can run, `npx -y @bezda/rhp-mcp check <file>`.
@@ -324,11 +391,21 @@ The html template (every format except Solid JSX):
324
391
  8. **A handler on a component takes its event:** `onClick=${(e) => pick(e)}`. A function with no parameter on a component (a block or a Poster) runs once while drawing and is never attached. Handlers on plain elements (`div`, `button`) work either way, and the Chart takes none at all (rule 16).
325
392
  9. **`class`, never `className`**: on a block, `className` replaces rhp's own class and the block disappears.
326
393
  10. **A space between two tags disappears**: `<b>${() => d.name}</b> <em>12</em>` renders "Name12". Write the space as `${" "}` or keep text next to it (`<b>Name</b>: <em>`).
327
- 11. **`style` on a Chart or a Plot is an object** (`style=${{ "pointer-events": "none" }}`); a string is dropped. Blocks and plain elements take either.
394
+ 11. **Chart, Plot and slat roots use style objects.**
395
+ On a Chart or a Plot, use `style=${{ "pointer-events": "none" }}`: a string is dropped.
396
+ On a slat's root, keep a changing style an object (`style=${() => ({ "--n": d.n })}`), because a string that changes or a style that becomes `undefined` erases its stack position.
397
+ On published rhp 2.0.2, the same replacement can erase a Block's position and color, so use objects there too; the Block bug is fixed in current source for the upcoming 2.0.3 release.
398
+ Other plain elements accept objects or strings.
328
399
 
329
400
  Structure and layout:
330
401
 
331
- 12. **One root element per slat**, holding its blocks (`<div>`); never return a block itself (only an `overlap` Plot may). Give the Plot the slat type `slat()` returns, not the function you passed to it.
402
+ 12. **One root element per slat.**
403
+ In an ordinary Plot, use a wrapper (`<div>`) holding its blocks.
404
+ In an `overlap` Plot or a Scale, a single block can be the root itself, including its own children, so avoid an extra wrapper.
405
+ Those children use the block's box, not the full slat's band.
406
+ Several blocks still need a common root.
407
+ Keep a wrapper for a full-band hover or pointer target, even with one block.
408
+ Give the Plot the slat type `slat()` returns, not the function you passed to it.
332
409
  13. **Import from one place.** In a Solid app, import rhp from `@bezda/rhp` and Solid from `solid-js`. Everywhere else, import everything from `@bezda/rhp/standalone` (rhp, `html`, `render`, `createSignal`, `createMemo`, `createEffect`, `Show`, `For`, `Index`, `onMount`, `onCleanup`, `batch`, `untrack`, `createStore`, `reconcile`), which carries its own Solid: never add a second `solid-js` beside it. From rhp 2.0.2 it also has `createSelector`, `createComputed`, `on`, `mergeProps`, `splitProps`, `Switch` and `Match` (2.0.1 lacks them); it has no `Dynamic`, `Portal`, `createResource` or `ErrorBoundary`. For the slat the reader is on, a per-row comparison (`on=${(d) => on() === d.index}`) works with every rhp 2.
333
410
  14. **Make room for text.** Names before the plot need `room.start` (px or "auto"), numbers past the bars need `room.end`. A `room` you give replaces the defaults on every side, so set each side you need. "auto" measures only edge Labels that are direct children of the slat's root. Long names need `max-width` with an ellipsis, or wrapping, in the slat CSS.
334
411
  15. **Size the chart.** A horizontal chart without `height` gives each slat 32px unless the slat type sets `thickness`; a vertical chart is 240px tall unless you set `height`. A chart inside a flex row, an inline-block or a `fit-content` box collapses: give it `flex: 1; min-width: 0` or a width.
@@ -336,8 +413,17 @@ Structure and layout:
336
413
 
337
414
  Style and color:
338
415
 
339
- 17. **Style the inside of a chart with the slat's `css` and the Chart's `theme` only.** rhp's CSS is `!important` inside cascade layers, so page CSS cannot change a block's color, background, font, border, radius, shadow, padding or transition. Page CSS styles the poster around the chart and the chart's box. Focus styles for slats (`.slat:focus-visible`) go in the slat's `css` too: a page `:focus-visible` rule never reaches a slat.
340
- 18. **Color with theme keys or CSS colors**: `color="series-2"`, `"positive"`, `"negative"`, `"muted"` or `"#c2410c"`. Set the palette in the Chart's `theme` (`series`, `ink`, `muted`, `grid`, `surface`, `positive`, `negative`, `low`, `high`, `font`). Theme values may read the page's own variables (`ink: "var(--ink)"`), so the poster and the chart share one set of tokens and dark mode is a few lines of CSS (design.md); a block's `color` may not (rhp warns), and page CSS never sets rhp's own `--rhp-*` variables. `series()` counts six colors: with a shorter `series` list, call `series(n)`. A Label does not take its Bar's color: color the slat root (`--rhp-color` in its style) or the Label's own text.
416
+ 17. **Style regular slat blocks with the slat's `css` and the Chart's `theme`.**
417
+ rhp's block CSS is `!important` inside cascade layers, so page CSS cannot change a block's color, background, font, border, radius, shadow, padding or transition.
418
+ Page CSS styles the poster around the chart and the chart's box.
419
+ Focus styles for slats (`.slat:focus-visible`) go in the slat's `css` too: a page `:focus-visible` rule never reaches a slat.
420
+ `ManyDots` instead uses `pointClass` and ordinary application CSS or `pointStyle` for appearance, while the collection guards point geometry.
421
+ 18.
422
+ **Color with theme keys or CSS colors**: `color="series-2"`, `"positive"`, `"negative"`, `"muted"` or `"#c2410c"`.
423
+ Set the palette in the Chart's `theme` (`series`, `ink`, `muted`, `grid`, `surface`, `positive`, `negative`, `low`, `high`, `font`).
424
+ Theme values may read the page's own variables (`ink: "var(--ink)"`), so the poster and the chart share one set of tokens and dark mode is a few lines of CSS (design.md); a block's `color` may not (rhp warns), and page CSS never sets rhp's own `--rhp-*` variables.
425
+ `series()` counts six colors: with a shorter `series` list, call `series(n)`.
426
+ A Label does not take its Bar's color: give the slat's root `--rhp-color` in a style object (`style=${() => ({ "--rhp-color": d.on ? ACCENT : QUIET })}`) or color the Label's own text.
341
427
  19. **A Dot's `size` is a length** (`size="12px"`); a bare number stretches it into an oval.
342
428
  20. **Overlay Plots** (a crosshair, a marker over other Plots) need `style=${{ "pointer-events": "none" }}` so the pointer reaches the Plot under them.
343
429
 
@@ -352,12 +438,13 @@ The full list, with the mistake and the fix for each, is [pitfalls.md](reference
352
438
 
353
439
  Read the ones the chart needs; each is self-contained.
354
440
 
355
- - [forms.md](references/forms.md): which chart for which data and story, and which recipe to start from.
356
- - [design.md](references/design.md): the poster, art direction from the subject, look kits, palettes, type, layout, annotation, dark mode.
441
+ - [forms.md](references/forms.md): the subject's own form, which chart for which data and story, and which recipe has the technique.
442
+ - [design.md](references/design.md): art direction from the subject (idea, marks, composition, palette, type), the poster, color, type, layout, annotation, charts inside an app, a style the user asks for.
443
+ - [looks.md](references/looks.md): eight looks written out in full (tokens, fonts, texture, type), as examples of how a look is written, not a menu.
357
444
  - [interaction.md](references/interaction.md): which interaction for which story, with complete code for each.
358
445
  - [environments.md](references/environments.md): plain HTML, Solid, Astro, React, Next.js, Vue, Svelte and Angular, each verified, and the html template to JSX rules.
359
446
  - [api.md](references/api.md): every component, prop, helper and CSS variable, exactly.
360
447
  - [pitfalls.md](references/pitfalls.md): mistakes and their fixes.
361
- - `recipes/*.html`: the tested starting points listed in step 3.
448
+ - `recipes/*.html`: the tested pages listed in step 3, for technique.
362
449
 
363
450
  Live gallery and docs: https://rhp.vercel.app
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>A summer day on the grid</title>
7
7
  <meta name="description" content="Area chart of one quantity over a day: a filled Line on a second axis, with its peak and low annotated, and a switch between two days that morphs the area while the other day stays as a thin reference line. Use it for one quantity over time, filled, or to compare two periods.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: a filled Line on a second axis, its peak and low annotated, an hour Scale, and a switch that morphs between two days while the other stays as a thin line.
11
+ Leave: the design. This poster was made for its own subject (a July day's power demand over Manhattan's street grid, on pale concrete).
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=Big+Shoulders+Display:wght@700..900&family=Work+Sans:wght@400..700&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>The most spoken languages</title>
7
7
  <meta name="description" content="Horizontal bar chart, ranked, the top bar highlighted, with a readout on hover, focus and tap. Use it to rank categories by one value, especially with long names.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: ranked Bars with names in an "auto" room, the leader lit, and a readout on hover, focus and tap that stays at the top of a phone's screen.
11
+ Leave: the design. This poster was made for its own subject (the world's languages as speech: each bar a speech bubble, each name in its own script, the share under a big quotation mark).
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=Instrument+Sans:wght@400..700&family=Instrument+Serif:ital@0;1&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Bikes beat cars, and keep to time</title>
7
7
  <meta name="description" content="Box plot of commute times by mode of transport made with summary(): whiskers, the middle half as a box, the median and outliers, with a readout of the picked group. Use it to compare the spread of several groups at a glance.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: summary() drawn as whiskers, a box and a median in one slat, outliers in an overlay Plot, and a readout of the picked group.
11
+ Leave: the design. This poster was made for its own subject (commute times on the enamel map board of a tiled station wall, the kicker as the station's sign).
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=Overpass:wght@400..900&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Two ways to hold 20 million</title>
7
7
  <meta name="description" content="Bubble chart: two measures per item on a second axis (log scales) and a third as the bubble's area, with guide lines of equal size, labels for the story's items and a readout for the bubble under the pointer, a tap or the arrow keys. Use it for items with two measures and a size.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: Dots on two log axes sized by area, guide lines of equal size, labels placed per row, and a readout of the bubble under the pointer.
11
+ Leave: the design. This poster was made for its own subject (cities seen from orbit at night, with a map's scale bar under the kicker).
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=Sora:wght@400..700&family=DM+Mono:wght@400;500&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Sales against target by region</title>
7
7
  <meta name="description" content="Bullet chart: actual against target for each item, over gray bands for poor, fair and good, with a switch that sorts by the actual value or by the share of target. Use it for performance against goals, budgets or quotas.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: band Bars, the actual Bar and a target Tick in one slat, and a switch that sorts by value or by share of target.
11
+ Leave: the design. This poster was made for its own subject (a page of an annual report, black on white with one orange, the year on a tab under the top rule).
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=Archivo:wdth,wght@62..125,400..900&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>A 15% drop erased the month's gains</title>
7
7
  <meta name="description" content="Candlestick chart of one month of daily share prices: a wick from each day's low to its high and a body from its open to its close (hollow when it rose, filled when it fell), with a crosshair and a price readout for the picked day. Use it for open, high, low and close prices over days.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: a thin Bar for the wick and a wide one for the body (hollow when the day rose), a crosshair in an overlay Plot, and a price readout on one line.
11
+ Leave: the design. This poster was made for its own subject (a stock's month printed on an engraved share certificate, ivory paper inside a double rule).
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=Cormorant+Garamond:ital,wght@0,500..700;1,500..600&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>A monsoon year</title>
7
7
  <meta name="description" content="Column chart of a value over twelve months, a season marked under its columns, with a scrub that reads out the running total by hover, focus and tap. Use it for values over periods (months, years) or a few categories.">
8
+ <!--
9
+ A recipe gives technique, not a design.
10
+ Take: vertical Bars over twelve periods, a bracket under a group of columns, and a scrub that reads out a running total.
11
+ Leave: the design. This poster was made for its own subject (a monsoon year as twelve rain gauges in the night rain, each month a glass tube with its water inside).
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=Archivo:wdth,wght@62..125,400..800&display=swap">
@@ -22,14 +28,16 @@
22
28
  .poster .dek { max-width: 54ch; font-size: 16px; line-height: 1.5; color: #b5bdc4; }
23
29
  .poster .note { display: block; margin-top: 24px; font-size: 12px; line-height: 1.5; color: #8d969e; }
24
30
  /* The readout: how much rain has fallen by the month in focus */
25
- .readout { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; min-height: 2lh; margin: 0 0 22px; padding-top: 16px; border-top: 1px solid #2c3135; font-size: 15px; line-height: 1.4; color: #b5bdc4; }
31
+ .readout { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; margin: 0 0 22px; padding-top: 16px; border-top: 1px solid #2c3135; font-size: 15px; line-height: 1.4; color: #b5bdc4; }
26
32
  .readout b { font-size: 30px; font-weight: 700; font-stretch: 110%; color: #fff; font-variant-numeric: tabular-nums; }
33
+ /* Where the figure and its longest text no longer fit on one line, the text goes under the figure with two lines
34
+ kept, so the readout has one height whatever it says and the chart never moves */
35
+ @media (max-width: 720px) { .readout { flex-direction: column; align-items: start; } .readout span { min-height: 2lh; } }
27
36
  @media (max-width: 560px) {
28
37
  body { padding: 16px 16px 32px; }
29
38
  .poster { padding: 26px 16px 18px; }
30
39
  .poster .headline { font-size: 32px; }
31
40
  .poster .dek { font-size: 15px; }
32
- .readout { min-height: 4lh; } /* the longest readout, so the chart never moves */
33
41
  .readout b { font-size: 24px; }
34
42
  }
35
43
  </style>
@@ -46,6 +54,8 @@ const SEASON = { name: "Monsoon", from: 5, to: 8 }; // June to September; null f
46
54
 
47
55
  const mm = new Intl.NumberFormat("en-US");
48
56
  const percent = new Intl.NumberFormat("en-US", { style: "percent" });
57
+ // A share in whole percents that never rounds some rain to "0%" or a year not yet over to "100%"
58
+ const share = (x) => (x > 0 && x < 0.005 ? "under 1%" : x >= 0.995 && x < 1 ? "over 99%" : percent.format(x));
49
59
  const year = RAIN.reduce((a, b) => a + b, 0);
50
60
  const scale = nice(0, Math.max(...RAIN), 4); // 0 to 1,000 mm, a line every 250
51
61
 
@@ -65,14 +75,17 @@ const Month = slat({
65
75
  .season { position: absolute; top: calc(100% + 30px); left: 24%; width: calc(var(--months) * 100% - 48%); height: 7px; border: 1.5px solid #6cc3ff; border-top: 0; }
66
76
  .season b { position: absolute; top: 13px; left: 50%; translate: -50% 0; font-size: 11px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: #6cc3ff; white-space: nowrap; }
67
77
  .month:focus-visible { outline: 2px solid #6cc3ff; outline-offset: -2px; border-radius: 4px; }
78
+ .name .short { display: none; }
68
79
  @container (max-width: 520px) { .name { font-size: 11px; font-stretch: 70%; letter-spacing: 0; } .mm { font-size: 11px; font-stretch: 80%; } }
80
+ /* Columns under about 21px: the month's first letter, the whole name staying in the readout and the slat's label */
81
+ @container (max-width: 293px) { .name .long { display: none; } .name .short { display: inline; } }
69
82
  `,
70
83
  }, (d) => html`
71
84
  <div class=${() => "month " + d.step} data-row=${() => d.index} aria-label=${() => `${d.month}: ${mm.format(d.mm)} mm`}>
72
85
  <${Bar} to=${scale.max} class="tube" />
73
86
  <${Bar} to=${() => d.mm} class="water" />
74
87
  <${Label} at=${() => d.mm} class="mm">${() => mm.format(d.mm)}<//>
75
- <${Label} edge="start" class="name">${() => d.month.slice(0, 3)}<//>
88
+ <${Label} edge="start" class="name"><span class="long">${() => d.month.slice(0, 3)}</span><span class="short">${() => d.month[0]}</span><//>
76
89
  <${Show} when=${() => d.index === SEASON?.from}>
77
90
  <span class="season" style=${() => `--months: ${SEASON.to - SEASON.from + 1}`}><b>${() => SEASON.name}</b></span>
78
91
  <//>
@@ -103,12 +116,12 @@ function MonsoonYear() {
103
116
  kicker="A monsoon city · rain by month"
104
117
  title="Four months bring nearly all of the year's rain"
105
118
  dek="Each tube holds a month's rain in millimetres. From June to September the monsoon fills them; for the rest of the year they stay almost dry."
106
- note="Illustrative data, shaped like a monsoon city's year. Point at a month, tap it, or Tab into the chart and use the arrow keys to add up the year."
119
+ note="Illustrative data, shaped like a monsoon city's year."
107
120
  onPointerMove=${(e) => e.pointerType !== "touch" && pick(e)} onClick=${pick} onFocusIn=${pick}
108
121
  onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setAt(null)}>
109
122
  <p class="readout" aria-live="polite">
110
123
  <b>${() => mm.format(sofar()) + " mm"}</b>
111
- <span>${() => (at() == null ? "in the whole year" : `by the end of ${MONTHS[end()]}, ${percent.format(sofar() / year)} of the year's rain`)}</span>
124
+ <span>${() => (at() == null ? "in the whole year. Tap or point at a month to add it up." : `by the end of ${MONTHS[end()]}, ${share(sofar() / year)} of the year's rain`)}</span>
112
125
  </p>
113
126
  <${Chart} orientation="vertical" height=${300} scale=${[scale.min, scale.max]} ticks=${scale.ticks} format=${(v) => mm.format(v)}
114
127
  label="Rainfall by month, in millimetres"