@devicechain/widget-sdk 0.21.1-rc.1 → 0.22.0-rc.1

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/echarts.js CHANGED
@@ -1,18 +1,65 @@
1
1
  import {
2
+ FIT_MIN_FONT_PX,
3
+ bandAt,
4
+ fitValueFont,
5
+ formatValue,
6
+ isAlarmLevel,
7
+ levelColor,
8
+ parseThresholds,
9
+ severityColorsAt,
10
+ severityFallbackColors,
11
+ thresholdBands,
2
12
  useWidgetThemeRoot
3
- } from "./chunks/chunk-CC2V7LVI.js";
13
+ } from "./chunks/chunk-A5BJ2ALW.js";
4
14
 
5
15
  // src/chart/echart.tsx
6
16
  import { GaugeChart, LineChart } from "echarts/charts";
7
- import { GridComponent, TooltipComponent } from "echarts/components";
17
+ import { GridComponent, LegendPlainComponent, TooltipComponent } from "echarts/components";
8
18
  import * as echarts from "echarts/core";
9
19
  import { CanvasRenderer } from "echarts/renderers";
10
- import { useEffect, useRef } from "react";
20
+ import { useEffect, useRef, useState } from "react";
11
21
  import { jsx } from "react/jsx-runtime";
12
- echarts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, CanvasRenderer]);
22
+ echarts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, LegendPlainComponent, CanvasRenderer]);
23
+ var markersReady = false;
24
+ var markersLoading;
25
+ function loadThresholdMarkers() {
26
+ markersLoading ?? (markersLoading = import("./chunks/echart-markers-JDFD265P.js").then(
27
+ (m) => {
28
+ echarts.use(m.MARKER_COMPONENTS);
29
+ markersReady = true;
30
+ },
31
+ (err) => {
32
+ markersLoading = void 0;
33
+ throw err;
34
+ }
35
+ ));
36
+ return markersLoading;
37
+ }
38
+ function optionUsesMarkers(option) {
39
+ const series = option.series;
40
+ const list = Array.isArray(series) ? series : series ? [series] : [];
41
+ return list.some((s) => {
42
+ const m = s;
43
+ return typeof m === "object" && m !== null && (m.markLine != null || m.markArea != null);
44
+ });
45
+ }
13
46
  function EChart({ option, className, style }) {
14
47
  const containerRef = useRef(null);
15
48
  const chartRef = useRef(null);
49
+ const [markersLoaded, setMarkersLoaded] = useState(markersReady);
50
+ const needsMarkers = optionUsesMarkers(option) && !markersLoaded;
51
+ useEffect(() => {
52
+ if (!needsMarkers) return;
53
+ let live = true;
54
+ loadThresholdMarkers().then(
55
+ () => live && setMarkersLoaded(true),
56
+ // Loud, not silent: the chart still draws, without its threshold lines.
57
+ (err) => console.error("EChart: the threshold markers failed to load", err)
58
+ );
59
+ return () => {
60
+ live = false;
61
+ };
62
+ }, [needsMarkers]);
16
63
  useEffect(() => {
17
64
  const el = containerRef.current;
18
65
  if (!el) return;
@@ -28,7 +75,7 @@ function EChart({ option, className, style }) {
28
75
  }, []);
29
76
  useEffect(() => {
30
77
  chartRef.current?.setOption(option, { notMerge: true });
31
- }, [option]);
78
+ }, [option, markersLoaded]);
32
79
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%", ...style } });
33
80
  }
34
81
 
@@ -40,17 +87,59 @@ function distinctNames(samples) {
40
87
  }
41
88
  return seen;
42
89
  }
90
+ function severityFill(theme, level, alpha) {
91
+ return theme.severity[level].replace(/^hsl\(/, "hsla(").replace(/\)$/, `, ${alpha})`);
92
+ }
93
+ var BAND_ALPHA = 0.14;
43
94
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
44
95
  function shortSide(size) {
45
96
  const s = Math.min(size?.width ?? 0, size?.height ?? 0);
46
97
  return s > 0 ? s : 200;
47
98
  }
