@bezda/rhp-mcp 0.0.0-stage → 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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -2
  3. package/bin/rhp-mcp.js +129 -0
  4. package/package.json +62 -4
  5. package/skill/SKILL.md +450 -0
  6. package/skill/recipes/area.html +156 -0
  7. package/skill/recipes/bar.html +145 -0
  8. package/skill/recipes/box-plot.html +164 -0
  9. package/skill/recipes/bubble.html +188 -0
  10. package/skill/recipes/bullet.html +138 -0
  11. package/skill/recipes/candlestick.html +179 -0
  12. package/skill/recipes/column.html +138 -0
  13. package/skill/recipes/dial.html +349 -0
  14. package/skill/recipes/diverging-bars.html +150 -0
  15. package/skill/recipes/donut.html +231 -0
  16. package/skill/recipes/dumbbell.html +137 -0
  17. package/skill/recipes/gantt.html +196 -0
  18. package/skill/recipes/grouped-bars.html +142 -0
  19. package/skill/recipes/heatmap.html +183 -0
  20. package/skill/recipes/histogram.html +199 -0
  21. package/skill/recipes/line.html +170 -0
  22. package/skill/recipes/live.html +255 -0
  23. package/skill/recipes/log-line.html +493 -0
  24. package/skill/recipes/lollipop.html +152 -0
  25. package/skill/recipes/multi-line.html +228 -0
  26. package/skill/recipes/pyramid.html +162 -0
  27. package/skill/recipes/race.html +278 -0
  28. package/skill/recipes/radial-bars.html +157 -0
  29. package/skill/recipes/scatter.html +199 -0
  30. package/skill/recipes/silhouettes.html +352 -0
  31. package/skill/recipes/slope.html +199 -0
  32. package/skill/recipes/sparklines.html +175 -0
  33. package/skill/recipes/stacked-100.html +158 -0
  34. package/skill/recipes/stacked-bars.html +150 -0
  35. package/skill/recipes/strip.html +221 -0
  36. package/skill/recipes/unit-stack.html +276 -0
  37. package/skill/recipes/violin.html +169 -0
  38. package/skill/recipes/waffle.html +165 -0
  39. package/skill/recipes/waterfall.html +127 -0
  40. package/skill/references/api.md +1684 -0
  41. package/skill/references/design.md +740 -0
  42. package/skill/references/environments.md +1577 -0
  43. package/skill/references/forms.md +197 -0
  44. package/skill/references/interaction.md +794 -0
  45. package/skill/references/looks.md +267 -0
  46. package/skill/references/pitfalls.md +202 -0
  47. package/src/check/README.md +176 -0
  48. package/src/check/browser.js +92 -0
  49. package/src/check/build.js +439 -0
  50. package/src/check/colors.js +324 -0
  51. package/src/check/findings.js +247 -0
  52. package/src/check/index.js +508 -0
  53. package/src/check/inpage.js +1332 -0
  54. package/src/check/interact.js +266 -0
  55. package/src/check/png.js +98 -0
  56. package/src/check/report.js +106 -0
  57. package/src/check/source.js +387 -0
  58. package/src/check/sourcemap.js +75 -0
  59. package/src/guard/note.js +30 -0
  60. package/src/guard/render.js +17 -0
  61. package/src/guard/rhp.js +612 -0
  62. package/src/guard/rules.js +125 -0
  63. package/src/server.js +266 -0
  64. package/src/skill.js +140 -0
