@camstack/ui-library 1.2.329 → 1.2.330

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/index.cjs CHANGED
@@ -22518,6 +22518,641 @@ function bucketIndexAt(buckets, bucketMs, t) {
22518
22518
  }
22519
22519
  return null;
22520
22520
  }
22521
+ var UNKNOWN$1 = "—";
22522
+ var NO_INFERENCE = new Set([
22523
+ "information",
22524
+ "duration",
22525
+ "duration-ms",
22526
+ "none",
22527
+ "device",
22528
+ "count",
22529
+ "index",
22530
+ "ratio",
22531
+ "humidity",
22532
+ "percentage"
22533
+ ]);
22534
+ var HIDDEN_INTERACTION = {
22535
+ selection: {
22536
+ visible: false,
22537
+ dataX: null,
22538
+ anchorX: 0,
22539
+ anchorY: 0,
22540
+ pinned: false
22541
+ },
22542
+ gesture: null,
22543
+ lockHorizontalScroll: false
22544
+ };
22545
+ var TOOLTIP_SIDES = [
22546
+ {
22547
+ placed: "right",
22548
+ vertical: "above"
22549
+ },
22550
+ {
22551
+ placed: "left",
22552
+ vertical: "above"
22553
+ },
22554
+ {
22555
+ placed: "right",
22556
+ vertical: "below"
22557
+ },
22558
+ {
22559
+ placed: "left",
22560
+ vertical: "below"
22561
+ }
22562
+ ];
22563
+ /** Index of the sample closest to `x`. A tie keeps the earlier sample. */
22564
+ function nearestPointIndex(xs, x) {
22565
+ if (xs.length === 0 || !Number.isFinite(x)) return null;
22566
+ let lo = 0;
22567
+ let hi = xs.length - 1;
22568
+ while (lo < hi) {
22569
+ const mid = Math.floor((lo + hi) / 2);
22570
+ if ((xs[mid] ?? 0) < x) lo = mid + 1;
22571
+ else hi = mid;
22572
+ }
22573
+ const right = lo;
22574
+ const left = Math.max(0, right - 1);
22575
+ return Math.abs((xs[left] ?? 0) - x) <= Math.abs((xs[right] ?? 0) - x) ? left : right;
22576
+ }
22577
+ /** Data x for a pointer inside the plot. `null` when it is outside and clamping is off. */
22578
+ function dataXFromPointer(input) {
22579
+ if (!(input.plotWidth > 0) || !Number.isFinite(input.localX)) return null;
22580
+ const rel = input.localX - input.plotLeft;
22581
+ if (!input.clamp && (rel < 0 || rel > input.plotWidth)) return null;
22582
+ const clamped = Math.min(input.plotWidth, Math.max(0, rel));
22583
+ return input.xMin + clamped / input.plotWidth * (input.xMax - input.xMin);
22584
+ }
22585
+ /** Bucket that contains `dataX`, or the nearest one when `dataX` falls in a gap. */
22586
+ function bucketSelectionIndex(buckets, bucketMs, dataX) {
22587
+ if (buckets.length === 0 || !Number.isFinite(dataX)) return null;
22588
+ let best = 0;
22589
+ let bestDist = Number.POSITIVE_INFINITY;
22590
+ for (let i = 0; i < buckets.length; i++) {
22591
+ const start = buckets[i]?.t;
22592
+ if (start === void 0) continue;
22593
+ const end = start + bucketMs;
22594
+ if (dataX >= start && dataX < end) return i;
22595
+ const dist = dataX < start ? start - dataX : dataX - end;
22596
+ if (dist < bestDist) {
22597
+ bestDist = dist;
22598
+ best = i;
22599
+ }
22600
+ }
22601
+ return best;
22602
+ }
22603
+ /** Next keyboard stop. A null current starts at the first or last stop. */
22604
+ function stepStop(stops, current, direction) {
22605
+ if (stops.length === 0) return null;
22606
+ const ordered = [...stops].sort((a, b) => a - b);
22607
+ if (current === null || !Number.isFinite(current)) return direction > 0 ? ordered[0] ?? null : ordered[ordered.length - 1] ?? null;
22608
+ let idx = 0;
22609
+ let best = Number.POSITIVE_INFINITY;
22610
+ for (let i = 0; i < ordered.length; i++) {
22611
+ const dist = Math.abs((ordered[i] ?? 0) - current);
22612
+ if (dist < best) {
22613
+ best = dist;
22614
+ idx = i;
22615
+ }
22616
+ }
22617
+ const next = idx + direction;
22618
+ if (next < 0 || next >= ordered.length) return ordered[idx] ?? null;
22619
+ return ordered[next] ?? null;
22620
+ }
22621
+ function shown(dataX, anchorX, anchorY, pinned) {
22622
+ return {
22623
+ visible: true,
22624
+ dataX,
22625
+ anchorX,
22626
+ anchorY,
22627
+ pinned
22628
+ };
22629
+ }
22630
+ function resting(selection, gesture, lock) {
22631
+ return {
22632
+ selection,
22633
+ gesture,
22634
+ lockHorizontalScroll: lock
22635
+ };
22636
+ }
22637
+ function lockAxis(gesture, x, y) {
22638
+ if (gesture.axis !== "pending") return gesture.axis;
22639
+ const dx = Math.abs(x - gesture.originX);
22640
+ const dy = Math.abs(y - gesture.originY);
22641
+ if (dx < 10 && dy < 10) return "pending";
22642
+ return dx >= dy ? "x" : "y";
22643
+ }
22644
+ function onDown(state, event) {
22645
+ if (event.kind === "mouse") return state;
22646
+ return resting(state.selection, {
22647
+ kind: event.kind,
22648
+ originX: event.x,
22649
+ originY: event.y,
22650
+ axis: "pending"
22651
+ }, false);
22652
+ }
22653
+ function onMove(state, event) {
22654
+ if (event.kind === "mouse") {
22655
+ if (event.dataX === null) return state.selection.pinned ? state : HIDDEN_INTERACTION;
22656
+ return resting(shown(event.dataX, event.x, event.y, false), null, false);
22657
+ }
22658
+ const gesture = state.gesture;
22659
+ if (gesture === null) return state;
22660
+ const axis = lockAxis(gesture, event.x, event.y);
22661
+ const nextGesture = {
22662
+ ...gesture,
22663
+ axis
22664
+ };
22665
+ if (axis !== "x" || event.dataX === null) return resting(state.selection, nextGesture, false);
22666
+ return resting(shown(event.dataX, event.x, event.y, true), nextGesture, true);
22667
+ }
22668
+ function onUp(state, event) {
22669
+ if (event.kind === "mouse" || state.gesture === null) return state;
22670
+ if (state.gesture.axis === "y" || state.gesture.axis === "x") return resting(state.selection, null, false);
22671
+ if (state.selection.visible && state.selection.pinned) return HIDDEN_INTERACTION;
22672
+ if (event.dataX === null) return HIDDEN_INTERACTION;
22673
+ return resting(shown(event.dataX, event.x, event.y, true), null, false);
22674
+ }
22675
+ /** Pointer, keyboard and dismiss rules for one chart. */
22676
+ function reduceChartInteraction(state, event) {
22677
+ switch (event.type) {
22678
+ case "hide":
22679
+ case "outside": return HIDDEN_INTERACTION;
22680
+ case "leave": return state.selection.pinned ? resting(state.selection, null, false) : HIDDEN_INTERACTION;
22681
+ case "hover": return resting(shown(event.dataX, event.anchorX, event.anchorY, false), null, false);
22682
+ case "key": return resting(shown(event.dataX, event.anchorX, event.anchorY, true), null, false);
22683
+ case "down": return onDown(state, event);
22684
+ case "move": return onMove(state, event);
22685
+ case "up": return onUp(state, event);
22686
+ default: return event;
22687
+ }
22688
+ }
22689
+ function quantityFromUnit(unit) {
22690
+ if (unit === null || unit === void 0 || unit.length === 0) return void 0;
22691
+ let found;
22692
+ for (const key of Object.keys(_camstack_types.QUANTITY_BASE)) {
22693
+ if (!(0, _camstack_types.isReadingQuantity)(key) || NO_INFERENCE.has(key)) continue;
22694
+ if (_camstack_types.QUANTITY_BASE[key] !== unit) continue;
22695
+ if (found !== void 0) return void 0;
22696
+ found = key;
22697
+ }
22698
+ return found;
22699
+ }
22700
+ function withUnit(text, unit, unknown) {
22701
+ if (text.length === 0) return unknown;
22702
+ if (unit.length === 0) return text;
22703
+ if (unit === "%") return `${text}%`;
22704
+ return `${text} ${unit}`;
22705
+ }
22706
+ /** One sample, with its unit. Missing samples are an em dash, never zero. */
22707
+ function formatTooltipValue(input) {
22708
+ const unknown = input.unknownLabel ?? UNKNOWN$1;
22709
+ if (input.value === null || !Number.isFinite(input.value)) return unknown;
22710
+ const quantity = input.quantity === "device" ? void 0 : input.quantity ?? quantityFromUnit(input.unit);
22711
+ if (quantity === void 0) return formatChartValue(input.value, input.unit ?? null);
22712
+ const rendered = (0, _camstack_types.formatReading)({
22713
+ value: input.value,
22714
+ quantity,
22715
+ preference: input.preference,
22716
+ precision: input.precision
22717
+ });
22718
+ return withUnit(rendered.text, rendered.unit, unknown);
22719
+ }
22720
+ function liveText(heading, lines) {
22721
+ if (lines.length === 0) return heading;
22722
+ return `${heading}, ${lines.map((line) => `${line.label} ${line.value}`).join(", ")}`;
22723
+ }
22724
+ function asBody(heading, lines) {
22725
+ return {
22726
+ heading,
22727
+ lines,
22728
+ liveText: liveText(heading, lines)
22729
+ };
22730
+ }
22731
+ /** Time plus one line per visible series. */
22732
+ function lineTooltipBody(input) {
22733
+ const lines = input.series.filter((sample) => sample.visible !== false).map((sample) => ({
22734
+ id: sample.id,
22735
+ label: sample.label,
22736
+ value: formatTooltipValue({
22737
+ value: sample.value,
22738
+ quantity: sample.quantity,
22739
+ unit: sample.unit,
22740
+ precision: sample.precision,
22741
+ preference: input.preference
22742
+ })
22743
+ }));
22744
+ return asBody(input.timeLabel, lines);
22745
+ }
22746
+ /** The state in force at `t`, and how long that span lasts. */
22747
+ function stateSpanAt(segments, t) {
22748
+ for (const segment of segments) if (t >= segment.from && t < segment.to) return {
22749
+ state: segment.state,
22750
+ spanMs: Math.max(0, segment.to - segment.from)
22751
+ };
22752
+ return null;
22753
+ }
22754
+ function stateTooltipValue(stateLabel, spanMs) {
22755
+ if (stateLabel === null || stateLabel.length === 0) return UNKNOWN$1;
22756
+ if (spanMs === null || !(spanMs > 0)) return stateLabel;
22757
+ return `${stateLabel} · ${formatSpan(spanMs)}`;
22758
+ }
22759
+ /** One row per state series: the label and the span under the pointer. */
22760
+ function stateTooltipBody(input) {
22761
+ const lines = input.rows.map((row) => ({
22762
+ id: row.id,
22763
+ label: row.label,
22764
+ value: stateTooltipValue(row.stateLabel, row.spanMs)
22765
+ }));
22766
+ return asBody(input.timeLabel, lines);
22767
+ }
22768
+ /** `start – end` for one bar bucket, at the window's time precision. */
22769
+ function barRangeLabel(start, bucketMs, spanMs) {
22770
+ return `${formatCursorTime(start, spanMs)} – ${formatCursorTime(start + bucketMs, spanMs)}`;
22771
+ }
22772
+ /** The bucket's range, and each layer's value with its unit. */
22773
+ function barTooltipBody(input) {
22774
+ const lines = input.layers.map((layer) => ({
22775
+ id: layer.id,
22776
+ label: layer.label,
22777
+ value: formatTooltipValue({
22778
+ value: layer.value,
22779
+ quantity: layer.quantity,
22780
+ unit: layer.unit,
22781
+ precision: layer.precision,
22782
+ preference: input.preference
22783
+ })
22784
+ }));
22785
+ return asBody(input.rangeLabel, lines);
22786
+ }
22787
+ function sideOrigin(side, input, gap) {
22788
+ return {
22789
+ left: side.placed === "right" ? input.anchorX + gap : input.anchorX - gap - input.tooltipWidth,
22790
+ top: side.vertical === "above" ? input.anchorY - gap - input.tooltipHeight : input.anchorY + gap
22791
+ };
22792
+ }
22793
+ function insideBounds(left, top, input) {
22794
+ const pad = 4;
22795
+ return left >= pad && top >= pad && left + input.tooltipWidth <= input.boundsWidth - pad && top + input.tooltipHeight <= input.boundsHeight - pad;
22796
+ }
22797
+ function coversPoint(left, top, input) {
22798
+ return input.anchorX >= left && input.anchorX <= left + input.tooltipWidth && input.anchorY >= top && input.anchorY <= top + input.tooltipHeight;
22799
+ }
22800
+ function clampOrigin(left, top, input) {
22801
+ const pad = 4;
22802
+ const maxLeft = Math.max(pad, input.boundsWidth - pad - input.tooltipWidth);
22803
+ const maxTop = Math.max(pad, input.boundsHeight - pad - input.tooltipHeight);
22804
+ return {
22805
+ left: Math.min(Math.max(left, pad), maxLeft),
22806
+ top: Math.min(Math.max(top, pad), maxTop)
22807
+ };
22808
+ }
22809
+ /**
22810
+ * Place the tooltip beside the point, inside the chart. The first side that
22811
+ * fits and does not cover the point wins; the edges flip before the corners.
22812
+ */
22813
+ function placeChartTooltip(input) {
22814
+ const gap = input.gap ?? 12;
22815
+ let fallback = null;
22816
+ for (const side of TOOLTIP_SIDES) {
22817
+ const origin = sideOrigin(side, input, gap);
22818
+ const clamped = clampOrigin(origin.left, origin.top, input);
22819
+ const placement = {
22820
+ ...side,
22821
+ ...clamped
22822
+ };
22823
+ if (fallback === null && !coversPoint(clamped.left, clamped.top, input)) fallback = placement;
22824
+ if (insideBounds(origin.left, origin.top, input) && !coversPoint(origin.left, origin.top, input)) return {
22825
+ ...side,
22826
+ ...origin
22827
+ };
22828
+ }
22829
+ if (fallback !== null) return fallback;
22830
+ const first = TOOLTIP_SIDES[0] ?? {
22831
+ placed: "right",
22832
+ vertical: "above"
22833
+ };
22834
+ const clamped = clampOrigin(sideOrigin(first, input, gap).left, sideOrigin(first, input, gap).top, input);
22835
+ return {
22836
+ ...first,
22837
+ ...clamped
22838
+ };
22839
+ }
22840
+ //#endregion
22841
+ //#region src/composites/charts/chart-tooltip-host.tsx
22842
+ /**
22843
+ * One tooltip for every chart. The plot stays in `UPlotHost`; this shell
22844
+ * tracks the pointer, a touch scrub and the arrow keys, and draws the
22845
+ * readout inside the chart.
22846
+ */
22847
+ function pointerKind(pointerType) {
22848
+ return pointerType === "mouse" ? "mouse" : "touch";
22849
+ }
22850
+ function pointerSample(clientX, clientY, shell, plot, spec, clamp) {
22851
+ const box = shell.getBoundingClientRect();
22852
+ const anchorX = clientX - box.left;
22853
+ const anchorY = clientY - box.top;
22854
+ if (plot !== null) {
22855
+ const over = plot.over.getBoundingClientRect();
22856
+ const left = clientX - over.left;
22857
+ const top = clientY - over.top;
22858
+ const minX = plot.bbox.left;
22859
+ const maxX = plot.bbox.left + plot.bbox.width;
22860
+ const inside = left >= minX && left <= maxX && top >= plot.bbox.top && top <= plot.bbox.top + plot.bbox.height;
22861
+ if (!inside && !clamp) return {
22862
+ dataX: null,
22863
+ anchorX,
22864
+ anchorY,
22865
+ inside: false
22866
+ };
22867
+ return {
22868
+ dataX: plot.posToVal(Math.min(maxX, Math.max(minX, left)), "x"),
22869
+ anchorX,
22870
+ anchorY,
22871
+ inside
22872
+ };
22873
+ }
22874
+ const dataX = dataXFromPointer({
22875
+ localX: anchorX,
22876
+ plotLeft: 0,
22877
+ plotWidth: box.width,
22878
+ xMin: spec.xMin,
22879
+ xMax: spec.xMax,
22880
+ clamp
22881
+ });
22882
+ return {
22883
+ dataX,
22884
+ anchorX,
22885
+ anchorY,
22886
+ inside: dataX !== null
22887
+ };
22888
+ }
22889
+ function anchorForDataX(dataX, shell, plot, spec) {
22890
+ if (plot !== null && shell !== null) {
22891
+ const over = plot.over.getBoundingClientRect();
22892
+ const box = shell.getBoundingClientRect();
22893
+ return {
22894
+ x: over.left - box.left + plot.valToPos(dataX, "x"),
22895
+ y: over.top - box.top + plot.bbox.top + plot.bbox.height / 2
22896
+ };
22897
+ }
22898
+ const width = shell?.getBoundingClientRect().width ?? 0;
22899
+ const height = shell?.getBoundingClientRect().height ?? 0;
22900
+ const span = spec.xMax - spec.xMin;
22901
+ const frac = span === 0 ? 0 : (dataX - spec.xMin) / span;
22902
+ return {
22903
+ x: Math.min(1, Math.max(0, frac)) * width,
22904
+ y: height / 2
22905
+ };
22906
+ }
22907
+ function movePlotCursor(plot, dataX) {
22908
+ if (plot === null) return;
22909
+ if (dataX === null) {
22910
+ plot.setCursor({
22911
+ left: -10,
22912
+ top: -10
22913
+ });
22914
+ return;
22915
+ }
22916
+ plot.setCursor({
22917
+ left: plot.valToPos(dataX, "x"),
22918
+ top: plot.bbox.top + plot.bbox.height / 2
22919
+ });
22920
+ }
22921
+ function ChartTooltipCard({ view }) {
22922
+ const ref = (0, react$1.useRef)(null);
22923
+ (0, react$1.useLayoutEffect)(() => {
22924
+ const el = ref.current;
22925
+ if (el === null) return;
22926
+ const placed = placeChartTooltip({
22927
+ anchorX: view.anchorX,
22928
+ anchorY: view.anchorY,
22929
+ tooltipWidth: el.offsetWidth,
22930
+ tooltipHeight: el.offsetHeight,
22931
+ boundsWidth: view.boundsWidth,
22932
+ boundsHeight: view.boundsHeight
22933
+ });
22934
+ el.style.left = `${String(placed.left)}px`;
22935
+ el.style.top = `${String(placed.top)}px`;
22936
+ }, [view]);
22937
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22938
+ ref,
22939
+ role: "tooltip",
22940
+ "data-chart-tooltip": "",
22941
+ className: "pointer-events-none absolute z-20 max-w-64 rounded-md border border-border bg-background-elevated px-2 py-1 text-foreground shadow-md",
22942
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
22943
+ className: "m-0 text-[10px] tabular-nums text-foreground-subtle",
22944
+ children: view.body.heading
22945
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
22946
+ className: "m-0 list-none p-0",
22947
+ children: view.body.lines.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
22948
+ className: "flex items-baseline justify-between gap-3 text-xs",
22949
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22950
+ className: "min-w-0 truncate text-foreground-muted",
22951
+ children: line.label
22952
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22953
+ className: "shrink-0 tabular-nums",
22954
+ children: line.value
22955
+ })]
22956
+ }, line.id))
22957
+ })]
22958
+ });
22959
+ }
22960
+ function ChartTooltipShell({ spec, plotRef, kind, label, inline = false, children }) {
22961
+ const shellRef = (0, react$1.useRef)(null);
22962
+ const specRef = (0, react$1.useRef)(spec);
22963
+ specRef.current = spec;
22964
+ const interactionRef = (0, react$1.useRef)(HIDDEN_INTERACTION);
22965
+ const dataXRef = (0, react$1.useRef)(null);
22966
+ const [view, setView] = (0, react$1.useState)(null);
22967
+ const pinned = view !== null && interactionRef.current.selection.pinned;
22968
+ const commit = (0, react$1.useCallback)((next) => {
22969
+ interactionRef.current = next;
22970
+ const current = specRef.current;
22971
+ const shell = shellRef.current;
22972
+ const dataX = next.selection.visible ? next.selection.dataX : null;
22973
+ const body = current !== null && dataX !== null ? current.resolve(dataX) : null;
22974
+ if (body === null || dataX === null) {
22975
+ setView(null);
22976
+ if (dataXRef.current !== null) {
22977
+ dataXRef.current = null;
22978
+ current?.onDataX?.(null);
22979
+ }
22980
+ return;
22981
+ }
22982
+ setView({
22983
+ body,
22984
+ anchorX: next.selection.anchorX,
22985
+ anchorY: next.selection.anchorY,
22986
+ boundsWidth: shell?.getBoundingClientRect().width ?? 0,
22987
+ boundsHeight: shell?.getBoundingClientRect().height ?? 0
22988
+ });
22989
+ if (dataXRef.current !== dataX) {
22990
+ dataXRef.current = dataX;
22991
+ current?.onDataX?.(dataX);
22992
+ }
22993
+ }, []);
22994
+ (0, react$1.useEffect)(() => {
22995
+ const shell = shellRef.current;
22996
+ if (shell === null) return;
22997
+ const sample = (event, clamp) => {
22998
+ const current = specRef.current;
22999
+ if (current === null) return null;
23000
+ return pointerSample(event.clientX, event.clientY, shell, plotRef.current, current, clamp);
23001
+ };
23002
+ const onDown = (event) => {
23003
+ if (pointerKind(event.pointerType) === "mouse") return;
23004
+ const at = sample(event, false);
23005
+ if (at === null) return;
23006
+ commit(reduceChartInteraction(interactionRef.current, {
23007
+ type: "down",
23008
+ kind: "touch",
23009
+ x: at.anchorX,
23010
+ y: at.anchorY,
23011
+ dataX: at.dataX
23012
+ }));
23013
+ };
23014
+ const onMove = (event) => {
23015
+ const kind = pointerKind(event.pointerType);
23016
+ const scrubbing = interactionRef.current.gesture?.axis === "x";
23017
+ const at = sample(event, kind !== "mouse" && scrubbing);
23018
+ if (at === null) return;
23019
+ if (kind === "mouse") {
23020
+ if (!at.inside || at.dataX === null) {
23021
+ commit(reduceChartInteraction(interactionRef.current, { type: "leave" }));
23022
+ if (!interactionRef.current.selection.visible) movePlotCursor(plotRef.current, null);
23023
+ return;
23024
+ }
23025
+ commit(reduceChartInteraction(interactionRef.current, {
23026
+ type: "hover",
23027
+ dataX: at.dataX,
23028
+ anchorX: at.anchorX,
23029
+ anchorY: at.anchorY
23030
+ }));
23031
+ movePlotCursor(plotRef.current, at.dataX);
23032
+ return;
23033
+ }
23034
+ const next = reduceChartInteraction(interactionRef.current, {
23035
+ type: "move",
23036
+ kind: "touch",
23037
+ x: at.anchorX,
23038
+ y: at.anchorY,
23039
+ dataX: at.dataX
23040
+ });
23041
+ if (next.lockHorizontalScroll) {
23042
+ event.preventDefault();
23043
+ if (event.pointerId !== 0 && !shell.hasPointerCapture(event.pointerId)) shell.setPointerCapture(event.pointerId);
23044
+ }
23045
+ commit(next);
23046
+ if (next.selection.dataX !== null && next.selection.visible) movePlotCursor(plotRef.current, next.selection.dataX);
23047
+ };
23048
+ const onUp = (event) => {
23049
+ if (pointerKind(event.pointerType) === "mouse") return;
23050
+ const at = sample(event, interactionRef.current.gesture?.axis === "x");
23051
+ if (at === null) return;
23052
+ const next = reduceChartInteraction(interactionRef.current, {
23053
+ type: "up",
23054
+ kind: "touch",
23055
+ x: at.anchorX,
23056
+ y: at.anchorY,
23057
+ dataX: at.dataX
23058
+ });
23059
+ commit(next);
23060
+ movePlotCursor(plotRef.current, next.selection.visible ? next.selection.dataX : null);
23061
+ };
23062
+ const onLeave = (event) => {
23063
+ if (pointerKind(event.pointerType) !== "mouse") return;
23064
+ const next = reduceChartInteraction(interactionRef.current, { type: "leave" });
23065
+ commit(next);
23066
+ if (!next.selection.visible) movePlotCursor(plotRef.current, null);
23067
+ };
23068
+ shell.addEventListener("pointerdown", onDown);
23069
+ shell.addEventListener("pointermove", onMove);
23070
+ shell.addEventListener("pointerup", onUp);
23071
+ shell.addEventListener("pointercancel", onUp);
23072
+ shell.addEventListener("pointerleave", onLeave);
23073
+ return () => {
23074
+ shell.removeEventListener("pointerdown", onDown);
23075
+ shell.removeEventListener("pointermove", onMove);
23076
+ shell.removeEventListener("pointerup", onUp);
23077
+ shell.removeEventListener("pointercancel", onUp);
23078
+ shell.removeEventListener("pointerleave", onLeave);
23079
+ };
23080
+ }, [commit, plotRef]);
23081
+ (0, react$1.useEffect)(() => {
23082
+ if (!pinned) return;
23083
+ const onDoc = (event) => {
23084
+ const shell = shellRef.current;
23085
+ if (shell !== null && event.target instanceof Node && shell.contains(event.target)) return;
23086
+ commit(reduceChartInteraction(interactionRef.current, { type: "outside" }));
23087
+ movePlotCursor(plotRef.current, null);
23088
+ };
23089
+ document.addEventListener("pointerdown", onDoc, true);
23090
+ return () => document.removeEventListener("pointerdown", onDoc, true);
23091
+ }, [
23092
+ commit,
23093
+ pinned,
23094
+ plotRef
23095
+ ]);
23096
+ const onKeyDown = (event) => {
23097
+ const current = specRef.current;
23098
+ if (current === null) return;
23099
+ if (event.key === "Escape") {
23100
+ event.preventDefault();
23101
+ commit(reduceChartInteraction(interactionRef.current, { type: "hide" }));
23102
+ movePlotCursor(plotRef.current, null);
23103
+ return;
23104
+ }
23105
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
23106
+ event.preventDefault();
23107
+ const from = interactionRef.current.selection.visible ? interactionRef.current.selection.dataX : null;
23108
+ const dataX = stepStop(current.stops, from, event.key === "ArrowRight" ? 1 : -1);
23109
+ if (dataX === null) return;
23110
+ const anchor = anchorForDataX(dataX, shellRef.current, plotRef.current, current);
23111
+ commit(reduceChartInteraction(interactionRef.current, {
23112
+ type: "key",
23113
+ dataX,
23114
+ anchorX: anchor.x,
23115
+ anchorY: anchor.y
23116
+ }));
23117
+ movePlotCursor(plotRef.current, dataX);
23118
+ };
23119
+ const setShell = (el) => {
23120
+ shellRef.current = el;
23121
+ };
23122
+ const className = cn("relative min-w-0 touch-pan-y outline-none focus-visible:ring-2 focus-visible:ring-primary", inline ? "inline-block w-full" : "w-full");
23123
+ const tooltip = view !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTooltipCard, { view }) : null;
23124
+ const live = view !== null && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
23125
+ className: "sr-only",
23126
+ "aria-live": "polite",
23127
+ "data-chart-tooltip-live": "",
23128
+ children: view.body.liveText
23129
+ }), document.body) : null;
23130
+ const shared = {
23131
+ className,
23132
+ "data-chart-shell": "",
23133
+ "data-chart-kind": kind,
23134
+ tabIndex: spec === null ? void 0 : 0,
23135
+ "aria-label": label,
23136
+ onKeyDown
23137
+ };
23138
+ return inline ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
23139
+ ref: setShell,
23140
+ ...shared,
23141
+ children: [
23142
+ children,
23143
+ tooltip,
23144
+ live
23145
+ ]
23146
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
23147
+ ref: setShell,
23148
+ ...shared,
23149
+ children: [
23150
+ children,
23151
+ tooltip,
23152
+ live
23153
+ ]
23154
+ });
23155
+ }
22521
23156
  //#endregion
