@fasl-work/caos-app-shell 0.18.0 → 0.19.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.
package/dist/chart.js CHANGED
@@ -1,10 +1,210 @@
1
- import { pick, useShellLang, useThemeStore, useStageSize, resolveToken, NBSP, formatNumber, formatTicks, referenceLineName } from './chunk-DBCMAL2G.js';
2
- export { referenceLineName } from './chunk-DBCMAL2G.js';
1
+ import { pick, placeXLabel, placeYLabel, useShellLang, useThemeStore, useStageSize, defaultChartName, axisTitleFits, resolveToken, DRAG_THRESHOLD, sameData, NBSP, formatNumber, thinRows, chartSummary, referenceLineName, ZOOM_HINT, CHART_ROLE, ChartSummary, ChartDataTable, ResetZoomButton, formatTicks, countOverlaps, isZoomed, formatTime, keyAction, KEY_PAN, KEY_ZOOM, firstVisible, stepIndex, wheelFactor, pinchState, repeatedLabels, panRange, zoomRange, revealValue, brushRange, leastLabelSpan, MIN_SPAN_SHARE } from './chunk-3LONJM5B.js';
2
+ export { referenceLineName, repeatedLabels } from './chunk-3LONJM5B.js';
3
3
  import './chunk-33WTUQ7Q.js';
4
- import { useRef, useCallback, useState, useEffect, useMemo } from 'react';
4
+ import { useRef, useCallback, useState, useId, useEffect, useMemo } from 'react';
5
5
  import uPlot from 'uplot';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
 
