@fasl-work/caos-app-shell 0.15.0 → 0.17.0

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.
@@ -8,20 +8,24 @@
8
8
  // npx caos-shell-gate --url https://contraste.fasl-work.com --expect-brand "Contraste" the deployed origin
9
9
  // Options:
10
10
  // --base-path /Repo/ the site's base path with --serve (a project page); default /
11
+ // --spa-fallback with --serve, answer as nginx does for a single-page app (try_files $uri $uri/ /index.html): an extensionless
12
+ // path that matches no file gets index.html with 200; a path with an extension stays 404; default off (Pages)
11
13
  // --routes /,/methodology the routes; default: read from the header navigation
12
- // --workbench /app the routes that carry the case workbench; default /app
14
+ // --workbench /app the routes that carry the case workbench; default /app; '' for a product whose App is a SurfacePage
15
+ // --app-route /app the App route measured as a surface when --workbench is '' (G18 walks its views' Focus mode); default /app
13
16
  // --sizes 390x844,...,2560x1440 --themes light,dark --langs en,es
14
17
  // --case-sample id1,id2 cases walked outside the primary mode; default the first and the last
15
18
  // --single-case the workbench has one case and no case control
16
19
  // --idle-ms 3000 --settle-ms 15000 --text-floor 400 --instrument-min 0.5 --stage-fill-min 0.3 --stage-floor 140
17
20
  // --wide-font-sizes 390x844,1280x800 the sizes of the wide-font pass (G10); --no-wide-font skips it
18
- // --require-focus fail an App without a Focus control (G18; ADR-0070 requires one in a product with simulations)
21
+ // --require-focus fail an App without a Focus control (G18; ADR-0070 requires one in a product with simulations); on a surface
22
+ // App, one with no view that offers it
19
23
  // --out gate-output report (gate-report.json), screenshots (shots/) and the capture index (index.html)
20
24
  // Needs `playwright` in the consuming project (optional peer dependency). Exit 1 on any failure, 2 on misuse.
21
25
 
22
26
  import { runGate } from '../gate/run.mjs';
23
27
 
24
- const FLAGS = new Set(['single-case', 'no-wide-font', 'require-focus']);
28
+ const FLAGS = new Set(['single-case', 'no-wide-font', 'require-focus', 'spa-fallback']);
25
29
  const args = {};
26
30
  const argv = process.argv.slice(2);
