@fasl-work/caos-app-shell 0.16.0 → 0.18.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,8 +1,9 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-YECZXGVG.js';
1
+ import { pick, useShellLang, useThemeStore, useStageSize, resolveToken, NBSP, formatNumber, formatTicks, referenceLineName } from './chunk-DBCMAL2G.js';
2
+ export { referenceLineName } from './chunk-DBCMAL2G.js';
2
3
  import './chunk-33WTUQ7Q.js';
3
- import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
+ import { useRef, useCallback, useState, 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
 
7
8
  var YMARK_TONE_TOKENS = {
8
9
  warn: "--color-warn",
@@ -11,6 +12,52 @@ var YMARK_TONE_TOKENS = {
11
12
  neutral: "--color-fg-subtle"
12
13
  };
13
14
  var ROTATION = ["--color-accent", "--color-magenta", "--color-accent-2", "--color-good", "--color-warn", "--color-bad"];
15
+ var SOLO_DIM_ALPHA = 0.12;
16
+ function soloStyle(solo, i, width) {
17
+ return solo !== null && solo !== i ? { alpha: SOLO_DIM_ALPHA, width: 1 } : { alpha: 1, width };
18
+ }
19
+ function tickSplits(ticks, min, max, log = false) {
20
+ const eps = Math.abs(max - min) * 1e-9;
21
+ const values = ticks.map((t) => t.value).filter((v) => Number.isFinite(v) && (!log || v > 0) && v >= min - eps && v <= max + eps);
22
+ return [...new Set(values)].sort((a, b) => a - b);
23
+ }
24
+ function tickLabelAt(ticks, x, lang, span = 0) {
25
+ const eps = Math.abs(span) * 1e-9;
26
+ const t = ticks?.find((one) => Math.abs(one.value - x) <= eps);
27
+ return t ? pick(t.label, lang) : null;
28
+ }
29
+ function namedRepeats(labels) {
30
+ const seen = /* @__PURE__ */ new Set();
31
+ const names = [];
32
+ let n = 0;
33
+ for (const l of labels) {
34
+ if (!l) continue;
35
+ if (seen.has(l)) {
36
+ n++;
37
+ if (!names.includes(l)) names.push(l);
38
+ } else seen.add(l);
39
+ }
40
+ return { n, names };
41
+ }
42
+ function soloKey(s) {
43
+ return s.id ?? pick(s.label, "en");
44
+ }
45
+ function xScaleRange(xs, ext, range, log) {
46
+ const given = range && !(log && Math.min(range[0], range[1]) <= 0) ? range : void 0;
47
+ if (!ext && !given || xs.length === 0) return null;
48
+ let lo = xs[0];
49
+ let hi = xs[xs.length - 1];
50
+ if (ext) {
51
+ lo = Math.min(lo, ext.x[0]);
52
+ hi = Math.max(hi, ext.x[1]);
53
+ }
54
+ if (given) {
55
+ lo = Math.min(lo, given[0], given[1]);
56
+ hi = Math.max(hi, given[0], given[1]);
57
+ }
58
+ if (log && !(lo > 0)) return null;
59
+ return hi > lo ? [lo, hi] : log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
60
+ }
14
61
  function seriesStyle(s, i) {
15
62
  const color = s.color ?? ROTATION[i % ROTATION.length];
16
63
  const dash = s.dash ?? (!s.color && i >= ROTATION.length ? [6, 4] : void 0);
@@ -83,11 +130,73 @@ function smallestPositive(columns, range) {
83
130
  for (const c of columns) for (const v of c) if (v !== null && Number.isFinite(v) && v > 0 && v < lo) lo = v;
84
131
  return lo;
85
132
  }
133
+ function isDecade(t) {
134
+ if (t === null || t === void 0 || !Number.isFinite(t) || t <= 0) return false;
135
+ const d = Math.log10(t);
136
+ return Math.abs(d - Math.round(d)) < 1e-6;
137
+ }
138
+ function decadeOnly(ticks) {
139
+ if (!ticks.some(isDecade)) return [...ticks];
140
+ return ticks.map((t) => isDecade(t) ? t : null);
141
+ }
142
+ function parityBox(boxWidth, boxHeight, height, fill) {
143
+ return Math.min(boxWidth, fill ? boxHeight : height);
144
+ }
145
+ function clampMark(y, drag) {
146
+ if (!Number.isFinite(y)) return drag.min;
147
+ const v = drag.step && drag.step > 0 ? drag.min + Math.round((y - drag.min) / drag.step) * drag.step : y;
148
+ const clamped = Math.min(drag.max, Math.max(drag.min, v));
149
+ return Number(clamped.toPrecision(12));
150
+ }
151
+ function nudgeMark(y, key, drag, log = false) {
152
+ const dir = key === "ArrowUp" || key === "ArrowRight" || key === "PageUp" ? 1 : key === "ArrowDown" || key === "ArrowLeft" || key === "PageDown" ? -1 : 0;
153
+ if (key === "Home") return drag.min;
154
+ if (key === "End") return drag.max;
155
+ if (dir === 0) return null;
156
+ const n = key === "PageUp" || key === "PageDown" ? 10 : 1;
157
+ if (log && drag.min > 0 && drag.max > drag.min) {
158
+ const decades = (Math.log10(drag.max) - Math.log10(drag.min)) / 100;
159
+ return Math.min(drag.max, Math.max(drag.min, y * 10 ** (dir * n * decades)));
160
+ }
161
+ const step = drag.step && drag.step > 0 ? drag.step : (drag.max - drag.min) / 100;
162
+ return clampMark(y + dir * n * step, { ...drag, step: void 0 });
163
+ }
164
+ function isOverlay(s) {
165
+ return Boolean(s.x) && s.mode === "points";
166
+ }
167
+ function dragWithin(drag, range) {
168
+ if (!range) return drag;
169
+ const lo = Math.min(range[0], range[1]);
170
+ const hi = Math.max(range[0], range[1]);
171
+ const min = Math.min(hi, Math.max(lo, drag.min));
172
+ const max = Math.max(lo, Math.min(hi, drag.max));
173
+ return { ...drag, min, max: Math.max(min, max) };
174
+ }
175
+ function overlayExtent(series, positiveX = false) {
176
+ let x0 = Infinity;
177
+ let x1 = -Infinity;
178
+ let y0 = Infinity;
179
+ let y1 = -Infinity;
180
+ for (const s of series) {
181
+ if (!isOverlay(s) || !s.x) continue;
182
+ for (let i = 0; i < Math.min(s.x.length, s.values.length); i++) {
183
+ const px = s.x[i];
184
+ const py = s.values[i];
185
+ if (py === null || py === void 0 || !Number.isFinite(px) || !Number.isFinite(py) || positiveX && !(px > 0)) continue;
186
+ x0 = Math.min(x0, px);
187
+ x1 = Math.max(x1, px);
188
+ y0 = Math.min(y0, py);
189
+ y1 = Math.max(y1, py);
190
+ }
191
+ }
192
+ return Number.isFinite(x0) ? { x: [x0, x1], y: [y0, y1] } : null;
193
+ }
86
194
  var DECADE_AXIS = {
87
195
  splits: (_u, _i, min, max) => logDecades(min, max),
88
196
  filter: (_u, splits) => splits
89
197
  };
90
198
  var PARITY_TICK_SPACE = 40;
199
+ var HANDLE_HEIGHT = 14;
91
200
  function repeatedLabels(labels) {
92
201
  let n = 0;
93
202
  for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
@@ -159,6 +268,7 @@ function drawYMarks(u, marks, style) {
159
268
  ctx.lineTo(right, py);
160
269
  ctx.stroke();
161
270
  ctx.setLineDash([]);
271
+ if (!m.label) continue;
162
272
  const w = ctx.measureText(m.label).width;
163
273
  const base = py - gap - textH < top ? py + gap + textH : py - gap;
164
274
  let x1 = right - gap;
@@ -193,7 +303,7 @@ function nearestPoint(xs, ys, left, top) {
193
303
  }
194
304
  return best;
195
305
  }
196
- function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPick, parity, readout, hint }) {
306
+ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo }) {
197
307
  const lang = useShellLang();
198
308
  const theme = useThemeStore((s) => s.theme);
199
309
  const fill = height === "fill";
@@ -206,11 +316,19 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
206
316
  },
207
317
  [measureRef]
208
318
  );
