@fasl-work/caos-app-shell 0.18.0 → 0.19.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.
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, dateNames, 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-FXPSZD6X.js';
2
+ export { referenceLineName, repeatedLabels } from './chunk-FXPSZD6X.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,20 @@ 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,
675
+ // the month and weekday names of a time axis in the page language: uPlot's own are English (0.19.1, SHC-099)
676
+ ...x.time ? { fmtDate: /* @__PURE__ */ ((names) => (tpl) => uPlot.fmtDate(tpl, names))(dateNames(lang)) } : {},
436
677
  legend: { show: false },
437
- ...scatter ? { cursor: { dataIdx: (u, sIdx, closest) => sIdx === 0 ? closest : nearest(u) ?? closest } } : {},
678
+ cursor: {
679
+ ...scatter ? { dataIdx: (u2, sIdx, closest) => sIdx === 0 ? closest : nearest(u2) ?? closest } : {},
680
+ // 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
681
+ // 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
682
+ // `setSelect` hook below, through the same operation as the wheel and the keys
683
+ drag: zoomable ? { x: true, y: both, dist: DRAG_THRESHOLD, setScale: false } : { x: false, y: false, setScale: false }
684
+ },
438
685
  // a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
439
686
  scales: {
440
687
  // a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
@@ -442,16 +689,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
442
689
  x: {
443
690
  time: Boolean(x.time),
444
691
  distr: x.log ? 3 : 1,
445
- ...range ? { range } : xExt ? { range: xExt } : x.log ? { range: (_u, min, max) => [min, max] } : {}
692
+ // 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
693
+ // (or an array) undid every zoom of a chart with `x.range` or `y.range` (Tier A, 0.19.0)
694
+ ...range ? { range: fixedRange(range) } : xExt ? { range: fixedRange(xExt) } : x.log ? { range: (_u, min, max) => [min, max] } : {}
446
695
  },
447
696
  y: {
448
697
  distr: y.log ? 3 : 1,
449
- ...range ? { range } : y.range ? { range: y.range } : yLevels ? {
698
+ ...range ? { range: fixedRange(range) } : y.range ? { range: fixedRange(y.range) } : yLevels ? {
450
699
  // uPlot's own range of the data and the levels: its padding, its decade snap on a log axis
451
- range: (u, dmin, dmax) => {
700
+ range: (u2, dmin, dmax) => {
452
701
  const w = rangeWithLevels(dmin, dmax, yLevels, Boolean(y.log), 0.05);
453
702
  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);
703
+ 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
704
  }
456
705
  } : {}
457
706
  }
@@ -462,7 +711,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
462
711
  grid: { stroke: grid, width: 1 },
463
712
  ticks: { stroke: grid },
464
713
  font,
465
- label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
714
+ label: xBelow ? void 0 : xTitle,
466
715
  labelFont: font,
467
716
  // a log x axis labels the decades only; the ticks between stay drawn, unlabelled
468
717
  values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
@@ -480,7 +729,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
480
729
  grid: { stroke: grid, width: 1 },
481
730
  ticks: { stroke: grid },
482
731
  font,
483
- label: yUnit ? `${yLabel} (${yUnit})` : yLabel,
732
+ label: yAbove ? void 0 : yTitle,
484
733
  labelFont: font,
485
734
  values: tick(y.format, "y"),
486
735
  ...ySteps ? { incrs: ySteps } : {},
@@ -489,12 +738,12 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
489
738
  // Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
490
739
  // label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
491
740
  // made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
492
- size: (u, values) => {
741
+ size: (u2, values) => {
493
742
  if (!values || values.length === 0) return 48;
494
743
  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;
744
+ u2.ctx.font = `${Math.round(11 * pr)}px ${family}`;
745
+ const widest = Math.max(...values.map((v) => u2.ctx.measureText(v ?? "").width));
746
+ return Math.ceil(widest / pr) + (yAbove ? 18 : 30);
498
747
  }
499
748
  }
500
749
  ],
@@ -509,16 +758,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
509
758
  ],
