@fasl-work/caos-app-shell 0.17.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,9 +1,210 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-YECZXGVG.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';
2
3
  import './chunk-33WTUQ7Q.js';
3
- import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
+ import { useRef, useCallback, useState, useId, useEffect, useMemo } from 'react';
4
5
  import uPlot from 'uplot';
5
- import { jsxs, jsx } from 'react/jsx-runtime';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
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
+ }
7
208
  var YMARK_TONE_TOKENS = {
8
209
  warn: "--color-warn",
9
210
  bad: "--color-bad",
@@ -11,6 +212,52 @@ var YMARK_TONE_TOKENS = {
11
212
  neutral: "--color-fg-subtle"
12
213
  };
13
214
  var ROTATION = ["--color-accent", "--color-magenta", "--color-accent-2", "--color-good", "--color-warn", "--color-bad"];
215
+ var SOLO_DIM_ALPHA = 0.12;
216
+ function soloStyle(solo, i, width) {
217
+ return solo !== null && solo !== i ? { alpha: SOLO_DIM_ALPHA, width: 1 } : { alpha: 1, width };
218
+ }
219
+ function tickSplits(ticks, min, max, log = false) {
220
+ const eps = Math.abs(max - min) * 1e-9;
221
+ const values = ticks.map((t) => t.value).filter((v) => Number.isFinite(v) && (!log || v > 0) && v >= min - eps && v <= max + eps);
222
+ return [...new Set(values)].sort((a, b) => a - b);
223
+ }
224
+ function tickLabelAt(ticks, x, lang, span = 0) {
225
+ const eps = Math.abs(span) * 1e-9;
226
+ const t = ticks?.find((one) => Math.abs(one.value - x) <= eps);
227
+ return t ? pick(t.label, lang) : null;
228
+ }
229
+ function namedRepeats(labels) {
230
+ const seen = /* @__PURE__ */ new Set();
231
+ const names = [];
232
+ let n = 0;
233
+ for (const l of labels) {
234
+ if (!l) continue;
235
+ if (seen.has(l)) {
236
+ n++;
237
+ if (!names.includes(l)) names.push(l);
238
+ } else seen.add(l);
239
+ }
240
+ return { n, names };
241
+ }
242
+ function soloKey(s) {
243
+ return s.id ?? pick(s.label, "en");
244
+ }
245
+ function xScaleRange(xs, ext, range, log) {
246
+ const given = range && !(log && Math.min(range[0], range[1]) <= 0) ? range : void 0;
247
+ if (!ext && !given || xs.length === 0) return null;
248
+ let lo = xs[0];
249
+ let hi = xs[xs.length - 1];
250
+ if (ext) {
251
+ lo = Math.min(lo, ext.x[0]);
252
+ hi = Math.max(hi, ext.x[1]);
253
+ }
254
+ if (given) {
255
+ lo = Math.min(lo, given[0], given[1]);
256
+ hi = Math.max(hi, given[0], given[1]);
257
+ }
258
+ if (log && !(lo > 0)) return null;
259
+ return hi > lo ? [lo, hi] : log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
260
+ }
14
261
  function seriesStyle(s, i) {
15
262
  const color = s.color ?? ROTATION[i % ROTATION.length];
16
263
  const dash = s.dash ?? (!s.color && i >= ROTATION.length ? [6, 4] : void 0);
@@ -150,19 +397,16 @@ var DECADE_AXIS = {
150
397
  };
151
398
  var PARITY_TICK_SPACE = 40;
152
399
  var HANDLE_HEIGHT = 14;
153
- function repeatedLabels(labels) {
154
- let n = 0;
155
- for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
156
- return n;
157
- }
158
- function drawMarks(u, marks, style) {
400
+ var CUT_SEP = String.fromCharCode(1);
401
+ function drawMarks(u, marks, style, boxes = []) {
159
402
  const ctx = u.ctx;
160
403
  const pr = uPlot.pxRatio || 1;
161
404
  const left = u.bbox.left;
162
405
  const right = u.bbox.left + u.bbox.width;
163
406
  const gap = 4 * pr;
164
407
  const rowH = 14 * pr;
165
- const placed = [];
408
+ const textH = 11 * pr;
409
+ const cut = [];
166
410
  ctx.save();
167
411
  ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
168
412
  ctx.textBaseline = "alphabetic";
@@ -178,24 +422,25 @@ function drawMarks(u, marks, style) {
178
422
  ctx.lineTo(px, u.bbox.top + u.bbox.height);
179
423
  ctx.stroke();
180
424
  ctx.setLineDash([]);
181
- const w = ctx.measureText(m.label).width;
182
- let x0 = px + gap;
183
- if (x0 + w > right) x0 = px - gap - w;
184
- if (x0 < left) x0 = Math.max(left, Math.min(right - w, px - w / 2));
185
- let row = 0;
186
- while (placed.some((p) => p.row === row && x0 < p.x1 + gap && x0 + w + gap > p.x0)) row += 1;
187
- placed.push({ x0, x1: x0 + w, row });
188
- 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 });
189
433
  ctx.textAlign = "left";
190
434
  ctx.lineWidth = 3 * pr;
191
435
  ctx.strokeStyle = style.halo;
192
- ctx.strokeText(m.label, x0, y);
436
+ ctx.strokeText(at.text, at.x0, at.y);
193
437
  ctx.fillStyle = style.color;
194
- ctx.fillText(m.label, x0, y);
438
+ ctx.fillText(at.text, at.x0, at.y);
195
439
  }
196
440
  ctx.restore();
441
+ return cut;
197
442
  }
198
- function drawYMarks(u, marks, style) {
443
+ function drawYMarks(u, marks, style, boxes) {
199
444
  const ctx = u.ctx;
200
445
  const pr = uPlot.pxRatio || 1;
201
446
  const left = u.bbox.left;
@@ -203,8 +448,10 @@ function drawYMarks(u, marks, style) {
203
448
  const top = u.bbox.top;
204
449
  const bottom = u.bbox.top + u.bbox.height;
205
450
  const gap = 4 * pr;
451
+ const rowH = 14 * pr;
206
452
  const textH = 11 * pr;
207
453
  const placed = [];
454
+ const cut = [];
208
455
  ctx.save();
209
456
  ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
210
457
  ctx.textBaseline = "alphabetic";
@@ -221,22 +468,24 @@ function drawYMarks(u, marks, style) {
221
468
  ctx.lineTo(right, py);
222
469
  ctx.stroke();
223
470
  ctx.setLineDash([]);
224
- const w = ctx.measureText(m.label).width;
225
- const base = py - gap - textH < top ? py + gap + textH : py - gap;
226
- let x1 = right - gap;
227
- while (placed.some((p) => x1 - w < p.x1 + gap && x1 > p.x0 - gap && base - textH < p.y1 && base > p.y0)) {
228
- x1 = Math.min(...placed.filter((p) => base - textH < p.y1 && base > p.y0).map((p) => p.x0)) - 2 * gap;
471
+ if (!m.label) continue;
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;
229
476
  }
230
- const x0 = Math.max(left + gap, x1 - w);
231
- 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 });
232
480
  ctx.textAlign = "left";
233
481
  ctx.lineWidth = 3 * pr;
234
482
  ctx.strokeStyle = style.halo;
235
- ctx.strokeText(m.label, x0, base);
483
+ ctx.strokeText(at.text, at.x0, at.y);
236
484
  ctx.fillStyle = color;
237
- ctx.fillText(m.label, x0, base);
485
+ ctx.fillText(at.text, at.x0, at.y);
238
486
  }
239
487
  ctx.restore();
488
+ return cut;
240
489
  }
241
490
  function nearestPoint(xs, ys, left, top) {
242
491
  let best = null;
@@ -255,7 +504,7 @@ function nearestPoint(xs, ys, left, top) {
255
504
  }
256
505
  return best;
257
506
  }
258
- function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint }) {
507
+ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo, title, zoomY, zoomOff }) {
259
508
  const lang = useShellLang();
260
509
  const theme = useThemeStore((s) => s.theme);
261
510
  const fill = height === "fill";
@@ -271,8 +520,29 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
271
520
  const width = parity ? parityBox(box.width, box.height, typeof height === "number" ? height : 0, fill) : box.width;
272
521
  const h = parity ? width : fill ? box.height : height;
273
522
  const uRef = useRef(null);
523
+ const ctlRef = useRef(null);
524
+ const explicitRef = useRef(false);
274
525
  const hostRef = useRef(null);
275
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("");
538
+ const [soloPick, setSoloPick] = useState(null);
539
+ const soloFound = solo && soloPick !== null ? series.findIndex((s) => soloKey(s) === soloPick) : -1;
540
+ const soloIdx = soloFound >= 0 ? soloFound : null;
541
+ const soloRef = useRef(soloIdx);
542
+ soloRef.current = soloIdx;
543
+ useEffect(() => {
544
+ if (soloPick !== null && soloIdx === null) setSoloPick(null);
545
+ }, [soloPick, soloIdx]);
276
546
  const onCursorRef = useRef(onCursor);
277
547
  onCursorRef.current = onCursor;
278
548
  const onPickRef = useRef(onPick);
@@ -290,19 +560,15 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
290
560
  const levels = useMemo(() => (yMarks ?? []).flatMap((m) => m.drag ? [m.drag.min, m.drag.max] : [m.y]), [yMarks]);
291
561
  const range = useMemo(() => parity ? parityRange(xs, ...ys, levels, ext ? [...ext.x, ...ext.y] : []) : null, [parity, xs, ys, levels, ext]);
292
562
  const yLevels = !parity && !y.range && (levels.length > 0 || ext) ? [...levels, ...ext ? ext.y : []] : null;
293
- const xExt = useMemo(() => {
294
- if (!ext || range || xs.length === 0) return null;
295
- const lo = Math.min(xs[0], ext.x[0]);
296
- const hi = Math.max(xs[xs.length - 1], ext.x[1]);
297
- if (x.log && !(lo > 0)) return null;
298
- return hi > lo ? [lo, hi] : x.log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
299
- }, [ext, range, xs, x.log]);
563
+ const xExt = useMemo(() => range ? null : xScaleRange(xs, ext, x.range, Boolean(x.log)), [ext, range, xs, x.log, x.range]);
564
+ const xSpan = xs.length > 0 ? Math.max(Math.abs(xs[xs.length - 1] - xs[0]), x.range ? Math.abs(x.range[1] - x.range[0]) : 0) : 0;
565
+ const ticks = !x.time && x.ticks && x.ticks.length > 0 ? x.ticks : void 0;
300
566
  const tiny = useMemo(
301
567
  () => ({
302
- x: Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
568
+ x: !ticks && Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
303
569
  y: Boolean(y.log) && smallestPositive(yLevels ? [...ys, yLevels] : ys, range ?? y.range) < LOG_TABLE_FLOOR
304
570
  }),
305
- [x.log, y.log, xs, ys, range, y.range, yLevels, ext]
571
+ [x.log, y.log, xs, ys, range, y.range, yLevels, ext, ticks]
306
572
  );
307
573
  const xLabel = pick(x.label, lang);
308
574
  const yLabel = pick(y.label, lang);
@@ -317,21 +583,45 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
317
583
  const dragCount = (yMarks ?? []).filter((m) => m.drag).length;
318
584
  const xUnit = x.unit ? pick(x.unit, lang) : "";
319
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);
320
600
  const structure = JSON.stringify({
321
- s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s)]),
322
- x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt],
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)]),
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],
323
603
  oe: ext,
