@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -2
  3. package/bin/rhp-mcp.js +129 -0
  4. package/package.json +62 -4
  5. package/skill/SKILL.md +450 -0
  6. package/skill/recipes/area.html +156 -0
  7. package/skill/recipes/bar.html +145 -0
  8. package/skill/recipes/box-plot.html +164 -0
  9. package/skill/recipes/bubble.html +188 -0
  10. package/skill/recipes/bullet.html +138 -0
  11. package/skill/recipes/candlestick.html +179 -0
  12. package/skill/recipes/column.html +138 -0
  13. package/skill/recipes/dial.html +349 -0
  14. package/skill/recipes/diverging-bars.html +150 -0
  15. package/skill/recipes/donut.html +231 -0
  16. package/skill/recipes/dumbbell.html +137 -0
  17. package/skill/recipes/gantt.html +196 -0
  18. package/skill/recipes/grouped-bars.html +142 -0
  19. package/skill/recipes/heatmap.html +183 -0
  20. package/skill/recipes/histogram.html +199 -0
  21. package/skill/recipes/line.html +170 -0
  22. package/skill/recipes/live.html +255 -0
  23. package/skill/recipes/log-line.html +493 -0
  24. package/skill/recipes/lollipop.html +152 -0
  25. package/skill/recipes/multi-line.html +228 -0
  26. package/skill/recipes/pyramid.html +162 -0
  27. package/skill/recipes/race.html +278 -0
  28. package/skill/recipes/radial-bars.html +157 -0
  29. package/skill/recipes/scatter.html +199 -0
  30. package/skill/recipes/silhouettes.html +352 -0
  31. package/skill/recipes/slope.html +199 -0
  32. package/skill/recipes/sparklines.html +175 -0
  33. package/skill/recipes/stacked-100.html +158 -0
  34. package/skill/recipes/stacked-bars.html +150 -0
  35. package/skill/recipes/strip.html +221 -0
  36. package/skill/recipes/unit-stack.html +276 -0
  37. package/skill/recipes/violin.html +169 -0
  38. package/skill/recipes/waffle.html +165 -0
  39. package/skill/recipes/waterfall.html +127 -0
  40. package/skill/references/api.md +1684 -0
  41. package/skill/references/design.md +740 -0
  42. package/skill/references/environments.md +1577 -0
  43. package/skill/references/forms.md +197 -0
  44. package/skill/references/interaction.md +794 -0
  45. package/skill/references/looks.md +267 -0
  46. package/skill/references/pitfalls.md +202 -0
  47. package/src/check/README.md +176 -0
  48. package/src/check/browser.js +92 -0
  49. package/src/check/build.js +439 -0
  50. package/src/check/colors.js +324 -0
  51. package/src/check/findings.js +247 -0
  52. package/src/check/index.js +508 -0
  53. package/src/check/inpage.js +1332 -0
  54. package/src/check/interact.js +266 -0
  55. package/src/check/png.js +98 -0
  56. package/src/check/report.js +106 -0
  57. package/src/check/source.js +387 -0
  58. package/src/check/sourcemap.js +75 -0
  59. package/src/guard/note.js +30 -0
  60. package/src/guard/render.js +17 -0
  61. package/src/guard/rhp.js +612 -0
  62. package/src/guard/rules.js +125 -0
  63. package/src/server.js +266 -0
  64. package/src/skill.js +140 -0
