@bezda/rhp-mcp 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -2
- package/bin/rhp-mcp.js +129 -0
- package/package.json +62 -4
- package/skill/SKILL.md +450 -0
- package/skill/recipes/area.html +156 -0
- package/skill/recipes/bar.html +145 -0
- package/skill/recipes/box-plot.html +164 -0
- package/skill/recipes/bubble.html +188 -0
- package/skill/recipes/bullet.html +138 -0
- package/skill/recipes/candlestick.html +179 -0
- package/skill/recipes/column.html +138 -0
- package/skill/recipes/dial.html +349 -0
- package/skill/recipes/diverging-bars.html +150 -0
- package/skill/recipes/donut.html +231 -0
- package/skill/recipes/dumbbell.html +137 -0
- package/skill/recipes/gantt.html +196 -0
- package/skill/recipes/grouped-bars.html +142 -0
- package/skill/recipes/heatmap.html +183 -0
- package/skill/recipes/histogram.html +199 -0
- package/skill/recipes/line.html +170 -0
- package/skill/recipes/live.html +255 -0
- package/skill/recipes/log-line.html +493 -0
- package/skill/recipes/lollipop.html +152 -0
- package/skill/recipes/multi-line.html +228 -0
- package/skill/recipes/pyramid.html +162 -0
- package/skill/recipes/race.html +278 -0
- package/skill/recipes/radial-bars.html +157 -0
- package/skill/recipes/scatter.html +199 -0
- package/skill/recipes/silhouettes.html +352 -0
- package/skill/recipes/slope.html +199 -0
- package/skill/recipes/sparklines.html +175 -0
- package/skill/recipes/stacked-100.html +158 -0
- package/skill/recipes/stacked-bars.html +150 -0
- package/skill/recipes/strip.html +221 -0
- package/skill/recipes/unit-stack.html +276 -0
- package/skill/recipes/violin.html +169 -0
- package/skill/recipes/waffle.html +165 -0
- package/skill/recipes/waterfall.html +127 -0
- package/skill/references/api.md +1684 -0
- package/skill/references/design.md +740 -0
- package/skill/references/environments.md +1577 -0
- package/skill/references/forms.md +197 -0
- package/skill/references/interaction.md +794 -0
- package/skill/references/looks.md +267 -0
- package/skill/references/pitfalls.md +202 -0
- package/src/check/README.md +176 -0
- package/src/check/browser.js +92 -0
- package/src/check/build.js +439 -0
- package/src/check/colors.js +324 -0
- package/src/check/findings.js +247 -0
- package/src/check/index.js +508 -0
- package/src/check/inpage.js +1332 -0
- package/src/check/interact.js +266 -0
- package/src/check/png.js +98 -0
- package/src/check/report.js +106 -0
- package/src/check/source.js +387 -0
- package/src/check/sourcemap.js +75 -0
- package/src/guard/note.js +30 -0
- package/src/guard/render.js +17 -0
- package/src/guard/rhp.js +612 -0
- package/src/guard/rules.js +125 -0
- package/src/server.js +266 -0
- package/src/skill.js +140 -0
|
@@ -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,202 @@
|
|
|
1
|
+
# rhp pitfalls: symptoms, causes and fixes
|
|
2
|
+
|
|
3
|
+
Use this file when a chart does not look or behave as it should, or when the checker reports a finding you do not understand.
|
|
4
|
+
Find the symptom, then try its causes in order.
|
|
5
|
+
Every cause here was reproduced with rhp 2.0.1; the numbers in brackets point to the "Gotchas" list at the end of [api.md](api.md).
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- Mistakes that come from other libraries
|
|
10
|
+
- Nothing draws, or the chart is empty
|
|
11
|
+
- Bars, dots or labels in the wrong place
|
|
12
|
+
- Text sticks out, is cut off or overlaps
|
|
13
|
+
- The chart is tiny, squashed or the wrong size
|
|
14
|
+
- Values never update, or the chart jumps
|
|
15
|
+
- A click, hover, tap or key does nothing, or the wrong thing
|
|
16
|
+
- The layout jumps when the reader interacts
|
|
17
|
+
- Text and layers laid over the chart
|
|
18
|
+
- Styles or colors do not apply
|
|
19
|
+
- Console warnings rhp prints
|
|
20
|
+
- Mistakes in the work itself
|
|
21
|
+
|
|
22
|
+
## Mistakes that come from other libraries
|
|
23
|
+
|
|
24
|
+
rhp is not configured with an options object, and none of these exist: `<BarChart data=...>`, `<Chart type="bar">`, `series: [{ data }]`, `new Chart(ctx, config)`, `xAxis`, `yAxis`, `tooltip: { ... }`, `legend: { ... }`.
|
|
25
|
+
A chart is a `Chart` holding `Plot`s, each Plot draws one slat per row of data, and the slat is a function you write from blocks.
|
|
26
|
+
Start from a recipe, never from what another library does.
|
|
27
|
+
|
|
28
|
+
- **rhp 1 code** (`react-html-plots`, Legend-State observables, `@bezda/rhp-core`, `BarPlot`, `useObservable`): a different, retired library. Use rhp 2 only, from `@bezda/rhp@2`.
|
|
29
|
+
- **React JSX around rhp components** (`<Bar to={d.sold} />` inside a React component): React does not render Solid components. In React, write the chart with the `html` template from `@bezda/rhp/standalone` and mount it into an element (environments.md, React).
|
|
30
|
+
- **`solid-js` imported in a React, Vue or plain page** next to `@bezda/rhp/standalone`: two copies of Solid, and reactivity silently stops between them. Import everything from the standalone module.
|
|
31
|
+
- **`useState` or `useEffect` inside an rhp chart component**: use `createSignal`, `createMemo`, `createEffect` and `onCleanup` from the same module as rhp.
|
|
32
|
+
- **A tooltip library or an SVG overlay**: rhp charts are HTML. Show a readout with a signal and `Show` inside the slat, or in the poster's dek (interaction.md).
|
|
33
|
+
|
|
34
|
+
## Nothing draws, or the chart is empty
|
|
35
|
+
|
|
36
|
+
1. **The module failed to load.** An error like "does not provide an export named createSelector": the standalone module has no `Dynamic`, `Portal`, `createResource` or `ErrorBoundary`, and rhp 2.0.1 also lacks `createSelector`, `createComputed`, `on`, `mergeProps`, `splitProps`, `Switch` and `Match`, which 2.0.2 adds [28]. Return the template from a function for `Dynamic`, `render()` into the other element for `Portal`, fetch in `onMount` for `createResource`, and compare in a function (`on=${(d) => on() === d.index}`) on any version.
|
|
37
|
+
2. **The import map is missing or below the module script.** It must come before any `<script type="module">` and map `@bezda/rhp/standalone` to `https://cdn.jsdelivr.net/npm/@bezda/rhp@2/dist/standalone.js`.
|
|
38
|
+
3. **`render` got the result instead of a function.** Write ``render(() => html`<${MyChart} />`, element)``.
|
|
39
|
+
4. **The slat returns two elements, text, or nothing** [30]. Return one root element (`<div>`) holding the blocks.
|
|
40
|
+
5. **The Plot is outside a Chart** [29], or the Plot got the bare function instead of what `slat()` returned (the slat then has no CSS or layout).
|
|
41
|
+
6. **Data groups are empty** or the Plot has none of the names the slat reads: every slat reads `undefined` and draws at the start of the scale [16]. Check the names: `d.sold` needs a Plot prop `sold`; a list of objects goes in `rows`, not `data` [7].
|
|
42
|
+
|
|
43
|
+
## Bars, dots or labels in the wrong place
|
|
44
|
+
|
|
45
|
+
1. **The scale does not hold the data** [13]: bars are cut at the end, dots and ticks land outside the plot.
|
|
46
|
+
Build it from the data: `const s = nice(Math.min(0, ...values), Math.max(0, ...values))`, then `scale=${[s.min, s.max]} ticks=${s.ticks}`.
|
|
47
|
+
2. **Negative values on a scale that starts at 0**: a value below the scale's min draws nothing; the scale above takes its min below 0.
|
|
48
|
+
A Bar whose `to` is below its `from` runs backward.
|
|
49
|
+
3. **Area or Line points past the scale squeeze the whole outline** [14]: keep every x inside the scale; give `density()` a `domain` inside it.
|
|
50
|
+
4. **A reversed or equal scale** (`[100, 0]`, `[5, 5]`) draws nothing [15]. For bars that run the other way, use negative values and print them with `format=${(v) => Math.abs(v)}`.
|
|
51
|
+
5. **NaN, undefined or null** draw at the scale's start and print "NaN" [16]: clean or filter the data first.
|
|
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()}`.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
60
|
+
|
|
61
|
+
## Text sticks out, is cut off or overlaps
|
|
62
|
+
|
|
63
|
+
1. **Value labels past the bars stick out at the end**: `room.end` is too small, or a `room` you gave dropped the defaults [10]. Give every side you use, in px: `room: { start: 120, end: 56 }`.
|
|
64
|
+
2. **Names are cut off or stick out at the start**: give `room.start` in px, or `"auto"` with the edge Labels as direct children of the slat's root [11]; cap long names with `max-width` and an ellipsis in the slat CSS, or let them wrap.
|
|
65
|
+
3. **Names under a vertical chart end in "..."** [25]: they are as wide as their columns. Shorten them, let them wrap (`.name:vertical { white-space: normal; }`), or make the chart horizontal on phones.
|
|
66
|
+
4. **Axis numbers stick out** [12]: the axis has fixed room, so keep `format` short ("1.2M", not "1,200,000 people").
|
|
67
|
+
5. **Labels of neighboring slats overlap**: the slats are too thin for their text. Raise `thickness`, shorten the labels, show fewer of them (only the highlighted slat, or every other one), or turn the chart.
|
|
68
|
+
6. **A label overlaps its own bar's end**: give the slat CSS `--rhp-label-gap` (the space between a Label and its value).
|
|
69
|
+
7. **The value label of the longest bar runs into the next column or out of the chart**: leave headroom on the scale (build it from about 1.1 times the largest value with `nice()`), or give `room.end` its width.
|
|
70
|
+
8. **Wide numbers on a cross axis stick out on the left**: the cross axis gets 42px for its numbers; numbers such as 30,000 need the slat type's `room.start` at about 48px.
|
|
71
|
+
9. **A value Label over the last column of a vertical chart sticks out**: give `room.after` half the label's width.
|
|
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.
|
|
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).
|
|
80
|
+
|
|
81
|
+
## The chart is tiny, squashed or the wrong size
|
|
82
|
+
|
|
83
|
+
1. **It is 148px wide with an empty plot** [24]: it sits in a flex row, an `inline-block`, a float or a `fit-content` box. Give its wrapper `flex: 1; min-width: 0`, or a width.
|
|
84
|
+
2. **A vertical chart is a flat strip**: it is 240px tall by default; set `height`.
|
|
85
|
+
3. **A horizontal chart's slats are 32px thick and the chart is too tall or too short**: set `thickness` on the slat type, or `height` on the Chart to make slats share it.
|
|
86
|
+
4. **The chart keeps a shape on every screen**: `aspect` on the Chart keeps width over height, and ignores `height` (rhp warns).
|
|
87
|
+
|
|
88
|
+
## Values never update, or the chart jumps
|
|
89
|
+
|
|
90
|
+
1. **`${d.sold}` was read once** [3]: wrap it, `${() => d.sold}`; never destructure `d`.
|
|
91
|
+
2. **The Plot got a signal's value once**: pass the accessor (`sold=${sold}`) or a function (`sold=${() => data().sold}`), not `sold=${sold()}` at the top of a component that runs once.
|
|
92
|
+
3. **The Chart is `static`** [33]: a static chart ignores signals other than its data. Remove `static` from interactive charts.
|
|
93
|
+
4. **Slats swap data when rows are removed**: give the Plot a `key` (`key="city"`).
|
|
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.
|
|
95
|
+
6. **Numbers show long decimals while they move** [26]: in the JS version (`animate`) values are fractional mid-way; round what you print.
|
|
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.
|
|
102
|
+
|
|
103
|
+
## A click, hover, tap or key does nothing, or the wrong thing
|
|
104
|
+
|
|
105
|
+
1. **A handler on a component has no parameter** [2]: `onClick=${() => pick()}` runs while drawing and is never attached. Write `onClick=${(e) => pick()}`. This holds for blocks and the Poster; plain elements are fine, and the Chart takes no handler at all (next item).
|
|
106
|
+
2. **The handler is on the Chart** [6]: the Chart drops handlers, `data-*` and `title`. Put them on the Poster or a wrapper element.
|
|
107
|
+
3. **A bare boolean** [1]: `<${Plot} keyboard>` passes an empty string, which is false. Write `keyboard=${true}` (also `overlap`, `static`, `animate`, `mirror`, `fill`, `smooth`).
|
|
108
|
+
4. **An overlay Plot covers the slats under it**: give it `style=${{ "pointer-events": "none" }}` (an object: a string is dropped on Chart and Plot [4]).
|
|
109
|
+
5. **The row is lost**: put `data-row=${() => d.index}` on the slat's root and find it with `e.target.closest("[data-row]")` from one handler on the poster.
|
|
110
|
+
6. **A scroll on a phone changes the pick, or the readout flickers between slats**: the pick runs on `pointerdown` or on a finger's `pointermove` (every scroll starts with both), or it clears when the pointer is on no slat.
|
|
111
|
+
Pick on `pointermove` only for a mouse or a pen (`e.pointerType !== "touch"`), on `click` for a tap or a click, and on `focusin` for the keyboard; leave the pick as it is on no slat, and clear it on `pointerleave` and `focusout` (interaction.md, section 2).
|
|
112
|
+
7. **The keyboard cannot reach it**: add `keyboard=${true}` to the Plot whose slats are interactive, and handle `focusin` like `pointermove`.
|
|
113
|
+
8. **A focus handler throws on some elements**: `e.target.closest("[data-row]")` can return null; check it before reading `dataset`.
|
|
114
|
+
9. **The chart vanished for screen readers**: `role="slider"` makes everything inside it presentational, so it must never wrap the chart; put it on its own element.
|
|
115
|
+
10. **A slider has no name for screen readers**: an `<output>` inside the `<label>` takes the label from the input, because an output is labelable too. Show the value in a `<span>` or `<b>` with `aria-hidden="true"` and give the input `aria-valuetext`.
|
|
116
|
+
|
|
117
|
+
## The layout jumps when the reader interacts
|
|
118
|
+
|
|
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.
|
|
120
|
+
|
|
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.
|
|
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").
|
|
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.
|
|
150
|
+
|
|
151
|
+
## Styles or colors do not apply
|
|
152
|
+
|
|
153
|
+
1. **Page CSS aimed at bars, labels or the axis** [21]: it cannot change their color, background, font, border, radius, shadow, padding or transition, even with `!important`. Move those rules into the slat type's `css`.
|
|
154
|
+
2. **`className`** [5]: on a block it replaces rhp's class and the block vanishes. Use `class`.
|
|
155
|
+
3. **The chart ignores the page's font or colors** [20]: give them in the Chart's `theme` (`font`, `ink`, `muted`, `grid`, `series`, ...).
|
|
156
|
+
4. **`var(--brand)` as a color warns** [20]: put the brand colors in the theme instead.
|
|
157
|
+
5. **Rows 5 and 6 take the first series color** [18]: `series()` counts six colors; with a shorter theme list call `series(n)`.
|
|
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.
|
|
159
|
+
7. **The poster looks unstyled** [31]: the Poster brings no CSS. Style `.poster`, `figcaption`, `.kicker`, `.headline`, `.dek` and `.note` in the page.
|
|
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`.
|
|
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.
|
|
162
|
+
10. **Padding on the chart's element does nothing**: rhp's room owns it. Pad a wrapper instead.
|
|
163
|
+
11. **A white label is unreadable on some segments**: a label needs 4.5:1 on each segment's color. Darkening a segment can break the color-blind check against its neighbor, so switch that label to the ink color instead.
|
|
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.
|
|
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.
|
|
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).
|
|
169
|
+
|
|
170
|
+
## Console warnings rhp prints
|
|
171
|
+
|
|
172
|
+
Each is worth fixing [34]:
|
|
173
|
+
|
|
174
|
+
| Warning starts with | Fix |
|
|
175
|
+
|---|---|
|
|
176
|
+
| "a Bar is a slat's root here" (or Dot, Label...) | wrap the block in an element: ``(d) => html`<div><${Bar} .../></div>` `` |
|
|
177
|
+
| "an edge Label is inside another element" | make the edge Label a direct child of the slat's root, or give room in px |
|
|
178
|
+
| "aspect takes a number above 0" | `aspect=${16 / 9}` |
|
|
179
|
+
| "a Chart with an aspect takes its height from its width" | remove `height` |
|
|
180
|
+
| "a flat Chart with an aspect has rows with a thickness" | remove `thickness` from the slat type |
|
|
181
|
+
| "... reads a page variable; use a theme key" | put the color in the Chart's `theme` and use its key |
|
|
182
|
+
| "--rhp-radius takes one length" | one length, or `--rhp-start-radius` and `--rhp-end-radius` |
|
|
183
|
+
| "unknown ease" | a CSS name, four cubic-bezier numbers, or a function |
|
|
184
|
+
| "linkedCss() was called, but this page doesn't link" | link `@bezda/rhp/rhp.css`, or remove `linkedCss()` |
|
|
185
|
+
|
|
186
|
+
## Mistakes in the work itself
|
|
187
|
+
|
|
188
|
+
These are not rhp errors, but they decide whether the user gets what they asked for.
|
|
189
|
+
|
|
190
|
+
- **Dropping or changing a request**: every explicit request in the brief is in the result, exactly (their colors, their title, their sort order, "no poster"). A warning is fixed without removing a requested feature.
|
|
191
|
+
- **Invented numbers presented as fact**: data you supplied names its source and year, or the note says "Illustrative data".
|
|
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.
|
|
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.
|
|
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.
|
|
199
|
+
- **A magazine poster dropped into an app**: inside an existing app, use the app's fonts and colors (design.md).
|
|
200
|
+
- **Hover-only information**: everything a hover shows also shows on focus and on tap.
|
|
201
|
+
- **Not looking at the phone screenshot**: most layout problems appear only at 390px.
|
|
202
|
+
- **Random data in a deliverable**: use fixed numbers unless the user asked for live or generated data.
|