209
- const width = parity ? Math.min(box.width, fill ? box.height : height) : box.width;
319
+ const width = parity ? parityBox(box.width, box.height, typeof height === "number" ? height : 0, fill) : box.width;
210
320
  const h = parity ? width : fill ? box.height : height;
211
321
  const uRef = useRef(null);
212
322
  const hostRef = useRef(null);
213
323
  const [cursor, setCursor] = useState(null);
324
+ const [soloPick, setSoloPick] = useState(null);
325
+ const soloFound = solo && soloPick !== null ? series.findIndex((s) => soloKey(s) === soloPick) : -1;
326
+ const soloIdx = soloFound >= 0 ? soloFound : null;
327
+ const soloRef = useRef(soloIdx);
328
+ soloRef.current = soloIdx;
329
+ useEffect(() => {
330
+ if (soloPick !== null && soloIdx === null) setSoloPick(null);
331
+ }, [soloPick, soloIdx]);
214
332
  const onCursorRef = useRef(onCursor);
215
333
  onCursorRef.current = onCursor;
216
334
  const onPickRef = useRef(onPick);
@@ -220,36 +338,53 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
220
338
  const orderRef = useRef(order);
221
339
  orderRef.current = order;
222
340
  const xs = useMemo(() => order ? order.map((i) => x.values[i]) : x.values, [order, x.values]);
