@bezda/rhp-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/bin/rhp-mcp.js +5 -1
- package/package.json +3 -3
- package/skill/SKILL.md +116 -26
- package/skill/recipes/area.html +6 -0
- package/skill/recipes/bar.html +9 -1
- package/skill/recipes/box-plot.html +9 -1
- package/skill/recipes/bubble.html +6 -0
- package/skill/recipes/bullet.html +6 -0
- package/skill/recipes/candlestick.html +9 -1
- package/skill/recipes/column.html +18 -5
- package/skill/recipes/dial.html +349 -0
- package/skill/recipes/diverging-bars.html +6 -0
- package/skill/recipes/donut.html +9 -1
- package/skill/recipes/dumbbell.html +6 -0
- package/skill/recipes/gantt.html +17 -5
- package/skill/recipes/grouped-bars.html +6 -0
- package/skill/recipes/heatmap.html +9 -1
- package/skill/recipes/histogram.html +9 -1
- package/skill/recipes/line.html +7 -1
- package/skill/recipes/live.html +6 -0
- package/skill/recipes/log-line.html +493 -0
- package/skill/recipes/lollipop.html +6 -0
- package/skill/recipes/multi-line.html +10 -5
- package/skill/recipes/pyramid.html +9 -1
- package/skill/recipes/race.html +6 -0
- package/skill/recipes/radial-bars.html +6 -0
- package/skill/recipes/scatter.html +23 -4
- package/skill/recipes/silhouettes.html +354 -0
- package/skill/recipes/slope.html +23 -4
- package/skill/recipes/sparklines.html +7 -1
- package/skill/recipes/stacked-100.html +6 -0
- package/skill/recipes/stacked-bars.html +6 -0
- package/skill/recipes/strip.html +9 -1
- package/skill/recipes/unit-stack.html +276 -0
- package/skill/recipes/violin.html +6 -0
- package/skill/recipes/waffle.html +6 -0
- package/skill/recipes/waterfall.html +6 -0
- package/skill/references/api.md +150 -22
- package/skill/references/design.md +303 -326
- package/skill/references/environments.md +11 -4
- package/skill/references/forms.md +41 -4
- package/skill/references/interaction.md +7 -2
- package/skill/references/looks.md +267 -0
- package/skill/references/performance.md +143 -0
- package/skill/references/pitfalls.md +52 -7
- package/src/check/README.md +33 -3
- package/src/check/browser.js +11 -5
- package/src/check/findings.js +15 -1
- package/src/check/index.js +182 -31
- package/src/check/inpage.js +82 -18
- package/src/check/interact.js +1 -19
- package/src/check/network.js +168 -0
- package/src/check/png.js +18 -0
- package/src/check/report.js +4 -1
- package/src/check/selection.js +123 -0
- package/src/guard/rhp.js +4 -0
- package/src/guard/rules.js +1 -1
- 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
|
|
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.
|
|
@@ -172,13 +173,27 @@ npx -y @bezda/rhp-mcp install-browser
|
|
|
172
173
|
It downloads Chromium's headless shell into Playwright's usual folder and leaves the other browsers there alone.
|
|
173
174
|
Other options go to Playwright's installer: on Linux, `install-browser --with-deps` also installs the system libraries the browser needs.
|
|
174
175
|
|
|
175
|
-
##
|
|
176
|
+
## Local checking
|
|
176
177
|
|
|
177
|
-
The checker renders your chart in a browser on your machine
|
|
178
|
+
The checker renders your chart in a browser on your machine and does not upload its source or screenshots.
|
|
178
179
|
It answers the page's requests for `@bezda/rhp` from the copy of rhp it ships with.
|
|
179
180
|
The only other requests are the ones your chart's page makes itself, such as its Google Fonts, and each gets 5 seconds before the check goes on without it.
|
|
180
181
|
There is no telemetry.
|
|
181
182
|
|
|
183
|
+
The checker treats the chart's page as code it did not write, and gives it only what a chart needs to render:
|
|
184
|
+
|
|
185
|
+
- It permits public HTTP GET, HEAD and OPTIONS requests for assets and data, without forwarding request bodies; POST and other methods are blocked.
|
|
186
|
+
WebSockets are blocked because they can send arbitrary messages.
|
|
187
|
+
This is not a guarantee against data sent in a public URL or its headers.
|
|
188
|
+
- It reaches public addresses only: never this machine (`localhost`), your network or a cloud's metadata service, including through a redirect or a DNS answer.
|
|
189
|
+
Remote responses have a 5-second overall deadline across redirects and a 64 MB size limit, including decompressed content.
|
|
190
|
+
A rejecting local proxy blocks browser connections that bypass the asset router, including worker sockets.
|
|
191
|
+
- It reads the files next to the chart and in folders under it, but not hidden ones (`.env`, `.git`) and nothing outside the chart's folder, including through symlinks.
|
|
192
|
+
These restrictions apply to standalone HTML pages and bundled charts.
|
|
193
|
+
- The browser runs with Chromium's sandbox on (without it only where it cannot start, as in some Linux containers), with no service workers and no downloads.
|
|
194
|
+
|
|
195
|
+
A request the checker refuses is reported in the check, and the check goes on without it.
|
|
196
|
+
|
|
182
197
|
## For maintainers
|
|
183
198
|
|
|
184
199
|
```
|
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.
|
|
3
|
+
"version": "0.1.2",
|
|
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/network.js && node test/settle-regression.js && node test/isolation.js && node test/selection-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.
|
|
54
|
+
"@bezda/rhp": "^2.0.4",
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
|
|
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
|
|
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
|
|
189
|
-
|
|
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>`.
|
|
@@ -283,13 +350,15 @@ function FruitChart() {
|
|
|
283
350
|
if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
|
|
284
351
|
setOn(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
|
|
285
352
|
};
|
|
353
|
+
// Off the chart (on the headline, the readout or the note), pointing is as if the pointer had left the poster
|
|
354
|
+
const move = (e) => e.pointerType !== "touch" && (e.target.closest(".rhp-chart") ? pick(e) : leave(e));
|
|
286
355
|
return html`
|
|
287
356
|
<${Poster}
|
|
288
357
|
kicker="Market stall, Saturday"
|
|
289
358
|
title="Bananas outsell everything else"
|
|
290
359
|
dek=${() => (on() == null ? "Crates sold by midday. Tap or point at a fruit." : `${fruit[on()]}: ${sold[on()]} crates by midday.`)}
|
|
291
360
|
note="Illustrative data."
|
|
292
|
-
onPointerMove=${
|
|
361
|
+
onPointerMove=${move} onClick=${pick} onFocusIn=${pick}
|
|
293
362
|
onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setOn(null)}>
|
|
294
363
|
<${Chart} scale=${[scale.min, scale.max]} ticks=${scale.ticks} label="Crates of fruit sold by midday"
|
|
295
364
|
theme=${{ font: "Inter, system-ui, sans-serif", ink: "#1f1a14", muted: "#6f665a", grid: "#e6dfd2" }}>
|
|
@@ -324,11 +393,21 @@ The html template (every format except Solid JSX):
|
|
|
324
393
|
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
394
|
9. **`class`, never `className`**: on a block, `className` replaces rhp's own class and the block disappears.
|
|
326
395
|
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.
|
|
396
|
+
11. **Chart, Plot and slat roots use style objects.**
|
|
397
|
+
On a Chart or a Plot, use `style=${{ "pointer-events": "none" }}`: a string is dropped.
|
|
398
|
+
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.
|
|
399
|
+
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.
|
|
400
|
+
Other plain elements accept objects or strings.
|
|
328
401
|
|
|
329
402
|
Structure and layout:
|
|
330
403
|
|
|
331
|
-
12. **One root element per slat
|
|
404
|
+
12. **One root element per slat.**
|
|
405
|
+
In an ordinary Plot, use a wrapper (`<div>`) holding its blocks.
|
|
406
|
+
In an `overlap` Plot or a Scale, a single block can be the root itself, including its own children, so avoid an extra wrapper.
|
|
407
|
+
Those children use the block's box, not the full slat's band.
|
|
408
|
+
Several blocks still need a common root.
|
|
409
|
+
Keep a wrapper for a full-band hover or pointer target, even with one block.
|
|
410
|
+
Give the Plot the slat type `slat()` returns, not the function you passed to it.
|
|
332
411
|
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
412
|
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
413
|
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 +415,17 @@ Structure and layout:
|
|
|
336
415
|
|
|
337
416
|
Style and color:
|
|
338
417
|
|
|
339
|
-
17. **Style
|
|
340
|
-
|
|
418
|
+
17. **Style regular slat blocks with the slat's `css` and the Chart's `theme`.**
|
|
419
|
+
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.
|
|
420
|
+
Page CSS styles the poster around the chart and the chart's box.
|
|
421
|
+
Focus styles for slats (`.slat:focus-visible`) go in the slat's `css` too: a page `:focus-visible` rule never reaches a slat.
|
|
422
|
+
`ManyDots` instead uses `pointClass` and ordinary application CSS or `pointStyle` for appearance, while the collection guards point geometry.
|
|
423
|
+
18.
|
|
424
|
+
**Color with theme keys or CSS colors**: `color="series-2"`, `"positive"`, `"negative"`, `"muted"` or `"#c2410c"`.
|
|
425
|
+
Set the palette in the Chart's `theme` (`series`, `ink`, `muted`, `grid`, `surface`, `positive`, `negative`, `low`, `high`, `font`).
|
|
426
|
+
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.
|
|
427
|
+
`series()` counts six colors: with a shorter `series` list, call `series(n)`.
|
|
428
|
+
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
429
|
19. **A Dot's `size` is a length** (`size="12px"`); a bare number stretches it into an oval.
|
|
342
430
|
20. **Overlay Plots** (a crosshair, a marker over other Plots) need `style=${{ "pointer-events": "none" }}` so the pointer reaches the Plot under them.
|
|
343
431
|
|
|
@@ -352,12 +440,14 @@ The full list, with the mistake and the fix for each, is [pitfalls.md](reference
|
|
|
352
440
|
|
|
353
441
|
Read the ones the chart needs; each is self-contained.
|
|
354
442
|
|
|
355
|
-
- [forms.md](references/forms.md): which chart for which data and story, and which recipe
|
|
356
|
-
- [design.md](references/design.md):
|
|
443
|
+
- [forms.md](references/forms.md): the subject's own form, which chart for which data and story, and which recipe has the technique.
|
|
444
|
+
- [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.
|
|
445
|
+
- [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
446
|
- [interaction.md](references/interaction.md): which interaction for which story, with complete code for each.
|
|
447
|
+
- [performance.md](references/performance.md): measured optimizations for an expensive chart, preserving its slats, styling, state, and interactions.
|
|
358
448
|
- [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
449
|
- [api.md](references/api.md): every component, prop, helper and CSS variable, exactly.
|
|
360
450
|
- [pitfalls.md](references/pitfalls.md): mistakes and their fixes.
|
|
361
|
-
- `recipes/*.html`: the tested
|
|
451
|
+
- `recipes/*.html`: the tested pages listed in step 3, for technique.
|
|
362
452
|
|
|
363
453
|
Live gallery and docs: https://rhp.vercel.app
|
package/skill/recipes/area.html
CHANGED
|
@@ -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">
|
package/skill/recipes/bar.html
CHANGED
|
@@ -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">
|
|
@@ -112,13 +118,15 @@ function SpokenLanguages() {
|
|
|
112
118
|
if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
|
|
113
119
|
setPicked(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
|
|
114
120
|
};
|
|
121
|
+
// Off the chart (on the headline, the readout or the note), pointing is as if the pointer had left the poster
|
|
122
|
+
const move = (e) => e.pointerType !== "touch" && (e.target.closest(".rhp-chart") ? pick(e) : leave(e));
|
|
115
123
|
return html`
|
|
116
124
|
<${Poster}
|
|
117
125
|
kicker="The world's languages"
|
|
118
126
|
title=${html`<em>English</em> is the world's most spoken language`}
|
|
119
127
|
dek="Total speakers in millions, counting everyone who speaks it as a first or a second language. Mandarin has more native speakers; English has more learners. Tap or point at a language."
|
|
120
128
|
note="Source: Ethnologue, 2023 (speakers); United Nations, 2023 (world population, 8.05 billion)."
|
|
121
|
-
onPointerMove=${
|
|
129
|
+
onPointerMove=${move} onClick=${pick} onFocusIn=${pick}
|
|
122
130
|
onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setPicked(null)}>
|
|
123
131
|
<p class="readout" aria-live="polite">
|
|
124
132
|
<b>${() => percent.format(LANGUAGES[lit()].millions / WORLD)}</b>
|
|
@@ -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">
|
|
@@ -126,13 +132,15 @@ function CommuteTimes() {
|
|
|
126
132
|
if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
|
|
127
133
|
setPicked(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
|
|
128
134
|
};
|
|
135
|
+
// Off the chart (on the headline, the readout or the note), pointing is as if the pointer had left the poster
|
|
136
|
+
const move = (e) => e.pointerType !== "touch" && (e.target.closest(".rhp-chart") ? pick(e) : leave(e));
|
|
129
137
|
return html`
|
|
130
138
|
<${Poster}
|
|
131
139
|
kicker="Commute times, door to door"
|
|
132
140
|
title="Bikes beat cars, and keep to time"
|
|
133
141
|
dek=${`One-way commutes timed over a month, ${MODES.reduce((n, m) => n + m.trips, 0)} trips in all. A cyclist's trips stay within a few minutes of each other; a driver's can take twice as long on a bad day. Tap or point at a mode for its numbers.`}
|
|
134
142
|
note="Illustrative data."
|
|
135
|
-
onPointerMove=${
|
|
143
|
+
onPointerMove=${move} onClick=${pick} onFocusIn=${pick}
|
|
136
144
|
onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setPicked(null)}>
|
|
137
145
|
<ul class="key">
|
|
138
146
|
<li><i class="box"></i>Middle half of trips</li>
|
|
@@ -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">
|
|
@@ -143,13 +149,15 @@ function SharePrice() {
|
|
|
143
149
|
if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
|
|
144
150
|
setPicked(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
|
|
145
151
|
};
|
|
152
|
+
// Off the chart (on the headline, the readout or the note), pointing is as if the pointer had left the poster
|
|
153
|
+
const move = (e) => e.pointerType !== "touch" && (e.target.closest(".rhp-chart") ? pick(e) : leave(e));
|
|
146
154
|
return html`
|
|
147
155
|
<${Poster}
|
|
148
156
|
kicker="Share price · March 2026"
|
|
149
157
|
title="A 15% drop on earnings day erased the month's gains"
|
|
150
158
|
dek="Each candle is a trading day. Its body runs from the opening price to the closing price, hollow when the price rose and filled when it fell; its wick spans the day's low to its high."
|
|
151
159
|
note="Illustrative data. Point at a day, tap it, or Tab into the chart and use the arrow keys."
|
|
152
|
-
onPointerMove=${
|
|
160
|
+
onPointerMove=${move} onClick=${pick} onFocusIn=${pick}
|
|
153
161
|
onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setPicked(null)}>
|
|
154
162
|
<p class="ticker">
|
|
155
163
|
<b>${() => day.format(new Date(shown().date))}</b>
|