@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.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/skill/SKILL.md +5 -5
  3. package/skill/recipes/area.html +349 -138
  4. package/skill/recipes/bar.html +349 -138
  5. package/skill/recipes/box-plot.html +349 -138
  6. package/skill/recipes/bubble.html +349 -138
  7. package/skill/recipes/bullet.html +349 -138
  8. package/skill/recipes/candlestick.html +349 -138
  9. package/skill/recipes/column.html +349 -138
  10. package/skill/recipes/dial.html +349 -138
  11. package/skill/recipes/diverging-bars.html +349 -138
  12. package/skill/recipes/donut.html +349 -138
  13. package/skill/recipes/dumbbell.html +349 -138
  14. package/skill/recipes/gantt.html +349 -138
  15. package/skill/recipes/grouped-bars.html +349 -138
  16. package/skill/recipes/heatmap.html +349 -138
  17. package/skill/recipes/histogram.html +349 -138
  18. package/skill/recipes/line.html +349 -138
  19. package/skill/recipes/live.html +349 -138
  20. package/skill/recipes/log-line.html +349 -138
  21. package/skill/recipes/lollipop.html +349 -138
  22. package/skill/recipes/multi-line.html +349 -138
  23. package/skill/recipes/pyramid.html +349 -138
  24. package/skill/recipes/race.html +363 -139
  25. package/skill/recipes/radial-bars.html +349 -138
  26. package/skill/recipes/scatter.html +349 -138
  27. package/skill/recipes/silhouettes.html +349 -138
  28. package/skill/recipes/slope.html +349 -138
  29. package/skill/recipes/sparklines.html +349 -138
  30. package/skill/recipes/stacked-100.html +349 -138
  31. package/skill/recipes/stacked-bars.html +349 -138
  32. package/skill/recipes/strip.html +349 -138
  33. package/skill/recipes/tape.html +415 -165
  34. package/skill/recipes/unit-stack.html +349 -138
  35. package/skill/recipes/violin.html +349 -138
  36. package/skill/recipes/waffle.html +349 -138
  37. package/skill/recipes/waterfall.html +349 -138
  38. package/skill/references/api.md +34 -6
  39. package/skill/references/design.md +18 -2
  40. package/skill/references/download.md +372 -140
  41. package/skill/references/environments.md +1 -1
  42. package/skill/references/interaction.md +2 -0
  43. package/skill/references/pitfalls.md +4 -3
  44. package/src/check/README.md +2 -1
  45. package/src/check/findings.js +2 -0
  46. package/src/check/inpage.js +9 -2
  47. 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` 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].
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(Math.min(0, ...values), Math.max(0, ...values))`, then `scale=${[s.min, s.max]} ticks=${s.ticks}`.
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, undefined or null** draw at the scale's start and print "NaN" [16]: clean or filter the data first.
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.
@@ -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
- | `nan-value` | error | A block's number is NaN; a block's `at` or `to` is given but null or undefined (a row with no value, drawn at the start of the scale); a `--rhp-*` value of a block or the Chart came out as NaN, undefined or Infinity; or a text in a chart prints NaN, undefined or Infinity. | Check the data the value is worked out from; leave rows with no value out, or draw the block in a `Show`. |
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
 
@@ -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":
@@ -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 Chart's scale
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
- checkNumber(name, prop, v);
325
- // a place given with no value (a row with no data) is drawn at the start of the scale, with no error
326
- if (v == null && (prop === "at" || prop === "to") && given.includes(prop)) {
327
- note("error", "nan-value", `${name}'s ${prop} is ${v} (a row with no value), so rhp draws it at the start of the scale.`,
328
- `Leave rows with no value out of the data, or draw the ${name} only when there is one: <\${Show} when=\${() => d.value != null}>...<//>.`);
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
- const pairs = isList(points) && Array.from(points).every((p) => isList(p) && p.length >= 2);
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 bad = Array.from(points).findIndex((p) => !Number.isFinite(p[0]) || !Number.isFinite(p[1]));
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", Number.isNaN(points[bad][0]) || Number.isNaN(points[bad][1]) ? "nan-value" : "bad-prop", `${name}'s point ${bad} is [${points[bad][0]}, ${points[bad][1]}].`, "Give every point two finite numbers.");
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
- const unsorted = points.findIndex((p, i) => i > 0 && p[0] < points[i - 1][0]);
375
- if (unsorted > 0) note("error", "bad-prop", `Area's points are not sorted by x (point ${unsorted} comes before point ${unsorted - 1}).`, "Sort an Area's points by x.");
376
- if (points.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.");
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