@squadbase/vantage 0.6.2-dev1.9365dcd → 0.6.2-dev1.9d7130b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) 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-add-feature/SKILL.md +5 -0
  208. package/skills/vantage-design/SKILL.md +54 -54
  209. package/skills/vantage-design/references/extract.md +26 -27
  210. package/skills/vantage-design/references/format.md +12 -172
  211. package/skills/vantage-design/references/presets.md +50 -110
  212. package/skills/vantage-pitfalls/SKILL.md +5 -0
  213. package/skills/vantage-slide/SKILL.md +54 -36
  214. package/templates/AGENTS.md +35 -18
  215. package/templates/agents/api.md +7 -0
  216. package/templates/agents/doc.md +48 -0
  217. package/templates/agents/page.md +59 -2
  218. package/templates/agents/slide.md +77 -3
  219. package/theme.css +23 -19
  220. package/dist/chunk-2ICNHUG6.js.map +0 -1
  221. package/dist/chunk-2N72FKLB.js.map +0 -1
  222. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  223. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  224. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  225. package/dist/chunk-JKLLFVEU.js.map +0 -1
  226. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  227. package/dist/chunk-V3LF7BE7.js.map +0 -1
  228. package/dist/chunk-WAMZWZJB.js.map +0 -1
  229. package/dist/chunk-XPQSTZUU.js.map +0 -1
  230. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -18,15 +18,148 @@ import { forwardRef, useMemo, type ForwardedRef, type ReactElement, type Ref } f
18
18
  import { ChartFrame } from "@squadbase/vantage/components"
19
19
  import { EChart } from "@squadbase/vantage/components"
20
20
  import type { ChartThreshold } from "@squadbase/vantage/components"
21
- import { createContext, useCallback, useEffect, useRef, useState } from "react"
21
+ import { useSyncExternalStore } from "react"
22
+ import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
22
23
  import type { ChartTone } from "@squadbase/vantage/components"
23
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
24
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
24
25
  import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
25
26
  import { type ReactNode } from "react"
26
27
  import type { EChartsOption } from "@squadbase/vantage/components"
27
28
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
29
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
28
30
  import type { ChartAnnotation, ChartCommonProps, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
29
31
 
32
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
33
+
34
+ /**
35
+ * The display language of the parts and the defaults that follow it.
36
+ *
37
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
38
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
39
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
40
+ *
41
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
42
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
43
+ */
44
+
45
+ /** The language of the built-in wording: Japanese or English. */
46
+ type UiLanguage = "ja" | "en"
47
+
48
+ /** The locale used when `<html lang>` gives none (and on the server). */
49
+ const FALLBACK_LOCALE = "en-US"
50
+
51
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
52
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
53
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
54
+ }
55
+
56
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
57
+ function normalizeLocale(tag: string | null | undefined): string {
58
+ const value = tag?.trim()
59
+ if (!value) return FALLBACK_LOCALE
60
+ try {
61
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
62
+ } catch {
63
+ return FALLBACK_LOCALE
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
69
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
70
+ */
71
+ function documentLocale(): string {
72
+ if (typeof document === "undefined") return FALLBACK_LOCALE
73
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
74
+ }
75
+
76
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
77
+ function resolveLocale(locale?: string | null): string {
78
+ return locale || documentLocale()
79
+ }
80
+
81
+ /** Picks the wording table of the locale's language. */
82
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
83
+ return table[uiLanguage(resolveLocale(locale))]
84
+ }
85
+
86
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
87
+ function defaultCurrency(locale?: string | null): string {
88
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
89
+ }
90
+
91
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
92
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
93
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
94
+ }
95
+
96
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
97
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
98
+
99
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
100
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
101
+
102
+ /**
103
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
104
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
105
+ * Monday for the rest.
106
+ */
107
+ function weekStartsOn(locale?: string | null): WeekDay {
108
+ const tag = normalizeLocale(resolveLocale(locale))
109
+ if (uiLanguage(tag) === "ja") return 1
110
+ try {
111
+ const info = new Intl.Locale(tag) as Intl.Locale & {
112
+ getWeekInfo?: () => { firstDay: number }
113
+ weekInfo?: { firstDay: number }
114
+ }
115
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
116
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
117
+ } catch {
118
+ // Fall through to the table below.
119
+ }
120
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
121
+ }
122
+
123
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
124
+
125
+ const listeners = new Set<() => void>()
126
+ let observer: MutationObserver | null = null
127
+
128
+ /** One observer on `<html lang>` for every subscribed part. */
129
+ function subscribe(onChange: () => void): () => void {
130
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
131
+ listeners.add(onChange)
132
+ if (!observer) {
133
+ observer = new MutationObserver(() => {
134
+ for (const listener of [...listeners]) listener()
135
+ })
136
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
137
+ }
138
+ return () => {
139
+ listeners.delete(onChange)
140
+ if (listeners.size === 0 && observer) {
141
+ observer.disconnect()
142
+ observer = null
143
+ }
144
+ }
145
+ }
146
+
147
+ const serverLocale = () => FALLBACK_LOCALE
148
+
149
+ /**
150
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
151
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
152
+ */
153
+ function useDocumentLocale(): string {
154
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
155
+ }
156
+
157
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
158
+ function useLocale(locale?: string | null): string {
159
+ const page = useDocumentLocale()
160
+ return locale || page
161
+ }
162
+
30
163
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
31
164
 