@@ -0,0 +1,612 @@
1
+ // rhp as an app imports it, with each component checking how it is used: props it doesn't have, values of the wrong
2
+ // kind, a slat that reads a data group its Plot doesn't have. What it finds goes to globalThis.__rhpCheck.findings.
3
+ // The guard never throws and never changes what rhp draws: each component gets the props it was given (a Plot gets
4
+ // them through a Proxy that only wraps its slat function, which records the keys d is read with), and the checker's
5
+ // tests compare screenshots with and without the guard.
6
+ // Props are read once, untracked, and only the ones checked (never children, ref, id or handlers). Slat functions are
7
+ // never called here.
8
+ import * as rhp from "rhp-check:rhp";
9
+ import { untrack, getListener } from "solid-js";
10
+ import { note, safely, busy, store } from "./note.js";
11
+ import {
12
+ PLOT_SETTINGS, SETTING_VALUE, CHART_PROPS, THEME_KEYS, SLAT_SETTINGS, ROOM_SIDES, BLOCKS, LENGTHS, RENAMES,
13
+ HTML_ATTRIBUTES, SVG_ATTRIBUTES, THEME_KEY, passesThrough, closest, isPalette, isList, isObject,
14
+ } from "./rules.js";
15
+
16
+ export * from "rhp-check:rhp";
17
+
18
+ const read = (props, key) => {
19
+ try {
20
+ return untrack(() => props[key]);
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ };
25
+
26
+ const keysOf = (props) => {
27
+ try {
28
+ return Object.keys(props);
29
+ } catch {
30
+ return [];
31
+ }
32
+ };
33
+
34
+ const show = (v) => {
35
+ if (typeof v === "string") return JSON.stringify(v);
36
+ if (typeof v === "function") return "a function";
37
+ if (isList(v)) return `a list of ${v.length}`;
38
+ if (isObject(v)) return "an object";
39
+ return String(v);
40
+ };
41
+
42
+ const isColor = (c) => typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", c);
43
+ const SPAN = new Set(["from", "to", "value"]);
44
+
45
+ // A number a block draws at: finite, or left out
46
+ function checkNumber(name, prop, v) {
47
+
48
+ if (v == null || (typeof v === "number" && Number.isFinite(v))) return;
49
+
50
+ if (typeof v === "number" && Number.isNaN(v)) {
51
+ note("error", "nan-value", `${name}'s ${prop} is NaN.`, `Give ${name} a number for ${prop}: check the data it is computed from (a missing value, a name that is misspelled, a division by zero).`);
52
+ return;
53
+ }
54
+
55
+ if (typeof v === "function") {
56
+ const args = v.length;
57
+ note("error", "bad-prop", `${name}'s ${prop} is a function${args ? ` of ${args} argument${args > 1 ? "s" : ""}` : ""}, not a number.`,
58
+ `Pass the number: ${prop}={d.value} in JSX. In an html template a value that changes is a function of no arguments: ${prop}=\${() => d.value}.`);
59
+ return;
60
+ }
61
+
62
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) {
63
+ note("warning", "bad-prop", `${name}'s ${prop} is the string ${JSON.stringify(v)}, not a number.`, `Pass a number (${prop}={+d.value}): a string doesn't animate in the JS version and sorts as text.`);
64
+ return;
65
+ }
66
+
67
+ note("error", "bad-prop", `${name}'s ${prop} is ${show(v)}, not a number.`, `Give ${name} a finite number for ${prop}${SPAN.has(prop) ? " on the Chart's scale" : ""}.`);
68
+ }
69
+
70
+ function checkColor(name, v) {
71
+
72
+ if (v == null || typeof v !== "string") {
73
+ if (v != null) note("error", "bad-prop", `${name}'s color is ${show(v)}, not a color.`, `Give a theme key ("series-2", "positive", "muted"...) or a CSS color ("#2a78d6").`);
74
+ return;
75
+ }
76
+
77
+ if (THEME_KEY.test(v) || /^var\(/.test(v) || isColor(v)) return;
78
+
79
+ note("error", "bad-prop", `${name}'s color ${JSON.stringify(v)} is not a color.`, `Give a theme key ("series-2", "positive", "muted"...) or a CSS color ("#2a78d6", "rgb(42 120 214)").`);
80
+ }
81
+
82
+ function fixFor(name, prop) {
83
+
84
+ if (name === "Chart" && (prop === "data" || prop === "rows")) return "Data goes on a Plot inside the Chart, as data groups: <Plot name={names} value={values}>.";
85
+ if (name === "Chart" && (prop === "width" || prop === "size")) return "A Chart is as wide as its parent: size the element around it with CSS.";
86
+ if (name === "Chart" && (prop === "title" || prop === "ariaLabel")) return "For a headline, put the Chart in a Poster with a title. For screen readers, give the Chart a label.";
87
+ if (name === "Chart" && (prop === "colors" || prop === "palette")) return "Give the colors as the theme: theme={{ series: [\"#2a78d6\", \"#eb6834\"] }}.";
88
+ if (name === "Chart" && (prop === "min" || prop === "max")) return "Give the value axis as scale={[min, max]}.";
89
+ if (name === "Chart" && /^on[A-Z:]/.test(prop)) return "A Chart takes no handlers: put the handler on an element around it, or on the elements inside the slats.";
90
+ if (name === "Chart" && /^data-/.test(prop)) return "A Chart doesn't put data-* on its element: give it an id or class, or wrap it in an element that carries the attribute.";
91
+ if (prop === "className") return "Use class, as in HTML.";
92
+ if (name === "Label" && (prop === "text" || prop === "label")) return "A Label's text is its children: <Label at={d.value}>{d.value}</Label>.";
93
+
94
+ const to = RENAMES[name]?.[prop];
95
+ if (to) return `Use ${to}.`;
96
+
97
+ return null;
98
+ }
99
+
100
+ // Props a component doesn't have
101
+ function checkProps(name, props, own, attributes) {
102
+
103
+ for (const key of keysOf(props)) {
104
+ if (own.includes(key) || (attributes && (attributes.has(key) || passesThrough(key)))) continue;
105
+ if (name === "Chart" && key.startsWith("aria-")) continue;
106
+ const fix = fixFor(name, key) ?? (closest(key, own) ? `Did you mean ${closest(key, own)}?` : `${name} takes ${own.join(", ")}.`);
107
+ const lead = name === "Chart" ? `Chart has no prop ${key}, so it is ignored.` : `${name} has no prop ${key}.`;
108
+ note("error", "unknown-prop", lead, fix);
109
+ }
110
+ }
111
+
112
+ function checkTheme(where, value) {
113
+
114
+ if (value == null) return;
115
+ if (!isObject(value)) {
116
+ note("error", "bad-prop", `${where} is ${show(value)}, not an object of theme keys.`, `Give an object: { series: [...], ink: "#222", muted: "#666", surface: "#fff", font: "..." }.`);
117
+ return;
118
+ }
119
+
120
+ for (const key of Object.keys(value)) {
121
+ if (THEME_KEYS.includes(key)) continue;
122
+ const to = RENAMES.theme[key] ?? closest(key, THEME_KEYS);
123
+ note("error", "unknown-prop", `${where} has no key ${key}.`, `${to ? `Use ${to}. ` : ""}The theme's keys are ${THEME_KEYS.join(", ")}.`);
124
+ }
125
+
126
+ if (value.series != null && !(isList(value.series) && Array.from(value.series).every((c) => typeof c === "string"))) {
127
+ note("error", "bad-prop", `${where}'s series is ${show(value.series)}, not a list of colors.`, `Give a list of CSS colors: series: ["#2a78d6", "#eb6834"].`);
128
+ }
129
+ }
130
+
131
+ // A Chart's scale (or cross scale): two finite numbers, the smaller first. Reversed or equal, rhp draws Bars with no
132
+ // length and no axis, without a message.
133
+ function checkScale(prop, scale) {
134
+
135
+ if (scale === undefined) return;
136
+
137
+ const shown = isList(scale) ? `[${Array.from(scale).map(show).join(", ")}]` : show(scale);
138
+ if (!(isList(scale) && scale.length === 2 && Number.isFinite(scale[0]) && Number.isFinite(scale[1]))) {
139
+ note("error", "bad-scale", `Chart's ${prop} is ${shown}, not two finite numbers.`, `Give ${prop} two finite numbers, the smaller first: ${prop}=\${[0, 100]} (nice(lo, hi) gives round ones).`);
140
+ return;
141
+ }
142
+
143
+ const [a, b] = scale;
144
+ if (a === b) note("error", "bad-scale", `Chart's ${prop} is ${shown}: both ends are the same, so Bars have no length and no axis is drawn.`, `Give the scale room: ${prop}=\${[${a}, ${a + (Math.abs(a) || 1)}]}, or build it from the data with nice(lo, hi).`);
145
+ else if (a > b) note("error", "bad-scale", `Chart's ${prop} is ${shown}: the larger number comes first, so Bars have no length and no axis is drawn.`, `Put the smaller first: ${prop}=\${[${b}, ${a}]}. To run bars the other way, use negative values with format=\${(v) => Math.abs(v)}.`);
146
+ }
147
+
148
+ function inspectChart(props) {
149
+
150
+ checkProps("Chart", props, CHART_PROPS, null);
151
+
152
+ checkScale("scale", read(props, "scale"));
153
+ checkScale("cross", read(props, "cross"));
154
+
155
+ const orientation = read(props, "orientation");
156
+ if (orientation != null && orientation !== "horizontal" && orientation !== "vertical") {
157
+ note("error", "bad-prop", `Chart's orientation is ${show(orientation)}.`, `Use "horizontal" (bars run left to right) or "vertical" (bars run up).`);
158
+ }
159
+
160
+ const height = read(props, "height");
161
+ if (height != null && !(typeof height === "number" && height > 0 && Number.isFinite(height)) && typeof height !== "string") {
162
+ note("error", "bad-prop", `Chart's height is ${show(height)}.`, "Give the plot's height in px: height={320}.");
163
+ }
164
+
165
+ const ticks = read(props, "ticks");
166
+ if (ticks != null && ticks !== false && typeof ticks !== "number" && typeof ticks !== "function" && !(isList(ticks) && Array.from(ticks).every(Number.isFinite))) {
167
+ note("error", "bad-prop", `Chart's ticks is ${show(ticks)}.`, "Give a list of values, a count, a function like every(10), or false for no axis.");
168
+ }
169
+
170
+ checkTheme("Chart's theme", read(props, "theme"));
171
+ for (const prop of ["grid", "crossGrid"]) {
172
+ const value = read(props, prop);
173
+ if (value != null && typeof value !== "boolean") note("error", "bad-prop", `Chart's ${prop} is ${show(value)}.`, "Use false to hide grid lines while retaining axis numbers, or true to show them.");
174
+ }
175
+ }
176
+
177
+ // The keys of the objects in rows, over all of them (a row may leave one out)
178
+ const rowKeys = (rows) => {
179
+
180
+ const keys = new Set();
181
+ if (!isList(rows)) return keys;
182
+
183
+ for (const row of rows) {
184
+ if (isObject(row)) {
185
+ for (const key of Object.keys(row)) {
186
+ keys.add(key);
187
+ }
188
+ }
189
+ }
190
+
191
+ return keys;
192
+ };
193
+
194
+ // Keys a slat may read on d without any data group: rhp's own, and what JavaScript itself asks an object for
195
+ const ALWAYS = new Set(["index", "position", "toString", "valueOf", "toJSON", "then", "constructor", "$$typeof", "nodeType", "__proto__", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "length"]);
196
+
197
+ function inspectPlot(props) {
198
+
199
+ const keys = keysOf(props);
200
+ const groups = keys.filter((key) => !PLOT_SETTINGS.includes(key));
201
+ const values = {};
202
+
203
+ for (const key of groups) {
204
+ values[key] = read(props, key);
205
+ }
206
+
207
+ for (const key of groups) {
208
+ if (key === "index" || key === "position") {
209
+ note("error", "bad-prop", `A Plot has a data group named ${key}, which a slat can't read: d.${key} is rhp's own (the row's ${key === "index" ? "number" : "place on screen"}).`, `Rename the data group (${key === "index" ? "rank, order, n" : "place, slot"}...).`);
210
+ continue;
211
+ }
212
+ if (key === "className") {
213
+ note("warning", "plot-typo", "A Plot has className, which rhp takes as a data group.", "Use class.");
214
+ continue;
215
+ }
216
+ const setting = closest(key, PLOT_SETTINGS.filter((s) => s !== "onLoop"));
217
+ if (setting && SETTING_VALUE[setting]?.(values[key])) {
218
+ note("warning", "plot-typo", `A Plot has ${key}, which is not a Plot setting, so rhp takes it as a data group.`, `Did you mean ${setting}? Plot's settings are ${PLOT_SETTINGS.filter((s) => s !== "onLoop").join(", ")}.`);
219
+ }
220
+ }
221
+
222
+ checkSettings(props, groups);
223
+ checkLengths(props, groups, values);
224
+
225
+ return groups;
226
+ }
227
+
228
+ function checkSettings(props, groups) {
229
+
230
+ const orientation = read(props, "orientation");
231
+ if (orientation != null && !SETTING_VALUE.orientation(orientation)) {
232
+ note("error", "bad-prop", `A Plot's orientation is ${show(orientation)}.`, `Use "horizontal", "vertical" or "across" (the other one).`);
233
+ }
234
+
235
+ const reorder = read(props, "reorder");
236
+ if (reorder != null && !SETTING_VALUE.reorder(reorder)) {
237
+ note("error", "bad-prop", `A Plot's reorder is ${show(reorder)}.`, `Use "slide" (the default), "move" or "refill".`);
238
+ }
239
+
240
+ const rows = read(props, "rows");
241
+ if (rows != null && !(isList(rows) && Array.from(rows).every((r) => r == null || isObject(r)))) {
242
+ note("error", "bad-prop", `A Plot's rows is ${show(rows)}, not a list of objects.`, "Give rows a list of objects (their keys read like data groups), or pass each list as its own data group.");
243
+ }
244
+
245
+ const key = read(props, "key");
246
+ if (typeof key === "string" && !groups.includes(key) && !rowKeys(rows).has(key)) {
247
+ note("error", "bad-prop", `A Plot's key is ${JSON.stringify(key)}, but it has no data group ${key}.`, `key names the data group that identifies a row: ${groups.length ? groups.join(", ") : "add one"}.`);
248
+ }
249
+
250
+ const order = read(props, "order");
251
+ if (order != null && typeof order !== "function" && !isList(order)) {
252
+ note("error", "bad-prop", `A Plot's order is ${show(order)}.`, "Give a list of positions (null hides a row) or an order function like sortBy(\"value\", \"desc\").");
253
+ }
254
+
255
+ const animate = read(props, "animate");
256
+ if (isList(animate)) {
257
+ for (const name of animate) {
258
+ if (!groups.includes(name)) note("warning", "bad-prop", `A Plot's animate names ${JSON.stringify(name)}, which is not one of its data groups.`, `Name data groups of numbers: ${groups.join(", ")}.`);
259
+ }
260
+ }
261
+ }
262
+
263
+ // Lists of data with different lengths: rhp repeats a shorter list from its start, which a palette wants and data doesn't
264
+ function checkLengths(props, groups, values) {
265
+
266
+ const slats = read(props, "slats");
267
+ const rows = read(props, "rows");
268
+ const data = groups.filter((key) => isList(values[key]) && values[key].length >= 3 && !isPalette(values[key], isColor) && !Array.from(values[key]).every((v) => typeof v === "boolean"));
269
+ const lengths = data.map((key) => [key, values[key].length]);
270
+ if (isList(rows) && rows.length >= 3) lengths.push(["rows", rows.length]);
271
+ if (lengths.length < 2 || typeof slats === "number") return;
272
+
273
+ const [longKey, longest] = lengths.reduce((a, b) => (b[1] > a[1] ? b : a));
274
+ const short = lengths.find(([, n]) => n < longest);
275
+ if (!short) return;
276
+
277
+ note("warning", "group-lengths", `A Plot's data groups have different lengths: ${longKey} has ${longest}, ${short[0]} has ${short[1]}.`,
278
+ `Give every data group one item per row. rhp repeats a shorter list from its start, so row ${short[1] + 1} shows ${short[0]}[0].`);
279
+ }
280
+
281
+ // A slat type's settings (slat({ ... }, fn))
282
+ function checkLayout(layout) {
283
+
284
+ if (!isObject(layout)) return;
285
+
286
+ for (const key of Object.keys(layout)) {
287
+ if (SLAT_SETTINGS.includes(key)) continue;
288
+ const to = RENAMES.slat[key] ?? closest(key, SLAT_SETTINGS);
289
+ note("error", "unknown-prop", `slat() has no setting ${key}.`, `${to ? `Use ${to}. ` : ""}A slat type's settings are css, thickness, inset and room.`);
290
+ }
291
+
292
+ const room = layout.room;
293
+ const sides = (r) => {
294
+ if (r == null || r === "auto") return;
295
+ if (!isObject(r)) {
296
+ note("error", "bad-prop", `A slat type's room is ${show(r)}.`, `Give room as { start, end, before, after } in px (or "auto" for start and end), or "auto".`);
297
+ return;
298
+ }
299
+ for (const side of Object.keys(r)) {
300
+ if (!ROOM_SIDES.includes(side)) {
301
+ const to = { left: "start", right: "end", top: "before", bottom: "after" }[side];
302
+ note("error", "unknown-prop", `A slat type's room has no side ${side}.`, `${to ? `Use ${to} (room is named along the value axis, so one room fits both orientations). ` : ""}Its sides are start, end, before and after.`);
303
+ }
304
+ }
305
+ };
306
+
307
+ if (isObject(room) && ("horizontal" in room || "vertical" in room)) {
308
+ sides(room.horizontal);
309
+ sides(room.vertical);
310
+ } else {
311
+ sides(room);
312
+ }
313
+ }
314
+
315
+ function inspectBlock(name, props) {
316
+
317
+ const spec = BLOCKS[name];
318
+ checkProps(name, props, [...spec.own, "class", "style", "ref", "children"], spec.svg ? SVG_ATTRIBUTES : HTML_ATTRIBUTES);
319
+
320
+ const given = keysOf(props);
321
+
322
+ for (const prop of spec.numbers) {
323
+ 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
+ }
330
+ }
331
+
332
+ for (const prop of LENGTHS[name] ?? []) {
333
+ const v = read(props, prop);
334
+ if (v != null && !(typeof v === "number" && Number.isFinite(v)) && typeof v !== "string") {
335
+ note("error", "bad-prop", `${name}'s ${prop} is ${show(v)}.`, `Give a share of the band (0.6) or a CSS length ("12px").`);
336
+ }
337
+ }
338
+
339
+ if (spec.own.includes("color")) checkColor(name, read(props, "color"));
340
+
341
+ // A Dot's size as a number is a share of the slat's width and its height, which are not the same: an oval
342
+ if (name === "Dot") {
343
+ const size = read(props, "size");
344
+ if (typeof size === "number") note("error", "dot-size", `Dot's size is the number ${size}, which rhp takes as a share of the slat's width and of its height, so the dot is drawn as an oval.`, `Give the size as a length: size="12px" (or "0.8em").`);
345
+ }
346
+
347
+ if (name === "Label") {
348
+ const side = read(props, "side");
349
+ const edge = read(props, "edge");
350
+ if (side != null && side !== "after" && side !== "before") note("error", "bad-prop", `Label's side is ${show(side)}.`, `Use "after" (the default) or "before".`);
351
+ if (edge != null && edge !== "start" && edge !== "end") note("error", "bad-prop", `Label's edge is ${show(edge)}.`, `Use "start" (names, before the scale) or "end" (totals, after it).`);
352
+ }
353
+
354
+ if (name === "Area" || name === "Line") checkPoints(name, read(props, "points"));
355
+ }
356
+
357
+ function checkPoints(name, points) {
358
+
359
+ if (points == null) return;
360
+
361
+ const pairs = isList(points) && Array.from(points).every((p) => isList(p) && p.length >= 2);
362
+ if (!pairs) {
363
+ 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
+ return;
365
+ }
366
+
367
+ const bad = Array.from(points).findIndex((p) => !Number.isFinite(p[0]) || !Number.isFinite(p[1]));
368
+ 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.");
370
+ return;
371
+ }
372
+
373
+ 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.");
377
+ }
378
+ }
379
+
380
+ // Keys JavaScript and Solid ask an object for, which are not the slat reading its data
381
+ const INTERNAL = new Set([...ALWAYS].filter((key) => key !== "index" && key !== "position"));
382
+ const KEPT = 400; // the slats of a Plot whose reads are kept, at most
383
+ const plots = [];
384
+
385
+ const same = (a, b) => Object.is(a, b) || (isList(a) && isList(b) && a.length === b.length && Array.from(a).every((v, i) => Object.is(v, b[i])));
386
+
387
+ // The slat function a Plot draws with, made to record what each slat reads on d: which keys (for missing-group), and
388
+ // the reads that are made once, so they can be read again after each interaction. A read is made once when it happens
389
+ // while the slat is drawn and outside any function (${d.sold} in an html template, a value read at the top of the
390
+ // slat), or, in a static Plot, when it reads a per-row function (static draws each row once and keeps no effects).
391
+ function watchedSlat(fn, plot) {
392
+
393
+ return new Proxy(fn, {
394
+ apply(target, self, args) {
395
+ const d = args[0];
396
+ if (d === null || typeof d !== "object") return Reflect.apply(target, self, args);
397
+ const drawing = { reads: [], keys: new Set(), on: true };
398
+ const watched = new Proxy(d, {
399
+ get(row, key) {
400
+ if (typeof key === "string" && !plot.checked.has(key)) safely(() => plot.check(key, row));
401
+ const value = Reflect.get(row, key);
402
+ if (drawing.on && typeof key === "string" && !INTERNAL.has(key) && !drawing.keys.has(key) && !busy()) {
403
+ const perRow = plot.still && plot.perRow(key);
404
+ if (perRow || getListener() == null) {
405
+ drawing.keys.add(key);
406
+ drawing.reads.push({ key, value, perRow });
407
+ }
408
+ }
409
+ return value;
410
+ },
411
+ });
412
+ let el;
413
+ try {
414
+ el = Reflect.apply(target, self, [watched, ...args.slice(1)]);
415
+ } finally {
416
+ drawing.on = false;
417
+ }
418
+ if (drawing.reads.length && plot.slats.length < KEPT && typeof Element !== "undefined" && el instanceof Element) plot.slats.push({ el, row: d, reads: drawing.reads });
419
+ return el;
420
+ },
421
+ });
422
+ }
423
+
424
+ // Reads again what the slats read once, for the slats still on the page: a value that changed since is one the slat
425
+ // still shows as it was. The checker calls this after each interaction.
426
+ store.recheck = () => {
427
+
428
+ for (const plot of plots) {
429
+ for (const slat of plot.slats) {
430
+ if (!slat.el.isConnected) continue;
431
+ for (const r of slat.reads) {
432
+ if (plot.changed.has(r.key)) continue;
433
+ let now;
434
+ try {
435
+ now = untrack(() => (r.perRow ? read(plot.props, r.key)(slat.row) : slat.row[r.key]));
436
+ } catch {
437
+ continue;
438
+ }
439
+ if (same(now, r.value)) continue;
440
+ plot.changed.add(r.key);
441
+ const was = typeof r.value === "number" || typeof r.value === "boolean" || r.value == null ? String(r.value) : show(r.value);
442
+ const is = typeof now === "number" || typeof now === "boolean" || now == null ? String(now) : show(now);
443
+ if (r.perRow) {
444
+ note("error", "static-chart", `The ${plot.still} is static, so each slat is drawn once: d.${r.key} changed during the interactions (from ${was} to ${is}), but the slats still show it as it was.`,
445
+ `Take static off the ${plot.still}: static draws each row once and ignores the signals a per-row function reads, so keep it for charts that never change.`);
446
+ } else {
447
+ note("error", "stale-read", `A slat reads d.${r.key} once, when it is drawn (\${d.${r.key}} in an html template, or a read outside any function), so it still shows ${was} after the data changed to ${is}.`,
448
+ `Read it in a function: \${() => d.${r.key}} in an html template, and never destructure d.`);
449
+ }
450
+ }
451
+ }
452
+ }
453
+ };
454
+
455
+ // What a Plot knows about its data, for the keys its slats read. still names what makes it static ("Chart" or "Plot"),
456
+ // or is false.
457
+ function plotData(props, groups, still) {
458
+
459
+ const fns = new Map();
460
+ const plot = { props, still, checked: new Set(), wrapped: new Map(), slats: [], changed: new Set() };
461
+ plot.perRow = (key) => {
462
+ if (!fns.has(key)) fns.set(key, groups.includes(key) && typeof read(props, key) === "function");
463
+ return fns.get(key);
464
+ };
465
+ plots.push(plot);
466
+
467
+ plot.check = (key, row) => {
468
+ plot.checked.add(key);
469
+ if (ALWAYS.has(key) || groups.includes(key)) return;
470
+ if (untrack(() => Reflect.has(row, key))) return;
471
+
472
+ const rows = read(props, "rows");
473
+ if (rowKeys(rows).has(key)) return;
474
+
475
+ // A data group of objects that have this key: the objects belong in rows
476
+ const holder = groups.find((g) => {
477
+ const v = read(props, g);
478
+ return isList(v) && isObject(v[0]) && key in v[0];
479
+ });
480
+ const near = closest(key, [...groups, ...rowKeys(rows)]);
481
+ const have = [...groups, ...rowKeys(rows)];
482
+ const fix = holder
483
+ ? `${holder} holds objects with ${key}: pass them as rows={${holder}} (their keys read like data groups, so d.${key} works), or read d.${holder}.${key}.`
484
+ : near ? `Did you mean d.${near}?` : `Pass ${key} to the Plot as a data group (${key}={[...]}, one item per row), or stop reading d.${key}.`;
485
+ note("error", "missing-group", `A slat reads d.${key}, but its Plot has no data group ${key}${have.length ? ` (it has ${have.join(", ")})` : " (it has no data groups)"}.`, fix);
486
+ };
487
+
488
+ return plot;
489
+ }
490
+
491
+ // Whether each Chart being drawn is static: its Plots are drawn while it is, and take it from there
492
+ const charts = [];
493
+
494
+ export function Plot(props) {
495
+
496
+ let given = props;
497
+
498
+ safely(() => {
499
+ const groups = inspectPlot(props);
500
+ const own = read(props, "static");
501
+ const still = own === true ? "Plot" : own == null && charts.at(-1) ? "Chart" : false;
502
+ const plot = plotData(props, groups, still);
503
+ given = new Proxy(props, {
504
+ get(target, key) {
505
+ const v = Reflect.get(target, key);
506
+ if (key !== "children" || typeof v !== "function") return v;
507
+ if (!plot.wrapped.has(v)) plot.wrapped.set(v, watchedSlat(v, plot));
508
+ return plot.wrapped.get(v);
509
+ },
510
+ });
511
+ });
512
+
513
+ return rhp.Plot(given);
514
+ }
515
+
516
+ export function Chart(props) {
517
+
518
+ let still = false;
519
+
520
+ safely(() => {
521
+ inspectChart(props);
522
+ still = read(props, "static") === true;
523
+ });
524
+
525
+ charts.push(still);
526
+ try {
527
+ return rhp.Chart(props);
528
+ } finally {
529
+ charts.pop();
530
+ }
531
+ }
532
+
533
+ export function Theme(props) {
534
+
535
+ safely(() => {
536
+ checkProps("Theme", props, ["value", "children"], null);
537
+ checkTheme("Theme's value", read(props, "value"));
538
+ });
539
+
540
+ return rhp.Theme(props);
541
+ }
542
+
543
+ export function Scale(props) {
544
+
545
+ safely(() => {
546
+ const ticks = read(props, "ticks");
547
+ if (ticks != null && ticks !== false && typeof ticks !== "number" && typeof ticks !== "function" && !(isList(ticks) && Array.from(ticks).every(Number.isFinite))) {
548
+ note("error", "bad-prop", `Scale's ticks is ${show(ticks)}.`, "Give a list of values, a count, or a function like every(10).");
549
+ }
550
+ });
551
+
552
+ return rhp.Scale(props);
553
+ }
554
+
555
+ export function Poster(props) {
556
+
557
+ safely(() => {
558
+ for (const key of keysOf(props)) {
559
+ const to = RENAMES.Poster[key];
560
+ if (to) note("error", "unknown-prop", `Poster has no prop ${key}, so it goes on the <figure> as an attribute and shows nothing.`, `Use ${to}. A Poster takes kicker, title, dek, note and look.`);
561
+ }
562
+ });
563
+
564
+ return rhp.Poster(props);
565
+ }
566
+
567
+ const block = (name) => {
568
+ const real = rhp[name];
569
+ return {
570
+ [name](props) {
571
+ safely(() => inspectBlock(name, props));
572
+ return real(props);
573
+ },
574
+ }[name];
575
+ };
576
+
577
+ export const Bar = block("Bar");
578
+ export const Dot = block("Dot");
579
+ export const Tick = block("Tick");
580
+ export const Cell = block("Cell");
581
+ export const Place = block("Place");
582
+ export const Label = block("Label");
583
+ export const Area = block("Area");
584
+ export const Line = block("Line");
585
+
586
+ export function slat(def, row) {
587
+
588
+ safely(() => {
589
+ if (typeof def !== "function") checkLayout(def);
590
+ });
591
+
592
+ return rhp.slat(def, row);
593
+ }
594
+
595
+ // sortBy by a name that is not a data group sorts nothing: the order function checks the rows it is given once
596
+ export function sortBy(key, direction) {
597
+
598
+ const order = rhp.sortBy(key, direction);
599
+ if (typeof key !== "string") return order;
600
+
601
+ let checked = false;
602
+
603
+ return (rows, current) => {
604
+ if (!checked && rows?.length) {
605
+ checked = true;
606
+ safely(() => {
607
+ if (!untrack(() => key in rows[0])) note("error", "missing-group", `sortBy(${JSON.stringify(key)}) sorts by a data group the Plot doesn't have.`, `Sort by one of the Plot's data groups (sortBy("value", "desc")), or by a function of the row: sortBy((d) => d.value).`);
608
+ });
609
+ }
610
+ return order(rows, current);
611
+ };
612
+ }