324
604
  p: range,
325
605
  pick: Boolean(onPick),
326
606
  y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
327
607
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
328
- ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag] : [m.y, pick(m.label, lang), m.tone ?? "warn"]),
608
+ ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag, Boolean(m.label)] : [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]),
329
609
  theme,
330
610
  lang,
331
611
  h,
332
- w: width
612
+ w: width,
613
+ ta: [yAbove, xBelow, zoomable, both]
333
614
  });
334
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
+ };
335
625
  const scatter = baseEntries.length > 0 && baseEntries.every(({ s }) => s.mode === "points");
336
626
  useEffect(() => {
337
627
  const el = plotRef.current;
@@ -342,22 +632,34 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
342
632
  const font = `11px ${family}`;
343
633
  const surface = resolveToken("--color-surface", "#fff");
344
634
  const repeats = { x: 0, y: 0 };
635
+ const declareTicks = (axis, labels, repeated = repeatedLabels(labels)) => {
636
+ repeats[axis] = repeated;
637
+ hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
638
+ hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
639
+ };
345
640
  const tick = (fmt, axis, pre) => (_u, vals) => {
346
641
  const labels = formatTicks(pre ? pre(vals) : vals, lang, fmt ?? { digits: 4 });
347
642
  if (axis === "x" && pre) hostRef.current?.setAttribute("data-log-labels", labels.filter((l) => l).join("|"));
348
- repeats[axis] = repeatedLabels(labels);
349
- hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
350
- hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
643
+ declareTicks(axis, labels);
644
+ return labels;
645
+ };
646
+ const namedTicks = (_u, vals) => {
647
+ const labels = vals.map((v) => v === null || v === void 0 ? "" : tickLabelAt(ticks, v, lang) ?? "");
648
+ hostRef.current?.setAttribute("data-x-tick-labels", labels.filter((l) => l).join("|"));
649
+ const dup = namedRepeats(labels);
650
+ if (dup.n > 0) hostRef.current?.setAttribute("data-ticks-repeated", dup.names.join("|"));
651
+ else hostRef.current?.removeAttribute("data-ticks-repeated");
652
+ declareTicks("x", labels, dup.n);
351
653
  return labels;
352
654
  };
353
655
  const near = { left: NaN, top: NaN, idx: null };
354
- const nearest = (u) => {
355
- const { left, top } = u.cursor;
656
+ const nearest = (u2) => {
657
+ const { left, top } = u2.cursor;
356
658
  if (left === void 0 || top === void 0 || left < 0 || top < 0) return null;
357
659
  if (left !== near.left || top !== near.top) {
358
- const d = u.data;
359
- const px = d[0].map((v) => v === null ? NaN : u.valToPos(v, "x"));
360
- 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")));
361
663
  near.left = left;
362
664
  near.top = top;
363
665
  near.idx = nearestPoint(px, py, left, top);
@@ -366,11 +668,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
366
668
  };
367
669
  const xSteps = x.time || x.log ? void 0 : tickSteps(x.format);
368
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]];
369
672
  const opts = {
370
673
  width,
371
674
  height: h,
372
675
  legend: { show: false },
373
- ...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
+ },
374
683
  // a log axis labels the ticks uPlot keeps and passes null for the rest, which formatTicks leaves blank
375
684
  scales: {
376
685
  // a log x axis spans the data, not the decades around it (uPlot rounds out to the next decade, which left an
@@ -378,16 +687,18 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
378
687
  x: {
379
688
  time: Boolean(x.time),
380
689
  distr: x.log ? 3 : 1,
381
- ...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] } : {}
382
693
  },
383
694
  y: {
384
695
  distr: y.log ? 3 : 1,
385
- ...range ? { range } : y.range ? { range: y.range } : yLevels ? {
696
+ ...range ? { range: fixedRange(range) } : y.range ? { range: fixedRange(y.range) } : yLevels ? {
386
697
  // uPlot's own range of the data and the levels: its padding, its decade snap on a log axis
387
- range: (u, dmin, dmax) => {
698
+ range: (u2, dmin, dmax) => {
388
699
  const w = rangeWithLevels(dmin, dmax, yLevels, Boolean(y.log), 0.05);
389
700
  if (!w) return [dmin, dmax];
390
- 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);
391
702
  }
392
703
  } : {}
393
704
  }
@@ -398,20 +709,25 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
398
709
  grid: { stroke: grid, width: 1 },
399
710
  ticks: { stroke: grid },
400
711
  font,
401
- label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
712
+ label: xBelow ? void 0 : xTitle,
402
713
  labelFont: font,
403
714
  // a log x axis labels the decades only; the ticks between stay drawn, unlabelled
404
- values: x.time ? void 0 : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
405
- ...xSteps ? { incrs: xSteps } : {},
406
- ...range ? { space: PARITY_TICK_SPACE } : {},
407
- ...tiny.x ? DECADE_AXIS : {}
715
+ values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
716
+ ...ticks ? (
717
+ // exactly the named values, no other tick
718
+ { splits: (_u, _i, min, max) => tickSplits(ticks, min, max, Boolean(x.log)), filter: (_u, splits) => splits }
719
+ ) : {
720
+ ...xSteps ? { incrs: xSteps } : {},
721
+ ...range ? { space: PARITY_TICK_SPACE } : {},
722
+ ...tiny.x ? DECADE_AXIS : {}
723
+ }
408
724
  },
409
725
  {
410
726
  stroke: fg,
411
727
  grid: { stroke: grid, width: 1 },
412
728
  ticks: { stroke: grid },
413
729
  font,
414
- label: yUnit ? `${yLabel} (${yUnit})` : yLabel,
730
+ label: yAbove ? void 0 : yTitle,
415
731
  labelFont: font,
416
732
  values: tick(y.format, "y"),
417
733
  ...ySteps ? { incrs: ySteps } : {},
@@ -420,12 +736,12 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
420
736
  // Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
421
737
  // label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
422
738
  // made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
423
- size: (u, values) => {
739
+ size: (u2, values) => {
424
740
  if (!values || values.length === 0) return 48;
425
741
  const pr = uPlot.pxRatio || 1;
426
- u.ctx.font = `${Math.round(11 * pr)}px ${family}`;
427
- const widest = Math.max(...values.map((v) => u.ctx.measureText(v ?? "").width));
428
- 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);
429
745
  }
430
746
  }
431
747
  ],