510
759
  hooks: {
511
760
  setCursor: [
512
- (u) => {
513
- const idx = scatter ? nearest(u) : u.cursor.idx ?? null;
761
+ (u2) => {
762
+ const idx = scatter ? nearest(u2) : u2.cursor.idx ?? null;
514
763
  pickedRef.current = idx;
515
764
  setCursor(idx);
516
765
  onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
517
766
  }
518
767
  ],
519
768
  ready: [
520
- (u) => {
521
- u.over.addEventListener("click", () => {
769
+ (u2) => {
770
+ u2.over.addEventListener("click", () => {
522
771
  const idx = pickedRef.current;
523
772
  if (idx === null || idx === void 0 || !onPickRef.current) return;
524
773
  onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
@@ -543,7 +792,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
543
792
  if (v === liveRef.current[i]) return;
544
793
  liveRef.current[i] = v;
545
794
  onYMarkChangeRef.current?.(i, v);
546
- u.redraw(false);
795
+ u2.redraw(false);
547
796
  };
548
797
  let dragging = false;
549
798
  el2.addEventListener("pointerdown", (e) => {
@@ -555,7 +804,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
555
804
  });
556
805
  el2.addEventListener("pointermove", (e) => {
557
806
  if (!dragging) return;
558
- set(u.posToVal(e.clientY - u.over.getBoundingClientRect().top, "y"));
807
+ set(u2.posToVal(e.clientY - u2.over.getBoundingClientRect().top, "y"));
559
808
  });
560
809
  const end = (e) => {
561
810
  dragging = false;
@@ -569,68 +818,100 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
569
818
  e.preventDefault();
570
819
  set(v);
571
820
  });
572
- u.over.appendChild(el2);
821
+ u2.over.appendChild(el2);
573
822
  });
574
- if (dragCount) queueMicrotask(() => uRef.current === u && u.redraw(false));
823
+ if (dragCount) queueMicrotask(() => uRef.current === u2 && u2.redraw(false));
824
+ }
825
+ ],
826
+ setSelect: [
827
+ (u2) => {
828
+ const sel = { left: u2.select.left, top: u2.select.top, width: u2.select.width, height: u2.select.height };
829
+ if (!(sel.width > 0 || sel.height > 0)) return;
830
+ const x0 = u2.posToVal(sel.left, "x");
831
+ const x1 = u2.posToVal(sel.left + sel.width, "x");
832
+ const y0 = u2.posToVal(sel.top + sel.height, "y");
833
+ const y1 = u2.posToVal(sel.top, "y");
834
+ u2.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
835
+ ctlRef.current?.brush(sel.width > 0 ? [x0, x1] : null, bothRef.current && sel.height > 0 ? [y0, y1] : void 0);
836
+ }
837
+ ],
838
+ setScale: [
839
+ (u2, key) => {
840
+ if (key !== "x" && key !== "y") return;
841
+ const f2 = fullRef.current;
842
+ if (!f2) return;
843
+ const rx = [u2.scales.x.min ?? f2.x[0], u2.scales.x.max ?? f2.x[1]];
844
+ const ry = [u2.scales.y.min ?? f2.y[0], u2.scales.y.max ?? f2.y[1]];
845
+ hostRef.current?.setAttribute("data-view-x", `${rx[0]},${rx[1]}`);
846
+ const zx = isZoomed(rx, f2.x);
847
+ const zy = bothRef.current && isZoomed(ry, f2.y);
848
+ zoomRef.current = { x: zx ? rx : null, y: zy ? ry : null };
849
+ const z = zx || zy;
850
+ if (z !== zoomedRef.current) {
851
+ zoomedRef.current = z;
852
+ setZoomed(z);
853
+ }
854
+ u2.over.style.touchAction = zoomableRef.current ? z ? "none" : "pan-y" : "";
575
855
  }
576
856
  ],
577
857
  draw: [
578
- (u) => {
858
+ (u2) => {
579
859
  if (range) {
580
- const ctx = u.ctx;
581
- const pr = uPlot.pxRatio || 1;
860
+ const ctx = u2.ctx;
861
+ const pr2 = uPlot.pxRatio || 1;
582
862
  ctx.save();
583
863
  ctx.strokeStyle = resolveToken("--color-fg-faint", "#888");
584
- ctx.lineWidth = Math.max(1, pr);
585
- ctx.setLineDash([5 * pr, 4 * pr]);
864
+ ctx.lineWidth = Math.max(1, pr2);
865
+ ctx.setLineDash([5 * pr2, 4 * pr2]);
586
866
  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));
867
+ ctx.moveTo(u2.valToPos(range[0], "x", true), u2.valToPos(range[0], "y", true));
868
+ ctx.lineTo(u2.valToPos(range[1], "x", true), u2.valToPos(range[1], "y", true));
589
869
  ctx.stroke();
590
870
  ctx.restore();
591
871
  }
592
872
  const dots = overlaysRef.current;
593
873
  if (dots.length) {
594
- const ctx = u.ctx;
595
- const pr = uPlot.pxRatio || 1;
596
- let drawn = 0;
874
+ const ctx = u2.ctx;
875
+ const pr2 = uPlot.pxRatio || 1;
876
+ let drawn2 = 0;
597
877
  ctx.save();
598
878
  ctx.beginPath();
599
- ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
879
+ ctx.rect(u2.bbox.left, u2.bbox.top, u2.bbox.width, u2.bbox.height);
600
880
  ctx.clip();
601
881
  for (const { s, i } of dots) {
602
882
  ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
603
883
  ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
604
884
  ctx.strokeStyle = surface;
605
- ctx.lineWidth = Math.max(1, pr);
606
- const r = (s.size ?? 9) / 2 * pr;
885
+ ctx.lineWidth = Math.max(1, pr2);
886
+ const r = (s.size ?? 9) / 2 * pr2;
607
887
  const ox = s.x ?? [];
608
888
  for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
609
889
  const v = s.values[k];
610
890
  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;
891
+ const px = u2.valToPos(ox[k], "x", true);
892
+ const py = u2.valToPos(v, "y", true);
893
+ 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
894
  ctx.beginPath();
615
895
  ctx.arc(px, py, r, 0, Math.PI * 2);
616
896
  ctx.fill();
617
897
  ctx.stroke();
618
- drawn++;
898
+ drawn2++;
619
899
  }
620
900
  }
621
901
  ctx.restore();
622
- hostRef.current?.setAttribute("data-overlay-drawn", String(drawn));
902
+ hostRef.current?.setAttribute("data-overlay-drawn", String(drawn2));
623
903
  }
624
904
  const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
625
905
  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) => {
906
+ const boxes = [];
907
+ 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) : [];
908
+ u2.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
628
909
  const i = Number(el2.getAttribute("data-y-handle"));
629
910
  const m = lines[i];
630
911
  const v = liveRef.current[i];
631
912
  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";
913
+ const top = u2.valToPos(v, "y");
914
+ el2.style.display = Number.isFinite(top) && top >= 0 && top <= u2.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
634
915
  el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
635
916
  const label = m.label ? pick(m.label, lang) : "";
636
917
  const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
@@ -639,7 +920,16 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
639
920
  el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
640
921
  el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
641
922
  });
