@bezda/rhp-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/bin/rhp-mcp.js +5 -1
- package/package.json +3 -3
- package/skill/SKILL.md +112 -25
- package/skill/recipes/area.html +6 -0
- package/skill/recipes/bar.html +6 -0
- package/skill/recipes/box-plot.html +6 -0
- package/skill/recipes/bubble.html +6 -0
- package/skill/recipes/bullet.html +6 -0
- package/skill/recipes/candlestick.html +6 -0
- 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 +6 -0
- package/skill/recipes/dumbbell.html +6 -0
- package/skill/recipes/gantt.html +14 -4
- package/skill/recipes/grouped-bars.html +6 -0
- package/skill/recipes/heatmap.html +6 -0
- package/skill/recipes/histogram.html +6 -0
- 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 +6 -0
- package/skill/recipes/race.html +6 -0
- package/skill/recipes/radial-bars.html +6 -0
- package/skill/recipes/scatter.html +6 -0
- package/skill/recipes/silhouettes.html +352 -0
- package/skill/recipes/slope.html +6 -0
- 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 +6 -0
- 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 +3 -1
- package/skill/references/looks.md +267 -0
- package/skill/references/pitfalls.md +52 -7
- package/src/check/README.md +15 -2
- package/src/check/findings.js +1 -1
- package/src/check/index.js +103 -14
- package/src/check/inpage.js +80 -16
- package/src/check/report.js +4 -1
- package/src/guard/rhp.js +4 -0
- package/src/guard/rules.js +1 -1
- package/src/server.js +9 -8
|
@@ -968,6 +968,9 @@ In `towers-chart.css`, the two font stacks become `var(--font-archivo), system-u
|
|
|
968
968
|
- The server's HTML has no chart, so the content below it moves down once the chart is drawn; give the parent a `min-height` if that matters.
|
|
969
969
|
- Development: StrictMode (on by default in the App Router) leaves one chart, and Fast Refresh updated the page after an edit to every file, without a reload (checked).
|
|
970
970
|
- Keep the data out of `components/towers/`: a `towers.ts` beside `Towers.tsx` broke the build, because macOS and Windows ignore case in file names and `import("./Towers")` found the data file (TS1149, checked).
|
|
971
|
+
- create-next-app's own page pads its `main` with `px-16` at every width, which leaves the chart 262px of a 390px phone and 192px of a 320px one.
|
|
972
|
+
When you add the chart to that page, let its box break out of the padding on phones: `<div className="-mx-12 self-stretch sm:mx-0">` around it (the `main` is a flex column, so the stretched box takes the margins back).
|
|
973
|
+
Measured in Next.js 16's starter: 358px at 390, 288px at 320, no sideways scroll, and the same box as before from 640px.
|
|
971
974
|
|
|
972
975
|
**Pages Router** (`pages/`): a page may call `dynamic` with `ssr: false` itself.
|
|
973
976
|
`Towers.jsx` is section 7's file with the wrapper imported from `@/lib/rhp/RhpChart.jsx` and no CSS import.
|
|
@@ -1531,13 +1534,17 @@ document.head.insertAdjacentHTML("beforeend", `
|
|
|
1531
1534
|
Give the poster's root its own font, color, `letter-spacing` and `text-align`, and your own elements inside it their own sizes and margins.
|
|
1532
1535
|
2. Scoped component styles never reach the chart (Vue `scoped`, Svelte, Astro, Angular's emulated encapsulation): rhp makes the chart's elements, not the component's template.
|
|
1533
1536
|
Use a plain CSS file, imported where section 2's table says.
|
|
1534
|
-
3.
|
|
1537
|
+
3. Style regular slat blocks with the slat type's `css` and the Chart's `theme`: page CSS cannot change their protected appearance (api.md, section 11).
|
|
1538
|
+
The experimental [ManyDots collection](api.md#manydots-experimental-point-collection) instead accepts application classes and `pointStyle` for point appearance while retaining its geometry guard.
|
|
1539
|
+
In frameworks with scoped component CSS, put those application classes in the plain CSS file too.
|
|
1535
1540
|
4. Only Solid draws the chart on the server (SolidStart, Astro islands).
|
|
1536
1541
|
Everywhere else the server sends an empty box and the browser draws the chart, so the content below moves down once; give the box a `min-height` if that matters.
|
|
1537
|
-
5.
|
|
1542
|
+
5. Data the app loads (a fetch, a Refresh button, a feed) has three more states than the chart: loading, failed and empty.
|
|
1543
|
+
Draw each in the chart's place, at the chart's height: a quiet "Loading…", a line that says what failed with a way to try again, and a sentence for no data ("No activity yet"), never an empty plot, an axis of 0.2, 0.4…, or a headline computed from nothing.
|
|
1544
|
+
6. macOS and Windows ignore case in file names: `towers.ts` beside `Towers.tsx` made `./Towers` load the data file (checked in Next.js).
|
|
1538
1545
|
Give such files different names or folders.
|
|
1539
|
-
|
|
1540
|
-
|
|
1546
|
+
7. In an app that is not a Solid app, import everything from `@bezda/rhp/standalone` and nothing from `solid-js` (api.md, section 2).
|
|
1547
|
+
8. Solid's html template compiles each template with `new Function`, so a page whose Content-Security-Policy forbids `unsafe-eval` cannot draw html-template charts: allow it for that page, or use a Solid app with JSX, which needs no eval.
|
|
1541
1548
|
|
|
1542
1549
|
## 15. What was verified, and the versions
|
|
1543
1550
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Choosing the form: data, story, slat and recipe
|
|
2
2
|
|
|
3
3
|
Use this file in step 3 of the workflow, after the brief.
|
|
4
|
-
It goes from what the reader should see, to the form, to the slat that draws it, to the recipe
|
|
4
|
+
It goes from the subject and what the reader should see, to the form, to the slat that draws it, to the recipe that has the technique.
|
|
5
5
|
When the user named a form ("a donut", "a gantt"), use it, and start at the recipe catalog below.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
|
+
- Start from the subject's own form
|
|
9
10
|
- Start from the reader's question
|
|
10
11
|
- The compositions rhp charts are made of
|
|
11
12
|
- The recipe catalog
|
|
@@ -13,6 +14,31 @@ When the user named a form ("a donut", "a gantt"), use it, and start at the reci
|
|
|
13
14
|
- Combining recipes
|
|
14
15
|
- What rhp does not draw, and the closest answer
|
|
15
16
|
|
|
17
|
+
## Start from the subject's own form
|
|
18
|
+
|
|
19
|
+
Before the data's shape, look at the subject's.
|
|
20
|
+
A reader already knows what it looks like, and a chart that takes that form is recognized before a word is read.
|
|
21
|
+
rhp draws all of these; the recipe named has the technique, not the design.
|
|
22
|
+
|
|
23
|
+
| The subject has | The form that carries it | Technique |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| a cycle that turns (hours of a day, days of a week, months, seasons, a rotation) | a ring or a dial with the cycle around it, or a calendar grid | `dial` (arcs around a 24-hour face), `donut` (wedges from shares), `radial-bars` (arcs), `heatmap` (a grid of Cells) |
|
|
26
|
+
| things known by their outline (buildings, mountains, animals, ships, bottles) | each one drawn in its own outline, to scale, on one ground line | `silhouettes` (a `shape()` per row on a vertical Bar) |
|
|
27
|
+
| units a reader can count (people, seats, days, coins, floors) | one small mark per unit, or a bar cut into units | `waffle` (Cells, a picture in each), `unit-stack` (a Bar cut by a mask) |
|
|
28
|
+
| something that fills (rain, fuel, a battery, a budget, a quota) | a vessel up to the top of the scale, with its content inside | `column` (a tube and its water), `bullet` (bands and a target) |
|
|
29
|
+
| a level things rise above or sink below (sea level, ground, zero, par, a freezing point) | bars or lines hanging from, or standing on, that one line | `diverging-bars`; in a vertical chart, values below the level are negative and hang from it |
|
|
30
|
+
| a route or a sequence in space (stops, stages, a course, a river) | one line with its stations in order | `stacked-100` (its key is a metro line), `gantt`, `slope` |
|
|
31
|
+
| a document the reader knows (a receipt, a ledger, a timetable, a scoreboard, a league table, a menu) | the chart laid out as that document, its lines the slats | `lollipop`, `waterfall`, `sparklines`, `live` |
|
|
32
|
+
| a place (states, districts, seats in a hall, a pitch, a shelf) | a grid of tiles that echoes the place | `heatmap`'s composition (a Plot of rows, each a Plot of Cells) |
|
|
33
|
+
| an instrument that measures it (a thermometer, a gauge, a ruler, a clock, a scale) | the instrument's own scale and pointer | a `Scale` drawn your own way, a Tick as the pointer, `radial-bars` |
|
|
34
|
+
|
|
35
|
+
- The native form is the hero, and the numbers still have to be read: print each value on or beside its mark, since a ring, an outline or a tile is read less exactly than a bar on a shared baseline.
|
|
36
|
+
- When an exact comparison is the whole point, keep bars from one baseline and let the subject show in how the marks are drawn (design.md section 2).
|
|
37
|
+
- When the subject has no form of its own (abstract values, quarters of a year, A against B), the form comes from the reader's question below, and the idea from what the numbers do: the gap, the turn, the outlier.
|
|
38
|
+
- A ring of the parts of a day reads as a clock: a part drawn from midnight to 9 is read as "from midnight to 9".
|
|
39
|
+
Put each part where it happens in the day, or keep the ring in order of size and leave the hour numerals off it.
|
|
40
|
+
- Never bend the data to fit the form: a ring is for parts of a cycle or of a whole, an outline is scaled in one direction only, and units are all one size.
|
|
41
|
+
|
|
16
42
|
## Start from the reader's question
|
|
17
43
|
|
|
18
44
|
Write the one question the chart answers before you pick a form.
|
|
@@ -33,6 +59,7 @@ The headline will be its answer.
|
|
|
33
59
|
| see progress toward goals | a share of a target per goal | radial bars, or bullets | `radial-bars`, `bullet` |
|
|
34
60
|
| see actual against target | actual, target, quality bands | bullet | `bullet` |
|
|
35
61
|
| follow one measure over time | a value per date | line | `line` |
|
|
62
|
+
| follow a measure that grows by multiples, and its falls | a value per date, over a wide range | a line on a log scale | `log-line` |
|
|
36
63
|
| compare a few measures over time | a value per date per series | multi-line | `multi-line` |
|
|
37
64
|
| see a quantity's volume over time | a value per hour or day | area | `area` |
|
|
38
65
|
| see change between two dates per item | two values per item | slope | `slope` |
|
|
@@ -48,6 +75,9 @@ The headline will be its answer.
|
|
|
48
75
|
| see two measures and a size | three values per item | bubble | `bubble` |
|
|
49
76
|
| see a value over two categories | a grid of values (day by hour) | heatmap | `heatmap` |
|
|
50
77
|
| see two groups across ordered bands | two values per band | pyramid | `pyramid` |
|
|
78
|
+
| see things known by their shape at their size | one value per item, each item with an outline | silhouettes to scale | `silhouettes` |
|
|
79
|
+
| see how a day or another cycle splits | parts that add up to the cycle | a dial | `dial` |
|
|
80
|
+
| follow one part of a total over periods, and count it | parts per period that add up | unit stacks with the part at the foot | `unit-stack` |
|
|
51
81
|
| see a schedule and its progress | tasks with start, end, progress | gantt | `gantt` |
|
|
52
82
|
| follow prices with their range | open, high, low, close per day | candlestick | `candlestick` |
|
|
53
83
|
|
|
@@ -56,19 +86,21 @@ Use a donut only for two to six parts with one or two of them the story; otherwi
|
|
|
56
86
|
|
|
57
87
|
## The compositions rhp charts are made of
|
|
58
88
|
|
|
59
|
-
|
|
89
|
+
The slat compositions below use one or more Plots inside a Chart.
|
|
90
|
+
For bulk scatter points, an experimental [ManyDots collection](api.md#manydots-experimental-point-collection) can instead sit directly inside a Chart with `cross`, without a Plot or per-point slat.
|
|
60
91
|
These are the patterns the recipes use; recognizing them lets you build a chart no recipe has.
|
|
61
92
|
|
|
62
93
|
1. **One value per slat**: an edge Label for the name, a Bar from 0 (or `from`) to the value, a value Label at its end.
|
|
63
|
-
Recipes: `bar`, `column`, `lollipop` (a thin Bar and a Dot), `histogram`, `waterfall` (Bars from the running total before to the one after).
|
|
94
|
+
Recipes: `bar`, `column`, `lollipop` (a thin Bar and a Dot), `histogram`, `waterfall` (Bars from the running total before to the one after), `silhouettes` (a Bar wearing its row's `shape()`).
|
|
64
95
|
2. **Several values on one slat, same scale**: several blocks in the same slat, each at its own value.
|
|
65
|
-
Recipes: `dumbbell` (two Dots and the Bar between them), `bullet` (band Bars, the actual Bar, a target Tick), `box-plot` (Bars for whiskers and box, Ticks for the median), `candlestick` (a thin Bar for the wick, a wide one for the body), `gantt` (planned, done and late Bars), `pyramid` (a Bar to each side of zero).
|
|
96
|
+
Recipes: `dumbbell` (two Dots and the Bar between them), `bullet` (band Bars, the actual Bar, a target Tick), `box-plot` (Bars for whiskers and box, Ticks for the median), `candlestick` (a thin Bar for the wick, a wide one for the body), `gantt` (planned, done and late Bars), `pyramid` (a Bar to each side of zero), `unit-stack` (a Bar to the picked part and a Bar from it to the total, each cut into coins by a mask).
|
|
66
97
|
3. **Parts of each slat**: a Plot inside the slat, one inner slat per part.
|
|
67
98
|
An `overlap` inner Plot puts the parts in one band, which makes a stack (`stacked-bars`, `stacked-100`, with `stackUp()` and `shares()`); an inner Plot without `overlap` lays the parts side by side across the band (`grouped-bars`).
|
|
68
99
|
4. **A grid**: a Plot of Cells inside each slat, one Cell per column of the grid.
|
|
69
100
|
Recipes: `heatmap` (days by hours), `waffle` (ten slats of ten people).
|
|
70
101
|
5. **Points on two axes**: `cross={[min, max]}` on the Chart adds a second axis across the slats; an `overlap` Plot's slats then share the whole plot, and a Dot or Label takes `at` (along the scale) and `cross` (across it), a Line takes `[x, y]` points.
|
|
71
102
|
Recipes: `scatter`, `bubble` (Dot size from a value), `line`, `multi-line`, `area` (a filled Line), `slope` (a Line of two points per item).
|
|
103
|
+
`ManyDots` is an explicit alternative for bulk points with individual colors, sizes, shapes, classes, and styles; keep `Dot` for rich point content and Plot's automatic interaction features.
|
|
72
104
|
6. **Every value of a group in its band**: an `overlap` Plot of Dots inside each group's slat.
|
|
73
105
|
Recipe: `strip`.
|
|
74
106
|
7. **A distribution's outline**: an Area from `density()` points, `mirror` for violins.
|
|
@@ -86,6 +118,7 @@ Inside any slat, add ordinary elements where the story needs them: an icon or a
|
|
|
86
118
|
## The recipe catalog
|
|
87
119
|
|
|
88
120
|
Each recipe is a complete page in `recipes/<name>.html` with a designed poster, fixed data and one interaction.
|
|
121
|
+
The comment at the top of each says what to take (its technique) and what to leave (its design, made for its own subject).
|
|
89
122
|
The interaction listed is the recipe's; interaction.md has the others.
|
|
90
123
|
|
|
91
124
|
| Recipe | Shows | Composition | Interaction |
|
|
@@ -115,6 +148,10 @@ The interaction listed is the recipe's; interaction.md has the others.
|
|
|
115
148
|
| `violin` | full distributions of groups | 7, `density()` | a switch that morphs the shapes |
|
|
116
149
|
| `strip` | every value of a few groups | 6 | readout of a dot |
|
|
117
150
|
| `pyramid` | two sides of a population by age | 2 | an age picked lights everyone that age or older |
|
|
151
|
+
| `silhouettes` | ten towers to scale, each in its own outline, on one street | 1, vertical, `shape()` per row | readout of the tower picked, its level carried to the tallest |
|
|
152
|
+
| `dial` | the parts of a day around a 24-hour dial | 10 + 9 | a hand that turns to the part picked, a readout in the hub |
|
|
153
|
+
| `log-line` | ten years of a price on a log scale with a line at each doubling, its falls shaded | 5 (the falls as one filled Line) + 8 + 9, log scale | a crosshair readout by pointer, tap, drag or keys |
|
|
154
|
+
| `unit-stack` | monthly spending as piles of coins, one part at the foot of every pile | 2 (the picked part and the rest), units by a mask | a picker of parts |
|
|
118
155
|
| `gantt` | tasks over weeks, progress, today | 2 + 8 + 9 | readout of a task and its dependencies |
|
|
119
156
|
| `candlestick` | daily open, high, low, close | 2 + 8 | a crosshair and price readout |
|
|
120
157
|
| `sparklines` | a table of trends, one per slat, each scaled to itself | 1 + a Line in each slat + 8 | a year scrubber (pointer, tap, drag; a keyboard slider) |
|
|
@@ -173,6 +173,8 @@ render(() => html`<${RainChart} />`, document.getElementById("chart"));
|
|
|
173
173
|
rhp fades the Bar's `color` itself; the opacity changes at once, because a `transition` on a block would replace rhp's own (rule 7 in section 11).
|
|
174
174
|
- **The readout** is one line of fixed height, so the chart under it never moves; keep its text short enough for 390px.
|
|
175
175
|
It is `aria-hidden` because each slat's `aria-label` says the same to a screen reader.
|
|
176
|
+
- **Where it sits is part of the design.** A band above the chart is this example's choice, not a rule.
|
|
177
|
+
The readout can be the big number of the composition, a line in the poster's rail or title block, or a note in the slat itself, as long as it keeps its size when its text changes (design.md, sections 2 and 6).
|
|
176
178
|
- **Next to the slat.** The readout can sit in the slat itself: `<${Show} when=${() => d.on}><${Label} at=…>…<//><//>` draws it in that one slat only (the pyramid and histogram recipes).
|
|
177
179
|
Do that when the chart is taller than about half a phone's screen, or pin the readout to the screen while the chart is in view: `position: sticky; bottom: 0` on a readout after the chart, with `z-index: 2`, the paper as its background and a hairline on its top edge (`box-shadow: 0 -1px 0 #d9d4ca`), so it reads as a strip laid over the chart.
|
|
178
180
|
A readout above the chart pins (`top: 0`, and the hairline on its bottom edge) only while a pick exists, with a class such as `.readout.picked`: pinned at rest, it covers the plot's top as soon as the reader scrolls to the chart.
|
|
@@ -384,7 +386,7 @@ const WEEKS = VISITORS.map((v, i) => i + 1);
|
|
|
384
386
|
const X = [1, 52];
|
|
385
387
|
|
|
386
388
|
// Slat types: the line, and the week picked (a hairline and a dot) on an overlay Plot
|
|
387
|
-
const Trend = (d) => html
|
|
389
|
+
const Trend = (d) => html`<${Line} points=${() => d.points} />`;
|
|
388
390
|
const Picked = (d) => html`
|
|
389
391
|
<div>
|
|
390
392
|
<${Tick} at=${() => d.week} thick=${1} color="muted" />
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Looks: eight written out in full
|
|
2
|
+
|
|
3
|
+
Eight looks for an editorial poster, each written out in full: tokens, fonts, paper texture, type and one ornament.
|
|
4
|
+
Each goes on top of the base CSS and reads the tokens of design.md section 1, and each was tested on two recipes with the checker at 1280px and 390px.
|
|
5
|
+
|
|
6
|
+
They are worked examples, not a menu.
|
|
7
|
+
A look picked because it matches the topic (newsprint for anything in the news, a dark screen for anything technical) gives the chart the look every chart on that topic gets, and a reader sees the template.
|
|
8
|
+
A look is also only the palette and the type (decisions 4 and 5 of design.md section 2): the idea, the marks and the composition are not in it.
|
|
9
|
+
So write the look for your subject, and take from these what is hard to get right: paper and ink values that pass the contrast floors, a texture, the CSS of a key or a button group.
|
|
10
|
+
design.md section 3 says how a look goes onto a page.
|
|
11
|
+
|
|
12
|
+
| Look | The object it is taken from | Tested on |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Broadsheet | a newspaper's front page | bar, diverging-bars |
|
|
15
|
+
| Swiss grid | a 1960s typographic poster | stacked-100 (light and dark), stacked-bars |
|
|
16
|
+
| Field guide | a naturalist's plate | column, waffle |
|
|
17
|
+
| Luxury magazine | a fashion monthly's feature page | donut, waterfall |
|
|
18
|
+
| Sports page | a matchday programme | race, grouped-bars |
|
|
19
|
+
| Scientific figure | a journal's figure | pyramid, sparklines |
|
|
20
|
+
| Night instrument | a cockpit display after dark | heatmap, area |
|
|
21
|
+
| Photocopied zine | a stapled fanzine | lollipop, radial-bars |
|
|
22
|
+
|
|
23
|
+
## Broadsheet
|
|
24
|
+
|
|
25
|
+
Newsprint with a fine halftone, black ink and one spot blue, a condensed serif headline under the masthead's double rule.
|
|
26
|
+
Marks: flat, square-ended bars (`--rhp-radius: 0px`), the story in the spot blue, a second series in orange, the rest grey.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@400..800&family=Noto+Serif+Display:wdth,wght@62.5..100,500..800&display=swap">
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
/* Look: Broadsheet */
|
|
34
|
+
:root {
|
|
35
|
+
--page: #d5d3cc; --paper: #f3f2ed; --ink: #151515; --soft: #64625c; --rule: #d3d1ca;
|
|
36
|
+
--accent: #1d58a8; --s2: #ca4a07; --s3: #262626; --quiet: #868580;
|
|
37
|
+
--display: "Noto Serif Display", Georgia, "Times New Roman", serif;
|
|
38
|
+
--text: "Libre Franklin", "Franklin Gothic Medium", Arial, sans-serif;
|
|
39
|
+
}
|
|
40
|
+
.poster { border-top: 6px solid var(--ink); box-shadow: inset 0 3px var(--paper), inset 0 4px var(--ink);
|
|
41
|
+
background-image: radial-gradient(rgb(0 0 0 / .05) .6px, transparent .9px); background-size: 3px 3px; }
|
|
42
|
+
.poster .kicker { letter-spacing: .1em; }
|
|
43
|
+
.poster .headline { font: 700 condensed clamp(42px, 6.5vw, 62px)/.96 var(--display); max-width: 16em; }
|
|
44
|
+
.poster .dek { font-size: 18px; color: var(--ink); }
|
|
45
|
+
.poster .note { border-top-color: var(--ink); }
|
|
46
|
+
.poster .keys { font-size: 13px; font-weight: 600; }
|
|
47
|
+
.poster .choice button { border-color: var(--ink); font: 700 12px/1 var(--text); letter-spacing: .08em; text-transform: uppercase; }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Swiss grid
|
|
51
|
+
|
|
52
|
+
The International Typographic Style: white stock with its twelve columns showing faintly, one strict left edge, a large grotesk, red and black.
|
|
53
|
+
Marks: thick square bars, red for the story, black (`series-2`) for a second series, grey for the rest.
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Host+Grotesk:wght@400..800&display=swap">
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
/* Look: Swiss grid */
|
|
61
|
+
:root {
|
|
62
|
+
--page: #d9d9d5; --paper: #f7f7f4; --ink: #111111; --soft: #676765; --rule: #d6d6d3;
|
|
63
|
+
--accent: #ca2218; --s2: #1c1c1c; --s3: #2a5bd7; --quiet: #888887;
|
|
64
|
+
--display: "Host Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif; --text: var(--display);
|
|
65
|
+
}
|
|
66
|
+
.poster { background-image: repeating-linear-gradient(90deg, rgb(0 0 0 / .035) 0 1px, transparent 1px calc(100% / 12)); background-origin: content-box; }
|
|
67
|
+
.poster .rhp-chart { background-color: var(--paper); }
|
|
68
|
+
.poster .kicker { display: flex; align-items: center; gap: 10px; font-size: 13px; letter-spacing: .02em; text-transform: none; color: var(--ink); }
|
|
69
|
+
.poster .kicker::before { content: ""; flex: none; width: 14px; height: 14px; background: var(--accent); }
|
|
70
|
+
.poster .headline { font: 800 clamp(44px, 7.3vw, 70px)/.94 var(--display); letter-spacing: -.035em; max-width: 12em; }
|
|
71
|
+
.poster .dek { max-width: 46ch; font-size: 18px; line-height: 1.45; color: var(--ink); }
|
|
72
|
+
.poster .note { border-top: 2px solid var(--ink); }
|
|
73
|
+
.poster .keys i { width: 14px; height: 14px; }
|
|
74
|
+
.poster .choice button { margin-left: -2px; border: 2px solid var(--ink); font-weight: 700; }
|
|
75
|
+
.poster .choice button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--paper); }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Field guide
|
|
79
|
+
|
|
80
|
+
A naturalist's plate: ivory stock with faint foxing, a ruled plate frame, a bookish serif with italics for names, earth inks.
|
|
81
|
+
Marks: flat earth inks, hairline outlines for containers (a gauge's tube), names of species in italics (`<em>`).
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Alegreya:ital,wght@0,400..800;1,400..800&family=Alegreya+Sans:wght@400;500;700&display=swap">
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
/* Look: Field guide */
|
|
89
|
+
:root {
|
|
90
|
+
--page: #cfcbbf; --paper: #f2f0e8; --ink: #22201b; --soft: #66615a; --rule: #d6d1c4;
|
|
91
|
+
--accent: #a73703; --s2: #3a6b8f; --s3: #56644f; --quiet: #87847c;
|
|
92
|
+
--display: Alegreya, "Iowan Old Style", Georgia, serif; --text: "Alegreya Sans", "Gill Sans", "Trebuchet MS", sans-serif;
|
|
93
|
+
}
|
|
94
|
+
.poster { box-shadow: inset 0 0 0 14px var(--paper), inset 0 0 0 15px var(--soft), inset 0 0 0 18px var(--paper), inset 0 0 0 19px var(--rule);
|
|
95
|
+
background-image: radial-gradient(circle at 14% 22%, rgb(120 100 70 / .07), transparent 16%), radial-gradient(circle at 86% 78%, rgb(120 100 70 / .06), transparent 20%), radial-gradient(circle at 64% 8%, rgb(120 100 70 / .05), transparent 9%); }
|
|
96
|
+
.poster .kicker { font-size: 13px; letter-spacing: .18em; }
|
|
97
|
+
.poster .headline { font: 800 clamp(38px, 5.6vw, 54px)/1.02 var(--display); letter-spacing: -.01em; }
|
|
98
|
+
.poster .dek { font: 400 19px/1.45 var(--display); }
|
|
99
|
+
.poster .note { border-top: 0; font: italic 13px/1.5 var(--display); }
|
|
100
|
+
.poster .keys i { border-radius: 50%; }
|
|
101
|
+
.poster .choice button { border-color: var(--ink); font: 700 12px/1 var(--text); letter-spacing: .1em; text-transform: uppercase; }
|
|
102
|
+
@media (max-width: 640px) {
|
|
103
|
+
.poster { box-shadow: inset 0 0 0 7px var(--paper), inset 0 0 0 8px var(--soft); }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Luxury magazine
|
|
108
|
+
|
|
109
|
+
A fashion or travel monthly: bone stock with a linen weave, a high-contrast serif at large sizes, wide margins, letterspaced capitals, one deep color.
|
|
110
|
+
Marks: square-ended bars (`--rhp-radius: 0px`), the story in the deep red, the rest in petrol and grey, large italic numbers for the key value.
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair:ital,opsz,wght@0,5..1200,300..800;1,5..1200,300..800&family=Urbanist:wght@400..700&display=swap">
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
/* Look: Luxury magazine */
|
|
118
|
+
:root {
|
|
119
|
+
--page: #24211e; --paper: #f6f4ef; --ink: #15120f; --soft: #68625a; --rule: #ddd8ce;
|
|
120
|
+
--accent: #8a1c2f; --s2: #a57d3d; --s3: #2f4b5f; --quiet: #898682;
|
|
121
|
+
--display: Playfair, "Bodoni 72", Didot, "Times New Roman", serif; --text: Urbanist, "Avenir Next", "Century Gothic", sans-serif;
|
|
122
|
+
}
|
|
123
|
+
.poster { padding: 72px 80px 40px;
|
|
124
|
+
background-image: repeating-linear-gradient(0deg, rgb(40 30 20 / .035) 0 1px, transparent 1px 3px), repeating-linear-gradient(90deg, rgb(40 30 20 / .035) 0 1px, transparent 1px 3px); }
|
|
125
|
+
.poster figcaption { gap: 18px; margin-bottom: 44px; }
|
|
126
|
+
.poster .kicker { font: 600 11px/1.6 var(--text); letter-spacing: .32em; color: var(--ink); }
|
|
127
|
+
.poster .kicker::before { content: ""; display: block; width: 56px; height: 1px; margin-bottom: 16px; background: currentColor; }
|
|
128
|
+
.poster .headline { font: 400 clamp(46px, 8.1vw, 78px)/.96 var(--display); letter-spacing: -.02em; max-width: 11em; }
|
|
129
|
+
.poster .headline em { color: var(--accent); }
|
|
130
|
+
.poster .dek { max-width: 42ch; font: italic 400 21px/1.45 var(--display); }
|
|
131
|
+
.poster .note { margin-top: 40px; border-top: 0; font: 500 11px/1.6 var(--text); letter-spacing: .04em; }
|
|
132
|
+
.poster .keys { font: 600 11px/1.3 var(--text); letter-spacing: .2em; text-transform: uppercase; }
|
|
133
|
+
.poster .keys i { width: 22px; height: 2px; vertical-align: 3px; }
|
|
134
|
+
.poster .choice button { min-width: 24px; margin: 0 20px 0 0; padding: 8px 0; border: 0; border-bottom: 1px solid transparent; font: 600 12px/1 var(--text); letter-spacing: .24em; text-transform: uppercase; color: var(--soft); }
|
|
135
|
+
.poster .choice button[aria-pressed="true"] { background: none; border-bottom-color: var(--ink); color: var(--ink); }
|
|
136
|
+
@media (max-width: 640px) {
|
|
137
|
+
.poster { padding: 36px 20px 22px; }
|
|
138
|
+
.poster figcaption { margin-bottom: 28px; }
|
|
139
|
+
.poster .dek { font-size: 17px; }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Sports page
|
|
144
|
+
|
|
145
|
+
A matchday programme: slanted extra-condensed capitals, speed stripes along the top, a jersey-mesh weave, racing orange and navy.
|
|
146
|
+
Marks: bars with a slanted end (`shape=${SLANT}` with `const SLANT = shape(["M", 0, 0], ["L", 1, 0], ["L", "-8px", 1], ["L", 0, 1], ["Z"])`), big tabular numbers, the leader in the accent.
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sofia+Sans+Extra+Condensed:ital,wght@0,700..900;1,700..900&family=Sofia+Sans:ital,wght@0,400..800;1,800&display=swap">
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```css
|
|
153
|
+
/* Look: Sports page */
|
|
154
|
+
:root {
|
|
155
|
+
--page: #c8cacb; --paper: #f3f3f0; --ink: #121418; --soft: #616468; --rule: #d5d6d4;
|
|
156
|
+
--accent: #b83900; --s2: #1d3b8f; --s3: #257656; --quiet: #858686;
|
|
157
|
+
--display: "Sofia Sans Extra Condensed", "Arial Narrow", Impact, sans-serif; --text: "Sofia Sans", "Segoe UI", Arial, sans-serif;
|
|
158
|
+
}
|
|
159
|
+
.poster { position: relative; padding-top: 58px;
|
|
160
|
+
background-image: radial-gradient(rgb(0 0 0 / .05) 1px, transparent 1.4px), radial-gradient(rgb(0 0 0 / .05) 1px, transparent 1.4px);
|
|
161
|
+
background-size: 6px 6px; background-position: 0 0, 3px 3px; }
|
|
162
|
+
.poster::before { content: ""; position: absolute; inset: 0 0 auto; height: 12px; background: repeating-linear-gradient(-60deg, var(--accent) 0 14px, transparent 14px 24px); }
|
|
163
|
+
.poster .kicker { font: italic 800 14px/1.2 var(--text); letter-spacing: .06em; }
|
|
164
|
+
.poster .headline { font: italic 900 clamp(54px, 8.75vw, 84px)/.86 var(--display); text-transform: uppercase; max-width: 13em; }
|
|
165
|
+
.poster .note { border-top: 3px solid var(--ink); font-weight: 500; }
|
|
166
|
+
.poster .keys { font: 800 13px/1.2 var(--text); letter-spacing: .04em; text-transform: uppercase; }
|
|
167
|
+
.poster .keys i { width: 18px; height: 10px; transform: skewX(-20deg); }
|
|
168
|
+
.poster .choice { gap: 4px; }
|
|
169
|
+
.poster .choice button { margin: 0; padding: 9px 14px; border: 0; background: var(--rule); font: italic 800 14px/1 var(--text); text-transform: uppercase; }
|
|
170
|
+
@media (max-width: 640px) {
|
|
171
|
+
.poster { padding-top: 34px; }
|
|
172
|
+
.poster::before { height: 8px; }
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Scientific figure
|
|
177
|
+
|
|
178
|
+
A journal's figure: white coated stock, a figure label, a plain sans for the figure and a serif for its caption, colors that hold for color-blind readers.
|
|
179
|
+
Marks: thin lines, small dots and square bars, the accent on what the finding is about, `n` and the method in the note.
|
|
180
|
+
|
|
181
|
+
```html
|
|
182
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400..700&family=STIX+Two+Text:ital,wght@0,400..700;1,400&display=swap">
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```css
|
|
186
|
+
/* Look: Scientific figure */
|
|
187
|
+
:root {
|
|
188
|
+
--page: #e6e7e9; --paper: #ffffff; --ink: #1a1c1f; --soft: #63676d; --rule: #e1e3e6;
|
|
189
|
+
--accent: #b45000; --s2: #3563d0; --s3: #18805f; --quiet: #8d8e8f;
|
|
190
|
+
--display: "IBM Plex Sans", "Helvetica Neue", Arial, sans-serif; --text: var(--display); --caption: "STIX Two Text", "Times New Roman", serif;
|
|
191
|
+
}
|
|
192
|
+
.poster { max-width: 880px; border: 1px solid #d3d6da; background-image: linear-gradient(170deg, #ffffff 50%, #f6f7f8); }
|
|
193
|
+
.poster .kicker { width: fit-content; padding: 3px 8px; background: var(--ink); color: var(--paper); font-size: 12px; letter-spacing: .06em; }
|
|
194
|
+
.poster .headline { font: 700 clamp(26px, 3.6vw, 34px)/1.14 var(--display); letter-spacing: -.01em; max-width: 24em; }
|
|
195
|
+
.poster .dek { max-width: 66ch; font: 400 17px/1.5 var(--caption); color: var(--ink); }
|
|
196
|
+
.poster .note { font: 400 13px/1.5 var(--caption); }
|
|
197
|
+
.poster .keys { font-size: 13px; }
|
|
198
|
+
.poster .keys i { border-radius: 50%; }
|
|
199
|
+
.poster .choice button { border-color: #aab0b8; font-weight: 500; font-size: 13px; }
|
|
200
|
+
.poster .choice button:first-child { border-radius: 4px 0 0 4px; }
|
|
201
|
+
.poster .choice button:last-child { border-radius: 0 4px 4px 0; }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Night instrument
|
|
205
|
+
|
|
206
|
+
A cockpit display after dark: graphite glass with a fine screen mesh, corner brackets, a typeface made for instruments, amber for what matters.
|
|
207
|
+
Marks: thin lines and small dots, cells from dark to bright (section 4), no glow: a flat bright color on a dark ground is already the brightest thing on the page.
|
|
208
|
+
|
|
209
|
+
```html
|
|
210
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=B612:wght@400;700&family=B612+Mono&display=swap">
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```css
|
|
214
|
+
/* Look: Night instrument */
|
|
215
|
+
:root {
|
|
216
|
+
color-scheme: dark;
|
|
217
|
+
--page: #070708; --paper: #121314; --ink: #e8eaec; --soft: #8f9499; --rule: #2e3134;
|
|
218
|
+
--accent: #ffb020; --s2: #69ddfc; --s3: #d26eca; --quiet: #666769;
|
|
219
|
+
--display: B612, "Segoe UI", system-ui, sans-serif; --text: var(--display); --mono: "B612 Mono", ui-monospace, Menlo, monospace;
|
|
220
|
+
}
|
|
221
|
+
.poster { position: relative; border-radius: 4px;
|
|
222
|
+
background-image: radial-gradient(rgb(255 255 255 / .035) .6px, transparent .9px), radial-gradient(ellipse at 50% 0%, rgb(255 255 255 / .05), transparent 70%);
|
|
223
|
+
background-size: 4px 4px, 100% 100%; }
|
|
224
|
+
.poster::before { content: ""; position: absolute; inset: 12px; border: 1px solid var(--soft); pointer-events: none;
|
|
225
|
+
mask: conic-gradient(at 18px 18px, transparent 75%, #000 0) 0 0 / calc(100% - 18px) calc(100% - 18px); }
|
|
226
|
+
.poster .kicker { font: 400 12px/1.3 var(--mono); letter-spacing: .16em; }
|
|
227
|
+
.poster .headline { font: 700 clamp(30px, 4.6vw, 44px)/1.08 var(--display); letter-spacing: -.01em; }
|
|
228
|
+
.poster .note { border-top-style: dashed; font-size: 12px; }
|
|
229
|
+
.poster .keys { font: 400 12px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
|
230
|
+
.poster .keys i { width: 14px; height: 3px; vertical-align: 3px; }
|
|
231
|
+
.poster .choice button { border-color: var(--rule); font: 400 12px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--soft); }
|
|
232
|
+
.poster .choice button[aria-pressed="true"] { background: none; border-color: var(--accent); color: var(--accent); }
|
|
233
|
+
.poster :focus-visible { outline-color: var(--accent); }
|
|
234
|
+
@media (max-width: 640px) {
|
|
235
|
+
.poster::before { inset: 6px; }
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Photocopied zine
|
|
240
|
+
|
|
241
|
+
A stapled fanzine: speckled copier paper with a dark copier edge, the headline cut out on a black strip, typewriter text, one hot pink.
|
|
242
|
+
Marks: black for most marks, pink for the one value that matters, dotted stems like a photocopied leader line.
|
|
243
|
+
|
|
244
|
+
```html
|
|
245
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Courier+Prime:wght@400;700&display=swap">
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```css
|
|
249
|
+
/* Look: Photocopied zine */
|
|
250
|
+
:root {
|
|
251
|
+
--page: #3a3936; --paper: #f1f0ec; --ink: #121212; --soft: #62615d; --rule: #cfcdc8;
|
|
252
|
+
--accent: #c30f59; --s2: #1f3fd1; --s3: #121212; --quiet: #848482;
|
|
253
|
+
--display: Anton, Impact, "Arial Black", sans-serif; --text: "Courier Prime", "Courier New", monospace;
|
|
254
|
+
}
|
|
255
|
+
.poster { position: relative; box-shadow: inset 16px 0 22px -16px rgb(0 0 0 / .22);
|
|
256
|
+
background-image: radial-gradient(rgb(0 0 0 / .3) .5px, transparent .9px), radial-gradient(rgb(0 0 0 / .22) .6px, transparent 1px), radial-gradient(rgb(0 0 0 / .26) .4px, transparent .8px), radial-gradient(rgb(0 0 0 / .18) .7px, transparent 1.1px);
|
|
257
|
+
background-size: 23px 31px, 47px 37px, 61px 89px, 113px 71px; background-position: 0 0, 17px 9px, 31px 52px, 7px 40px; }
|
|
258
|
+
.poster::before { content: ""; position: absolute; top: 14px; left: 30px; width: 36px; height: 4px; border-radius: 1px; background: #8d8d8d; rotate: -6deg; }
|
|
259
|
+
.poster .rhp-chart { background-color: var(--paper); }
|
|
260
|
+
.poster .kicker { width: fit-content; padding: 3px 8px; background: var(--accent); color: var(--paper); letter-spacing: .08em; }
|
|
261
|
+
.poster .headline { width: fit-content; max-width: 100%; padding: .04em .18em .08em; background: var(--ink); color: var(--paper); font: 400 clamp(40px, 6.5vw, 62px)/1.06 var(--display); text-transform: uppercase; text-wrap: wrap; rotate: -1deg; }
|
|
262
|
+
.poster .dek { color: var(--ink); }
|
|
263
|
+
.poster .note { border-top: 2px dashed var(--ink); }
|
|
264
|
+
.poster .keys { font-size: 13px; font-weight: 700; }
|
|
265
|
+
.poster .keys i { border: 1.5px solid var(--ink); }
|
|
266
|
+
.poster .choice button { margin-left: -2px; border: 2px solid var(--ink); font-weight: 700; text-transform: uppercase; }
|
|
267
|
+
```
|
|
@@ -14,6 +14,7 @@ Every cause here was reproduced with rhp 2.0.1; the numbers in brackets point to
|
|
|
14
14
|
- Values never update, or the chart jumps
|
|
15
15
|
- A click, hover, tap or key does nothing, or the wrong thing
|
|
16
16
|
- The layout jumps when the reader interacts
|
|
17
|
+
- Text and layers laid over the chart
|
|
17
18
|
- Styles or colors do not apply
|
|
18
19
|
- Console warnings rhp prints
|
|
19
20
|
- Mistakes in the work itself
|
|
@@ -50,9 +51,12 @@ Start from a recipe, never from what another library does.
|
|
|
50
51
|
5. **NaN, undefined or null** draw at the scale's start and print "NaN" [16]: clean or filter the data first.
|
|
51
52
|
6. **Every slat got one item of a list meant for all of them**: an array given to a Plot is a column, item i for row i. To give every slat the same list (the series still shown, a set of picked names), pass a function of the row: `shown=${(d) => shown()}`.
|
|
52
53
|
7. **Lists of different lengths** wrap around without an error [8]: the third slat of a 3-item list shown beside a 5-item list repeats the first values. Give every data group the same length.
|
|
53
|
-
8. **A Dot is an oval**: its `size` was a number [17].
|
|
54
|
+
8. **A Dot is an oval**: its `size` was a number [17].
|
|
55
|
+
Give a length, `size="12px"`.
|
|
56
|
+
`ManyDots` has a different size contract: its numbers mean pixels, so `size=${12}` is a 12px point.
|
|
54
57
|
9. **A vertical Plot over a horizontal chart draws 0px-tall columns**: in rhp 2.0.1, a second top-level Plot with `orientation="vertical"` in a horizontal Chart whose slat type has `room: "auto"` gets columns as wide as the chart and no height. Give the slats room in px, or put the Plot in a chart with `cross` (as the line recipe does).
|
|
55
|
-
10. **A scatter or line chart puts everything on one line**: the Chart needs `cross=${[min, max]}`, the Plot needs `overlap=${true}`, and each Dot needs `cross=${() => d.y}`.
|
|
58
|
+
10. **A scatter or line chart puts everything on one line**: with Dot slats, the Chart needs `cross=${[min, max]}`, the Plot needs `overlap=${true}`, and each Dot needs `cross=${() => d.y}`.
|
|
59
|
+
The experimental [ManyDots collection](api.md#manydots-experimental-point-collection) goes directly inside a Chart with `cross` and takes its own `rows`, `at`, and `cross` accessors.
|
|
56
60
|
|
|
57
61
|
## Text sticks out, is cut off or overlaps
|
|
58
62
|
|
|
@@ -67,6 +71,12 @@ Start from a recipe, never from what another library does.
|
|
|
67
71
|
9. **A value Label over the last column of a vertical chart sticks out**: give `room.after` half the label's width.
|
|
68
72
|
10. **Text overlaps on a phone only**: test at 390px. Use a media query or a container query on the page for the poster, and `:vertical` / `:horizontal` rules for the slat.
|
|
69
73
|
11. **Two words run together in a readout** ("All regions12") [35]: Solid's html template drops a space that stands alone between two tags. Write it as `${" "}`.
|
|
74
|
+
12. **The first or last name under a column sticks out of the chart**: a label centered under its column with `translate: -50%` runs past the plot's ends.
|
|
75
|
+
Align the first one's start and the last one's end with the plot, or give `room.before` and `room.after` half its width.
|
|
76
|
+
13. **Axis numbers run together at 320px**: the names' and the values' room leave a narrow plot.
|
|
77
|
+
Give the Chart fewer ticks when the window is narrow (`ticks=${() => (narrow() ? every(10) : every(5))}`, with `narrow` a signal from `matchMedia("(max-width: 400px)")`), shorter numbers, or less room.
|
|
78
|
+
14. **Small parts lose their names**: hiding a label that does not fit is never the only fallback.
|
|
79
|
+
Put it outside its mark with a short leader, or in a key beside the chart in the marks' order; on a phone use short codes before dropping names (design.md section 7).
|
|
70
80
|
|
|
71
81
|
## The chart is tiny, squashed or the wrong size
|
|
72
82
|
|
|
@@ -83,9 +93,12 @@ Start from a recipe, never from what another library does.
|
|
|
83
93
|
4. **Slats swap data when rows are removed**: give the Plot a `key` (`key="city"`).
|
|
84
94
|
5. **Blocks jump to new values instead of moving** [22]: a `transition` on a block replaces rhp's own. Put your transition on an element inside the block.
|
|
85
95
|
6. **Numbers show long decimals while they move** [26]: in the JS version (`animate`) values are fractional mid-way; round what you print.
|
|
86
|
-
7.
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
7. **Bars vanish, or a slat jumps to the first place, after the data changes** [4]: a `style` given as a string that changed (or turned to nothing) replaced the variables rhp keeps in the element's style.
|
|
97
|
+
Give a slat root's changing style as an object: `style=${() => ({ "--n": d.n })}`.
|
|
98
|
+
Blocks need the same workaround on published rhp 2.0.2; current source fixes Block style replacements for the upcoming 2.0.3 release.
|
|
99
|
+
8. **`animate` is on, but a value still jumps**: the JS version never moves a group given as a function of the row (`v=${(d) => ...}`). Give the changing numbers as a list (`v=${() => values()}`), a single value or a field of `rows`.
|
|
100
|
+
9. **A race stops at each step, or a live feed's marks stop between readings**: to play back data you have, never step through it with a timer; move a playhead on every frame, give the Plot the values interpolated between the steps around it, and keep the JS version's default 150 ms, so the marks glide 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); with ease-out or a shorter duration the marks rest between readings.
|
|
101
|
+
10. **Marks trail behind data that changes rapidly or continuously** (a live feed, a slider or drag that drives the data, a play button, a bar chart race): in the default CSS version every new value restarts a CSS transition from wherever the mark is, so under steady change the marks always lag, and Safari can stall transitions that input events retarget. Give the Plot `animate` for the data groups that change (`animate=${["value"]}`, or `animate=${true}`), or the Chart when the scale moves with the data: rhp's JS clock carries each move into the next value. Measured with a slider changing the data every frame: the CSS version trailed the data by 10 points of 100, the JS version by 6.
|
|
89
102
|
|
|
90
103
|
## A click, hover, tap or key does nothing, or the wrong thing
|
|
91
104
|
|
|
@@ -106,8 +119,34 @@ Start from a recipe, never from what another library does.
|
|
|
106
119
|
The checker warns (`layout-jump`) when a control or the chart moves after one of its interactions; these are the usual causes. It tries only some controls, so try every one yourself and compare.
|
|
107
120
|
|
|
108
121
|
1. **Text that changes with a switch moves the controls below it** (a headline or a dek per question): stack every version in one grid cell (a wrapper with `display: grid`, each version at `grid-area: 1 / 1`, `visibility: hidden` except the current one), so the cell is always as tall as the longest version.
|
|
109
|
-
|
|
122
|
+
Align the versions to the cell's end (`align-self: end`) when the chart is below them, so a short version leaves no empty line above the chart.
|
|
123
|
+
2. **A readout that wraps on a phone pushes the chart down**: give it a `min-height` for its longest text at 320px (`min-height: 2lh`, or `3lh`), counting the longest numbers and dates that gain a year ("Jan 4, 2027").
|
|
110
124
|
3. **Numbers that change width shift what follows them**: use `font-variant-numeric: tabular-nums`, and give a column of numbers a fixed room; in an "auto" end room, align them across slats with `justify-self: stretch` and `justify-content: flex-end` on the label.
|
|
125
|
+
4. **Every bar slides when a slat is lit**: the lit name turns bold in a `room.start` of "auto", which measures the widest name again and moves the plot.
|
|
126
|
+
Change only the lit name's color, or keep its bold width at rest: room in px, or the bold text drawn hidden under the name (`.name::after { content: attr(data-name); display: block; height: 0; overflow: hidden; visibility: hidden; font-weight: 700; }`, with `data-name=${() => d.name}` on the Label; measured: the bars held still at 1280px, where a plain bold name moved them 10.8px).
|
|
127
|
+
5. **A button whose text changes (Play, Pause) moves its neighbors**: stack both texts in one grid cell, the hidden one `visibility: hidden`, so the button is always as wide as the longer one.
|
|
128
|
+
|
|
129
|
+
## Text and layers laid over the chart
|
|
130
|
+
|
|
131
|
+
A bold composition sets a headline, a picture or a key over the plot's empty part.
|
|
132
|
+
These are the traps it brings.
|
|
133
|
+
|
|
134
|
+
1. **A focus ring strikes through the text laid over the plot**: an outline on the slat's root runs the whole length of the plot.
|
|
135
|
+
Draw the ring on the mark instead: `.slat:focus-visible { outline: none; }` and `.slat:focus-visible .bar { outline: 2px solid var(--rhp-ink); outline-offset: 2px; }`, with `class="bar"` on the Bar.
|
|
136
|
+
A mark that wears a `shape()` or a mask shows no outline, because both cut it away: ring the slat's name instead (`.slat:focus-visible .name { outline: 2px solid var(--rhp-ink); outline-offset: 3px; }`), as `silhouettes` does.
|
|
137
|
+
Never take the slat's ring away without drawing another.
|
|
138
|
+
2. **Gridlines run through the value labels**: a label just past a bar's end sits on the next gridline when the value is near a tick.
|
|
139
|
+
Give the number the paper as its background, on a span inside the Label so the Label keeps the gap rhp gives it (its own padding): `<${Label} at=${() => d.v} class="value"><span class="num">${() => d.v}</span><//>`, with `.num { margin: 0 -3px; padding: 0 3px; background: var(--rhp-surface); }` in the slat's css.
|
|
140
|
+
The grid is drawn under the slats, so the line stops at the text.
|
|
141
|
+
The theme's `surface` must be the paper (`surface: "var(--paper)"`; it is white by default), and the box suits a flat paper only: over a grain or a texture it shows as a patch.
|
|
142
|
+
Not next to a reference line, which stays whole (design.md section 7).
|
|
143
|
+
3. **Taps on the marks pick nothing**: a layer laid over the chart (a picture, a glow, a caption box) takes every pointer event when it is drawn above rhp's plot (`z-index: 1`): at `z-index: 2` or more, or at 1 after the Plot in the page.
|
|
144
|
+
Give it `pointer-events: none`.
|
|
145
|
+
What the reader presses must be the slat or an element inside it: a mark that takes no pointer events sends the press to the chart under it, which cannot take focus, so the focused slat loses focus and the pick clears.
|
|
146
|
+
Give the mark `pointer-events: auto` (`donut`), or, when the chart picks by the pointer's place, put `onMouseDown=${(e) => e.preventDefault()}` on the element under the marks (`dial`).
|
|
147
|
+
4. **The text laid over the plot runs into the marks with other data**: its place was set by hand (a fixed `top`, a share of the width) for these values.
|
|
148
|
+
Set it for your data, then check a copy whose values all sit near the largest, so the plot has no empty part, at every width, and look at the screenshots: the checker reports words over value labels, not words over bare marks.
|
|
149
|
+
Where the words meet a mark, set them above the chart at that width.
|
|
111
150
|
|
|
112
151
|
## Styles or colors do not apply
|
|
113
152
|
|
|
@@ -116,7 +155,7 @@ The checker warns (`layout-jump`) when a control or the chart moves after one of
|
|
|
116
155
|
3. **The chart ignores the page's font or colors** [20]: give them in the Chart's `theme` (`font`, `ink`, `muted`, `grid`, `series`, ...).
|
|
117
156
|
4. **`var(--brand)` as a color warns** [20]: put the brand colors in the theme instead.
|
|
118
157
|
5. **Rows 5 and 6 take the first series color** [18]: `series()` counts six colors; with a shorter theme list call `series(n)`.
|
|
119
|
-
6. **A value label does not take its bar's color** [19]: set `--rhp-color` on the slat's root (and leave `color` off the Bar), or color the label itself.
|
|
158
|
+
6. **A value label does not take its bar's color** [19]: set `--rhp-color` on the slat's root in a style object (and leave `color` off the Bar), or color the label itself.
|
|
120
159
|
7. **The poster looks unstyled** [31]: the Poster brings no CSS. Style `.poster`, `figcaption`, `.kicker`, `.headline`, `.dek` and `.note` in the page.
|
|
121
160
|
8. **Focus rings on slats do not show your style** [22]: rhp's core CSS resets `outline` on slat roots, so a page `:focus-visible` rule never reaches them. Put `.slat:focus-visible { ... }` in the slat type's `css`.
|
|
122
161
|
9. **The poster's styles do not apply inside a framework component**: scoped component styles (Vue `scoped`, Svelte, Astro, Angular's emulated encapsulation) never reach elements rhp makes. Use a plain CSS file; environments.md section 14 has every framework's verified caveats.
|
|
@@ -125,6 +164,8 @@ The checker warns (`layout-jump`) when a control or the chart moves after one of
|
|
|
125
164
|
12. **A transition on the slat's root does nothing** [22]: rhp keeps the root's own transition (its slide). Transition an element inside a block instead.
|
|
126
165
|
13. **A rule in the slat's CSS matches nothing**: the class is on a different element than you think. Classes you put on blocks go on the block's own element (`.rhp-bar.bar`); `:horizontal` and `:vertical` go on the slat or a block, not on an element inside a block.
|
|
127
166
|
14. **A pinned readout or a menu over the chart is drawn under the bars** [36]: give it `z-index: 2` or more.
|
|
167
|
+
15. **The marks are invisible when the component is shown on its own**: its theme reads the app's variables with no fallback (`series: ["var(--chart-1)", …]`), so on a page without the app's CSS (the checker's page, a test, Storybook) every mark is transparent.
|
|
168
|
+
Give each a fallback with the app's own value: `"var(--chart-1, oklch(0.646 0.222 41.116))"` (design.md section 8).
|
|
128
169
|
|
|
129
170
|
## Console warnings rhp prints
|
|
130
171
|
|
|
@@ -150,6 +191,10 @@ These are not rhp errors, but they decide whether the user gets what they asked
|
|
|
150
191
|
- **Invented numbers presented as fact**: data you supplied names its source and year, or the note says "Illustrative data".
|
|
151
192
|
- **A series the user did not ask for**: what is left when their parts do not reach 100% is a quiet, unlabeled remainder explained in the note, not a new series with its own color and key entry; the brief lists it under Asked as a deviation.
|
|
152
193
|
- **A headline that is not true for the data**: recompute every number in the headline, the dek and the note from the final data ("four in ten" was false for 52%).
|
|
194
|
+
- **Text computed from data that turns false on other data**: "rose ∞%" from a zero first week, "as many as" for a change from none, "held steady" because the first and last values match after a peak, "led for eight years" counted only inside the chart's window.
|
|
195
|
+
SKILL.md step 6 has the rules; step 8's hardest data finds them.
|
|
196
|
+
- **Mixed releases**: real yearly figures taken partly from first estimates and partly from later revisions can reverse a move between two years.
|
|
197
|
+
Take each year from the latest release that covers it.
|
|
153
198
|
- **A real name on invented numbers**: never put a real country, city, company or person on illustrative data; use real figures with their source, or plain invented names ("Riverton", "a monsoon city", "Team A") and say "invented" in the note.
|
|
154
199
|
- **A magazine poster dropped into an app**: inside an existing app, use the app's fonts and colors (design.md).
|
|
155
200
|
- **Hover-only information**: everything a hover shows also shows on focus and on tap.
|