@squadbase/vantage 0.6.2-dev1.9365dcd → 0.6.2-dev1.9d7130b
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/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-3RJEOP3J.js +174 -0
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
- package/dist/chunk-CKWPK4RO.js.map +1 -0
- package/dist/chunk-EG4PX726.js +678 -0
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
- package/dist/chunk-QNOX7WLZ.js.map +1 -0
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
- package/dist/chunk-SHZM3EXB.js.map +1 -0
- package/dist/chunk-TRMHNMMZ.js +61 -0
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +57 -273
- package/dist/components/index.js +6 -3
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +422 -4
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/query/index.js +2 -2
- package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
- package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1023 -2
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +9 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -2
- package/dist/ui/index.js +6 -6
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
- package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +2 -0
- 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/pages-and-metadata.md +15 -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/agent-skills.md +1 -1
- package/docs/ja/changelog.md +2 -0
- 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/page-types/slide.md +1 -0
- package/docs/ja/pages-and-metadata.md +15 -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/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +389 -104
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +386 -104
- package/registry/ui/bar-list.tsx +376 -94
- package/registry/ui/box-plot-chart.tsx +450 -116
- package/registry/ui/category-bar.tsx +292 -57
- package/registry/ui/chart-frame.tsx +301 -63
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +181 -19
- package/registry/ui/donut-chart.tsx +385 -103
- package/registry/ui/echart.tsx +313 -58
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +338 -71
- package/registry/ui/histogram-chart.tsx +440 -116
- package/registry/ui/kpi-card.tsx +254 -48
- package/registry/ui/legend.tsx +374 -93
- package/registry/ui/line-chart.tsx +389 -104
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +148 -10
- package/registry/ui/paired-chart.tsx +394 -109
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +154 -10
- package/registry/ui/scatter-chart.tsx +436 -119
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +201 -36
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +214 -42
- package/registry/ui/trend-indicator.tsx +151 -8
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +460 -121
- package/skills/vantage-add-feature/SKILL.md +5 -0
- package/skills/vantage-design/SKILL.md +54 -54
- package/skills/vantage-design/references/extract.md +26 -27
- package/skills/vantage-design/references/format.md +12 -172
- package/skills/vantage-design/references/presets.md +50 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +35 -18
- package/templates/agents/api.md +7 -0
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +59 -2
- package/templates/agents/slide.md +77 -3
- package/theme.css +23 -19
- package/dist/chunk-2ICNHUG6.js.map +0 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-WAMZWZJB.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
package/registry/ui/echart.tsx
CHANGED
|
@@ -58,9 +58,141 @@ 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 {
|
|
61
|
+
import { useSyncExternalStore } from "react"
|
|
62
|
+
import { createContext, type ForwardedRef, type RefObject } from "react"
|
|
62
63
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
63
64
|
|
|
65
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The display language of the parts and the defaults that follow it.
|
|
69
|
+
*
|
|
70
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
71
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
72
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
73
|
+
*
|
|
74
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
75
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
79
|
+
type UiLanguage = "ja" | "en"
|
|
80
|
+
|
|
81
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
82
|
+
const FALLBACK_LOCALE = "en-US"
|
|
83
|
+
|
|
84
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
85
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
86
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
90
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
91
|
+
const value = tag?.trim()
|
|
92
|
+
if (!value) return FALLBACK_LOCALE
|
|
93
|
+
try {
|
|
94
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
95
|
+
} catch {
|
|
96
|
+
return FALLBACK_LOCALE
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
102
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
103
|
+
*/
|
|
104
|
+
function documentLocale(): string {
|
|
105
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
106
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
110
|
+
function resolveLocale(locale?: string | null): string {
|
|
111
|
+
return locale || documentLocale()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Picks the wording table of the locale's language. */
|
|
115
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
116
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
120
|
+
function defaultCurrency(locale?: string | null): string {
|
|
121
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
125
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
126
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
130
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
131
|
+
|
|
132
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
133
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
137
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
138
|
+
* Monday for the rest.
|
|
139
|
+
*/
|
|
140
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
141
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
142
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
143
|
+
try {
|
|
144
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
145
|
+
getWeekInfo?: () => { firstDay: number }
|
|
146
|
+
weekInfo?: { firstDay: number }
|
|
147
|
+
}
|
|
148
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
149
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
150
|
+
} catch {
|
|
151
|
+
// Fall through to the table below.
|
|
152
|
+
}
|
|
153
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
157
|
+
|
|
158
|
+
const listeners = new Set<() => void>()
|
|
159
|
+
let observer: MutationObserver | null = null
|
|
160
|
+
|
|
161
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
162
|
+
function subscribe(onChange: () => void): () => void {
|
|
163
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
164
|
+
listeners.add(onChange)
|
|
165
|
+
if (!observer) {
|
|
166
|
+
observer = new MutationObserver(() => {
|
|
167
|
+
for (const listener of [...listeners]) listener()
|
|
168
|
+
})
|
|
169
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
170
|
+
}
|
|
171
|
+
return () => {
|
|
172
|
+
listeners.delete(onChange)
|
|
173
|
+
if (listeners.size === 0 && observer) {
|
|
174
|
+
observer.disconnect()
|
|
175
|
+
observer = null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
184
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
185
|
+
*/
|
|
186
|
+
function useDocumentLocale(): string {
|
|
187
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
191
|
+
function useLocale(locale?: string | null): string {
|
|
192
|
+
const page = useDocumentLocale()
|
|
193
|
+
return locale || page
|
|
194
|
+
}
|
|
195
|
+
|
|
64
196
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
65
197
|
|
|
66
198
|
/**
|
|
@@ -95,10 +227,23 @@ interface ChartTokens {
|
|
|
95
227
|
border: string | null
|
|
96
228
|
/** Tooltip surface. */
|
|
97
229
|
popover: string | null
|
|
230
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
231
|
+
colors?: Record<string, string>
|
|
232
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
233
|
+
surface?: string | null
|
|
98
234
|
/** Tooltip text. */
|
|
99
235
|
popoverForeground: string | null
|
|
100
236
|
}
|
|
101
237
|
|
|
238
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
239
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
240
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
241
|
+
const OPTION_COLOR_TOKENS = [
|
|
242
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
243
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
244
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
245
|
+
]
|
|
246
|
+
|
|
102
247
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
103
248
|
|
|
104
249
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -154,8 +299,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
154
299
|
const color = read(token)
|
|
155
300
|
if (color) series.push(color)
|
|
156
301
|
}
|
|
157
|
-
|
|
302
|
+
const tokens: ChartTokens = {
|
|
158
303
|
series,
|
|
304
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
305
|
+
surface: readChartSurface(target),
|
|
159
306
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
160
307
|
foreground: read("--foreground"),
|
|
161
308
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -163,6 +310,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
163
310
|
popover: read("--popover"),
|
|
164
311
|
popoverForeground: read("--popover-foreground"),
|
|
165
312
|
}
|
|
313
|
+
tokenScopes.set(tokens, target)
|
|
314
|
+
return tokens
|
|
166
315
|
}
|
|
167
316
|
|
|
168
317
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -170,6 +319,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
170
319
|
if (a === b) return true
|
|
171
320
|
if (!a || !b) return false
|
|
172
321
|
return (
|
|
322
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
323
|
+
a.surface === b.surface &&
|
|
324
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
325
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
173
326
|
a.series.length === b.series.length &&
|
|
174
327
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
175
328
|
a.fontFamily === b.fontFamily &&
|
|
@@ -195,7 +348,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
195
348
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
196
349
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
197
350
|
*/
|
|
198
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
351
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
199
352
|
if (typeof document === "undefined") return () => {}
|
|
200
353
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
201
354
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -210,8 +363,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
210
363
|
|
|
211
364
|
const themeObserver = new MutationObserver(schedule)
|
|
212
365
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
213
|
-
|
|
214
|
-
|
|
366
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
367
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
368
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
369
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
370
|
+
}
|
|
215
371
|
|
|
216
372
|
const styleObserver = new MutationObserver(schedule)
|
|
217
373
|
if (document.head) {
|
|
@@ -299,22 +455,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
299
455
|
* components that build an ECharts option from token colours themselves (the
|
|
300
456
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
301
457
|
*/
|
|
302
|
-
function useChartTokens(): ChartTokens | null {
|
|
458
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
303
459
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
304
460
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
305
461
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
306
462
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
307
463
|
const tokensRef = useRef(tokens)
|
|
308
464
|
useEffect(() => {
|
|
465
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
309
466
|
const sync = () => {
|
|
310
|
-
const next = readChartTokens()
|
|
467
|
+
const next = readChartTokens(target)
|
|
311
468
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
312
469
|
tokensRef.current = next
|
|
313
470
|
setTokens(next)
|
|
314
471
|
}
|
|
315
472
|
sync()
|
|
316
|
-
return watchChartTokens(sync)
|
|
317
|
-
}, [])
|
|
473
|
+
return watchChartTokens(sync, target)
|
|
474
|
+
}, [scope])
|
|
318
475
|
return tokens
|
|
319
476
|
}
|
|
320
477
|
|
|
@@ -326,7 +483,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
326
483
|
/**
|
|
327
484
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
328
485
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
329
|
-
* variants `"positive-fill"`, …) are read
|
|
486
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
330
487
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
331
488
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
332
489
|
*/
|
|
@@ -337,12 +494,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
337
494
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
338
495
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
339
496
|
if (typeof document === "undefined") return ""
|
|
340
|
-
|
|
497
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
498
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
341
499
|
}
|
|
342
500
|
if (typeof document === "undefined") return color
|
|
343
501
|
let value = color.trim()
|
|
344
502
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
345
|
-
const styles = getComputedStyle(document.documentElement)
|
|
503
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
346
504
|
if (cssVar) {
|
|
347
505
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
348
506
|
} else if (value.includes("var(")) {
|
|
@@ -388,7 +546,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
388
546
|
if (color) return color
|
|
389
547
|
}
|
|
390
548
|
}
|
|
391
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
549
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
392
550
|
}
|
|
393
551
|
|
|
394
552
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -402,9 +560,10 @@ const WIDTH_BUCKET = 40
|
|
|
402
560
|
*/
|
|
403
561
|
function useChartSurface<E extends Element>(
|
|
404
562
|
forwarded: ForwardedRef<E>,
|
|
405
|
-
tokens
|
|
406
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
563
|
+
tokens?: ChartTokens | null,
|
|
564
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
407
565
|
const [node, setNode] = useState<E | null>(null)
|
|
566
|
+
const scopedTokens = useChartTokens(node)
|
|
408
567
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
409
568
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
410
569
|
const ref = useCallback(
|
|
@@ -418,7 +577,7 @@ function useChartSurface<E extends Element>(
|
|
|
418
577
|
useEffect(() => {
|
|
419
578
|
if (!node) return
|
|
420
579
|
setSurface(readChartSurface(node) ?? undefined)
|
|
421
|
-
}, [node, tokens])
|
|
580
|
+
}, [node, tokens, scopedTokens])
|
|
422
581
|
useEffect(() => {
|
|
423
582
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
424
583
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -427,7 +586,7 @@ function useChartSurface<E extends Element>(
|
|
|
427
586
|
observer.observe(node)
|
|
428
587
|
return () => observer.disconnect()
|
|
429
588
|
}, [node])
|
|
430
|
-
return { ref, surface, width }
|
|
589
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
431
590
|
}
|
|
432
591
|
|
|
433
592
|
/* ---------- end of inlined code ---------- */
|
|
@@ -567,11 +726,16 @@ export function chartExportHeaderLayout(
|
|
|
567
726
|
return { lines, height: HEADER_PAD_TOP + lines.length * HEADER_LINE_HEIGHT + gap + HEADER_PAD_BOTTOM }
|
|
568
727
|
}
|
|
569
728
|
|
|
729
|
+
const TEXT = {
|
|
730
|
+
ja: { chart: "チャート", copied: "コピーしました", copy: "PNG をクリップボードにコピー", download: "PNG をダウンロード" },
|
|
731
|
+
en: { chart: "Chart", copied: "Copied", copy: "Copy PNG to clipboard", download: "Download PNG" },
|
|
732
|
+
}
|
|
733
|
+
|
|
570
734
|
function loadImage(src: string): Promise<HTMLImageElement> {
|
|
571
735
|
return new Promise((resolve, reject) => {
|
|
572
736
|
const image = new Image()
|
|
573
737
|
image.onload = () => resolve(image)
|
|
574
|
-
image.onerror = () => reject(new Error("PNG
|
|
738
|
+
image.onerror = () => reject(new Error("Couldn't load the PNG"))
|
|
575
739
|
image.src = src
|
|
576
740
|
})
|
|
577
741
|
}
|
|
@@ -664,6 +828,116 @@ export function echartSetOptionOpts(prev: EChartsOption | null, next: EChartsOpt
|
|
|
664
828
|
return { notMerge: false, replaceMerge: [...types], lazyUpdate: true }
|
|
665
829
|
}
|
|
666
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Component types that get a generated `id` when the option has none, so replaceMerge can match them across updates.
|
|
833
|
+
* Not `dataset` / `graphic` / `tooltip`: they are rebuilt on every update (there is nothing to transition).
|
|
834
|
+
*/
|
|
835
|
+
const ID_TYPES = [
|
|
836
|
+
"title", "legend", "grid", "xAxis", "yAxis", "polar", "radiusAxis", "angleAxis", "radar",
|
|
837
|
+
"dataZoom", "visualMap", "geo", "singleAxis", "calendar",
|
|
838
|
+
] as const
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* What `withStableIds` remembers between updates: per generated id, the shape last applied and how many
|
|
842
|
+
* times it changed (the id's `~v<n>` suffix); per component type, the order of its ids.
|
|
843
|
+
*/
|
|
844
|
+
export interface StableIdState {
|
|
845
|
+
ids: Map<string, { shape: string; version: number }>
|
|
846
|
+
order: Map<string, string[]>
|
|
847
|
+
}
|
|
848
|
+
export const createStableIdState = (): StableIdState => ({ ids: new Map(), order: new Map() })
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Shape of a component: which keys it has, recursively (arrays only as "array", values not at all —
|
|
852
|
+
* except `type`). A merge keeps keys the new option dropped, so a changed shape must not be merged.
|
|
853
|
+
*/
|
|
854
|
+
function shapeOf(v: unknown): string {
|
|
855
|
+
if (Array.isArray(v)) return "[]"
|
|
856
|
+
if (!v || typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype) return "."
|
|
857
|
+
const o = v as Record<string, unknown>
|
|
858
|
+
return `{${Object.keys(o)
|
|
859
|
+
.filter((k) => o[k] !== undefined)
|
|
860
|
+
.sort()
|
|
861
|
+
.map((k) => (k === "type" || k === "id" ? `${k}=${String(o[k])}` : `${k}:${shapeOf(o[k])}`))
|
|
862
|
+
.join(",")}}`
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Gives every series (and the common components) an `id` it does not have yet —
|
|
867
|
+
* series: `series:<name ?? index>`, others `<type>:<index>` — so an update that
|
|
868
|
+
* only changes values is matched to the existing component and transitions
|
|
869
|
+
* instead of being rebuilt. If the component's shape changed since `state` last
|
|
870
|
+
* saw it (a key was removed or added, the type changed), the id gets a new
|
|
871
|
+
* `~v<n>` suffix so replaceMerge builds it from scratch: the result then always
|
|
872
|
+
* equals applying the option with `notMerge`. Explicit ids are kept; the input is not mutated.
|
|
873
|
+
*/
|
|
874
|
+
export function withStableIds(option: EChartsOption, state: StableIdState = createStableIdState()): EChartsOption {
|
|
875
|
+
const out: Record<string, unknown> = { ...(option as Record<string, unknown>) }
|
|
876
|
+
const taken = new Set<string>()
|
|
877
|
+
const list = (v: unknown): Record<string, unknown>[] | null =>
|
|
878
|
+
Array.isArray(v) ? (v as Record<string, unknown>[]) : v && typeof v === "object" ? [v as Record<string, unknown>] : null
|
|
879
|
+
const give = (key: string, pick: (c: Record<string, unknown>, i: number) => string) => {
|
|
880
|
+
const items = list(out[key])
|
|
881
|
+
if (!items) return
|
|
882
|
+
const used = new Set<string>(items.flatMap((c) => (c && c.id != null ? [String(c.id)] : [])))
|
|
883
|
+
const bases = items.map((c, i) => {
|
|
884
|
+
if (!c || typeof c !== "object" || c.id != null) return null
|
|
885
|
+
let base = pick(c, i)
|
|
886
|
+
for (let n = 2; used.has(base) || taken.has(`${key}\u0000${base}`); n++) base = `${pick(c, i)}#${n}`
|
|
887
|
+
used.add(base)
|
|
888
|
+
taken.add(`${key}\u0000${base}`)
|
|
889
|
+
return base
|
|
890
|
+
})
|
|
891
|
+
// A merge fills the empty slots a removal left and keeps the slot order of what exists, so after any
|
|
892
|
+
// change in which components there are (or their order) it would not match a fresh render: rebuild them all.
|
|
893
|
+
const prevOrder = state.order.get(key) ?? []
|
|
894
|
+
const cur = bases.filter((b): b is string => b !== null)
|
|
895
|
+
const inOrder = cur.length === prevOrder.length && cur.every((b, i) => b === prevOrder[i])
|
|
896
|
+
state.order.set(key, cur)
|
|
897
|
+
out[key] = items.map((c, i) => {
|
|
898
|
+
const base = bases[i]
|
|
899
|
+
if (base == null) return c
|
|
900
|
+
const shape = shapeOf(c)
|
|
901
|
+
const prev = state.ids.get(base)
|
|
902
|
+
const version = !prev ? 0 : prev.shape === shape && inOrder ? prev.version : prev.version + 1
|
|
903
|
+
state.ids.set(base, { shape, version })
|
|
904
|
+
return { ...c, id: version ? `${base}~v${version}` : base }
|
|
905
|
+
})
|
|
906
|
+
}
|
|
907
|
+
give("series", (c, i) => `series:${typeof c.name === "string" && c.name ? c.name : i}`)
|
|
908
|
+
for (const key of ID_TYPES) give(key, (_c, i) => `${key}:${i}`)
|
|
909
|
+
return out as EChartsOption
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** Copy of the plain objects / arrays of an option (functions, class instances, Dates stay shared), to compare against later in-place edits. */
|
|
913
|
+
export function snapshotOption<T>(v: T): T {
|
|
914
|
+
if (Array.isArray(v)) return v.map(snapshotOption) as T
|
|
915
|
+
if (v && typeof v === "object") {
|
|
916
|
+
const proto = Object.getPrototypeOf(v)
|
|
917
|
+
if (proto === Object.prototype || proto === null) {
|
|
918
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, snapshotOption(x)])) as T
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return v
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Structural equality for option trees; functions and other non-plain objects compare by reference. */
|
|
925
|
+
export function optionEquals(a: unknown, b: unknown): boolean {
|
|
926
|
+
if (Object.is(a, b)) return true
|
|
927
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false
|
|
928
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
929
|
+
if (Array.isArray(a)) {
|
|
930
|
+
const bb = b as unknown[]
|
|
931
|
+
return a.length === bb.length && a.every((v, i) => optionEquals(v, bb[i]))
|
|
932
|
+
}
|
|
933
|
+
if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
|
|
934
|
+
const pa = Object.getPrototypeOf(a)
|
|
935
|
+
if ((pa !== Object.prototype && pa !== null) || Object.getPrototypeOf(b) !== pa) return false
|
|
936
|
+
const ka = Object.keys(a)
|
|
937
|
+
const kb = Object.keys(b)
|
|
938
|
+
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]))
|
|
939
|
+
}
|
|
940
|
+
|
|
667
941
|
/** `graphic` text doesn't inherit the global `textStyle`, so give it the inherited font unless it sets its own. */
|
|
668
942
|
function withGraphicFont(option: EChartsOption, fontFamily: string | undefined): EChartsOption {
|
|
669
943
|
const graphic = (option as { graphic?: unknown }).graphic
|
|
@@ -717,42 +991,14 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
717
991
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
718
992
|
const [instance, setInstance] = useState<ECharts | null>(null)
|
|
719
993
|
const [copied, setCopied] = useState(false)
|
|
720
|
-
|
|
721
|
-
const
|
|
994
|
+
const t = TEXT[uiLanguage(useLocale())]
|
|
995
|
+
const tokens = useChartTokens(containerRef)
|
|
722
996
|
const reducedMotion = usePrefersReducedMotion()
|
|
723
997
|
// What the current instance last received, to pick the merge mode of the next update.
|
|
724
998
|
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])
|
|
999
|
+
// The option (before ids) as it was when applied, deep-copied; and the shape history behind the generated ids.
|
|
1000
|
+
const snapshotRef = useRef<EChartsOption | null>(null)
|
|
1001
|
+
const idStateRef = useRef<StableIdState>(createStableIdState())
|
|
756
1002
|
|
|
757
1003
|
const autoTheme = useMemo(() => (tokens ? buildChartTheme(tokens) : undefined), [tokens])
|
|
758
1004
|
const resolvedTheme = theme ?? autoTheme
|
|
@@ -764,6 +1010,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
764
1010
|
if (!container) return
|
|
765
1011
|
const inst = init(container, resolvedTheme, { renderer: "canvas" })
|
|
766
1012
|
appliedRef.current = null
|
|
1013
|
+
snapshotRef.current = null
|
|
767
1014
|
// `data-rendering` stays on until the (animated) paint is done, so anything
|
|
768
1015
|
// capturing the page — printing a document — can wait for real pixels.
|
|
769
1016
|
container.dataset.rendering = ""
|
|
@@ -790,21 +1037,29 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
790
1037
|
}, [resolvedTheme])
|
|
791
1038
|
|
|
792
1039
|
useEffect(() => {
|
|
793
|
-
if (!instance) return
|
|
1040
|
+
if (!instance || instance.isDisposed()) return
|
|
1041
|
+
const prepared = withGraphicFont(
|
|
1042
|
+
withMotionPreference(option, reducedMotion),
|
|
1043
|
+
theme === undefined ? tokens?.fontFamily : undefined,
|
|
1044
|
+
)
|
|
1045
|
+
// Same content as last time (a re-render that built an equal option): nothing to redraw.
|
|
1046
|
+
// Compared with a snapshot, so an in-place edit of a nested array of the caller's option still counts as a change.
|
|
1047
|
+
if (snapshotRef.current && optionEquals(snapshotRef.current, prepared)) return
|
|
794
1048
|
if (containerRef.current) containerRef.current.dataset.rendering = ""
|
|
795
|
-
const next =
|
|
1049
|
+
const next = withStableIds(prepared, idStateRef.current)
|
|
796
1050
|
instance.setOption(next, echartSetOptionOpts(appliedRef.current, next))
|
|
797
1051
|
appliedRef.current = next
|
|
1052
|
+
snapshotRef.current = snapshotOption(prepared)
|
|
798
1053
|
}, [instance, option, reducedMotion, theme, tokens?.fontFamily])
|
|
799
1054
|
|
|
800
1055
|
useEffect(() => {
|
|
801
|
-
if (!instance) return
|
|
1056
|
+
if (!instance || instance.isDisposed()) return
|
|
802
1057
|
if (loading) instance.showLoading()
|
|
803
1058
|
else instance.hideLoading()
|
|
804
1059
|
}, [instance, loading])
|
|
805
1060
|
|
|
806
1061
|
useEffect(() => {
|
|
807
|
-
if (!instance || !onEvents) return
|
|
1062
|
+
if (!instance || instance.isDisposed() || !onEvents) return
|
|
808
1063
|
const entries = Object.entries(onEvents)
|
|
809
1064
|
for (const [event, handler] of entries) {
|
|
810
1065
|
instance.on(event, handler as EChartsEventHandler)
|
|
@@ -888,7 +1143,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
888
1143
|
data-slot="echart-canvas"
|
|
889
1144
|
className="h-full w-full"
|
|
890
1145
|
role={inFrame ? undefined : "img"}
|
|
891
|
-
aria-label={inFrame ? undefined : (ariaLabel ??
|
|
1146
|
+
aria-label={inFrame ? undefined : (ariaLabel ?? t.chart)}
|
|
892
1147
|
/>
|
|
893
1148
|
{actions && (
|
|
894
1149
|
<div
|
|
@@ -900,7 +1155,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
900
1155
|
variant="ghost"
|
|
901
1156
|
size="icon-sm"
|
|
902
1157
|
onClick={handleCopy}
|
|
903
|
-
aria-label={copied ?
|
|
1158
|
+
aria-label={copied ? t.copied : t.copy}
|
|
904
1159
|
>
|
|
905
1160
|
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
906
1161
|
</Button>
|
|
@@ -909,7 +1164,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
909
1164
|
variant="ghost"
|
|
910
1165
|
size="icon-sm"
|
|
911
1166
|
onClick={handleDownload}
|
|
912
|
-
aria-label=
|
|
1167
|
+
aria-label={t.download}
|
|
913
1168
|
>
|
|
914
1169
|
<Download className="size-4" />
|
|
915
1170
|
</Button>
|