642
- if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
923
+ const cut = [...ycut, ...marks?.length ? drawMarks(u2, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle, boxes) : []];
924
+ const pr = uPlot.pxRatio || 1;
925
+ const rounded = (v) => Math.round(v / pr * 10) / 10;
926
+ hostRef.current?.setAttribute("data-mark-overlaps", String(countOverlaps(boxes)));
927
+ 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 } : {} }))));
928
+ else hostRef.current?.removeAttribute("data-mark-boxes");
929
+ if (cut.length) hostRef.current?.setAttribute("data-mark-cut", cut.join("|"));
930
+ else hostRef.current?.removeAttribute("data-mark-cut");
931
+ const now = cut.join(CUT_SEP);
932
+ setCutKey((prev) => prev === now ? prev : now);
643
933
  }
644
934
  ]
645
935
  }
@@ -649,14 +939,60 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
649
939
  hostRef.current?.removeAttribute("data-ticks-repeated");
650
940
  }
651
941
  uRef.current?.destroy();
652
- uRef.current = new uPlot(opts, data, el);
942
+ fullRef.current = null;
943
+ dataRef.current = data;
944
+ const u = new uPlot(opts, data, el);
945
+ uRef.current = u;
946
+ u.batch(() => void 0);
947
+ const f = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
948
+ fullRef.current = f;
949
+ hostRef.current?.setAttribute("data-view-x", `${f.x[0]},${f.x[1]}`);
950
+ 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 });
951
+ ctlRef.current = ctl;
952
+ u.over.style.touchAction = zoomable ? "pan-y" : "";
953
+ const held = zoomRef.current;
954
+ 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;
955
+ const hx = zoomable ? meets(held.x, f.x) : null;
956
+ const hy = zoomable && both ? meets(held.y, f.y) : null;
957
+ if (hx || hy) {
958
+ explicitRef.current = true;
959
+ u.batch(() => {
960
+ if (hx) u.setScale("x", { min: hx[0], max: hx[1] });
961
+ if (hy) u.setScale("y", { min: hy[0], max: hy[1] });
962
+ });
963
+ explicitRef.current = false;
964
+ } else {
965
+ zoomRef.current = { x: null, y: null };
966
+ if (zoomedRef.current) {
967
+ zoomedRef.current = false;
968
+ setZoomed(false);
969
+ }
970
+ }
971
+ if (!yAbove && yTitle) {
972
+ const pr = uPlot.pxRatio || 1;
973
+ const cy = (u.bbox.top + u.bbox.height / 2) / pr;
974
+ if (!axisTitleFits(yTitle, 2 * Math.min(cy, h - cy) - 4)) setForced(yTitle);
975
+ }
653
976
  return () => {
977
+ ctl.detach();
978
+ ctlRef.current = null;
654
979
  uRef.current?.destroy();
655
980
  uRef.current = null;
656
981
  };