99
+ function thresholdMarks(theme, thresholds, unit, precision, fontSize) {
100
+ const suffix = unit ? ` ${unit}` : "";
101
+ const ink = (t) => levelColor(theme.severity, t.level) ?? theme.mutedForeground;
102
+ const markLine = {
103
+ silent: true,
104
+ symbol: "none",
105
+ animation: false,
106
+ data: thresholds.map((t) => ({
107
+ yAxis: t.value,
108
+ name: t.label ?? String(t.value),
109
+ lineStyle: { color: ink(t), type: "dashed", width: 1.5 },
110
+ label: {
111
+ show: true,
112
+ position: "insideEndTop",
113
+ color: ink(t),
114
+ fontSize,
115
+ formatter: () => t.label ?? `${formatValue(t.value, precision)}${suffix}`
116
+ }
117
+ }))
118
+ };
119
+ const markArea = {
120
+ silent: true,
121
+ animation: false,
122
+ data: thresholdBands(thresholds, -Infinity, Infinity).filter((b) => isAlarmLevel(b.level)).map((b) => [
123
+ { yAxis: b.from, itemStyle: { color: severityFill(theme, b.level, BAND_ALPHA) } },
124
+ { yAxis: b.to }
125
+ ])
126
+ };
127
+ return { markLine, markArea };
128
+ }
48
129
  function buildLineOption(samples, theme, options = {}, size) {
49
130
  const names = options.measurements?.length ? options.measurements : distinctNames(samples);
50
- const axisFont = clamp(Math.round(shortSide(size) / 22), 8, 12);
131
+ const scale = options.scale ?? 1;
132
+ const axisFont = clamp(Math.round(shortSide(size) / 22), Math.round(8 * scale), Math.round(12 * scale));
133
+ const thresholds = parseThresholds(options.thresholds ?? []);
134
+ const lowest = thresholds.length > 0 ? thresholds[0].value : 0;
135
+ const highest = thresholds.length > 0 ? thresholds[thresholds.length - 1].value : 0;
136
+ const marks = thresholds.length > 0 ? thresholdMarks(theme, thresholds, options.unit, options.precision, axisFont) : void 0;
137
+ const suffix = options.unit ? ` ${options.unit}` : "";
138
+ const precision = options.precision;
51
139
  const series = names.map((name, i) => {
52
140
  const color = theme.series[i % theme.series.length];
53
141
  return {
142
+ ...i === 0 ? marks : void 0,
54
143
  name,
55
144
  type: "line",
56
145
  showSymbol: false,
@@ -62,8 +151,11 @@ function buildLineOption(samples, theme, options = {}, size) {
62
151
  return {
63
152
  animation: false,
64
153
  textStyle: { color: theme.foreground, fontSize: axisFont },
65
- grid: { left: 8, right: 12, top: names.length > 1 ? 28 : 12, bottom: 8, containLabel: true },
66
- tooltip: { trigger: "axis" },
154
+ grid: { left: 8, right: 12, top: names.length > 1 || options.unit ? 28 : 12, bottom: 8, containLabel: true },
155
+ tooltip: {
156
+ trigger: "axis",
157
+ valueFormatter: (v) => typeof v === "number" ? `${formatValue(v, precision)}${suffix}` : String(v ?? "\u2014")
158
+ },
67
159
  legend: names.length > 1 ? { textStyle: { color: theme.mutedForeground, fontSize: axisFont }, top: 0 } : void 0,
68
160
  xAxis: {
69
161
  type: "time",
@@ -72,21 +164,67 @@ function buildLineOption(samples, theme, options = {}, size) {
72
164
  },
73
165
  yAxis: {
74
166
  type: "value",
167
+ // The unit rides the axis once, at its head, rather than on every tick.
168
+ name: options.unit,
169
+ nameTextStyle: { color: theme.mutedForeground, fontSize: axisFont, align: "left" },
75
170
  splitLine: { lineStyle: { color: theme.border } },
76
- axisLabel: { color: theme.mutedForeground, fontSize: axisFont }
171
+ axisLabel: {
172
+ color: theme.mutedForeground,
173
+ fontSize: axisFont,
174
+ // Rounded only when the author asked; otherwise ECharts' own tick rounding applies.
175
+ ...precision != null ? { formatter: (v) => formatValue(v, precision) } : {}
176
+ },
177
+ // A threshold outside the data's range must still be on the chart: extend the range to
178
+ // take it in. Only then -- a returned number FIXES that end of the axis (no nice
179
+ // rounding, no zero baseline), so a threshold inside the data returns null and leaves
180
+ // ECharts' own auto range exactly as it is without thresholds.
181
+ ...thresholds.length > 0 ? {
182
+ min: (e) => lowest < e.min ? lowest : null,
183
+ max: (e) => highest > e.max ? highest : null
184
+ } : {}
77
185
  },
78
186
  series
79
187
  };
80
188
  }
189
+ var GAUGE_CENTER_Y = 0.58;
190
+ var GAUGE_RADIUS = 0.95;
191
+ var GAUGE_READOUT_OFFSET = 0.3;
192
+ function gaugeReadoutBox(size) {
193
+ const s = shortSide(size);
194
+ const w = (size?.width ?? 0) > 0 ? size.width : s;
195
+ const h = (size?.height ?? 0) > 0 ? size.height : s;
196
+ return {
197
+ centerX: w / 2,
198
+ centerY: GAUGE_CENTER_Y * h + GAUGE_READOUT_OFFSET * GAUGE_RADIUS * (s / 2),
199
+ width: s * 0.7,
200
+ height: s * 0.25
201
+ };
202
+ }
203
+ var GAUGE_AXIS_LABELS_MIN_SIDE = 120;
204
+ function gaugeArcColors(bands, min, max, theme) {
205
+ if (bands.length === 0) return [[1, theme.border]];
206
+ return bands.map((b, i) => [
207
+ i === bands.length - 1 ? 1 : (b.to - min) / (max - min),
208
+ levelColor(theme.severity, b.level) ?? theme.border
209
+ ]);
210
+ }
81
211
  function buildGaugeOption(value, theme, options = {}, size) {
82
212
  const min = options.min ?? 0;
83
213
  const max = options.max ?? 100;
84
- const accent = theme.series[0];
85
- const unitSuffix = options.unit ? ` ${options.unit}` : "";
214
+ const thresholds = parseThresholds(options.thresholds ?? []);
215
+ const bands = thresholdBands(thresholds, min, max);
216
+ const banded = bands.some((b) => isAlarmLevel(b.level));
217
+ const accent = levelColor(theme.severity, bandAt(thresholds, value)?.level) ?? theme.series[0];
86
218
  const s = shortSide(size);
87
219
  const arcWidth = clamp(Math.round(s / 26), 4, 12);
88
- const labelFont = clamp(Math.round(s / 20), 8, 13);
89
- const valueFont = clamp(Math.round(s / 7), 14, 40);
220
+ const scale = options.scale ?? 1;
221
+ const labelFont = clamp(Math.round(s / 20), Math.round(8 * scale), Math.round(13 * scale));
222
+ const showAxisLabels = s >= GAUGE_AXIS_LABELS_MIN_SIDE;
223
+ const unitSuffix = options.unit ? ` ${options.unit}` : "";
224
+ const readout = value == null ? "\u2014" : `${formatValue(value, options.precision)}${unitSuffix}`;
225
+ const valueFont = fitValueFont({ width: s * 0.7, height: s * 0.25 }, readout.length, {
226
+ floor: Math.round(FIT_MIN_FONT_PX * scale)
227
+ });
90
228
  return {
91
229
  animation: false,
92
230
  series: [
@@ -96,27 +234,80 @@ function buildGaugeOption(value, theme, options = {}, size) {
96
234
  max,
97
235
  splitNumber: s < 160 ? 4 : 5,
98
236
  // fewer ticks than the default 10 → no cram
99
- radius: "95%",
100
- center: ["50%", "58%"],
101
- progress: { show: true, width: arcWidth, itemStyle: { color: accent } },
102
- pointer: { itemStyle: { color: accent }, length: "55%", width: clamp(Math.round(s / 50), 2, 6) },
237
+ radius: `${GAUGE_RADIUS * 100}%`,
238
+ center: ["50%", `${GAUGE_CENTER_Y * 100}%`],
239
+ // The progress arc would paint over the bands, so with bands the needle alone marks the value.
240
+ progress: { show: !banded, width: arcWidth, itemStyle: { color: accent } },
241
+ pointer: {
242
+ show: value != null || options.emptyNeedle !== "none",
243
+ itemStyle: { color: accent },
244
+ length: "55%",
245
+ width: clamp(Math.round(s / 50), 2, 6)
246
+ },
103
247
  anchor: { show: false },
104
- axisLine: { lineStyle: { width: arcWidth, color: [[1, theme.border]] } },
248
+ axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, min, max, theme) } },
105
249
  axisTick: { show: false },
106
250
  splitLine: { length: arcWidth, lineStyle: { color: theme.border, width: 1 } },
107
- axisLabel: { color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },
108
- detail: {
251
+ axisLabel: { show: showAxisLabels, color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },
252
+ detail: options.readout === "none" ? { show: false } : {
109
253
  valueAnimation: false,
110
254
  color: theme.foreground,
111
255
  fontSize: valueFont,
112
- offsetCenter: [0, "30%"],
113
- formatter: (v) => value == null ? "\u2014" : `${v}${unitSuffix}`
256
+ offsetCenter: [0, `${GAUGE_READOUT_OFFSET * 100}%`],
257
+ formatter: (v) => value == null ? "\u2014" : `${formatValue(v, options.precision)}${unitSuffix}`
114
258
  },
115
259
  data: [{ value: value ?? min }]
116
260
  }
117
261
  ]
118
262
  };
119
263
  }
264
+ function wrapHeading(value) {
265
+ if (value == null || !Number.isFinite(value)) return void 0;
266
+ return (value % 360 + 360) % 360;
267
+ }
268
+ function buildCompassOption(value, theme, options, size) {
269
+ const heading = wrapHeading(value);
270
+ const thresholds = parseThresholds(options.thresholds ?? []);
271
+ const bands = thresholdBands(thresholds, 0, 360);
272
+ const accent = levelColor(theme.severity, bandAt(thresholds, heading ?? null)?.level) ?? theme.series[0];
273
+ const s = shortSide(size);
274
+ const scale = options.scale ?? 1;
275
+ const arcWidth = clamp(Math.round(s / 30), 3, 10);
276
+ const labelFont = clamp(Math.round(s / 11), Math.round(10 * scale), Math.round(20 * scale));
277
+ const [north, east, south, west] = options.cardinals;
278
+ const points = { 0: north, 90: east, 180: south, 270: west };
279
+ return {
280
+ animation: false,
281
+ series: [
282
+ {
283
+ type: "gauge",
284
+ min: 0,
285
+ max: 360,
286
+ startAngle: 90,
287
+ endAngle: -270,
288
+ splitNumber: 4,
289
+ radius: "92%",
290
+ center: ["50%", "50%"],
291
+ progress: { show: false },
292
+ pointer: { itemStyle: { color: accent }, length: "62%", width: clamp(Math.round(s / 36), 3, 8) },
293
+ anchor: { show: true, size: clamp(Math.round(s / 24), 5, 12), itemStyle: { color: accent } },
294
+ axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, 0, 360, theme) } },
295
+ axisTick: { show: s >= GAUGE_AXIS_LABELS_MIN_SIDE, splitNumber: 3, length: arcWidth, lineStyle: { color: theme.border, width: 1 } },
296
+ splitLine: { length: arcWidth * 1.6, lineStyle: { color: theme.mutedForeground, width: 2 } },
297
+ // Only the four points are labelled; 360 is the same tick as 0, so it stays blank.
298
+ axisLabel: {
299
+ show: true,
300
+ color: theme.foreground,
301
+ fontSize: labelFont,
302
+ distance: arcWidth * 1.6 + 6,
303
+ formatter: (v) => points[v] ?? ""
304
+ },
305
+ detail: { show: false },
306
+ data: [{ value: heading ?? 0 }]
307
+ }
308
+ ]
309
+ };
310
+ }
120
311
 
121
312
  // src/chart/chart-theme.ts
122
313
  import { useMemo, useSyncExternalStore } from "react";
@@ -130,7 +321,8 @@ var FALLBACK = {
130
321
  "hsl(30 80% 55%)",
131
322
  "hsl(280 65% 60%)",
132
323
  "hsl(340 75% 55%)"
133
- ]
324
+ ],
325
+ severity: severityFallbackColors()
134
326
  };
135
327
  function readVar(style, name, fallback) {
136
328
  const raw = style.getPropertyValue(`--${name}`).trim();
@@ -142,7 +334,8 @@ function resolveChartTheme(el) {
142
334
  foreground: readVar(style, "foreground", FALLBACK.foreground),
143
335
  mutedForeground: readVar(style, "muted-foreground", FALLBACK.mutedForeground),
144
336
  border: readVar(style, "border", FALLBACK.border),
145
- series: FALLBACK.series.map((fallback, i) => readVar(style, `chart-${i + 1}`, fallback))
337
+ series: FALLBACK.series.map((fallback, i) => readVar(style, `chart-${i + 1}`, fallback)),
338
+ severity: severityColorsAt(el, style)
146
339
  };
147
340
  }