8
+ // src/chart/uplotZoom.ts
9
+ var rangeOf = (u, key) => [u.scales[key].min ?? 0, u.scales[key].max ?? 1];
10
+ function createZoom(u, ctx) {
11
+ const differs = (a, b, full) => {
12
+ const tol = Math.abs(full[1] - full[0]) * 1e-9;
13
+ return Math.abs(a[0] - b[0]) > tol || Math.abs(a[1] - b[1]) > tol;
14
+ };
15
+ const apply = (nx, ny) => {
16
+ const full = ctx.full();
17
+ const cx = rangeOf(u, "x");
18
+ const cy = rangeOf(u, "y");
19
+ const moved = Boolean(nx && differs(nx, cx, full ? full.x : cx) || ny && differs(ny, cy, full ? full.y : cy));
20
+ if (!moved) return false;
21
+ ctx.explicit.current = true;
22
+ try {
23
+ u.batch(() => {
24
+ if (nx) u.setScale("x", { min: nx[0], max: nx[1] });
25
+ if (ny) u.setScale("y", { min: ny[0], max: ny[1] });
26
+ });
27
+ } finally {
28
+ ctx.explicit.current = false;
29
+ }
30
+ return true;
31
+ };
32
+ const isNow = () => {
33
+ const full = ctx.full();
34
+ if (!full) return false;
35
+ return isZoomed(rangeOf(u, "x"), full.x) || ctx.both && isZoomed(rangeOf(u, "y"), full.y);
36
+ };
37
+ const leastOf = (axis, full) => {
38
+ if (ctx.log[axis]) return void 0;
39
+ return Math.max(Math.abs(full[1] - full[0]) * MIN_SPAN_SHARE, ctx.least?.(axis) ?? 0);
40
+ };
41
+ const zoomBy = (factor, ax, ay) => {
42
+ const full = ctx.full();
43
+ if (!full || !ctx.enabled()) return false;
44
+ const nx = zoomRange(rangeOf(u, "x"), full.x, ax, factor, { log: ctx.log.x, minSpan: leastOf("x", full.x) });
45
+ const ny = ctx.both && ay !== void 0 ? zoomRange(rangeOf(u, "y"), full.y, ay, factor, { log: ctx.log.y, minSpan: leastOf("y", full.y) }) : null;
46
+ return apply(nx, ny);
47
+ };
48
+ const panBy = (shareX, shareY = 0) => {
49
+ const full = ctx.full();
50
+ if (!full || !ctx.enabled()) return false;
51
+ const nx = shareX ? panRange(rangeOf(u, "x"), full.x, shareX, ctx.log.x) : null;
52
+ const ny = ctx.both && shareY ? panRange(rangeOf(u, "y"), full.y, shareY, ctx.log.y) : null;
53
+ return apply(nx, ny);
54
+ };
55
+ const brush = (sx, sy) => {
56
+ const full = ctx.full();
57
+ if (!full || !ctx.enabled()) return false;
58
+ const nx = sx ? brushRange(sx, full.x, leastOf("x", full.x) ?? 0) : null;
59
+ const ny = ctx.both && sy ? brushRange(sy, full.y, leastOf("y", full.y) ?? 0) : null;
60
+ return apply(nx, ny);
61
+ };
62
+ const reveal = (x) => {
63
+ const full = ctx.full();
64
+ if (!full || !isNow()) return;
65
+ const cur = rangeOf(u, "x");
66
+ const next = revealValue(cur, full.x, x, ctx.log.x);
67
+ if (next[0] !== cur[0] || next[1] !== cur[1]) apply(next, null);
68
+ };
69
+ const reset = () => {
70
+ const full = ctx.full();
71
+ if (!full) return;
72
+ apply(full.x, ctx.both ? full.y : null);
73
+ };
74
+ const place = (idx) => {
75
+ const d = u.data;
76
+ const xv = d[0][idx];
77
+ if (xv === null || xv === void 0) return;
78
+ const left = u.valToPos(xv, "x");
79
+ let top = u.bbox.height / (window.devicePixelRatio || 1) / 2;
80
+ for (let k = 1; k < d.length; k++) {
81
+ const v = d[k][idx];
82
+ if (v !== null && v !== void 0 && Number.isFinite(v)) {
83
+ top = u.valToPos(v, "y");
84
+ break;
85
+ }
86
+ }
87
+ u.setCursor({ left, top }, true);
88
+ };
89
+ const clear = () => u.setCursor({ left: -10, top: -10 }, true);
90
+ const over = u.over;
91
+ const onHandle = (e) => e.target instanceof Element && e.target.closest("[data-y-handle]") !== null;
92
+ const ac = new AbortController();
93
+ const opt = { signal: ac.signal };
94
+ const sizeOf = () => {
95
+ const r = over.getBoundingClientRect();
96
+ return { w: Math.max(1, r.width), h: Math.max(1, r.height), left: r.left, top: r.top };
97
+ };
98
+ over.addEventListener(
99
+ "wheel",
100
+ (e) => {
101
+ if (!ctx.enabled()) return;
102
+ if (!ctx.plot.contains(document.activeElement)) return;
103
+ const s = sizeOf();
104
+ const changed = Math.abs(e.deltaX) > Math.abs(e.deltaY) && isNow() ? panBy(e.deltaX / s.w) : zoomBy(wheelFactor(e.deltaY, e.deltaMode), u.posToVal(e.clientX - s.left, "x"), ctx.both ? u.posToVal(e.clientY - s.top, "y") : void 0);
105
+ if (changed) e.preventDefault();
106
+ },
107
+ { ...opt, passive: false }
108
+ );
109
+ u.root.addEventListener(
110
+ "mousedown",
111
+ (e) => {
112
+ if (e.shiftKey && e.button === 0 && ctx.enabled() && !onHandle(e)) {
113
+ e.stopPropagation();
114
+ e.preventDefault();
115
+ }
116
+ },
117
+ { ...opt, capture: true }
118
+ );
119
+ const pts = /* @__PURE__ */ new Map();
120
+ let pan = null;
121
+ let pinch = null;
122
+ const startPan = (id, x, y) => {
123
+ pan = { id, x, y };
124
+ try {
125
+ over.setPointerCapture(id);
126
+ } catch {
127
+ }
128
+ };
129
+ over.addEventListener(
130
+ "pointerdown",
131
+ (e) => {
132
+ if (onHandle(e)) return;
133
+ ctx.plot.focus({ preventScroll: true });
134
+ if (!ctx.enabled()) return;
135
+ pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
136
+ if (e.pointerType === "mouse") {
137
+ if (e.button === 0 && e.shiftKey) startPan(e.pointerId, e.clientX, e.clientY);
138
+ return;
139
+ }
140
+ if (pts.size === 2) {
141
+ const [a, b] = [...pts.values()];
142
+ pinch = { dist: pinchState(a, b).dist };
143
+ pan = null;
144
+ } else if (pts.size === 1 && isNow()) startPan(e.pointerId, e.clientX, e.clientY);
145
+ },
146
+ opt
147
+ );
148
+ over.addEventListener(
149
+ "pointermove",
150
+ (e) => {
151
+ if (!pts.has(e.pointerId)) return;
152
+ const before = pts.get(e.pointerId);
153
+ pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
154
+ const s = sizeOf();
155
+ if (pinch && pts.size === 2) {
156
+ const [a, b] = [...pts.values()];
157
+ const now = pinchState(a, b);
158
+ if (pinch.dist > 0 && now.dist > 0) {
159
+ zoomBy(pinch.dist / now.dist, u.posToVal(now.mx - s.left, "x"), ctx.both ? u.posToVal(now.my - s.top, "y") : void 0);
160
+ pinch.dist = now.dist;
161
+ }
162
+ panBy(-(e.clientX - before.x) / 2 / s.w, ctx.both ? (e.clientY - before.y) / 2 / s.h : 0);
163
+ return;
164
+ }
165
+ if (pan && pan.id === e.pointerId) {
166
+ panBy(-(e.clientX - pan.x) / s.w, ctx.both ? (e.clientY - pan.y) / s.h : 0);
167
+ pan.x = e.clientX;
168
+ pan.y = e.clientY;
169
+ }
170
+ },
171
+ opt
172
+ );
173
+ const end = (e) => {
174
+ pts.delete(e.pointerId);
175
+ if (pan && pan.id === e.pointerId) {
176
+ pan = null;
177
+ if (over.hasPointerCapture?.(e.pointerId)) over.releasePointerCapture(e.pointerId);
178
+ }
179
+ if (pts.size < 2) pinch = null;
180
+ if (pts.size === 1 && !pan && isNow()) {
181
+ const [[id, p]] = [...pts.entries()];
182
+ startPan(id, p.x, p.y);
183
+ }
184
+ };
185
+ over.addEventListener("pointerup", end, opt);
186
+ over.addEventListener("pointercancel", end, opt);
187
+ over.addEventListener(
188
+ "dblclick",
189
+ (e) => {
190
+ if (onHandle(e) || !ctx.enabled()) return;
191
+ reset();
192
+ },
193
+ opt
194
+ );
195
+ return {
196
+ zoomBy,
197
+ panBy,
198
+ brush,
199
+ reveal,
200
+ reset,
201
+ view: () => ({ x: rangeOf(u, "x"), y: rangeOf(u, "y") }),
202
+ place,
203
+ clear,
204
+ zoomed: isNow,
205
+ detach: () => ac.abort()
206
+ };
207
+ }
8
208
  var YMARK_TONE_TOKENS = {
9
209
  warn: "--color-warn",
10
210
  bad: "--color-bad",
@@ -197,19 +397,16 @@ var DECADE_AXIS = {
197
397
  };
198
398
  var PARITY_TICK_SPACE = 40;
199
399
  var HANDLE_HEIGHT = 14;
200
- function repeatedLabels(labels) {
201
- let n = 0;
202
- for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
203
- return n;
204
- }
205
- function drawMarks(u, marks, style) {
400
+ var CUT_SEP = String.fromCharCode(1);
401
+ function drawMarks(u, marks, style, boxes = []) {
206
402
  const ctx = u.ctx;
207
403
  const pr = uPlot.pxRatio || 1;
208
404
  const left = u.bbox.left;
209
405
  const right = u.bbox.left + u.bbox.width;
210
406
  const gap = 4 * pr;
211
407
  const rowH = 14 * pr;
212
- const placed = [];
408
+ const textH = 11 * pr;
409
+ const cut = [];
213
410
  ctx.save();
214
411
  ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
215
412
  ctx.textBaseline = "alphabetic";
@@ -225,24 +422,25 @@ function drawMarks(u, marks, style) {
225
422
  ctx.lineTo(px, u.bbox.top + u.bbox.height);
226
423
  ctx.stroke();
227
424
  ctx.setLineDash([]);
228
- const w = ctx.measureText(m.label).width;
229
- let x0 = px + gap;
230
- if (x0 + w > right) x0 = px - gap - w;
231
- if (x0 < left) x0 = Math.max(left, Math.min(right - w, px - w / 2));
232
- let row = 0;
233
- while (placed.some((p) => p.row === row && x0 < p.x1 + gap && x0 + w + gap > p.x0)) row += 1;
234
- placed.push({ x0, x1: x0 + w, row });
235
- const y = u.bbox.top + 12 * pr + row * rowH;
425
+ if (!m.label) continue;
426
+ const at = placeXLabel(m.label, { px, left, right, bottom: u.bbox.top + u.bbox.height, gap, rowH, textH, first: u.bbox.top + 12 * pr, boxes, measure: (t) => ctx.measureText(t).width });
427
+ if (!at) {
428
+ cut.push(m.label);
429
+ continue;
430
+ }
431
+ if (at.cut) cut.push(m.label);
432
+ boxes.push({ kind: "x", text: at.text, full: m.label, x0: at.x0, x1: at.x0 + at.w, y0: at.y - textH, y1: at.y });
236
433
  ctx.textAlign = "left";
237
434
  ctx.lineWidth = 3 * pr;
238
435
  ctx.strokeStyle = style.halo;
239
- ctx.strokeText(m.label, x0, y);
436
+ ctx.strokeText(at.text, at.x0, at.y);
240
437
  ctx.fillStyle = style.color;
241
- ctx.fillText(m.label, x0, y);
438
+ ctx.fillText(at.text, at.x0, at.y);
242
439
  }
243
440
  ctx.restore();
441
+ return cut;
244
442
  }
245
- function drawYMarks(u, marks, style) {
443
+ function drawYMarks(u, marks, style, boxes) {
246
444
  const ctx = u.ctx;
247
445
  const pr = uPlot.pxRatio || 1;
248
446
  const left = u.bbox.left;
@@ -250,8 +448,10 @@ function drawYMarks(u, marks, style) {
250
448
  const top = u.bbox.top;
251
449
  const bottom = u.bbox.top + u.bbox.height;
252
450
  const gap = 4 * pr;
451
+ const rowH = 14 * pr;
253
452
  const textH = 11 * pr;
254
453
  const placed = [];
454
+ const cut = [];
255
455
  ctx.save();
256
456
  ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
257
457
  ctx.textBaseline = "alphabetic";
@@ -269,22 +469,23 @@ function drawYMarks(u, marks, style) {
269
469
  ctx.stroke();
270
470
  ctx.setLineDash([]);
271
471
  if (!m.label) continue;
272
- const w = ctx.measureText(m.label).width;
273
- const base = py - gap - textH < top ? py + gap + textH : py - gap;
274
- let x1 = right - gap;
275
- while (placed.some((p) => x1 - w < p.x1 + gap && x1 > p.x0 - gap && base - textH < p.y1 && base > p.y0)) {
276
- x1 = Math.min(...placed.filter((p) => base - textH < p.y1 && base > p.y0).map((p) => p.x0)) - 2 * gap;
472
+ const at = placeYLabel(m.label, { py, left, right, top, bottom, gap, rowH, textH, boxes: [...placed, ...boxes ?? []], measure: (t) => ctx.measureText(t).width });
473
+ if (!at) {
474
+ cut.push(m.label);
475
+ continue;
277
476
  }
278
- const x0 = Math.max(left + gap, x1 - w);
279
- placed.push({ x0, x1: x0 + w, y0: base - textH, y1: base });
477
+ if (at.cut) cut.push(m.label);
478
+ placed.push({ x0: at.x0, x1: at.x0 + at.w, y0: at.y - textH, y1: at.y });
479
+ boxes?.push({ kind: "y", text: at.text, full: m.label, x0: at.x0, x1: at.x0 + at.w, y0: at.y - textH, y1: at.y });
280
480
  ctx.textAlign = "left";
281
481
  ctx.lineWidth = 3 * pr;
282
482
  ctx.strokeStyle = style.halo;
283
- ctx.strokeText(m.label, x0, base);
483
+ ctx.strokeText(at.text, at.x0, at.y);
284
484
  ctx.fillStyle = color;
285
- ctx.fillText(m.label, x0, base);
485
+ ctx.fillText(at.text, at.x0, at.y);
286
486
  }
287
487
  ctx.restore();
488
+ return cut;
288
489
  }
289
490
  function nearestPoint(xs, ys, left, top) {
290
491
  let best = null;
@@ -303,7 +504,7 @@ function nearestPoint(xs, ys, left, top) {
303
504
  }
304
505
  return best;
305
506
  }
306
- function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo }) {
507
+ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo, title, zoomY, zoomOff }) {
307
508
  const lang = useShellLang();
308
509
  const theme = useThemeStore((s) => s.theme);
309
510
  const fill = height === "fill";
@@ -319,8 +520,21 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
319
520
  const width = parity ? parityBox(box.width, box.height, typeof height === "number" ? height : 0, fill) : box.width;
320
521
  const h = parity ? width : fill ? box.height : height;
321
522
  const uRef = useRef(null);
523
+ const ctlRef = useRef(null);
524
+ const explicitRef = useRef(false);
322
525
  const hostRef = useRef(null);
323
526
  const [cursor, setCursor] = useState(null);
527
+ const ids = useId();
528
+ const tableId = `${ids}-table`;
529
+ const summaryId = `${ids}-summary`;
530
+ const [zoomed, setZoomed] = useState(false);
531
+ const fullRef = useRef(null);
532
+ const zoomRef = useRef({ x: null, y: null });
533
+ const zoomedRef = useRef(false);
534
+ const kbRef = useRef(null);
535
+ const dataRef = useRef(null);
536
+ const [forced, setForced] = useState("");
537
+ const [cutKey, setCutKey] = useState("");
324
538
  const [soloPick, setSoloPick] = useState(null);
325
539
  const soloFound = solo && soloPick !== null ? series.findIndex((s) => soloKey(s) === soloPick) : -1;
326
540
  const soloIdx = soloFound >= 0 ? soloFound : null;
@@ -369,6 +583,20 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
369
583
  const dragCount = (yMarks ?? []).filter((m) => m.drag).length;
370
584
  const xUnit = x.unit ? pick(x.unit, lang) : "";
371
585
  const yUnit = y.unit ? pick(y.unit, lang) : "";
586
+ const drawn = width > 0 && h > 0;
587
+ const name = title ? pick(title, lang) : defaultChartName(lang, yLabel, xLabel);
588
+ const zoomIsOff = zoomOff !== void 0 && zoomOff !== null;
589
+ const zoomWhy = zoomIsOff ? pick(zoomOff, lang) : "";
590
+ const zoomable = !zoomIsOff && xs.length >= 2 && xs[xs.length - 1] > xs[0];
591
+ const both = Boolean(zoomY || parity);
592
+ const zoomableRef = useRef(zoomable);
593
+ zoomableRef.current = zoomable;
594
+ const bothRef = useRef(both);
595
+ bothRef.current = both;
596
+ const xTitle = xUnit ? `${xLabel} (${xUnit})` : xLabel;
597
+ const yTitle = yUnit ? `${yLabel} (${yUnit})` : yLabel;
598
+ const yAbove = !axisTitleFits(yTitle, h - 44) || forced === yTitle;
599
+ const xBelow = !axisTitleFits(xTitle, width - 16);
372
600
  const structure = JSON.stringify({
373
601
  s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s), Boolean(s.dots)]),
374
602
  x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt, ticks ? ticks.map((t) => [t.value, pick(t.label, lang)]) : null],
@@ -381,9 +609,19 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
381
609
  theme,
382
610
  lang,
383
611
  h,
384
- w: width
612
+ w: width,
613
+ ta: [yAbove, xBelow, zoomable, both]
385
614
  });