32
165
  /**
@@ -61,10 +194,23 @@ interface ChartTokens {
61
194
  border: string | null
62
195
  /** Tooltip surface. */
63
196
  popover: string | null
197
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
198
+ colors?: Record<string, string>
199
+ /** Actual inherited painting surface (may change without a token changing). */
200
+ surface?: string | null
64
201
  /** Tooltip text. */
65
202
  popoverForeground: string | null
66
203
  }
67
204
 
205
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
206
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
207
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
208
+ const OPTION_COLOR_TOKENS = [
209
+ "--background", "--card", "--muted", "--primary", "--destructive",
210
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
211
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
212
+ ]
213
+
68
214
  let normalizeContext: CanvasRenderingContext2D | null | undefined
69
215
 
70
216
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -120,8 +266,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
120
266
  const color = read(token)
121
267
  if (color) series.push(color)
122
268
  }
123
- return {
269
+ const tokens: ChartTokens = {
124
270
  series,
271
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
272
+ surface: readChartSurface(target),
125
273
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
126
274
  foreground: read("--foreground"),
127
275
  mutedForeground: read("--muted-foreground"),
@@ -129,6 +277,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
129
277
  popover: read("--popover"),
130
278
  popoverForeground: read("--popover-foreground"),
131
279
  }
280
+ tokenScopes.set(tokens, target)
281
+ return tokens
132
282
  }
133
283
 
134
284
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -136,6 +286,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
136
286
  if (a === b) return true
137
287
  if (!a || !b) return false