@@ -434,21 +750,22 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
434
750
  ...baseEntries.map(({ s, i }) => {
435
751
  const style = seriesStyle(s, i);
436
752
  const stroke = resolveToken(style.color, "#4a8");
437
- return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: s.size ?? 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: style.dash, spanGaps: false, points: { show: false } };
753
+ const dim = soloStyle(soloRef.current, i, s.mode === "points" ? 1.5 : s.width ?? 2);
754
+ return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: dim.width, alpha: dim.alpha, paths: () => null, points: { show: true, space: 0, size: s.size ?? 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: dim.width, alpha: dim.alpha, dash: style.dash, spanGaps: false, points: s.dots ? { show: true, size: 5, fill: stroke } : { show: false } };
438
755
  })
439
756
  ],
440
757
  hooks: {
441
758
  setCursor: [
442
- (u) => {
443
- const idx = scatter ? nearest(u) : u.cursor.idx ?? null;
759
+ (u2) => {
760
+ const idx = scatter ? nearest(u2) : u2.cursor.idx ?? null;
444
761
  pickedRef.current = idx;
445
762
  setCursor(idx);
446
763
  onCursorRef.current?.(idx === null ? null : orderRef.current ? orderRef.current[idx] : idx);
447
764
  }
448
765
  ],
449
766
  ready: [
450
- (u) => {
451
- u.over.addEventListener("click", () => {
767
+ (u2) => {
768
+ u2.over.addEventListener("click", () => {
452
769
  const idx = pickedRef.current;
453
770
  if (idx === null || idx === void 0 || !onPickRef.current) return;
454
771
  onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
@@ -473,7 +790,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
473
790
  if (v === liveRef.current[i]) return;
474
791
  liveRef.current[i] = v;
475
792
  onYMarkChangeRef.current?.(i, v);
476
- u.redraw(false);
793
+ u2.redraw(false);
477
794
  };
478
795
  let dragging = false;
479
796
  el2.addEventListener("pointerdown", (e) => {
@@ -485,7 +802,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
485
802
  });
486
803
  el2.addEventListener("pointermove", (e) => {
487
804
  if (!dragging) return;
488
- set(u.posToVal(e.clientY - u.over.getBoundingClientRect().top, "y"));
805
+ set(u2.posToVal(e.clientY - u2.over.getBoundingClientRect().top, "y"));
489
806
  });
490
807
  const end = (e) => {
491
808
  dragging = false;
@@ -499,97 +816,337 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
499
816
  e.preventDefault();
500
817
  set(v);
501
818
  });
502
- u.over.appendChild(el2);
819
+ u2.over.appendChild(el2);
503
820
  });
504
- 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" : "";
505
853
  }
506
854
  ],
507
855
  draw: [
508
- (u) => {
856
+ (u2) => {
509
857
  if (range) {
510
- const ctx = u.ctx;
511
- const pr = uPlot.pxRatio || 1;
858
+ const ctx = u2.ctx;
859
+ const pr2 = uPlot.pxRatio || 1;
512
860
  ctx.save();
513
861
  ctx.strokeStyle = resolveToken("--color-fg-faint", "#888");
514
- ctx.lineWidth = Math.max(1, pr);
515
- ctx.setLineDash([5 * pr, 4 * pr]);
862
+ ctx.lineWidth = Math.max(1, pr2);
863
+ ctx.setLineDash([5 * pr2, 4 * pr2]);
516
864
  ctx.beginPath();
517
- ctx.moveTo(u.valToPos(range[0], "x", true), u.valToPos(range[0], "y", true));
518
- 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));
519
867
  ctx.stroke();
520
868
  ctx.restore();
521
869
  }
522
870
  const dots = overlaysRef.current;
523
871
  if (dots.length) {
524
- const ctx = u.ctx;
525
- const pr = uPlot.pxRatio || 1;
526
- let drawn = 0;
872
+ const ctx = u2.ctx;
873
+ const pr2 = uPlot.pxRatio || 1;
874
+ let drawn2 = 0;
527
875
  ctx.save();
528
876
  ctx.beginPath();
529
- 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);
530
878
  ctx.clip();
531
879
  for (const { s, i } of dots) {
880
+ ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
532
881
  ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
533
882
  ctx.strokeStyle = surface;
534
- ctx.lineWidth = Math.max(1, pr);
535
- const r = (s.size ?? 9) / 2 * pr;
883
+ ctx.lineWidth = Math.max(1, pr2);
884
+ const r = (s.size ?? 9) / 2 * pr2;
536
885
  const ox = s.x ?? [];
537
886
  for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
538
887
  const v = s.values[k];
539
888
  if (v === null || v === void 0 || !Number.isFinite(v) || !Number.isFinite(ox[k])) continue;
540
- const px = u.valToPos(ox[k], "x", true);
541
- const py = u.valToPos(v, "y", true);
542
- if (!Number.isFinite(px) || !Number.isFinite(py) || px < u.bbox.left - 0.5 || px > u.bbox.left + u.bbox.width + 0.5 || py < u.bbox.top - 0.5 || py > u.bbox.top + u.bbox.height + 0.5) continue;
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;
543
892
  ctx.beginPath();
544
893
  ctx.arc(px, py, r, 0, Math.PI * 2);
545
894
  ctx.fill();
546
895
  ctx.stroke();
547
- drawn++;
896
+ drawn2++;
548
897
  }
549
898
  }
550
899
  ctx.restore();
551
- hostRef.current?.setAttribute("data-overlay-drawn", String(drawn));
900
+ hostRef.current?.setAttribute("data-overlay-drawn", String(drawn2));
552
901
  }
553
902
  const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
554
903
  const lines = yMarksRef.current ?? [];
555
- if (lines.length) drawYMarks(u, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: pick(m.label, lang), color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
556
- u.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
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) => {
557
907
  const i = Number(el2.getAttribute("data-y-handle"));
558
908
  const m = lines[i];
559
909
  const v = liveRef.current[i];
560
910
  if (!m || v === void 0) return;
561
- const top = u.valToPos(v, "y");
562
- el2.style.display = Number.isFinite(top) && top >= 0 && top <= u.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
911
+ const top = u2.valToPos(v, "y");
912
+ el2.style.display = Number.isFinite(top) && top >= 0 && top <= u2.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
563
913
  el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
564
- const label = pick(m.label, lang);
565
- el2.setAttribute("aria-label", label);
914
+ const label = m.label ? pick(m.label, lang) : "";
915
+ const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
916
+ const unnamed = lines.flatMap((one, k) => one.drag && !one.label ? [k] : []);
917
+ el2.setAttribute("aria-label", label || referenceLineName(lang, unnamed.indexOf(i) + 1, unnamed.length));
566
918
  el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
567
- el2.setAttribute("aria-valuetext", `${label}: ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`);
919
+ el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
568
920
  });
569
- 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);
570
931
  }
