@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.js CHANGED
@@ -5,7 +5,7 @@ import * as __mfHostReact from "react";
5
5
  import React, { Fragment, Suspense, cloneElement, createContext, createElement, forwardRef, lazy, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
6
6
  import * as __mfHostReactJsxRuntime from "react/jsx-runtime";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
- import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, COMPOSER_ADDON_ID, CONTAINER_CHILD_PRIORITY, ConsumableLevelSchema, DEFAULT_DISPLAY_PREFERENCE, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEVICE_HISTORY_ENERGY_KEY, DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, HISTORY_CATALOG, ICON_CATEGORIES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, NODE_COLOR_IDS, NodeColorIdSchema, PALETTE_ENTRY_IDS, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, REDACTED_SECRET, RecordingConfigSchema, RecordingStatusSchema, SECRET_CLEAR, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, assignNodeColorId, assignNodeColorId as assignNodeColorId$1, baseUnitOf, buildModelVariantGroups, buildSubjectPath, bundledCamstack, canConvertUnit, canonicalIconKey, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, collectModelBoundKeys, consumableFormOf, consumableKindOf, createDeviceProxy, createLazyTrpcSource, createSliceHandle, dehydrateSchema, deriveRecordingMode, describeModelVariant, displayUnitFor, effectiveStepSettings, findTimezone, formatParkedFor, formatReading, hasModelBoundOverrides, iconCategoryLabel, iconsInCategory, isDrawableSubjectPath, isKeychainStoredField, isModelScopedStepSettingKey, isReadingQuantity, isRootSuppliableStepId, isSecretClear, isSecretFieldView, isSecretMarker, modelBindingKey, normalizeUnit, paintHex, parseOccupancyScope, projectSubjectPath, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveIconKey, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchIcons, selectAssignedProfileSlots, stepSettingSource, subjectPathSampleFromBox, subjectPathSvgPoints, tryConvertUnit, unitsForDimension, withFieldFrom, withoutField, writeModelBoundSetting } from "@camstack/types";
8
+ import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, COMPOSER_ADDON_ID, CONTAINER_CHILD_PRIORITY, ConsumableLevelSchema, DEFAULT_DISPLAY_PREFERENCE, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEVICE_HISTORY_ENERGY_KEY, DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, HISTORY_CATALOG, ICON_CATEGORIES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, NODE_COLOR_IDS, NodeColorIdSchema, PALETTE_ENTRY_IDS, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, QUANTITY_BASE, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, REDACTED_SECRET, RecordingConfigSchema, RecordingStatusSchema, SECRET_CLEAR, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, assignNodeColorId, assignNodeColorId as assignNodeColorId$1, baseUnitOf, buildModelVariantGroups, buildSubjectPath, bundledCamstack, canConvertUnit, canonicalIconKey, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, collectModelBoundKeys, consumableFormOf, consumableKindOf, createDeviceProxy, createLazyTrpcSource, createSliceHandle, dehydrateSchema, deriveRecordingMode, describeModelVariant, displayUnitFor, effectiveStepSettings, findTimezone, formatParkedFor, formatReading, hasModelBoundOverrides, iconCategoryLabel, iconsInCategory, isDrawableSubjectPath, isKeychainStoredField, isModelScopedStepSettingKey, isReadingQuantity, isRootSuppliableStepId, isSecretClear, isSecretFieldView, isSecretMarker, modelBindingKey, normalizeUnit, paintHex, parseOccupancyScope, projectSubjectPath, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveIconKey, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchIcons, selectAssignedProfileSlots, stepSettingSource, subjectPathSampleFromBox, subjectPathSvgPoints, tryConvertUnit, unitsForDimension, withFieldFrom, withoutField, writeModelBoundSetting } from "@camstack/types";
9
9
  import * as __mfHostTrpcReactQuery from "@trpc/react-query";
10
10
  import { createTRPCReact } from "@trpc/react-query";
11
11
  import * as __mfHostReactDOM from "react-dom";
@@ -22494,6 +22494,641 @@ function bucketIndexAt(buckets, bucketMs, t) {
22494
22494
  }
22495
22495
  return null;
22496
22496
  }