22522
23157
  //#region src/composites/charts/uplot-host.tsx
22523
23158
  /**
@@ -22548,7 +23183,7 @@ function loadUPlot() {
22548
23183
  if (ctorPromise === null) ctorPromise = canDrawCanvas() ? Promise.resolve().then(() => require("./uPlot.esm-BbxBUnav.cjs")).then((m) => m.default).catch(() => null) : Promise.resolve(null);
22549
23184
  return ctorPromise;
22550
23185
  }
22551
- function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, className, inline = false }) {
23186
+ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, className, inline = false, tooltip = null, kind, label }) {
22552
23187
  const hostRef = (0, react$1.useRef)(null);
22553
23188
  const plotRef = (0, react$1.useRef)(null);
22554
23189
  const [ctor, setCtor] = (0, react$1.useState)(null);
@@ -22580,6 +23215,8 @@ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, classNa
22580
23215
  width,
22581
23216
  height
22582
23217
  }, latest.current.data, el);
23218
+ u.over.style.touchAction = "pan-y";
23219
+ u.root.style.touchAction = "pan-y";
22583
23220
  plotRef.current = u;
22584
23221
  latest.current.onInstance?.(u);
22585
23222
  return () => {
@@ -22621,14 +23258,21 @@ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, classNa
22621
23258
  const setHost = (el) => {
22622
23259
  hostRef.current = el;
22623
23260
  };
22624
- return inline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22625
- ref: setHost,
22626
- className: className ?? "block w-full min-w-0",
22627
- "data-chart-plot": ""
22628
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22629
- ref: setHost,
22630
- className: className ?? "w-full min-w-0",
22631
- "data-chart-plot": ""
23261
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTooltipShell, {
23262
+ spec: tooltip,
23263
+ plotRef,
23264
+ kind,
23265
+ label,
23266
+ inline,
23267
+ children: inline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
23268
+ ref: setHost,
23269
+ className: className ?? "block w-full min-w-0",
23270
+ "data-chart-plot": ""
23271
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
23272
+ ref: setHost,
23273
+ className: className ?? "w-full min-w-0",
23274
+ "data-chart-plot": ""
23275
+ })
22632
23276
  });
22633
23277
  }
22634
23278
  //#endregion
@@ -22643,7 +23287,7 @@ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, classNa
22643
23287
  * lives inside another control; there a bucket can be selected (`onSelect`)
22644
23288
  * and every bucket is also a real button for keyboard and screen readers.
22645
23289
  */
