@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
@@ -20,11 +20,143 @@ import { forwardRef, useId, type HTMLAttributes, type ReactNode } from "react"
20
20
 
21
21
  import { cn } from "@squadbase/vantage/ui"
22
22
  import type { ChartEmptyState } from "@squadbase/vantage/components"
23
- import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef } from "react"
23
+ import { useSyncExternalStore } from "react"
24
+ import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
24
25
  import type { ChartTone } from "@squadbase/vantage/components"
25
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
26
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
26
27
  import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
27
28
 
29
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
30
+
31
+ /**
32
+ * The display language of the parts and the defaults that follow it.
33
+ *
34
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
35
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
36
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
37
+ *
38
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
39
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
40
+ */
41
+
42
+ /** The language of the built-in wording: Japanese or English. */
43
+ type UiLanguage = "ja" | "en"
44
+
45
+ /** The locale used when `<html lang>` gives none (and on the server). */
46
+ const FALLBACK_LOCALE = "en-US"
47
+
48
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
49
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
50
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
51
+ }
52
+
53
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
54
+ function normalizeLocale(tag: string | null | undefined): string {
55
+ const value = tag?.trim()
56
+ if (!value) return FALLBACK_LOCALE
57
+ try {
58
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
59
+ } catch {
60
+ return FALLBACK_LOCALE
61
+ }
62
+ }
63
+
64
+ /**
65
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
66
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
67
+ */
68
+ function documentLocale(): string {
69
+ if (typeof document === "undefined") return FALLBACK_LOCALE
70
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
71
+ }
72
+
73
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
74
+ function resolveLocale(locale?: string | null): string {
75
+ return locale || documentLocale()
76
+ }
77
+
78
+ /** Picks the wording table of the locale's language. */
79
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
80
+ return table[uiLanguage(resolveLocale(locale))]
81
+ }
82
+
83
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
84
+ function defaultCurrency(locale?: string | null): string {
85
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
86
+ }
87
+
88
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
89
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
90
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
91
+ }
92
+
93
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
94
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
95
+
96
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
97
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
98
+
99
+ /**
100
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
101
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
102
+ * Monday for the rest.
103
+ */
104
+ function weekStartsOn(locale?: string | null): WeekDay {
105
+ const tag = normalizeLocale(resolveLocale(locale))
106
+ if (uiLanguage(tag) === "ja") return 1
107
+ try {
108
+ const info = new Intl.Locale(tag) as Intl.Locale & {
109
+ getWeekInfo?: () => { firstDay: number }
110
+ weekInfo?: { firstDay: number }
111
+ }
112
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
113
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
114
+ } catch {
115
+ // Fall through to the table below.
116
+ }
117
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
118
+ }
119
+
120
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
121
+
122
+ const listeners = new Set<() => void>()
123
+ let observer: MutationObserver | null = null
124
+
125
+ /** One observer on `<html lang>` for every subscribed part. */
126
+ function subscribe(onChange: () => void): () => void {
127
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
128
+ listeners.add(onChange)
129
+ if (!observer) {
130
+ observer = new MutationObserver(() => {
131
+ for (const listener of [...listeners]) listener()
132
+ })
133
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
134
+ }
135
+ return () => {
136
+ listeners.delete(onChange)
137
+ if (listeners.size === 0 && observer) {
138
+ observer.disconnect()
139
+ observer = null
140
+ }
141
+ }
142
+ }
143
+
144
+ const serverLocale = () => FALLBACK_LOCALE
145
+
146
+ /**
147
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
148
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
149
+ */
150
+ function useDocumentLocale(): string {
151
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
152
+ }
153
+
154
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
155
+ function useLocale(locale?: string | null): string {
156
+ const page = useDocumentLocale()
157
+ return locale || page
158
+ }
159
+
28
160
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
29
161
 
30
162
  /**
@@ -59,10 +191,23 @@ interface ChartTokens {
59
191
  border: string | null
60
192
  /** Tooltip surface. */
61
193
  popover: string | null
194
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
195
+ colors?: Record<string, string>
196
+ /** Actual inherited painting surface (may change without a token changing). */
197
+ surface?: string | null
62
198
  /** Tooltip text. */