22497
+ var UNKNOWN$1 = "—";
22498
+ var NO_INFERENCE = new Set([
22499
+ "information",
22500
+ "duration",
22501
+ "duration-ms",
22502
+ "none",
22503
+ "device",
22504
+ "count",
22505
+ "index",
22506
+ "ratio",
22507
+ "humidity",
22508
+ "percentage"
22509
+ ]);
22510
+ var HIDDEN_INTERACTION = {
22511
+ selection: {
22512
+ visible: false,
22513
+ dataX: null,
22514
+ anchorX: 0,
22515
+ anchorY: 0,
22516
+ pinned: false
22517
+ },
22518
+ gesture: null,
22519
+ lockHorizontalScroll: false
22520
+ };
22521
+ var TOOLTIP_SIDES = [
22522
+ {
22523
+ placed: "right",
22524
+ vertical: "above"
22525
+ },
22526
+ {
22527
+ placed: "left",
22528
+ vertical: "above"
22529
+ },
22530
+ {
22531
+ placed: "right",
22532
+ vertical: "below"
22533
+ },
22534
+ {
22535
+ placed: "left",
22536
+ vertical: "below"
22537
+ }
22538
+ ];
22539
+ /** Index of the sample closest to `x`. A tie keeps the earlier sample. */
22540
+ function nearestPointIndex(xs, x) {
22541
+ if (xs.length === 0 || !Number.isFinite(x)) return null;
22542
+ let lo = 0;
22543
+ let hi = xs.length - 1;
22544
+ while (lo < hi) {
22545
+ const mid = Math.floor((lo + hi) / 2);
22546
+ if ((xs[mid] ?? 0) < x) lo = mid + 1;
22547
+ else hi = mid;
22548
+ }
22549
+ const right = lo;
22550
+ const left = Math.max(0, right - 1);
22551
+ return Math.abs((xs[left] ?? 0) - x) <= Math.abs((xs[right] ?? 0) - x) ? left : right;
22552
+ }
22553
+ /** Data x for a pointer inside the plot. `null` when it is outside and clamping is off. */
22554
+ function dataXFromPointer(input) {
22555
+ if (!(input.plotWidth > 0) || !Number.isFinite(input.localX)) return null;
22556
+ const rel = input.localX - input.plotLeft;
22557
+ if (!input.clamp && (rel < 0 || rel > input.plotWidth)) return null;
22558
+ const clamped = Math.min(input.plotWidth, Math.max(0, rel));
22559
+ return input.xMin + clamped / input.plotWidth * (input.xMax - input.xMin);
22560
+ }
22561
+ /** Bucket that contains `dataX`, or the nearest one when `dataX` falls in a gap. */
22562
+ function bucketSelectionIndex(buckets, bucketMs, dataX) {
22563
+ if (buckets.length === 0 || !Number.isFinite(dataX)) return null;
22564
+ let best = 0;
22565
+ let bestDist = Number.POSITIVE_INFINITY;
22566
+ for (let i = 0; i < buckets.length; i++) {
22567
+ const start = buckets[i]?.t;
22568
+ if (start === void 0) continue;
22569
+ const end = start + bucketMs;
22570
+ if (dataX >= start && dataX < end) return i;
22571
+ const dist = dataX < start ? start - dataX : dataX - end;
22572
+ if (dist < bestDist) {
22573
+ bestDist = dist;
22574
+ best = i;
22575
+ }
22576
+ }
22577
+ return best;
22578
+ }
22579
+ /** Next keyboard stop. A null current starts at the first or last stop. */
22580
+ function stepStop(stops, current, direction) {
22581
+ if (stops.length === 0) return null;
22582
+ const ordered = [...stops].sort((a, b) => a - b);
22583
+ if (current === null || !Number.isFinite(current)) return direction > 0 ? ordered[0] ?? null : ordered[ordered.length - 1] ?? null;
22584
+ let idx = 0;
22585
+ let best = Number.POSITIVE_INFINITY;
22586
+ for (let i = 0; i < ordered.length; i++) {
22587
+ const dist = Math.abs((ordered[i] ?? 0) - current);
22588
+ if (dist < best) {
22589
+ best = dist;
22590
+ idx = i;
22591
+ }
22592
+ }
22593
+ const next = idx + direction;
22594
+ if (next < 0 || next >= ordered.length) return ordered[idx] ?? null;
22595
+ return ordered[next] ?? null;
22596
+ }
22597
+ function shown(dataX, anchorX, anchorY, pinned) {
22598
+ return {
22599
+ visible: true,
22600
+ dataX,
22601
+ anchorX,
22602
+ anchorY,
22603
+ pinned
22604
+ };
22605
+ }
22606
+ function resting(selection, gesture, lock) {
22607
+ return {
22608
+ selection,
22609
+ gesture,
22610
+ lockHorizontalScroll: lock
22611
+ };
22612
+ }
22613
+ function lockAxis(gesture, x, y) {
22614
+ if (gesture.axis !== "pending") return gesture.axis;
22615
+ const dx = Math.abs(x - gesture.originX);
22616
+ const dy = Math.abs(y - gesture.originY);
22617
+ if (dx < 10 && dy < 10) return "pending";
22618
+ return dx >= dy ? "x" : "y";
22619
+ }
22620
+ function onDown(state, event) {
22621
+ if (event.kind === "mouse") return state;
22622
+ return resting(state.selection, {
22623
+ kind: event.kind,
22624
+ originX: event.x,
22625
+ originY: event.y,
22626
+ axis: "pending"
22627
+ }, false);
22628
+ }
22629
+ function onMove(state, event) {
22630
+ if (event.kind === "mouse") {
22631
+ if (event.dataX === null) return state.selection.pinned ? state : HIDDEN_INTERACTION;
22632
+ return resting(shown(event.dataX, event.x, event.y, false), null, false);
22633
+ }
22634
+ const gesture = state.gesture;
22635
+ if (gesture === null) return state;
22636
+ const axis = lockAxis(gesture, event.x, event.y);
22637
+ const nextGesture = {
22638
+ ...gesture,
22639
+ axis
22640
+ };
22641
+ if (axis !== "x" || event.dataX === null) return resting(state.selection, nextGesture, false);
22642
+ return resting(shown(event.dataX, event.x, event.y, true), nextGesture, true);
22643
+ }
22644
+ function onUp(state, event) {
22645
+ if (event.kind === "mouse" || state.gesture === null) return state;
22646
+ if (state.gesture.axis === "y" || state.gesture.axis === "x") return resting(state.selection, null, false);
22647
+ if (state.selection.visible && state.selection.pinned) return HIDDEN_INTERACTION;
22648
+ if (event.dataX === null) return HIDDEN_INTERACTION;
22649
+ return resting(shown(event.dataX, event.x, event.y, true), null, false);
22650
+ }
22651
+ /** Pointer, keyboard and dismiss rules for one chart. */
22652
+ function reduceChartInteraction(state, event) {
22653
+ switch (event.type) {
22654
+ case "hide":
22655
+ case "outside": return HIDDEN_INTERACTION;
22656
+ case "leave": return state.selection.pinned ? resting(state.selection, null, false) : HIDDEN_INTERACTION;
22657
+ case "hover": return resting(shown(event.dataX, event.anchorX, event.anchorY, false), null, false);
22658
+ case "key": return resting(shown(event.dataX, event.anchorX, event.anchorY, true), null, false);
22659
+ case "down": return onDown(state, event);
22660
+ case "move": return onMove(state, event);
22661
+ case "up": return onUp(state, event);
22662
+ default: return event;
22663
+ }
22664
+ }
22665
+ function quantityFromUnit(unit) {
22666
+ if (unit === null || unit === void 0 || unit.length === 0) return void 0;
22667
+ let found;
22668
+ for (const key of Object.keys(QUANTITY_BASE)) {
22669
+ if (!isReadingQuantity(key) || NO_INFERENCE.has(key)) continue;
22670
+ if (QUANTITY_BASE[key] !== unit) continue;
22671
+ if (found !== void 0) return void 0;
22672
+ found = key;
22673
+ }
22674
+ return found;
22675
+ }
22676
+ function withUnit(text, unit, unknown) {
22677
+ if (text.length === 0) return unknown;
22678
+ if (unit.length === 0) return text;
22679
+ if (unit === "%") return `${text}%`;
22680
+ return `${text} ${unit}`;
22681
+ }
22682
+ /** One sample, with its unit. Missing samples are an em dash, never zero. */
22683
+ function formatTooltipValue(input) {
22684
+ const unknown = input.unknownLabel ?? UNKNOWN$1;
22685
+ if (input.value === null || !Number.isFinite(input.value)) return unknown;
22686
+ const quantity = input.quantity === "device" ? void 0 : input.quantity ?? quantityFromUnit(input.unit);
22687
+ if (quantity === void 0) return formatChartValue(input.value, input.unit ?? null);
22688
+ const rendered = formatReading({
22689
+ value: input.value,
22690
+ quantity,
22691
+ preference: input.preference,
22692
+ precision: input.precision
22693
+ });
22694
+ return withUnit(rendered.text, rendered.unit, unknown);
22695
+ }
22696
+ function liveText(heading, lines) {
22697
+ if (lines.length === 0) return heading;
22698
+ return `${heading}, ${lines.map((line) => `${line.label} ${line.value}`).join(", ")}`;
22699
+ }
22700
+ function asBody(heading, lines) {
22701
+ return {
22702
+ heading,
22703
+ lines,
22704
+ liveText: liveText(heading, lines)
22705
+ };
22706
+ }
22707
+ /** Time plus one line per visible series. */
22708
+ function lineTooltipBody(input) {
22709
+ const lines = input.series.filter((sample) => sample.visible !== false).map((sample) => ({
22710
+ id: sample.id,
22711
+ label: sample.label,
22712
+ value: formatTooltipValue({
22713
+ value: sample.value,
22714
+ quantity: sample.quantity,
22715
+ unit: sample.unit,
22716
+ precision: sample.precision,
22717
+ preference: input.preference
22718
+ })
22719
+ }));
22720
+ return asBody(input.timeLabel, lines);
22721
+ }
22722
+ /** The state in force at `t`, and how long that span lasts. */
22723
+ function stateSpanAt(segments, t) {
22724
+ for (const segment of segments) if (t >= segment.from && t < segment.to) return {
22725
+ state: segment.state,
22726
+ spanMs: Math.max(0, segment.to - segment.from)
22727
+ };
22728
+ return null;
22729
+ }
22730
+ function stateTooltipValue(stateLabel, spanMs) {
22731
+ if (stateLabel === null || stateLabel.length === 0) return UNKNOWN$1;
22732
+ if (spanMs === null || !(spanMs > 0)) return stateLabel;
22733
+ return `${stateLabel} · ${formatSpan(spanMs)}`;
22734
+ }
22735
+ /** One row per state series: the label and the span under the pointer. */
22736
+ function stateTooltipBody(input) {
22737
+ const lines = input.rows.map((row) => ({
22738
+ id: row.id,
22739
+ label: row.label,
22740
+ value: stateTooltipValue(row.stateLabel, row.spanMs)
22741
+ }));
22742
+ return asBody(input.timeLabel, lines);
22743
+ }
22744
+ /** `start – end` for one bar bucket, at the window's time precision. */
22745
+ function barRangeLabel(start, bucketMs, spanMs) {
22746
+ return `${formatCursorTime(start, spanMs)} – ${formatCursorTime(start + bucketMs, spanMs)}`;
22747
+ }
22748
+ /** The bucket's range, and each layer's value with its unit. */
22749
+ function barTooltipBody(input) {
22750
+ const lines = input.layers.map((layer) => ({
22751
+ id: layer.id,
22752
+ label: layer.label,
22753
+ value: formatTooltipValue({
22754
+ value: layer.value,
22755
+ quantity: layer.quantity,
22756
+ unit: layer.unit,
22757
+ precision: layer.precision,
22758
+ preference: input.preference
22759
+ })
22760
+ }));
22761
+ return asBody(input.rangeLabel, lines);
22762
+ }
22763
+ function sideOrigin(side, input, gap) {
22764
+ return {
22765
+ left: side.placed === "right" ? input.anchorX + gap : input.anchorX - gap - input.tooltipWidth,
22766
+ top: side.vertical === "above" ? input.anchorY - gap - input.tooltipHeight : input.anchorY + gap
22767
+ };
22768
+ }
22769
+ function insideBounds(left, top, input) {
22770
+ const pad = 4;
22771
+ return left >= pad && top >= pad && left + input.tooltipWidth <= input.boundsWidth - pad && top + input.tooltipHeight <= input.boundsHeight - pad;
22772
+ }
22773
+ function coversPoint(left, top, input) {
22774
+ return input.anchorX >= left && input.anchorX <= left + input.tooltipWidth && input.anchorY >= top && input.anchorY <= top + input.tooltipHeight;
22775
+ }
22776
+ function clampOrigin(left, top, input) {
22777
+ const pad = 4;
22778
+ const maxLeft = Math.max(pad, input.boundsWidth - pad - input.tooltipWidth);
22779
+ const maxTop = Math.max(pad, input.boundsHeight - pad - input.tooltipHeight);
22780
+ return {
22781
+ left: Math.min(Math.max(left, pad), maxLeft),
22782
+ top: Math.min(Math.max(top, pad), maxTop)
22783
+ };
22784
+ }
22785
+ /**
22786
+ * Place the tooltip beside the point, inside the chart. The first side that
22787
+ * fits and does not cover the point wins; the edges flip before the corners.
22788
+ */
22789
+ function placeChartTooltip(input) {
22790
+ const gap = input.gap ?? 12;
22791
+ let fallback = null;
22792
+ for (const side of TOOLTIP_SIDES) {
22793
+ const origin = sideOrigin(side, input, gap);
22794
+ const clamped = clampOrigin(origin.left, origin.top, input);
22795
+ const placement = {
22796
+ ...side,
22797
+ ...clamped
22798
+ };
22799
+ if (fallback === null && !coversPoint(clamped.left, clamped.top, input)) fallback = placement;
22800
+ if (insideBounds(origin.left, origin.top, input) && !coversPoint(origin.left, origin.top, input)) return {
22801
+ ...side,
22802
+ ...origin
22803
+ };
22804
+ }
22805
+ if (fallback !== null) return fallback;
22806
+ const first = TOOLTIP_SIDES[0] ?? {
22807
+ placed: "right",
22808
+ vertical: "above"
22809
+ };
22810
+ const clamped = clampOrigin(sideOrigin(first, input, gap).left, sideOrigin(first, input, gap).top, input);
22811
+ return {
22812
+ ...first,
22813
+ ...clamped
22814
+ };
22815
+ }
22816
+ //#endregion
22817
+ //#region src/composites/charts/chart-tooltip-host.tsx
22818
+ /**
22819
+ * One tooltip for every chart. The plot stays in `UPlotHost`; this shell
22820
+ * tracks the pointer, a touch scrub and the arrow keys, and draws the
22821
+ * readout inside the chart.
22822
+ */
22823
+ function pointerKind(pointerType) {
22824
+ return pointerType === "mouse" ? "mouse" : "touch";
22825
+ }
22826
+ function pointerSample(clientX, clientY, shell, plot, spec, clamp) {
22827
+ const box = shell.getBoundingClientRect();
22828
+ const anchorX = clientX - box.left;
22829
+ const anchorY = clientY - box.top;
22830
+ if (plot !== null) {
22831
+ const over = plot.over.getBoundingClientRect();
22832
+ const left = clientX - over.left;
22833
+ const top = clientY - over.top;
22834
+ const minX = plot.bbox.left;
22835
+ const maxX = plot.bbox.left + plot.bbox.width;
22836
+ const inside = left >= minX && left <= maxX && top >= plot.bbox.top && top <= plot.bbox.top + plot.bbox.height;
22837
+ if (!inside && !clamp) return {
22838
+ dataX: null,
22839
+ anchorX,
22840
+ anchorY,
22841
+ inside: false
22842
+ };
22843
+ return {
22844
+ dataX: plot.posToVal(Math.min(maxX, Math.max(minX, left)), "x"),
22845
+ anchorX,
22846
+ anchorY,
22847
+ inside
22848
+ };
22849
+ }
22850
+ const dataX = dataXFromPointer({
22851
+ localX: anchorX,
22852
+ plotLeft: 0,
22853
+ plotWidth: box.width,
22854
+ xMin: spec.xMin,
22855
+ xMax: spec.xMax,
22856
+ clamp
22857
+ });
22858
+ return {
22859
+ dataX,
22860
+ anchorX,
22861
+ anchorY,
22862
+ inside: dataX !== null
22863
+ };
22864
+ }
22865
+ function anchorForDataX(dataX, shell, plot, spec) {
22866
+ if (plot !== null && shell !== null) {
22867
+ const over = plot.over.getBoundingClientRect();
22868
+ const box = shell.getBoundingClientRect();
22869
+ return {
22870
+ x: over.left - box.left + plot.valToPos(dataX, "x"),
22871
+ y: over.top - box.top + plot.bbox.top + plot.bbox.height / 2
22872
+ };
22873
+ }
22874
+ const width = shell?.getBoundingClientRect().width ?? 0;
22875
+ const height = shell?.getBoundingClientRect().height ?? 0;
22876
+ const span = spec.xMax - spec.xMin;
22877
+ const frac = span === 0 ? 0 : (dataX - spec.xMin) / span;
22878
+ return {
22879
+ x: Math.min(1, Math.max(0, frac)) * width,
22880
+ y: height / 2
22881
+ };
22882
+ }
22883
+ function movePlotCursor(plot, dataX) {
22884
+ if (plot === null) return;
22885
+ if (dataX === null) {
22886
+ plot.setCursor({
22887
+ left: -10,
22888
+ top: -10
22889
+ });
22890
+ return;
22891
+ }
22892
+ plot.setCursor({
22893
+ left: plot.valToPos(dataX, "x"),
22894
+ top: plot.bbox.top + plot.bbox.height / 2
22895
+ });
22896
+ }
22897
+ function ChartTooltipCard({ view }) {
22898
+ const ref = useRef(null);
22899
+ useLayoutEffect(() => {
22900
+ const el = ref.current;
22901
+ if (el === null) return;
22902
+ const placed = placeChartTooltip({
22903
+ anchorX: view.anchorX,
22904
+ anchorY: view.anchorY,
22905
+ tooltipWidth: el.offsetWidth,
22906
+ tooltipHeight: el.offsetHeight,
22907
+ boundsWidth: view.boundsWidth,
22908
+ boundsHeight: view.boundsHeight
22909
+ });
22910
+ el.style.left = `${String(placed.left)}px`;
22911
+ el.style.top = `${String(placed.top)}px`;
22912
+ }, [view]);
22913
+ return /* @__PURE__ */ jsxs("div", {
22914
+ ref,
22915
+ role: "tooltip",
22916
+ "data-chart-tooltip": "",
22917
+ 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",
22918
+ children: [/* @__PURE__ */ jsx("p", {
22919
+ className: "m-0 text-[10px] tabular-nums text-foreground-subtle",
22920
+ children: view.body.heading
22921
+ }), /* @__PURE__ */ jsx("ul", {
22922
+ className: "m-0 list-none p-0",
22923
+ children: view.body.lines.map((line) => /* @__PURE__ */ jsxs("li", {
22924
+ className: "flex items-baseline justify-between gap-3 text-xs",
22925
+ children: [/* @__PURE__ */ jsx("span", {
22926
+ className: "min-w-0 truncate text-foreground-muted",
22927
+ children: line.label
22928
+ }), /* @__PURE__ */ jsx("span", {
22929
+ className: "shrink-0 tabular-nums",
22930
+ children: line.value
22931
+ })]
22932
+ }, line.id))
22933
+ })]
22934
+ });
22935
+ }
22936
+ function ChartTooltipShell({ spec, plotRef, kind, label, inline = false, children }) {
22937
+ const shellRef = useRef(null);
22938
+ const specRef = useRef(spec);
22939
+ specRef.current = spec;
22940
+ const interactionRef = useRef(HIDDEN_INTERACTION);
22941
+ const dataXRef = useRef(null);
22942
+ const [view, setView] = useState(null);
22943
+ const pinned = view !== null && interactionRef.current.selection.pinned;
22944
+ const commit = useCallback((next) => {
22945
+ interactionRef.current = next;
22946
+ const current = specRef.current;
22947
+ const shell = shellRef.current;
22948
+ const dataX = next.selection.visible ? next.selection.dataX : null;
22949
+ const body = current !== null && dataX !== null ? current.resolve(dataX) : null;
22950
+ if (body === null || dataX === null) {
22951
+ setView(null);
22952
+ if (dataXRef.current !== null) {
22953
+ dataXRef.current = null;
22954
+ current?.onDataX?.(null);
22955
+ }
22956
+ return;
22957
+ }
22958
+ setView({
22959
+ body,
22960
+ anchorX: next.selection.anchorX,
22961
+ anchorY: next.selection.anchorY,
22962
+ boundsWidth: shell?.getBoundingClientRect().width ?? 0,
22963
+ boundsHeight: shell?.getBoundingClientRect().height ?? 0
22964
+ });
22965
+ if (dataXRef.current !== dataX) {
22966
+ dataXRef.current = dataX;
22967
+ current?.onDataX?.(dataX);
22968
+ }
22969
+ }, []);
22970
+ useEffect(() => {
22971
+ const shell = shellRef.current;
22972
+ if (shell === null) return;
22973
+ const sample = (event, clamp) => {
22974
+ const current = specRef.current;
22975
+ if (current === null) return null;
22976
+ return pointerSample(event.clientX, event.clientY, shell, plotRef.current, current, clamp);
22977
+ };
22978
+ const onDown = (event) => {
22979
+ if (pointerKind(event.pointerType) === "mouse") return;
22980
+ const at = sample(event, false);
22981
+ if (at === null) return;
22982
+ commit(reduceChartInteraction(interactionRef.current, {
22983
+ type: "down",
22984
+ kind: "touch",
22985
+ x: at.anchorX,
22986
+ y: at.anchorY,
22987
+ dataX: at.dataX
22988
+ }));
22989
+ };
22990
+ const onMove = (event) => {
22991
+ const kind = pointerKind(event.pointerType);
22992
+ const scrubbing = interactionRef.current.gesture?.axis === "x";
22993
+ const at = sample(event, kind !== "mouse" && scrubbing);
22994
+ if (at === null) return;
22995
+ if (kind === "mouse") {
22996
+ if (!at.inside || at.dataX === null) {
22997
+ commit(reduceChartInteraction(interactionRef.current, { type: "leave" }));
22998
+ if (!interactionRef.current.selection.visible) movePlotCursor(plotRef.current, null);
22999
+ return;
23000
+ }
23001
+ commit(reduceChartInteraction(interactionRef.current, {
23002
+ type: "hover",
23003
+ dataX: at.dataX,
23004
+ anchorX: at.anchorX,
23005
+ anchorY: at.anchorY
23006
+ }));
23007
+ movePlotCursor(plotRef.current, at.dataX);
23008
+ return;
23009
+ }
23010
+ const next = reduceChartInteraction(interactionRef.current, {
23011
+ type: "move",
23012
+ kind: "touch",
23013
+ x: at.anchorX,
23014
+ y: at.anchorY,
23015
+ dataX: at.dataX
23016
+ });
23017
+ if (next.lockHorizontalScroll) {
23018
+ event.preventDefault();
23019
+ if (event.pointerId !== 0 && !shell.hasPointerCapture(event.pointerId)) shell.setPointerCapture(event.pointerId);
23020
+ }
23021
+ commit(next);
23022
+ if (next.selection.dataX !== null && next.selection.visible) movePlotCursor(plotRef.current, next.selection.dataX);
23023
+ };
23024
+ const onUp = (event) => {
23025
+ if (pointerKind(event.pointerType) === "mouse") return;
23026
+ const at = sample(event, interactionRef.current.gesture?.axis === "x");
23027
+ if (at === null) return;
23028
+ const next = reduceChartInteraction(interactionRef.current, {
23029
+ type: "up",
23030
+ kind: "touch",
23031
+ x: at.anchorX,
23032
+ y: at.anchorY,
23033
+ dataX: at.dataX
23034
+ });
23035
+ commit(next);
23036
+ movePlotCursor(plotRef.current, next.selection.visible ? next.selection.dataX : null);
23037
+ };
23038
+ const onLeave = (event) => {
23039
+ if (pointerKind(event.pointerType) !== "mouse") return;
23040
+ const next = reduceChartInteraction(interactionRef.current, { type: "leave" });
23041
+ commit(next);
23042
+ if (!next.selection.visible) movePlotCursor(plotRef.current, null);
23043
+ };
23044
+ shell.addEventListener("pointerdown", onDown);
23045
+ shell.addEventListener("pointermove", onMove);
23046
+ shell.addEventListener("pointerup", onUp);
23047
+ shell.addEventListener("pointercancel", onUp);
23048
+ shell.addEventListener("pointerleave", onLeave);
23049
+ return () => {
23050
+ shell.removeEventListener("pointerdown", onDown);
23051
+ shell.removeEventListener("pointermove", onMove);
23052
+ shell.removeEventListener("pointerup", onUp);
23053
+ shell.removeEventListener("pointercancel", onUp);
23054
+ shell.removeEventListener("pointerleave", onLeave);
23055
+ };
23056
+ }, [commit, plotRef]);
23057
+ useEffect(() => {
23058
+ if (!pinned) return;
23059
+ const onDoc = (event) => {
23060
+ const shell = shellRef.current;
23061
+ if (shell !== null && event.target instanceof Node && shell.contains(event.target)) return;
23062
+ commit(reduceChartInteraction(interactionRef.current, { type: "outside" }));
23063
+ movePlotCursor(plotRef.current, null);
23064
+ };
23065
+ document.addEventListener("pointerdown", onDoc, true);
23066
+ return () => document.removeEventListener("pointerdown", onDoc, true);
23067
+ }, [
23068
+ commit,
23069
+ pinned,
23070
+ plotRef
23071
+ ]);
23072
+ const onKeyDown = (event) => {
23073
+ const current = specRef.current;
23074
+ if (current === null) return;
23075
+ if (event.key === "Escape") {
23076
+ event.preventDefault();
23077
+ commit(reduceChartInteraction(interactionRef.current, { type: "hide" }));
23078
+ movePlotCursor(plotRef.current, null);
23079
+ return;
23080
+ }
23081
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
23082
+ event.preventDefault();
23083
+ const from = interactionRef.current.selection.visible ? interactionRef.current.selection.dataX : null;
23084
+ const dataX = stepStop(current.stops, from, event.key === "ArrowRight" ? 1 : -1);
23085
+ if (dataX === null) return;
23086
+ const anchor = anchorForDataX(dataX, shellRef.current, plotRef.current, current);
23087
+ commit(reduceChartInteraction(interactionRef.current, {
23088
+ type: "key",
23089
+ dataX,
23090
+ anchorX: anchor.x,
23091
+ anchorY: anchor.y
23092
+ }));
23093
+ movePlotCursor(plotRef.current, dataX);
23094
+ };
23095
+ const setShell = (el) => {
23096
+ shellRef.current = el;
23097
+ };
23098
+ 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");
23099
+ const tooltip = view !== null ? /* @__PURE__ */ jsx(ChartTooltipCard, { view }) : null;
23100
+ const live = view !== null && typeof document !== "undefined" ? createPortal(/* @__PURE__ */ jsx("div", {
23101
+ className: "sr-only",
23102
+ "aria-live": "polite",
23103
+ "data-chart-tooltip-live": "",
23104
+ children: view.body.liveText
23105
+ }), document.body) : null;
23106
+ const shared = {
23107
+ className,
23108
+ "data-chart-shell": "",
23109
+ "data-chart-kind": kind,
23110
+ tabIndex: spec === null ? void 0 : 0,
23111
+ "aria-label": label,
23112
+ onKeyDown
23113
+ };
23114
+ return inline ? /* @__PURE__ */ jsxs("span", {
23115
+ ref: setShell,
23116
+ ...shared,
23117
+ children: [
23118
+ children,
23119
+ tooltip,
23120
+ live
23121
+ ]
23122
+ }) : /* @__PURE__ */ jsxs("div", {
23123
+ ref: setShell,
23124
+ ...shared,
23125
+ children: [
23126
+ children,
23127
+ tooltip,
23128
+ live
23129
+ ]
23130
+ });
23131
+ }
22497
23132
  //#endregion