22646
- function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, formatValue, height, stacked, showTotal = false, syncKey, pending = false, emptyMessage, actions, footer, windowToggle, compact = false, minBarFraction = 0, selectedIndex = null, onSelect, bucketLabel, bucketTestId, className }) {
23290
+ function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, formatValue, displayPreference, height, stacked, showTotal = false, syncKey, pending = false, emptyMessage, actions, footer, windowToggle, compact = false, minBarFraction = 0, selectedIndex = null, onSelect, bucketLabel, bucketTestId, className }) {
22647
23291
  const t = useUiText();
22648
23292
  const plotHeight = height ?? (compact ? 24 : 100);
22649
23293
  const isStacked = stacked ?? layers.length > 1;
@@ -22655,6 +23299,37 @@ function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, forma
22655
23299
  ]);
22656
23300
  const totals = (0, react$1.useMemo)(() => bucketTotals(buckets, layers.length), [buckets, layers.length]);
22657
23301
  const [cursorIdx, setCursorIdx] = (0, react$1.useState)(null);
23302
+ const tooltip = (0, react$1.useMemo)(() => ({
23303
+ xMin: range.from,
23304
+ xMax: range.to,
23305
+ stops: buckets.map((bucket) => bucket.t),
23306
+ resolve: (dataX) => {
23307
+ const index = bucketSelectionIndex(buckets, bucketMs, dataX);
23308
+ if (index === null) return null;
23309
+ const bucket = buckets[index];
23310
+ if (bucket === void 0) return null;
23311
+ return barTooltipBody({
23312
+ rangeLabel: barRangeLabel(bucket.t, bucketMs, range.to - range.from),
23313
+ preference: displayPreference,
23314
+ layers: layers.map((layer, li) => ({
23315
+ id: layer.id,
23316
+ label: layer.label,
23317
+ value: bucket.values[li] ?? null,
23318
+ unit
23319
+ }))
23320
+ });
23321
+ },
23322
+ onDataX: (dataX) => {
23323
+ setCursorIdx(dataX === null ? null : bucketSelectionIndex(buckets, bucketMs, dataX));
23324
+ }
23325
+ }), [
23326
+ buckets,
23327
+ bucketMs,
23328
+ range,
23329
+ layers,
23330
+ unit,
23331
+ displayPreference
23332
+ ]);
22658
23333
  const live = (0, react$1.useRef)({
22659
23334
  buckets,
22660
23335
  tops,
@@ -22817,7 +23492,10 @@ function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, forma
22817
23492
  optionsKey,
22818
23493
  buildOptions,
22819
23494
  data,
22820
- height: plotHeight
23495
+ height: plotHeight,
23496
+ tooltip,
23497
+ kind: "bars",
23498
+ label: title
22821
23499
  });