27
31
  for (let i = 0; i < argv.length; i += 1) {
@@ -40,12 +44,14 @@ const options = {
40
44
  expectBrand: args['expect-brand'],
41
45
  routes: args.routes,
42
46
  workbench: args.workbench,
47
+ appRoute: args['app-route'],
43
48
  sizes: args.sizes,
44
49
  themes: args.themes,
45
50
  langs: args.langs,
46
51
  caseSample: args['case-sample'],
47
52
  singleCase: args['single-case'] === true,
48
53
  requireFocus: args['require-focus'] === true,
54
+ spaFallback: args['spa-fallback'] === true,
49
55
  idleMs: num('idle-ms'),
50
56
  settleMs: num('settle-ms'),
51
57
  textFloor: num('text-floor'),
package/dist/chart.d.ts CHANGED
@@ -16,6 +16,17 @@ interface ChartSeries {
16
16
  mode?: 'line' | 'points';
17
17
  /** The marker diameter of a `points` series, in CSS pixels (9 by default): a larger one rings the selected case. */
18
18
  size?: number;
19
+ /** A `points` series on x values of its own (measured cases over a modelled curve that has other sizes): one x per
20
+ * value. It is drawn over the plot on its own x, outside the curve's data, so it takes no part in the cursor, the
21
+ * pick or the readout, and the axes take its extent in. */
22
+ x?: number[];
23
+ }
24
+ /** What a reference line may be dragged over: the range it stays inside, and the step it snaps to (a hundredth of the
25
+ * range when not given; on a log axis the step is a factor). */
26
+ interface YMarkDrag {
27
+ min: number;
28
+ max: number;
29
+ step?: number;
19
30
  }
20
31
  interface ChartAxis {
21
32
  label: BiText;
@@ -52,7 +63,12 @@ interface UPlotChartProps {
52
63
  y: number;
53
64
  label: BiText;
54
65
  tone?: YMarkTone;
66
+ drag?: YMarkDrag;
55
67
  }[];
68
+ /** Called with the index in `yMarks` and the new y of a draggable reference line (`drag`) every time the reader moves
69
+ * it, by the pointer or the keyboard (a slider on the line: arrows, Page keys, Home and End); the caller sets the new
70
+ * y in `yMarks`. */
71
+ onYMarkChange?: (index: number, y: number) => void;
56
72
  /** Height in pixels, or `fill` to take the container's height (inside `PlotCard fill`); the width is always the
57
73
  * container's. */
58
74
  height?: number | 'fill';
@@ -110,6 +126,29 @@ declare const LOG_TABLE_FLOOR = 1e-22;
110
126
  declare function logDecades(min: number, max: number, most?: number): number[];
111
127
  /** The smallest positive value among columns of values and the lower end of a fixed range; Infinity if none. */
112
128
  declare function smallestPositive(columns: readonly (readonly (number | null)[])[], range?: readonly [number, number] | null): number;
129
+ /**
130
+ * The ticks of a log x axis that get a label: the powers of ten only (null for every other tick, which formatTicks leaves
131
+ * blank). A range with no power of ten in it keeps its ticks, so the axis is never left without a label.
132
+ */
133
+ declare function decadeOnly(ticks: readonly (number | null)[]): (number | null)[];
134
+ /** The side of the square box of a parity plot: the smaller of the box's width and the height asked for (the box's
135
+ * height when the chart fills it). */
136
+ declare function parityBox(boxWidth: number, boxHeight: number, height: number, fill: boolean): number;
137
+ /** A dragged value kept inside the range of its line and snapped to its step; a value that is not a number falls to the
138
+ * lower end. */
139
+ declare function clampMark(y: number, drag: YMarkDrag): number;
140
+ /**
141
+ * The value a key sets on a draggable line, or null for a key it does not handle: the arrows one step, Page Up and Down
142
+ * ten, Home and End the ends of the range. A step is a hundredth of the range, or `drag.step`; on a log axis it is a factor
143
+ * (a hundredth of the range in decades) and `drag.step` is not used.
144
+ */
145
+ declare function nudgeMark(y: number, key: string, drag: YMarkDrag, log?: boolean): number | null;
146
+ /** The x and y extent of the series that bring an x of their own, finite points only (on a log x axis, `positiveX`,
147
+ * only the points with x above 0); null when there are none. */
148
+ declare function overlayExtent(series: readonly ChartSeries[], positiveX?: boolean): {
149
+ x: [number, number];
150
+ y: [number, number];
151
+ } | null;
113
152
  /** How many tick labels repeat the label before them (empty labels apart); the gate fails a chart with any (G6). */
114
153
  declare function repeatedLabels(labels: readonly (string | null | undefined)[]): number;
115
154
  /**
@@ -158,6 +197,6 @@ declare function nearestPoint(xs: readonly number[], ys: readonly (readonly (num
158
197
  * (known shell defect 17). An axis with fixed decimals ticks only where its labels differ (`tickSteps`). The host
159
198
  * declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate.
160
199
  */
161
- declare function UPlotChart({ x, y, series, marks, yMarks, height, onCursor, onPick, parity, readout, hint }: UPlotChartProps): react.JSX.Element;
200
+ declare function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height, onCursor, onPick, parity, readout, hint }: UPlotChartProps): react.JSX.Element;
162
201
 
163
- export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, YMARK_TONE_TOKENS, type YMarkTone, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
202
+ export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, YMARK_TONE_TOKENS, type YMarkDrag, type YMarkTone, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
package/dist/chart.js CHANGED
@@ -83,11 +83,73 @@ function smallestPositive(columns, range) {
83
83
  for (const c of columns) for (const v of c) if (v !== null && Number.isFinite(v) && v > 0 && v < lo) lo = v;
84
84
  return lo;
85
85
  }
86
+ function isDecade(t) {
87
+ if (t === null || t === void 0 || !Number.isFinite(t) || t <= 0) return false;
88
+ const d = Math.log10(t);
89
+ return Math.abs(d - Math.round(d)) < 1e-6;
90
+ }
91
+ function decadeOnly(ticks) {
92
+ if (!ticks.some(isDecade)) return [...ticks];
93
+ return ticks.map((t) => isDecade(t) ? t : null);
94
+ }
95
+ function parityBox(boxWidth, boxHeight, height, fill) {
96
+ return Math.min(boxWidth, fill ? boxHeight : height);
97
+ }
98
+ function clampMark(y, drag) {
99
+ if (!Number.isFinite(y)) return drag.min;
100
+ const v = drag.step && drag.step > 0 ? drag.min + Math.round((y - drag.min) / drag.step) * drag.step : y;
101
+ const clamped = Math.min(drag.max, Math.max(drag.min, v));
102
+ return Number(clamped.toPrecision(12));
103
+ }
104
+ function nudgeMark(y, key, drag, log = false) {
105
+ const dir = key === "ArrowUp" || key === "ArrowRight" || key === "PageUp" ? 1 : key === "ArrowDown" || key === "ArrowLeft" || key === "PageDown" ? -1 : 0;
106
+ if (key === "Home") return drag.min;
107
+ if (key === "End") return drag.max;
108
+ if (dir === 0) return null;
109
+ const n = key === "PageUp" || key === "PageDown" ? 10 : 1;
110
+ if (log && drag.min > 0 && drag.max > drag.min) {
111
+ const decades = (Math.log10(drag.max) - Math.log10(drag.min)) / 100;
112
+ return Math.min(drag.max, Math.max(drag.min, y * 10 ** (dir * n * decades)));
113
+ }
114
+ const step = drag.step && drag.step > 0 ? drag.step : (drag.max - drag.min) / 100;
115
+ return clampMark(y + dir * n * step, { ...drag, step: void 0 });
116
+ }
117
+ function isOverlay(s) {
118
+ return Boolean(s.x) && s.mode === "points";
119
+ }
120
+ function dragWithin(drag, range) {
121
+ if (!range) return drag;
122
+ const lo = Math.min(range[0], range[1]);
123
+ const hi = Math.max(range[0], range[1]);
124
+ const min = Math.min(hi, Math.max(lo, drag.min));
125
+ const max = Math.max(lo, Math.min(hi, drag.max));
126
+ return { ...drag, min, max: Math.max(min, max) };
127
+ }
128
+ function overlayExtent(series, positiveX = false) {
129
+ let x0 = Infinity;
130
+ let x1 = -Infinity;
131
+ let y0 = Infinity;
132
+ let y1 = -Infinity;
133
+ for (const s of series) {
134
+ if (!isOverlay(s) || !s.x) continue;
135
+ for (let i = 0; i < Math.min(s.x.length, s.values.length); i++) {
136
+ const px = s.x[i];
137
+ const py = s.values[i];
138
+ if (py === null || py === void 0 || !Number.isFinite(px) || !Number.isFinite(py) || positiveX && !(px > 0)) continue;
139
+ x0 = Math.min(x0, px);
140
+ x1 = Math.max(x1, px);
141
+ y0 = Math.min(y0, py);
142
+ y1 = Math.max(y1, py);
143
+ }
144
+ }
145
+ return Number.isFinite(x0) ? { x: [x0, x1], y: [y0, y1] } : null;
146
+ }
86
147
  var DECADE_AXIS = {
87
148
  splits: (_u, _i, min, max) => logDecades(min, max),
88
149
  filter: (_u, splits) => splits
89
150
  };
90
151
  var PARITY_TICK_SPACE = 40;
152
+ var HANDLE_HEIGHT = 14;
91
153
  function repeatedLabels(labels) {
92
154
  let n = 0;
93
155
  for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
@@ -193,7 +255,7 @@ function nearestPoint(xs, ys, left, top) {
193
255
  }
194
256
  return best;
195
257
  }
196
- function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPick, parity, readout, hint }) {
258
+ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint }) {
197
259
  const lang = useShellLang();
198
260
  const theme = useThemeStore((s) => s.theme);
199
261
  const fill = height === "fill";
@@ -206,7 +268,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
206
268
  },