22498
23133
  //#region src/composites/charts/uplot-host.tsx
22499
23134
  /**
@@ -22524,7 +23159,7 @@ function loadUPlot() {
22524
23159
  if (ctorPromise === null) ctorPromise = canDrawCanvas() ? import("./uPlot.esm-C_8LiAPm.js").then((m) => m.default).catch(() => null) : Promise.resolve(null);
22525
23160
  return ctorPromise;
22526
23161
  }
22527
- function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, className, inline = false }) {
23162
+ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, className, inline = false, tooltip = null, kind, label }) {
22528
23163
  const hostRef = useRef(null);
22529
23164
  const plotRef = useRef(null);
22530
23165
  const [ctor, setCtor] = useState(null);
@@ -22556,6 +23191,8 @@ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, classNa
22556
23191
  width,
22557
23192
  height
22558
23193
  }, latest.current.data, el);
23194
+ u.over.style.touchAction = "pan-y";
23195
+ u.root.style.touchAction = "pan-y";
22559
23196
  plotRef.current = u;
22560
23197
  latest.current.onInstance?.(u);
22561
23198
  return () => {
@@ -22597,14 +23234,21 @@ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, classNa
22597
23234
  const setHost = (el) => {
22598
23235
  hostRef.current = el;
22599
23236
  };
22600
- return inline ? /* @__PURE__ */ jsx("span", {
22601
- ref: setHost,
22602
- className: className ?? "block w-full min-w-0",
22603
- "data-chart-plot": ""
22604
- }) : /* @__PURE__ */ jsx("div", {
22605
- ref: setHost,
22606
- className: className ?? "w-full min-w-0",
22607
- "data-chart-plot": ""
23237
+ return /* @__PURE__ */ jsx(ChartTooltipShell, {
23238
+ spec: tooltip,
23239
+ plotRef,
23240
+ kind,
23241
+ label,
23242
+ inline,
23243
+ children: inline ? /* @__PURE__ */ jsx("span", {
23244
+ ref: setHost,
23245
+ className: className ?? "block w-full min-w-0",
23246
+ "data-chart-plot": ""
23247
+ }) : /* @__PURE__ */ jsx("div", {
23248
+ ref: setHost,
23249
+ className: className ?? "w-full min-w-0",
23250
+ "data-chart-plot": ""
23251
+ })
22608
23252
  });
22609
23253
  }
22610
23254
  //#endregion
@@ -22619,7 +23263,7 @@ function UPlotHost({ optionsKey, buildOptions, data, height, onInstance, classNa
22619
23263
  * lives inside another control; there a bucket can be selected (`onSelect`)
22620
23264
  * and every bucket is also a real button for keyboard and screen readers.
22621
23265
  */
22622
- 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 }) {
23266
+ 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 }) {
22623
23267
  const t = useUiText();
22624
23268
  const plotHeight = height ?? (compact ? 24 : 100);
22625
23269
  const isStacked = stacked ?? layers.length > 1;
@@ -22631,6 +23275,37 @@ function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, forma
22631
23275
  ]);
