@squadbase/vantage 0.6.2-dev1.5aae04e → 0.6.2-dev1.72360fb
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/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-57GHR5LN.js +163 -0
- package/dist/chunk-57GHR5LN.js.map +1 -0
- package/dist/chunk-6OTTGAPO.js +44 -0
- package/dist/chunk-6OTTGAPO.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
- package/dist/chunk-7S52M2SL.js.map +1 -0
- package/dist/chunk-JXFCSF3K.js +673 -0
- package/dist/chunk-JXFCSF3K.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-QOXXOFGL.js} +61 -929
- package/dist/chunk-QOXXOFGL.js.map +1 -0
- package/dist/components/index.d.ts +11 -247
- package/dist/components/index.js +5 -2
- package/dist/delta-text-CvwBGJ95.d.ts +15 -0
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +300 -3
- package/dist/doc/index.js.map +1 -1
- package/dist/format-U0cQl069.d.ts +243 -0
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +966 -1
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +5 -2
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +4 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/index.js.map +1 -1
- package/docs/en/components.md +13 -2
- package/docs/en/doc/delta-text.md +40 -0
- package/docs/en/doc/doc-action-items.md +49 -0
- package/docs/en/doc/doc-callout.md +38 -0
- package/docs/en/doc/doc-data-table.md +60 -0
- package/docs/en/doc/doc-executive-summary.md +64 -0
- package/docs/en/doc/doc-figure.md +56 -0
- package/docs/en/doc/doc-key-findings.md +61 -0
- package/docs/en/doc/doc-kpi-scorecard.md +80 -0
- package/docs/en/doc/doc-notes-and-sources.md +43 -0
- package/docs/en/doc/doc-report-header.md +46 -0
- package/docs/en/parts/area-chart.md +99 -0
- package/docs/en/parts/bar-chart.md +91 -0
- package/docs/en/parts/bar-list.md +50 -0
- package/docs/en/parts/category-bar.md +37 -0
- package/docs/en/parts/chart-frame.md +39 -0
- package/docs/en/parts/delta.md +4 -3
- package/docs/en/parts/donut-chart.md +72 -0
- package/docs/en/parts/funnel-steps.md +2 -1
- package/docs/en/parts/heatmap.md +53 -0
- package/docs/en/parts/kpi-card.md +2 -1
- package/docs/en/parts/legend.md +35 -0
- package/docs/en/parts/line-chart.md +94 -0
- package/docs/en/parts/progress-circle.md +33 -0
- package/docs/en/parts/sample-data.md +39 -0
- package/docs/en/parts/scatter-chart.md +56 -0
- package/docs/en/parts/tracker.md +36 -0
- package/docs/en/parts/updated-at.md +30 -0
- package/docs/en/parts/waterfall-chart.md +56 -0
- package/docs/en/slide/slide-chart-full.md +48 -0
- package/docs/en/slide/slide-chart-insight.md +52 -0
- package/docs/en/slide/slide-columns.md +46 -0
- package/docs/en/slide/slide-comparison.md +68 -0
- package/docs/en/slide/slide-cover.md +39 -0
- package/docs/en/slide/slide-exec-summary.md +47 -0
- package/docs/en/slide/slide-heading.md +38 -0
- package/docs/en/slide/slide-kpi-grid.md +55 -0
- package/docs/en/slide/slide-kpi-hero.md +58 -0
- package/docs/en/slide/slide-next-actions.md +49 -0
- package/docs/en/slide/slide-scorecard-table.md +54 -0
- package/docs/en/slide/slide-section.md +44 -0
- package/docs/en/slide/slide-source-line.md +34 -0
- package/docs/en/ui/alert-dialog.md +42 -0
- package/docs/en/ui/avatar.md +31 -0
- package/docs/en/ui/combobox.md +74 -0
- package/docs/en/ui/drawer.md +41 -0
- package/docs/en/ui/empty.md +1 -1
- package/docs/en/ui/error-state.md +4 -3
- package/docs/en/ui/field.md +42 -0
- package/docs/en/ui/hover-card.md +21 -0
- package/docs/en/ui/kbd.md +17 -0
- package/docs/en/ui/loading.md +1 -1
- package/docs/en/ui/pagination.md +36 -0
- package/docs/en/ui/radio-group.md +21 -0
- package/docs/en/ui/toast.md +44 -0
- package/docs/en/ui/toggle-group.md +4 -3
- package/docs/index.json +857 -77
- package/docs/ja/components.md +12 -1
- package/docs/ja/doc/delta-text.md +40 -0
- package/docs/ja/doc/doc-action-items.md +49 -0
- package/docs/ja/doc/doc-callout.md +38 -0
- package/docs/ja/doc/doc-data-table.md +60 -0
- package/docs/ja/doc/doc-executive-summary.md +64 -0
- package/docs/ja/doc/doc-figure.md +56 -0
- package/docs/ja/doc/doc-key-findings.md +61 -0
- package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
- package/docs/ja/doc/doc-notes-and-sources.md +43 -0
- package/docs/ja/doc/doc-report-header.md +46 -0
- package/docs/ja/parts/area-chart.md +49 -25
- package/docs/ja/parts/bar-chart.md +50 -24
- package/docs/ja/parts/bar-list.md +31 -8
- package/docs/ja/parts/chart-frame.md +36 -0
- package/docs/ja/parts/delta.md +5 -4
- package/docs/ja/parts/donut-chart.md +45 -26
- package/docs/ja/parts/funnel-steps.md +27 -8
- package/docs/ja/parts/heatmap.md +45 -0
- package/docs/ja/parts/kpi-card.md +2 -1
- package/docs/ja/parts/legend.md +36 -0
- package/docs/ja/parts/line-chart.md +48 -22
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -0
- package/docs/ja/parts/updated-at.md +27 -0
- package/docs/ja/parts/waterfall-chart.md +61 -0
- package/docs/ja/slide/slide-chart-full.md +48 -0
- package/docs/ja/slide/slide-chart-insight.md +52 -0
- package/docs/ja/slide/slide-columns.md +46 -0
- package/docs/ja/slide/slide-comparison.md +68 -0
- package/docs/ja/slide/slide-cover.md +39 -0
- package/docs/ja/slide/slide-exec-summary.md +47 -0
- package/docs/ja/slide/slide-heading.md +38 -0
- package/docs/ja/slide/slide-kpi-grid.md +55 -0
- package/docs/ja/slide/slide-kpi-hero.md +58 -0
- package/docs/ja/slide/slide-next-actions.md +49 -0
- package/docs/ja/slide/slide-scorecard-table.md +54 -0
- package/docs/ja/slide/slide-section.md +44 -0
- package/docs/ja/slide/slide-source-line.md +34 -0
- package/docs/ja/ui/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- package/docs/ja/ui/loading.md +1 -1
- package/docs/ja/ui/toggle-group.md +4 -3
- package/package.json +2 -2
- package/registry/ui/area-chart.tsx +45 -19
- package/registry/ui/bar-chart.tsx +45 -19
- package/registry/ui/bar-list.tsx +44 -17
- package/registry/ui/box-plot-chart.tsx +45 -19
- package/registry/ui/category-bar.tsx +44 -17
- package/registry/ui/chart-frame.tsx +44 -17
- package/registry/ui/delta.tsx +11 -8
- package/registry/ui/donut-chart.tsx +46 -19
- package/registry/ui/echart.tsx +171 -54
- package/registry/ui/heatmap.tsx +45 -19
- package/registry/ui/histogram-chart.tsx +45 -19
- package/registry/ui/kpi-card.tsx +9 -4
- package/registry/ui/legend.tsx +44 -17
- package/registry/ui/line-chart.tsx +45 -19
- package/registry/ui/page-shell.tsx +7 -7
- package/registry/ui/paired-chart.tsx +45 -19
- package/registry/ui/sample-data.tsx +1 -1
- package/registry/ui/scatter-chart.tsx +45 -19
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/status-badge.tsx +8 -8
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +9 -3
- package/registry/ui/trend-indicator.tsx +2 -2
- package/registry/ui/waterfall-chart.tsx +45 -19
- package/skills/vantage-design/SKILL.md +27 -25
- package/skills/vantage-design/references/extract.md +16 -14
- package/skills/vantage-design/references/format.md +6 -5
- package/skills/vantage-design/references/presets.md +60 -110
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +2 -2
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +22 -1
- package/templates/agents/slide.md +70 -2
- package/theme.css +16 -15
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
|
@@ -22,7 +22,7 @@ import { forwardRef, useEffect, useMemo, useState, type ForwardedRef, type React
|
|
|
22
22
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
23
23
|
import { EChart } from "@squadbase/vantage/components"
|
|
24
24
|
import type { ChartCommonProps, ChartEventPayload, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
25
|
-
import { createContext, useCallback, useRef } from "react"
|
|
25
|
+
import { createContext, useCallback, useRef, type RefObject } from "react"
|
|
26
26
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
27
27
|
import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
28
28
|
import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
|
|
@@ -64,10 +64,23 @@ interface ChartTokens {
|
|
|
64
64
|
border: string | null
|
|
65
65
|
/** Tooltip surface. */
|
|
66
66
|
popover: string | null
|
|
67
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
68
|
+
colors?: Record<string, string>
|
|
69
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
70
|
+
surface?: string | null
|
|
67
71
|
/** Tooltip text. */
|
|
68
72
|
popoverForeground: string | null
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
76
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
77
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
78
|
+
const OPTION_COLOR_TOKENS = [
|
|
79
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
80
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
81
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
82
|
+
]
|
|
83
|
+
|
|
71
84
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
72
85
|
|
|
73
86
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -123,8 +136,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
123
136
|
const color = read(token)
|
|
124
137
|
if (color) series.push(color)
|
|
125
138
|
}
|
|
126
|
-
|
|
139
|
+
const tokens: ChartTokens = {
|
|
127
140
|
series,
|
|
141
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
142
|
+
surface: readChartSurface(target),
|
|
128
143
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
129
144
|
foreground: read("--foreground"),
|
|
130
145
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -132,6 +147,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
132
147
|
popover: read("--popover"),
|
|
133
148
|
popoverForeground: read("--popover-foreground"),
|
|
134
149
|
}
|
|
150
|
+
tokenScopes.set(tokens, target)
|
|
151
|
+
return tokens
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -139,6 +156,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
139
156
|
if (a === b) return true
|
|
140
157
|
if (!a || !b) return false
|
|
141
158
|
return (
|
|
159
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
160
|
+
a.surface === b.surface &&
|
|
161
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
162
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
142
163
|
a.series.length === b.series.length &&
|
|
143
164
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
144
165
|
a.fontFamily === b.fontFamily &&
|
|
@@ -164,7 +185,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
164
185
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
165
186
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
166
187
|
*/
|
|
167
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
188
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
168
189
|
if (typeof document === "undefined") return () => {}
|
|
169
190
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
170
191
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -179,8 +200,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
179
200
|
|
|
180
201
|
const themeObserver = new MutationObserver(schedule)
|
|
181
202
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
204
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
205
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
206
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
207
|
+
}
|
|
184
208
|
|
|
185
209
|
const styleObserver = new MutationObserver(schedule)
|
|
186
210
|
if (document.head) {
|
|
@@ -268,22 +292,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
268
292
|
* components that build an ECharts option from token colours themselves (the
|
|
269
293
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
270
294
|
*/
|
|
271
|
-
function useChartTokens(): ChartTokens | null {
|
|
295
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
272
296
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
273
297
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
274
298
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
275
299
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
276
300
|
const tokensRef = useRef(tokens)
|
|
277
301
|
useEffect(() => {
|
|
302
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
278
303
|
const sync = () => {
|
|
279
|
-
const next = readChartTokens()
|
|
304
|
+
const next = readChartTokens(target)
|
|
280
305
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
281
306
|
tokensRef.current = next
|
|
282
307
|
setTokens(next)
|
|
283
308
|
}
|
|
284
309
|
sync()
|
|
285
|
-
return watchChartTokens(sync)
|
|
286
|
-
}, [])
|
|
310
|
+
return watchChartTokens(sync, target)
|
|
311
|
+
}, [scope])
|
|
287
312
|
return tokens
|
|
288
313
|
}
|
|
289
314
|
|
|
@@ -295,7 +320,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
295
320
|
/**
|
|
296
321
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
297
322
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
298
|
-
* variants `"positive-fill"`, …) are read
|
|
323
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
299
324
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
300
325
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
301
326
|
*/
|
|
@@ -306,12 +331,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
306
331
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
307
332
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
308
333
|
if (typeof document === "undefined") return ""
|
|
309
|
-
|
|
334
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
335
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
310
336
|
}
|
|
311
337
|
if (typeof document === "undefined") return color
|
|
312
338
|
let value = color.trim()
|
|
313
339
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
314
|
-
const styles = getComputedStyle(document.documentElement)
|
|
340
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
315
341
|
if (cssVar) {
|
|
316
342
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
317
343
|
} else if (value.includes("var(")) {
|
|
@@ -357,7 +383,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
357
383
|
if (color) return color
|
|
358
384
|
}
|
|
359
385
|
}
|
|
360
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
386
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
361
387
|
}
|
|
362
388
|
|
|
363
389
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -371,9 +397,10 @@ const WIDTH_BUCKET = 40
|
|
|
371
397
|
*/
|
|
372
398
|
function useChartSurface<E extends Element>(
|
|
373
399
|
forwarded: ForwardedRef<E>,
|
|
374
|
-
tokens
|
|
375
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
400
|
+
tokens?: ChartTokens | null,
|
|
401
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
376
402
|
const [node, setNode] = useState<E | null>(null)
|
|
403
|
+
const scopedTokens = useChartTokens(node)
|
|
377
404
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
378
405
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
379
406
|
const ref = useCallback(
|
|
@@ -387,7 +414,7 @@ function useChartSurface<E extends Element>(
|
|
|
387
414
|
useEffect(() => {
|
|
388
415
|
if (!node) return
|
|
389
416
|
setSurface(readChartSurface(node) ?? undefined)
|
|
390
|
-
}, [node, tokens])
|
|
417
|
+
}, [node, tokens, scopedTokens])
|
|
391
418
|
useEffect(() => {
|
|
392
419
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
393
420
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -396,7 +423,7 @@ function useChartSurface<E extends Element>(
|
|
|
396
423
|
observer.observe(node)
|
|
397
424
|
return () => observer.disconnect()
|
|
398
425
|
}, [node])
|
|
399
|
-
return { ref, surface, width }
|
|
426
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
400
427
|
}
|
|
401
428
|
|
|
402
429
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -3579,7 +3606,7 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3579
3606
|
const showLegend = donutOptions?.showLegend ?? true
|
|
3580
3607
|
const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format), [valueFormatterProp, format])
|
|
3581
3608
|
|
|
3582
|
-
const tokens =
|
|
3609
|
+
const { ref: frameRef, tokens } = useChartSurface(ref)
|
|
3583
3610
|
const slices = useMemo(
|
|
3584
3611
|
() => donutSlices(data, { index: labelKey, value: valueKey, colors, maxSlices, otherColor: CHART_OTHER_COLOR }),
|
|
3585
3612
|
[data, labelKey, valueKey, colors, maxSlices],
|
|
@@ -3661,7 +3688,7 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3661
3688
|
return (
|
|
3662
3689
|
<ChartFrame
|
|
3663
3690
|
{...chartRootProps(rest)}
|
|
3664
|
-
ref={
|
|
3691
|
+
ref={frameRef}
|
|
3665
3692
|
title={title}
|
|
3666
3693
|
summary={resolvedSummary}
|
|
3667
3694
|
isLoading={isLoading ?? loading ?? false}
|
package/registry/ui/echart.tsx
CHANGED
|
@@ -58,7 +58,7 @@ import { Check, Copy, Download } from "lucide-react"
|
|
|
58
58
|
import { cn } from "@squadbase/vantage/ui"
|
|
59
59
|
import { Button } from "@squadbase/vantage/ui"
|
|
60
60
|
import type { ChartActionsOptions } from "@squadbase/vantage/components"
|
|
61
|
-
import { createContext, type ForwardedRef } from "react"
|
|
61
|
+
import { createContext, type ForwardedRef, type RefObject } from "react"
|
|
62
62
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
63
63
|
|
|
64
64
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -95,10 +95,23 @@ interface ChartTokens {
|
|
|
95
95
|
border: string | null
|
|
96
96
|
/** Tooltip surface. */
|
|
97
97
|
popover: string | null
|
|
98
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
99
|
+
colors?: Record<string, string>
|
|
100
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
101
|
+
surface?: string | null
|
|
98
102
|
/** Tooltip text. */
|
|
99
103
|
popoverForeground: string | null
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
107
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
108
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
109
|
+
const OPTION_COLOR_TOKENS = [
|
|
110
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
111
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
112
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
113
|
+
]
|
|
114
|
+
|
|
102
115
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
103
116
|
|
|
104
117
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -154,8 +167,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
154
167
|
const color = read(token)
|
|
155
168
|
if (color) series.push(color)
|
|
156
169
|
}
|
|
157
|
-
|
|
170
|
+
const tokens: ChartTokens = {
|
|
158
171
|
series,
|
|
172
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
173
|
+
surface: readChartSurface(target),
|
|
159
174
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
160
175
|
foreground: read("--foreground"),
|
|
161
176
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -163,6 +178,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
163
178
|
popover: read("--popover"),
|
|
164
179
|
popoverForeground: read("--popover-foreground"),
|
|
165
180
|
}
|
|
181
|
+
tokenScopes.set(tokens, target)
|
|
182
|
+
return tokens
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -170,6 +187,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
170
187
|
if (a === b) return true
|
|
171
188
|
if (!a || !b) return false
|
|
172
189
|
return (
|
|
190
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
191
|
+
a.surface === b.surface &&
|
|
192
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
193
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
173
194
|
a.series.length === b.series.length &&
|
|
174
195
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
175
196
|
a.fontFamily === b.fontFamily &&
|
|
@@ -195,7 +216,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
195
216
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
196
217
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
197
218
|
*/
|
|
198
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
219
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
199
220
|
if (typeof document === "undefined") return () => {}
|
|
200
221
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
201
222
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -210,8 +231,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
210
231
|
|
|
211
232
|
const themeObserver = new MutationObserver(schedule)
|
|
212
233
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
235
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
236
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
237
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
238
|
+
}
|
|
215
239
|
|
|
216
240
|
const styleObserver = new MutationObserver(schedule)
|
|
217
241
|
if (document.head) {
|
|
@@ -299,22 +323,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
299
323
|
* components that build an ECharts option from token colours themselves (the
|
|
300
324
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
301
325
|
*/
|
|
302
|
-
function useChartTokens(): ChartTokens | null {
|
|
326
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
303
327
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
304
328
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
305
329
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
306
330
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
307
331
|
const tokensRef = useRef(tokens)
|
|
308
332
|
useEffect(() => {
|
|
333
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
309
334
|
const sync = () => {
|
|
310
|
-
const next = readChartTokens()
|
|
335
|
+
const next = readChartTokens(target)
|
|
311
336
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
312
337
|
tokensRef.current = next
|
|
313
338
|
setTokens(next)
|
|
314
339
|
}
|
|
315
340
|
sync()
|
|
316
|
-
return watchChartTokens(sync)
|
|
317
|
-
}, [])
|
|
341
|
+
return watchChartTokens(sync, target)
|
|
342
|
+
}, [scope])
|
|
318
343
|
return tokens
|
|
319
344
|
}
|
|
320
345
|
|
|
@@ -326,7 +351,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
326
351
|
/**
|
|
327
352
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
328
353
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
329
|
-
* variants `"positive-fill"`, …) are read
|
|
354
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
330
355
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
331
356
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
332
357
|
*/
|
|
@@ -337,12 +362,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
337
362
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
338
363
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
339
364
|
if (typeof document === "undefined") return ""
|
|
340
|
-
|
|
365
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
366
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
341
367
|
}
|
|
342
368
|
if (typeof document === "undefined") return color
|
|
343
369
|
let value = color.trim()
|
|
344
370
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
345
|
-
const styles = getComputedStyle(document.documentElement)
|
|
371
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
346
372
|
if (cssVar) {
|
|
347
373
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
348
374
|
} else if (value.includes("var(")) {
|
|
@@ -388,7 +414,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
388
414
|
if (color) return color
|
|
389
415
|
}
|
|
390
416
|
}
|
|
391
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
417
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
392
418
|
}
|
|
393
419
|
|
|
394
420
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -402,9 +428,10 @@ const WIDTH_BUCKET = 40
|
|
|
402
428
|
*/
|
|
403
429
|
function useChartSurface<E extends Element>(
|
|
404
430
|
forwarded: ForwardedRef<E>,
|
|
405
|
-
tokens
|
|
406
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
431
|
+
tokens?: ChartTokens | null,
|
|
432
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
407
433
|
const [node, setNode] = useState<E | null>(null)
|
|
434
|
+
const scopedTokens = useChartTokens(node)
|
|
408
435
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
409
436
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
410
437
|
const ref = useCallback(
|
|
@@ -418,7 +445,7 @@ function useChartSurface<E extends Element>(
|
|
|
418
445
|
useEffect(() => {
|
|
419
446
|
if (!node) return
|
|
420
447
|
setSurface(readChartSurface(node) ?? undefined)
|
|
421
|
-
}, [node, tokens])
|
|
448
|
+
}, [node, tokens, scopedTokens])
|
|
422
449
|
useEffect(() => {
|
|
423
450
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
424
451
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -427,7 +454,7 @@ function useChartSurface<E extends Element>(
|
|
|
427
454
|
observer.observe(node)
|
|
428
455
|
return () => observer.disconnect()
|
|
429
456
|
}, [node])
|
|
430
|
-
return { ref, surface, width }
|
|
457
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
431
458
|
}
|
|
432
459
|
|
|
433
460
|
/* ---------- end of inlined code ---------- */
|
|
@@ -664,6 +691,116 @@ export function echartSetOptionOpts(prev: EChartsOption | null, next: EChartsOpt
|
|
|
664
691
|
return { notMerge: false, replaceMerge: [...types], lazyUpdate: true }
|
|
665
692
|
}
|
|
666
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Component types that get a generated `id` when the option has none, so replaceMerge can match them across updates.
|
|
696
|
+
* Not `dataset` / `graphic` / `tooltip`: they are rebuilt on every update (there is nothing to transition).
|
|
697
|
+
*/
|
|
698
|
+
const ID_TYPES = [
|
|
699
|
+
"title", "legend", "grid", "xAxis", "yAxis", "polar", "radiusAxis", "angleAxis", "radar",
|
|
700
|
+
"dataZoom", "visualMap", "geo", "singleAxis", "calendar",
|
|
701
|
+
] as const
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* What `withStableIds` remembers between updates: per generated id, the shape last applied and how many
|
|
705
|
+
* times it changed (the id's `~v<n>` suffix); per component type, the order of its ids.
|
|
706
|
+
*/
|
|
707
|
+
export interface StableIdState {
|
|
708
|
+
ids: Map<string, { shape: string; version: number }>
|
|
709
|
+
order: Map<string, string[]>
|
|
710
|
+
}
|
|
711
|
+
export const createStableIdState = (): StableIdState => ({ ids: new Map(), order: new Map() })
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Shape of a component: which keys it has, recursively (arrays only as "array", values not at all —
|
|
715
|
+
* except `type`). A merge keeps keys the new option dropped, so a changed shape must not be merged.
|
|
716
|
+
*/
|
|
717
|
+
function shapeOf(v: unknown): string {
|
|
718
|
+
if (Array.isArray(v)) return "[]"
|
|
719
|
+
if (!v || typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype) return "."
|
|
720
|
+
const o = v as Record<string, unknown>
|
|
721
|
+
return `{${Object.keys(o)
|
|
722
|
+
.filter((k) => o[k] !== undefined)
|
|
723
|
+
.sort()
|
|
724
|
+
.map((k) => (k === "type" || k === "id" ? `${k}=${String(o[k])}` : `${k}:${shapeOf(o[k])}`))
|
|
725
|
+
.join(",")}}`
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Gives every series (and the common components) an `id` it does not have yet —
|
|
730
|
+
* series: `series:<name ?? index>`, others `<type>:<index>` — so an update that
|
|
731
|
+
* only changes values is matched to the existing component and transitions
|
|
732
|
+
* instead of being rebuilt. If the component's shape changed since `state` last
|
|
733
|
+
* saw it (a key was removed or added, the type changed), the id gets a new
|
|
734
|
+
* `~v<n>` suffix so replaceMerge builds it from scratch: the result then always
|
|
735
|
+
* equals applying the option with `notMerge`. Explicit ids are kept; the input is not mutated.
|
|
736
|
+
*/
|
|
737
|
+
export function withStableIds(option: EChartsOption, state: StableIdState = createStableIdState()): EChartsOption {
|
|
738
|
+
const out: Record<string, unknown> = { ...(option as Record<string, unknown>) }
|
|
739
|
+
const taken = new Set<string>()
|
|
740
|
+
const list = (v: unknown): Record<string, unknown>[] | null =>
|
|
741
|
+
Array.isArray(v) ? (v as Record<string, unknown>[]) : v && typeof v === "object" ? [v as Record<string, unknown>] : null
|
|
742
|
+
const give = (key: string, pick: (c: Record<string, unknown>, i: number) => string) => {
|
|
743
|
+
const items = list(out[key])
|
|
744
|
+
if (!items) return
|
|
745
|
+
const used = new Set<string>(items.flatMap((c) => (c && c.id != null ? [String(c.id)] : [])))
|
|
746
|
+
const bases = items.map((c, i) => {
|
|
747
|
+
if (!c || typeof c !== "object" || c.id != null) return null
|
|
748
|
+
let base = pick(c, i)
|
|
749
|
+
for (let n = 2; used.has(base) || taken.has(`${key}\u0000${base}`); n++) base = `${pick(c, i)}#${n}`
|
|
750
|
+
used.add(base)
|
|
751
|
+
taken.add(`${key}\u0000${base}`)
|
|
752
|
+
return base
|
|
753
|
+
})
|
|
754
|
+
// A merge fills the empty slots a removal left and keeps the slot order of what exists, so after any
|
|
755
|
+
// change in which components there are (or their order) it would not match a fresh render: rebuild them all.
|
|
756
|
+
const prevOrder = state.order.get(key) ?? []
|
|
757
|
+
const cur = bases.filter((b): b is string => b !== null)
|
|
758
|
+
const inOrder = cur.length === prevOrder.length && cur.every((b, i) => b === prevOrder[i])
|
|
759
|
+
state.order.set(key, cur)
|
|
760
|
+
out[key] = items.map((c, i) => {
|
|
761
|
+
const base = bases[i]
|
|
762
|
+
if (base == null) return c
|
|
763
|
+
const shape = shapeOf(c)
|
|
764
|
+
const prev = state.ids.get(base)
|
|
765
|
+
const version = !prev ? 0 : prev.shape === shape && inOrder ? prev.version : prev.version + 1
|
|
766
|
+
state.ids.set(base, { shape, version })
|
|
767
|
+
return { ...c, id: version ? `${base}~v${version}` : base }
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
give("series", (c, i) => `series:${typeof c.name === "string" && c.name ? c.name : i}`)
|
|
771
|
+
for (const key of ID_TYPES) give(key, (_c, i) => `${key}:${i}`)
|
|
772
|
+
return out as EChartsOption
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Copy of the plain objects / arrays of an option (functions, class instances, Dates stay shared), to compare against later in-place edits. */
|
|
776
|
+
export function snapshotOption<T>(v: T): T {
|
|
777
|
+
if (Array.isArray(v)) return v.map(snapshotOption) as T
|
|
778
|
+
if (v && typeof v === "object") {
|
|
779
|
+
const proto = Object.getPrototypeOf(v)
|
|
780
|
+
if (proto === Object.prototype || proto === null) {
|
|
781
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, snapshotOption(x)])) as T
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return v
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** Structural equality for option trees; functions and other non-plain objects compare by reference. */
|
|
788
|
+
export function optionEquals(a: unknown, b: unknown): boolean {
|
|
789
|
+
if (Object.is(a, b)) return true
|
|
790
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false
|
|
791
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
792
|
+
if (Array.isArray(a)) {
|
|
793
|
+
const bb = b as unknown[]
|
|
794
|
+
return a.length === bb.length && a.every((v, i) => optionEquals(v, bb[i]))
|
|
795
|
+
}
|
|
796
|
+
if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
|
|
797
|
+
const pa = Object.getPrototypeOf(a)
|
|
798
|
+
if ((pa !== Object.prototype && pa !== null) || Object.getPrototypeOf(b) !== pa) return false
|
|
799
|
+
const ka = Object.keys(a)
|
|
800
|
+
const kb = Object.keys(b)
|
|
801
|
+
return ka.length === kb.length && ka.every((k) => Object.hasOwn(b, k) && optionEquals((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]))
|
|
802
|
+
}
|
|
803
|
+
|
|
667
804
|
/** `graphic` text doesn't inherit the global `textStyle`, so give it the inherited font unless it sets its own. */
|
|
668
805
|
function withGraphicFont(option: EChartsOption, fontFamily: string | undefined): EChartsOption {
|
|
669
806
|
const graphic = (option as { graphic?: unknown }).graphic
|
|
@@ -717,42 +854,13 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
717
854
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
718
855
|
const [instance, setInstance] = useState<ECharts | null>(null)
|
|
719
856
|
const [copied, setCopied] = useState(false)
|
|
720
|
-
|
|
721
|
-
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
857
|
+
const tokens = useChartTokens(containerRef)
|
|
722
858
|
const reducedMotion = usePrefersReducedMotion()
|
|
723
859
|
// What the current instance last received, to pick the merge mode of the next update.
|
|
724
860
|
const appliedRef = useRef<EChartsOption | null>(null)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
// ないときは state を更新せず、再 init を防ぐ。比較は updater 関数の中ではなく ref で行う:
|
|
729
|
-
// DOM を読む updater は React に何度も再生され、そのたびに新しいオブジェクトを返して
|
|
730
|
-
// 再描画が止まらなくなる(変数を後から当てた直後に Maximum update depth exceeded)。
|
|
731
|
-
const tokensRef = useRef(tokens)
|
|
732
|
-
useEffect(() => {
|
|
733
|
-
if (theme !== undefined) return
|
|
734
|
-
// Colours come from the document root, but the font is whatever this container inherits: an app
|
|
735
|
-
// can set its own font on a wrapper, and the canvas text has to match the DOM around it.
|
|
736
|
-
const sync = () => {
|
|
737
|
-
const base = readChartTokens()
|
|
738
|
-
const font = containerRef.current ? getComputedStyle(containerRef.current).fontFamily : ""
|
|
739
|
-
const next = base && font ? { ...base, fontFamily: font } : base
|
|
740
|
-
if (sameChartTokens(tokensRef.current, next)) return
|
|
741
|
-
tokensRef.current = next
|
|
742
|
-
setTokens(next)
|
|
743
|
-
}
|
|
744
|
-
sync()
|
|
745
|
-
const stop = watchChartTokens(sync)
|
|
746
|
-
// A font set on a wrapper (class / inline style) isn't seen by the document-level watcher.
|
|
747
|
-
const ancestors = new MutationObserver(sync)
|
|
748
|
-
for (let node = containerRef.current?.parentElement; node; node = node.parentElement) {
|
|
749
|
-
ancestors.observe(node, { attributes: true, attributeFilter: ["class", "style"] })
|
|
750
|
-
}
|
|
751
|
-
return () => {
|
|
752
|
-
stop()
|
|
753
|
-
ancestors.disconnect()
|
|
754
|
-
}
|
|
755
|
-
}, [theme])
|
|
861
|
+
// The option (before ids) as it was when applied, deep-copied; and the shape history behind the generated ids.
|
|
862
|
+
const snapshotRef = useRef<EChartsOption | null>(null)
|
|
863
|
+
const idStateRef = useRef<StableIdState>(createStableIdState())
|
|
756
864
|
|
|
757
865
|
const autoTheme = useMemo(() => (tokens ? buildChartTheme(tokens) : undefined), [tokens])
|
|
758
866
|
const resolvedTheme = theme ?? autoTheme
|
|
@@ -764,6 +872,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
764
872
|
if (!container) return
|
|
765
873
|
const inst = init(container, resolvedTheme, { renderer: "canvas" })
|
|
766
874
|
appliedRef.current = null
|
|
875
|
+
snapshotRef.current = null
|
|
767
876
|
// `data-rendering` stays on until the (animated) paint is done, so anything
|
|
768
877
|
// capturing the page — printing a document — can wait for real pixels.
|
|
769
878
|
container.dataset.rendering = ""
|
|
@@ -790,21 +899,29 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
790
899
|
}, [resolvedTheme])
|
|
791
900
|
|
|
792
901
|
useEffect(() => {
|
|
793
|
-
if (!instance) return
|
|
902
|
+
if (!instance || instance.isDisposed()) return
|
|
903
|
+
const prepared = withGraphicFont(
|
|
904
|
+
withMotionPreference(option, reducedMotion),
|
|
905
|
+
theme === undefined ? tokens?.fontFamily : undefined,
|
|
906
|
+
)
|
|
907
|
+
// Same content as last time (a re-render that built an equal option): nothing to redraw.
|
|
908
|
+
// Compared with a snapshot, so an in-place edit of a nested array of the caller's option still counts as a change.
|
|
909
|
+
if (snapshotRef.current && optionEquals(snapshotRef.current, prepared)) return
|
|
794
910
|
if (containerRef.current) containerRef.current.dataset.rendering = ""
|
|
795
|
-
const next =
|
|
911
|
+
const next = withStableIds(prepared, idStateRef.current)
|
|
796
912
|
instance.setOption(next, echartSetOptionOpts(appliedRef.current, next))
|
|
797
913
|
appliedRef.current = next
|
|
914
|
+
snapshotRef.current = snapshotOption(prepared)
|
|
798
915
|
}, [instance, option, reducedMotion, theme, tokens?.fontFamily])
|
|
799
916
|
|
|
800
917
|
useEffect(() => {
|
|
801
|
-
if (!instance) return
|
|
918
|
+
if (!instance || instance.isDisposed()) return
|
|
802
919
|
if (loading) instance.showLoading()
|
|
803
920
|
else instance.hideLoading()
|
|
804
921
|
}, [instance, loading])
|
|
805
922
|
|
|
806
923
|
useEffect(() => {
|
|
807
|
-
if (!instance || !onEvents) return
|
|
924
|
+
if (!instance || instance.isDisposed() || !onEvents) return
|
|
808
925
|
const entries = Object.entries(onEvents)
|
|
809
926
|
for (const [event, handler] of entries) {
|
|
810
927
|
instance.on(event, handler as EChartsEventHandler)
|