386
615
  const data = useMemo(() => [xs, ...ys], [xs, ys]);
616
+ const leastSpan = (axis) => {
617
+ const u = uRef.current;
618
+ const f = fullRef.current;
619
+ if (!u || !f) return void 0;
620
+ if (axis === "x" ? x.time || x.log || Boolean(ticks) : Boolean(y.log)) return void 0;
621
+ const pr = uPlot.pxRatio || 1;
622
+ const room = (axis === "x" ? u.bbox.width : u.bbox.height) / pr;
623
+ return leastLabelSpan(axis === "x" ? x.format : y.format, f[axis], room, range ? PARITY_TICK_SPACE : axis === "x" ? 50 : 30);
624
+ };
387
625
  const scatter = baseEntries.length > 0 && baseEntries.every(({ s }) => s.mode === "points");
388
626
  useEffect(() => {
389
627
  const el = plotRef.current;
@@ -415,13 +653,13 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
415
653
  return labels;
416
654
  };
417
655
  const near = { left: NaN, top: NaN, idx: null };
418
- const nearest = (u) => {
419
- const { left, top } = u.cursor;
656
+ const nearest = (u2) => {
657
+ const { left, top } = u2.cursor;
420
658
  if (left === void 0 || top === void 0 || left < 0 || top < 0) return null;
421
659
  if (left !== near.left || top !== near.top) {
422
- const d = u.data;
423
- const px = d[0].map((v) => v === null ? NaN : u.valToPos(v, "x"));
424
- const py = d.slice(1).map((col) => col.map((v) => v === null ? null : u.valToPos(v, "y")));
660
+ const d = u2.data;
661
+ const px = d[0].map((v) => v === null ? NaN : u2.valToPos(v, "x"));
662
+ const py = d.slice(1).map((col) => col.map((v) => v === null ? null : u2.valToPos(v, "y")));
425
663
  near.left = left;
426
664
  near.top = top;
427
665
  near.idx = nearestPoint(px, py, left, top);
@@ -430,11 +668,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
430
668
  };
431
669
  const xSteps = x.time || x.log ? void 0 : tickSteps(x.format);
432
670
  const ySteps = y.log ? void 0 : tickSteps(y.format);
671
+ const fixedRange = (r) => (_u, min, max) => explicitRef.current && min !== null && max !== null ? [min, max] : [r[0], r[1]];
433
672
  const opts = {
434
673
  width,
435
674
  height: h,
436
675
  legend: { show: false },
437
- ...scatter ? { cursor: { dataIdx: (u, sIdx, closest) => sIdx === 0 ? closest : nearest(u) ?? closest } } : {},
676
+ cursor: {
677
+ ...scatter ? { dataIdx: (u2, sIdx, closest) => sIdx === 0 ? closest : nearest(u2) ?? closest } : {},
678
+ // the brush: a drag along x (and y where both axes zoom); a drag under DRAG_THRESHOLD px is a click, not a zoom. uPlot's own setScale
679
+ // is off: a fixed range (`range`, `x.range`) turns the scale it sets back into the fixed one, so the selection is applied by the
680
+ // `setSelect` hook below, through the same operation as the wheel and the keys
681
+ drag: zoomable ? { x: true, y: both, dist: DRAG_THRESHOLD, setScale: false } : { x: false, y: false, setScale: false }
682
+ },
438
683
  // a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
439
684
  scales: {
440
685
  // a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
@@ -442,16 +687,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
442
687
  x: {
443
688
  time: Boolean(x.time),
444
689
  distr: x.log ? 3 : 1,
445
- ...range ? { range } : xExt ? { range: xExt } : x.log ? { range: (_u, min, max) => [min, max] } : {}
690
+ // a fixed range is a function that gives way to a zoom: uPlot passes even an explicit range through the scale's range, so a constant
691
+ // (or an array) undid every zoom of a chart with `x.range` or `y.range` (Tier A, 0.19.0)
692
+ ...range ? { range: fixedRange(range) } : xExt ? { range: fixedRange(xExt) } : x.log ? { range: (_u, min, max) => [min, max] } : {}
446
693
  },
447
694
  y: {
448
695
  distr: y.log ? 3 : 1,
449
- ...range ? { range } : y.range ? { range: y.range } : yLevels ? {
696
+ ...range ? { range: fixedRange(range) } : y.range ? { range: fixedRange(y.range) } : yLevels ? {
450
697
  // uPlot's own range of the data and the levels: its padding, its decade snap on a log axis
451
- range: (u, dmin, dmax) => {
698
+ range: (u2, dmin, dmax) => {
452
699
  const w = rangeWithLevels(dmin, dmax, yLevels, Boolean(y.log), 0.05);
453
700
  if (!w) return [dmin, dmax];
454
- return y.log ? uPlot.rangeLog(w[0], w[1], u.scales.y.log ?? 10, false) : uPlot.rangeNum(w[0], w[1], 0.1, true);
701
+ return y.log ? uPlot.rangeLog(w[0], w[1], u2.scales.y.log ?? 10, false) : uPlot.rangeNum(w[0], w[1], 0.1, true);
455
702
  }
456
703
  } : {}
457
704
  }
@@ -462,7 +709,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
462
709
  grid: { stroke: grid, width: 1 },
463
710
  ticks: { stroke: grid },
464
711
  font,
465
- label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
712
+ label: xBelow ? void 0 : xTitle,
466
713
  labelFont: font,
467
714
  // a log x axis labels the decades only; the ticks between stay drawn, unlabelled
468
715
  values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
@@ -480,7 +727,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
480
727
  grid: { stroke: grid, width: 1 },
481
728
  ticks: { stroke: grid },
482
729
  font,
483
- label: yUnit ? `${yLabel} (${yUnit})` : yLabel,
730
+ label: yAbove ? void 0 : yTitle,
484
731
  labelFont: font,
485
732
  values: tick(y.format, "y"),
486
733
  ...ySteps ? { incrs: ySteps } : {},
@@ -489,12 +736,12 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
489
736
  // Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
490
737
  // label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
491
738
  // made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
492
- size: (u, values) => {
739
+ size: (u2, values) => {
493
740
  if (!values || values.length === 0) return 48;
494
741
  const pr = uPlot.pxRatio || 1;
495
- u.ctx.font = `${Math.round(11 * pr)}px ${family}`;
496
- const widest = Math.max(...values.map((v) => u.ctx.measureText(v ?? "").width));
497
- return Math.ceil(widest / pr) + 30;
742
+ u2.ctx.font = `${Math.round(11 * pr)}px ${family}`;
743
+ const widest = Math.max(...values.map((v) => u2.ctx.measureText(v ?? "").width));
744
+ return Math.ceil(widest / pr) + (yAbove ? 18 : 30);
498
745
  }
499
746
  }
500
747
  ],
@@ -509,16 +756,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
509
756
  ],
510
757
  hooks: {
511
758
  setCursor: [
512
- (u) => {
513
- const idx = scatter ? nearest(u) : u.cursor.idx ?? null;
759
+ (u2) => {
760
+ const idx = scatter ? nearest(u2) : u2.cursor.idx ?? null;
514
761
  pickedRef.current = idx;
515
762
  setCursor(idx);
516
763
  onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
517
764
  }
518
765
  ],
519
766
  ready: [
520
- (u) => {
521
- u.over.addEventListener("click", () => {
767
+ (u2) => {
768
+ u2.over.addEventListener("click", () => {
522
769
  const idx = pickedRef.current;
523
770
  if (idx === null || idx === void 0 || !onPickRef.current) return;
524
771
  onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
@@ -543,7 +790,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
543
790
  if (v === liveRef.current[i]) return;
544
791
  liveRef.current[i] = v;
545
792
  onYMarkChangeRef.current?.(i, v);
546
- u.redraw(false);
793
+ u2.redraw(false);
547
794
  };
548
795
  let dragging = false;
549
796
  el2.addEventListener("pointerdown", (e) => {
@@ -555,7 +802,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
555
802
  });
556
803
  el2.addEventListener("pointermove", (e) => {
557
804
  if (!dragging) return;
558
- set(u.posToVal(e.clientY - u.over.getBoundingClientRect().top, "y"));
805
+ set(u2.posToVal(e.clientY - u2.over.getBoundingClientRect().top, "y"));
559
806
  });
560
807
  const end = (e) => {
561
808
  dragging = false;
@@ -569,68 +816,100 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
569
816
  e.preventDefault();
570
817
  set(v);
571
818
  });
572
- u.over.appendChild(el2);
819
+ u2.over.appendChild(el2);
573
820
  });
574
- if (dragCount) queueMicrotask(() => uRef.current === u && u.redraw(false));
821
+ if (dragCount) queueMicrotask(() => uRef.current === u2 && u2.redraw(false));
822
+ }
823
+ ],
824
+ setSelect: [
825
+ (u2) => {
826
+ const sel = { left: u2.select.left, top: u2.select.top, width: u2.select.width, height: u2.select.height };
827
+ if (!(sel.width > 0 || sel.height > 0)) return;
828
+ const x0 = u2.posToVal(sel.left, "x");
829
+ const x1 = u2.posToVal(sel.left + sel.width, "x");
830
+ const y0 = u2.posToVal(sel.top + sel.height, "y");
831
+ const y1 = u2.posToVal(sel.top, "y");
832
+ u2.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
833
+ ctlRef.current?.brush(sel.width > 0 ? [x0, x1] : null, bothRef.current && sel.height > 0 ? [y0, y1] : void 0);
834
+ }
835
+ ],
836
+ setScale: [
837
+ (u2, key) => {
838
+ if (key !== "x" && key !== "y") return;
839
+ const f2 = fullRef.current;
840
+ if (!f2) return;
841
+ const rx = [u2.scales.x.min ?? f2.x[0], u2.scales.x.max ?? f2.x[1]];
842
+ const ry = [u2.scales.y.min ?? f2.y[0], u2.scales.y.max ?? f2.y[1]];
843
+ hostRef.current?.setAttribute("data-view-x", `${rx[0]},${rx[1]}`);
844
+ const zx = isZoomed(rx, f2.x);
845
+ const zy = bothRef.current && isZoomed(ry, f2.y);
846
+ zoomRef.current = { x: zx ? rx : null, y: zy ? ry : null };
847
+ const z = zx || zy;
848
+ if (z !== zoomedRef.current) {
849
+ zoomedRef.current = z;
850
+ setZoomed(z);
851
+ }
852
+ u2.over.style.touchAction = zoomableRef.current ? z ? "none" : "pan-y" : "";
575
853
  }
576
854
  ],
577
855
  draw: [
578
- (u) => {
856
+ (u2) => {
579
857
  if (range) {
580
- const ctx = u.ctx;
581
- const pr = uPlot.pxRatio || 1;
858
+ const ctx = u2.ctx;
859
+ const pr2 = uPlot.pxRatio || 1;
582
860
  ctx.save();
583
861
  ctx.strokeStyle = resolveToken("--color-fg-faint", "#888");
584
- ctx.lineWidth = Math.max(1, pr);
585
- ctx.setLineDash([5 * pr, 4 * pr]);
862
+ ctx.lineWidth = Math.max(1, pr2);
863
+ ctx.setLineDash([5 * pr2, 4 * pr2]);
586
864
  ctx.beginPath();
587
- ctx.moveTo(u.valToPos(range[0], "x", true), u.valToPos(range[0], "y", true));
588
- ctx.lineTo(u.valToPos(range[1], "x", true), u.valToPos(range[1], "y", true));
865
+ ctx.moveTo(u2.valToPos(range[0], "x", true), u2.valToPos(range[0], "y", true));
866
+ ctx.lineTo(u2.valToPos(range[1], "x", true), u2.valToPos(range[1], "y", true));
589
867
  ctx.stroke();
590
868
  ctx.restore();
591
869
  }
592
870
  const dots = overlaysRef.current;
593
871
  if (dots.length) {
594
- const ctx = u.ctx;
595
- const pr = uPlot.pxRatio || 1;
596
- let drawn = 0;
872
+ const ctx = u2.ctx;
873
+ const pr2 = uPlot.pxRatio || 1;
874
+ let drawn2 = 0;
597
875
  ctx.save();
598
876
  ctx.beginPath();
599
- ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
877
+ ctx.rect(u2.bbox.left, u2.bbox.top, u2.bbox.width, u2.bbox.height);
600
878
  ctx.clip();
601
879
  for (const { s, i } of dots) {
602
880
  ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
603
881
  ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
604
882
  ctx.strokeStyle = surface;
605
- ctx.lineWidth = Math.max(1, pr);
606
- const r = (s.size ?? 9) / 2 * pr;
883
+ ctx.lineWidth = Math.max(1, pr2);
884
+ const r = (s.size ?? 9) / 2 * pr2;
607
885
  const ox = s.x ?? [];
608
886
  for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
609
887
  const v = s.values[k];
610
888
  if (v === null || v === void 0 || !Number.isFinite(v) || !Number.isFinite(ox[k])) continue;
611
- const px = u.valToPos(ox[k], "x", true);
612
- const py = u.valToPos(v, "y", true);
613
- 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;
889
+ const px = u2.valToPos(ox[k], "x", true);
890
+ const py = u2.valToPos(v, "y", true);
891
+ if (!Number.isFinite(px) || !Number.isFinite(py) || px < u2.bbox.left - 0.5 || px > u2.bbox.left + u2.bbox.width + 0.5 || py < u2.bbox.top - 0.5 || py > u2.bbox.top + u2.bbox.height + 0.5) continue;
614
892
  ctx.beginPath();
615
893
  ctx.arc(px, py, r, 0, Math.PI * 2);
616
894
  ctx.fill();
617
895
  ctx.stroke();
618
- drawn++;
896
+ drawn2++;
619
897
  }
620
898
  }
621
899
  ctx.restore();
622
- hostRef.current?.setAttribute("data-overlay-drawn", String(drawn));
900
+ hostRef.current?.setAttribute("data-overlay-drawn", String(drawn2));
623
901
  }
624
902
  const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
625
903
  const lines = yMarksRef.current ?? [];
626
- if (lines.length) drawYMarks(u, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: m.label ? pick(m.label, lang) : void 0, color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
627
- u.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
904
+ const boxes = [];
905
+ const ycut = lines.length ? drawYMarks(u2, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: m.label ? pick(m.label, lang) : void 0, color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle, boxes) : [];
906
+ u2.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
628
907
  const i = Number(el2.getAttribute("data-y-handle"));
629
908
  const m = lines[i];
630
909
  const v = liveRef.current[i];
631
910
  if (!m || v === void 0) return;
632
- const top = u.valToPos(v, "y");
633
- el2.style.display = Number.isFinite(top) && top >= 0 && top <= u.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
911
+ const top = u2.valToPos(v, "y");
912
+ el2.style.display = Number.isFinite(top) && top >= 0 && top <= u2.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
634
913
  el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
635
914
  const label = m.label ? pick(m.label, lang) : "";
636
915
  const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
@@ -639,7 +918,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
639
918
  el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
640
919
  el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
641
920
  });
642
- if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
921
+ const cut = [...ycut, ...marks?.length ? drawMarks(u2, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle, boxes) : []];
922
+ const pr = uPlot.pxRatio || 1;
923
+ const rounded = (v) => Math.round(v / pr * 10) / 10;
924
+ hostRef.current?.setAttribute("data-mark-overlaps", String(countOverlaps(boxes)));
925
+ if (boxes.length) hostRef.current?.setAttribute("data-mark-boxes", JSON.stringify(boxes.map((b) => ({ k: b.kind, t: b.text, x0: rounded(b.x0), y0: rounded(b.y0), x1: rounded(b.x1), y1: rounded(b.y1), ...b.text !== b.full ? { cut: true } : {} }))));
926
+ else hostRef.current?.removeAttribute("data-mark-boxes");
927
+ if (cut.length) hostRef.current?.setAttribute("data-mark-cut", cut.join("|"));
928
+ else hostRef.current?.removeAttribute("data-mark-cut");
929
+ const now = cut.join(CUT_SEP);
930
+ setCutKey((prev) => prev === now ? prev : now);
643
931
  }
644
932
  ]
645
933
  }
@@ -649,14 +937,60 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
649
937
  hostRef.current?.removeAttribute("data-ticks-repeated");
650
938
  }
651
939
  uRef.current?.destroy();
652
- uRef.current = new uPlot(opts, data, el);
940
+ fullRef.current = null;
941
+ dataRef.current = data;
942
+ const u = new uPlot(opts, data, el);
943
+ uRef.current = u;
944
+ u.batch(() => void 0);
945
+ const f = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
946
+ fullRef.current = f;
947
+ hostRef.current?.setAttribute("data-view-x", `${f.x[0]},${f.x[1]}`);
948
+ const ctl = createZoom(u, { plot: el, log: { x: Boolean(x.log), y: Boolean(y.log) }, both, enabled: () => zoomableRef.current, full: () => fullRef.current, explicit: explicitRef, least: leastSpan });
949
+ ctlRef.current = ctl;
950
+ u.over.style.touchAction = zoomable ? "pan-y" : "";
951
+ const held = zoomRef.current;
952
+ const meets = (r, full) => r && r[1] > full[0] && r[0] < full[1] ? [Math.max(r[0], full[0]), Math.min(r[1], full[1])] : null;
953
+ const hx = zoomable ? meets(held.x, f.x) : null;
954
+ const hy = zoomable && both ? meets(held.y, f.y) : null;
955
+ if (hx || hy) {
956
+ explicitRef.current = true;
957
+ u.batch(() => {
958
+ if (hx) u.setScale("x", { min: hx[0], max: hx[1] });
959
+ if (hy) u.setScale("y", { min: hy[0], max: hy[1] });
960
+ });
961
+ explicitRef.current = false;
962
+ } else {
963
+ zoomRef.current = { x: null, y: null };
964
+ if (zoomedRef.current) {
965
+ zoomedRef.current = false;
966
+ setZoomed(false);
967
+ }
968
+ }
969
+ if (!yAbove && yTitle) {
970
+ const pr = uPlot.pxRatio || 1;
971
+ const cy = (u.bbox.top + u.bbox.height / 2) / pr;
972
+ if (!axisTitleFits(yTitle, 2 * Math.min(cy, h - cy) - 4)) setForced(yTitle);
973
+ }
653
974
  return () => {
975
+ ctl.detach();
976
+ ctlRef.current = null;
654
977
  uRef.current?.destroy();
655
978
  uRef.current = null;
656
979
  };
657
980
  }, [structure]);
658
981
  useEffect(() => {
659
- uRef.current?.setData(data);
982
+ const u = uRef.current;
983
+ if (!u) return;
984
+ if (dataRef.current && sameData(dataRef.current, data)) return;
985
+ dataRef.current = data;
986
+ fullRef.current = null;
987
+ zoomRef.current = { x: null, y: null };
988
+ u.batch(() => u.setData(data));
989
+ fullRef.current = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
990
+ if (zoomedRef.current) {
991
+ zoomedRef.current = false;
992
+ setZoomed(false);
993
+ }
660
994
  }, [data]);
661
995
  const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
662
996
  const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
@@ -675,11 +1009,144 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
675
1009
  });
676
1010
  u.redraw(false);
677
1011
  }, [soloIdx, baseEntries]);