22822
23500
  if (compact) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22823
23501
  className: cn("relative min-w-0", className),
@@ -66242,10 +66920,6 @@ function RetrainReviewBadge({ kind, approximate = false, className, ...props })
66242
66920
  }
66243
66921
  //#endregion
66244
66922
  //#region src/composites/charts/time-series-model.ts
66245
- /**
66246
- * The pure half of `TimeSeriesChart`: which series go on which value axis,
66247
- * the aligned data uPlot draws, and what the readout says at an index.
66248
- */
66249
66923
  /** The unit key a series lives on (`''` for no / unknown unit). */
66250
66924
  function unitKey(unit) {
66251
66925
  return unit ?? "";
@@ -66313,7 +66987,39 @@ function latestValues(model) {
66313
66987
  * for the cross-device History page, where the reader chose the series.
66314
66988
  */
66315
66989
  var DEFAULT_HEIGHT = 120;
66316
- function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacked = false, area = false, syncKey, formatValue, axisFor, emptyMessage, pending = false, actions, footer, windowToggle, className }) {
66990
+ function seriesSpan(range, xs) {
66991
+ if (range !== void 0) return range.to - range.from;
66992
+ if (xs.length < 2) return 0;
66993
+ return (xs[xs.length - 1] ?? 0) - (xs[0] ?? 0);
66994
+ }
66995
+ function seriesTooltip(input) {
66996
+ const span = seriesSpan(input.range, input.model.xs);
66997
+ return {
66998
+ xMin: input.range?.from ?? input.model.xs[0] ?? 0,
66999
+ xMax: input.range?.to ?? input.model.xs.at(-1) ?? 0,
67000
+ stops: input.model.xs,
67001
+ resolve: (dataX) => {
67002
+ const index = nearestPointIndex(input.model.xs, dataX);
67003
+ if (index === null) return null;
67004
+ const at = input.model.xs[index];
67005
+ if (at === void 0) return null;
67006
+ const values = readoutAt(input.model, index);
67007
+ return lineTooltipBody({
67008
+ timeLabel: formatCursorTime(at, span),
67009
+ preference: input.preference,
67010
+ series: input.series.map((sample, i) => ({
67011
+ id: sample.id,
67012
+ label: sample.label,
67013
+ value: values[i] ?? null,
67014
+ quantity: sample.quantity,
67015
+ unit: sample.unit ?? null
67016
+ }))
67017
+ });
67018
+ },
67019
+ onDataX: input.onDataX
67020
+ };
67021
+ }
67022
+ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacked = false, area = false, syncKey, formatValue, displayPreference, axisFor, emptyMessage, pending = false, actions, footer, windowToggle, className }) {
66317
67023
  const t = useUiText();
66318
67024
  const model = (0, react$1.useMemo)(() => buildTimeSeriesModel(series, stacked), [series, stacked]);
66319
67025
  const [cursorIdx, setCursorIdx] = (0, react$1.useState)(null);
@@ -66456,6 +67162,20 @@ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacke
66456
67162
  fmt,
66457
67163
  specFor
66458
67164
  ]);