22632
23276
  const totals = useMemo(() => bucketTotals(buckets, layers.length), [buckets, layers.length]);
22633
23277
  const [cursorIdx, setCursorIdx] = useState(null);
23278
+ const tooltip = useMemo(() => ({
23279
+ xMin: range.from,
23280
+ xMax: range.to,
23281
+ stops: buckets.map((bucket) => bucket.t),
23282
+ resolve: (dataX) => {
23283
+ const index = bucketSelectionIndex(buckets, bucketMs, dataX);
23284
+ if (index === null) return null;
23285
+ const bucket = buckets[index];
23286
+ if (bucket === void 0) return null;
23287
+ return barTooltipBody({
23288
+ rangeLabel: barRangeLabel(bucket.t, bucketMs, range.to - range.from),
23289
+ preference: displayPreference,
23290
+ layers: layers.map((layer, li) => ({
23291
+ id: layer.id,
23292
+ label: layer.label,
23293
+ value: bucket.values[li] ?? null,
23294
+ unit
23295
+ }))
23296
+ });
23297
+ },
23298
+ onDataX: (dataX) => {
23299
+ setCursorIdx(dataX === null ? null : bucketSelectionIndex(buckets, bucketMs, dataX));
23300
+ }
23301
+ }), [
23302
+ buckets,
23303
+ bucketMs,
23304
+ range,
23305
+ layers,
23306
+ unit,
23307
+ displayPreference
23308
+ ]);
22634
23309
  const live = useRef({
22635
23310
  buckets,
22636
23311
  tops,
@@ -22793,7 +23468,10 @@ function BarSeries({ title, layers, buckets, bucketMs, range, unit = null, forma
22793
23468
  optionsKey,
22794
23469
  buildOptions,
22795
23470
  data,
22796
- height: plotHeight
23471
+ height: plotHeight,
23472
+ tooltip,
23473
+ kind: "bars",
23474
+ label: title
22797
23475
  });
22798
23476
  if (compact) return /* @__PURE__ */ jsxs("div", {
22799
23477
  className: cn("relative min-w-0", className),
@@ -66218,10 +66896,6 @@ function RetrainReviewBadge({ kind, approximate = false, className, ...props })
66218
66896
  }
66219
66897
  //#endregion
66220
66898
  //#region src/composites/charts/time-series-model.ts
66221
- /**
66222
- * The pure half of `TimeSeriesChart`: which series go on which value axis,
66223
- * the aligned data uPlot draws, and what the readout says at an index.
66224
- */
66225
66899
  /** The unit key a series lives on (`''` for no / unknown unit). */
66226
66900
  function unitKey(unit) {
66227
66901
  return unit ?? "";
@@ -66289,7 +66963,39 @@ function latestValues(model) {
66289
66963
  * for the cross-device History page, where the reader chose the series.
66290
66964
  */
66291
66965
  var DEFAULT_HEIGHT = 120;
66292
- function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacked = false, area = false, syncKey, formatValue, axisFor, emptyMessage, pending = false, actions, footer, windowToggle, className }) {
66966
+ function seriesSpan(range, xs) {
66967
+ if (range !== void 0) return range.to - range.from;
66968
+ if (xs.length < 2) return 0;
66969
+ return (xs[xs.length - 1] ?? 0) - (xs[0] ?? 0);
66970
+ }
66971
+ function seriesTooltip(input) {
66972
+ const span = seriesSpan(input.range, input.model.xs);
66973
+ return {
66974
+ xMin: input.range?.from ?? input.model.xs[0] ?? 0,
66975
+ xMax: input.range?.to ?? input.model.xs.at(-1) ?? 0,
66976
+ stops: input.model.xs,
66977
+ resolve: (dataX) => {
66978
+ const index = nearestPointIndex(input.model.xs, dataX);
66979
+ if (index === null) return null;
66980
+ const at = input.model.xs[index];
66981
+ if (at === void 0) return null;
66982
+ const values = readoutAt(input.model, index);
66983
+ return lineTooltipBody({
66984
+ timeLabel: formatCursorTime(at, span),
66985
+ preference: input.preference,
66986
+ series: input.series.map((sample, i) => ({
66987
+ id: sample.id,
66988
+ label: sample.label,
66989
+ value: values[i] ?? null,
66990
+ quantity: sample.quantity,
66991
+ unit: sample.unit ?? null
66992
+ }))
66993
+ });
66994
+ },
66995
+ onDataX: input.onDataX
66996
+ };
66997
+ }
66998
+ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacked = false, area = false, syncKey, formatValue, displayPreference, axisFor, emptyMessage, pending = false, actions, footer, windowToggle, className }) {
66293
66999
  const t = useUiText();
66294
67000
  const model = useMemo(() => buildTimeSeriesModel(series, stacked), [series, stacked]);
66295
67001
  const [cursorIdx, setCursorIdx] = useState(null);
@@ -66432,6 +67138,20 @@ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacke
66432
67138
  fmt,
66433
67139
  specFor
66434
67140
  ]);
67141
+ const tooltip = useMemo(() => seriesTooltip({
67142
+ model,
67143
+ series,
67144
+ range,
67145
+ preference: displayPreference,
67146
+ onDataX: (dataX) => {
67147
+ setCursorIdx(dataX === null ? null : nearestPointIndex(model.xs, dataX));
67148
+ }
67149
+ }), [
67150
+ model,
67151
+ series,
67152
+ range,
67153
+ displayPreference
67154
+ ]);
66435
67155
  const shown = readoutAt(model, cursorIdx);
66436
67156
  const legend = series.map((s, i) => {
66437
67157
  const style = chartSeriesStyle(s.colorIndex);
@@ -66493,7 +67213,10 @@ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacke
66493
67213
  optionsKey,
66494
67214
  buildOptions,
66495
67215
  data,
66496
- height
67216
+ height,
67217
+ tooltip,
67218
+ kind: "lines",
67219
+ label: title
66497
67220
  })
66498
67221
  });