223
- const ys = useMemo(() => series.map((s) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, series]);
224
- const levels = useMemo(() => (yMarks ?? []).map((m) => m.y), [yMarks]);
225
- const range = useMemo(() => parity ? parityRange(xs, ...ys, levels) : null, [parity, xs, ys, levels]);
226
- const yLevels = !parity && !y.range && levels.length > 0 ? levels : null;
341
+ const entries = useMemo(() => series.map((s, i) => ({ s, i })), [series]);
342
+ const baseEntries = useMemo(() => entries.filter(({ s }) => !isOverlay(s)), [entries]);
343
+ const overlays = useMemo(() => entries.filter(({ s }) => isOverlay(s)), [entries]);
344
+ const ext = useMemo(() => overlayExtent(series, Boolean(x.log)), [series, x.log]);
345
+ const ys = useMemo(() => baseEntries.map(({ s }) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, baseEntries]);
346
+ const levels = useMemo(() => (yMarks ?? []).flatMap((m) => m.drag ? [m.drag.min, m.drag.max] : [m.y]), [yMarks]);
347
+ const range = useMemo(() => parity ? parityRange(xs, ...ys, levels, ext ? [...ext.x, ...ext.y] : []) : null, [parity, xs, ys, levels, ext]);
348
+ const yLevels = !parity && !y.range && (levels.length > 0 || ext) ? [...levels, ...ext ? ext.y : []] : null;
349
+ const xExt = useMemo(() => range ? null : xScaleRange(xs, ext, x.range, Boolean(x.log)), [ext, range, xs, x.log, x.range]);
350
+ 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;
351
+ const ticks = !x.time && x.ticks && x.ticks.length > 0 ? x.ticks : void 0;
227
352
  const tiny = useMemo(
228
353
  () => ({
229
- x: Boolean(x.log) && smallestPositive([xs], range) < LOG_TABLE_FLOOR,
354
+ x: !ticks && Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
230
355
  y: Boolean(y.log) && smallestPositive(yLevels ? [...ys, yLevels] : ys, range ?? y.range) < LOG_TABLE_FLOOR
231
356
  }),
232
- [x.log, y.log, xs, ys, range, y.range, yLevels]
357
+ [x.log, y.log, xs, ys, range, y.range, yLevels, ext, ticks]
233
358
  );
234
359
  const xLabel = pick(x.label, lang);
235
360
  const yLabel = pick(y.label, lang);
361
+ const yMarksRef = useRef(yMarks);
362
+ yMarksRef.current = yMarks;
363
+ const liveRef = useRef([]);
364
+ liveRef.current = (yMarks ?? []).map((m) => m.y);
365
+ const overlaysRef = useRef(overlays);
366
+ overlaysRef.current = overlays;
367
+ const onYMarkChangeRef = useRef(onYMarkChange);
368
+ onYMarkChangeRef.current = onYMarkChange;
369
+ const dragCount = (yMarks ?? []).filter((m) => m.drag).length;
236
370
  const xUnit = x.unit ? pick(x.unit, lang) : "";