67165
+ const tooltip = (0, react$1.useMemo)(() => seriesTooltip({
67166
+ model,
67167
+ series,
67168
+ range,
67169
+ preference: displayPreference,
67170
+ onDataX: (dataX) => {
67171
+ setCursorIdx(dataX === null ? null : nearestPointIndex(model.xs, dataX));
67172
+ }
67173
+ }), [
67174
+ model,
67175
+ series,
67176
+ range,
67177
+ displayPreference
67178
+ ]);
66459
67179
  const shown = readoutAt(model, cursorIdx);
66460
67180
  const legend = series.map((s, i) => {
66461
67181
  const style = chartSeriesStyle(s.colorIndex);
@@ -66517,7 +67237,10 @@ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacke
66517
67237
  optionsKey,
66518
67238
  buildOptions,
66519
67239
  data,
66520
- height
67240
+ height,
67241
+ tooltip,
67242
+ kind: "lines",
67243
+ label: title
66521
67244
  })
66522
67245
  });
66523
67246
  }
@@ -66536,6 +67259,33 @@ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacke
66536
67259
  /** Pixel height of one row band — matches the `h-5` label cells. */
66537
67260
  var ROW_PX = 20;
66538
67261
  var AXIS_PX = 22;
67262
+ function stateStops(rows, from, to) {
67263
+ const found = new Set([from]);
67264
+ for (const row of rows) for (const segment of row.segments) if (segment.from >= from && segment.from < to) found.add(segment.from);
67265
+ return [...found].sort((a, b) => a - b);
67266
+ }
67267
+ function stateTooltip(rows, range) {
67268
+ const span = range.to - range.from;
67269
+ return {
67270
+ xMin: range.from,
67271
+ xMax: range.to,
67272
+ stops: stateStops(rows, range.from, range.to),
67273
+ resolve: (dataX) => stateTooltipBody({
67274
+ timeLabel: formatCursorTime(dataX, span),
67275
+ rows: rows.map((row) => {
67276
+ const hit = stateSpanAt(row.segments, dataX);
67277
+ const state = hit?.state ?? null;
67278
+ const stateLabel = state === null ? null : row.stateLabel?.(state) ?? state;
67279
+ return {
67280
+ id: row.id,
67281
+ label: row.label,
67282
+ stateLabel,
67283
+ spanMs: state === null ? null : hit?.spanMs ?? null
67284
+ };
67285
+ })
67286
+ })
67287
+ };
67288
+ }
66539
67289
  /** The colour role of a state on a row: a categorical slot, or the recessive "off". */