138
288
  return (
289
+ tokenScopes.get(a) === tokenScopes.get(b) &&
290
+ a.surface === b.surface &&
291
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
292
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
139
293
  a.series.length === b.series.length &&
140
294
  a.series.every((color, i) => color === b.series[i]) &&
141
295
  a.fontFamily === b.fontFamily &&
@@ -161,7 +315,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
161
315
  * attributes alone leaves the chart painted in the previous palette until a
162
316
  * reload — the tokens in the DOM are new, the canvas is not.
163
317
  */
164
- function watchChartTokens(onChange: () => void): () => void {
318
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
165
319
  if (typeof document === "undefined") return () => {}
166
320
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
167
321
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -176,8 +330,11 @@ function watchChartTokens(onChange: () => void): () => void {
176
330
 
177
331
  const themeObserver = new MutationObserver(schedule)
178
332
  const attributeFilter = ["class", "style", "data-theme"]
179
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
180
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
333
+ // Only the chart and its ancestors can change its inherited theme. Never observe
334
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
335
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
336
+ themeObserver.observe(node, { attributes: true, attributeFilter })
337
+ }
181
338
 
182
339
  const styleObserver = new MutationObserver(schedule)
183
340
  if (document.head) {
@@ -265,22 +422,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
265
422
  * components that build an ECharts option from token colours themselves (the
266
423
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
267
424
  */
268
- function useChartTokens(): ChartTokens | null {
425
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
269
426
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
270
427
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
271
428
  // replayed by React on every rebase and returns a fresh object each time, so the state
272
429
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
273
430
  const tokensRef = useRef(tokens)
274
431
  useEffect(() => {
432
+ const target = scope && "current" in scope ? scope.current : scope
275
433
  const sync = () => {
276
- const next = readChartTokens()
434
+ const next = readChartTokens(target)
277
435
  if (sameChartTokens(tokensRef.current, next)) return
278
436
  tokensRef.current = next
279
437
  setTokens(next)
280
438
  }
281
439
  sync()
282
- return watchChartTokens(sync)
283
- }, [])
440
+ return watchChartTokens(sync, target)
441
+ }, [scope])
284
442
  return tokens
285
443
  }
286
444
 
@@ -292,7 +450,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
292
450
  /**
293
451
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
294
452
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
295
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
453
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
296
454
  * other CSS colour is normalized like the tokens are. A colour that can't be
297
455
  * resolved comes back as `""`, which ECharts treats as "use the palette".
298
456
  */
@@ -303,12 +461,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
303
461
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
304
462
  if (TONE_NAME_RE.test(color.trim())) {
305
463
  if (typeof document === "undefined") return ""
306
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
464
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
465
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
307
466
  }
308
467
  if (typeof document === "undefined") return color
309
468
  let value = color.trim()
310
469
  const cssVar = CSS_VAR_RE.exec(value)
311
- const styles = getComputedStyle(document.documentElement)
470
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
312
471
  if (cssVar) {
313
472
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
314
473
  } else if (value.includes("var(")) {
@@ -354,7 +513,7 @@ function readChartSurface(el: Element | null): string | null {
354
513
  if (color) return color
355
514
  }
356
515
  }
357
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
516
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
358
517
  }
359
518
 
360
519
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -368,9 +527,10 @@ const WIDTH_BUCKET = 40
368
527
  */
369
528
  function useChartSurface<E extends Element>(
370
529
  forwarded: ForwardedRef<E>,
371
- tokens: ChartTokens | null,
372
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
530
+ tokens?: ChartTokens | null,
531
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
373
532
  const [node, setNode] = useState<E | null>(null)
533
+ const scopedTokens = useChartTokens(node)
374
534
  const [surface, setSurface] = useState<string | undefined>(undefined)
375
535
  const [width, setWidth] = useState<number | undefined>(undefined)
376
536
  const ref = useCallback(
@@ -384,7 +544,7 @@ function useChartSurface<E extends Element>(
384
544
  useEffect(() => {
385
545
  if (!node) return
386
546
  setSurface(readChartSurface(node) ?? undefined)
387
- }, [node, tokens])
547
+ }, [node, tokens, scopedTokens])
388
548
  useEffect(() => {
389
549
  if (!node || typeof ResizeObserver === "undefined") return
390
550
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -393,7 +553,7 @@ function useChartSurface<E extends Element>(
393
553
  observer.observe(node)
394
554
  return () => observer.disconnect()
395
555
  }, [node])
396
- return { ref, surface, width }
556
+ return { ref, surface, width, tokens: scopedTokens }
397
557
  }
398
558
 
399
559
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -503,9 +663,10 @@ function resolveValueFormatter(
503
663
  function resolveAxisFormatter(
504
664
  valueFormatter: ValueFormatter | undefined,
505
665
  format: ValueFormat | undefined,
506
- locale: string = DEFAULT_LOCALE,
666
+ localeArg?: string,
507
667
  ): ValueFormatter {
508
668
  if (valueFormatter) return valueFormatter
669
+ const locale = resolveLocale(localeArg)
509
670
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
510
671
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
511
672
  const currency = "currency" in spec ? spec.currency : undefined
@@ -514,40 +675,75 @@ function resolveAxisFormatter(
514
675
  return (value) => formatValue(value, short, locale)
515
676
  }
516
677
 
517
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
518
- const JA_UNITS = [
519
- { char: "兆", size: 1e12 },
520
- { char: "億", size: 1e8 },
521
- { char: "万", size: 1e4 },
522
- ] as const
678
+ interface CompactUnit {
679
+ token: string
680
+ size: number
681
+ }
682
+
683
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
684
+
685
+ /**
686
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
687
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
688
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
689
+ */
690
+ function compactUnits(locale: string): CompactUnit[] | null {
691
+ const cached = compactUnitCache.get(locale)
692
+ if (cached !== undefined) return cached
693
+ let units: CompactUnit[] | null = []
694
+ try {
695
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
696
+ const seen = new Set<string>()
697
+ for (let exp = 3; exp <= 15; exp++) {
698
+ const parts = compact.formatToParts(10 ** exp)
699
+ const at = parts.findIndex((p) => p.type === "compact")
700
+ if (at === -1) continue
701
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
702
+ units = null
703
+ break
704
+ }
705
+ const token = parts[at]!.value
706
+ if (seen.has(token)) continue
707
+ seen.add(token)
708
+ units.push({ token, size: 10 ** exp })
709
+ }
710
+ } catch {
711
+ units = null
712
+ }
713
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
714
+ compactUnitCache.set(locale, sorted)
715
+ return sorted
716
+ }
523
717
 
524
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
718
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
525
719
 
526
720
  /**
527
721
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
528
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
529
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
530
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
722
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
723
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
724
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
725
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
531
726
  */
532
727
  function resolveAxisTickFormatter(
533
728
  valueFormatter: ValueFormatter | undefined,
534
729
  format: ValueFormat | undefined,
535
- locale: string = DEFAULT_LOCALE,
730
+ localeArg?: string,
536
731
  ): (top: number) => ValueFormatter {
732
+ const locale = resolveLocale(localeArg)
537
733
  const base = resolveAxisFormatter(valueFormatter, format, locale)
538
734
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
539
- const aligns =
540
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
541
- if (!aligns) return () => base
735
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
736
+ if (!units) return () => base
542
737
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
738
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
543
739
  return (top) => {
544
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
740
+ const unit = units.find((u) => Math.abs(top) >= u.size)
545
741
  if (!unit) return base
546
742
  return (value) => {
547
743
  const text = base(value)
548
744
  if (value === 0 || !Number.isFinite(value)) return text
549
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
550
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
745
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
746
+ return text.replace(numberToken, () => scaled)
551
747
  }
552
748
  }
553
749
  }
@@ -576,8 +772,20 @@ function clamp(value: number, min: number, max: number): number {
576
772
  /** How many categories a chart colours individually; the rest fold into「その他」. */
577
773
  const MAX_CHART_SERIES = 5
578
774
 
579
- /** The series name the folded categories get. */
580
- const OTHER_SERIES_LABEL = "その他"
775
+ /** The series name the folded categories get, by language. */
776
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
777
+
778
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
779
+ function otherSeriesLabel(locale?: string): string {
780
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
781
+ }
782
+
783
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
784
+
785
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
786
+ function isOtherName(name: string | null | undefined): boolean {
787
+ return typeof name === "string" && OTHER_NAMES.has(name)
788
+ }
581
789
 
582
790
  interface AggregatedItems<Item extends { name: string; value: number }> {
583
791
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -597,7 +805,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
597
805
  function aggregateTopN<Item extends { name: string; value: number }>(
598
806
  items: readonly Item[],
599
807
  n: number = MAX_CHART_SERIES,
600
- otherLabel: string = OTHER_SERIES_LABEL,
808
+ otherLabel: string = otherSeriesLabel(),
601
809
  ): AggregatedItems<Item> {
602
810
  if (items.length <= n) return { top: [...items], rest: [], other: null }
603
811
  const ranked = items
@@ -626,7 +834,7 @@ function aggregateSeries(
626
834
  data: readonly Record<string, unknown>[],
627
835
  categories: readonly string[],
628
836
  n: number = MAX_CHART_SERIES,
629
- otherLabel: string = OTHER_SERIES_LABEL,
837
+ otherLabel: string = otherSeriesLabel(),
630
838
  ): AggregatedSeries {
631
839
  const totals = categories.map((name) => ({
632
840
  name,
@@ -658,12 +866,14 @@ function aggregateSeriesKeeping(
658
866
  categories: readonly string[],
659
867
  keep: string | undefined,
660
868
  n: number = MAX_CHART_SERIES,
869
+ otherLabel: string = otherSeriesLabel(),
661
870
  ): AggregatedSeries {
662
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
871
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
663
872
  const aggregated = aggregateSeries(
664
873
  data,
665
874
  categories.filter((c) => c !== keep),
666
875
  n,
876
+ otherLabel,
667
877
  )
668
878
  return { ...aggregated, categories: [...aggregated.categories, keep] }
669
879
  }
@@ -699,7 +909,7 @@ function seriesColorsWithComparison(
699
909
  * Colour per drawn series: the record's token for that category, else the
700
910
  * array's token at the category's position in `allCategories` (cycled), else
701
911
  * the token palette by drawn position. `otherCategory` — and a category the
702
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
912
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
703
913
  */
704
914
  function seriesColorsFor(
705
915
  categories: readonly string[],
@@ -711,7 +921,7 @@ function seriesColorsFor(
711
921
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
712
922
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
713
923
  return categories.map((category, i) => {
714
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
924
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
715
925
  const fromRecord = record?.[category]
716
926
  if (fromRecord) return fromRecord
717
927
  if (list && list.length > 0) {
@@ -758,7 +968,7 @@ interface DonutSlicesConfig {
758
968
  */
759
969
  function donutSlices(
760
970
  data: readonly Record<string, unknown>[],
761
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
971
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
762
972
  ): DonutSlice[] {
763
973
  const items = data
764
974
  .map((row, i) => {
@@ -771,7 +981,8 @@ function donutSlices(
771
981
  const slices: DonutSlice[] = top.map((item, rank) => ({
772
982
  name: item.name,
773
983
  value: item.value,
774
- color: palette[rank % palette.length]!,
984
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
985
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
775
986
  isOther: false,
776
987
  row: item.row,
777
988
  }))
@@ -844,7 +1055,7 @@ interface BarListRowsConfig extends BarListOptions {
844
1055
  */
845
1056
  function barListRows<Row extends object>(
846
1057
  data: readonly Row[],
847
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1058
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
848
1059
  ): BarListRow<Row>[] {
849
1060
  const items = data.map((row): BarListRow<Row> => {
850
1061
  const cells = row as Record<string, unknown>
@@ -890,19 +1101,21 @@ const BAR_LIST_OTHER_TINT = 30
890
1101
 
891
1102
  /**
892
1103
  * The fill of each BarList row: all real rows in the same tint of `color`
893
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1104
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
894
1105
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
895
1106
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
896
1107
  * for that row. A tint, not the solid token, so the label printed over the bar
897
1108
  * keeps its contrast.
898
1109
  */
899
1110
  function barListFills(
900
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1111
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
901
1112
  color: ChartColor,
902
1113
  otherColor: string,
903
1114
  ): string[] {
904
1115
  return rows.map((row) =>
905
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1116
+ row.isOther || isOtherName(row.name)
1117
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1118
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
906
1119
  )
907
1120
  }
908
1121
 
@@ -1118,11 +1331,30 @@ interface ChartSummaryInput {
1118
1331
  format?: ValueFormat
1119
1332
  /** Wins over `format`. */
1120
1333
  valueFormatter?: ValueFormatter
1334
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1335
+ locale?: string
1336
+ }
1337
+
1338
+ const SUMMARY_TEXT = {
1339
+ ja: {
1340
+ up: "、前の点より増加",
1341
+ down: "、前の点より減少",
1342
+ flat: "、前の点と横ばい",
1343
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1344
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1345
+ },
1346
+ en: {
1347
+ up: ", up from the previous point",
1348
+ down: ", down from the previous point",
1349
+ flat: ", flat from the previous point",
1350
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1351
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1352
+ },
1121
1353
  }
1122
1354
 
1123
1355
  /**
1124
1356
  * One-sentence summary built from the rows, for `summary="auto"`:
1125
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1357
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1126
1358
  * computed from the last two non-empty points of the first series — never
1127
1359
  * written by hand, so it cannot contradict the data. `undefined` when there is
1128
1360
  * no usable point.
@@ -1138,30 +1370,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1138
1370
  }
1139
1371
  const latest = points[points.length - 1]
1140
1372
  if (!latest) return undefined
1141
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1373
+ const locale = resolveLocale(input.locale)
1374
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1375
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1142
1376
  const prev = points[points.length - 2]
1143
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1377
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1144
1378
  const head = input.title ? `${input.title}: ` : ""
1145
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1379
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1146
1380
  }
1147
1381
 
1148
1382
  /**
1149
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1150
- * (largest first). `undefined` when there is nothing to describe.
1383
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1384
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1151
1385
  */
1152
1386
  function buildShareSummary(
1153
1387
  slices: readonly { name: string; value: number }[],
1154
1388
  title?: string,
1389
+ locale?: string,
1155
1390
  ): string | undefined {
1156
1391
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1157
1392
  const top = slices[0]
1158
1393
  if (!top || total <= 0) return undefined
1159
1394
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1160
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1395
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1161
1396
  }
1162
1397
 
1163
1398
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1164
1399
 
1400
+ /** The built-in words of the option builders, by language. */
1401
+ const CHART_TEXT = {
1402
+ ja: {
1403
+ target: "目標",
1404
+ forecast: "予測",
1405
+ total: "合計",
1406
+ provisional: "速報",
1407
+ noComparison: "比較なし",
1408
+ share: (share: string) => `(${share})`,
1409
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1410
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1411
+ },
1412
+ en: {
1413
+ target: "Target",
1414
+ forecast: "Forecast",
1415
+ total: "Total",
1416
+ provisional: "Preliminary",
1417
+ noComparison: "No comparison",
1418
+ share: (share: string) => ` (${share})`,
1419
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1420
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1421
+ },
1422
+ } satisfies Record<UiLanguage, unknown>
1423
+
1424
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1425
+
1165
1426
  type CartesianChartKind = "area" | "bar" | "line"
1166
1427
  type CartesianStackType = "default" | "stacked" | "percent"
1167
1428
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1346,7 +1607,7 @@ interface CartesianChartConfig {
1346
1607
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1347
1608
  */
1348
1609
  axisTicks?: (top: number) => (value: number) => string
1349
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1610
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1350
1611
  locale?: string
1351
1612
  type?: CartesianStackType
1352
1613
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1584,11 +1845,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1584
1845
  */
1585
1846
  function timeAxisLabels(
1586
1847
  values: readonly unknown[],
1587
- locale: string = DEFAULT_LOCALE,
1848
+ localeArg?: string,
1588
1849
  shown: (i: number) => boolean = () => true,
1589
1850
  ): string[] | null {
1590
1851
  const parsed = parseTimeIndex(values)
1591
1852
  if (!parsed) return null
1853
+ const locale = resolveLocale(localeArg)
1592
1854
  const { granularity, times } = parsed
1593
1855
  let prevYear: number | null = null
1594
1856
  return times.map((t, i) => {
@@ -1611,9 +1873,10 @@ function timeAxisLabels(
1611
1873
  }
1612
1874
 
1613
1875
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1614
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1876
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1615
1877
  const parsed = parseTimeIndex(values)
1616
1878
  if (!parsed) return null
1879
+ const locale = resolveLocale(localeArg)
1617
1880
  const options: Intl.DateTimeFormatOptions =
1618
1881
  parsed.granularity === "year"
1619
1882
  ? { year: "numeric" }
@@ -1753,7 +2016,9 @@ function tooltipHtml(
1753
2016
  rows: readonly TooltipRow[],
1754
2017
  valueFormatter: (value: number) => string,
1755
2018
  notes: readonly string[] = [],
2019
+ locale?: string,
1756
2020
  ): string {
2021
+ const t = chartText(locale)
1757
2022
  const lines = rows.map(
1758
2023
  (row) =>
1759
2024
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1762,7 +2027,7 @@ function tooltipHtml(
1762
2027
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1763
2028
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1764
2029
  }${
1765
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2030
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1766
2031
  }</span></div>` +
1767
2032
  (row.note === undefined
1768
2033
  ? ""
@@ -1776,8 +2041,10 @@ function tooltipHtml(
1776
2041
 
1777
2042
  /* ------------------------------------------------- per-bar targets / notes */
1778
2043
 
1779
- /** Default name of the per-bar target marker (legend, tooltip). */
1780
- const BAR_TARGET_LABEL = "目標"
2044
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2045
+ function barTargetLabel(locale?: string): string {
2046
+ return chartText(locale).target
2047
+ }
1781
2048
 
1782
2049
  /** How far (px) the target marker reaches past each side of its bar. */
1783
2050
  const TARGET_MARKER_OVERHANG = 4
@@ -1828,16 +2095,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1828
2095
  return (value / target) * 100
1829
2096
  }
1830
2097
 
1831
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2098
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1832
2099
  function barTargetNote(
1833
2100
  label: string,
1834
2101
  value: number | null,
1835
2102
  target: number | null,
1836
2103
  valueFormatter: (value: number) => string,
2104
+ locale?: string,
1837
2105
  ): string | null {
1838
2106
  if (target === null) return null
1839
2107
  const rate = achievementRate(value, target)
1840
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2108
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1841
2109
  }
1842
2110
 
1843
2111
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1929,11 +2197,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1929
2197
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1930
2198
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1931
2199
 
1932
- const DEFAULT_FORECAST_LABEL = "予測"
1933
-
1934
- /** 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. */
1935
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1936
- return `${label} →`
2200
+ /**
2201
+ * 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.
2202
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2203
+ */
2204
+ function forecastBoundaryText(label?: string, locale?: string): string {
2205
+ return `${label ?? chartText(locale).forecast} →`
1937
2206
  }
1938
2207
 
1939
2208
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1973,8 +2242,10 @@ interface AxisExtent {
1973
2242
  max: number
1974
2243
  }
1975
2244
 
1976
- /** The label of a stack's total (highlight and tooltip). */
1977
- const STACK_TOTAL_LABEL = "合計"
2245
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2246
+ function stackTotalLabel(locale?: string): string {
2247
+ return chartText(locale).total
2248
+ }
1978
2249
 
1979
2250
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
1980
2251
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2096,8 +2367,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2096
2367
 
2097
2368
  /**
2098
2369
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2099
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2100
- * either value is missing or the baseline is 0.
2370
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2371
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2101
2372
  */
2102
2373
  function comparisonNote(
2103
2374
  current: number | null,
@@ -2105,16 +2376,18 @@ function comparisonNote(
2105
2376
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2106
2377
  grain?: PeriodGrain,
2107
2378
  format?: ValueFormat,
2108
- locale: string = DEFAULT_LOCALE,
2379
+ localeArg?: string,
2109
2380
  ): string {
2381
+ const locale = resolveLocale(localeArg)
2110
2382
  const result = computeComparison(
2111
2383
  current,
2112
2384
  comparison.vs === "custom"
2113
2385
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2114
2386
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2387
+ { locale },
2115
2388
  )
2116
2389
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2117
- if (text === null) return `比較なし ${result.label}`
2390
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2118
2391
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2119
2392
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2120
2393
  }
@@ -2157,7 +2430,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2157
2430
  toneColors,
2158
2431
  valueFormatter,
2159
2432
  axisFormatter = valueFormatter,
2160
- locale = DEFAULT_LOCALE,
2161
2433
  type = "default",
2162
2434
  layout = "horizontal",
2163
2435
  curveType = "linear",
@@ -2194,7 +2466,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2194
2466
  chartOptions,
2195
2467
  reserveActions = false,
2196
2468
  } = cfg
2197
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2469
+ const locale = resolveLocale(cfg.locale)
2470
+ const text = chartText(locale)
2471
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2198
2472
 
2199
2473
  const stacked = kind !== "line" && type !== "default"
2200
2474
  const percent = kind !== "line" && type === "percent"
@@ -2343,9 +2617,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2343
2617
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2344
2618
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2345
2619
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2346
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2620
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2347
2621
  const highlightText = (value: number) =>
2348
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2622
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2349
2623
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2350
2624
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2351
2625
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2371,7 +2645,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2371
2645
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2372
2646
  const barTargets: (number | null)[] | null =
2373
2647
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2374
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2648
+ const barTargetName = targetLabel ?? text.target
2375
2649
  const barTargetColor = labelColor ?? guideColor
2376
2650
 
2377
2651
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2585,7 +2859,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2585
2859
  }
2586
2860
 
2587
2861
  /* -- forecast: dashed continuation (+ optional band) of one series */
2588
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2862
+ const forecastLabel = forecast?.label ?? text.forecast
2589
2863
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2590
2864
  const forecastValues: (number | null)[] = []
2591
2865
  if (forecast) {
@@ -3061,7 +3335,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3061
3335
  })
3062
3336
  }
3063
3337
  if (stackTotals && i <= lastActual && rows.length > 1) {
3064
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3338
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3065
3339
  }
3066
3340
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3067
3341
  if (provisional) {
@@ -3077,19 +3351,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3077
3351
  ...noteFor(at),
3078
3352
  })
3079
3353
  })
3080
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3354
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3081
3355
  }
3082
3356
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3083
3357
  const lines: string[] = []
3084
3358
  if (barTargets && i <= lastActual) {
3085
3359
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3086
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3360
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3087
3361
  if (note) lines.push(note)
3088
3362
  }
3089
3363
  for (const note of notes) {
3090
3364
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3091
3365
  }
3092
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3366
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3093
3367
  },
3094
3368
  },
3095
3369
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3181,8 +3455,6 @@ interface DonutOptionSlice {
3181
3455
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3182
3456
  const DONUT_OUTSIDE_LABELS = 2
3183
3457
 
3184
- const DEFAULT_TOTAL_LABEL = "合計"
3185
-
3186
3458
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3187
3459
 
3188
3460
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3215,15 +3487,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3215
3487
  }
3216
3488
 
3217
3489
  /**
3218
- * The default center label: what is totalled (「合計」), the formatted total
3490
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3219
3491
  * (`amount`, the big line), the unit (a small line under it) and both as one
3220
3492
  * string (`value`, 「9.2万 セッション」).
3221
3493
  */
3222
3494
  function donutCenterText(
3223
3495
  total: number,
3224
3496
  valueFormatter: (value: number) => string,
3225
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3497
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3226
3498
  ): { label: string; value: string; amount: string; unit?: string } {
3499
+ totalLabel ??= chartText(locale).total
3227
3500
  const amount = valueFormatter(total)
3228
3501
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3229
3502
  }
@@ -3451,8 +3724,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3451
3724
 
3452
3725
  /* ------------------------------------------------------------ provisional */
3453
3726
 
3454
- /** The default label for points that are not final yet. */
3455
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3727
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3728
+ function defaultProvisionalLabel(locale?: string): string {
3729
+ return chartText(locale).provisional
3730
+ }
3456
3731
 
3457
3732
  /**
3458
3733
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3504,6 +3779,11 @@ export interface BarChartProps<T extends Record<string, unknown> = Record<string
3504
3779
  barCategoryGap?: string | number
3505
3780
  }
3506
3781
 
3782
+ const TEXT = {
3783
+ ja: { item: "項目", chart: (categories: readonly string[]) => `${categories.join("・")}の棒グラフ` },
3784
+ en: { item: "Item", chart: (categories: readonly string[]) => `Bar chart of ${categories.join(", ")}` },
3785
+ }
3786
+
3507
3787
  /** Rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
3508
3788
  const NO_ROWS: never[] = []
3509
3789
 
@@ -3554,15 +3834,16 @@ function BarChartImpl<T extends Record<string, unknown>>(
3554
3834
  }: BarChartProps<T>,
3555
3835
  ref: ForwardedRef<HTMLDivElement>,
3556
3836
  ) {
3557
- const tokens = useChartTokens()
3558
3837
  // Highlight labels are backed by the surface the chart sits on (a card, the page).
3559
- const { ref: frameRef, surface, width: plotWidth } = useChartSurface(ref, tokens)
3838
+ const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
3839
+ const locale = useLocale()
3840
+ const t = TEXT[uiLanguage(locale)]
3560
3841
  // Ticks and on-plot labels read short (¥1億); the tooltip reads the value as `format` says.
3561
- const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format), [valueFormatter, format])
3562
- const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format), [valueFormatter, format])
3842
+ const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3843
+ const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3563
3844
  // Ticks share one unit — the largest tick's (¥0.5億 / ¥1億).
3564
- const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format), [valueFormatter, format])
3565
- const series = useMemo(() => aggregateSeries(data, categories), [data, categories])
3845
+ const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3846
+ const series = useMemo(() => aggregateSeries(data, categories, undefined, otherSeriesLabel(locale)), [data, categories, locale])
3566
3847
  const palette = useMemo(
3567
3848
  () =>
3568
3849
  resolveChartColors(
@@ -3624,24 +3905,25 @@ function BarChartImpl<T extends Record<string, unknown>>(
3624
3905
  plotWidth,
3625
3906
  plotHeight: typeof height === "number" ? height : undefined,
3626
3907
  chartOptions,
3908
+ locale,
3627
3909
  }),
3628
3910
  [
3629
3911
  series, index, palette, labels, thresholds, toneColors, tooltipFormatter, axisFormatter, axisTicks, stackType, barLayout, gap, showZeroValues,
3630
3912
  showLegend, showTooltip, showGridLines, showXAxis, showYAxis, startEndOnly, yAxisWidth,
3631
3913
  maxValue, allowDecimals, xAxisLabel, yAxisLabel, active,
3632
- provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions,
3914
+ provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions, locale,
3633
3915
  ],
3634
3916
  )
3635
3917
  // The screen-reader table: exact values (tooltip format), dates read as dates, 「その他」 as its own column.
3636
3918
  const dataTable = useMemo(() => {
3637
- const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, "項目")
3638
- const times = timeTooltipLabels(series.data.map((row) => row[index]))
3919
+ const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, t.item)
3920
+ const times = timeTooltipLabels(series.data.map((row) => row[index]), locale)
3639
3921
  return times ? { ...table, rows: table.rows.map((row, i) => [times[i] ?? row[0] ?? "", ...row.slice(1)]) } : table
3640
- }, [series, index, tooltipFormatter, labels])
3641
- const label = ariaLabel ?? `${categories.join("・")}の棒グラフ`
3922
+ }, [series, index, tooltipFormatter, labels, t, locale])
3923
+ const label = ariaLabel ?? t.chart(categories)
3642
3924
  const resolvedSummary = useMemo(
3643
- () => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter }) : summary),
3644
- [summary, data, index, categories, title, format, valueFormatter],
3925
+ () => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter, locale }) : summary),
3926
+ [summary, data, index, categories, title, format, valueFormatter, locale],
3645
3927
  )
3646
3928
  return (
3647
3929
  <ChartFrame