63
199
  popoverForeground: string | null
64
200
  }
65
201
 
202
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
203
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
204
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
205
+ const OPTION_COLOR_TOKENS = [
206
+ "--background", "--card", "--muted", "--primary", "--destructive",
207
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
208
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
209
+ ]
210
+
66
211
  let normalizeContext: CanvasRenderingContext2D | null | undefined
67
212
 
68
213
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -118,8 +263,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
118
263
  const color = read(token)
119
264
  if (color) series.push(color)
120
265
  }
121
- return {
266
+ const tokens: ChartTokens = {
122
267
  series,
268
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
269
+ surface: readChartSurface(target),
123
270
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
124
271
  foreground: read("--foreground"),
125
272
  mutedForeground: read("--muted-foreground"),
@@ -127,6 +274,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
127
274
  popover: read("--popover"),
128
275
  popoverForeground: read("--popover-foreground"),
129
276
  }
277
+ tokenScopes.set(tokens, target)
278
+ return tokens
130
279
  }
131
280
 
132
281
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -134,6 +283,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
134
283
  if (a === b) return true
135
284
  if (!a || !b) return false
136
285
  return (
286
+ tokenScopes.get(a) === tokenScopes.get(b) &&
287
+ a.surface === b.surface &&
288
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
289
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
137
290
  a.series.length === b.series.length &&
138
291
  a.series.every((color, i) => color === b.series[i]) &&
139
292
  a.fontFamily === b.fontFamily &&
@@ -159,7 +312,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
159
312
  * attributes alone leaves the chart painted in the previous palette until a
160
313
  * reload — the tokens in the DOM are new, the canvas is not.
161
314
  */
162
- function watchChartTokens(onChange: () => void): () => void {
315
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
163
316
  if (typeof document === "undefined") return () => {}
164
317
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
165
318
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -174,8 +327,11 @@ function watchChartTokens(onChange: () => void): () => void {
174
327
 
175
328
  const themeObserver = new MutationObserver(schedule)
176
329
  const attributeFilter = ["class", "style", "data-theme"]
177
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
178
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
330
+ // Only the chart and its ancestors can change its inherited theme. Never observe
331
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
332
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
333
+ themeObserver.observe(node, { attributes: true, attributeFilter })
334
+ }
179
335
 
180
336
  const styleObserver = new MutationObserver(schedule)
181
337
  if (document.head) {
@@ -263,22 +419,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
263
419
  * components that build an ECharts option from token colours themselves (the
264
420
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
265
421
  */
266
- function useChartTokens(): ChartTokens | null {
422
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
267
423
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
268
424
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
269
425
  // replayed by React on every rebase and returns a fresh object each time, so the state
270
426
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
271
427
  const tokensRef = useRef(tokens)
272
428
  useEffect(() => {
429
+ const target = scope && "current" in scope ? scope.current : scope
273
430
  const sync = () => {
274
- const next = readChartTokens()
431
+ const next = readChartTokens(target)
275
432
  if (sameChartTokens(tokensRef.current, next)) return
276
433
  tokensRef.current = next
277
434
  setTokens(next)
278
435
  }
279
436
  sync()
280
- return watchChartTokens(sync)
281
- }, [])
437
+ return watchChartTokens(sync, target)
438
+ }, [scope])
282
439
  return tokens
283
440
  }
284
441
 
@@ -290,7 +447,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
290
447
  /**
291
448
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
292
449
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
293
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
450
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
294
451
  * other CSS colour is normalized like the tokens are. A colour that can't be
295
452
  * resolved comes back as `""`, which ECharts treats as "use the palette".
296
453
  */
@@ -301,12 +458,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
301
458
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
302
459
  if (TONE_NAME_RE.test(color.trim())) {
303
460
  if (typeof document === "undefined") return ""
304
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
461
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
462
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
305
463
  }
306
464
  if (typeof document === "undefined") return color
307
465
  let value = color.trim()
308
466
  const cssVar = CSS_VAR_RE.exec(value)
309
- const styles = getComputedStyle(document.documentElement)
467
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
310
468
  if (cssVar) {
311
469
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
312
470
  } else if (value.includes("var(")) {
@@ -352,7 +510,7 @@ function readChartSurface(el: Element | null): string | null {
352
510
  if (color) return color
353
511
  }
354
512
  }
355
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
513
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
356
514
  }
357
515
 
358
516
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -366,9 +524,10 @@ const WIDTH_BUCKET = 40
366
524
  */
367
525
  function useChartSurface<E extends Element>(
368
526
  forwarded: ForwardedRef<E>,
369
- tokens: ChartTokens | null,
370
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
527
+ tokens?: ChartTokens | null,
528
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
371
529
  const [node, setNode] = useState<E | null>(null)
530
+ const scopedTokens = useChartTokens(node)
372
531
  const [surface, setSurface] = useState<string | undefined>(undefined)
373
532
  const [width, setWidth] = useState<number | undefined>(undefined)
374
533
  const ref = useCallback(
@@ -382,7 +541,7 @@ function useChartSurface<E extends Element>(
382
541
  useEffect(() => {
383
542
  if (!node) return
384
543
  setSurface(readChartSurface(node) ?? undefined)
385
- }, [node, tokens])
544
+ }, [node, tokens, scopedTokens])
386
545
  useEffect(() => {
387
546
  if (!node || typeof ResizeObserver === "undefined") return
388
547
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -391,7 +550,7 @@ function useChartSurface<E extends Element>(
391
550
  observer.observe(node)
392
551
  return () => observer.disconnect()
393
552
  }, [node])
394
- return { ref, surface, width }
553
+ return { ref, surface, width, tokens: scopedTokens }
395
554
  }
396
555
 
397
556
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -501,9 +660,10 @@ function resolveValueFormatter(
501
660
  function resolveAxisFormatter(
502
661
  valueFormatter: ValueFormatter | undefined,
503
662
  format: ValueFormat | undefined,
504
- locale: string = DEFAULT_LOCALE,
663
+ localeArg?: string,
505
664
  ): ValueFormatter {
506
665
  if (valueFormatter) return valueFormatter
666
+ const locale = resolveLocale(localeArg)
507
667
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
508
668
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
509
669
  const currency = "currency" in spec ? spec.currency : undefined
@@ -512,40 +672,75 @@ function resolveAxisFormatter(
512
672
  return (value) => formatValue(value, short, locale)
513
673
  }
514
674
 
515
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
516
- const JA_UNITS = [
517
- { char: "兆", size: 1e12 },
518
- { char: "億", size: 1e8 },
519
- { char: "万", size: 1e4 },
520
- ] as const
675
+ interface CompactUnit {
676
+ token: string
677
+ size: number
678
+ }
521
679
 
522
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
680
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
681
+
682
+ /**
683
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
684
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
685
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
686
+ */
687
+ function compactUnits(locale: string): CompactUnit[] | null {
688
+ const cached = compactUnitCache.get(locale)
689
+ if (cached !== undefined) return cached
690
+ let units: CompactUnit[] | null = []
691
+ try {
692
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
693
+ const seen = new Set<string>()
694
+ for (let exp = 3; exp <= 15; exp++) {
695
+ const parts = compact.formatToParts(10 ** exp)
696
+ const at = parts.findIndex((p) => p.type === "compact")
697
+ if (at === -1) continue
698
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
699
+ units = null
700
+ break
701
+ }
702
+ const token = parts[at]!.value
703
+ if (seen.has(token)) continue
704
+ seen.add(token)
705
+ units.push({ token, size: 10 ** exp })
706
+ }
707
+ } catch {
708
+ units = null
709
+ }
710
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
711
+ compactUnitCache.set(locale, sorted)
712
+ return sorted
713
+ }
714
+
715
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
523
716
 
524
717
  /**
525
718
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
526
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
527
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
528
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
719
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
720
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
721
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
722
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
529
723
  */
530
724
  function resolveAxisTickFormatter(
531
725
  valueFormatter: ValueFormatter | undefined,
532
726
  format: ValueFormat | undefined,
533
- locale: string = DEFAULT_LOCALE,
727
+ localeArg?: string,
534
728
  ): (top: number) => ValueFormatter {
729
+ const locale = resolveLocale(localeArg)
535
730
  const base = resolveAxisFormatter(valueFormatter, format, locale)
536
731
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
537
- const aligns =
538
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
539
- if (!aligns) return () => base
732
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
733
+ if (!units) return () => base
540
734
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
735
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
541
736
  return (top) => {
542
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
737
+ const unit = units.find((u) => Math.abs(top) >= u.size)
543
738
  if (!unit) return base
544
739
  return (value) => {
545
740
  const text = base(value)
546
741
  if (value === 0 || !Number.isFinite(value)) return text
547
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
548
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
742
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
743
+ return text.replace(numberToken, () => scaled)
549
744
  }
550
745
  }
551
746
  }
@@ -574,8 +769,20 @@ function clamp(value: number, min: number, max: number): number {
574
769
  /** How many categories a chart colours individually; the rest fold into「その他」. */
575
770
  const MAX_CHART_SERIES = 5
576
771
 
577
- /** The series name the folded categories get. */
578
- const OTHER_SERIES_LABEL = "その他"
772
+ /** The series name the folded categories get, by language. */
773
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
774
+
775
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
776
+ function otherSeriesLabel(locale?: string): string {
777
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
778
+ }
779
+
780
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
781
+
782
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
783
+ function isOtherName(name: string | null | undefined): boolean {
784
+ return typeof name === "string" && OTHER_NAMES.has(name)
785
+ }
579
786
 
580
787
  interface AggregatedItems<Item extends { name: string; value: number }> {
581
788
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -595,7 +802,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
595
802
  function aggregateTopN<Item extends { name: string; value: number }>(
596
803
  items: readonly Item[],
597
804
  n: number = MAX_CHART_SERIES,
598
- otherLabel: string = OTHER_SERIES_LABEL,
805
+ otherLabel: string = otherSeriesLabel(),
599
806
  ): AggregatedItems<Item> {
600
807
  if (items.length <= n) return { top: [...items], rest: [], other: null }
601
808
  const ranked = items
@@ -624,7 +831,7 @@ function aggregateSeries(
624
831
  data: readonly Record<string, unknown>[],
625
832
  categories: readonly string[],
626
833
  n: number = MAX_CHART_SERIES,
627
- otherLabel: string = OTHER_SERIES_LABEL,
834
+ otherLabel: string = otherSeriesLabel(),
628
835
  ): AggregatedSeries {
629
836
  const totals = categories.map((name) => ({
630
837
  name,
@@ -656,12 +863,14 @@ function aggregateSeriesKeeping(
656
863
  categories: readonly string[],
657
864
  keep: string | undefined,
658
865
  n: number = MAX_CHART_SERIES,
866
+ otherLabel: string = otherSeriesLabel(),
659
867
  ): AggregatedSeries {
660
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
868
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
661
869
  const aggregated = aggregateSeries(
662
870
  data,
663
871
  categories.filter((c) => c !== keep),
664
872
  n,
873
+ otherLabel,
665
874
  )
666
875
  return { ...aggregated, categories: [...aggregated.categories, keep] }
667
876
  }
@@ -697,7 +906,7 @@ function seriesColorsWithComparison(
697
906
  * Colour per drawn series: the record's token for that category, else the
698
907
  * array's token at the category's position in `allCategories` (cycled), else
699
908
  * the token palette by drawn position. `otherCategory` — and a category the
700
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
909
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
701
910
  */
702
911
  function seriesColorsFor(
703
912
  categories: readonly string[],
@@ -709,7 +918,7 @@ function seriesColorsFor(
709
918
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
710
919
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
711
920
  return categories.map((category, i) => {
712
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
921
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
713
922
  const fromRecord = record?.[category]
714
923
  if (fromRecord) return fromRecord
715
924
  if (list && list.length > 0) {
@@ -756,7 +965,7 @@ interface DonutSlicesConfig {
756
965
  */
757
966
  function donutSlices(
758
967
  data: readonly Record<string, unknown>[],
759
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
968
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
760
969
  ): DonutSlice[] {
761
970
  const items = data
762
971
  .map((row, i) => {
@@ -769,7 +978,8 @@ function donutSlices(
769
978
  const slices: DonutSlice[] = top.map((item, rank) => ({
770
979
  name: item.name,
771
980
  value: item.value,
772
- color: palette[rank % palette.length]!,
981
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
982
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
773
983
  isOther: false,
774
984
  row: item.row,
775
985
  }))
@@ -842,7 +1052,7 @@ interface BarListRowsConfig extends BarListOptions {
842
1052
  */
843
1053
  function barListRows<Row extends object>(
844
1054
  data: readonly Row[],
845
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1055
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
846
1056
  ): BarListRow<Row>[] {
847
1057
  const items = data.map((row): BarListRow<Row> => {
848
1058
  const cells = row as Record<string, unknown>
@@ -888,19 +1098,21 @@ const BAR_LIST_OTHER_TINT = 30
888
1098
 
889
1099
  /**
890
1100
  * The fill of each BarList row: all real rows in the same tint of `color`
891
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1101
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
892
1102
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
893
1103
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
894
1104
  * for that row. A tint, not the solid token, so the label printed over the bar
895
1105
  * keeps its contrast.
896
1106
  */
897
1107
  function barListFills(
898
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1108
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
899
1109
  color: ChartColor,
900
1110
  otherColor: string,
901
1111
  ): string[] {
902
1112
  return rows.map((row) =>
903
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1113
+ row.isOther || isOtherName(row.name)
1114
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1115
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
904
1116
  )
905
1117
  }
906
1118
 
@@ -1116,11 +1328,30 @@ interface ChartSummaryInput {
1116
1328
  format?: ValueFormat
1117
1329
  /** Wins over `format`. */
1118
1330
  valueFormatter?: ValueFormatter
1331
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1332
+ locale?: string
1333
+ }
1334
+
1335
+ const SUMMARY_TEXT = {
1336
+ ja: {
1337
+ up: "、前の点より増加",
1338
+ down: "、前の点より減少",
1339
+ flat: "、前の点と横ばい",
1340
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1341
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1342
+ },
1343
+ en: {
1344
+ up: ", up from the previous point",
1345
+ down: ", down from the previous point",
1346
+ flat: ", flat from the previous point",
1347
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1348
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1349
+ },
1119
1350
  }
1120
1351
 
1121
1352
  /**
1122
1353
  * One-sentence summary built from the rows, for `summary="auto"`:
1123
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1354
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1124
1355
  * computed from the last two non-empty points of the first series — never
1125
1356
  * written by hand, so it cannot contradict the data. `undefined` when there is
1126
1357
  * no usable point.
@@ -1136,26 +1367,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1136
1367
  }
1137
1368
  const latest = points[points.length - 1]
1138
1369
  if (!latest) return undefined
1139
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1370
+ const locale = resolveLocale(input.locale)
1371
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1372
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1140
1373
  const prev = points[points.length - 2]
1141
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1374
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1142
1375
  const head = input.title ? `${input.title}: ` : ""
1143
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1376
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1144
1377
  }
1145
1378
 
1146
1379
  /**
1147
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1148
- * (largest first). `undefined` when there is nothing to describe.
1380
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1381
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1149
1382
  */
1150
1383
  function buildShareSummary(
1151
1384
  slices: readonly { name: string; value: number }[],
1152
1385
  title?: string,
1386
+ locale?: string,
1153
1387
  ): string | undefined {
1154
1388
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1155
1389
  const top = slices[0]
1156
1390
  if (!top || total <= 0) return undefined
1157
1391
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1158
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1392
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1159
1393
  }
1160
1394
 
1161
1395
  /* ---------- end of inlined code ---------- */
@@ -1166,7 +1400,10 @@ export type ChartSkeletonVariant = "line" | "area" | "bar" | "donut" | "list"
1166
1400
  /** The plot height of every typed chart unless `height` says otherwise (px). */
1167
1401
  export const DEFAULT_CHART_HEIGHT = 288
1168
1402
 
1169
- const DEFAULT_EMPTY_TITLE = "表示できるデータがありません"
1403
+ const TEXT = {
1404
+ ja: { emptyTitle: "表示できるデータがありません", loading: "読み込み中" },
1405
+ en: { emptyTitle: "No data to display", loading: "Loading" },
1406
+ }
1170
1407
 
1171
1408
  export interface ChartFrameProps extends Omit<HTMLAttributes<HTMLElement>, "title" | "children"> {
1172
1409
  /** グラフの見出し。プロット領域の `aria-labelledby` になる。 */
@@ -1218,6 +1455,7 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
1218
1455
  ref,
1219
1456
  ) {
1220
1457
  const id = useId()
1458
+ const t = TEXT[uiLanguage(useLocale())]
1221
1459
  const titleId = title ? `${id}-title` : undefined
1222
1460
  const summaryId = summary ? `${id}-summary` : undefined
1223
1461
  const state = isLoading ? "loading" : isEmpty ? "empty" : "ready"
@@ -1251,9 +1489,9 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
1251
1489
  style={{ height: typeof height === "number" ? `${height}px` : height }}
1252
1490
  >
1253
1491
  {state === "loading" ? (
1254
- <ChartSkeleton variant={variant} />
1492
+ <ChartSkeleton variant={variant} loadingText={t.loading} />
1255
1493
  ) : state === "empty" ? (
1256
- <ChartEmpty {...emptyState} />
1494
+ <ChartEmpty {...emptyState} defaultTitle={t.emptyTitle} />
1257
1495
  ) : (
1258
1496
  <ChartFrameContext.Provider value>{children}</ChartFrameContext.Provider>
1259
1497
  )}
@@ -1272,7 +1510,7 @@ const BAR_HEIGHTS = [58, 74, 46, 88, 64, 80, 52]
1272
1510
  const LIST_WIDTHS = [92, 76, 64, 48, 36]
1273
1511
  const LINE_POINTS = "0,70 16,58 33,62 50,40 66,46 83,28 100,34"
1274
1512
 
1275
- function ChartSkeleton({ variant }: { variant: ChartSkeletonVariant }) {
1513
+ function ChartSkeleton({ variant, loadingText }: { variant: ChartSkeletonVariant; loadingText: string }) {
1276
1514
  const pulse = "animate-pulse motion-reduce:animate-none"
1277
1515
  return (
1278
1516
  <div data-slot="chart-skeleton" data-variant={variant} className="absolute inset-0 flex flex-col gap-3">
@@ -1306,7 +1544,7 @@ function ChartSkeleton({ variant }: { variant: ChartSkeletonVariant }) {
1306
1544
  </div>
1307
1545
  )}
1308
1546
  </div>
1309
- <span className="sr-only">読み込み中</span>
1547
+ <span className="sr-only">{loadingText}</span>
1310
1548
  </div>
1311
1549
  )
1312
1550
  }
@@ -1352,13 +1590,13 @@ function ChartDataTableView({ table, fallbackCaption }: { table: ChartDataTable;
1352
1590
 
1353
1591
  /* ------------------------------------------------------------------- empty */
1354
1592
 
1355
- function ChartEmpty({ title, description, action }: ChartEmptyState) {
1593
+ function ChartEmpty({ title, description, action, defaultTitle }: ChartEmptyState & { defaultTitle: string }) {
1356
1594
  return (
1357
1595
  <div
1358
1596
  data-slot="chart-empty"
1359
1597
  className="absolute inset-0 flex flex-col items-center justify-center gap-1 rounded-md border border-dashed border-border p-6 text-center"
1360
1598
  >
1361
- <p className="text-sm font-medium text-balance">{title ?? DEFAULT_EMPTY_TITLE}</p>
1599
+ <p className="text-sm font-medium text-balance">{title ?? defaultTitle}</p>
1362
1600
  {description && <p className="max-w-prose text-sm text-balance text-muted-foreground">{description}</p>}
1363
1601
  {action && <div className="mt-3">{action}</div>}
1364
1602
  </div>