@bezda/rhp-mcp 0.1.8 → 0.1.10
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/package.json +2 -2
- package/skill/SKILL.md +5 -5
- package/skill/recipes/area.html +349 -138
- package/skill/recipes/bar.html +349 -138
- package/skill/recipes/box-plot.html +349 -138
- package/skill/recipes/bubble.html +349 -138
- package/skill/recipes/bullet.html +349 -138
- package/skill/recipes/candlestick.html +349 -138
- package/skill/recipes/column.html +349 -138
- package/skill/recipes/dial.html +349 -138
- package/skill/recipes/diverging-bars.html +349 -138
- package/skill/recipes/donut.html +349 -138
- package/skill/recipes/dumbbell.html +349 -138
- package/skill/recipes/gantt.html +349 -138
- package/skill/recipes/grouped-bars.html +349 -138
- package/skill/recipes/heatmap.html +349 -138
- package/skill/recipes/histogram.html +349 -138
- package/skill/recipes/line.html +349 -138
- package/skill/recipes/live.html +349 -138
- package/skill/recipes/log-line.html +349 -138
- package/skill/recipes/lollipop.html +349 -138
- package/skill/recipes/multi-line.html +349 -138
- package/skill/recipes/pyramid.html +349 -138
- package/skill/recipes/race.html +363 -139
- package/skill/recipes/radial-bars.html +349 -138
- package/skill/recipes/scatter.html +349 -138
- package/skill/recipes/silhouettes.html +349 -138
- package/skill/recipes/slope.html +349 -138
- package/skill/recipes/sparklines.html +349 -138
- package/skill/recipes/stacked-100.html +349 -138
- package/skill/recipes/stacked-bars.html +349 -138
- package/skill/recipes/strip.html +349 -138
- package/skill/recipes/tape.html +415 -165
- package/skill/recipes/unit-stack.html +349 -138
- package/skill/recipes/violin.html +349 -138
- package/skill/recipes/waffle.html +349 -138
- package/skill/recipes/waterfall.html +349 -138
- package/skill/references/api.md +34 -6
- package/skill/references/design.md +18 -2
- package/skill/references/download.md +372 -140
- package/skill/references/environments.md +1 -1
- package/skill/references/interaction.md +2 -0
- package/skill/references/pitfalls.md +4 -3
- package/src/check/README.md +2 -1
- package/src/check/findings.js +2 -0
- package/src/check/inpage.js +9 -2
- package/src/guard/rhp.js +31 -11
|
@@ -738,6 +738,8 @@ render(() => html`<${League} />`, document.getElementById("chart"));
|
|
|
738
738
|
The output keeps one width (`min-width` in the page's CSS): when "Round 9" became "Round 10", a wider output would shorten the slider and the handle would jump back.
|
|
739
739
|
- The button's text says what it will do (Play or Pause), so it has no `aria-pressed`.
|
|
740
740
|
- **Reduced motion:** rhp stops its own motion when the reader asks for less (values jump), and the playhead moves a whole round at a time, each round kept on screen for two seconds; your own CSS animations need `@media (prefers-reduced-motion: reduce) { … }`.
|
|
741
|
+
- **Tip, numbers outside the chart:** a year, a count or a readout that changes on every frame of the playback shifts the words beside it as its digits change width; consider tabular figures, or digits in equal-width slots when the font's tabular figures do not suit it (design.md, section 5).
|
|
742
|
+
- **Download:** give the poster a `playback` (`duration`, `position`, `seek`) once the chart is mounted, and its download icon offers the whole playback as an animated GIF beside the PNG; download.md has the lines, and the `race` recipe uses them.
|
|
741
743
|
|
|
742
744
|
**Option: snap the slider to important points.**
|
|
743
745
|
Snap only when the timeline has points worth reading exactly all along it: the `race` recipe snaps to its years, because each year is a measured value and the values between them are interpolated.
|
|
@@ -38,17 +38,17 @@ Start from a recipe, never from what another library does.
|
|
|
38
38
|
3. **`render` got the result instead of a function.** Write ``render(() => html`<${MyChart} />`, element)``.
|
|
39
39
|
4. **The slat returns two elements, text, or nothing** [30]. Return one root element (`<div>`) holding the blocks.
|
|
40
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
|
|
41
|
+
6. **Data groups are empty** or the Plot has none of the names the slat reads: every slat reads `undefined`, a missing value, and its blocks draw nothing [16]. Check the names: `d.sold` needs a Plot prop `sold`; a list of objects goes in `rows`, not `data` [7].
|
|
42
42
|
|
|
43
43
|
## Bars, dots or labels in the wrong place
|
|
44
44
|
|
|
45
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(
|
|
46
|
+
Build it from the data: `const s = nice(...extent([0, ...values]))`, then `scale=${[s.min, s.max]} ticks=${s.ticks}`.
|
|
47
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
48
|
A Bar whose `to` is below its `from` runs backward.
|
|
49
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
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,
|
|
51
|
+
5. **A Label prints "NaN", "null" or "undefined"**: its value is missing, and the blocks it places draw nothing [16]. Print "no data" for it (api.md, "Missing data"), and check the data when the value should be there.
|
|
52
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
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
54
|
8. **A Dot is an oval**: its `size` was a number [17].
|
|
@@ -127,6 +127,7 @@ The checker warns (`layout-jump`) when a control or the chart moves after one of
|
|
|
127
127
|
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").
|
|
128
128
|
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.
|
|
129
129
|
In a readout, tabular figures alone are not enough when the number of digits changes ("$654" to "$84,176") or a date's month names differ in width: start each part where nothing before it changes width (a grid with a column per part, or each part on its own line), or give the value a box with a `min-width` in `ch` for the longest value it can show, right-aligned. A label or a unit after a value on the same line ("8,101 tons in 2026") slides at every step of a pointer moving along the chart.
|
|
130
|
+
Tip: when the font's tabular figures do not suit the poster (a slashed or dotted zero), keep its own digits and set each in an equal-width slot (design.md, section 5); a right-aligned number that changes, such as a large date, needs the same.
|
|
130
131
|
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.
|
|
131
132
|
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).
|
|
132
133
|
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.
|
package/src/check/README.md
CHANGED
|
@@ -114,7 +114,8 @@ The tests read this table: every code in it must have a chart in `test/broken/`
|
|
|
114
114
|
| `plot-typo` | warning | A Plot prop that is a typo of a Plot setting, with a value of that setting's kind (`overlpa=${true}`): rhp takes it as a data group. | The setting it means. |
|
|
115
115
|
| `group-lengths` | warning | A Plot's data groups (lists of 3 or more, not palettes) have different lengths, and rhp repeats the shorter ones. | One item per row in every group. |
|
|
116
116
|
| `no-component` | error | A Solid, React or module file exports several components and no default one. | Export the chart as the default. |
|
|
117
|
-
| `
|
|
117
|
+
| `missing-value` | info | A block given a missing value (null, undefined, NaN or an infinite number) for its place (`from`, `to`, `at`, `cross`, `value`), which rhp doesn't draw; or a Line or Area point that is missing, where the outline breaks. | Say "no data" in the slat for a row with none; join a line over a gap by leaving the point out; otherwise check the data the value is worked out from. |
|
|
118
|
+
| `nan-value` | error | A block's number that is not a place (`across`, `peak`, `base`) is NaN; a `--rhp-*` value of a block rhp draws, or of the Chart, came out as NaN, undefined or Infinity; every value a chart would draw is missing, so it draws no data; or a text in a chart prints NaN, undefined or Infinity. | Check the data the value is worked out from (a misspelled name, numbers read as text, a division by zero), and format numbers before printing them. |
|
|
118
119
|
| `stale-read` | error | A slat read `d.x` once, while it was drawn and outside any function (`${d.x}` in an html template), and the interaction pass changed it: the slat still shows the old value. | `${() => d.x}`, and never destructure `d`. |
|
|
119
120
|
| `static-chart` | error | A static Chart or Plot whose per-row function (`on=${(d) => on() === d.index}`) changed during the interaction pass: static draws each row once, so the interaction changes nothing. | Take `static` off. |
|
|
120
121
|
|
package/src/check/findings.js
CHANGED
|
@@ -27,6 +27,7 @@ export const SEE = {
|
|
|
27
27
|
"value-past-scale": PITFALLS("Bars, dots or labels in the wrong place"),
|
|
28
28
|
"points-past-scale": PITFALLS("Bars, dots or labels in the wrong place"),
|
|
29
29
|
"nan-value": PITFALLS("Bars, dots or labels in the wrong place"),
|
|
30
|
+
"missing-value": 'api.md, "Missing data"',
|
|
30
31
|
"dot-size": PITFALLS("Bars, dots or labels in the wrong place"),
|
|
31
32
|
"sticks-out": PITFALLS("Text sticks out, is cut off or overlaps"),
|
|
32
33
|
"text-cut-off": PITFALLS("Text sticks out, is cut off or overlaps"),
|
|
@@ -95,6 +96,7 @@ export function fromProbe(f, context) {
|
|
|
95
96
|
fix: f.vertical ? "Give the Chart a height (height={320}) or an aspect (aspect={16 / 9}), and its parent the room for it." : "Give the chart more width: its parent is too narrow, or the room for labels (room start and end) takes it all.",
|
|
96
97
|
};
|
|
97
98
|
case "nan-value":
|
|
99
|
+
if (f.everything) return { level: "error", code: f.code, message: `Every value ${f.chart} would draw is missing (${f.count} blocks given null, undefined, NaN or an infinite number), so it draws no data.`, fix: "Check the data the values come from: a misspelled data group or key, numbers read as text and not converted, or a division by zero." };
|
|
98
100
|
if (f.printed) return { level: "error", code: f.code, message: `${f.what} in the chart shows ${q(f.text)}: a value it prints is missing or not a number.`, fix: "Check the data the label prints (a missing value, a misspelled name, a division by zero), and format numbers before printing them." };
|
|
99
101
|
return { level: "error", code: f.code, message: `${f.block}'s ${f.prop} is ${f.value}.`, fix: `Give ${f.block} a number for ${f.prop}: check the data it is computed from (a missing value, a name that is misspelled, a division by zero).` };
|
|
100
102
|
case "plot-outside-chart":
|
package/src/check/inpage.js
CHANGED
|
@@ -391,6 +391,12 @@ export function install() {
|
|
|
391
391
|
// or a Plot of slats with none, draws no data
|
|
392
392
|
const empty = (p) => ![...p.children].some((s) => !s.hidden);
|
|
393
393
|
if (plots.length && (plots.every(empty) || plots.some((p) => empty(p) && !p.hasAttribute("data-rhp-overlap")))) add({ code: "empty-plot", chart: describe(c) });
|
|
394
|
+
// every block that could show a value has a missing one: the chart draws no data (a misspelled name, values read
|
|
395
|
+
// as text), though each block alone is a note
|
|
396
|
+
const gone = c.querySelectorAll(".rhp-body [data-rhp-missing]").length;
|
|
397
|
+
if (gone && !c.querySelector(".rhp-body :is(.rhp-bar, .rhp-dot, .rhp-tick, .rhp-cell, .rhp-place, .rhp-label[data-rhp-at], .rhp-line, .rhp-area):not([data-rhp-missing])")) {
|
|
398
|
+
add({ code: "nan-value", everything: true, count: gone, chart: describe(c) });
|
|
399
|
+
}
|
|
394
400
|
for (const p of plots) {
|
|
395
401
|
const shown = [...p.children].filter((s) => !s.hidden);
|
|
396
402
|
if (!shown.length) continue;
|
|
@@ -409,10 +415,11 @@ export function install() {
|
|
|
409
415
|
}
|
|
410
416
|
}
|
|
411
417
|
|
|
412
|
-
// CSS variables that came out as NaN or undefined, and values past the
|
|
418
|
+
// CSS variables that came out as NaN or undefined (not a block rhp hides for a missing value), and values past the
|
|
419
|
+
// Chart's scale
|
|
413
420
|
const VALUE = { "--rhp-from": "from", "--rhp-to": "to", "--rhp-at": "at", "--rhp-value": "value", "--rhp-size": "size", "--rhp-across": "across", "--rhp-cross": "cross", "--rhp-min": "scale", "--rhp-max": "scale", "--rhp-thick": "thick" };
|
|
414
421
|
for (const c of all) {
|
|
415
|
-
for (const el of [c, ...c.querySelectorAll("[style]")]) {
|
|
422
|
+
for (const el of [c, ...c.querySelectorAll("[style]:not([data-rhp-missing])")]) {
|
|
416
423
|
for (let i = 0; i < el.style.length; i++) {
|
|
417
424
|
const name = el.style[i];
|
|
418
425
|
if (!name.startsWith("--rhp-")) continue;
|
package/src/guard/rhp.js
CHANGED
|
@@ -42,6 +42,11 @@ const show = (v) => {
|
|
|
42
42
|
const isColor = (c) => typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", c);
|
|
43
43
|
const SPAN = new Set(["from", "to", "value"]);
|
|
44
44
|
|
|
45
|
+
// A missing value (null, undefined, NaN or infinite), as rhp sees one: a block given one for its place draws nothing,
|
|
46
|
+
// and a Line or an Area breaks at a point with one
|
|
47
|
+
const missing = (v) => v == null || (typeof v === "number" && !Number.isFinite(v));
|
|
48
|
+
const PLACED = new Set(["from", "to", "at", "cross", "value"]);
|
|
49
|
+
|
|
45
50
|
// A number a block draws at: finite, or left out
|
|
46
51
|
function checkNumber(name, prop, v) {
|
|
47
52
|
|
|
@@ -321,12 +326,16 @@ function inspectBlock(name, props) {
|
|
|
321
326
|
|
|
322
327
|
for (const prop of spec.numbers) {
|
|
323
328
|
const v = read(props, prop);
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
+
// a place given a missing value (a row with no data) is not drawn, which is right, so it is a note: the slat should
|
|
330
|
+
// say there is no data (a Label at an edge is drawn there and doesn't use its at)
|
|
331
|
+
if (PLACED.has(prop) && given.includes(prop) && missing(v)) {
|
|
332
|
+
if (!(name === "Label" && read(props, "edge") != null)) {
|
|
333
|
+
note("info", "missing-value", `${name}'s ${prop} is ${v}, a missing value, so rhp draws no ${name} there.`,
|
|
334
|
+
`For a row with no data that is right: say so in the slat (a Label at its edge: "no data"). If the row should have a value, check the data it is worked out from (a misspelled name, an empty cell, a division by zero).`);
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
329
337
|
}
|
|
338
|
+
checkNumber(name, prop, v);
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
for (const prop of LENGTHS[name] ?? []) {
|
|
@@ -358,22 +367,33 @@ function checkPoints(name, points) {
|
|
|
358
367
|
|
|
359
368
|
if (points == null) return;
|
|
360
369
|
|
|
361
|
-
|
|
370
|
+
// (a point may be null: a missing one)
|
|
371
|
+
const list = isList(points) ? Array.from(points) : null;
|
|
372
|
+
const pairs = list && list.every((p) => p == null || (isList(p) && p.length >= 2));
|
|
362
373
|
if (!pairs) {
|
|
363
374
|
note("error", "bad-prop", `${name}'s points is ${show(points)}, not a list of [x, y] pairs.`, `Give points as [[x, y], [x, y], ...]: x on the value axis, y across the band.`);
|
|
364
375
|
return;
|
|
365
376
|
}
|
|
366
377
|
|
|
367
|
-
const
|
|
378
|
+
const shownPoint = (p) => (p == null ? String(p) : `[${p[0]}, ${p[1]}]`);
|
|
379
|
+
const bad = list.findIndex((p) => p != null && [p[0], p[1]].some((v) => !missing(v) && !Number.isFinite(v)));
|
|
368
380
|
if (bad >= 0) {
|
|
369
|
-
note("error",
|
|
381
|
+
note("error", "bad-prop", `${name}'s point ${bad} is ${shownPoint(list[bad])}, not two numbers.`, "Give every point two numbers (null for a value that is missing).");
|
|
370
382
|
return;
|
|
371
383
|
}
|
|
372
384
|
|
|
385
|
+
const gap = list.findIndex((p) => p == null || missing(p[0]) || missing(p[1]));
|
|
386
|
+
if (gap >= 0) {
|
|
387
|
+
note("info", "missing-value", `${name}'s point ${gap} is ${shownPoint(list[gap])}, a missing value, so the ${name === "Line" ? "line" : "shape"} breaks there.`,
|
|
388
|
+
`For a gap in the data that is right: a break shows the reader where it is. To join the points around it, leave the missing point out of the list. If it should have a value, check the data it is worked out from.`);
|
|
389
|
+
}
|
|
390
|
+
|
|
373
391
|
if (name === "Area") {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
392
|
+
// the points that are there, with their places in the list
|
|
393
|
+
const kept = list.map((p, i) => [i, p]).filter(([, p]) => p != null && !missing(p[0]) && !missing(p[1]));
|
|
394
|
+
const unsorted = kept.findIndex(([, p], k) => k > 0 && p[0] < kept[k - 1][1][0]);
|
|
395
|
+
if (unsorted > 0) note("error", "bad-prop", `Area's points are not sorted by x (point ${kept[unsorted][0]} comes before point ${kept[unsorted - 1][0]}).`, "Sort an Area's points by x.");
|
|
396
|
+
if (kept.some(([, p]) => p[1] < 0)) note("error", "bad-prop", "Area has a point with y below 0.", "An Area's y is 0 or more; for values on both sides use a Line on a cross scale.");
|
|
377
397
|
}
|
|
378
398
|
}
|
|
379
399
|
|