657
982
  }, [structure]);
658
983
  useEffect(() => {
659
- uRef.current?.setData(data);
984
+ const u = uRef.current;
985
+ if (!u) return;
986
+ if (dataRef.current && sameData(dataRef.current, data)) return;
987
+ dataRef.current = data;
988
+ fullRef.current = null;
989
+ zoomRef.current = { x: null, y: null };
990
+ u.batch(() => u.setData(data));
991
+ fullRef.current = { x: [u.scales.x.min ?? 0, u.scales.x.max ?? 1], y: [u.scales.y.min ?? 0, u.scales.y.max ?? 1] };
992
+ if (zoomedRef.current) {
993
+ zoomedRef.current = false;
994
+ setZoomed(false);
995
+ }
660
996
  }, [data]);
661
997
  const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
662
998
  const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
@@ -675,11 +1011,144 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
675
1011
  });
676
1012
  u.redraw(false);
677
1013
  }, [soloIdx, baseEntries]);
1014
+ const timeSpan = xs.length > 1 ? Math.abs(xs[xs.length - 1] - xs[0]) : 0;
1015
+ const xValue = (v) => x.time ? formatTime(v, lang, timeSpan) : formatNumber(v, lang, x.format ?? { digits: 4 });
678
1016
  const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
679
1017
  // 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}` : ""}`,
