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