571
932
  ]
572
933
  }
573
934
  };
935
+ if (!ticks) {
936
+ hostRef.current?.removeAttribute("data-x-tick-labels");
937
+ hostRef.current?.removeAttribute("data-ticks-repeated");
938
+ }
574
939
  uRef.current?.destroy();
575
- 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
+ }
576
974
  return () => {
975
+ ctl.detach();
976
+ ctlRef.current = null;
577
977
  uRef.current?.destroy();
578
978
  uRef.current = null;
579
979
  };
580
980
  }, [structure]);
581
981
  useEffect(() => {
582
- 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
+ }
583
994
  }, [data]);
584
995
  const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
585
- const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]));
996
+ const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
586
997
  useEffect(() => {
587
998
  uRef.current?.redraw(false);
588
999
  }, [linesKey, overlaysKey]);
1000
+ useEffect(() => {
1001
+ const u = uRef.current;
1002
+ if (!u) return;
1003
+ baseEntries.forEach(({ s, i }, k) => {
1004
+ const dim = soloStyle(soloIdx, i, s.mode === "points" ? 1.5 : s.width ?? 2);
1005
+ const one = u.series[k + 1];
1006
+ if (!one) return;
1007
+ one.alpha = dim.alpha;
1008
+ one.width = dim.width;
1009
+ });
1010
+ u.redraw(false);
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 });
589
1014
  const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
