@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
@@ -22,14 +22,147 @@ import { forwardRef, useEffect, useMemo, useState, type ForwardedRef, type React
22
22
  import { ChartFrame } from "@squadbase/vantage/components"
23
23
  import { EChart } from "@squadbase/vantage/components"
24
24
  import type { ChartCommonProps, ChartEventPayload, ChartSeriesColor } from "@squadbase/vantage/components"
25
- import { createContext, useCallback, useRef } from "react"
25
+ import { useSyncExternalStore } from "react"
26
+ import { createContext, useCallback, useRef, type RefObject } from "react"
26
27
  import type { ChartTone } from "@squadbase/vantage/components"
27
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
28
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
28
29
  import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
29
30
  import type { EChartsOption } from "@squadbase/vantage/components"
30
31
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
32
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
31
33
  import type { ChartThreshold, ChartAnnotation, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
32
34
 
35
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
36
+
37
+ /**
38
+ * The display language of the parts and the defaults that follow it.
39
+ *
40
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
41
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
42
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
43
+ *
44
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
45
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
46
+ */
47
+
48
+ /** The language of the built-in wording: Japanese or English. */
49
+ type UiLanguage = "ja" | "en"
50
+
51
+ /** The locale used when `<html lang>` gives none (and on the server). */
52
+ const FALLBACK_LOCALE = "en-US"
53
+
54
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
55
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
56
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
57
+ }
58
+
59
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
60
+ function normalizeLocale(tag: string | null | undefined): string {
61
+ const value = tag?.trim()
62
+ if (!value) return FALLBACK_LOCALE
63
+ try {
64
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
65
+ } catch {
66
+ return FALLBACK_LOCALE
67
+ }
68
+ }
69
+
70
+ /**
71
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
72
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
73
+ */
74
+ function documentLocale(): string {
75
+ if (typeof document === "undefined") return FALLBACK_LOCALE
76
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
77
+ }
78
+
79
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
80
+ function resolveLocale(locale?: string | null): string {
81
+ return locale || documentLocale()
82
+ }
83
+
84
+ /** Picks the wording table of the locale's language. */
85
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
86
+ return table[uiLanguage(resolveLocale(locale))]
87
+ }
88
+
89
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
90
+ function defaultCurrency(locale?: string | null): string {
91
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
92
+ }
93
+
94
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
95
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
96
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
97
+ }
98
+
99
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
100
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
101
+
102
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
103
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
104
+
105
+ /**
106
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
107
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
108
+ * Monday for the rest.
109
+ */
110
+ function weekStartsOn(locale?: string | null): WeekDay {
111
+ const tag = normalizeLocale(resolveLocale(locale))
112
+ if (uiLanguage(tag) === "ja") return 1
113
+ try {
114
+ const info = new Intl.Locale(tag) as Intl.Locale & {
115
+ getWeekInfo?: () => { firstDay: number }
116
+ weekInfo?: { firstDay: number }
117
+ }
118
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
119
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
120
+ } catch {
121
+ // Fall through to the table below.
122
+ }
123
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
124
+ }
125
+
126
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
127
+
128
+ const listeners = new Set<() => void>()
129
+ let observer: MutationObserver | null = null
130
+
131
+ /** One observer on `<html lang>` for every subscribed part. */
132
+ function subscribe(onChange: () => void): () => void {
133
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
134
+ listeners.add(onChange)
135
+ if (!observer) {
136
+ observer = new MutationObserver(() => {
137
+ for (const listener of [...listeners]) listener()
138
+ })
139
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
140
+ }
141
+ return () => {
142
+ listeners.delete(onChange)
143
+ if (listeners.size === 0 && observer) {
144
+ observer.disconnect()
145
+ observer = null
146
+ }
147
+ }
148
+ }
149
+
150
+ const serverLocale = () => FALLBACK_LOCALE
151
+
152
+ /**
153
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
154
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
155
+ */
156
+ function useDocumentLocale(): string {
157
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
158
+ }
159
+
160
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
161
+ function useLocale(locale?: string | null): string {
162
+ const page = useDocumentLocale()
163
+ return locale || page
164
+ }
165
+
33
166
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
34
167
 
35
168
  /**
@@ -64,10 +197,23 @@ interface ChartTokens {
64
197
  border: string | null
65
198
  /** Tooltip surface. */
66
199
  popover: string | null
200
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
201
+ colors?: Record<string, string>
202
+ /** Actual inherited painting surface (may change without a token changing). */
203
+ surface?: string | null
67
204
  /** Tooltip text. */