66499
67222
  }
@@ -66512,6 +67235,33 @@ function TimeSeriesChart({ title, series, range, height = DEFAULT_HEIGHT, stacke
66512
67235
  /** Pixel height of one row band — matches the `h-5` label cells. */
66513
67236
  var ROW_PX = 20;
66514
67237
  var AXIS_PX = 22;
67238
+ function stateStops(rows, from, to) {
67239
+ const found = new Set([from]);
67240
+ for (const row of rows) for (const segment of row.segments) if (segment.from >= from && segment.from < to) found.add(segment.from);
67241
+ return [...found].sort((a, b) => a - b);
67242
+ }
67243
+ function stateTooltip(rows, range) {
67244
+ const span = range.to - range.from;
67245
+ return {
67246
+ xMin: range.from,
67247
+ xMax: range.to,
67248
+ stops: stateStops(rows, range.from, range.to),
67249
+ resolve: (dataX) => stateTooltipBody({
67250
+ timeLabel: formatCursorTime(dataX, span),
67251
+ rows: rows.map((row) => {
67252
+ const hit = stateSpanAt(row.segments, dataX);
67253
+ const state = hit?.state ?? null;
67254
+ const stateLabel = state === null ? null : row.stateLabel?.(state) ?? state;
67255
+ return {
67256
+ id: row.id,
67257
+ label: row.label,
67258
+ stateLabel,
67259
+ spanMs: state === null ? null : hit?.spanMs ?? null
67260
+ };
67261
+ })
67262
+ })
67263
+ };
67264
+ }
66515
67265
  /** The colour role of a state on a row: a categorical slot, or the recessive "off". */
66516
67266
  function stateStyle(row, state) {
66517
67267
  const base = row.colorIndex ?? 0;
@@ -66586,6 +67336,10 @@ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMess
66586
67336
  }]
66587
67337
  }
