@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
|
@@ -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) |
|
|
@@ -142,9 +142,11 @@ function RainChart() {
|
|
|
142
142
|
if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
|
|
143
143
|
setOn(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
|
|
144
144
|
};
|
|
145
|
+
// Off the chart (on the headline, the readout or the note), pointing is as if the pointer had left the poster
|
|
146
|
+
const move = (e) => e.pointerType !== "touch" && (e.target.closest(".rhp-chart") ? pick(e) : leave(e));
|
|
145
147
|
return html`
|
|
146
148
|
<${Poster} title="City A gets more than three times City D's October rain" note="Illustrative data."
|
|
147
|
-
onPointerMove=${
|
|
149
|
+
onPointerMove=${move} onClick=${pick} onFocusIn=${pick}
|
|
148
150
|
onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setOn(null)}>
|
|
149
151
|
<p class="readout" aria-hidden="true">${() => (on() == null ? "Tap or point at a city." : say(on()))}</p>
|
|
150
152
|
<${Chart} scale=${[0, 200]} ticks=${[0, 50, 100, 150, 200]} label="Rain in October, in mm">
|
|
@@ -173,6 +175,8 @@ render(() => html`<${RainChart} />`, document.getElementById("chart"));
|
|
|
173
175
|
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
176
|
- **The readout** is one line of fixed height, so the chart under it never moves; keep its text short enough for 390px.
|
|
175
177
|
It is `aria-hidden` because each slat's `aria-label` says the same to a screen reader.
|
|
178
|
+
- **Where it sits is part of the design.** A band above the chart is this example's choice, not a rule.
|
|
179
|
+
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
180
|
- **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
181
|
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
182
|
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.
|
|
@@ -181,6 +185,7 @@ render(() => html`<${RainChart} />`, document.getElementById("chart"));
|
|
|
181
185
|
Checked: the checker reported a change for each slat it pointed at (City A, City C, City F) and tapped (City A, City F); its Tab reached City A and ArrowDown moved focus to City B.
|
|
182
186
|
Playwright with a mouse: at rest City A was lit and the readout gave the hint; on City C's bar the readout named City C, City C was lit, and the five others were at opacity .3 with rhp's transition still on their bars; from City F down onto the axis the readout stayed on City F; leaving the poster cleared it all.
|
|
183
187
|
With focus on City A, the mouse over City D took the readout, and leaving the poster gave it back to City A.
|
|
188
|
+
Off the chart, in Chromium and WebKit: from City C onto the headline the readout went back to the hint, and with City A clicked, from City D onto the readout it went back to City A; the checker reported nothing.
|
|
184
189
|
At 390px with touch: a tap on City B kept the readout after the finger lifted, a scroll that started on City E moved the page and left the readout on City B, a tap on City E's number picked City E, and a tap on the headline or below the poster cleared it.
|
|
185
190
|
With slats 110px thick (a chart taller than a 390 × 664 screen), a readout pinned with `position: sticky` and `z-index: 2`, after the chart or above it, showed City B on screen, and City E after a scroll, with its hairline edge; without `z-index: 2` the slats covered it.
|
|
186
191
|
Scrolled to the chart before any pick, the readout above it that pins only while a pick exists was out of the way, and one pinned at rest covered the plot's top.
|
|
@@ -384,7 +389,7 @@ const WEEKS = VISITORS.map((v, i) => i + 1);
|
|
|
384
389
|
const X = [1, 52];
|
|
385
390
|
|
|
386
391
|
// Slat types: the line, and the week picked (a hairline and a dot) on an overlay Plot
|
|
387
|
-
const Trend = (d) => html
|
|
392
|
+
const Trend = (d) => html`<${Line} points=${() => d.points} />`;
|
|
388
393
|
const Picked = (d) => html`
|
|
389
394
|
<div>
|
|
390
395
|
<${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
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Keeping a big chart light
|
|
2
|
+
|
|
3
|
+
Use this reference when a finished chart has measurable interaction or loading costs, particularly with many slats or many blocks per slat.
|
|
4
|
+
Measure the chart before choosing an optimization.
|
|
5
|
+
These patterns preserve rhp's slats, blocks, nested Plots, and public APIs.
|
|
6
|
+
They do not require any particular interaction.
|
|
7
|
+
|
|
8
|
+
## Measure the work
|
|
9
|
+
|
|
10
|
+
Count elements in the page and hidden slats, and observe attribute and child-list mutations during a representative action.
|
|
11
|
+
Separate DOM writes from rendering cost: fewer JavaScript writes can still cause the browser to restyle and paint many marks.
|
|
12
|
+
Compare actual interaction latency as well as counts when speed matters.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
const chart = document.querySelector('.rhp-chart');
|
|
16
|
+
console.log(chart.querySelectorAll('*').length);
|
|
17
|
+
const changes = [];
|
|
18
|
+
const observer = new MutationObserver(records => changes.push(...records));
|
|
19
|
+
observer.observe(chart, { subtree: true, attributes: true, childList: true });
|
|
20
|
+
// Perform the action, then disconnect and inspect changes.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Switch mark colors through a slat class
|
|
24
|
+
|
|
25
|
+
When one slat's state changes many marks' colors, give the marks their colors once and switch a CSS variable through a class on the owning slat.
|
|
26
|
+
Keep the `color` API so all consumers of the mark's color, including outlines or descendant styling, continue to see the active color.
|
|
27
|
+
A generic `background` shorthand override can erase background images and changes only that paint property.
|
|
28
|
+
Use an override of a specific paint property only when that is the intended behavior of the existing chart.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const Segment = slat({
|
|
32
|
+
css: `
|
|
33
|
+
.seg { --active-tone: var(--base-tone); }
|
|
34
|
+
.lit .seg { --active-tone: var(--lit-tone); }
|
|
35
|
+
`,
|
|
36
|
+
}, s => html`
|
|
37
|
+
<${Bar} class="seg" from=${() => s.from} to=${() => s.to}
|
|
38
|
+
color="var(--active-tone)"
|
|
39
|
+
style=${() => ({ '--base-tone': s.tone, '--lit-tone': s.gold })} />`);
|
|
40
|
+
|
|
41
|
+
const Shelf = slat({ thickness: 24, room: { start: 70, end: 40 } }, d => html`
|
|
42
|
+
<div class=${() => d.lit ? 'shelf lit' : 'shelf'}>
|
|
43
|
+
<${Label} edge="start">${() => d.name}<//>
|
|
44
|
+
<${Plot} overlap=${true} from=${() => d.from} to=${() => d.to}
|
|
45
|
+
tone=${() => d.tones} gold=${() => d.golds}>${Segment}<//>
|
|
46
|
+
</div>`);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`tone` and `gold` here are CSS colors, including theme variables such as `var(--rhp-series-8)`, rather than bare theme-key strings.
|
|
50
|
+
Keep their data groups independent of the interaction state.
|
|
51
|
+
Continue passing reactive getters when the underlying data or palette can change.
|
|
52
|
+
Only the shelf's `lit` data group needs to read the state that chooses the highlighted shelf.
|
|
53
|
+
This does not change how the chart chooses that state.
|
|
54
|
+
|
|
55
|
+
Put the rule in the `css` of the slat type that owns the mark, here `Segment`.
|
|
56
|
+
Its selector may name ancestors inside the same chart, including the shelf root.
|
|
57
|
+
A shelf's slat CSS does not style another nested slat type's marks.
|
|
58
|
+
See api.md, “Slat CSS scope”.
|
|
59
|
+
|
|
60
|
+
## Put shared dimming state on a parent
|
|
61
|
+
|
|
62
|
+
When a shared selection dims many slats, a class on their Plot or grouping slat can replace a flag rewritten on every row.
|
|
63
|
+
Keep the classes that identify the exceptions.
|
|
64
|
+
For example, inside the mark-owning slat's CSS:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
.picking .shelf:not(.lit):not(.pointed) .seg { opacity: .3; }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Set `picking` on a Plot or slat root inside the chart when the existing interaction has an active selection.
|
|
71
|
+
Keep `lit` and `pointed` consistent with the existing selection and preview behavior.
|
|
72
|
+
Charts with multiple selections, disabled marks, separate groups, or other exceptions need selectors that preserve all those states.
|
|
73
|
+
A class outside the chart, on the poster for example, is outside slat CSS's ancestor scope.
|
|
74
|
+
|
|
75
|
+
## Render hidden slats' contents conditionally
|
|
76
|
+
|
|
77
|
+
A Plot row whose `d.position` is `null` is hidden, but its contents normally remain mounted and reactive.
|
|
78
|
+
For expensive rows hidden by a filter or top-N view, Solid's `Show` can leave only the slat root mounted:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const Shelf = slat({ thickness: 24, room: { start: 70, end: 40 } }, d => html`
|
|
82
|
+
<div class=${() => d.lit ? 'shelf lit' : 'shelf'}>
|
|
83
|
+
<${Show} when=${() => d.position != null}>
|
|
84
|
+
<${Label} edge="start">${() => d.name}<//>
|
|
85
|
+
<${Plot} overlap=${true} from=${() => d.from} to=${() => d.to}
|
|
86
|
+
tone=${() => d.tones} gold=${() => d.golds}>${Segment}<//>
|
|
87
|
+
<//>
|
|
88
|
+
</div>`);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is normal Solid composition and retains rhp's slat design.
|
|
92
|
+
Keep the root outside `Show`, so rhp retains its identity, position, focus target, and keyboard order.
|
|
93
|
+
The contents mount when the row receives a position and unmount when it loses that position.
|
|
94
|
+
Check leaving animations against when the chart's own order actually becomes `null`.
|
|
95
|
+
|
|
96
|
+
State owned by the slat root survives this cycle.
|
|
97
|
+
State, native input values, subscriptions, and effects owned inside the conditional content follow Solid's mount and cleanup lifecycle.
|
|
98
|
+
Keep state that must survive in the root or application, or leave that content mounted.
|
|
99
|
+
Showing many rows at once can cost more because their blocks must be created then.
|
|
100
|
+
Measure that action before deciding whether this tradeoff suits the chart.
|
|
101
|
+
|
|
102
|
+
Automatic gutters already follow the visible edge labels in the tested eager and conditional versions.
|
|
103
|
+
Both widened when a longer label became visible, with identical widths before and after filtering.
|
|
104
|
+
Conditional rendering does not by itself require fixed room.
|
|
105
|
+
Verify the chart's gutter geometry, including its own sizing constraints, when changing which labels are mounted.
|
|
106
|
+
|
|
107
|
+
## Verified example and limits
|
|
108
|
+
|
|
109
|
+
The vocabulary article already uses these patterns.
|
|
110
|
+
A review on 2026-10-16 compared four isolated variants of the article at source commit `4101fdb`: eager contents with per-mark color and dimming updates, then each optimization added in turn.
|
|
111
|
+
The test used Chromium and WebKit, widths 1280 and 390, and dark and light themes, for 32 configurations.
|
|
112
|
+
|
|
113
|
+
| Measurement | Earlier implementation | Optimized implementation |
|
|
114
|
+
|---|---:|---:|
|
|
115
|
+
| Style writes for one tested shelf hover | 14 | 0 |
|
|
116
|
+
| Shelf class changes for that hover | 1 | 1 |
|
|
117
|
+
| Changed DOM nodes in the era chart after opening a rapper | 122 | 9 |
|
|
118
|
+
| Page elements at desktop rest | 3,101 | 1,449 |
|
|
119
|
+
| Descendants retained inside hidden ranking slats | 1,652 | 0 |
|
|
120
|
+
| Retained JS heap after collection, Chromium desktop | 16.7 MB | about 10 MB |
|
|
121
|
+
|
|
122
|
+
These figures describe this page and these actions, not a complexity or frame-rate guarantee.
|
|
123
|
+
Visible mark geometry, computed colors and opacity, readout text, tab order, and accessible breakdown content matched in seven states: rest, hover, open breakdown, normalized measure, era filter, show all, and search.
|
|
124
|
+
The hovered ranking screenshots matched pixel for pixel in all eight engine/width/theme combinations.
|
|
125
|
+
Color transition values matched at 0, 100, and 200ms, and face dimming matched at 0, 100, and 250ms in both engines.
|
|
126
|
+
A separate lifecycle fixture verified root identity, root-owned state, arrow-key navigation, nested state reset, native input reset, and cleanup behavior in both engines.
|
|
127
|
+
The color-variable example above was also checked with reactive palette changes, an outline that consumes the mark color, and a background image in both engines.
|
|
128
|
+
|
|
129
|
+
In five fresh-page trials per action at 390px with Chromium CPU throttled 4 times, showing all 149 rows took a median 175ms with mounted contents and 258ms with conditional contents, measured from the action through two animation frames with reduced motion.
|
|
130
|
+
Search medians were 239ms and 217ms respectively, with overlapping ranges (206 to 310ms and 195 to 310ms), so these trials establish no consistent search advantage or regression.
|
|
131
|
+
CPU throttling is a reproducible browser test setting, not a measurement on a particular phone.
|
|
132
|
+
|
|
133
|
+
## Validate the specific chart
|
|
134
|
+
|
|
135
|
+
Compare before and after at the same viewport, theme, scroll position, and interaction state.
|
|
136
|
+
Check geometry, colors, labels, readouts, keyboard navigation, touch actions, and accessibility content.
|
|
137
|
+
For motion, pause animations at equal times from their starts and compare intermediate frames as well as settled output.
|
|
138
|
+
Use a before-versus-before control when screenshots vary between identical builds.
|
|
139
|
+
Run the checker again after the change.
|
|
140
|
+
|
|
141
|
+
Use the class patterns when they reduce measured work and preserve the chart's styling and state semantics.
|
|
142
|
+
Choose conditional contents when their lifecycle and reveal cost fit the chart, and explain a material tradeoff to the user before changing existing behavior.
|
|
143
|
+
No pattern here removes an rhp capability or calls for replacing slats with a different rendering model.
|