207
269
  [measureRef]
208
270
  );
209
- const width = parity ? Math.min(box.width, fill ? box.height : height) : box.width;
271
+ const width = parity ? parityBox(box.width, box.height, typeof height === "number" ? height : 0, fill) : box.width;
210
272
  const h = parity ? width : fill ? box.height : height;
211
273
  const uRef = useRef(null);
212
274
  const hostRef = useRef(null);
@@ -220,36 +282,57 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
220
282
  const orderRef = useRef(order);
221
283
  orderRef.current = order;
222
284
  const xs = useMemo(() => order ? order.map((i) => x.values[i]) : x.values, [order, x.values]);
223
- const ys = useMemo(() => series.map((s) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, series]);
224
- const levels = useMemo(() => (yMarks ?? []).map((m) => m.y), [yMarks]);
225
- const range = useMemo(() => parity ? parityRange(xs, ...ys, levels) : null, [parity, xs, ys, levels]);
226
- const yLevels = !parity && !y.range && levels.length > 0 ? levels : null;
285
+ const entries = useMemo(() => series.map((s, i) => ({ s, i })), [series]);
286
+ const baseEntries = useMemo(() => entries.filter(({ s }) => !isOverlay(s)), [entries]);
287
+ const overlays = useMemo(() => entries.filter(({ s }) => isOverlay(s)), [entries]);
288
+ const ext = useMemo(() => overlayExtent(series, Boolean(x.log)), [series, x.log]);
289
+ const ys = useMemo(() => baseEntries.map(({ s }) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, baseEntries]);
290
+ const levels = useMemo(() => (yMarks ?? []).flatMap((m) => m.drag ? [m.drag.min, m.drag.max] : [m.y]), [yMarks]);
291
+ const range = useMemo(() => parity ? parityRange(xs, ...ys, levels, ext ? [...ext.x, ...ext.y] : []) : null, [parity, xs, ys, levels, ext]);
292
+ const yLevels = !parity && !y.range && (levels.length > 0 || ext) ? [...levels, ...ext ? ext.y : []] : null;
293
+ const xExt = useMemo(() => {
294
+ if (!ext || range || xs.length === 0) return null;
295
+ const lo = Math.min(xs[0], ext.x[0]);
296
+ const hi = Math.max(xs[xs.length - 1], ext.x[1]);
297
+ if (x.log && !(lo > 0)) return null;
298
+ return hi > lo ? [lo, hi] : x.log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
299
+ }, [ext, range, xs, x.log]);
227
300
  const tiny = useMemo(
228
301
  () => ({
229
- x: Boolean(x.log) && smallestPositive([xs], range) < LOG_TABLE_FLOOR,
302
+ x: Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
230
303
  y: Boolean(y.log) && smallestPositive(yLevels ? [...ys, yLevels] : ys, range ?? y.range) < LOG_TABLE_FLOOR
231
304
  }),
232
- [x.log, y.log, xs, ys, range, y.range, yLevels]
305
+ [x.log, y.log, xs, ys, range, y.range, yLevels, ext]
233
306
  );