237
371
  const yUnit = y.unit ? pick(y.unit, lang) : "";
238
372
  const structure = JSON.stringify({
239
- s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9]),
240
- x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x],
373
+ 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
+ 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],
375
+ oe: ext,
241
376
  p: range,
242
377
  pick: Boolean(onPick),
243
378
  y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
244
379
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
245
- ym: (yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]),
380
+ ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag, Boolean(m.label)] : [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]),
246
381
  theme,
247
382
  lang,
248
383
  h,
249
384
  w: width
250
385
  });
251
386
  const data = useMemo(() => [xs, ...ys], [xs, ys]);
252
- const scatter = series.length > 0 && series.every((s) => s.mode === "points");
387
+ const scatter = baseEntries.length > 0 && baseEntries.every(({ s }) => s.mode === "points");
253
388
  useEffect(() => {
254
389
  const el = plotRef.current;
255
390
  if (!el || width <= 0 || h <= 0) return;
@@ -259,11 +394,24 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
259
394
  const font = `11px ${family}`;
260
395
  const surface = resolveToken("--color-surface", "#fff");
261
396
  const repeats = { x: 0, y: 0 };
262
- const tick = (fmt, axis) => (_u, vals) => {
263
- const labels = formatTicks(vals, lang, fmt ?? { digits: 4 });
264
- repeats[axis] = repeatedLabels(labels);
397
+ const declareTicks = (axis, labels, repeated = repeatedLabels(labels)) => {
398
+ repeats[axis] = repeated;
265
399
  hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
266
400
  hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
401
+ };
402
+ const tick = (fmt, axis, pre) => (_u, vals) => {
403
+ const labels = formatTicks(pre ? pre(vals) : vals, lang, fmt ?? { digits: 4 });
404
+ if (axis === "x" && pre) hostRef.current?.setAttribute("data-log-labels", labels.filter((l) => l).join("|"));
405
+ declareTicks(axis, labels);
406
+ return labels;
407
+ };
408
+ const namedTicks = (_u, vals) => {
409
+ const labels = vals.map((v) => v === null || v === void 0 ? "" : tickLabelAt(ticks, v, lang) ?? "");
410
+ hostRef.current?.setAttribute("data-x-tick-labels", labels.filter((l) => l).join("|"));
411
+ const dup = namedRepeats(labels);
412
+ if (dup.n > 0) hostRef.current?.setAttribute("data-ticks-repeated", dup.names.join("|"));
413
+ else hostRef.current?.removeAttribute("data-ticks-repeated");
414
+ declareTicks("x", labels, dup.n);
267
415
  return labels;
268
416
  };
269
417
  const near = { left: NaN, top: NaN, idx: null };
@@ -294,7 +442,7 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
294
442
  x: {
295
443
  time: Boolean(x.time),
296
444
  distr: x.log ? 3 : 1,
297
- ...range ? { range } : x.log ? { range: (_u, min, max) => [min, max] } : {}
445
+ ...range ? { range } : xExt ? { range: xExt } : x.log ? { range: (_u, min, max) => [min, max] } : {}
298
446
  },
299
447
  y: {
300
448
  distr: y.log ? 3 : 1,
@@ -316,10 +464,16 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
316
464
  font,
317
465
  label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
318
466
  labelFont: font,
319
- values: x.time ? void 0 : tick(x.format, "x"),
320
- ...xSteps ? { incrs: xSteps } : {},
321
- ...range ? { space: PARITY_TICK_SPACE } : {},
322
- ...tiny.x ? DECADE_AXIS : {}
467
+ // a log x axis labels the decades only; the ticks between stay drawn, unlabelled
468
+ values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
469
+ ...ticks ? (
470
+ // exactly the named values, no other tick
471
+ { splits: (_u, _i, min, max) => tickSplits(ticks, min, max, Boolean(x.log)), filter: (_u, splits) => splits }
472
+ ) : {
473
+ ...xSteps ? { incrs: xSteps } : {},
474
+ ...range ? { space: PARITY_TICK_SPACE } : {},
475
+ ...tiny.x ? DECADE_AXIS : {}
476
+ }
323
477
  },
324
478
  {
325
479
  stroke: fg,
@@ -346,10 +500,11 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
346
500
  ],
347
501
  series: [
348
502
  {},
349
- ...series.map((s, i) => {
503
+ ...baseEntries.map(({ s, i }) => {
350
504
  const style = seriesStyle(s, i);
351
505
  const stroke = resolveToken(style.color, "#4a8");
352
- 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 } };
506
+ const dim = soloStyle(soloRef.current, i, s.mode === "points" ? 1.5 : s.width ?? 2);
507
+ 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 } };
353
508
  })
354
509
  ],
355
510
  hooks: {
@@ -368,6 +523,55 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
368
523
  if (idx === null || idx === void 0 || !onPickRef.current) return;
369
524
  onPickRef.current(orderRef.current ? orderRef.current[idx] : idx);
370
525
  });