package/skill/SKILL.md ADDED
@@ -0,0 +1,450 @@
1
+ ---
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, 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
+ license: MIT
5
+ metadata:
6
+ author: Bezda (rhp)
7
+ library: "@bezda/rhp@2"
8
+ homepage: https://rhp.vercel.app
9
+ ---
10
+
11
+ # Making charts with rhp
12
+
13
+ rhp draws charts out of HTML elements placed by CSS.
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.
17
+
18
+ **You do not know rhp yet.**
19
+ rhp 2 was written in September 2026, after your training data.
20
+ An older, unrelated rhp 1 ("react-html-plots", React components with Legend-State, packages `@bezda/rhp@0.0.x`, `@bezda/rhp-core`, `@bezda/rhp-base`) may be in your memory: never use any of it.
21
+ Write rhp code only from this skill's recipes and references, never from memory, and check what you wrote with the checker before you hand it over.
22
+
23
+ ## The workflow
24
+
25
+ Follow these eight steps in order, for every chart, however short or detailed the request.
26
+
27
+ ### 1. Write the brief
28
+
29
+ Before any code, write a short brief in your reply:
30
+
31
+ ```
32
+ Brief
33
+ Asked (must all be in the result):
34
+ - "<the user's words>" -> what that means for the chart
35
+ - ...
36
+ Chosen (the user left these open):
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
44
+ ```
45
+
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.
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.
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.
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.
52
+ Write it under **Asked**, beside the request it changes, as a deviation.
53
+ - An explicit request beats every default here: "no title", "no poster", "static", "no animation", "minimal", "dark", "use Chart.js colors" all win.
54
+ - Ask a question only when the request cannot be built at all without the answer. Otherwise choose, build, and say what you chose.
55
+
56
+ ### 2. Find where the chart goes
57
+
58
+ Look at the project before you write anything: read package.json (dependencies and devDependencies) and go down this table; the first row that matches wins, because a Next.js app also has `react`, a SvelteKit app `svelte`, a Nuxt app `vue`.
59
+
60
+ | What you find | Write the chart as | [environments.md](references/environments.md) |
61
+ |---|---|---|
62
+ | no project, an empty folder, a chatbot conversation, or "an HTML file" | one `.html` file that loads rhp from jsDelivr (it needs a connection) | section 3 |
63
+ | `astro` | a Solid island with `@astrojs/solid-js`, otherwise a custom element | section 12 |
64
+ | `next` | a React component the server never renders (loaded with `next/dynamic` and `ssr: false`) | section 8 |
65
+ | `nuxt` | a Vue component | section 9 |
66
+ | `@sveltejs/kit` | a Svelte component | section 10 |
67
+ | `@solidjs/start` | a JSX component importing `@bezda/rhp` | section 4 |
68
+ | `@angular/core` | a standalone component | section 11 |
69
+ | `solid-js` (with `vite-plugin-solid`) | a JSX component importing `@bezda/rhp` | section 4 |
70
+ | `react` | a React component that mounts an html-template chart from `@bezda/rhp/standalone` | section 7 |
71
+ | `vue` | a Vue component | section 9 |
72
+ | `svelte` | a Svelte component | section 10 |
73
+
74
+ `solid-js` in node_modules alone does not make a Solid app: npm installs it with `@bezda/rhp` in every app.
75
+
76
+ Name a new file after its subject (`languages.html`, `RainfallChart.jsx`), and put it where the project keeps similar files.
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.
78
+
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.
86
+
87
+ An rhp chart has no chart type to configure.
88
+ It is one slat, the element drawn for each row of data, composed from blocks, so you design that slat:
89
+
90
+ 1. **What is one row?** A country, a month, a task, a runner, an hour of a weekday. That is what the Plot gets, one slat per row.
91
+ 2. **What must the reader see in that row?** Its name, its value as a bar or a dot, a range from one value to another, a target, a trend, its share, an icon. Each of those is a block (`Label`, `Bar`, `Dot`, `Tick`, `Line`, `Area`, `Cell`, `Place`) or an element of your own inside one.
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.
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.
94
+
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.
96
+ Then open the closest recipe in `recipes/` and read it whole.
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.
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.
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
+
113
+ | Recipe | Use it for |
114
+ |---|---|
115
+ | `bar` | ranked categories, long names (horizontal bars) |
116
+ | `column` | values over a few periods or categories (vertical bars) |
117
+ | `grouped-bars` | two to four values side by side per category |
118
+ | `stacked-bars` | parts adding up to a total per category |
119
+ | `stacked-100` | shares of 100% per category |
120
+ | `diverging-bars` | values either side of zero, survey agree / disagree |
121
+ | `waterfall` | how a start value becomes an end value, step by step |
122
+ | `line` | one series over time |
123
+ | `multi-line` | several series over time |
124
+ | `area` | one quantity over time, filled |
125
+ | `scatter` | two measures per item |
126
+ | `bubble` | two measures plus a size per item |
127
+ | `slope` | change between two dates per item |
128
+ | `sparklines` | many small series side by side, a table of trends |
129
+ | `race` | a ranking that changes step by step, played over time (JS motion) |
130
+ | `live` | values that arrive from a feed without pause (JS motion) |
131
+ | `histogram` | the distribution of one measure |
132
+ | `box-plot` | distributions compared by group, summarized |
133
+ | `violin` | distributions compared by group, their full outline |
134
+ | `strip` | every value of a few groups as dots |
135
+ | `dumbbell` | two values per item and the gap between them |
136
+ | `gantt` | tasks over time, a schedule |
137
+ | `candlestick` | open, high, low and close prices |
138
+ | `donut` | a few parts of one whole |
139
+ | `radial-bars` | progress toward goals, in a ring |
140
+ | `waffle` | counts out of 100, people or units |
141
+ | `heatmap` | a value over two categories (weekday by hour) |
142
+ | `lollipop` | ranked values, lighter than bars |
143
+ | `bullet` | actual against target, with bands |
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 |
149
+
150
+ The rhp MCP server's `rhp_recipe` tool (`rhp:rhp_recipe`) returns the same recipes when the files are not on disk.
151
+
152
+ ### 4. Design it
153
+
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.
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):
177
+
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.
180
+ - Color has a purpose: one accent for the story, quieter colors for the rest. Direct labels beat legends.
181
+ - The series or item the headline names leads at rest: lit, labeled and in the accent before the reader does anything.
182
+ When the user's color for it is weak on the background, keep the color and outline or label its marks.
183
+ - It must read on a phone (390px wide) as well as on a desktop, with the chart in the phone's first screen.
184
+ - Legibility is never traded for the idea: design.md's contrast floors, text sizes and honesty rules hold for every design.
185
+
186
+ ### 5. Give it an interaction
187
+
188
+ Unless the user said otherwise, the chart gets one interaction that serves its story, from [interaction.md](references/interaction.md): a readout on hover, focus and tap for comparing items, a sort or a switch between years for rankings, a crosshair for time, a toggle for series.
189
+ It works with a mouse, a finger and the keyboard, and nothing is shown only on hover.
190
+ A readout follows a mouse or a pen as it moves; a tap, a click or the keyboard picks a slat, and a scroll never does (pick on `click`, never on `pointerdown`); between slats the pick stays; leaving the poster gives the pick back to the slat that has focus, or clears it.
191
+ The example below does all of it.
192
+ The hint for an interaction sits beside its control, in words that fit every device ("Tap or point at a fruit"), never in the source note.
193
+ When the user asks for a "static" chart, an image-like chart or no interaction, add none.
194
+ A chart with fixed data and no interaction gets `static=${true}` on its Chart: rhp draws it once and keeps no signals.
195
+
196
+ ### 6. Build it, with the recipe's technique
197
+
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.
200
+ - Use the user's data exactly, every value, in their units.
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.
202
+ Otherwise use plausible figures and the note says "Illustrative data".
203
+ Never present invented numbers as fact, and never put a real name (a country, a city, a company, a person) on them: give invented items plain invented names and say "invented" or "fictional" in the note.
204
+ - When an item was renamed recently, show the name most readers know beside the new one ("X (formerly Twitter)").
205
+ - A request about the user's own data with none given ("chart my spending") gets illustrative numbers in one obvious data block at the top of the script, with the note's text in the same block, so the user changes both in one place.
206
+ The headline and the dek compute their numbers from that data, so they stay true for the user's own.
207
+ The handoff says exactly how to put their own numbers in, or offers to read a file they point to.
208
+ - Compute the scale from the data: `nice(Math.min(0, ...values), Math.max(0, ...values))` (rule 2 below).
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").
217
+ - Look up anything you are unsure of in [api.md](references/api.md) rather than guessing a prop.
218
+ - Keep the code short and plain: comments at section heads and where a reader needs one.
219
+
220
+ ### 7. Check it, look at it, fix it
221
+
222
+ Run the checker on the file after the first full draft and again after every fix, until it reports no errors and no warnings:
223
+
224
+ - with the rhp MCP server: call its `rhp_check` tool (`rhp:rhp_check`) with the file's absolute path;
225
+ - otherwise run `npx -y @bezda/rhp-mcp check <file>` (add `--dark` when the page has a dark mode).
226
+
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.
230
+
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.
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.
233
+ They go to a folder in the system's temp directory, and the report gives their paths; `--out <dir>` puts them elsewhere.
234
+ Keep screenshots, and any test files of your own, out of the user's project.
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
+
244
+ Fix what you see, and check again.
245
+
246
+ Never make a warning go away by removing something the user asked for.
247
+ If the checker cannot run here (no Node, no browser, a chatbot without tools), go through the rules in "Rules that prevent most bugs" below one by one against your code instead, and say in the handoff that the chart was not rendered.
248
+
249
+ ### 8. Audit and hand over
250
+
251
+ - Go through every **Asked** item of the brief against the final code and screenshots, and fix anything missing.
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.
253
+ - Try every control once more and compare before and after: nothing else on the page may move (pitfalls.md, "The layout jumps").
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.
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.
258
+ For a short request, those few lines are the whole handoff: no test logs, tool versions or local paths.
259
+ To name the checker, give the command anyone can run, `npx -y @bezda/rhp-mcp check <file>`.
260
+
261
+ ## How an rhp chart is built
262
+
263
+ ```
264
+ Poster optional panel: kicker, headline, dek, the chart, note
265
+ └─ Chart the frame: scale, orientation, axis, theme, height or aspect
266
+ └─ Plot one stack of slats; every prop that is not a setting is a column of data
267
+ └─ slat (d) => <div> one element per row of data, made of blocks placed by d's values
268
+ ├─ Label edge="start" the row's name, before the chart
269
+ ├─ Bar to={d.value} a bar from 0 (or from) to the value
270
+ └─ Label at={d.value} the value's number, just past the bar
271
+ ```
272
+
273
+ - **Chart** sets the value axis: `scale={[min, max]}`, `orientation` ("horizontal", the default, or "vertical"), `ticks`, `format`, `theme`, and `height` or `aspect`. A Chart can hold several Plots drawn on the same scale.
274
+ - **Plot** gets the data. Each list prop is a column of data (`name={[...]}`, `sold={[...]}`), each single value is shared by every row, and each function of `d` is computed per row. A list of objects goes in `rows`. Its own settings are `order`, `reorder`, `key`, `rows`, `overlap`, `orientation`, `slats`, `animate`, `thick`, `static`, `keyboard`, `class`, `style`, `ref`.
275
+ - **The slat** is a function of `d`, one row: it reads `d.name`, `d.sold`, `d.index` (the row's place in the data) and `d.position` (its place on screen), and returns one root element holding blocks.
276
+ - **`slat(settings, fn)`** turns that function into a slat type with its own scoped CSS and layout: `css`, `thickness` (px per slat), `room` (px outside the plot for names and numbers, or "auto"), `inset`.
277
+ - **Blocks** sit at values on the scale: `Bar from to`, `Dot at`, `Tick at`, `Label at` or `edge`, `Cell value`, `Place at`, `Area points`, `Line points`. Each also takes `class`, `style`, handlers, `data-*` and children, and all but Label and Place take `color`.
278
+ - **A second axis** (`cross={[min, max]}` on the Chart) turns an `overlap` Plot into a scatter plot or a line chart: `Dot at={x} cross={y}`, `Line points={[[x, y], ...]}`.
279
+ - **Helpers** prepare numbers: `nice`, `extent`, `every`, `stackUp`, `shares`, `running`, `summary`, `bins`, `density`, `sortBy`, `series`, `cycle`.
280
+ - **Everything is HTML.** A slat and its blocks are ordinary elements: put text, images, inline SVG icons and other elements inside them, and style them with any CSS in the slat type's `css` (gradients, textures, type, custom outlines with `shape()`). That freedom is where an original design comes from.
281
+
282
+ A complete chart in the plain HTML format (the format of every recipe), with a poster and a hover, focus and tap readout:
283
+
284
+ ```html
285
+ <!doctype html>
286
+ <html lang="en">
287
+ <head>
288
+ <meta charset="utf-8">
289
+ <meta name="viewport" content="width=device-width, initial-scale=1">
290
+ <title>Bananas outsell everything</title>
291
+ <link rel="preconnect" href="https://fonts.googleapis.com">
292
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
293
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,700&family=Inter:wght@400;600&display=swap">
294
+ <script type="importmap">{ "imports": { "@bezda/rhp/standalone": "https://cdn.jsdelivr.net/npm/@bezda/rhp@2/dist/standalone.js" } }</script>
295
+ <style>
296
+ body { margin: 0; padding: 40px 16px; background: #e8e4dc; }
297
+ .poster { max-width: 720px; margin: 0 auto; padding: 28px 28px 18px; background: #fbf8f1; color: #1f1a14; font-family: Inter, system-ui, sans-serif; }
298
+ .poster figcaption { display: grid; gap: 6px; margin-bottom: 18px; }
299
+ .poster .kicker { font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: #8a5a12; }
300
+ .poster .headline { font: 700 clamp(27px, 4.7vw, 34px)/1.05 Fraunces, Georgia, serif; text-wrap: balance; }
301
+ .poster .dek { font-size: 15px; line-height: 1.45; color: #4d453b; max-width: 52ch; }
302
+ .poster .note { display: block; margin-top: 14px; font-size: 11.5px; color: #6f665a; }
303
+ /* The dek is the readout: on a phone it keeps two lines, its longest text at 320px, so a pick never moves the chart */
304
+ @media (max-width: 480px) { .poster { padding: 20px 16px 14px; } .poster .dek { min-height: 2lh; } }
305
+ </style>
306
+ </head>
307
+ <body>
308
+ <div id="chart"></div>
309
+ <script type="module">
310
+ import { Chart, Plot, Bar, Label, Poster, slat, nice, html, render, createSignal } from "@bezda/rhp/standalone";
311
+
312
+ // Data, and what depends on it
313
+ const fruit = ["Bananas", "Apples", "Cherries", "Pears", "Plums"];
314
+ const sold = [18, 12, 7, 5, 3];
315
+ const scale = nice(Math.min(0, ...sold), Math.max(0, ...sold)); // 0 to 20, ticks every 5
316
+ const lead = sold.indexOf(Math.max(...sold)); // the fruit the headline names: lit until the reader picks another
317
+
318
+ // Slat type: one slat per row; its CSS reaches only these slats
319
+ const Fruit = slat({
320
+ thickness: 40,
321
+ room: { start: "auto", end: 44 },
322
+ css: `
323
+ .name { font-size: 14px; font-weight: 600; }
324
+ .bar { --rhp-end-radius: 6px; }
325
+ .value { font-size: 13px; font-variant-numeric: tabular-nums; }
326
+ .slat.on .value { font-weight: 700; }
327
+ `,
328
+ }, (d) => html`
329
+ <div class=${() => (d.on ? "slat on" : "slat")} data-row=${() => d.index}>
330
+ <${Label} edge="start" class="name">${() => d.fruit}<//>
331
+ <${Bar} to=${() => d.sold} color=${() => (d.on ? "#c2410c" : "#e0a65b")} />
332
+ <${Label} at=${() => d.sold} class="value">${() => d.sold}<//>
333
+ </div>`);
334
+
335
+ // Chart component: the app holds the state (which slat the reader picked)
336
+ function FruitChart() {
337
+ const [on, setOn] = createSignal(null);
338
+ // The row of the slat an element is in, or null (between slats, on the axis or the text)
339
+ const rowOf = (el) => {
340
+ const root = el.closest("[data-row]");
341
+ return root ? +root.dataset.row : null;
342
+ };
343
+ // A mouse or a pen picks as it moves, a tap or a click picks, the keyboard picks; on no slat the pick stays
344
+ const pick = (e) => {
345
+ const row = rowOf(e.target);
346
+ if (row != null) setOn(row);
347
+ };
348
+ // Leaving the poster: back to the slat that has focus (a keyboard's or a click's pick), or none
349
+ const leave = (e) => {
350
+ if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
351
+ setOn(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
352
+ };
353
+ return html`
354
+ <${Poster}
355
+ kicker="Market stall, Saturday"
356
+ title="Bananas outsell everything else"
357
+ dek=${() => (on() == null ? "Crates sold by midday. Tap or point at a fruit." : `${fruit[on()]}: ${sold[on()]} crates by midday.`)}
358
+ note="Illustrative data."
359
+ onPointerMove=${(e) => e.pointerType !== "touch" && pick(e)} onClick=${pick} onFocusIn=${pick}
360
+ onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setOn(null)}>
361
+ <${Chart} scale=${[scale.min, scale.max]} ticks=${scale.ticks} label="Crates of fruit sold by midday"
362
+ theme=${{ font: "Inter, system-ui, sans-serif", ink: "#1f1a14", muted: "#6f665a", grid: "#e6dfd2" }}>
363
+ <${Plot} keyboard=${true} fruit=${fruit} sold=${sold} on=${(d) => (on() ?? lead) === d.index}>${Fruit}<//>
364
+ <//>
365
+ <//>`;
366
+ }
367
+
368
+ render(() => html`<${FruitChart} />`, document.getElementById("chart"));
369
+ </script>
370
+ </body>
371
+ </html>
372
+ ```
373
+
374
+ In a Solid app the same chart is JSX: `<Bar to={d.sold} />` instead of `<${Bar} to=${() => d.sold} />`, imports from `@bezda/rhp` and `solid-js`; [environments.md](references/environments.md) has the exact rules.
375
+
376
+ ## Rules that prevent most bugs
377
+
378
+ Data:
379
+
380
+ 1. **Data names must match.** A slat reads `d.sold` only if the Plot has a prop `sold`. A list of objects goes in `rows={[...]}`, never `data={...}`: `data` would just be a column named "data". Any prop a Plot does not know (`id`, `title`, `onClick`) is a column too, and an array is always a column, item i for row i: to give every slat the same list, pass a function of the row (`shown=${(d) => shown()}`).
381
+ 2. **The scale must hold the data.** `scale={[min, max]}` with min below max covers every value: a Bar past it is cut off, and Dots, Ticks, Places and the points of an Area or a Line past it land outside or squeeze the outline.
382
+ Build it from the data, `const s = nice(Math.min(0, ...values), Math.max(0, ...values))`, which keeps 0 and holds negative values too, and give the Chart `scale=${[s.min, s.max]} ticks=${s.ticks}`.
383
+ 3. **Rows that come and go need a `key`** on the Plot (`key="city"`), or slats swap data instead of leaving. `d.index` can change then, so read it in a function: `data-row=${() => d.index}`.
384
+ 4. **Format numbers** with `Intl.NumberFormat` (thousands, units, percentages, currency) and use `font-variant-numeric: tabular-nums` where numbers line up. In the JS version (`animate`), values in between are fractional: round what you print.
385
+
386
+ The html template (every format except Solid JSX):
387
+
388
+ 5. **A component is `<${Bar} ...>`**, closed by `<//>` or `/>`.
389
+ 6. **A value that can change is a function:** `${() => d.sold}`. `${d.sold}` is read once and never updates. Never destructure `d`. Lists and numbers that never change can be passed as they are (`fruit=${fruit}`).
390
+ 7. **A boolean on a component needs a value:** `keyboard=${true}`, `overlap=${true}`, `static=${true}`, `fill=${true}`, `smooth=${true}`. A bare `keyboard` passes an empty string, which is false, and silently does nothing.
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).
392
+ 9. **`class`, never `className`**: on a block, `className` replaces rhp's own class and the block disappears.
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>`).
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.
399
+
400
+ Structure and layout:
401
+
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.
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.
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.
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.
412
+ 16. **Put handlers and `data-*` on the Poster or a wrapper element, not on the Chart**: the Chart keeps only `id`, `class`, `role`, `style`, `ref`, `label` and `aria-*`.
413
+
414
+ Style and color:
415
+
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.
427
+ 19. **A Dot's `size` is a length** (`size="12px"`); a bare number stretches it into an oval.
428
+ 20. **Overlay Plots** (a crosshair, a marker over other Plots) need `style=${{ "pointer-events": "none" }}` so the pointer reaches the Plot under them.
429
+
430
+ Motion:
431
+
432
+ 21. **Data that changes rapidly or continuously moves by the JS version.** When the data will change faster than a transition lasts, or without pause (a live feed, a play button that moves through the data, a slider or a drag that drives the data, a bar chart race), give the Plot `animate` for the data groups that change: `animate=${["sold"]}`, or `animate=${true}` for all of its numbers, or `animate=${{ groups: ["sold"], duration: 1200, ease: "linear" }}` to time them (here for a feed with a reading every 800 ms). The blocks drawn from those values then move on rhp's JS clock, which carries each move through the next new value instead of restarting a CSS transition on every change, and labels stay exactly on their bars. When the scale follows the data too, put `animate` on the Chart, so the axis moves with the marks. A Plot inside a slat needs its own `animate`. Give the changing numbers as lists (`gdp=${() => gdpAt(t())}`): the JS version never moves a group given as a function of the row (`(d) => ...`). To play back data you have (a race, a year slider with Play), move a playhead `t` on every frame, give the Plot the values interpolated between the steps around it, and keep the default 150 ms: the marks glide without stopping at each step, and Pause leaves them between steps (interaction.md section 10). For a live feed, whose next value is not known yet, ease linear into each new reading over a little more than one interval (`ease: "linear"`, 1.5 intervals), or the marks stop between readings. For a slider or a drag keep the default 150 ms. Round the numbers you print while they move. The `race` and `live` recipes show all of it. Data that changes now and then (a click that switches years) keeps the default CSS version.
433
+ 22. **Interaction effects stay CSS, in both versions**: a hover highlight or a growing focus ring is a CSS transition on an element inside a block. Never put `transition` on a block itself (`.bar { transition: ... }`): rhp moves blocks with its own transitions and yours would replace them, so the block jumps.
434
+
435
+ The full list, with the mistake and the fix for each, is [pitfalls.md](references/pitfalls.md).
436
+
437
+ ## References
438
+
439
+ Read the ones the chart needs; each is self-contained.
440
+
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.
444
+ - [interaction.md](references/interaction.md): which interaction for which story, with complete code for each.
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.
446
+ - [api.md](references/api.md): every component, prop, helper and CSS variable, exactly.
447
+ - [pitfalls.md](references/pitfalls.md): mistakes and their fixes.
448
+ - `recipes/*.html`: the tested pages listed in step 3, for technique.
449
+
450
+ Live gallery and docs: https://rhp.vercel.app
@@ -0,0 +1,156 @@
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>A summer day on the grid</title>
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
+ -->
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=Big+Shoulders+Display:wght@700..900&family=Work+Sans:wght@400..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
+ * { box-sizing: border-box; }
20
+ body { margin: 0; padding: 48px 16px; background: #d6d4cf; font-family: "Work Sans", "Helvetica Neue", Arial, sans-serif; }
21
+ /* The poster: pale concrete, with Manhattan's street grid (turned 29 degrees) fading out under the headline */
22
+ .poster { max-width: 920px; margin: 0 auto; padding: 44px 48px 26px; color: #1c1d1f; border-radius: 3px;
23
+ background: linear-gradient(transparent, #f3f2ee 300px),
24
+ repeating-linear-gradient(29deg, transparent 0 52px, rgb(28 29 31 / .07) 52px 53px),
25
+ repeating-linear-gradient(119deg, transparent 0 15px, rgb(28 29 31 / .05) 15px 16px),
26
+ #f3f2ee;
27
+ box-shadow: 0 1px 2px rgb(28 29 31 / .15), 0 26px 50px -26px rgb(28 29 31 / .5); }
28
+ .poster figcaption { display: grid; gap: 12px; margin-bottom: 22px; }
29
+ .poster .kicker { font-size: 12px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: #a34210; }
30
+ .poster .headline { max-width: 14em; font: 900 62px/.9 "Big Shoulders Display", "Arial Narrow", sans-serif; text-transform: uppercase; letter-spacing: .005em; text-wrap: balance; }
31
+ .poster .dek { max-width: 58ch; font-size: 16px; line-height: 1.5; color: #48494c; }
32
+ .poster .note { display: block; margin-top: 16px; font-size: 12px; line-height: 1.5; color: #6e6b66; }
33
+ /* The switch: one button per day, the one shown filled in its own color */
34
+ .days { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
35
+ .days button { display: inline-flex; align-items: center; gap: 8px; padding: 7px 14px 7px 10px; border: 1.5px solid #c9c6bf; border-radius: 99px;
36
+ font: 600 13px/1.2 "Work Sans", sans-serif; color: #48494c; background: #f9f8f5; cursor: pointer; }
37
+ .days button i { width: 12px; height: 12px; border-radius: 50%; background: var(--c); }
38
+ .days button[aria-pressed="true"] { border-color: var(--c); color: #1c1d1f; background: color-mix(in srgb, var(--c) 12%, #f9f8f5); }
39
+ .days button:focus-visible { outline: 2px solid #1c1d1f; outline-offset: 2px; }
40
+ @media (max-width: 600px) {
41
+ body { padding: 16px 16px 32px; }
42
+ .poster { padding: 26px 18px 18px; }
43
+ .poster .headline { font-size: 40px; }
44
+ .poster .dek { font-size: 15px; }
45
+ }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <div id="chart"></div>
50
+ <script type="module">
51
+ import { Chart, Plot, Scale, Line, Dot, Tick, Label, Place, Poster, slat, html, render, createSignal } from "@bezda/rhp/standalone";
52
+
53
+ // Data: New York City's electricity demand in megawatts, the mean of each hour from midnight, NYISO real-time load
54
+ // (zone N.Y.C.). Replace with your own: one list of numbers per period, the same length.
55
+ const DAYS = [
56
+ { name: "16 Jul", label: "16 July", color: "#e8590c", mw: [
57
+ 8174, 7822, 7548, 7354, 7268, 7328, 7646, 8243, 8736, 9089, 9348, 9566, 9774, 9941, 9971, 10033, 10118, 10242, 10121, 9688, 9397, 9419, 8965, 8373,
58
+ ] },
59
+ { name: "17 Jan", label: "17 January", color: "#1c7ed6", mw: [
60
+ 5443, 5265, 5142, 5095, 5141, 5343, 5797, 6312, 6636, 6845, 6922, 6951, 6971, 6966, 6928, 6948, 7019, 7196, 7151, 7032, 6856, 6636, 6277, 5910,
61
+ ] },
62
+ ];
63
+
64
+ const TOP = 12000; // the top of the scale, in megawatts
65
+ const mw = new Intl.NumberFormat("en-US");
66
+ const clock = (h) => (h % 24 === 0 ? "Midnight" : h === 12 ? "Noon" : `${h % 12} ${h < 12 ? "am" : "pm"}`);
67
+ const points = (list) => list.map((v, h) => [h + 0.5, v]); // each hour's mean, drawn at the middle of its hour
68
+ const extreme = (list, pick) => list.indexOf(pick(...list));
69
+
70
+ // Slat types
71
+ // The scale under the plot: a short mark and the time every six hours, the two ends kept inside the chart
72
+ const Hour = slat({
73
+ room: { after: 28 },
74
+ css: `
75
+ .mark { top: 100%; height: 6px; translate: -50% 0; --rhp-tick-width: 1px; background: #a9a69f; }
76
+ .time { top: calc(100% + 10px); bottom: auto; translate: -50% 0; --rhp-label-gap: 0px; font-size: 11px; color: var(--rhp-muted); }
77
+ .first .time { translate: 0 0; }
78
+ .last .time { translate: -100% 0; }
79
+ @container (max-width: 420px) { .minor .time { display: none; } }
80
+ `,
81
+ }, (t) => html`
82
+ <div class=${() => (t.first ? "first" : t.last ? "last" : t.at % 12 ? "minor" : "")}>
83
+ <${Tick} at=${() => t.at} class="mark" />
84
+ <${Label} at=${() => t.at} class="time">${() => clock(t.at)}<//>
85
+ </div>`);
86
+
87
+ // Every day as a thin line with its name at the end: the day not shown stays as a reference
88
+ const Outline = slat({
89
+ room: { end: 58 },
90
+ css: `
91
+ .outline { stroke: #8f8c86; stroke-width: 1.5px; }
92
+ .name { --rhp-label-gap: 8px; font-size: 12px; font-weight: 600; color: var(--rhp-muted); }
93
+ .on .name { font-weight: 700; color: var(--rhp-ink); }
94
+ `,
95
+ }, (d) => html`
96
+ <div class=${() => (d.on ? "on" : "")}>
97
+ <${Line} points=${() => points(d.mw)} class="outline" />
98
+ <${Label} at=${() => d.mw.length - 0.5} cross=${() => d.mw.at(-1)} class="name">${() => d.name}<//>
99
+ </div>`);
100
+
101
+ // The day shown: its filled area, with the peak's tag just above its dot and the low's just below.
102
+ // Center each tag on its point, clamping it to the plot's edges on a phone.
103
+ const Demand = slat({
104
+ css: `
105
+ .area { --rhp-length-time: .6s; --rhp-length-ease: cubic-bezier(.3, .7, .2, 1); stroke-width: 2.5px; }
106
+ .point, .pin { --rhp-length-time: .6s; --rhp-length-ease: cubic-bezier(.3, .7, .2, 1); }
107
+ .point { box-shadow: 0 0 0 2px #f3f2ee; }
108
+ .tag { position: absolute; left: 0; translate: clamp(calc(var(--rhp-p) * -100cqw), -50%, calc((1 - var(--rhp-p)) * 100cqw - 100%)) 0;
109
+ padding: 3px 7px; border-radius: 4px; white-space: nowrap;
110
+ font-size: 12px; background: #fff; box-shadow: 0 0 0 1px rgb(28 29 31 / .14); }
111
+ .tag b { font-weight: 700; }
112
+ .peak { bottom: 10px; }
113
+ .low { top: 10px; }
114
+ `,
115
+ }, (d) => html`
116
+ <div>
117
+ <${Line} points=${() => points(d.mw)} fill=${true} color=${() => d.color} class="area" />
118
+ <${Dot} at=${() => d.peak + 0.5} cross=${() => d.mw[d.peak]} color=${() => d.color} size="9px" class="point" />
119
+ <${Dot} at=${() => d.low + 0.5} cross=${() => d.mw[d.low]} color=${() => d.color} size="9px" class="point" />
120
+ <${Place} at=${() => d.peak + 0.5} cross=${() => d.mw[d.peak]} class="pin">
121
+ <span class="tag peak"><b>Peak ${() => mw.format(d.mw[d.peak])} MW</b>, ${() => clock(d.peak)}</span>
122
+ <//>
123
+ <${Place} at=${() => d.low + 0.5} cross=${() => d.mw[d.low]} class="pin">
124
+ <span class="tag low"><b>Low ${() => mw.format(d.mw[d.low])} MW</b>, ${() => clock(d.low)}</span>
125
+ <//>
126
+ </div>`);
127
+
128
+ // Chart component: the buttons pick the day that is filled and annotated
129
+ function PowerChart() {
130
+ const [shown, setShown] = createSignal(0);
131
+ const day = () => DAYS[shown()];
132
+ return html`
133
+ <${Poster}
134
+ kicker="New York City power demand"
135
+ title="A July day used 40% more power than a January one"
136
+ dek="Demand on the city's grid, hour by hour, on Tuesday 16 July and Wednesday 17 January 2024. Both days peaked at 5 pm, and in July the 4 am low (7,268 megawatts) was higher than January's peak (7,196). Pick a day with the buttons; the other day stays as a gray line."
137
+ note="Source: NYISO real-time actual load, zone N.Y.C., 2024, hourly means of five-minute readings.">
138
+ <div class="days" role="group" aria-label="Day shown">
139
+ ${DAYS.map((d, k) => html`
140
+ <button type="button" aria-pressed=${() => shown() === k} onClick=${() => setShown(k)} style=${{ "--c": d.color }}><i></i>${d.label}</button>`)}
141
+ </div>
142
+ <${Chart} scale=${[0, 24]} cross=${[0, TOP]} crossTicks=${[0, 3000, 6000, 9000, TOP]} crossFormat=${(v) => mw.format(v)} height=${300}
143
+ label=${() => `New York City electricity demand by hour on ${day().label} 2024, in megawatts`}
144
+ theme=${{ font: '"Work Sans", "Helvetica Neue", Arial, sans-serif', ink: "#1c1d1f", muted: "#6e6b66", grid: "#dddad3" }}>
145
+ <${Scale} ticks=${[0, 6, 12, 18, 24]}>${Hour}<//>
146
+ <${Plot} overlap=${true} rows=${DAYS} on=${(d) => d.index === shown()}>${Outline}<//>
147
+ <${Plot} overlap=${true} slats=${1} mw=${() => [day().mw]} color=${() => day().color}
148
+ peak=${() => extreme(day().mw, Math.max)} low=${() => extreme(day().mw, Math.min)}>${Demand}<//>
149
+ <//>
150
+ <//>`;
151
+ }
152
+
153
+ render(() => html`<${PowerChart} />`, document.getElementById("chart"));
154
+ </script>
155
+ </body>
156
+ </html>