1018
+ tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${xValue(xs[cursor])}${xUnit && !x.time ? `${NBSP}${xUnit}` : ""}`,
681
1019
  ...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
1020
  ].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";
1021
+ const cutTexts = cutKey ? cutKey.split(CUT_SEP) : [];
1022
+ const readFull = cutTexts.length ? [read, ...cutTexts].join(" \xB7 ") : read;
1023
+ const xText = (v) => tickLabelAt(ticks, v, lang, xSpan) ?? xValue(v);
1024
+ const yText = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : `${formatNumber(v, lang, y.format ?? { digits: 4 })}`;
1025
+ const xFormatKey = JSON.stringify(x.format ?? null);
1026
+ const yFormatKey = JSON.stringify(y.format ?? null);
1027
+ const a11y = useMemo(() => {
1028
+ if (!drawn) return null;
1029
+ 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))];
1030
+ const kept = thinRows(xs.length);
1031
+ const blanks = (n) => Array.from({ length: n }, () => "");
1032
+ const rows = kept.map((i) => [xText(xs[i]), ...ys.map((col) => yText(col[i])), ...blanks(overlays.length)]);
1033
+ overlays.forEach(({ s: one }, o) => {
1034
+ const ox = one.x ?? [];
1035
+ for (const k of thinRows(Math.min(ox.length, one.values.length))) {
1036
+ if (!Number.isFinite(ox[k]) || one.values[k] === null || one.values[k] === void 0) continue;
1037
+ const cells = [...blanks(ys.length), ...blanks(overlays.length)];
1038
+ cells[ys.length + o] = yText(one.values[k]);
1039
+ rows.push([xText(ox[k]), ...cells]);
1040
+ }
1041
+ });
1042
+ const multi = baseEntries.length + overlays.length > 1;
1043
+ let low = null;
1044
+ let high = null;
1045
+ baseEntries.forEach(({ s: one }, k) => {
1046
+ for (let i = 0; i < xs.length; i++) {
1047
+ const v = ys[k][i];
1048
+ if (v === null || v === void 0 || !Number.isFinite(v)) continue;
1049
+ const at = `${multi ? `${pick(one.label, lang)}, ` : ""}${xLabel} ${xText(xs[i])}`;
1050
+ if (!low || v < low.v) low = { v, at };
1051
+ if (!high || v > high.v) high = { v, at };
1052
+ }
1053
+ });
1054
+ const unit = yUnit ? `${NBSP}${yUnit}` : "";
1055
+ const lo = low;
1056
+ const hi = high;
1057
+ const summary = chartSummary(lang, {
1058
+ kind: "line",
1059
+ name,
1060
+ count: xs.length,
1061
+ series: series.map((one) => pick(one.label, lang)),
1062
+ xLabel,
1063
+ xFrom: xs.length ? xText(xs[0]) : void 0,
1064
+ xTo: xs.length ? xText(xs[xs.length - 1]) : void 0,
1065
+ yLabel: yTitle,
1066
+ low: lo ? { text: `${yText(lo.v)}${unit}`, at: lo.at } : void 0,
1067
+ high: hi ? { text: `${yText(hi.v)}${unit}`, at: hi.at } : void 0
1068
+ });
1069
+ return { head, rows, summary, thinned: kept.length < xs.length };
1070
+ }, [drawn, xs, ys, baseEntries, overlays, series, lang, ticks, xSpan, xTitle, yTitle, xLabel, yUnit, name, xFormatKey, yFormatKey, x.time, timeSpan]);
1071
+ const marksId = `${ids}-marks`;
1072
+ const markRows = useMemo(() => {
1073
+ const unnamed = (yMarks ?? []).flatMap((m, k) => m.label ? [] : [k]);
1074
+ 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}` : ""}`]);
1075
+ 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}` : ""}`]);
1076
+ return [...xs2, ...ys2];
1077
+ }, [marks, yMarks, lang, xLabel, xUnit, yUnit, ticks, xSpan, xFormatKey, yFormatKey, x.time, timeSpan]);
1078
+ const onKeyDown = (e) => {
1079
+ if (e.target !== e.currentTarget) return;
1080
+ const u = uRef.current;
1081
+ const ctl = ctlRef.current;
1082
+ if (!u || !ctl) return;
1083
+ const act = keyAction(e.key, { shift: e.shiftKey, ctrl: e.ctrlKey, meta: e.metaKey, alt: e.altKey });
1084
+ if (!act) return;
1085
+ const n = xs.length;
1086
+ const stand = kbRef.current;
1087
+ switch (act) {
1088
+ case "prev":
1089
+ case "next":
1090
+ case "first":
1091
+ case "last": {
1092
+ let idx;
1093
+ if (stand === null && zoomedRef.current && (act === "next" || act === "prev")) {
1094
+ const v = ctl.view().x;
1095
+ let last = null;
1096
+ for (let i = n - 1; i >= 0; i--) {
1097
+ if (xs[i] >= v[0] && xs[i] <= v[1]) {
1098
+ last = i;
1099
+ break;
1100
+ }
1101
+ }
1102
+ idx = act === "next" ? firstVisible(xs, v[0], v[1]) : last;
1103
+ } else idx = stepIndex(stand, act, n);
1104
+ if (idx === null) return;
1105
+ kbRef.current = idx;
1106
+ ctl.reveal(xs[idx]);
1107
+ ctl.place(idx);
1108
+ break;
1109
+ }
1110
+ case "zoomIn":
1111
+ case "zoomOut": {
1112
+ if (!zoomableRef.current) return;
1113
+ const v = ctl.view();
1114
+ 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;
1115
+ const firstY = stand !== null ? ys.map((col) => col[stand]).find((val) => val !== null && val !== void 0 && Number.isFinite(val)) : void 0;
1116
+ const ay = firstY ?? (y.log && v.y[0] > 0 ? Math.sqrt(v.y[0] * v.y[1]) : (v.y[0] + v.y[1]) / 2);
1117
+ ctl.zoomBy(act === "zoomIn" ? KEY_ZOOM : 1 / KEY_ZOOM, ax, ay);
1118
+ if (stand !== null) ctl.place(stand);
1119
+ break;
1120
+ }
1121
+ case "panPrev":
1122
+ case "panNext":
1123
+ if (!zoomableRef.current || !zoomedRef.current) return;
1124
+ ctl.panBy(act === "panNext" ? KEY_PAN : -KEY_PAN);
1125
+ break;
1126
+ case "reset":
1127
+ if (e.key === "Escape" && !zoomedRef.current) return;
1128
+ ctl.reset();
1129
+ if (stand !== null) ctl.place(stand);
1130
+ e.stopPropagation();
1131
+ break;
1132
+ case "activate": {
1133
+ if (stand === null || !onPickRef.current) return;
1134
+ onPickRef.current(orderRef.current ? orderRef.current[stand] : stand);
1135
+ break;
1136
+ }
1137
+ }
1138
+ e.preventDefault();
1139
+ };
1140
+ const onBlur = (e) => {
1141
+ if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;
1142
+ if (kbRef.current === null) return;
1143
+ kbRef.current = null;
1144
+ ctlRef.current?.clear();
1145
+ };
1146
+ const resetZoom = () => {
1147
+ ctlRef.current?.reset();
1148
+ plotRef.current?.focus({ preventScroll: true });
1149
+ };
1150
+ const moved = [xBelow ? "x" : "", yAbove ? "y" : ""].filter(Boolean).join("");
1151
+ const readoutEl = /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: readFull, children: readFull });
683
1152
  return /* @__PURE__ */ jsxs(
684
1153
  "div",
685
1154
  {
@@ -697,8 +1166,48 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
697
1166
  "data-y-drag": dragCount ? String(dragCount) : void 0,
698
1167
  "data-overlay": overlays.length ? String(overlays.length) : void 0,
699
1168
  "data-solo": soloIdx !== null ? String(soloIdx) : void 0,
1169
+ "data-chart-host": drawn ? "line" : void 0,
1170
+ "data-points": drawn ? String(xs.length) : void 0,
1171
+ "data-zoomable": drawn ? zoomable ? "1" : "0" : void 0,
1172
+ "data-zoomed": drawn && zoomed ? "1" : void 0,
1173
+ "data-zoom-off": drawn && zoomIsOff ? zoomWhy : void 0,
1174
+ "data-axis-title-moved": drawn && moved ? moved : void 0,
700
1175
  children: [
701
- /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
1176
+ drawn && yAbove && yTitle && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "y", children: yTitle }),
1177
+ /* @__PURE__ */ jsx(
1178
+ "div",
1179
+ {
1180
+ ref: setPlot,
1181
+ className: "caos-chart-plot",
1182
+ title: cutTexts.length ? cutTexts.join("; ") : void 0,
1183
+ ...drawn ? {
1184
+ tabIndex: 0,
1185
+ role: "group",
1186
+ "aria-roledescription": CHART_ROLE[lang],
1187
+ "aria-label": name,
1188
+ "aria-describedby": summaryId,
1189
+ "aria-details": markRows.length ? `${tableId} ${marksId}` : tableId,
1190
+ "aria-description": zoomable ? ZOOM_HINT[lang] : void 0,
1191
+ "data-chart-focus": "",
1192
+ onKeyDown,
1193
+ onBlur
1194
+ } : {}
1195
+ }
1196
+ ),
1197
+ drawn && xBelow && /* @__PURE__ */ jsx("p", { className: "caos-chart-axis-title", "data-axis-title": "x", children: xTitle }),
1198
+ drawn && a11y && /* @__PURE__ */ jsx(ChartSummary, { id: summaryId, text: a11y.summary }),
1199
+ 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: [
1200
+ /* @__PURE__ */ jsx("caption", { children: lang === "es" ? `Marcas de ${name}` : `Marks of ${name}` }),
1201
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1202
+ /* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Marca" : "Mark" }),
1203
+ /* @__PURE__ */ jsx("th", { scope: "col", children: lang === "es" ? "Posici\xF3n" : "Position" })
1204
+ ] }) }),
1205
+ /* @__PURE__ */ jsx("tbody", { children: markRows.map((row, r) => /* @__PURE__ */ jsxs("tr", { children: [
1206
+ /* @__PURE__ */ jsx("th", { scope: "row", children: row[0] }),
1207
+ /* @__PURE__ */ jsx("td", { children: row[1] })
1208
+ ] }, r)) })
1209
+ ] }) }),
1210
+ 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
1211
  (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
1212
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
704
1213
  pick(s.label, lang)
@@ -706,10 +1215,13 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
706
1215
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
707
1216
  pick(s.label, lang)
708
1217
  ] }) }, i)) }),
709
- /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
1218
+ drawn ? /* @__PURE__ */ jsxs("div", { className: "caos-chart-foot", children: [
1219
+ readoutEl,
1220
+ zoomed && zoomable && /* @__PURE__ */ jsx(ResetZoomButton, { lang, onReset: resetZoom })
1221
+ ] }) : readoutEl
710
1222
  ]
711
1223
  }
712
1224
  );
713
1225
  }
714
1226
 
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 };
1227
+ 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 };