526
+ const accent = resolveToken("--color-accent", "#06c");
527
+ (yMarks ?? []).forEach((m, i) => {
528
+ if (!m.drag) return;
529
+ const drag = dragWithin(m.drag, range ? void 0 : y.range);
530
+ const el2 = document.createElement("div");
531
+ el2.setAttribute("role", "slider");
532
+ el2.setAttribute("tabindex", "0");
533
+ el2.setAttribute("aria-orientation", "vertical");
534
+ el2.setAttribute("aria-valuemin", String(drag.min));
535
+ el2.setAttribute("aria-valuemax", String(drag.max));
536
+ el2.setAttribute("data-y-handle", String(i));
537
+ Object.assign(el2.style, { position: "absolute", left: "0", width: "100%", height: `${HANDLE_HEIGHT}px`, cursor: "ns-resize", touchAction: "none", outlineOffset: "-2px", zIndex: "3" });
538
+ el2.addEventListener("focus", () => el2.style.outline = `2px solid ${accent}`);
539
+ el2.addEventListener("blur", () => el2.style.outline = "none");
540
+ el2.addEventListener("click", (e) => e.stopPropagation());
541
+ const set = (raw) => {
542
+ const v = clampMark(raw, y.log ? { ...drag, step: void 0 } : drag);
543
+ if (v === liveRef.current[i]) return;
544
+ liveRef.current[i] = v;
545
+ onYMarkChangeRef.current?.(i, v);
546
+ u.redraw(false);
547
+ };
548
+ let dragging = false;
549
+ el2.addEventListener("pointerdown", (e) => {
550
+ if (e.button !== 0) return;
551
+ dragging = true;
552
+ el2.setPointerCapture(e.pointerId);
553
+ el2.focus();
554
+ e.preventDefault();
555
+ });
556
+ el2.addEventListener("pointermove", (e) => {
557
+ if (!dragging) return;
558
+ set(u.posToVal(e.clientY - u.over.getBoundingClientRect().top, "y"));
559
+ });
560
+ const end = (e) => {
561
+ dragging = false;
562
+ if (el2.hasPointerCapture(e.pointerId)) el2.releasePointerCapture(e.pointerId);
563
+ };
564
+ el2.addEventListener("pointerup", end);
565
+ el2.addEventListener("pointercancel", end);
566
+ el2.addEventListener("keydown", (e) => {
567
+ const v = nudgeMark(liveRef.current[i], e.key, drag, Boolean(y.log));
568
+ if (v === null) return;
569
+ e.preventDefault();
570
+ set(v);
571
+ });
572
+ u.over.appendChild(el2);
573
+ });
574
+ if (dragCount) queueMicrotask(() => uRef.current === u && u.redraw(false));
371
575
  }
372
576
  ],
373
577
  draw: [
@@ -385,13 +589,65 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
385
589
  ctx.stroke();
386
590
  ctx.restore();
387
591
  }
