@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/composites/charts/BarSeries.d.ts +4 -1
- package/dist/composites/charts/Sparkline.d.ts +6 -1
- package/dist/composites/charts/TimeSeriesChart.d.ts +4 -1
- package/dist/composites/charts/chart-tooltip-host.d.ts +27 -0
- package/dist/composites/charts/chart-tooltip-model.d.ts +174 -0
- package/dist/composites/charts/time-series-model.d.ts +3 -0
- package/dist/composites/charts/uplot-host.d.ts +9 -1
- package/dist/index.cjs +811 -20
- package/dist/index.js +812 -21
- package/package.json +1 -1
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
|
|
22601
|
-
|
|
22602
|
-
|
|
22603
|
-
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
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
|
|
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
|
}
|