234
307
  const xLabel = pick(x.label, lang);
235
308
  const yLabel = pick(y.label, lang);
309
+ const yMarksRef = useRef(yMarks);
310
+ yMarksRef.current = yMarks;
311
+ const liveRef = useRef([]);
312
+ liveRef.current = (yMarks ?? []).map((m) => m.y);
313
+ const overlaysRef = useRef(overlays);
314
+ overlaysRef.current = overlays;
315
+ const onYMarkChangeRef = useRef(onYMarkChange);
316
+ onYMarkChangeRef.current = onYMarkChange;
317
+ const dragCount = (yMarks ?? []).filter((m) => m.drag).length;
236
318
  const xUnit = x.unit ? pick(x.unit, lang) : "";
237
319
  const yUnit = y.unit ? pick(y.unit, lang) : "";
238
320
  const structure = JSON.stringify({
239
- s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9]),
240
- x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x],
321
+ s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s)]),
322
+ x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt],
323
+ oe: ext,
241
324
  p: range,
242
325
  pick: Boolean(onPick),
243
326
  y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
244
327
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
245
- ym: (yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]),
328
+ ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag] : [m.y, pick(m.label, lang), m.tone ?? "warn"]),
246
329
  theme,
247
330
  lang,
248
331
  h,
249
332
  w: width
250
333
  });
251
334
  const data = useMemo(() => [xs, ...ys], [xs, ys]);
