@squadbase/vantage 0.6.2-dev1.5aae04e → 0.6.2-dev1.5c79ef5

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.
Files changed (228) hide show
  1. package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
  4. package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/chunk-2IAPIOPS.js +152 -0
  6. package/dist/chunk-2IAPIOPS.js.map +1 -0
  7. package/dist/chunk-3RJEOP3J.js +174 -0
  8. package/dist/chunk-3RJEOP3J.js.map +1 -0
  9. package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
  10. package/dist/chunk-77CISBMG.js.map +1 -0
  11. package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
  12. package/dist/chunk-ANZLVSEP.js.map +1 -0
  13. package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
  14. package/dist/chunk-CKWPK4RO.js.map +1 -0
  15. package/dist/chunk-EG4PX726.js +678 -0
  16. package/dist/chunk-EG4PX726.js.map +1 -0
  17. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  18. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  19. package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
  20. package/dist/chunk-PJ4SPXHS.js.map +1 -0
  21. package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
  22. package/dist/chunk-QNOX7WLZ.js.map +1 -0
  23. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  24. package/dist/chunk-ROHNXP7J.js.map +1 -0
  25. package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
  26. package/dist/chunk-S5MDTHH7.js.map +1 -0
  27. package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
  28. package/dist/chunk-SDJTTMLK.js.map +1 -0
  29. package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
  30. package/dist/chunk-SHZM3EXB.js.map +1 -0
  31. package/dist/chunk-TRMHNMMZ.js +61 -0
  32. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  33. package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
  34. package/dist/chunk-UWSQUHX6.js.map +1 -0
  35. package/dist/cli.js +6 -6
  36. package/dist/client/index.d.ts +2 -0
  37. package/dist/client/index.js +8 -2
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/components/index.d.ts +57 -273
  40. package/dist/components/index.js +6 -3
  41. package/dist/delta-text-CbrOaW83.d.ts +17 -0
  42. package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
  43. package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
  44. package/dist/doc/index.d.ts +215 -2
  45. package/dist/doc/index.js +422 -4
  46. package/dist/doc/index.js.map +1 -1
  47. package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
  48. package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
  49. package/dist/format-DihQkL1N.d.ts +249 -0
  50. package/dist/query/index.js +2 -2
  51. package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
  52. package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
  53. package/dist/slide/index.d.ts +272 -1
  54. package/dist/slide/index.js +1023 -2
  55. package/dist/slide/index.js.map +1 -1
  56. package/dist/table/index.js +9 -6
  57. package/dist/table/index.js.map +1 -1
  58. package/dist/ui/index.d.ts +5 -2
  59. package/dist/ui/index.js +6 -6
  60. package/dist/ui/index.js.map +1 -1
  61. package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
  62. package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
  63. package/dist/vite/index.js +3 -3
  64. package/docs/en/agent-skills.md +1 -1
  65. package/docs/en/changelog.md +2 -0
  66. package/docs/en/components.md +13 -2
  67. package/docs/en/doc/delta-text.md +40 -0
  68. package/docs/en/doc/doc-action-items.md +49 -0
  69. package/docs/en/doc/doc-callout.md +38 -0
  70. package/docs/en/doc/doc-data-table.md +60 -0
  71. package/docs/en/doc/doc-executive-summary.md +64 -0
  72. package/docs/en/doc/doc-figure.md +56 -0
  73. package/docs/en/doc/doc-key-findings.md +61 -0
  74. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  75. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  76. package/docs/en/doc/doc-report-header.md +46 -0
  77. package/docs/en/pages-and-metadata.md +15 -0
  78. package/docs/en/parts/area-chart.md +99 -0
  79. package/docs/en/parts/bar-chart.md +91 -0
  80. package/docs/en/parts/bar-list.md +50 -0
  81. package/docs/en/parts/category-bar.md +37 -0
  82. package/docs/en/parts/chart-frame.md +39 -0
  83. package/docs/en/parts/delta.md +4 -3
  84. package/docs/en/parts/donut-chart.md +72 -0
  85. package/docs/en/parts/funnel-steps.md +2 -1
  86. package/docs/en/parts/heatmap.md +53 -0
  87. package/docs/en/parts/kpi-card.md +2 -1
  88. package/docs/en/parts/legend.md +35 -0
  89. package/docs/en/parts/line-chart.md +94 -0
  90. package/docs/en/parts/progress-circle.md +33 -0
  91. package/docs/en/parts/sample-data.md +39 -0
  92. package/docs/en/parts/scatter-chart.md +56 -0
  93. package/docs/en/parts/tracker.md +36 -0
  94. package/docs/en/parts/updated-at.md +30 -0
  95. package/docs/en/parts/waterfall-chart.md +56 -0
  96. package/docs/en/slide/slide-chart-full.md +48 -0
  97. package/docs/en/slide/slide-chart-insight.md +52 -0
  98. package/docs/en/slide/slide-columns.md +46 -0
  99. package/docs/en/slide/slide-comparison.md +68 -0
  100. package/docs/en/slide/slide-cover.md +39 -0
  101. package/docs/en/slide/slide-exec-summary.md +47 -0
  102. package/docs/en/slide/slide-heading.md +38 -0
  103. package/docs/en/slide/slide-kpi-grid.md +55 -0
  104. package/docs/en/slide/slide-kpi-hero.md +58 -0
  105. package/docs/en/slide/slide-next-actions.md +49 -0
  106. package/docs/en/slide/slide-scorecard-table.md +54 -0
  107. package/docs/en/slide/slide-section.md +44 -0
  108. package/docs/en/slide/slide-source-line.md +34 -0
  109. package/docs/en/ui/alert-dialog.md +42 -0
  110. package/docs/en/ui/avatar.md +31 -0
  111. package/docs/en/ui/combobox.md +74 -0
  112. package/docs/en/ui/drawer.md +41 -0
  113. package/docs/en/ui/empty.md +1 -1
  114. package/docs/en/ui/error-state.md +4 -3
  115. package/docs/en/ui/field.md +42 -0
  116. package/docs/en/ui/hover-card.md +21 -0
  117. package/docs/en/ui/kbd.md +17 -0
  118. package/docs/en/ui/loading.md +1 -1
  119. package/docs/en/ui/pagination.md +36 -0
  120. package/docs/en/ui/radio-group.md +21 -0
  121. package/docs/en/ui/toast.md +44 -0
  122. package/docs/en/ui/toggle-group.md +4 -3
  123. package/docs/index.json +857 -77
  124. package/docs/ja/agent-skills.md +1 -1
  125. package/docs/ja/changelog.md +2 -0
  126. package/docs/ja/components.md +12 -1
  127. package/docs/ja/doc/delta-text.md +40 -0
  128. package/docs/ja/doc/doc-action-items.md +49 -0
  129. package/docs/ja/doc/doc-callout.md +38 -0
  130. package/docs/ja/doc/doc-data-table.md +60 -0
  131. package/docs/ja/doc/doc-executive-summary.md +64 -0
  132. package/docs/ja/doc/doc-figure.md +56 -0
  133. package/docs/ja/doc/doc-key-findings.md +61 -0
  134. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  135. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  136. package/docs/ja/doc/doc-report-header.md +46 -0
  137. package/docs/ja/page-types/slide.md +1 -0
  138. package/docs/ja/pages-and-metadata.md +15 -0
  139. package/docs/ja/parts/area-chart.md +49 -25
  140. package/docs/ja/parts/bar-chart.md +50 -24
  141. package/docs/ja/parts/bar-list.md +31 -8
  142. package/docs/ja/parts/chart-frame.md +36 -0
  143. package/docs/ja/parts/delta.md +5 -4
  144. package/docs/ja/parts/donut-chart.md +45 -26
  145. package/docs/ja/parts/funnel-steps.md +27 -8
  146. package/docs/ja/parts/heatmap.md +45 -0
  147. package/docs/ja/parts/kpi-card.md +2 -1
  148. package/docs/ja/parts/legend.md +36 -0
  149. package/docs/ja/parts/line-chart.md +48 -22
  150. package/docs/ja/parts/sample-data.md +61 -0
  151. package/docs/ja/parts/scatter-chart.md +69 -0
  152. package/docs/ja/parts/updated-at.md +27 -0
  153. package/docs/ja/parts/waterfall-chart.md +61 -0
  154. package/docs/ja/slide/slide-chart-full.md +48 -0
  155. package/docs/ja/slide/slide-chart-insight.md +52 -0
  156. package/docs/ja/slide/slide-columns.md +46 -0
  157. package/docs/ja/slide/slide-comparison.md +68 -0
  158. package/docs/ja/slide/slide-cover.md +39 -0
  159. package/docs/ja/slide/slide-exec-summary.md +47 -0
  160. package/docs/ja/slide/slide-heading.md +38 -0
  161. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  162. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  163. package/docs/ja/slide/slide-next-actions.md +49 -0
  164. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  165. package/docs/ja/slide/slide-section.md +44 -0
  166. package/docs/ja/slide/slide-source-line.md +34 -0
  167. package/docs/ja/ui/empty.md +1 -1
  168. package/docs/ja/ui/error-state.md +4 -3
  169. package/docs/ja/ui/loading.md +1 -1
  170. package/docs/ja/ui/toggle-group.md +4 -3
  171. package/package.json +2 -2
  172. package/registry/ui/app-shell.tsx +138 -1
  173. package/registry/ui/area-chart.tsx +389 -104
  174. package/registry/ui/attention-list.tsx +177 -15
  175. package/registry/ui/bar-chart.tsx +386 -104
  176. package/registry/ui/bar-list.tsx +376 -94
  177. package/registry/ui/box-plot-chart.tsx +450 -116
  178. package/registry/ui/category-bar.tsx +292 -57
  179. package/registry/ui/chart-frame.tsx +301 -63
  180. package/registry/ui/data-table.tsx +229 -37
  181. package/registry/ui/date-range-picker.tsx +190 -36
  182. package/registry/ui/delta.tsx +181 -19
  183. package/registry/ui/donut-chart.tsx +385 -103
  184. package/registry/ui/echart.tsx +313 -58
  185. package/registry/ui/filter-bar.tsx +179 -20
  186. package/registry/ui/funnel-steps.tsx +354 -80
  187. package/registry/ui/heatmap.tsx +338 -71
  188. package/registry/ui/histogram-chart.tsx +440 -116
  189. package/registry/ui/kpi-card.tsx +254 -48
  190. package/registry/ui/legend.tsx +374 -93
  191. package/registry/ui/line-chart.tsx +389 -104
  192. package/registry/ui/multi-select.tsx +159 -9
  193. package/registry/ui/page-shell.tsx +148 -10
  194. package/registry/ui/paired-chart.tsx +394 -109
  195. package/registry/ui/progress-circle.tsx +205 -39
  196. package/registry/ui/sample-data.tsx +154 -10
  197. package/registry/ui/scatter-chart.tsx +436 -119
  198. package/registry/ui/searchable-select.tsx +145 -7
  199. package/registry/ui/slider.tsx +1 -1
  200. package/registry/ui/states.tsx +170 -13
  201. package/registry/ui/status-badge.tsx +201 -36
  202. package/registry/ui/tooltip.tsx +6 -2
  203. package/registry/ui/tracker.tsx +214 -42
  204. package/registry/ui/trend-indicator.tsx +151 -8
  205. package/registry/ui/updated-at.tsx +170 -16
  206. package/registry/ui/waterfall-chart.tsx +460 -121
  207. package/skills/vantage-design/SKILL.md +54 -54
  208. package/skills/vantage-design/references/extract.md +26 -27
  209. package/skills/vantage-design/references/format.md +12 -172
  210. package/skills/vantage-design/references/presets.md +50 -110
  211. package/skills/vantage-pitfalls/SKILL.md +5 -0
  212. package/skills/vantage-slide/SKILL.md +54 -36
  213. package/templates/AGENTS.md +32 -16
  214. package/templates/agents/doc.md +48 -0
  215. package/templates/agents/page.md +23 -2
  216. package/templates/agents/slide.md +77 -3
  217. package/theme.css +23 -19
  218. package/dist/chunk-2ICNHUG6.js.map +0 -1
  219. package/dist/chunk-2N72FKLB.js.map +0 -1
  220. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  221. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  222. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  223. package/dist/chunk-JKLLFVEU.js.map +0 -1
  224. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  225. package/dist/chunk-V3LF7BE7.js.map +0 -1
  226. package/dist/chunk-WAMZWZJB.js.map +0 -1
  227. package/dist/chunk-XPQSTZUU.js.map +0 -1
  228. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -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 { createContext, type ForwardedRef } from "react"
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
- return {
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
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
214
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
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 off the document root (`--positive`, …); `var(--token)` is read off the document root; any
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
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
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: ChartTokens | null,
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 [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
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
- // 明暗の切り替えとスタイルシートの差し替え(dev で styles.css を書き換えたときなど)を
727
- // 拾ってトークンを読み直す ― 何を見張るかは `watchChartTokens` 側が持つ。値が変わら
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 = withGraphicFont(withMotionPreference(option, reducedMotion), theme === undefined ? tokens?.fontFamily : undefined)
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 ? "コピーしました" : "PNG をクリップボードにコピー"}
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="PNG をダウンロード"
1167
+ aria-label={t.download}
913
1168
  >
914
1169
  <Download className="size-4" />
915
1170
  </Button>