592
+ const dots = overlaysRef.current;
593
+ if (dots.length) {
594
+ const ctx = u.ctx;
595
+ const pr = uPlot.pxRatio || 1;
596
+ let drawn = 0;
597
+ ctx.save();
598
+ ctx.beginPath();
599
+ ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
600
+ ctx.clip();
601
+ for (const { s, i } of dots) {
602
+ ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
603
+ ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
604
+ ctx.strokeStyle = surface;
605
+ ctx.lineWidth = Math.max(1, pr);
606
+ const r = (s.size ?? 9) / 2 * pr;
607
+ const ox = s.x ?? [];
608
+ for (let k = 0; k < Math.min(ox.length, s.values.length); k++) {
609
+ const v = s.values[k];
610
+ 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;
614
+ ctx.beginPath();
615
+ ctx.arc(px, py, r, 0, Math.PI * 2);
616
+ ctx.fill();
617
+ ctx.stroke();
618
+ drawn++;
619
+ }
620
+ }
621
+ ctx.restore();
622
+ hostRef.current?.setAttribute("data-overlay-drawn", String(drawn));
623
+ }
388
624
  const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
389
- if (yMarks?.length) drawYMarks(u, yMarks.map((m) => ({ y: m.y, label: pick(m.label, lang), color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
625
+ 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) => {
628
+ const i = Number(el2.getAttribute("data-y-handle"));
629
+ const m = lines[i];
630
+ const v = liveRef.current[i];
631
+ 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";
634
+ el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
635
+ const label = m.label ? pick(m.label, lang) : "";
636
+ const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
637
+ const unnamed = lines.flatMap((one, k) => one.drag && !one.label ? [k] : []);
638
+ el2.setAttribute("aria-label", label || referenceLineName(lang, unnamed.indexOf(i) + 1, unnamed.length));
639
+ el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
640
+ el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
641
+ });
390
642
  if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
391
643
  }
392
644
  ]
393
645
  }
394
646
  };
647
+ if (!ticks) {
648
+ hostRef.current?.removeAttribute("data-x-tick-labels");
649
+ hostRef.current?.removeAttribute("data-ticks-repeated");
650
+ }
395
651
  uRef.current?.destroy();
396
652
  uRef.current = new uPlot(opts, data, el);