68
205
  popoverForeground: string | null
69
206
  }
70
207
 
208
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
209
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
210
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
211
+ const OPTION_COLOR_TOKENS = [
212
+ "--background", "--card", "--muted", "--primary", "--destructive",
213
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
214
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
215
+ ]
216
+
71
217
  let normalizeContext: CanvasRenderingContext2D | null | undefined
72
218
 
73
219
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -123,8 +269,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
123
269
  const color = read(token)
124
270
  if (color) series.push(color)
125
271
  }
126
- return {
272
+ const tokens: ChartTokens = {
127
273
  series,
274
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
275
+ surface: readChartSurface(target),
128
276
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
129
277
  foreground: read("--foreground"),
130
278
  mutedForeground: read("--muted-foreground"),
@@ -132,6 +280,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
132
280
  popover: read("--popover"),
133
281
  popoverForeground: read("--popover-foreground"),
134
282
  }
283
+ tokenScopes.set(tokens, target)
284
+ return tokens
135
285
  }
136
286
 
137
287
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -139,6 +289,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
139
289
  if (a === b) return true
140
290
  if (!a || !b) return false
141
291
  return (
292
+ tokenScopes.get(a) === tokenScopes.get(b) &&
293
+ a.surface === b.surface &&
294
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
295
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
142
296
  a.series.length === b.series.length &&
143
297
  a.series.every((color, i) => color === b.series[i]) &&
144
298
  a.fontFamily === b.fontFamily &&
@@ -164,7 +318,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
164
318
  * attributes alone leaves the chart painted in the previous palette until a
165
319
  * reload — the tokens in the DOM are new, the canvas is not.
166
320
  */
167
- function watchChartTokens(onChange: () => void): () => void {
321
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
168
322
  if (typeof document === "undefined") return () => {}
169
323
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
170
324
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -179,8 +333,11 @@ function watchChartTokens(onChange: () => void): () => void {
179
333
 
180
334
  const themeObserver = new MutationObserver(schedule)
181
335
  const attributeFilter = ["class", "style", "data-theme"]
182
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
183
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
336
+ // Only the chart and its ancestors can change its inherited theme. Never observe
337
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
338
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
339
+ themeObserver.observe(node, { attributes: true, attributeFilter })
340
+ }
184
341
 
185
342
  const styleObserver = new MutationObserver(schedule)
186
343
  if (document.head) {
@@ -268,22 +425,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
268
425
  * components that build an ECharts option from token colours themselves (the
269
426
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
270
427
  */
271
- function useChartTokens(): ChartTokens | null {
428
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
272
429
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
273
430
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
274
431
  // replayed by React on every rebase and returns a fresh object each time, so the state
275
432
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
276
433
  const tokensRef = useRef(tokens)
277
434
  useEffect(() => {
435
+ const target = scope && "current" in scope ? scope.current : scope
278
436
  const sync = () => {
279
- const next = readChartTokens()
437
+ const next = readChartTokens(target)
280
438
  if (sameChartTokens(tokensRef.current, next)) return
281
439
  tokensRef.current = next
282
440
  setTokens(next)
283
441
  }
284
442
  sync()
285
- return watchChartTokens(sync)
286
- }, [])
443
+ return watchChartTokens(sync, target)
444
+ }, [scope])
287
445
  return tokens
288
446
  }
289
447
 
@@ -295,7 +453,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
295
453
  /**
296
454
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
297
455
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
298
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
456
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
299
457
  * other CSS colour is normalized like the tokens are. A colour that can't be
300
458
  * resolved comes back as `""`, which ECharts treats as "use the palette".
301
459
  */
@@ -306,12 +464,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
306
464
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
307
465
  if (TONE_NAME_RE.test(color.trim())) {
308
466
  if (typeof document === "undefined") return ""
309
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
467
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
468
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
310
469
  }
311
470
  if (typeof document === "undefined") return color
312
471
  let value = color.trim()
313
472
  const cssVar = CSS_VAR_RE.exec(value)
314
- const styles = getComputedStyle(document.documentElement)
473
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
315
474
  if (cssVar) {
316
475
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
317
476
  } else if (value.includes("var(")) {
@@ -357,7 +516,7 @@ function readChartSurface(el: Element | null): string | null {
357
516
  if (color) return color
358
517
  }
359
518
  }
360
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
519
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
361
520
  }
362
521
 
363
522
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -371,9 +530,10 @@ const WIDTH_BUCKET = 40
371
530
  */
372
531
  function useChartSurface<E extends Element>(
373
532
  forwarded: ForwardedRef<E>,
374
- tokens: ChartTokens | null,
375
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
533
+ tokens?: ChartTokens | null,
534
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
376
535
  const [node, setNode] = useState<E | null>(null)
536
+ const scopedTokens = useChartTokens(node)
377
537
  const [surface, setSurface] = useState<string | undefined>(undefined)
378
538
  const [width, setWidth] = useState<number | undefined>(undefined)
379
539
  const ref = useCallback(
@@ -387,7 +547,7 @@ function useChartSurface<E extends Element>(
387
547
  useEffect(() => {
388
548
  if (!node) return
389
549
  setSurface(readChartSurface(node) ?? undefined)
390
- }, [node, tokens])
550
+ }, [node, tokens, scopedTokens])
391
551
  useEffect(() => {
392
552
  if (!node || typeof ResizeObserver === "undefined") return
393
553
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -396,7 +556,7 @@ function useChartSurface<E extends Element>(
396
556
  observer.observe(node)
397
557
  return () => observer.disconnect()
398
558
  }, [node])
399
- return { ref, surface, width }
559
+ return { ref, surface, width, tokens: scopedTokens }
400
560
  }
401
561
 
402
562
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -506,9 +666,10 @@ function resolveValueFormatter(
506
666
  function resolveAxisFormatter(
507
667
  valueFormatter: ValueFormatter | undefined,
508
668
  format: ValueFormat | undefined,
509
- locale: string = DEFAULT_LOCALE,
669
+ localeArg?: string,
510
670
  ): ValueFormatter {
511
671
  if (valueFormatter) return valueFormatter
672
+ const locale = resolveLocale(localeArg)
512
673
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
513
674
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
514
675
  const currency = "currency" in spec ? spec.currency : undefined
@@ -517,40 +678,75 @@ function resolveAxisFormatter(
517
678
  return (value) => formatValue(value, short, locale)
518
679
  }
519
680
 
520
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
521
- const JA_UNITS = [
522
- { char: "兆", size: 1e12 },
523
- { char: "億", size: 1e8 },
524
- { char: "万", size: 1e4 },
525
- ] as const
681
+ interface CompactUnit {
682
+ token: string
683
+ size: number
684
+ }
685
+
686
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
526
687
 
527
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
688
+ /**
689
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
690
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
691
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
692
+ */
693
+ function compactUnits(locale: string): CompactUnit[] | null {
694
+ const cached = compactUnitCache.get(locale)
695
+ if (cached !== undefined) return cached
696
+ let units: CompactUnit[] | null = []
697
+ try {
698
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
699
+ const seen = new Set<string>()
700
+ for (let exp = 3; exp <= 15; exp++) {
701
+ const parts = compact.formatToParts(10 ** exp)
702
+ const at = parts.findIndex((p) => p.type === "compact")
703
+ if (at === -1) continue
704
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
705
+ units = null
706
+ break
707
+ }
708
+ const token = parts[at]!.value
709
+ if (seen.has(token)) continue
710
+ seen.add(token)
711
+ units.push({ token, size: 10 ** exp })
712
+ }
713
+ } catch {
714
+ units = null
715
+ }
716
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
717
+ compactUnitCache.set(locale, sorted)
718
+ return sorted
719
+ }
720
+
721
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
528
722
 
529
723
  /**
530
724
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
531
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
532
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
533
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
725
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
726
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
727
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
728
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
534
729
  */
535
730
  function resolveAxisTickFormatter(
536
731
  valueFormatter: ValueFormatter | undefined,
537
732
  format: ValueFormat | undefined,
538
- locale: string = DEFAULT_LOCALE,
733
+ localeArg?: string,
539
734
  ): (top: number) => ValueFormatter {
735
+ const locale = resolveLocale(localeArg)
540
736
  const base = resolveAxisFormatter(valueFormatter, format, locale)
541
737
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
542
- const aligns =
543
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
544
- if (!aligns) return () => base
738
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
739
+ if (!units) return () => base
545
740
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
741
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
546
742
  return (top) => {
547
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
743
+ const unit = units.find((u) => Math.abs(top) >= u.size)
548
744
  if (!unit) return base
549
745
  return (value) => {
550
746
  const text = base(value)
551
747
  if (value === 0 || !Number.isFinite(value)) return text
552
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
553
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
748
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
749
+ return text.replace(numberToken, () => scaled)
554
750
  }
555
751
  }
556
752
  }
@@ -579,8 +775,20 @@ function clamp(value: number, min: number, max: number): number {
579
775
  /** How many categories a chart colours individually; the rest fold into「その他」. */
580
776
  const MAX_CHART_SERIES = 5
581
777
 
582
- /** The series name the folded categories get. */
583
- const OTHER_SERIES_LABEL = "その他"
778
+ /** The series name the folded categories get, by language. */
779
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
780
+
781
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
782
+ function otherSeriesLabel(locale?: string): string {
783
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
784
+ }
785
+
786
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
787
+
788
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
789
+ function isOtherName(name: string | null | undefined): boolean {
790
+ return typeof name === "string" && OTHER_NAMES.has(name)
791
+ }
584
792
 
585
793
  interface AggregatedItems<Item extends { name: string; value: number }> {
586
794
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -600,7 +808,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
600
808
  function aggregateTopN<Item extends { name: string; value: number }>(
601
809
  items: readonly Item[],
602
810
  n: number = MAX_CHART_SERIES,
603
- otherLabel: string = OTHER_SERIES_LABEL,
811
+ otherLabel: string = otherSeriesLabel(),
604
812
  ): AggregatedItems<Item> {
605
813
  if (items.length <= n) return { top: [...items], rest: [], other: null }
606
814
  const ranked = items
@@ -629,7 +837,7 @@ function aggregateSeries(
629
837
  data: readonly Record<string, unknown>[],
630
838
  categories: readonly string[],
631
839
  n: number = MAX_CHART_SERIES,
632
- otherLabel: string = OTHER_SERIES_LABEL,
840
+ otherLabel: string = otherSeriesLabel(),
633
841
  ): AggregatedSeries {
634
842
  const totals = categories.map((name) => ({
635
843
  name,
@@ -661,12 +869,14 @@ function aggregateSeriesKeeping(
661
869
  categories: readonly string[],
662
870
  keep: string | undefined,
663
871
  n: number = MAX_CHART_SERIES,
872
+ otherLabel: string = otherSeriesLabel(),
664
873
  ): AggregatedSeries {
665
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
874
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
666
875
  const aggregated = aggregateSeries(
667
876
  data,
668
877
  categories.filter((c) => c !== keep),
669
878
  n,
879
+ otherLabel,
670
880
  )
671
881
  return { ...aggregated, categories: [...aggregated.categories, keep] }
672
882
  }
@@ -702,7 +912,7 @@ function seriesColorsWithComparison(
702
912
  * Colour per drawn series: the record's token for that category, else the
703
913
  * array's token at the category's position in `allCategories` (cycled), else
704
914
  * the token palette by drawn position. `otherCategory` — and a category the
705
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
915
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
706
916
  */
707
917
  function seriesColorsFor(
708
918
  categories: readonly string[],
@@ -714,7 +924,7 @@ function seriesColorsFor(
714
924
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
715
925
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
716
926
  return categories.map((category, i) => {
717
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
927
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
718
928
  const fromRecord = record?.[category]
719
929
  if (fromRecord) return fromRecord
720
930
  if (list && list.length > 0) {
@@ -761,7 +971,7 @@ interface DonutSlicesConfig {
761
971
  */
762
972
  function donutSlices(
763
973
  data: readonly Record<string, unknown>[],
764
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
974
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
765
975
  ): DonutSlice[] {
766
976
  const items = data
767
977
  .map((row, i) => {
@@ -774,7 +984,8 @@ function donutSlices(
774
984
  const slices: DonutSlice[] = top.map((item, rank) => ({
775
985
  name: item.name,
776
986
  value: item.value,
777
- color: palette[rank % palette.length]!,
987
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
988
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
778
989
  isOther: false,
779
990
  row: item.row,
780
991
  }))
@@ -847,7 +1058,7 @@ interface BarListRowsConfig extends BarListOptions {
847
1058
  */
848
1059
  function barListRows<Row extends object>(
849
1060
  data: readonly Row[],
850
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1061
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
851
1062
  ): BarListRow<Row>[] {
852
1063
  const items = data.map((row): BarListRow<Row> => {
853
1064
  const cells = row as Record<string, unknown>
@@ -893,19 +1104,21 @@ const BAR_LIST_OTHER_TINT = 30
893
1104
 
894
1105
  /**
895
1106
  * The fill of each BarList row: all real rows in the same tint of `color`
896
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1107
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
897
1108
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
898
1109
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
899
1110
  * for that row. A tint, not the solid token, so the label printed over the bar
900
1111
  * keeps its contrast.
901
1112
  */
902
1113
  function barListFills(
903
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1114
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
904
1115
  color: ChartColor,
905
1116
  otherColor: string,
906
1117
  ): string[] {
907
1118
  return rows.map((row) =>
908
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1119
+ row.isOther || isOtherName(row.name)
1120
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1121
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
909
1122
  )
910
1123
  }
911
1124
 
@@ -1121,11 +1334,30 @@ interface ChartSummaryInput {
1121
1334
  format?: ValueFormat
1122
1335
  /** Wins over `format`. */
1123
1336
  valueFormatter?: ValueFormatter
1337
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1338
+ locale?: string
1339
+ }
1340
+
1341
+ const SUMMARY_TEXT = {
1342
+ ja: {
1343
+ up: "、前の点より増加",
1344
+ down: "、前の点より減少",
1345
+ flat: "、前の点と横ばい",
1346
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1347
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1348
+ },
1349
+ en: {
1350
+ up: ", up from the previous point",
1351
+ down: ", down from the previous point",
1352
+ flat: ", flat from the previous point",
1353
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1354
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1355
+ },
1124
1356
  }
1125
1357
 
1126
1358
  /**
1127
1359
  * One-sentence summary built from the rows, for `summary="auto"`:
1128
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1360
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1129
1361
  * computed from the last two non-empty points of the first series — never
1130
1362
  * written by hand, so it cannot contradict the data. `undefined` when there is
1131
1363
  * no usable point.
@@ -1141,30 +1373,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1141
1373
  }
1142
1374
  const latest = points[points.length - 1]
1143
1375
  if (!latest) return undefined
1144
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1376
+ const locale = resolveLocale(input.locale)
1377
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1378
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1145
1379
  const prev = points[points.length - 2]
1146
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1380
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1147
1381
  const head = input.title ? `${input.title}: ` : ""
1148
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1382
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1149
1383
  }
1150
1384
 
1151
1385
  /**
1152
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1153
- * (largest first). `undefined` when there is nothing to describe.
1386
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1387
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1154
1388
  */
1155
1389
  function buildShareSummary(
1156
1390
  slices: readonly { name: string; value: number }[],
1157
1391
  title?: string,
1392
+ locale?: string,
1158
1393
  ): string | undefined {
1159
1394
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1160
1395
  const top = slices[0]
1161
1396
  if (!top || total <= 0) return undefined
1162
1397
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1163
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1398
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1164
1399
  }
1165
1400
 
1166
1401
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1167
1402
 
1403
+ /** The built-in words of the option builders, by language. */
1404
+ const CHART_TEXT = {
1405
+ ja: {
1406
+ target: "目標",
1407
+ forecast: "予測",
1408
+ total: "合計",
1409
+ provisional: "速報",
1410
+ noComparison: "比較なし",
1411
+ share: (share: string) => `(${share})`,
1412
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1413
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1414
+ },
1415
+ en: {
1416
+ target: "Target",
1417
+ forecast: "Forecast",
1418
+ total: "Total",
1419
+ provisional: "Preliminary",
1420
+ noComparison: "No comparison",
1421
+ share: (share: string) => ` (${share})`,
1422
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1423
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1424
+ },
1425
+ } satisfies Record<UiLanguage, unknown>
1426
+
1427
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1428
+
1168
1429
  type CartesianChartKind = "area" | "bar" | "line"
1169
1430
  type CartesianStackType = "default" | "stacked" | "percent"
1170
1431
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1349,7 +1610,7 @@ interface CartesianChartConfig {
1349
1610
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1350
1611
  */
1351
1612
  axisTicks?: (top: number) => (value: number) => string
1352
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1613
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1353
1614
  locale?: string
1354
1615
  type?: CartesianStackType
1355
1616
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1587,11 +1848,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1587
1848
  */
1588
1849
  function timeAxisLabels(
1589
1850
  values: readonly unknown[],
1590
- locale: string = DEFAULT_LOCALE,
1851
+ localeArg?: string,
1591
1852
  shown: (i: number) => boolean = () => true,
1592
1853
  ): string[] | null {
1593
1854
  const parsed = parseTimeIndex(values)
1594
1855
  if (!parsed) return null
1856
+ const locale = resolveLocale(localeArg)
1595
1857
  const { granularity, times } = parsed
1596
1858
  let prevYear: number | null = null
1597
1859
  return times.map((t, i) => {
@@ -1614,9 +1876,10 @@ function timeAxisLabels(
1614
1876
  }
1615
1877
 
1616
1878
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1617
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1879
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1618
1880
  const parsed = parseTimeIndex(values)
1619
1881
  if (!parsed) return null
1882
+ const locale = resolveLocale(localeArg)
1620
1883
  const options: Intl.DateTimeFormatOptions =
1621
1884
  parsed.granularity === "year"
1622
1885
  ? { year: "numeric" }
@@ -1756,7 +2019,9 @@ function tooltipHtml(
1756
2019
  rows: readonly TooltipRow[],
1757
2020
  valueFormatter: (value: number) => string,
1758
2021
  notes: readonly string[] = [],
2022
+ locale?: string,
1759
2023
  ): string {
2024
+ const t = chartText(locale)
1760
2025
  const lines = rows.map(
1761
2026
  (row) =>
1762
2027
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1765,7 +2030,7 @@ function tooltipHtml(
1765
2030
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1766
2031
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1767
2032
  }${
1768
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2033
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1769
2034
  }</span></div>` +
1770
2035
  (row.note === undefined
1771
2036
  ? ""
@@ -1779,8 +2044,10 @@ function tooltipHtml(
1779
2044
 
1780
2045
  /* ------------------------------------------------- per-bar targets / notes */
1781
2046
 
1782
- /** Default name of the per-bar target marker (legend, tooltip). */
1783
- const BAR_TARGET_LABEL = "目標"
2047
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2048
+ function barTargetLabel(locale?: string): string {
2049
+ return chartText(locale).target
2050
+ }
1784
2051
 
1785
2052
  /** How far (px) the target marker reaches past each side of its bar. */
1786
2053
  const TARGET_MARKER_OVERHANG = 4
@@ -1831,16 +2098,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1831
2098
  return (value / target) * 100
1832
2099
  }
1833
2100
 
1834
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2101
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1835
2102
  function barTargetNote(
1836
2103
  label: string,
1837
2104
  value: number | null,
1838
2105
  target: number | null,
1839
2106
  valueFormatter: (value: number) => string,
2107
+ locale?: string,
1840
2108
  ): string | null {
1841
2109
  if (target === null) return null
1842
2110
  const rate = achievementRate(value, target)
1843
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2111
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1844
2112
  }
1845
2113
 
1846
2114
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1932,11 +2200,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1932
2200
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1933
2201
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1934
2202
 
1935
- const DEFAULT_FORECAST_LABEL = "予測"
1936
-
1937
- /** The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side. */
1938
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1939
- return `${label} →`
2203
+ /**
2204
+ * The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side.
2205
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2206
+ */
2207
+ function forecastBoundaryText(label?: string, locale?: string): string {
2208
+ return `${label ?? chartText(locale).forecast} →`
1940
2209
  }
1941
2210
 
1942
2211
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1976,8 +2245,10 @@ interface AxisExtent {
1976
2245
  max: number
1977
2246
  }
1978
2247
 
1979
- /** The label of a stack's total (highlight and tooltip). */
1980
- const STACK_TOTAL_LABEL = "合計"
2248
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2249
+ function stackTotalLabel(locale?: string): string {
2250
+ return chartText(locale).total
2251
+ }
1981
2252
 
1982
2253
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
1983
2254
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2099,8 +2370,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2099
2370
 
2100
2371
  /**
2101
2372
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2102
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2103
- * either value is missing or the baseline is 0.
2373
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2374
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2104
2375
  */
2105
2376
  function comparisonNote(
2106
2377
  current: number | null,
@@ -2108,16 +2379,18 @@ function comparisonNote(
2108
2379
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2109
2380
  grain?: PeriodGrain,
2110
2381
  format?: ValueFormat,
2111
- locale: string = DEFAULT_LOCALE,
2382
+ localeArg?: string,
2112
2383
  ): string {
2384
+ const locale = resolveLocale(localeArg)
2113
2385
  const result = computeComparison(
2114
2386
  current,
2115
2387
  comparison.vs === "custom"
2116
2388
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2117
2389
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2390
+ { locale },
2118
2391
  )
2119
2392
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2120
- if (text === null) return `比較なし ${result.label}`
2393
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2121
2394
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2122
2395
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2123
2396
  }
@@ -2160,7 +2433,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2160
2433
  toneColors,
2161
2434
  valueFormatter,
2162
2435
  axisFormatter = valueFormatter,
2163
- locale = DEFAULT_LOCALE,
2164
2436
  type = "default",
2165
2437
  layout = "horizontal",
2166
2438
  curveType = "linear",
@@ -2197,7 +2469,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2197
2469
  chartOptions,
2198
2470
  reserveActions = false,
2199
2471
  } = cfg
2200
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2472
+ const locale = resolveLocale(cfg.locale)
2473
+ const text = chartText(locale)
2474
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2201
2475
 
2202
2476
  const stacked = kind !== "line" && type !== "default"
2203
2477
  const percent = kind !== "line" && type === "percent"
@@ -2346,9 +2620,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2346
2620
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2347
2621
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2348
2622
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2349
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2623
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2350
2624
  const highlightText = (value: number) =>
2351
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2625
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2352
2626
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2353
2627
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2354
2628
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2374,7 +2648,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2374
2648
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2375
2649
  const barTargets: (number | null)[] | null =
2376
2650
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2377
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2651
+ const barTargetName = targetLabel ?? text.target
2378
2652
  const barTargetColor = labelColor ?? guideColor
2379
2653
 
2380
2654
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2588,7 +2862,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2588
2862
  }
2589
2863
 
2590
2864
  /* -- forecast: dashed continuation (+ optional band) of one series */
2591
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2865
+ const forecastLabel = forecast?.label ?? text.forecast
2592
2866
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2593
2867
  const forecastValues: (number | null)[] = []
2594
2868
  if (forecast) {
@@ -3064,7 +3338,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3064
3338
  })
3065
3339
  }
3066
3340
  if (stackTotals && i <= lastActual && rows.length > 1) {
3067
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3341
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3068
3342
  }
3069
3343
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3070
3344
  if (provisional) {
@@ -3080,19 +3354,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3080
3354
  ...noteFor(at),
3081
3355
  })
3082
3356
  })
3083
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3357
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3084
3358
  }
3085
3359
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3086
3360
  const lines: string[] = []
3087
3361
  if (barTargets && i <= lastActual) {
3088
3362
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3089
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3363
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3090
3364
  if (note) lines.push(note)
3091
3365
  }
3092
3366
  for (const note of notes) {
3093
3367
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3094
3368
  }
3095
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3369
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3096
3370
  },
3097
3371
  },
3098
3372
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3184,8 +3458,6 @@ interface DonutOptionSlice {
3184
3458
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3185
3459
  const DONUT_OUTSIDE_LABELS = 2
3186
3460
 
3187
- const DEFAULT_TOTAL_LABEL = "合計"
3188
-
3189
3461
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3190
3462
 
3191
3463
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3218,15 +3490,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3218
3490
  }
3219
3491
 
3220
3492
  /**
3221
- * The default center label: what is totalled (「合計」), the formatted total
3493
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3222
3494
  * (`amount`, the big line), the unit (a small line under it) and both as one
3223
3495
  * string (`value`, 「9.2万 セッション」).
3224
3496
  */
3225
3497
  function donutCenterText(
3226
3498
  total: number,
3227
3499
  valueFormatter: (value: number) => string,
3228
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3500
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3229
3501
  ): { label: string; value: string; amount: string; unit?: string } {
3502
+ totalLabel ??= chartText(locale).total
3230
3503
  const amount = valueFormatter(total)
3231
3504
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3232
3505
  }
@@ -3454,8 +3727,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3454
3727
 
3455
3728
  /* ------------------------------------------------------------ provisional */
3456
3729
 
3457
- /** The default label for points that are not final yet. */
3458
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3730
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3731
+ function defaultProvisionalLabel(locale?: string): string {
3732
+ return chartText(locale).provisional
3733
+ }
3459
3734
 
3460
3735
  /**
3461
3736
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3522,6 +3797,11 @@ export interface DonutChartProps<T extends Record<string, unknown> = Record<stri
3522
3797
  loading?: boolean
3523
3798
  }
3524
3799
 
3800
+ const TEXT = {
3801
+ ja: { item: "項目", value: "値", chart: (shares: readonly string[]) => `構成比のドーナツグラフ(${shares.join("、")})` },
3802
+ en: { item: "Item", value: "Value", chart: (shares: readonly string[]) => `Donut chart of shares (${shares.join(", ")})` },
3803
+ }
3804
+
3525
3805
  /** The element's width, kept current (`null` until measured). */
3526
3806
  function useElementWidth(): [(el: HTMLDivElement | null) => void, number | null] {
3527
3807
  const [node, setNode] = useState<HTMLDivElement | null>(null)
@@ -3577,12 +3857,15 @@ function DonutChartImpl<T extends Record<string, unknown>>(
3577
3857
  const center = resolveDonutCenter(donutOptions?.centerLabel, { showLabel: donutOptions?.showLabel ?? showLabel, label })
3578
3858
  const maxSlices = donutOptions?.maxSlices
3579
3859
  const showLegend = donutOptions?.showLegend ?? true
3580
- const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format), [valueFormatterProp, format])
3860
+ const locale = useLocale()
3861
+ const t = TEXT[uiLanguage(locale)]
3862
+ const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
3581
3863
 
3582
- const tokens = useChartTokens()
3864
+ const { ref: frameRef, tokens } = useChartSurface(ref)
3583
3865
  const slices = useMemo(
3584
- () => donutSlices(data, { index: labelKey, value: valueKey, colors, maxSlices, otherColor: CHART_OTHER_COLOR }),
3585
- [data, labelKey, valueKey, colors, maxSlices],
3866
+ () =>
3867
+ donutSlices(data, { index: labelKey, value: valueKey, colors, maxSlices, otherColor: CHART_OTHER_COLOR, otherLabel: otherSeriesLabel(locale) }),
3868
+ [data, labelKey, valueKey, colors, maxSlices, locale],
3586
3869
  )
3587
3870
  const palette = useMemo(() => resolveChartColors(slices.map((s) => chartFillName(s.color)), tokens), [slices, tokens])
3588
3871
  const rows = useMemo(() => slices.map((s) => s.row), [slices])
@@ -3632,15 +3915,14 @@ function DonutChartImpl<T extends Record<string, unknown>>(
3632
3915
  let centerLabel: ReactNode = null
3633
3916
  if (center.kind === "custom") centerLabel = center.node
3634
3917
  else if (center.kind === "total") {
3635
- const text = donutCenterText(total, valueFormatter, { totalLabel: donutOptions?.totalLabel, unit: donutOptions?.unit })
3918
+ const text = donutCenterText(total, valueFormatter, { totalLabel: donutOptions?.totalLabel, unit: donutOptions?.unit, locale })
3636
3919
  centerLabel = caption(text.label, text.amount, text.unit)
3637
3920
  } else if (center.kind === "top") {
3638
3921
  const top = donutTopText(slices, labels)
3639
3922
  centerLabel = top && caption(top.label, top.amount)
3640
3923
  }
3641
3924
  // Built from the slices (names and shares), not the column keys.
3642
- const a11yLabel =
3643
- ariaLabel ?? `構成比のドーナツグラフ(${legend.map((item) => `${labelOf(item.name, labels)} ${item.share}`).join("、")})`
3925
+ const a11yLabel = ariaLabel ?? t.chart(legend.map((item) => `${labelOf(item.name, labels)} ${item.share}`))
3644
3926
  const dataTable = useMemo(
3645
3927
  () =>
3646
3928
  chartDataTable(
@@ -3649,19 +3931,19 @@ function DonutChartImpl<T extends Record<string, unknown>>(
3649
3931
  "name",
3650
3932
  ["value"],
3651
3933
  valueFormatter,
3652
- { name: labels?.[labelKey] ?? "項目", value: labels?.[valueKey] ?? "値" },
3934
+ { name: labels?.[labelKey] ?? t.item, value: labels?.[valueKey] ?? t.value },
3653
3935
  ),
3654
- [rows, labelKey, valueKey, valueFormatter, labels],
3936
+ [rows, labelKey, valueKey, valueFormatter, labels, t],
3655
3937
  )
3656
3938
 
3657
3939
  const resolvedSummary = useMemo(
3658
- () => (summary === "auto" ? buildShareSummary(slices, title) : summary),
3659
- [summary, slices, title],
3940
+ () => (summary === "auto" ? buildShareSummary(slices, title, locale) : summary),
3941
+ [summary, slices, title, locale],
3660
3942
  )
3661
3943
  return (
3662
3944
  <ChartFrame
3663
3945
  {...chartRootProps(rest)}
3664
- ref={ref}
3946
+ ref={frameRef}
3665
3947
  title={title}
3666
3948
  summary={resolvedSummary}
3667
3949
  isLoading={isLoading ?? loading ?? false}