1012
+ const timeSpan = xs.length > 1 ? Math.abs(xs[xs.length - 1] - xs[0]) : 0;
1013
+ const xValue = (v) => x.time ? formatTime(v, lang, timeSpan) : formatNumber(v, lang, x.format ?? { digits: 4 });
678
1014
  const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
679
1015
  // an x that sits on a named tick is named by it, not by its number
680
- tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
1016
+ tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${xValue(xs[cursor])}${xUnit && !x.time ? `${NBSP}${xUnit}` : ""}`,
681
1017
  ...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}` : ""}`)
682
1018
  ].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";
1019
+ const cutTexts = cutKey ? cutKey.split(CUT_SEP) : [];
1020
+ const readFull = cutTexts.length ? [read, ...cutTexts].join(" \xB7 ") : read;
1021
+ const xText = (v) => tickLabelAt(ticks, v, lang, xSpan) ?? xValue(v);
1022
+ const yText = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : `${formatNumber(v, lang, y.format ?? { digits: 4 })}`;
1023
+ const xFormatKey = JSON.stringify(x.format ?? null);
1024
+ const yFormatKey = JSON.stringify(y.format ?? null);
1025
+ const a11y = useMemo(() => {
1026
+ if (!drawn) return null;
1027
+ const head = [xTitle, ...baseEntries.map(({ s: one }) => yUnit ? `${pick(one.label, lang)} (${yUnit})` : pick(one.label, lang)), ...overlays.map(({ s: one }) => yUnit ? `${pick(one.label, lang)} (${yUnit})` : pick(one.label, lang))];
1028
+ const kept = thinRows(xs.length);
1029
+ const blanks = (n) => Array.from({ length: n }, () => "");
1030
+ const rows = kept.map((i) => [xText(xs[i]), ...ys.map((col) => yText(col[i])), ...blanks(overlays.length)]);
1031
+ overlays.forEach(({ s: one }, o) => {
1032
+ const ox = one.x ?? [];
1033
+ for (const k of thinRows(Math.min(ox.length, one.values.length))) {
1034
+ if (!Number.isFinite(ox[k]) || one.values[k] === null || one.values[k] === void 0) continue;
1035
+ const cells = [...blanks(ys.length), ...blanks(overlays.length)];
1036
+ cells[ys.length + o] = yText(one.values[k]);
1037
+ rows.push([xText(ox[k]), ...cells]);
1038
+ }
1039
+ });
1040
+ const multi = baseEntries.length + overlays.length > 1;
1041
+ let low = null;
1042
+ let high = null;
1043
+ baseEntries.forEach(({ s: one }, k) => {
1044
+ for (let i = 0; i < xs.length; i++) {
1045
+ const v = ys[k][i];
1046
+ if (v === null || v === void 0 || !Number.isFinite(v)) continue;
1047
+ const at = `${multi ? `${pick(one.label, lang)}, ` : ""}${xLabel} ${xText(xs[i])}`;
1048
+ if (!low || v < low.v) low = { v, at };
1049
+ if (!high || v > high.v) high = { v, at };
1050
+ }
1051
+ });
1052
+ const unit = yUnit ? `${NBSP}${yUnit}` : "";
1053
+ const lo = low;
1054
+ const hi = high;
1055
+ const summary = chartSummary(lang, {
1056
+ kind: "line",
1057
+ name,
1058
+ count: xs.length,
1059
+ series: series.map((one) => pick(one.label, lang)),
1060
+ xLabel,
1061
+ xFrom: xs.length ? xText(xs[0]) : void 0,
1062
+ xTo: xs.length ? xText(xs[xs.length - 1]) : void 0,
1063
+ yLabel: yTitle,
1064
+ low: lo ? { text: `${yText(lo.v)}${unit}`, at: lo.at } : void 0,
1065
+ high: hi ? { text: `${yText(hi.v)}${unit}`, at: hi.at } : void 0
1066
+ });
1067
+ return { head, rows, summary, thinned: kept.length < xs.length };
1068
+ }, [drawn, xs, ys, baseEntries, overlays, series, lang, ticks, xSpan, xTitle, yTitle, xLabel, yUnit, name, xFormatKey, yFormatKey, x.time, timeSpan]);
1069
+ const marksId = `${ids}-marks`;
1070
+ const markRows = useMemo(() => {
1071
+ const unnamed = (yMarks ?? []).flatMap((m, k) => m.label ? [] : [k]);
1072
+ const xs2 = (marks ?? []).map((m) => [pick(m.label, lang), `${xLabel} ${xText(m.x)}${xUnit && !x.time && tickLabelAt(ticks, m.x, lang, xSpan) === null ? `${NBSP}${xUnit}` : ""}`]);
1073
+ const ys2 = (yMarks ?? []).map((m, k) => [m.label ? pick(m.label, lang) : referenceLineName(lang, unnamed.indexOf(k) + 1, unnamed.length), `${yText(m.y)}${yUnit ? `${NBSP}${yUnit}` : ""}`]);
1074
+ return [...xs2, ...ys2];
1075
+ }, [marks, yMarks, lang, xLabel, xUnit, yUnit, ticks, xSpan, xFormatKey, yFormatKey, x.time, timeSpan]);
1076
+ const onKeyDown = (e) => {
1077
+ if (e.target !== e.currentTarget) return;
1078
+ const u = uRef.current;
1079
+ const ctl = ctlRef.current;
1080
+ if (!u || !ctl) return;
1081
+ const act = keyAction(e.key, { shift: e.shiftKey, ctrl: e.ctrlKey, meta: e.metaKey, alt: e.altKey });
1082
+ if (!act) return;
1083
+ const n = xs.length;
1084
+ const stand = kbRef.current;
1085
+ switch (act) {
1086
+ case "prev":
1087
+ case "next":
1088
+ case "first":
1089
+ case "last": {
1090
+ let idx;
1091
+ if (stand === null && zoomedRef.current && (act === "next" || act === "prev")) {
1092
+ const v = ctl.view().x;
1093
+ let last = null;
1094
+ for (let i = n - 1; i >= 0; i--) {
1095
+ if (xs[i] >= v[0] && xs[i] <= v[1]) {
1096
+ last = i;
1097
+ break;
1098
+ }
1099
+ }
1100
+ idx = act === "next" ? firstVisible(xs, v[0], v[1]) : last;
1101
+ } else idx = stepIndex(stand, act, n);
1102
+ if (idx === null) return;
1103
+ kbRef.current = idx;
1104
+ ctl.reveal(xs[idx]);
1105
+ ctl.place(idx);
1106
+ break;
1107
+ }
1108
+ case "zoomIn":
1109
+ case "zoomOut": {
1110
+ if (!zoomableRef.current) return;
1111
+ const v = ctl.view();
1112
+ const ax = stand !== null ? xs[stand] : x.log && v.x[0] > 0 ? Math.sqrt(v.x[0] * v.x[1]) : (v.x[0] + v.x[1]) / 2;
1113
+ const firstY = stand !== null ? ys.map((col) => col[stand]).find((val) => val !== null && val !== void 0 && Number.isFinite(val)) : void 0;
1114
+ const ay = firstY ?? (y.log && v.y[0] > 0 ? Math.sqrt(v.y[0] * v.y[1]) : (v.y[0] + v.y[1]) / 2);
1115
+ ctl.zoomBy(act === "zoomIn" ? KEY_ZOOM : 1 / KEY_ZOOM, ax, ay);
1116
+ if (stand !== null) ctl.place(stand);
1117
+ break;
1118
+ }
1119
+ case "panPrev":
1120
+ case "panNext":
1121
+ if (!zoomableRef.current || !zoomedRef.current) return;
1122
+ ctl.panBy(act === "panNext" ? KEY_PAN : -KEY_PAN);
1123
+ break;
1124
+ case "reset":
1125
+ if (e.key === "Escape" && !zoomedRef.current) return;
1126
+ ctl.reset();
1127
+ if (stand !== null) ctl.place(stand);
1128
+ e.stopPropagation();
1129
+ break;
1130
+ case "activate": {
1131
+ if (stand === null || !onPickRef.current) return;
1132
+ onPickRef.current(orderRef.current ? orderRef.current[stand] : stand);
1133
+ break;
1134
+ }
1135
+ }
1136
+ e.preventDefault();
1137
+ };
1138
+ const onBlur = (e) => {
1139
+ if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;
1140
+ if (kbRef.current === null) return;
1141
+ kbRef.current = null;
1142
+ ctlRef.current?.clear();
1143
+ };
1144
+ const resetZoom = () => {
1145
+ ctlRef.current?.reset();
1146
+ plotRef.current?.focus({ preventScroll: true });
1147
+ };
1148
+ const moved = [xBelow ? "x" : "", yAbove ? "y" : ""].filter(Boolean).join("");
1149
+ const readoutEl = /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: readFull, children: readFull });
683
1150
  return /* @__PURE__ */ jsxs(
684
1151
  "div",
685
1152
  {
@@ -697,8 +1164,48 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
697
1164
  "data-y-drag": dragCount ? String(dragCount) : void 0,
698
1165
  "data-overlay": overlays.length ? String(overlays.length) : void 0,
699
1166
  "data-solo": soloIdx !== null ? String(soloIdx) : void 0,
1167
+ "data-chart-host": drawn ? "line" : void 0,
1168
+ "data-points": drawn ? String(xs.length) : void 0,
1169
+ "data-zoomable": drawn ? zoomable ? "1" : "0" : void 0,
1170
+ "data-zoomed": drawn && zoomed ? "1" : void 0,
1171
+ "data-zoom-off": drawn && zoomIsOff ? zoomWhy : void 0,
1172
+ "data-axis-title-moved": drawn && moved ? moved : void 0,
700
1173
  children: [
701
- /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
1174
+ drawn && yAbove && yTitle && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "y", children: yTitle }),
1175
+ /* @__PURE__ */ jsx(
1176
+ "div",
1177
+ {
1178
+ ref: setPlot,
1179
+ className: "caos-chart-plot",
1180
+ title: cutTexts.length ? cutTexts.join("; ") : void 0,
1181
+ ...drawn ? {
1182
+ tabIndex: 0,
1183
+ role: "group",
1184
+ "aria-roledescription": CHART_ROLE[lang],
1185
+ "aria-label": name,
1186
+ "aria-describedby": summaryId,
1187
+ "aria-details": markRows.length ? `${tableId} ${marksId}` : tableId,
1188
+ "aria-description": zoomable ? ZOOM_HINT[lang] : void 0,
1189
+ "data-chart-focus": "",
1190
+ onKeyDown,
1191
+ onBlur
1192
+ } : {}
1193
+ }
1194
+ ),
1195
+ drawn && xBelow && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "x", children: xTitle }),
1196
+ drawn && a11y && /* @__PURE__ */ jsx(ChartSummary, { id: summaryId, text: a11y.summary }),
1197
+ drawn && markRows.length > 0 && /* @__PURE__ */ jsx("div", { className: "sr-only", "data-chart-marks-box": "", children: /* @__PURE__ */ jsxs("table", { id: marksId, className: "caos-chart-table", "data-chart-marks": "", children: [
1198
+ /* @__PURE__ */ jsx("caption", { children: lang === "es" ? `Marcas de ${name}` : `Marks of ${name}` }),
1199
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1200
+ /* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Marca" : "Mark" }),
1201
+ /* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Posici\xF3n" : "Position" })
1202
+ ] }) }),
1203
+ /* @__PURE__ */ jsx("tbody", { children: markRows.map((row, r) => /* @__PURE__ */ jsxs("tr", { children: [
1204
+ /* @__PURE__ */ jsx("th", { scope: "row", children: row[0] }),
1205
+ /* @__PURE__ */ jsx("td", { children: row[1] })
1206
+ ] }, r)) })
1207
+ ] }) }),
1208
+ drawn && a11y && /* @__PURE__ */ jsx(ChartDataTable, { id: tableId, caption: a11y.thinned ? `${name} (${lang === "es" ? "una de cada varias filas" : "every few rows"})` : name, head: a11y.head, rows: a11y.rows }),
702
1209
  (series.length > 1 || solo) && /* @__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__ */ jsx("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": seriesStyle(s, i).dash ? "1" : void 0, children: solo ? /* @__PURE__ */ jsxs("button", { type: "button", className: "caos-chart-key-button", "aria-pressed": soloIdx === i, onClick: () => setSoloPick(soloIdx === i ? null : soloKey(s)), children: [
703
1210
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
704
1211
  pick(s.label, lang)
@@ -706,10 +1213,13 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
706
1213
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
707
1214
  pick(s.label, lang)
708
1215
  ] }) }, i)) }),
709
- /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
1216
+ drawn ? /* @__PURE__ */ jsxs("div", { className: "caos-chart-foot", children: [
1217
+ readoutEl,
1218
+ zoomed && zoomable && /* @__PURE__ */ jsx(ResetZoomButton, { lang, onReset: resetZoom })
1219
+ ] }) : readoutEl
710
1220
  ]
711
1221
  }
712
1222
  );
713
1223
  }
714
1224
 
715
- export { LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, soloKey, soloStyle, sortOrder, tickLabelAt, tickSplits, tickSteps, xScaleRange };
1225
+ export { LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, seriesStyle, smallestPositive, soloKey, soloStyle, sortOrder, tickLabelAt, tickSplits, tickSteps, xScaleRange };