148
341
  var sharedTheme = null;
@@ -185,9 +378,13 @@ function useChartTheme() {
185
378
  }
186
379
  export {
187
380
  EChart,
381
+ buildCompassOption,
188
382
  buildGaugeOption,
189
383
  buildLineOption,
384
+ gaugeReadoutBox,
190
385
  resolveChartTheme,
191
- useChartTheme
386
+ severityFill,
387
+ useChartTheme,
388
+ wrapHeading
192
389
  };
193
390
  //# sourceMappingURL=echarts.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/chart/echart.tsx", "../src/chart/chart-options.ts", "../src/chart/chart-theme.ts"],
4
- "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A thin React wrapper over Apache ECharts (ADR-039 chart engine).\n//\n// It imports from echarts/core and registers ONLY the chart types + components the\n// widgets use (line + gauge, grid/tooltip, canvas renderer) \u2014 tree-shaking away the\n// rest of ECharts so the bundle carries only what a dashboard needs. The wrapper\n// owns the imperative lifecycle: init on mount, setOption on change, resize with the\n// container, dispose on unmount.\n\nimport { GaugeChart, LineChart } from 'echarts/charts';\nimport { GridComponent, TooltipComponent } from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { CanvasRenderer } from 'echarts/renderers';\nimport { useEffect, useRef } from 'react';\n\necharts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, CanvasRenderer]);\n\n// A resolved ECharts option object (the shape echarts.setOption accepts). Widgets\n// build these with the pure builders in chart-options.ts.\nexport type EChartOption = echarts.EChartsCoreOption;\n\nexport interface EChartProps {\n option: EChartOption;\n className?: string;\n style?: React.CSSProperties;\n}\n\n// EChart fills its parent; the dashboard canvas sizes the parent, and a\n// ResizeObserver keeps the chart matched to it.\nexport function EChart({ option, className, style }: EChartProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const chartRef = useRef<echarts.ECharts | null>(null);\n\n // Init + teardown once for the element's lifetime.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const chart = echarts.init(el);\n chartRef.current = chart;\n\n const observer = new ResizeObserver(() => chart.resize());\n observer.observe(el);\n\n return () => {\n observer.disconnect();\n chart.dispose();\n chartRef.current = null;\n };\n }, []);\n\n // Re-apply the option whenever it changes. notMerge:true so a shrinking series\n // (rolling window) doesn't leave stale points behind.\n useEffect(() => {\n chartRef.current?.setOption(option, { notMerge: true });\n }, [option]);\n\n return (\n <div ref={containerRef} className={className} style={{ width: '100%', height: '100%', ...style }} />\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Pure ECharts option builders. Kept free of React/DOM so the data\u2192chart mapping\n// is unit-testable without a canvas: a widget resolves a ChartTheme + samples and\n// hands the result straight to <EChart>.\n\nimport type { MeasurementSample } from '@devicechain/dashboards';\n\nimport type { ElementSize } from '../hooks';\n\nimport type { ChartTheme } from './chart-theme';\nimport type { EChartOption } from './echart';\n\n// distinctNames returns the measurement names present in `samples`, in first-seen\n// order \u2014 used when a chart's datasource doesn't pin an explicit series list.\nfunction distinctNames(samples: MeasurementSample[]): string[] {\n const seen: string[] = [];\n for (const s of samples) {\n if (!seen.includes(s.name)) seen.push(s.name);\n }\n return seen;\n}\n\nconst clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));\n\n// gauge/line fonts and tick counts scale with the smaller container dimension so a\n// widget stays legible when resized. A zero/absent size (before first measure, or\n// in a non-DOM test) falls back to a nominal medium size.\nfunction shortSide(size?: ElementSize): number {\n const s = Math.min(size?.width ?? 0, size?.height ?? 0);\n return s > 0 ? s : 200;\n}\n\nexport interface LineChartOptions {\n // Measurement names to plot, one series each. Empty \u2192 every name seen in the\n // window (first-seen order).\n measurements?: string[];\n}\n\n// buildLineOption maps a rolling window of samples to a multi-series time chart,\n// one line per measurement name, coloured from the theme's chart palette.\nexport function buildLineOption(\n samples: MeasurementSample[],\n theme: ChartTheme,\n options: LineChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const names = options.measurements?.length ? options.measurements : distinctNames(samples);\n const axisFont = clamp(Math.round(shortSide(size) / 22), 8, 12);\n\n const series = names.map((name, i) => {\n const color = theme.series[i % theme.series.length];\n return {\n name,\n type: 'line' as const,\n showSymbol: false,\n lineStyle: { color },\n itemStyle: { color },\n data: samples\n .filter((s) => s.name === name && s.value != null && s.occurredTime != null)\n .map((s) => [s.occurredTime as string, s.value as number]),\n };\n });\n\n return {\n animation: false,\n textStyle: { color: theme.foreground, fontSize: axisFont },\n grid: { left: 8, right: 12, top: names.length > 1 ? 28 : 12, bottom: 8, containLabel: true },\n tooltip: { trigger: 'axis' },\n legend:\n names.length > 1 ? { textStyle: { color: theme.mutedForeground, fontSize: axisFont }, top: 0 } : undefined,\n xAxis: {\n type: 'time',\n axisLine: { lineStyle: { color: theme.border } },\n axisLabel: { color: theme.mutedForeground, fontSize: axisFont, hideOverlap: true },\n },\n yAxis: {\n type: 'value',\n splitLine: { lineStyle: { color: theme.border } },\n axisLabel: { color: theme.mutedForeground, fontSize: axisFont },\n },\n series,\n };\n}\n\nexport interface GaugeChartOptions {\n min?: number;\n max?: number;\n unit?: string;\n}\n\n// buildGaugeOption maps a single latest value to a gauge. A null value (nothing\n// received yet) shows an em dash and rests the needle at the minimum. Ticks, fonts,\n// and the arc width scale with the widget size so it stays legible when resized\n// (ECharts' default gauge crams ~10 labels at fixed fonts on a small canvas).\nexport function buildGaugeOption(\n value: number | null,\n theme: ChartTheme,\n options: GaugeChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const min = options.min ?? 0;\n const max = options.max ?? 100;\n const accent = theme.series[0];\n const unitSuffix = options.unit ? ` ${options.unit}` : '';\n\n const s = shortSide(size);\n const arcWidth = clamp(Math.round(s / 26), 4, 12);\n const labelFont = clamp(Math.round(s / 20), 8, 13);\n const valueFont = clamp(Math.round(s / 7), 14, 40);\n\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min,\n max,\n splitNumber: s < 160 ? 4 : 5, // fewer ticks than the default 10 \u2192 no cram\n radius: '95%',\n center: ['50%', '58%'],\n progress: { show: true, width: arcWidth, itemStyle: { color: accent } },\n pointer: { itemStyle: { color: accent }, length: '55%', width: clamp(Math.round(s / 50), 2, 6) },\n anchor: { show: false },\n axisLine: { lineStyle: { width: arcWidth, color: [[1, theme.border]] } },\n axisTick: { show: false },\n splitLine: { length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n axisLabel: { color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },\n detail: {\n valueAnimation: false,\n color: theme.foreground,\n fontSize: valueFont,\n offsetCenter: [0, '30%'],\n formatter: (v: number) => (value == null ? '\u2014' : `${v}${unitSuffix}`),\n },\n data: [{ value: value ?? min }],\n },\n ],\n };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The chart colour theme: concrete colours resolved from the host's CSS custom properties,\n// because an ECharts canvas cannot resolve `var(--x)`. Pure DOM + React, no echarts import,\n// but it lives on the `/echarts` subpath with the rest of the chart helpers so the SDK root\n// stays chart-free.\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useWidgetThemeRoot } from '../context';\n\n// Concrete colors for an ECharts canvas, resolved from the CSS variables in scope\n// at a given element (charts can't use CSS vars directly).\nexport interface ChartTheme {\n foreground: string;\n mutedForeground: string;\n border: string;\n series: string[];\n}\n\n// Fallbacks used when a variable is absent (e.g. jsdom, or a host that hasn't set\n// the token) so a chart still renders in sensible default colors.\nconst FALLBACK: ChartTheme = {\n foreground: 'hsl(240 10% 3.9%)',\n mutedForeground: 'hsl(240 3.8% 46.1%)',\n border: 'hsl(240 5.9% 90%)',\n series: [\n 'hsl(220 70% 50%)',\n 'hsl(160 60% 45%)',\n 'hsl(30 80% 55%)',\n 'hsl(280 65% 60%)',\n 'hsl(340 75% 55%)',\n ],\n};\n\nfunction readVar(style: CSSStyleDeclaration, name: string, fallback: string): string {\n const raw = style.getPropertyValue(`--${name}`).trim();\n return raw ? `hsl(${raw})` : fallback;\n}\n\n// resolveChartTheme reads the theme tokens in scope at `el` into concrete colors\n// for ECharts. Reading at the element (not the document root) means a widget\n// nested under a re-themed subtree picks up the local override.\nexport function resolveChartTheme(el: Element): ChartTheme {\n const style = getComputedStyle(el);\n return {\n foreground: readVar(style, 'foreground', FALLBACK.foreground),\n mutedForeground: readVar(style, 'muted-foreground', FALLBACK.mutedForeground),\n border: readVar(style, 'border', FALLBACK.border),\n series: FALLBACK.series.map((fallback, i) => readVar(style, `chart-${i + 1}`, fallback)),\n };\n}\n\n// --- shared chart-theme store ------------------------------------------------\n//\n// Charts need concrete colors resolved from the CSS-variable theme. Rather than\n// have every chart widget mount its own MutationObserver on <html> and recompute\n// the same theme, one page-wide observer resolves it once per change and every\n// widget reads the shared, memoized value via useSyncExternalStore \u2014 so a busy\n// dashboard pays O(1), not O(number of charts), per theme toggle.\n\nlet sharedTheme: ChartTheme | null = null;\nconst themeListeners = new Set<() => void>();\nlet themeObserver: MutationObserver | null = null;\n\nfunction recomputeTheme(): void {\n sharedTheme = resolveChartTheme(document.documentElement);\n}\n\nfunction subscribeTheme(listener: () => void): () => void {\n if (!themeObserver) {\n recomputeTheme();\n themeObserver = new MutationObserver(() => {\n recomputeTheme();\n for (const l of themeListeners) l();\n });\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class', 'style'],\n });\n }\n themeListeners.add(listener);\n return () => {\n themeListeners.delete(listener);\n if (themeListeners.size === 0 && themeObserver) {\n themeObserver.disconnect();\n themeObserver = null;\n sharedTheme = null;\n }\n };\n}\n\nfunction getThemeSnapshot(): ChartTheme {\n if (!sharedTheme) recomputeTheme();\n return sharedTheme as ChartTheme;\n}\n\n// useChartTheme returns the current chart colors and re-renders the caller when\n// the document theme changes (a light/dark class or inline-style swap on <html>).\nexport function useChartTheme(): ChartTheme {\n const shared = useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeSnapshot);\n // A host that scopes its tokens to a container says so (WidgetThemeRoot); its tokens are\n // fixed for the container's life, so they are read once there rather than observed.\n const root = useWidgetThemeRoot();\n const scoped = useMemo(() => (root ? resolveChartTheme(root) : null), [root]);\n return scoped ?? shared;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A thin React wrapper over Apache ECharts (ADR-039 chart engine).\n//\n// It imports from echarts/core and registers ONLY the chart types + components the\n// widgets use (line + gauge, grid/tooltip/legend, canvas renderer; threshold markers on demand) \u2014\n// tree-shaking away the rest of ECharts so the bundle carries only what a dashboard needs. The wrapper\n// owns the imperative lifecycle: init on mount, setOption on change, resize with the\n// container, dispose on unmount.\n\nimport { GaugeChart, LineChart } from 'echarts/charts';\nimport { GridComponent, LegendPlainComponent, TooltipComponent } from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { CanvasRenderer } from 'echarts/renderers';\nimport { useEffect, useRef, useState } from 'react';\n\n// \uD83D\uDD34 EVERY COMPONENT AN OPTION BUILDER EMITS MUST BE REGISTERED -- here, or (the threshold\n// markers) by loadThresholdMarkers below. ECharts ignores an option key whose component is\n// not registered -- silently, no warning -- so a `legend` or a `markLine` the builder sets\n// draws nothing. The legend was dropped this way for as long as multi-series charts existed.\n// echart.registration.test.tsx renders each through a real ECharts instance, so a builder\n// that grows a new component fails there until it is registered.\n//\n// The PLAIN legend, not LegendComponent: that one also installs the scrollable legend, which\n// no builder asks for (~1.8 KB gzip).\necharts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, LegendPlainComponent, CanvasRenderer]);\n\n// markLine / markArea are registered on first use (see echart-markers.ts for why). Until they\n// are, a chart that asks for them draws without them, and EChart re-applies its option once\n// they arrive -- a beat later on the first threshold chart of a page, never silently absent.\nlet markersReady = false;\nlet markersLoading: Promise<void> | undefined;\n\nexport function loadThresholdMarkers(): Promise<void> {\n markersLoading ??= import('./echart-markers').then(\n (m) => {\n echarts.use(m.MARKER_COMPONENTS);\n markersReady = true;\n },\n (err: unknown) => {\n markersLoading = undefined; // a failed chunk load is retried by the next chart that needs it\n throw err;\n },\n );\n return markersLoading;\n}\n\n// True when any series of the option carries a markLine or markArea.\nexport function optionUsesMarkers(option: EChartOption): boolean {\n const series = (option as { series?: unknown }).series;\n const list = Array.isArray(series) ? series : series ? [series] : [];\n return list.some((s) => {\n const m = s as { markLine?: unknown; markArea?: unknown } | null;\n return typeof m === 'object' && m !== null && (m.markLine != null || m.markArea != null);\n });\n}\n\n// A resolved ECharts option object (the shape echarts.setOption accepts). Widgets\n// build these with the pure builders in chart-options.ts.\nexport type EChartOption = echarts.EChartsCoreOption;\n\nexport interface EChartProps {\n option: EChartOption;\n className?: string;\n style?: React.CSSProperties;\n}\n\n// EChart fills its parent; the dashboard canvas sizes the parent, and a\n// ResizeObserver keeps the chart matched to it.\nexport function EChart({ option, className, style }: EChartProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const chartRef = useRef<echarts.ECharts | null>(null);\n const [markersLoaded, setMarkersLoaded] = useState(markersReady);\n const needsMarkers = optionUsesMarkers(option) && !markersLoaded;\n\n useEffect(() => {\n if (!needsMarkers) return;\n let live = true;\n loadThresholdMarkers().then(\n () => live && setMarkersLoaded(true),\n // Loud, not silent: the chart still draws, without its threshold lines.\n (err: unknown) => console.error('EChart: the threshold markers failed to load', err),\n );\n return () => {\n live = false;\n };\n }, [needsMarkers]);\n\n // Init + teardown once for the element's lifetime.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const chart = echarts.init(el);\n chartRef.current = chart;\n\n const observer = new ResizeObserver(() => chart.resize());\n observer.observe(el);\n\n return () => {\n observer.disconnect();\n chart.dispose();\n chartRef.current = null;\n };\n }, []);\n\n // Re-apply the option whenever it changes. notMerge:true so a shrinking series\n // (rolling window) doesn't leave stale points behind.\n useEffect(() => {\n chartRef.current?.setOption(option, { notMerge: true });\n }, [option, markersLoaded]);\n\n return (\n <div ref={containerRef} className={className} style={{ width: '100%', height: '100%', ...style }} />\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Pure ECharts option builders. Kept free of React/DOM so the data\u2192chart mapping\n// is unit-testable without a canvas: a widget resolves a ChartTheme + samples and\n// hands the result straight to <EChart>.\n\nimport type { MeasurementSample } from '@devicechain/dashboards';\n\nimport { FIT_MIN_FONT_PX, fitValueFont } from '../fit-text';\nimport { formatValue } from '../format';\nimport type { ElementSize } from '../hooks';\nimport { isAlarmLevel, levelColor, type AlarmLevel } from '../severity';\nimport { bandAt, parseThresholds, thresholdBands, type Threshold } from '../thresholds';\n\nimport type { ChartTheme } from './chart-theme';\nimport type { EChartOption } from './echart';\n\n// distinctNames returns the measurement names present in `samples`, in first-seen\n// order \u2014 used when a chart's datasource doesn't pin an explicit series list.\nfunction distinctNames(samples: MeasurementSample[]): string[] {\n const seen: string[] = [];\n for (const s of samples) {\n if (!seen.includes(s.name)) seen.push(s.name);\n }\n return seen;\n}\n\n// A severity colour with an alpha, for a band drawn UNDER the data (markArea). The theme's\n// severity colours are comma-form hsl(), so the alpha form is a string edit.\nexport function severityFill(theme: ChartTheme, level: AlarmLevel, alpha: number): string {\n return theme.severity[level].replace(/^hsl\\(/, 'hsla(').replace(/\\)$/, `, ${alpha})`);\n}\n\n// Opacity of a threshold band behind a line: enough to read as a zone, faint enough that the\n// line and its grid stay the subject. A `normal` band has no fill at all.\nconst BAND_ALPHA = 0.14;\n\nconst clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));\n\n// gauge/line fonts and tick counts scale with the smaller container dimension so a\n// widget stays legible when resized. A zero/absent size (before first measure, or\n// in a non-DOM test) falls back to a nominal medium size.\nfunction shortSide(size?: ElementSize): number {\n const s = Math.min(size?.width ?? 0, size?.height ?? 0);\n return s > 0 ? s : 200;\n}\n\nexport interface LineChartOptions {\n // Measurement names to plot, one series each. Empty \u2192 every name seen in the\n // window (first-seen order).\n measurements?: string[];\n // Unit suffix: named on the y-axis and appended to every tooltip value.\n unit?: string;\n // Decimal places for axis labels and tooltip values; unset uses the default rounding.\n precision?: number;\n // Severity thresholds, drawn as a labelled line each plus a faint band between them.\n // Any order: the builder sorts them.\n thresholds?: readonly Threshold[];\n // The board's density scale (useWidgetScale().scale): multiplies the axis-font clamp. Default 1.\n scale?: number;\n}\n\n// The markLine / markArea for a chart's thresholds, attached to the first series (the markers\n// belong to a series in ECharts, and every series shares the one y-axis).\nfunction thresholdMarks(theme: ChartTheme, thresholds: readonly Threshold[], unit: string | undefined, precision: number | undefined, fontSize: number) {\n const suffix = unit ? ` ${unit}` : '';\n // A `normal` threshold is still drawn: it is where the value RETURNS to normal, which an\n // operator wants to see. It is drawn in the muted text colour, the same ink as the axis\n // labels, which reads in light and dark and says \"boundary\" without saying \"alarm\".\n const ink = (t: Threshold) => levelColor(theme.severity, t.level) ?? theme.mutedForeground;\n const markLine = {\n silent: true,\n symbol: 'none',\n animation: false,\n data: thresholds.map((t) => ({\n yAxis: t.value,\n name: t.label ?? String(t.value),\n lineStyle: { color: ink(t), type: 'dashed' as const, width: 1.5 },\n label: {\n show: true,\n position: 'insideEndTop' as const,\n color: ink(t),\n fontSize,\n formatter: () => t.label ?? `${formatValue(t.value, precision)}${suffix}`,\n },\n })),\n };\n const markArea = {\n silent: true,\n animation: false,\n data: thresholdBands(thresholds, -Infinity, Infinity)\n .filter((b) => isAlarmLevel(b.level))\n .map((b) => [\n { yAxis: b.from, itemStyle: { color: severityFill(theme, b.level as AlarmLevel, BAND_ALPHA) } },\n { yAxis: b.to },\n ]),\n };\n return { markLine, markArea };\n}\n\n// buildLineOption maps a rolling window of samples to a multi-series time chart,\n// one line per measurement name, coloured from the theme's chart palette.\nexport function buildLineOption(\n samples: MeasurementSample[],\n theme: ChartTheme,\n options: LineChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const names = options.measurements?.length ? options.measurements : distinctNames(samples);\n const scale = options.scale ?? 1;\n const axisFont = clamp(Math.round(shortSide(size) / 22), Math.round(8 * scale), Math.round(12 * scale));\n\n // Re-parsed (validated + sorted): the builder is public, and a caller may hand it a list\n // that never went through parseThresholds. Every rule below assumes ascending order.\n const thresholds = parseThresholds(options.thresholds ?? []);\n const lowest = thresholds.length > 0 ? thresholds[0].value : 0;\n const highest = thresholds.length > 0 ? thresholds[thresholds.length - 1].value : 0;\n const marks = thresholds.length > 0 ? thresholdMarks(theme, thresholds, options.unit, options.precision, axisFont) : undefined;\n const suffix = options.unit ? ` ${options.unit}` : '';\n const precision = options.precision;\n\n const series = names.map((name, i) => {\n const color = theme.series[i % theme.series.length];\n return {\n ...(i === 0 ? marks : undefined),\n name,\n type: 'line' as const,\n showSymbol: false,\n lineStyle: { color },\n itemStyle: { color },\n data: samples\n .filter((s) => s.name === name && s.value != null && s.occurredTime != null)\n .map((s) => [s.occurredTime as string, s.value as number]),\n };\n });\n\n return {\n animation: false,\n textStyle: { color: theme.foreground, fontSize: axisFont },\n grid: { left: 8, right: 12, top: names.length > 1 || options.unit ? 28 : 12, bottom: 8, containLabel: true },\n tooltip: {\n trigger: 'axis',\n valueFormatter: (v: unknown) => (typeof v === 'number' ? `${formatValue(v, precision)}${suffix}` : String(v ?? '\u2014')),\n },\n legend:\n names.length > 1 ? { textStyle: { color: theme.mutedForeground, fontSize: axisFont }, top: 0 } : undefined,\n xAxis: {\n type: 'time',\n axisLine: { lineStyle: { color: theme.border } },\n axisLabel: { color: theme.mutedForeground, fontSize: axisFont, hideOverlap: true },\n },\n yAxis: {\n type: 'value',\n // The unit rides the axis once, at its head, rather than on every tick.\n name: options.unit,\n nameTextStyle: { color: theme.mutedForeground, fontSize: axisFont, align: 'left' as const },\n splitLine: { lineStyle: { color: theme.border } },\n axisLabel: {\n color: theme.mutedForeground,\n fontSize: axisFont,\n // Rounded only when the author asked; otherwise ECharts' own tick rounding applies.\n ...(precision != null ? { formatter: (v: number) => formatValue(v, precision) } : {}),\n },\n // A threshold outside the data's range must still be on the chart: extend the range to\n // take it in. Only then -- a returned number FIXES that end of the axis (no nice\n // rounding, no zero baseline), so a threshold inside the data returns null and leaves\n // ECharts' own auto range exactly as it is without thresholds.\n ...(thresholds.length > 0\n ? {\n min: (e: { min: number }) => (lowest < e.min ? lowest : null),\n max: (e: { max: number }) => (highest > e.max ? highest : null),\n }\n : {}),\n },\n series,\n };\n}\n\nexport interface GaugeChartOptions {\n min?: number;\n max?: number;\n unit?: string;\n // Decimal places for the readout; unset uses the default rounding.\n precision?: number;\n // Severity thresholds, drawn as coloured bands on the arc (any order).\n thresholds?: readonly Threshold[];\n // The board's density scale: multiplies the label-font clamp and the readout's floor. Default 1.\n scale?: number;\n // Where the number is drawn. 'canvas' (the default) paints it inside the dial, as the builder\n // always has, so a widget that renders only the chart keeps its readout. 'none' leaves the dial\n // bare for a caller that draws the number itself as DOM text in gaugeReadoutBox, which the\n // readability gate can measure (the built-in gauge does).\n readout?: 'canvas' | 'none';\n // Where the needle is with no value. 'min' (the default) rests it at the scale's minimum, as the\n // builder always has. 'none' draws no needle, so an empty dial cannot be read as a reading of\n // its minimum (the built-in gauge, whose readout already says there is no value, asks for it).\n emptyNeedle?: 'min' | 'none';\n}\n\n// The dial's geometry as fractions of its box: where its centre sits and how much of the short\n// side's half it fills. The DOM readout is placed from the same numbers (gaugeReadoutBox).\nconst GAUGE_CENTER_Y = 0.58;\nconst GAUGE_RADIUS = 0.95;\n// How far below the hub the readout sits, as a fraction of the radius.\nconst GAUGE_READOUT_OFFSET = 0.3;\n\n// The box (px, relative to the chart) the gauge's DOM readout is centred in and fitted to: under\n// the hub and inside the dial, where the canvas detail used to be drawn. A zero size (before the\n// first measure) takes the nominal one the builder assumes.\nexport function gaugeReadoutBox(size?: ElementSize): { centerX: number; centerY: number; width: number; height: number } {\n const s = shortSide(size);\n const w = (size?.width ?? 0) > 0 ? size!.width : s;\n const h = (size?.height ?? 0) > 0 ? size!.height : s;\n return {\n centerX: w / 2,\n centerY: GAUGE_CENTER_Y * h + GAUGE_READOUT_OFFSET * GAUGE_RADIUS * (s / 2),\n width: s * 0.7,\n height: s * 0.25,\n };\n}\n\n// Below this short side (px) the gauge drops its axis labels: at that size they would be\n// drawn at the 8 px floor, which is noise rather than a scale.\nexport const GAUGE_AXIS_LABELS_MIN_SIDE = 120;\n\n// The arc's colour stops: ECharts wants [[endFraction, colour], ...] ending at 1. A neutral\n// stretch (below the lowest threshold, or a `normal` band) takes the track colour. With no bands: one track.\nfunction gaugeArcColors(bands: ReturnType<typeof thresholdBands>, min: number, max: number, theme: ChartTheme): [number, string][] {\n if (bands.length === 0) return [[1, theme.border]];\n return bands.map((b, i): [number, string] => [\n i === bands.length - 1 ? 1 : (b.to - min) / (max - min),\n levelColor(theme.severity, b.level) ?? theme.border,\n ]);\n}\n\n// buildGaugeOption maps a single latest value to a gauge. A null value (nothing\n// received yet) shows an em dash and rests the needle at the minimum. Ticks, fonts,\n// and the arc width scale with the widget size so it stays legible when resized\n// (ECharts' default gauge crams ~10 labels at fixed fonts on a small canvas).\nexport function buildGaugeOption(\n value: number | null,\n theme: ChartTheme,\n options: GaugeChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const min = options.min ?? 0;\n const max = options.max ?? 100;\n const thresholds = parseThresholds(options.thresholds ?? []); // validated + sorted, as above\n const bands = thresholdBands(thresholds, min, max);\n // Banded only when a COLOURED band actually lands on the arc: thresholds that all sit above\n // `max`, or only `normal` bands, leave a plain neutral track, which keeps the progress arc\n // like any other gauge.\n const banded = bands.some((b) => isAlarmLevel(b.level));\n // The needle takes the colour of the band the value sits in; with no bands, below the\n // lowest, or in a `normal` band it keeps the chart accent.\n const accent = levelColor(theme.severity, bandAt(thresholds, value)?.level) ?? theme.series[0];\n\n const s = shortSide(size);\n const arcWidth = clamp(Math.round(s / 26), 4, 12);\n const scale = options.scale ?? 1;\n const labelFont = clamp(Math.round(s / 20), Math.round(8 * scale), Math.round(13 * scale));\n const showAxisLabels = s >= GAUGE_AXIS_LABELS_MIN_SIDE;\n const unitSuffix = options.unit ? ` ${options.unit}` : '';\n // The readout is fitted to the dial's inner box and its own length, like every big number.\n const readout = value == null ? '\u2014' : `${formatValue(value, options.precision)}${unitSuffix}`;\n const valueFont = fitValueFont({ width: s * 0.7, height: s * 0.25 }, readout.length, {\n floor: Math.round(FIT_MIN_FONT_PX * scale),\n });\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min,\n max,\n splitNumber: s < 160 ? 4 : 5, // fewer ticks than the default 10 \u2192 no cram\n radius: `${GAUGE_RADIUS * 100}%`,\n center: ['50%', `${GAUGE_CENTER_Y * 100}%`],\n // The progress arc would paint over the bands, so with bands the needle alone marks the value.\n progress: { show: !banded, width: arcWidth, itemStyle: { color: accent } },\n pointer: {\n show: value != null || options.emptyNeedle !== 'none',\n itemStyle: { color: accent },\n length: '55%',\n width: clamp(Math.round(s / 50), 2, 6),\n },\n anchor: { show: false },\n axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, min, max, theme) } },\n axisTick: { show: false },\n splitLine: { length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n axisLabel: { show: showAxisLabels, color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },\n detail:\n options.readout === 'none'\n ? { show: false }\n : {\n valueAnimation: false,\n color: theme.foreground,\n fontSize: valueFont,\n offsetCenter: [0, `${GAUGE_READOUT_OFFSET * 100}%`],\n formatter: (v: number) => (value == null ? '\u2014' : `${formatValue(v, options.precision)}${unitSuffix}`),\n },\n data: [{ value: value ?? min }],\n },\n ],\n };\n}\n\n// A heading in degrees folded onto [0, 360): 370 is 10 and -90 is 270, because a compass has no\n// \"beyond a full turn\" and a bearing sensor may well report either. 360 is 0, never 360, and a\n// value that is not a finite number has no heading at all (undefined), not a plausible one.\nexport function wrapHeading(value: number | null | undefined): number | undefined {\n if (value == null || !Number.isFinite(value)) return undefined;\n return ((value % 360) + 360) % 360;\n}\n\nexport interface CompassChartOptions {\n // The four point labels in N, E, S, W order (a locale writes West as \"O\", so the caller owns them).\n cardinals: readonly [string, string, string, string];\n thresholds?: readonly Threshold[];\n // The board's density scale; multiplies the label-font clamp. Default 1.\n scale?: number;\n}\n\n// buildCompassOption draws a full turn on the registered gauge series: north at the top, the\n// angle running clockwise, so 90 is east. The needle takes the colour of the band the (wrapped)\n// heading is in and the chart accent in a `normal` band or none. The degrees themselves are DOM\n// text beside it (compass.tsx). With no reading the needle rests at north.\nexport function buildCompassOption(\n value: number | null,\n theme: ChartTheme,\n options: CompassChartOptions,\n size?: ElementSize,\n): EChartOption {\n const heading = wrapHeading(value);\n const thresholds = parseThresholds(options.thresholds ?? []);\n const bands = thresholdBands(thresholds, 0, 360);\n const accent = levelColor(theme.severity, bandAt(thresholds, heading ?? null)?.level) ?? theme.series[0];\n const s = shortSide(size);\n const scale = options.scale ?? 1;\n const arcWidth = clamp(Math.round(s / 30), 3, 10);\n const labelFont = clamp(Math.round(s / 11), Math.round(10 * scale), Math.round(20 * scale));\n const [north, east, south, west] = options.cardinals;\n const points: Record<number, string> = { 0: north, 90: east, 180: south, 270: west };\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min: 0,\n max: 360,\n startAngle: 90,\n endAngle: -270,\n splitNumber: 4,\n radius: '92%',\n center: ['50%', '50%'],\n progress: { show: false },\n pointer: { itemStyle: { color: accent }, length: '62%', width: clamp(Math.round(s / 36), 3, 8) },\n anchor: { show: true, size: clamp(Math.round(s / 24), 5, 12), itemStyle: { color: accent } },\n axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, 0, 360, theme) } },\n axisTick: { show: s >= GAUGE_AXIS_LABELS_MIN_SIDE, splitNumber: 3, length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n splitLine: { length: arcWidth * 1.6, lineStyle: { color: theme.mutedForeground, width: 2 } },\n // Only the four points are labelled; 360 is the same tick as 0, so it stays blank.\n axisLabel: {\n show: true,\n color: theme.foreground,\n fontSize: labelFont,\n distance: arcWidth * 1.6 + 6,\n formatter: (v: number) => points[v] ?? '',\n },\n detail: { show: false },\n data: [{ value: heading ?? 0 }],\n },\n ],\n };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The chart colour theme: concrete colours resolved from the host's CSS custom properties,\n// because an ECharts canvas cannot resolve `var(--x)`. Pure DOM + React, no echarts import,\n// but it lives on the `/echarts` subpath with the rest of the chart helpers so the SDK root\n// stays chart-free.\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useWidgetThemeRoot } from '../context';\nimport { severityColorsAt, severityFallbackColors, type AlarmLevel } from '../severity';\n\n// Concrete colors for an ECharts canvas, resolved from the CSS variables in scope\n// at a given element (charts can't use CSS vars directly).\nexport interface ChartTheme {\n foreground: string;\n mutedForeground: string;\n border: string;\n series: string[];\n // The four coloured threshold levels, concrete (comma-form `hsl(h, s%, l%)`, which every\n // canvas colour parser accepts and which severityFill can add an alpha to). `normal` has\n // none: it renders in the widget's own neutral (see severity.ts).\n severity: Record<AlarmLevel, string>;\n}\n\n// Fallbacks used when a variable is absent (e.g. jsdom, or a host that hasn't set\n// the token) so a chart still renders in sensible default colors.\nconst FALLBACK: ChartTheme = {\n foreground: 'hsl(240 10% 3.9%)',\n mutedForeground: 'hsl(240 3.8% 46.1%)',\n border: 'hsl(240 5.9% 90%)',\n series: [\n 'hsl(220 70% 50%)',\n 'hsl(160 60% 45%)',\n 'hsl(30 80% 55%)',\n 'hsl(280 65% 60%)',\n 'hsl(340 75% 55%)',\n ],\n severity: severityFallbackColors(),\n};\n\nfunction readVar(style: CSSStyleDeclaration, name: string, fallback: string): string {\n const raw = style.getPropertyValue(`--${name}`).trim();\n return raw ? `hsl(${raw})` : fallback;\n}\n\n// resolveChartTheme reads the theme tokens in scope at `el` into concrete colors\n// for ECharts. Reading at the element (not the document root) means a widget\n// nested under a re-themed subtree picks up the local override.\nexport function resolveChartTheme(el: Element): ChartTheme {\n const style = getComputedStyle(el);\n return {\n foreground: readVar(style, 'foreground', FALLBACK.foreground),\n mutedForeground: readVar(style, 'muted-foreground', FALLBACK.mutedForeground),\n border: readVar(style, 'border', FALLBACK.border),\n series: FALLBACK.series.map((fallback, i) => readVar(style, `chart-${i + 1}`, fallback)),\n severity: severityColorsAt(el, style),\n };\n}\n\n// --- shared chart-theme store ------------------------------------------------\n//\n// Charts need concrete colors resolved from the CSS-variable theme. Rather than\n// have every chart widget mount its own MutationObserver on <html> and recompute\n// the same theme, one page-wide observer resolves it once per change and every\n// widget reads the shared, memoized value via useSyncExternalStore \u2014 so a busy\n// dashboard pays O(1), not O(number of charts), per theme toggle.\n\nlet sharedTheme: ChartTheme | null = null;\nconst themeListeners = new Set<() => void>();\nlet themeObserver: MutationObserver | null = null;\n\nfunction recomputeTheme(): void {\n sharedTheme = resolveChartTheme(document.documentElement);\n}\n\nfunction subscribeTheme(listener: () => void): () => void {\n if (!themeObserver) {\n recomputeTheme();\n themeObserver = new MutationObserver(() => {\n recomputeTheme();\n for (const l of themeListeners) l();\n });\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class', 'style'],\n });\n }\n themeListeners.add(listener);\n return () => {\n themeListeners.delete(listener);\n if (themeListeners.size === 0 && themeObserver) {\n themeObserver.disconnect();\n themeObserver = null;\n sharedTheme = null;\n }\n };\n}\n\nfunction getThemeSnapshot(): ChartTheme {\n if (!sharedTheme) recomputeTheme();\n return sharedTheme as ChartTheme;\n}\n\n// useChartTheme returns the current chart colors and re-renders the caller when\n// the document theme changes (a light/dark class or inline-style swap on <html>).\nexport function useChartTheme(): ChartTheme {\n const shared = useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeSnapshot);\n // A host that scopes its tokens to a container says so (WidgetThemeRoot); its tokens are\n // fixed for the container's life, so they are read once there rather than observed.\n const root = useWidgetThemeRoot();\n const scoped = useMemo(() => (root ? resolveChartTheme(root) : null), [root]);\n return scoped ?? shared;\n}\n\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,17 @@
1
+ export declare const FIT_MIN_FONT_PX = 12;
2
+ export declare const FIT_FALLBACK_FONT_PX = 32;
3
+ export declare const FIT_HEIGHT_SHARE = 0.6;
4
+ export declare const FIT_GLYPH_WIDTH = 0.65;
5
+ export interface FitOptions {
6
+ floor?: number;
7
+ }
8
+ export declare function fitValueFont(size: {
9
+ width: number;
10
+ height: number;
11
+ }, chars: number, options?: FitOptions): number;
12
+ export declare function fitValueFontCss(chars: number, options?: FitOptions): string;
13
+ export interface StickyChars {
14
+ value: number;
15
+ since: number;
16
+ }
17
+ export declare function stickyChars(prev: StickyChars | undefined, chars: number, now: number, holdMs: number): StickyChars;
package/dist/format.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export declare function formatTimestamp(iso: string | null, timeZone?: string): string;
2
2
  export declare function formatDateTime(iso: string | null, timeZone?: string): string;