66540
67290
  function stateStyle(row, state) {
66541
67291
  const base = row.colorIndex ?? 0;
@@ -66610,6 +67360,10 @@ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMess
66610
67360
  }]
66611
67361
  }
66612
67362
  }), [optionsKey]);
67363
+ const tooltip = (0, react$1.useMemo)(() => ({
67364
+ ...stateTooltip(rows, range),
67365
+ onDataX: (dataX) => setCursorT(dataX)
67366
+ }), [rows, range]);
66613
67367
  const durations = (0, react$1.useMemo)(() => rows.map((r) => r.totals ?? timeInState(r.segments)), [rows]);
66614
67368
  const summary = rows.map((row, i) => {
66615
67369
  const parts = row.states.map((s) => {
@@ -66663,7 +67417,10 @@ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMess
66663
67417
  optionsKey,
66664
67418
  buildOptions,
66665
67419
  data,
66666
- height: rows.length * ROW_PX + AXIS_PX
67420
+ height: rows.length * ROW_PX + AXIS_PX,
67421
+ tooltip,
67422
+ kind: "states",
67423
+ label: title
66667
67424
  })
66668
67425
  })]
66669
67426
  }),
@@ -66708,7 +67465,38 @@ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMess
66708
67465
  * row (a training job's mAP per epoch). The x is whatever the caller orders
66709
67466
  * by (an epoch, a time); the value next to it names what the line is.
66710
67467
  */
66711
- function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, widthClassName = "w-40", zeroBased = true, className }) {
67468
+ function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, widthClassName = "w-40", zeroBased = true, unit = null, quantity, displayPreference, className }) {
67469
+ const tooltip = (0, react$1.useMemo)(() => {
67470
+ const xs = points.map((point) => point.x);
67471
+ return {
67472
+ xMin: xs[0] ?? 0,
67473
+ xMax: xs.at(-1) ?? 0,
67474
+ stops: xs,
67475
+ resolve: (dataX) => {
67476
+ const index = nearestPointIndex(xs, dataX);
67477
+ if (index === null) return null;
67478
+ const point = points[index];
67479
+ if (point === void 0) return null;
67480
+ return lineTooltipBody({
67481
+ timeLabel: String(point.x),
67482
+ preference: displayPreference,
67483
+ series: [{
67484
+ id: "value",
67485
+ label: ariaLabel,
67486
+ value: point.v,
67487
+ unit,
67488
+ quantity
67489
+ }]
67490
+ });
67491
+ }
67492
+ };
67493
+ }, [
67494
+ points,
67495
+ ariaLabel,
67496
+ unit,
67497
+ quantity,
67498
+ displayPreference
67499
+ ]);
66712
67500
  const data = (0, react$1.useMemo)(() => [points.map((p) => p.x), points.map((p) => p.v)], [points]);
66713
67501
  const buildOptions = (0, react$1.useCallback)((_w, lib) => ({
66714
67502
  legend: { show: false },
@@ -66744,7 +67532,10 @@ function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, w
66744
67532
  buildOptions,
66745
67533
  data,
66746
67534
  height,
66747
- inline: true
67535
+ inline: true,
67536
+ tooltip,
67537
+ kind: "spark",
67538
+ label: ariaLabel
66748
67539
  })
66749
67540
  });
66750
67541
  }