590
- `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
1015
+ // an x that sits on a named tick is named by it, not by its number
1016
+ tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${xValue(xs[cursor])}${xUnit && !x.time ? `${NBSP}${xUnit}` : ""}`,
591
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}` : ""}`)
592
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 });
593
1150
  return /* @__PURE__ */ jsxs(
594
1151
  "div",
595
1152
  {
@@ -606,16 +1163,63 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
606
1163
  "data-y-marks": yMarks?.length ? String(yMarks.length) : void 0,
607
1164
  "data-y-drag": dragCount ? String(dragCount) : void 0,
608
1165
  "data-overlay": overlays.length ? String(overlays.length) : void 0,
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,
609
1173
  children: [
610
- /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
611
- series.length > 1 && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsxs("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": seriesStyle(s, i).dash ? "1" : void 0, children: [
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 }),
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: [
1210
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
1211
+ pick(s.label, lang)
1212
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
612
1213
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
613
1214
  pick(s.label, lang)
614
- ] }, i)) }),
615
- /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
1215
+ ] }) }, i)) }),
1216
+ drawn ? /* @__PURE__ */ jsxs("div", { className: "caos-chart-foot", children: [
1217
+ readoutEl,
1218
+ zoomed && zoomable && /* @__PURE__ */ jsx(ResetZoomButton, { lang, onReset: resetZoom })
1219
+ ] }) : readoutEl
616
1220
  ]
617
1221
  }
618
1222
  );
619
1223
  }
620
1224
 
621
- export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
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 };