3
+ export declare const DEFAULT_FRACTION_DIGITS = 3;
4
+ export declare const DEFAULT_SMALL_SIGNIFICANT = 4;
3
5
  export declare function formatValue(value: number | null | undefined, precision?: number): string;
package/dist/frame.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import { type CSSProperties, type ReactNode } from 'react';
2
+ import { type WidgetScale } from './scale';
3
+ export declare const FRAME_SUBTITLE_MIN_HEIGHT = 120;
4
+ export declare const FRAME_TITLE_MIN_HEIGHT = 76;
2
5
  export interface WidgetFrameProps {
3
6
  title?: string;
4
7
  subtitle?: string;
5
- children: ReactNode;
8
+ children: ReactNode | ((scale: WidgetScale) => ReactNode);
6
9
  bodyStyle?: CSSProperties;
10
+ flush?: boolean;
11
+ sampleTime?: string | null;
7
12
  }
8
- export declare function WidgetFrame({ title, subtitle, children, bodyStyle }: WidgetFrameProps): import("react").JSX.Element;
13
+ export declare function WidgetFrame({ title, subtitle, children, bodyStyle, flush, sampleTime }: WidgetFrameProps): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { type CSSProperties } from 'react';
2
+ export declare function useNow(everyMs?: number, enabled?: boolean): number;
3
+ export interface FreshnessReading {
4
+ age: number;
5
+ stale: boolean;
6
+ }
7
+ export declare function useFreshness(at: number | undefined, staleAfterSeconds: number | undefined): FreshnessReading | undefined;
8
+ export declare function StaleBadge({ label }: {
9
+ label?: string;
10
+ }): import("react").JSX.Element;
11
+ export interface FreshnessProps {
12
+ at: number | undefined;
13
+ staleAfterSeconds?: number;
14
+ staleLabel?: string;
15
+ style?: CSSProperties;
16
+ }
17
+ export declare function Freshness({ at, staleAfterSeconds, staleLabel, style }: FreshnessProps): import("react").JSX.Element | null;
18
+ export declare function FreshnessReadout({ reading, staleLabel, style, }: {
19
+ reading: FreshnessReading;
20
+ staleLabel?: string;
21
+ style?: CSSProperties;
22
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import type { MeasurementSample } from '@devicechain/dashboards';
2
+ export declare function resolveStaleAfterSeconds(options: Record<string, unknown> | undefined): number | undefined;
3
+ export declare function newestSampleTime(latest: Record<string, MeasurementSample>, names?: readonly string[]): number | undefined;
4
+ export declare function ageSeconds(at: number, now: number): number;
5
+ export declare function isStale(age: number, staleAfter: number | undefined): boolean;
6
+ export declare function viewerLocale(): string | undefined;
7
+ export declare function formatAge(age: number, locale?: string | undefined): string;
8
+ export interface FreshnessStrings {
9
+ stale: string;
10
+ ageLabel: string;
11
+ }
12
+ export declare const FRESHNESS_STRINGS: Record<'en' | 'es', FreshnessStrings>;
13
+ export declare function freshnessStrings(locale?: string | undefined): FreshnessStrings;
package/dist/hooks.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { AlarmSubscription, CommandSubscription, DashboardDefinition, DatasourceSelector, DeviceResolver, EntityCandidateLister, LocationSubscription, MemberResolver, MeasurementSample, SelectionCandidate, SlotBinding, WidgetDataSource } from '@devicechain/dashboards';
2
- import type { AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState } from '@devicechain/dashboards';
1
+ import type { AlarmSubscription, CommandSubscription, DashboardDefinition, DatasourceSelector, DeviceResolver, EntityCandidateLister, HubConnectionState, LocationSubscription, MemberResolver, MeasurementSample, SelectionCandidate, SlotBinding, WidgetDataSource } from '@devicechain/dashboards';
2
+ import type { AggregateStreamState, AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState } from '@devicechain/dashboards';
3
3
  import { DashboardHub } from '@devicechain/dashboards';
4
4
  import { type RefObject } from 'react';
5
5
  import { type WidgetCandidates } from './context';
6
- export type { AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState };
6
+ export type { AggregateStreamState, AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState };
7
7
  export declare function useDashboardHub(resolver: DeviceResolver, authorities: string[] | undefined): DashboardHub;
8
8
  export declare function useResolvedBindings(definition: DashboardDefinition, base: Record<string, SlotBinding>, selection: Record<string, SlotBinding>, resolver: MemberResolver): Record<string, SlotBinding>;
9
9
  export declare function useSlotCandidates(definition: DashboardDefinition, bindings: Record<string, SlotBinding>, resolver: MemberResolver, lister: EntityCandidateLister | undefined): WidgetCandidates | undefined;
@@ -16,8 +16,13 @@ export interface CandidatesState {
16
16
  export declare function useCandidates(slot: string | undefined): CandidatesState;
17
17
  export interface MeasurementStreamOptions {
18
18
  window?: number;
19
+ windowSeconds?: number;
20
+ pausedAt?: number;
21
+ now?: () => number;
19
22
  initialSamples?: MeasurementSample[];
20
23
  }
24
+ export declare const MAX_SPAN_SAMPLES = 2000;
25
+ export declare function withinSpan(samples: MeasurementSample[], spanSeconds: number, end: number): MeasurementSample[];
21
26
  export declare function useMeasurementStream(hub: WidgetDataSource, datasource: DatasourceSelector | undefined, options?: MeasurementStreamOptions): MeasurementStreamState;
22
27
  export declare function useAlarmStream(hub: WidgetDataSource, subscription: AlarmSubscription): AlarmStreamState;
23
28
  export declare function useCommandStream(hub: WidgetDataSource, subscription: CommandSubscription): CommandStreamState;
@@ -28,4 +33,9 @@ export interface ElementSize {
28
33
  width: number;
29
34
  height: number;
30
35
  }
31
- export declare function useElementSize<T extends HTMLElement>(): [RefObject<T | null>, ElementSize];
36
+ export declare function useElementSize<T extends HTMLElement>(step?: number): [RefObject<T | null>, ElementSize];
37
+ export interface ConnectionStateSource {
38
+ readonly connectionState: HubConnectionState;
39
+ onConnectionState(listener: () => void): () => void;
40
+ }
41
+ export declare function useHubConnectionState(hub: Partial<ConnectionStateSource> | undefined): HubConnectionState;
package/dist/index.d.ts CHANGED
@@ -2,11 +2,19 @@ export { defineWidget } from './define-widget';
2
2
  export type { ErasedComponent, ErasedWidgetDefinition, WidgetDefinition, WidgetImplementation } from './define-widget';
3
3
  export { createWidgetRegistry, WidgetRegistryError, type WidgetRegistry } from './registry';
4
4
  export type { EditorCommandChoice, EditorCommandChoices, WidgetComponent, WidgetEditorProps, WidgetEditorServices, WidgetProps, } from './widget';
5
- export { WidgetCandidatesProvider, WidgetSelectProvider, WidgetSubjectProvider, WidgetThemeRootProvider, WidgetTimeZoneProvider, useWidgetCandidates, useWidgetSelect, useWidgetSubject, useWidgetThemeRoot, useWidgetTimeZone, type WidgetCandidates, type WidgetSelect, } from './context';
5
+ export { WidgetCandidatesProvider, WidgetDensityProvider, WidgetFrameStyleProvider, WidgetFreshnessProvider, WidgetSelectProvider, WidgetSubjectProvider, WidgetThemeRootProvider, WidgetTimeRangeProvider, WidgetTimeZoneProvider, useWidgetCandidates, useWidgetFrameStyle, useWidgetFreshness, useWidgetSelect, useWidgetSubject, useWidgetThemeRoot, useWidgetTimeRange, useWidgetTimeZone, type WidgetBodySize, type WidgetCandidates, type WidgetFreshness, type WidgetSelect, } from './context';
6
6
  export { WidgetFrame, type WidgetFrameProps } from './frame';
7
7
  export { css, type ThemeVar } from './theme';
8
+ export { FIT_MIN_FONT_PX, fitValueFont, fitValueFontCss, type FitOptions } from './fit-text';
9
+ export { FONT_TOKENS, SPACE_TOKENS, WidgetTokenStyles, densityScale, fontVar, spaceVar, tokenPx, tokenStyle, type FontToken, type SizeToken, type SpaceToken, } from './tokens';
10
+ export { SIZE_CLASS_BREAKS, useStickyChars, useWidgetScale, type SizeClass, type WidgetScale } from './scale';
11
+ export { HubConnectionBadge, type HubConnectionBadgeProps } from './connection-badge';
12
+ export { Freshness, FreshnessReadout, StaleBadge, useFreshness, useNow, type FreshnessProps, type FreshnessReading } from './freshness-readout';
13
+ export { ageSeconds, formatAge, freshnessStrings, isStale, newestSampleTime, resolveStaleAfterSeconds, viewerLocale, } from './freshness';
8
14
  export { formatDateTime, formatTimestamp, formatValue } from './format';
15
+ export { ALARM_LEVELS, SEVERITY_LEVELS, SEVERITY_TOKENS, isAlarmLevel, isSeverityLevel, levelColor, severityTokenCss, severityVar, useSeverityColors, type AlarmLevel, type SeverityLevel, } from './severity';
16
+ export { bandAt, optThresholds, parseThresholds, thresholdBands, type Threshold, type ThresholdBand, } from './thresholds';
9
17
  export { FlashValue, flashBackgroundStyle, flashTextStyle, useFlashOnChange, type FlashDirection, } from './flash';
10
18
  export { optBoolean, optNumber, optString, pickSample, primaryMeasurementName } from './widget-options';
11
- export { useAlarmStream, useCandidates, useCommandStream, useDashboardHub, useDatasourceAvailability, useElementSize, useLocationStream, useMeasurementStream, useResolvedBindings, useSlotCandidates, type AlarmStreamState, type CandidatesState, type CommandStreamState, type DatasourceAvailability, type ElementSize, type LocationStreamState, type MeasurementStreamOptions, type MeasurementStreamState, } from './hooks';
12
- export { SDK_API_VERSION, WidgetCatalogError, defineWidgetDescriptor, isBuiltinWidgetId, isNamespacedWidgetId, validateWidgetDescriptor, type AckOptionRef, type BooleanOptionSpec, type BuiltinWidgetId, type ChannelBindingPolicy, type ChannelSnapshot, type ConstBinding, type DatasourcePolicy, type DatasourceSelectorKind, type DefaultsOf, type DescriptorIssue, type EnumOptionSpec, type GreaterThanConstraint, type NamespacedWidgetId, type NumberOptionRef, type NumberOptionSpec, type OptionConstraint, type OptionSpec, type OptionsOf, type StringOptionRef, type StringOptionSpec, type WidgetCatalog, type WidgetChannel, type WidgetDescriptor, type WidgetDisplay, type WidgetId, type WidgetInteractionDeclaration, type WidgetOptionSpecs, } from '@devicechain/dashboards';
19
+ export { useAlarmStream, useCandidates, useCommandStream, useDashboardHub, useDatasourceAvailability, useElementSize, useHubConnectionState, useLocationStream, useMeasurementStream, MAX_SPAN_SAMPLES, withinSpan, useResolvedBindings, useSlotCandidates, type AggregateStreamState, type AlarmStreamState, type CandidatesState, type CommandStreamState, type ConnectionStateSource, type DatasourceAvailability, type ElementSize, type LocationStreamState, type MeasurementStreamOptions, type MeasurementStreamState, } from './hooks';
20
+ export { SDK_API_VERSION, WidgetCatalogError, defineWidgetDescriptor, isBuiltinWidgetId, isNamespacedWidgetId, validateWidgetDescriptor, type AckOptionRef, type BooleanOptionSpec, type BuiltinWidgetId, type ChannelBindingPolicy, type ChannelSnapshot, type ConstBinding, type DatasourcePolicy, type DatasourceSelectorKind, type DefaultsOf, type DescriptorIssue, type EnumOptionSpec, type ListItemFieldSpec, type ListOptionSpec, type GreaterThanConstraint, type HubConnectionState, type NamespacedWidgetId, type NumberOptionRef, type NumberOptionSpec, type OptionConstraint, type OptionSpec, type OptionsOf, type StringOptionRef, type StringOptionSpec, type WidgetCatalog, type WidgetChannel, type WidgetDescriptor, type WidgetDisplay, type WidgetId, type WidgetInteractionDeclaration, type WidgetOptionSpecs, } from '@devicechain/dashboards';