66588
67338
  }), [optionsKey]);
67339
+ const tooltip = useMemo(() => ({
67340
+ ...stateTooltip(rows, range),
67341
+ onDataX: (dataX) => setCursorT(dataX)
67342
+ }), [rows, range]);
66589
67343
  const durations = useMemo(() => rows.map((r) => r.totals ?? timeInState(r.segments)), [rows]);
66590
67344
  const summary = rows.map((row, i) => {
66591
67345
  const parts = row.states.map((s) => {
@@ -66639,7 +67393,10 @@ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMess
66639
67393
  optionsKey,
66640
67394
  buildOptions,
66641
67395
  data,
66642
- height: rows.length * ROW_PX + AXIS_PX
67396
+ height: rows.length * ROW_PX + AXIS_PX,
67397
+ tooltip,
67398
+ kind: "states",
67399
+ label: title
66643
67400
  })
66644
67401
  })]
66645
67402
  }),
@@ -66684,7 +67441,38 @@ function StateTimeline({ title, rows, range, syncKey, pending = false, emptyMess
66684
67441
  * row (a training job's mAP per epoch). The x is whatever the caller orders
66685
67442
  * by (an epoch, a time); the value next to it names what the line is.
66686
67443
  */
66687
- function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, widthClassName = "w-40", zeroBased = true, className }) {
67444
+ function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, widthClassName = "w-40", zeroBased = true, unit = null, quantity, displayPreference, className }) {
67445
+ const tooltip = useMemo(() => {
67446
+ const xs = points.map((point) => point.x);
67447
+ return {
67448
+ xMin: xs[0] ?? 0,
67449
+ xMax: xs.at(-1) ?? 0,
67450
+ stops: xs,
67451
+ resolve: (dataX) => {
67452
+ const index = nearestPointIndex(xs, dataX);
67453
+ if (index === null) return null;
67454
+ const point = points[index];
67455
+ if (point === void 0) return null;
67456
+ return lineTooltipBody({
67457
+ timeLabel: String(point.x),
67458
+ preference: displayPreference,
67459
+ series: [{
67460
+ id: "value",
67461
+ label: ariaLabel,
67462
+ value: point.v,
67463
+ unit,
67464
+ quantity
67465
+ }]
67466
+ });
67467
+ }
67468
+ };
67469
+ }, [
67470
+ points,
67471
+ ariaLabel,
67472
+ unit,
67473
+ quantity,
67474
+ displayPreference
67475
+ ]);
66688
67476
  const data = useMemo(() => [points.map((p) => p.x), points.map((p) => p.v)], [points]);
66689
67477
  const buildOptions = useCallback((_w, lib) => ({
66690
67478
  legend: { show: false },
@@ -66720,7 +67508,10 @@ function Sparkline({ points, ariaLabel, token = "chart-series-a", height = 32, w
66720
67508
  buildOptions,
66721
67509
  data,
66722
67510
  height,
66723
- inline: true
67511
+ inline: true,
67512
+ tooltip,
67513
+ kind: "spark",
67514
+ label: ariaLabel
66724
67515
  })
66725
67516
  });
66726
67517
  }