397
653
  return () => {
@@ -402,9 +658,27 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
402
658
  useEffect(() => {
403
659
  uRef.current?.setData(data);
404
660
  }, [data]);
661
+ const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
662
+ const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
663
+ useEffect(() => {
664
+ uRef.current?.redraw(false);
665
+ }, [linesKey, overlaysKey]);
666
+ useEffect(() => {
667
+ const u = uRef.current;
668
+ if (!u) return;
669
+ baseEntries.forEach(({ s, i }, k) => {
670
+ const dim = soloStyle(soloIdx, i, s.mode === "points" ? 1.5 : s.width ?? 2);
671
+ const one = u.series[k + 1];
672
+ if (!one) return;
673
+ one.alpha = dim.alpha;
674
+ one.width = dim.width;
675
+ });
676
+ u.redraw(false);
677
+ }, [soloIdx, baseEntries]);
405
678
  const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
406
- `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
407
- ...series.map((s, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
679
+ // 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}` : ""}`,
681
+ ...baseEntries.map(({ s }, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
408
682
  ].join(" \xB7 ") : hint ? pick(hint, lang) : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
409
683
  return /* @__PURE__ */ jsxs(
410
684
  "div",
@@ -420,16 +694,22 @@ function UPlotChart({ x, y, series, marks, yMarks, height = 280, onCursor, onPic
420
694
  "data-log-x": x.log ? "1" : void 0,
421
695
  "data-pick": onPick ? "1" : void 0,
422
696
  "data-y-marks": yMarks?.length ? String(yMarks.length) : void 0,
697
+ "data-y-drag": dragCount ? String(dragCount) : void 0,
698
+ "data-overlay": overlays.length ? String(overlays.length) : void 0,
699
+ "data-solo": soloIdx !== null ? String(soloIdx) : void 0,
423
700
  children: [
424
701
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
425
- 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: [
702
+ (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
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
704
+ pick(s.label, lang)
705
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
426
706
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
427
707
  pick(s.label, lang)
428
- ] }, i)) }),
708
+ ] }) }, i)) }),
429
709
  /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
430
710
  ]
431
711
  }
432
712
  );
433
713
  }
434
714
 
435
- export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, drawMarks, drawYMarks, logDecades, nearestPoint, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
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 };
@@ -214,4 +214,76 @@ function Stage({ label, children, className }) {
214
214
  );
215
215
  }
216
216
 
217
- export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens };
217
+ // src/chart/text.ts
218
+ function referenceLineName(lang, n, of) {
219
+ const base = lang === "es" ? "L\xEDnea de referencia" : "Reference line";
220
+ return of > 1 ? `${base} ${n}` : base;
221
+ }
222
+ var context;
223
+ function fontFamily() {
224
+ if (typeof document === "undefined") return "sans-serif";
225
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-sans").trim() || "sans-serif";
226
+ }
227
+ function textWidth(text, px, weight = 400) {
228
+ if (context === void 0) {
229
+ try {
230
+ context = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
231
+ } catch {
232
+ context = null;
233
+ }
234
+ }
235
+ if (!context) return text.length * px * 0.62;
236
+ context.font = `${weight} ${px}px ${fontFamily()}`;
237
+ return context.measureText(text).width;
238
+ }
239
+ function widestLabel(labels, px, weight = 400) {
240
+ return Math.max(0, ...labels.map((label) => textWidth(label, px, weight)));
241
+ }
242
+ function shorten(line, width, px) {
243
+ if (textWidth(line, px) <= width) return line;
244
+ let cut = line;
245
+ while (cut.length > 1 && textWidth(`${cut}\u2026`, px) > width) cut = cut.slice(0, -1);
246
+ return `${cut.trimEnd()}\u2026`;
247
+ }
248
+ function fitLabel(label, width, px, maxLines = 2) {
249
+ if (textWidth(label, px) <= width) return { lines: [label], shortened: false };
250
+ const words = label.split(" ");
251
+ if (maxLines === 2 && words.length > 1) {
252
+ let best = null;
253
+ let bestWidth = Infinity;
254
+ for (let k = 1; k < words.length; k += 1) {
255
+ const first = words.slice(0, k).join(" ");
256
+ const second = words.slice(k).join(" ");
257
+ const widest = Math.max(textWidth(first, px), textWidth(second, px));
258
+ if (widest < bestWidth) {
259
+ bestWidth = widest;
260
+ best = [first, second];
261
+ }
262
+ }
263
+ if (best && bestWidth <= width) return { lines: best, shortened: false };
264
+ if (best) return { lines: [shorten(best[0], width, px), shorten(best[1], width, px)], shortened: true };
265
+ }
266
+ return { lines: [shorten(label, width, px)], shortened: true };
267
+ }
268
+ function niceStep(x) {
269
+ if (!(x > 0) || !Number.isFinite(x)) return 1;
270
+ const magnitude = 10 ** Math.floor(Math.log10(x));
271
+ return ([1, 2, 2.5, 5, 10].find((m) => m * magnitude >= x * (1 - 1e-12)) ?? 10) * magnitude;
272
+ }
273
+ function niceTicks(lo, hi, room, labelSize, most = 6) {
274
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [0];
275
+ if (hi < lo) [lo, hi] = [hi, lo];
276
+ if (hi === lo) hi = lo === 0 ? 1 : lo + Math.abs(lo);
277
+ const count = Math.max(2, Math.min(most, Math.floor(room / (labelSize + 16)) + 1));
278
+ const step = niceStep((hi - lo) / (count - 1));
279
+ const first = Math.floor(lo / step + 1e-9) * step;
280
+ const ticks = [];
281
+ for (let k = 0; first + k * step <= hi + step * 1e-9 || ticks.length < 2; k += 1) {
282
+ ticks.push(Number((first + k * step).toPrecision(12)));
283
+ if (ticks.length > 50) break;
284
+ }
285
+ if (ticks[ticks.length - 1] < hi - step * 1e-9) ticks.push(Number((ticks[ticks.length - 1] + step).toPrecision(12)));
286
+ return ticks;
287
+ }
288
+
289
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, fitLabel, fontFamily, formatNumber, formatTicks, niceStep, niceTicks, pick, readTheme, referenceLineName, resolveToken, textWidth, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens, widestLabel };