252
- const scatter = series.length > 0 && series.every((s) => s.mode === "points");
335
+ const scatter = baseEntries.length > 0 && baseEntries.every(({ s }) => s.mode === "points");
253
336
  useEffect(() => {
254
337
  const el = plotRef.current;
255
338
  if (!el || width <= 0 || h <= 0) return;
@@ -259,8 +342,9 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
259
342
  const font = `11px ${family}`;
260
343
  const surface = resolveToken("--color-surface", "#fff");
261
344
  const repeats = { x: 0, y: 0 };
262
- const tick = (fmt, axis) => (_u, vals) => {
263
- const labels = formatTicks(vals, lang, fmt ?? { digits: 4 });
345
+ const tick = (fmt, axis, pre) => (_u, vals) => {
346
+ const labels = formatTicks(pre ? pre(vals) : vals, lang, fmt ?? { digits: 4 });
347
+ if (axis === "x" && pre) hostRef.current?.setAttribute("data-log-labels", labels.filter((l) => l).join("|"));
264
348
  repeats[axis] = repeatedLabels(labels);
265
349
  hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
266
350
  hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
@@ -294,7 +378,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
294
378
  x: {
295
379
  time: Boolean(x.time),
296
380
  distr: x.log ? 3 : 1,
297
- ...range ? { range } : x.log ? { range: (_u, min, max) => [min, max] } : {}
381
+ ...range ? { range } : xExt ? { range: xExt } : x.log ? { range: (_u, min, max) => [min, max] } : {}
298
382
  },
299
383
  y: {
300
384
  distr: y.log ? 3 : 1,
@@ -316,7 +400,8 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
316
400
  font,
317
401
  label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
318
402
  labelFont: font,
319
- values: x.time ? void 0 : tick(x.format, "x"),
403
+ // a log x axis labels the decades only; the ticks between stay drawn, unlabelled
404
+ values: x.time ? void 0 : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
320
405
  ...xSteps ? { incrs: xSteps } : {},
321
406
  ...range ? { space: PARITY_TICK_SPACE } : {},
322
407
  ...tiny.x ? DECADE_AXIS : {}
@@ -346,7 +431,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
346
431
  ],
347
432
  series: [
348
433
  {},
349
- ...series.map((s, i) => {
434
+ ...baseEntries.map(({ s, i }) => {
350
435
  const style = seriesStyle(s, i);
351
436
  const stroke = resolveToken(style.color, "#4a8");
352
437
  return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: s.size ?? 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: style.dash, spanGaps: false, points: { show: false } };
@@ -368,6 +453,55 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
368
453
  if (idx === null || idx === void 0 || !onPickRef.current) return;
369
454
  onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
370
455
  });
456
+ const accent = resolveToken("--color-accent", "#06c");
457
+ (yMarks ?? []).forEach((m, i) => {
458
+ if (!m.drag) return;
459
+ const drag = dragWithin(m.drag, range ? void 0 : y.range);
460
+ const el2 = document.createElement("div");
461
+ el2.setAttribute("role", "slider");
462
+ el2.setAttribute("tabindex", "0");
463
+ el2.setAttribute("aria-orientation", "vertical");
464
+ el2.setAttribute("aria-valuemin", String(drag.min));
465
+ el2.setAttribute("aria-valuemax", String(drag.max));
466
+ el2.setAttribute("data-y-handle", String(i));
467
+ Object.assign(el2.style, { position: "absolute", left: "0", width: "100%", height: `${HANDLE_HEIGHT}px`, cursor: "ns-resize", touchAction: "none", outlineOffset: "-2px", zIndex: "3" });
468
+ el2.addEventListener("focus", () => el2.style.outline = `2px solid ${accent}`);
469
+ el2.addEventListener("blur", () => el2.style.outline = "none");
470
+ el2.addEventListener("click", (e) => e.stopPropagation());
471
+ const set = (raw) => {
472
+ const v = clampMark(raw, y.log ? { ...drag, step: void 0 } : drag);
473
+ if (v === liveRef.current[i]) return;
474
+ liveRef.current[i] = v;
475
+ onYMarkChangeRef.current?.(i, v);
476
+ u.redraw(false);
477
+ };
478
+ let dragging = false;
479
+ el2.addEventListener("pointerdown", (e) => {
480
+ if (e.button !== 0) return;
481
+ dragging = true;
482
+ el2.setPointerCapture(e.pointerId);
483
+ el2.focus();
484
+ e.preventDefault();
485
+ });
486
+ el2.addEventListener("pointermove", (e) => {
487
+ if (!dragging) return;
488
+ set(u.posToVal(e.clientY - u.over.getBoundingClientRect().top, "y"));
489
+ });
490
+ const end = (e) => {
491
+ dragging = false;
492
+ if (el2.hasPointerCapture(e.pointerId)) el2.releasePointerCapture(e.pointerId);
493
+ };
494
+ el2.addEventListener("pointerup", end);
495
+ el2.addEventListener("pointercancel", end);
496
+ el2.addEventListener("keydown", (e) => {
497
+ const v = nudgeMark(liveRef.current[i], e.key, drag, Boolean(y.log));
498
+ if (v === null) return;
499
+ e.preventDefault();
500
+ set(v);
501
+ });
502
+ u.over.appendChild(el2);
503
+ });
504
+ if (dragCount) queueMicrotask(() => uRef.current === u && u.redraw(false));
371
505
  }
372
506
  ],
373
507
  draw: [
@@ -385,8 +519,53 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
385
519
  ctx.stroke();
386
520
  ctx.restore();
387
521
  }
522
+ const dots = overlaysRef.current;
523
+ if (dots.length) {
524
+ const ctx = u.ctx;
525
+ const pr = uPlot.pxRatio || 1;
526
+ let drawn = 0;
527
+ ctx.save();
528
+ ctx.beginPath();
529
+ ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
530
+ ctx.clip();
531
+ for (const { s, i } of dots) {
532
+ ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
533
+ ctx.strokeStyle = surface;
534
+ ctx.lineWidth = Math.max(1, pr);
535
+ const r = (s.size ?? 9) / 2 * pr;
536
+ const ox = s.x ?? [];
537
+ for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
538
+ const v = s.values[k];
539
+ if (v === null || v === void 0 || !Number.isFinite(v) || !Number.isFinite(ox[k])) continue;
540
+ const px = u.valToPos(ox[k], "x", true);
541
+ const py = u.valToPos(v, "y", true);
542
+ if (!Number.isFinite(px) || !Number.isFinite(py) || px < u.bbox.left - 0.5 || px > u.bbox.left + u.bbox.width + 0.5 || py < u.bbox.top - 0.5 || py > u.bbox.top + u.bbox.height + 0.5) continue;
543
+ ctx.beginPath();
544
+ ctx.arc(px, py, r, 0, Math.PI * 2);
545
+ ctx.fill();
546
+ ctx.stroke();
547
+ drawn++;
548
+ }
549
+ }
550
+ ctx.restore();
551
+ hostRef.current?.setAttribute("data-overlay-drawn", String(drawn));
552
+ }
388
553
  const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
389
- if (yMarks?.length) drawYMarks(u, yMarks.map((m) => ({ y: m.y, label: pick(m.label, lang), color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
554
+ const lines = yMarksRef.current ?? [];
555
+ if (lines.length) drawYMarks(u, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: pick(m.label, lang), color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
556
+ u.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
557
+ const i = Number(el2.getAttribute("data-y-handle"));
558
+ const m = lines[i];
559
+ const v = liveRef.current[i];
560
+ if (!m || v === void 0) return;
561
+ const top = u.valToPos(v, "y");
562
+ el2.style.display = Number.isFinite(top) && top >= 0 && top <= u.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
563
+ el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
564
+ const label = pick(m.label, lang);
565
+ el2.setAttribute("aria-label", label);
566
+ el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
567
+ el2.setAttribute("aria-valuetext", `${label}: ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`);
568
+ });
390
569
  if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
391
570
  }
392
571
  ]
@@ -402,9 +581,14 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
402
581
  useEffect(() => {
403
582
  uRef.current?.setData(data);
404
583
  }, [data]);
584
+ const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
585
+ const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]));
586
+ useEffect(() => {
587
+ uRef.current?.redraw(false);
588
+ }, [linesKey, overlaysKey]);
405
589
  const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
406
590
  `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
407
- ...series.map((s, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
591
+ ...baseEntries.map(({ s }, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
408
592
  ].join(" \xB7 ") : hint ? pick(hint, lang) : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
409
593
  return /* @__PURE__ */ jsxs(
410
594
  "div",
@@ -420,6 +604,8 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
420
604
  "data-log-x": x.log ? "1" : void 0,
421
605
  "data-pick": onPick ? "1" : void 0,
422
606
  "data-y-marks": yMarks?.length ? String(yMarks.length) : void 0,
607
+ "data-y-drag": dragCount ? String(dragCount) : void 0,
608
+ "data-overlay": overlays.length ? String(overlays.length) : void 0,
423
609
  children: [
424
610
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
425
611
  series.length > 1 && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsxs("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": seriesStyle(s, i).dash ? "1" : void 0, children: [
@@ -432,4 +618,4 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
432
618
  );
433
619
  }
434
620
 
435
- export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
621
+ export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };