@bezda/rhp-mcp 0.1.9 → 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 +4 -4
- package/skill/references/api.md +34 -6
- package/skill/references/design.md +5 -2
- package/skill/references/pitfalls.md +3 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bezda/rhp-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Make charts with rhp from any AI agent: an MCP server with rhp's guide, references and recipes, and a checker that renders a chart in a browser and reports what is wrong, with screenshots",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rhp",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@babel/core": "^7.29.7",
|
|
54
|
-
"@bezda/rhp": "^2.
|
|
54
|
+
"@bezda/rhp": "^2.2.0",
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
56
56
|
"babel-preset-solid": "^1.9.15",
|
|
57
57
|
"esbuild": "^0.28.2",
|
package/skill/SKILL.md
CHANGED
|
@@ -269,7 +269,7 @@ Download saves the full poster, including the headline and note, even when the v
|
|
|
269
269
|
- A request about the user's own data with none given ("chart my spending") gets illustrative numbers in one obvious data block at the top of the script, with the note's text in the same block, so the user changes both in one place.
|
|
270
270
|
The headline and the dek compute their numbers from that data, so they stay true for the user's own.
|
|
271
271
|
The handoff says exactly how to put their own numbers in, or offers to read a file they point to.
|
|
272
|
-
- Compute the scale from the data: `nice(
|
|
272
|
+
- Compute the scale from the data: `nice(...extent([0, ...values]))`, which skips missing values (rule 2 below).
|
|
273
273
|
- Compute the ranks, the leader and every number in the headline and the dek from a sorted copy of the data, never from its input order: a Plot's `order` sorts only what it draws.
|
|
274
274
|
- Text computed from the data (the headline, the dek, a readout, an `aria-label`) must stay true for any data it may get:
|
|
275
275
|
- A change from a zero or missing base prints "new" or "up from none", never ∞%, NaN% or "as many as"; a change in a rate is in percentage points ("down 0.8 points").
|
|
@@ -375,12 +375,12 @@ A complete chart in the plain HTML format (the format of every recipe), with a p
|
|
|
375
375
|
<body>
|
|
376
376
|
<div id="chart"></div>
|
|
377
377
|
<script type="module">
|
|
378
|
-
import { Chart, Plot, Bar, Label, Poster, slat, nice, html, render, createSignal } from "@bezda/rhp/standalone";
|
|
378
|
+
import { Chart, Plot, Bar, Label, Poster, slat, nice, extent, html, render, createSignal } from "@bezda/rhp/standalone";
|
|
379
379
|
|
|
380
380
|
// Data, and what depends on it
|
|
381
381
|
const fruit = ["Bananas", "Apples", "Cherries", "Pears", "Plums"];
|
|
382
382
|
const sold = [18, 12, 7, 5, 3];
|
|
383
|
-
const scale = nice(
|
|
383
|
+
const scale = nice(...extent([0, ...sold])); // 0 to 20, ticks every 5
|
|
384
384
|
const lead = sold.indexOf(Math.max(...sold)); // the fruit the headline names: lit until the reader picks another
|
|
385
385
|
|
|
386
386
|
// Slat type: one slat per row; its CSS reaches only these slats
|
|
@@ -449,7 +449,7 @@ Data:
|
|
|
449
449
|
|
|
450
450
|
1. **Data names must match.** A slat reads `d.sold` only if the Plot has a prop `sold`. A list of objects goes in `rows={[...]}`, never `data={...}`: `data` would just be a column named "data". Any prop a Plot does not know (`id`, `title`, `onClick`) is a column too, and an array is always a column, item i for row i: to give every slat the same list, pass a function of the row (`shown=${(d) => shown()}`).
|
|
451
451
|
2. **The scale must hold the data.** `scale={[min, max]}` with min below max covers every value: a Bar past it is cut off, and Dots, Ticks, Places and the points of an Area or a Line past it land outside or squeeze the outline.
|
|
452
|
-
Build it from the data, `const s = nice(
|
|
452
|
+
Build it from the data, `const s = nice(...extent([0, ...values]))`, which keeps 0, holds negative values and skips missing ones, and give the Chart `scale=${[s.min, s.max]} ticks=${s.ticks}`.
|
|
453
453
|
3. **Rows that come and go need a `key`** on the Plot (`key="city"`), or slats swap data instead of leaving. `d.index` can change then, so read it in a function: `data-row=${() => d.index}`.
|
|
454
454
|
4. **Format numbers** with `Intl.NumberFormat` (thousands, units, percentages, currency) and use `font-variant-numeric: tabular-nums` where numbers line up. In the JS version (`animate`), values in between are fractional: round what you print.
|
|
455
455
|
|
package/skill/references/api.md
CHANGED
|
@@ -659,6 +659,31 @@ const ChangeChart = () => html`
|
|
|
659
659
|
render(ChangeChart, document.getElementById("chart"));
|
|
660
660
|
```
|
|
661
661
|
|
|
662
|
+
### Missing data
|
|
663
|
+
|
|
664
|
+
`null`, `undefined`, `NaN` and infinite numbers are missing values, and rhp never draws one as 0.
|
|
665
|
+
|
|
666
|
+
- A block given a missing value for its place draws nothing: a Bar's `from` or `to`, the `at` or `cross` of a Dot, Tick, Label or Place, a Cell's `value`.
|
|
667
|
+
Its slat and the slat's other blocks stay.
|
|
668
|
+
- Only given values count: a Bar with no `from` starts at 0, and a Label at an `edge` is always drawn.
|
|
669
|
+
- A Line or an Area breaks at a point that is `null` or has a missing x or y, and draws each run of two or more points.
|
|
670
|
+
To join the line across a gap, leave the point out: `points.filter(([, y]) => y != null)`.
|
|
671
|
+
- The helpers leave missing values out, and `sortBy` puts them last in either direction (section 9).
|
|
672
|
+
- A value that comes back is drawn where it is, without moving from where it was.
|
|
673
|
+
- Read a CSV's empty cells as `null`, never 0: `(s) => (s.trim() === "" ? null : +s)`.
|
|
674
|
+
|
|
675
|
+
Say "no data" where the value would be, in the slat and in any readout:
|
|
676
|
+
|
|
677
|
+
```js
|
|
678
|
+
const shown = (v) => (Number.isFinite(v) ? v.toLocaleString() : "no data");
|
|
679
|
+
const Country = slat({ room: { start: 90, end: 60 } }, (d) => html`
|
|
680
|
+
<div>
|
|
681
|
+
<${Label} edge="start">${() => d.country}<//>
|
|
682
|
+
<${Bar} to=${() => d.rate} />
|
|
683
|
+
<${Label} edge="end">${() => shown(d.rate)}<//>
|
|
684
|
+
</div>`);
|
|
685
|
+
```
|
|
686
|
+
|
|
662
687
|
### ManyDots (experimental point collection)
|
|
663
688
|
|
|
664
689
|
`ManyDots` draws a bulk collection of points directly inside a `Chart` with `scale` and `cross`.
|
|
@@ -947,15 +972,15 @@ When their input changes, call them inside `createMemo` so they run once per cha
|
|
|
947
972
|
| Helper | Returns | Example (real output) |
|
|
948
973
|
|---|---|---|
|
|
949
974
|
| `nice(lo, hi, count = 5)` | a round scale `{ min, max, step, ticks }` | `nice(0, 87)` → `{ min: 0, max: 100, step: 20, ticks: [0, 20, 40, 60, 80, 100] }`; `nice(-23, 41)` → min -40, max 60, step 20 |
|
|
950
|
-
| `extent(values)` | `[smallest, largest]` (`[Infinity, -Infinity]` when empty) | `extent([5, -2, 9])` → `[-2, 9]` |
|
|
975
|
+
| `extent(values)` | `[smallest, largest]` (`[Infinity, -Infinity]` when empty) | `extent([5, null, -2, 9])` → `[-2, 9]` |
|
|
951
976
|
| `every(step, { ends })` | a ticks function of `[min, max]` (a step not above 0 throws) | `every(25)([0, 100])` → `[0, 25, 50, 75, 100]`; `every(5, { ends: true })([0, 27])` → `[0, 5, …, 25, 27]` |
|
|
952
|
-
| `stackUp(values)` | `{ from, to }`: positives stack up from 0, negatives down | `stackUp([5, -3, 4])` → from `[0, 0, 5]`, to `[5, -3, 9]` |
|
|
977
|
+
| `stackUp(values)` | `{ from, to }`: positives stack up from 0, negatives down; a missing value's are `null` | `stackUp([5, -3, 4])` → from `[0, 0, 5]`, to `[5, -3, 9]` |
|
|
953
978
|
| `shares(values, total = 100)` | each value's share of the sum of absolute values, scaled to `total` | `shares([1, 1, 2])` → `[25, 25, 50]` |
|
|
954
979
|
| `running(changes)` | `{ from, to }`: running totals (waterfall) | `running([100, -30, 45])` → from `[0, 100, 70]`, to `[100, 70, 115]` |
|
|
955
980
|
| `summary(samples)` | `{ min, q1, median, q3, max, low, high, mean, outliers }`; low and high are the whisker ends (within 1.5 × IQR) | `summary([1, …, 9, 40])` → median 5.5, high 9, outliers `[40]` |
|
|
956
981
|
| `bins(samples, { domain, count = 10 })` | `{ x0, x1, tally }`: bin k covers x0[k] to x1[k] | `bins([1, 2, 2, 3, 9, 10], { count: 3 })` → x0 `[1, 4, 7]`, tally `[4, 0, 2]` |
|
|
957
982
|
| `density(samples, { domain, points = 40, bandwidth })` | `[[x, density], …]` for an Area (Gaussian kernel, Silverman's bandwidth) | keep `domain` inside the Chart's scale |
|
|
958
|
-
| `sortBy(key, "asc" or "desc")` | an `order` function; `key` is a group name or `(d) => value`; ties keep their places on screen | `order=${sortBy("sold", "desc")}` |
|
|
983
|
+
| `sortBy(key, "asc" or "desc")` | an `order` function; `key` is a group name or `(d) => value`; ties keep their places on screen, missing values go last | `order=${sortBy("sold", "desc")}` |
|
|
959
984
|
| `series(n = 6)` | a data group: `"series-1"`… by row index | `color=${series(4)}` |
|
|
960
985
|
| `cycle(list)` | a data group repeating `list` over the rows (does not set the number of rows) | `color=${cycle(["#111", "#999"])}` |
|
|
961
986
|
| `at(group, i)` | item i of a data group (a list wraps around, a single value is every row's) | `at([1, 2, 3], 4)` → `2` |
|
|
@@ -967,6 +992,8 @@ When their input changes, call them inside `createMemo` so they run once per cha
|
|
|
967
992
|
| `restyle(slatType, css)` | gives a slat type made with `css` new CSS; its slats restyle in place | for style editors |
|
|
968
993
|
| `linkedCss()` | tells rhp the page links `@bezda/rhp/rhp.css` itself | call before the first chart; without the link rhp warns and adds its CSS anyway |
|
|
969
994
|
|
|
995
|
+
`extent`, `summary`, `bins` and `density` leave missing values out (section 6, Missing data); `stackUp` and `running` give one `null` for `from` and `to`, and `shares` gives it no share (`null`).
|
|
996
|
+
|
|
970
997
|
A shape must start with `M`.
|
|
971
998
|
Further `M` commands start separate subpaths, allowing disconnected pieces or holes with the opposite winding direction.
|
|
972
999
|
These work with native CSS `shape()` and the polygon fallback; curves in the fallback retain the existing approximation.
|
|
@@ -1364,7 +1391,8 @@ const VoteChart = () => {
|
|
|
1364
1391
|
render(VoteChart, document.getElementById("chart"));
|
|
1365
1392
|
```
|
|
1366
1393
|
|
|
1367
|
-
|
|
1394
|
+
### A top-N race whose rows leave and come in
|
|
1395
|
+
|
|
1368
1396
|
A row that leaves the top N slides to place N, just below the last place, fades there, and is hidden at the next step; a row that comes in fades in at its place.
|
|
1369
1397
|
Rows a source leaves out (a table of the top four each year) are `null` in the data.
|
|
1370
1398
|
Three parts do it:
|
|
@@ -1635,8 +1663,8 @@ Every one of these was found by running rhp 2.0.1.
|
|
|
1635
1663
|
15. **`scale` needs `min < max`.**
|
|
1636
1664
|
An equal or reversed pair draws Bars with no length and no axis.
|
|
1637
1665
|
To run bars right to left, use negative values and `format=${(v) => Math.abs(v)}`.
|
|
1638
|
-
16. **
|
|
1639
|
-
|
|
1666
|
+
16. **A missing value draws nothing, and a Label prints it as "NaN" or "null".**
|
|
1667
|
+
Print "no data" for it (section 6, Missing data).
|
|
1640
1668
|
17. **A numeric Dot `size` makes an ellipse** (a share of the slat's width and height).
|
|
1641
1669
|
Give a length: `size="12px"`.
|
|
1642
1670
|
18. **`series()` counts 6 colors whatever the theme holds.**
|
|
@@ -619,6 +619,9 @@ const Reference = slat({
|
|
|
619
619
|
- **Units** once, where the reader looks first: in the dek, the Chart's `label` and the axis `format`.
|
|
620
620
|
A short symbol ($, %, °C) may repeat on every value; a unit word ("megawatts", "TWh") goes in the dek or a column head, or after the first value only.
|
|
621
621
|
- **Numbers** rounded to what matters (two or three significant figures), with the same decimals down a column.
|
|
622
|
+
- **Missing data said, never drawn as 0.** A row with no value keeps its name and says "no data" where its value would be (api.md, "Missing data"), and a line breaks at a gap.
|
|
623
|
+
Join a line across a gap only when the dek or note says the points are joined.
|
|
624
|
+
When many values are missing, the note says how many and why ("no survey in 2020").
|
|
622
625
|
|
|
623
626
|
## 8. Inside an existing app
|
|
624
627
|
|
|
@@ -708,12 +711,12 @@ Whatever they did not mention keeps the defaults: text at 4.5:1 and marks at 3:1
|
|
|
708
711
|
- A size that fits the content (about 480 to 640px wide for a few bars), centered with room around it (`#chart { max-width: 480px; margin: 0 auto; }` and `body { padding: 64px 16px; }`), not pinned to the top of an empty page.
|
|
709
712
|
|
|
710
713
|
```js
|
|
711
|
-
import { Chart, Plot, Bar, Label, Tick, slat, nice, html, render } from "@bezda/rhp/standalone";
|
|
714
|
+
import { Chart, Plot, Bar, Label, Tick, slat, nice, extent, html, render } from "@bezda/rhp/standalone";
|
|
712
715
|
|
|
713
716
|
// Data
|
|
714
717
|
const NAME = ["A", "B", "C"];
|
|
715
718
|
const VALUE = [3, 7, 5];
|
|
716
|
-
const scale = nice(
|
|
719
|
+
const scale = nice(...extent([0, ...VALUE]));
|
|
717
720
|
|
|
718
721
|
// Slat types: columns with their numbers (no axis numbers) and square feet, and a baseline, a Tick at 0 across the plot
|
|
719
722
|
const Column = slat({
|
|
@@ -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].
|
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
|
|