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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
  4. package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/chunk-2IAPIOPS.js +152 -0
  6. package/dist/chunk-2IAPIOPS.js.map +1 -0
  7. package/dist/chunk-3RJEOP3J.js +174 -0
  8. package/dist/chunk-3RJEOP3J.js.map +1 -0
  9. package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
  10. package/dist/chunk-77CISBMG.js.map +1 -0
  11. package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
  12. package/dist/chunk-ANZLVSEP.js.map +1 -0
  13. package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
  14. package/dist/chunk-CKWPK4RO.js.map +1 -0
  15. package/dist/chunk-EG4PX726.js +678 -0
  16. package/dist/chunk-EG4PX726.js.map +1 -0
  17. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  18. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  19. package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
  20. package/dist/chunk-PJ4SPXHS.js.map +1 -0
  21. package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
  22. package/dist/chunk-QNOX7WLZ.js.map +1 -0
  23. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  24. package/dist/chunk-ROHNXP7J.js.map +1 -0
  25. package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
  26. package/dist/chunk-S5MDTHH7.js.map +1 -0
  27. package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
  28. package/dist/chunk-SDJTTMLK.js.map +1 -0
  29. package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
  30. package/dist/chunk-SHZM3EXB.js.map +1 -0
  31. package/dist/chunk-TRMHNMMZ.js +61 -0
  32. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  33. package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
  34. package/dist/chunk-UWSQUHX6.js.map +1 -0
  35. package/dist/cli.js +6 -6
  36. package/dist/client/index.d.ts +2 -0
  37. package/dist/client/index.js +8 -2
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/components/index.d.ts +57 -273
  40. package/dist/components/index.js +6 -3
  41. package/dist/delta-text-CbrOaW83.d.ts +17 -0
  42. package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
  43. package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
  44. package/dist/doc/index.d.ts +215 -2
  45. package/dist/doc/index.js +422 -4
  46. package/dist/doc/index.js.map +1 -1
  47. package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
  48. package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
  49. package/dist/format-DihQkL1N.d.ts +249 -0
  50. package/dist/query/index.js +2 -2
  51. package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
  52. package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
  53. package/dist/slide/index.d.ts +272 -1
  54. package/dist/slide/index.js +1023 -2
  55. package/dist/slide/index.js.map +1 -1
  56. package/dist/table/index.js +9 -6
  57. package/dist/table/index.js.map +1 -1
  58. package/dist/ui/index.d.ts +5 -2
  59. package/dist/ui/index.js +6 -6
  60. package/dist/ui/index.js.map +1 -1
  61. package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
  62. package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
  63. package/dist/vite/index.js +3 -3
  64. package/docs/en/agent-skills.md +1 -1
  65. package/docs/en/changelog.md +2 -0
  66. package/docs/en/components.md +13 -2
  67. package/docs/en/doc/delta-text.md +40 -0
  68. package/docs/en/doc/doc-action-items.md +49 -0
  69. package/docs/en/doc/doc-callout.md +38 -0
  70. package/docs/en/doc/doc-data-table.md +60 -0
  71. package/docs/en/doc/doc-executive-summary.md +64 -0
  72. package/docs/en/doc/doc-figure.md +56 -0
  73. package/docs/en/doc/doc-key-findings.md +61 -0
  74. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  75. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  76. package/docs/en/doc/doc-report-header.md +46 -0
  77. package/docs/en/pages-and-metadata.md +15 -0
  78. package/docs/en/parts/area-chart.md +99 -0
  79. package/docs/en/parts/bar-chart.md +91 -0
  80. package/docs/en/parts/bar-list.md +50 -0
  81. package/docs/en/parts/category-bar.md +37 -0
  82. package/docs/en/parts/chart-frame.md +39 -0
  83. package/docs/en/parts/delta.md +4 -3
  84. package/docs/en/parts/donut-chart.md +72 -0
  85. package/docs/en/parts/funnel-steps.md +2 -1
  86. package/docs/en/parts/heatmap.md +53 -0
  87. package/docs/en/parts/kpi-card.md +2 -1
  88. package/docs/en/parts/legend.md +35 -0
  89. package/docs/en/parts/line-chart.md +94 -0
  90. package/docs/en/parts/progress-circle.md +33 -0
  91. package/docs/en/parts/sample-data.md +39 -0
  92. package/docs/en/parts/scatter-chart.md +56 -0
  93. package/docs/en/parts/tracker.md +36 -0
  94. package/docs/en/parts/updated-at.md +30 -0
  95. package/docs/en/parts/waterfall-chart.md +56 -0
  96. package/docs/en/slide/slide-chart-full.md +48 -0
  97. package/docs/en/slide/slide-chart-insight.md +52 -0
  98. package/docs/en/slide/slide-columns.md +46 -0
  99. package/docs/en/slide/slide-comparison.md +68 -0
  100. package/docs/en/slide/slide-cover.md +39 -0
  101. package/docs/en/slide/slide-exec-summary.md +47 -0
  102. package/docs/en/slide/slide-heading.md +38 -0
  103. package/docs/en/slide/slide-kpi-grid.md +55 -0
  104. package/docs/en/slide/slide-kpi-hero.md +58 -0
  105. package/docs/en/slide/slide-next-actions.md +49 -0
  106. package/docs/en/slide/slide-scorecard-table.md +54 -0
  107. package/docs/en/slide/slide-section.md +44 -0
  108. package/docs/en/slide/slide-source-line.md +34 -0
  109. package/docs/en/ui/alert-dialog.md +42 -0
  110. package/docs/en/ui/avatar.md +31 -0
  111. package/docs/en/ui/combobox.md +74 -0
  112. package/docs/en/ui/drawer.md +41 -0
  113. package/docs/en/ui/empty.md +1 -1
  114. package/docs/en/ui/error-state.md +4 -3
  115. package/docs/en/ui/field.md +42 -0
  116. package/docs/en/ui/hover-card.md +21 -0
  117. package/docs/en/ui/kbd.md +17 -0
  118. package/docs/en/ui/loading.md +1 -1
  119. package/docs/en/ui/pagination.md +36 -0
  120. package/docs/en/ui/radio-group.md +21 -0
  121. package/docs/en/ui/toast.md +44 -0
  122. package/docs/en/ui/toggle-group.md +4 -3
  123. package/docs/index.json +857 -77
  124. package/docs/ja/agent-skills.md +1 -1
  125. package/docs/ja/changelog.md +2 -0
  126. package/docs/ja/components.md +12 -1
  127. package/docs/ja/doc/delta-text.md +40 -0
  128. package/docs/ja/doc/doc-action-items.md +49 -0
  129. package/docs/ja/doc/doc-callout.md +38 -0
  130. package/docs/ja/doc/doc-data-table.md +60 -0
  131. package/docs/ja/doc/doc-executive-summary.md +64 -0
  132. package/docs/ja/doc/doc-figure.md +56 -0
  133. package/docs/ja/doc/doc-key-findings.md +61 -0
  134. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  135. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  136. package/docs/ja/doc/doc-report-header.md +46 -0
  137. package/docs/ja/page-types/slide.md +1 -0
  138. package/docs/ja/pages-and-metadata.md +15 -0
  139. package/docs/ja/parts/area-chart.md +49 -25
  140. package/docs/ja/parts/bar-chart.md +50 -24
  141. package/docs/ja/parts/bar-list.md +31 -8
  142. package/docs/ja/parts/chart-frame.md +36 -0
  143. package/docs/ja/parts/delta.md +5 -4
  144. package/docs/ja/parts/donut-chart.md +45 -26
  145. package/docs/ja/parts/funnel-steps.md +27 -8
  146. package/docs/ja/parts/heatmap.md +45 -0
  147. package/docs/ja/parts/kpi-card.md +2 -1
  148. package/docs/ja/parts/legend.md +36 -0
  149. package/docs/ja/parts/line-chart.md +48 -22
  150. package/docs/ja/parts/sample-data.md +61 -0
  151. package/docs/ja/parts/scatter-chart.md +69 -0
  152. package/docs/ja/parts/updated-at.md +27 -0
  153. package/docs/ja/parts/waterfall-chart.md +61 -0
  154. package/docs/ja/slide/slide-chart-full.md +48 -0
  155. package/docs/ja/slide/slide-chart-insight.md +52 -0
  156. package/docs/ja/slide/slide-columns.md +46 -0
  157. package/docs/ja/slide/slide-comparison.md +68 -0
  158. package/docs/ja/slide/slide-cover.md +39 -0
  159. package/docs/ja/slide/slide-exec-summary.md +47 -0
  160. package/docs/ja/slide/slide-heading.md +38 -0
  161. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  162. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  163. package/docs/ja/slide/slide-next-actions.md +49 -0
  164. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  165. package/docs/ja/slide/slide-section.md +44 -0
  166. package/docs/ja/slide/slide-source-line.md +34 -0
  167. package/docs/ja/ui/empty.md +1 -1
  168. package/docs/ja/ui/error-state.md +4 -3
  169. package/docs/ja/ui/loading.md +1 -1
  170. package/docs/ja/ui/toggle-group.md +4 -3
  171. package/package.json +2 -2
  172. package/registry/ui/app-shell.tsx +138 -1
  173. package/registry/ui/area-chart.tsx +389 -104
  174. package/registry/ui/attention-list.tsx +177 -15
  175. package/registry/ui/bar-chart.tsx +386 -104
  176. package/registry/ui/bar-list.tsx +376 -94
  177. package/registry/ui/box-plot-chart.tsx +450 -116
  178. package/registry/ui/category-bar.tsx +292 -57
  179. package/registry/ui/chart-frame.tsx +301 -63
  180. package/registry/ui/data-table.tsx +229 -37
  181. package/registry/ui/date-range-picker.tsx +190 -36
  182. package/registry/ui/delta.tsx +181 -19
  183. package/registry/ui/donut-chart.tsx +385 -103
  184. package/registry/ui/echart.tsx +313 -58
  185. package/registry/ui/filter-bar.tsx +179 -20
  186. package/registry/ui/funnel-steps.tsx +354 -80
  187. package/registry/ui/heatmap.tsx +338 -71
  188. package/registry/ui/histogram-chart.tsx +440 -116
  189. package/registry/ui/kpi-card.tsx +254 -48
  190. package/registry/ui/legend.tsx +374 -93
  191. package/registry/ui/line-chart.tsx +389 -104
  192. package/registry/ui/multi-select.tsx +159 -9
  193. package/registry/ui/page-shell.tsx +148 -10
  194. package/registry/ui/paired-chart.tsx +394 -109
  195. package/registry/ui/progress-circle.tsx +205 -39
  196. package/registry/ui/sample-data.tsx +154 -10
  197. package/registry/ui/scatter-chart.tsx +436 -119
  198. package/registry/ui/searchable-select.tsx +145 -7
  199. package/registry/ui/slider.tsx +1 -1
  200. package/registry/ui/states.tsx +170 -13
  201. package/registry/ui/status-badge.tsx +201 -36
  202. package/registry/ui/tooltip.tsx +6 -2
  203. package/registry/ui/tracker.tsx +214 -42
  204. package/registry/ui/trend-indicator.tsx +151 -8
  205. package/registry/ui/updated-at.tsx +170 -16
  206. package/registry/ui/waterfall-chart.tsx +460 -121
  207. package/skills/vantage-design/SKILL.md +54 -54
  208. package/skills/vantage-design/references/extract.md +26 -27
  209. package/skills/vantage-design/references/format.md +12 -172
  210. package/skills/vantage-design/references/presets.md +50 -110
  211. package/skills/vantage-pitfalls/SKILL.md +5 -0
  212. package/skills/vantage-slide/SKILL.md +54 -36
  213. package/templates/AGENTS.md +32 -16
  214. package/templates/agents/doc.md +48 -0
  215. package/templates/agents/page.md +23 -2
  216. package/templates/agents/slide.md +77 -3
  217. package/theme.css +23 -19
  218. package/dist/chunk-2ICNHUG6.js.map +0 -1
  219. package/dist/chunk-2N72FKLB.js.map +0 -1
  220. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  221. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  222. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  223. package/dist/chunk-JKLLFVEU.js.map +0 -1
  224. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  225. package/dist/chunk-V3LF7BE7.js.map +0 -1
  226. package/dist/chunk-WAMZWZJB.js.map +0 -1
  227. package/dist/chunk-XPQSTZUU.js.map +0 -1
  228. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -17,14 +17,147 @@ import { ChartFrame } from "@squadbase/vantage/components"
17
17
  import { EChart } from "@squadbase/vantage/components"
18
18
  import type { ChartCommonProps, ChartColors, NumericKeys, ValueFormatter } from "@squadbase/vantage/components"
19
19
  import type { ValueFormat } from "@squadbase/vantage/components"
20
- import { createContext, useCallback, useEffect, useRef, useState } from "react"
20
+ import { useSyncExternalStore } from "react"
21
+ import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
21
22
  import type { ChartTone } from "@squadbase/vantage/components"
22
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
23
+ import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
23
24
  import type { ChartColor, ChartSeriesColor } from "@squadbase/vantage/components"
24
25
  import { type ReactNode } from "react"
25
26
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
27
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
26
28
  import type { ChartThreshold, ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight } from "@squadbase/vantage/components"
27
29
 
30
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
31
+
32
+ /**
33
+ * The display language of the parts and the defaults that follow it.
34
+ *
35
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
36
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
37
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
38
+ *
39
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
40
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
41
+ */
42
+
43
+ /** The language of the built-in wording: Japanese or English. */
44
+ type UiLanguage = "ja" | "en"
45
+
46
+ /** The locale used when `<html lang>` gives none (and on the server). */
47
+ const FALLBACK_LOCALE = "en-US"
48
+
49
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
50
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
51
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
52
+ }
53
+
54
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
55
+ function normalizeLocale(tag: string | null | undefined): string {
56
+ const value = tag?.trim()
57
+ if (!value) return FALLBACK_LOCALE
58
+ try {
59
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
60
+ } catch {
61
+ return FALLBACK_LOCALE
62
+ }
63
+ }
64
+
65
+ /**
66
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
67
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
68
+ */
69
+ function documentLocale(): string {
70
+ if (typeof document === "undefined") return FALLBACK_LOCALE
71
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
72
+ }
73
+
74
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
75
+ function resolveLocale(locale?: string | null): string {
76
+ return locale || documentLocale()
77
+ }
78
+
79
+ /** Picks the wording table of the locale's language. */
80
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
81
+ return table[uiLanguage(resolveLocale(locale))]
82
+ }
83
+
84
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
85
+ function defaultCurrency(locale?: string | null): string {
86
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
87
+ }
88
+
89
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
90
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
91
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
92
+ }
93
+
94
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
95
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
96
+
97
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
98
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
99
+
100
+ /**
101
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
102
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
103
+ * Monday for the rest.
104
+ */
105
+ function weekStartsOn(locale?: string | null): WeekDay {
106
+ const tag = normalizeLocale(resolveLocale(locale))
107
+ if (uiLanguage(tag) === "ja") return 1
108
+ try {
109
+ const info = new Intl.Locale(tag) as Intl.Locale & {
110
+ getWeekInfo?: () => { firstDay: number }
111
+ weekInfo?: { firstDay: number }
112
+ }
113
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
114
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
115
+ } catch {
116
+ // Fall through to the table below.
117
+ }
118
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
119
+ }
120
+
121
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
122
+
123
+ const listeners = new Set<() => void>()
124
+ let observer: MutationObserver | null = null
125
+
126
+ /** One observer on `<html lang>` for every subscribed part. */
127
+ function subscribe(onChange: () => void): () => void {
128
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
129
+ listeners.add(onChange)
130
+ if (!observer) {
131
+ observer = new MutationObserver(() => {
132
+ for (const listener of [...listeners]) listener()
133
+ })
134
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
135
+ }
136
+ return () => {
137
+ listeners.delete(onChange)
138
+ if (listeners.size === 0 && observer) {
139
+ observer.disconnect()
140
+ observer = null
141
+ }
142
+ }
143
+ }
144
+
145
+ const serverLocale = () => FALLBACK_LOCALE
146
+
147
+ /**
148
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
149
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
150
+ */
151
+ function useDocumentLocale(): string {
152
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
153
+ }
154
+
155
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
156
+ function useLocale(locale?: string | null): string {
157
+ const page = useDocumentLocale()
158
+ return locale || page
159
+ }
160
+
28
161
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
29
162
 
30
163
  /**
@@ -59,10 +192,23 @@ interface ChartTokens {
59
192
  border: string | null
60
193
  /** Tooltip surface. */
61
194
  popover: string | null
195
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
196
+ colors?: Record<string, string>
197
+ /** Actual inherited painting surface (may change without a token changing). */
198
+ surface?: string | null
62
199
  /** Tooltip text. */
63
200
  popoverForeground: string | null
64
201
  }
65
202
 
203
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
204
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
205
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
206
+ const OPTION_COLOR_TOKENS = [
207
+ "--background", "--card", "--muted", "--primary", "--destructive",
208
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
209
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
210
+ ]
211
+
66
212
  let normalizeContext: CanvasRenderingContext2D | null | undefined
67
213
 
68
214
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -118,8 +264,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
118
264
  const color = read(token)
119
265
  if (color) series.push(color)
120
266
  }
121
- return {
267
+ const tokens: ChartTokens = {
122
268
  series,
269
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
270
+ surface: readChartSurface(target),
123
271
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
124
272
  foreground: read("--foreground"),
125
273
  mutedForeground: read("--muted-foreground"),
@@ -127,6 +275,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
127
275
  popover: read("--popover"),
128
276
  popoverForeground: read("--popover-foreground"),
129
277
  }
278
+ tokenScopes.set(tokens, target)
279
+ return tokens
130
280
  }
131
281
 
132
282
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -134,6 +284,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
134
284
  if (a === b) return true
135
285
  if (!a || !b) return false
136
286
  return (
287
+ tokenScopes.get(a) === tokenScopes.get(b) &&
288
+ a.surface === b.surface &&
289
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
290
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
137
291
  a.series.length === b.series.length &&
138
292
  a.series.every((color, i) => color === b.series[i]) &&
139
293
  a.fontFamily === b.fontFamily &&
@@ -159,7 +313,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
159
313
  * attributes alone leaves the chart painted in the previous palette until a
160
314
  * reload — the tokens in the DOM are new, the canvas is not.
161
315
  */
162
- function watchChartTokens(onChange: () => void): () => void {
316
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
163
317
  if (typeof document === "undefined") return () => {}
164
318
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
165
319
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -174,8 +328,11 @@ function watchChartTokens(onChange: () => void): () => void {
174
328
 
175
329
  const themeObserver = new MutationObserver(schedule)
176
330
  const attributeFilter = ["class", "style", "data-theme"]
177
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
178
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
331
+ // Only the chart and its ancestors can change its inherited theme. Never observe
332
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
333
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
334
+ themeObserver.observe(node, { attributes: true, attributeFilter })
335
+ }
179
336
 
180
337
  const styleObserver = new MutationObserver(schedule)
181
338
  if (document.head) {
@@ -263,22 +420,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
263
420
  * components that build an ECharts option from token colours themselves (the
264
421
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
265
422
  */
266
- function useChartTokens(): ChartTokens | null {
423
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
267
424
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
268
425
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
269
426
  // replayed by React on every rebase and returns a fresh object each time, so the state
270
427
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
271
428
  const tokensRef = useRef(tokens)
272
429
  useEffect(() => {
430
+ const target = scope && "current" in scope ? scope.current : scope
273
431
  const sync = () => {
274
- const next = readChartTokens()
432
+ const next = readChartTokens(target)
275
433
  if (sameChartTokens(tokensRef.current, next)) return
276
434
  tokensRef.current = next
277
435
  setTokens(next)
278
436
  }
279
437
  sync()
280
- return watchChartTokens(sync)
281
- }, [])
438
+ return watchChartTokens(sync, target)
439
+ }, [scope])
282
440
  return tokens
283
441
  }
284
442
 
@@ -290,7 +448,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
290
448
  /**
291
449
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
292
450
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
293
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
451
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
294
452
  * other CSS colour is normalized like the tokens are. A colour that can't be
295
453
  * resolved comes back as `""`, which ECharts treats as "use the palette".
296
454
  */
@@ -301,12 +459,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
301
459
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
302
460
  if (TONE_NAME_RE.test(color.trim())) {
303
461
  if (typeof document === "undefined") return ""
304
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
462
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
463
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
305
464
  }
306
465
  if (typeof document === "undefined") return color
307
466
  let value = color.trim()
308
467
  const cssVar = CSS_VAR_RE.exec(value)
309
- const styles = getComputedStyle(document.documentElement)
468
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
310
469
  if (cssVar) {
311
470
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
312
471
  } else if (value.includes("var(")) {
@@ -352,7 +511,7 @@ function readChartSurface(el: Element | null): string | null {
352
511
  if (color) return color
353
512
  }
354
513
  }
355
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
514
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
356
515
  }
357
516
 
358
517
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -366,9 +525,10 @@ const WIDTH_BUCKET = 40
366
525
  */
367
526
  function useChartSurface<E extends Element>(
368
527
  forwarded: ForwardedRef<E>,
369
- tokens: ChartTokens | null,
370
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
528
+ tokens?: ChartTokens | null,
529
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
371
530
  const [node, setNode] = useState<E | null>(null)
531
+ const scopedTokens = useChartTokens(node)
372
532
  const [surface, setSurface] = useState<string | undefined>(undefined)
373
533
  const [width, setWidth] = useState<number | undefined>(undefined)
374
534
  const ref = useCallback(
@@ -382,7 +542,7 @@ function useChartSurface<E extends Element>(
382
542
  useEffect(() => {
383
543
  if (!node) return
384
544
  setSurface(readChartSurface(node) ?? undefined)
385
- }, [node, tokens])
545
+ }, [node, tokens, scopedTokens])
386
546
  useEffect(() => {
387
547
  if (!node || typeof ResizeObserver === "undefined") return
388
548
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -391,7 +551,7 @@ function useChartSurface<E extends Element>(
391
551
  observer.observe(node)
392
552
  return () => observer.disconnect()
393
553
  }, [node])
394
- return { ref, surface, width }
554
+ return { ref, surface, width, tokens: scopedTokens }
395
555
  }
396
556
 
397
557
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -501,9 +661,10 @@ function resolveValueFormatter(
501
661
  function resolveAxisFormatter(
502
662
  valueFormatter: ValueFormatter | undefined,
503
663
  format: ValueFormat | undefined,
504
- locale: string = DEFAULT_LOCALE,
664
+ localeArg?: string,
505
665
  ): ValueFormatter {
506
666
  if (valueFormatter) return valueFormatter
667
+ const locale = resolveLocale(localeArg)
507
668
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
508
669
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
509
670
  const currency = "currency" in spec ? spec.currency : undefined
@@ -512,40 +673,75 @@ function resolveAxisFormatter(
512
673
  return (value) => formatValue(value, short, locale)
513
674
  }
514
675
 
515
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
516
- const JA_UNITS = [
517
- { char: "兆", size: 1e12 },
518
- { char: "億", size: 1e8 },
519
- { char: "万", size: 1e4 },
520
- ] as const
676
+ interface CompactUnit {
677
+ token: string
678
+ size: number
679
+ }
680
+
681
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
521
682
 
522
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
683
+ /**
684
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
685
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
686
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
687
+ */
688
+ function compactUnits(locale: string): CompactUnit[] | null {
689
+ const cached = compactUnitCache.get(locale)
690
+ if (cached !== undefined) return cached
691
+ let units: CompactUnit[] | null = []
692
+ try {
693
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
694
+ const seen = new Set<string>()
695
+ for (let exp = 3; exp <= 15; exp++) {
696
+ const parts = compact.formatToParts(10 ** exp)
697
+ const at = parts.findIndex((p) => p.type === "compact")
698
+ if (at === -1) continue
699
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
700
+ units = null
701
+ break
702
+ }
703
+ const token = parts[at]!.value
704
+ if (seen.has(token)) continue
705
+ seen.add(token)
706
+ units.push({ token, size: 10 ** exp })
707
+ }
708
+ } catch {
709
+ units = null
710
+ }
711
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
712
+ compactUnitCache.set(locale, sorted)
713
+ return sorted
714
+ }
715
+
716
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
523
717
 
524
718
  /**
525
719
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
526
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
527
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
528
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
720
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
721
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
722
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
723
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
529
724
  */
530
725
  function resolveAxisTickFormatter(
531
726
  valueFormatter: ValueFormatter | undefined,
532
727
  format: ValueFormat | undefined,
533
- locale: string = DEFAULT_LOCALE,
728
+ localeArg?: string,
534
729
  ): (top: number) => ValueFormatter {
730
+ const locale = resolveLocale(localeArg)
535
731
  const base = resolveAxisFormatter(valueFormatter, format, locale)
536
732
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
537
- const aligns =
538
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
539
- if (!aligns) return () => base
733
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
734
+ if (!units) return () => base
540
735
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
736
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
541
737
  return (top) => {
542
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
738
+ const unit = units.find((u) => Math.abs(top) >= u.size)
543
739
  if (!unit) return base
544
740
  return (value) => {
545
741
  const text = base(value)
546
742
  if (value === 0 || !Number.isFinite(value)) return text
547
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
548
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
743
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
744
+ return text.replace(numberToken, () => scaled)
549
745
  }
550
746
  }
551
747
  }
@@ -574,8 +770,20 @@ function clamp(value: number, min: number, max: number): number {
574
770
  /** How many categories a chart colours individually; the rest fold into「その他」. */
575
771
  const MAX_CHART_SERIES = 5
576
772
 
577
- /** The series name the folded categories get. */
578
- const OTHER_SERIES_LABEL = "その他"
773
+ /** The series name the folded categories get, by language. */
774
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
775
+
776
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
777
+ function otherSeriesLabel(locale?: string): string {
778
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
779
+ }
780
+
781
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
782
+
783
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
784
+ function isOtherName(name: string | null | undefined): boolean {
785
+ return typeof name === "string" && OTHER_NAMES.has(name)
786
+ }
579
787
 
580
788
  interface AggregatedItems<Item extends { name: string; value: number }> {
581
789
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -595,7 +803,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
595
803
  function aggregateTopN<Item extends { name: string; value: number }>(
596
804
  items: readonly Item[],
597
805
  n: number = MAX_CHART_SERIES,
598
- otherLabel: string = OTHER_SERIES_LABEL,
806
+ otherLabel: string = otherSeriesLabel(),
599
807
  ): AggregatedItems<Item> {
600
808
  if (items.length <= n) return { top: [...items], rest: [], other: null }
601
809
  const ranked = items
@@ -624,7 +832,7 @@ function aggregateSeries(
624
832
  data: readonly Record<string, unknown>[],
625
833
  categories: readonly string[],
626
834
  n: number = MAX_CHART_SERIES,
627
- otherLabel: string = OTHER_SERIES_LABEL,
835
+ otherLabel: string = otherSeriesLabel(),
628
836
  ): AggregatedSeries {
629
837
  const totals = categories.map((name) => ({
630
838
  name,
@@ -656,12 +864,14 @@ function aggregateSeriesKeeping(
656
864
  categories: readonly string[],
657
865
  keep: string | undefined,
658
866
  n: number = MAX_CHART_SERIES,
867
+ otherLabel: string = otherSeriesLabel(),
659
868
  ): AggregatedSeries {
660
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
869
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
661
870
  const aggregated = aggregateSeries(
662
871
  data,
663
872
  categories.filter((c) => c !== keep),
664
873
  n,
874
+ otherLabel,
665
875
  )
666
876
  return { ...aggregated, categories: [...aggregated.categories, keep] }
667
877
  }
@@ -697,7 +907,7 @@ function seriesColorsWithComparison(
697
907
  * Colour per drawn series: the record's token for that category, else the
698
908
  * array's token at the category's position in `allCategories` (cycled), else
699
909
  * the token palette by drawn position. `otherCategory` — and a category the
700
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
910
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
701
911
  */
702
912
  function seriesColorsFor(
703
913
  categories: readonly string[],
@@ -709,7 +919,7 @@ function seriesColorsFor(
709
919
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
710
920
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
711
921
  return categories.map((category, i) => {
712
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
922
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
713
923
  const fromRecord = record?.[category]
714
924
  if (fromRecord) return fromRecord
715
925
  if (list && list.length > 0) {
@@ -756,7 +966,7 @@ interface DonutSlicesConfig {
756
966
  */
757
967
  function donutSlices(
758
968
  data: readonly Record<string, unknown>[],
759
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
969
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
760
970
  ): DonutSlice[] {
761
971
  const items = data
762
972
  .map((row, i) => {
@@ -769,7 +979,8 @@ function donutSlices(
769
979
  const slices: DonutSlice[] = top.map((item, rank) => ({
770
980
  name: item.name,
771
981
  value: item.value,
772
- color: palette[rank % palette.length]!,
982
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
983
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
773
984
  isOther: false,
774
985
  row: item.row,
775
986
  }))
@@ -842,7 +1053,7 @@ interface BarListRowsConfig extends BarListOptions {
842
1053
  */
843
1054
  function barListRows<Row extends object>(
844
1055
  data: readonly Row[],
845
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1056
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
846
1057
  ): BarListRow<Row>[] {
847
1058
  const items = data.map((row): BarListRow<Row> => {
848
1059
  const cells = row as Record<string, unknown>
@@ -888,19 +1099,21 @@ const BAR_LIST_OTHER_TINT = 30
888
1099
 
889
1100
  /**
890
1101
  * The fill of each BarList row: all real rows in the same tint of `color`
891
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1102
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
892
1103
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
893
1104
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
894
1105
  * for that row. A tint, not the solid token, so the label printed over the bar
895
1106
  * keeps its contrast.
896
1107
  */
897
1108
  function barListFills(
898
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1109
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
899
1110
  color: ChartColor,
900
1111
  otherColor: string,
901
1112
  ): string[] {
902
1113
  return rows.map((row) =>
903
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1114
+ row.isOther || isOtherName(row.name)
1115
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1116
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
904
1117
  )
905
1118
  }
906
1119
 
@@ -1116,11 +1329,30 @@ interface ChartSummaryInput {
1116
1329
  format?: ValueFormat
1117
1330
  /** Wins over `format`. */
1118
1331
  valueFormatter?: ValueFormatter
1332
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1333
+ locale?: string
1334
+ }
1335
+
1336
+ const SUMMARY_TEXT = {
1337
+ ja: {
1338
+ up: "、前の点より増加",
1339
+ down: "、前の点より減少",
1340
+ flat: "、前の点と横ばい",
1341
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1342
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1343
+ },
1344
+ en: {
1345
+ up: ", up from the previous point",
1346
+ down: ", down from the previous point",
1347
+ flat: ", flat from the previous point",
1348
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1349
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1350
+ },
1119
1351
  }
1120
1352
 
1121
1353
  /**
1122
1354
  * One-sentence summary built from the rows, for `summary="auto"`:
1123
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1355
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1124
1356
  * computed from the last two non-empty points of the first series — never
1125
1357
  * written by hand, so it cannot contradict the data. `undefined` when there is
1126
1358
  * no usable point.
@@ -1136,30 +1368,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1136
1368
  }
1137
1369
  const latest = points[points.length - 1]
1138
1370
  if (!latest) return undefined
1139
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1371
+ const locale = resolveLocale(input.locale)
1372
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1373
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1140
1374
  const prev = points[points.length - 2]
1141
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1375
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1142
1376
  const head = input.title ? `${input.title}: ` : ""
1143
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1377
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1144
1378
  }
1145
1379
 
1146
1380
  /**
1147
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1148
- * (largest first). `undefined` when there is nothing to describe.
1381
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1382
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1149
1383
  */
1150
1384
  function buildShareSummary(
1151
1385
  slices: readonly { name: string; value: number }[],
1152
1386
  title?: string,
1387
+ locale?: string,
1153
1388
  ): string | undefined {
1154
1389
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1155
1390
  const top = slices[0]
1156
1391
  if (!top || total <= 0) return undefined
1157
1392
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1158
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1393
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1159
1394
  }
1160
1395
 
1161
1396
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1162
1397
 
1398
+ /** The built-in words of the option builders, by language. */
1399
+ const CHART_TEXT = {
1400
+ ja: {
1401
+ target: "目標",
1402
+ forecast: "予測",
1403
+ total: "合計",
1404
+ provisional: "速報",
1405
+ noComparison: "比較なし",
1406
+ share: (share: string) => `(${share})`,
1407
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1408
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1409
+ },
1410
+ en: {
1411
+ target: "Target",
1412
+ forecast: "Forecast",
1413
+ total: "Total",
1414
+ provisional: "Preliminary",
1415
+ noComparison: "No comparison",
1416
+ share: (share: string) => ` (${share})`,
1417
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1418
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1419
+ },
1420
+ } satisfies Record<UiLanguage, unknown>
1421
+
1422
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1423
+
1163
1424
  type CartesianChartKind = "area" | "bar" | "line"
1164
1425
  type CartesianStackType = "default" | "stacked" | "percent"
1165
1426
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1344,7 +1605,7 @@ interface CartesianChartConfig {
1344
1605
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1345
1606
  */
1346
1607
  axisTicks?: (top: number) => (value: number) => string
1347
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1608
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1348
1609
  locale?: string
1349
1610
  type?: CartesianStackType
1350
1611
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1582,11 +1843,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1582
1843
  */
1583
1844
  function timeAxisLabels(
1584
1845
  values: readonly unknown[],
1585
- locale: string = DEFAULT_LOCALE,
1846
+ localeArg?: string,
1586
1847
  shown: (i: number) => boolean = () => true,
1587
1848
  ): string[] | null {
1588
1849
  const parsed = parseTimeIndex(values)
1589
1850
  if (!parsed) return null
1851
+ const locale = resolveLocale(localeArg)
1590
1852
  const { granularity, times } = parsed
1591
1853
  let prevYear: number | null = null
1592
1854
  return times.map((t, i) => {
@@ -1609,9 +1871,10 @@ function timeAxisLabels(
1609
1871
  }
1610
1872
 
1611
1873
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1612
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1874
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1613
1875
  const parsed = parseTimeIndex(values)
1614
1876
  if (!parsed) return null
1877
+ const locale = resolveLocale(localeArg)
1615
1878
  const options: Intl.DateTimeFormatOptions =
1616
1879
  parsed.granularity === "year"
1617
1880
  ? { year: "numeric" }
@@ -1751,7 +2014,9 @@ function tooltipHtml(
1751
2014
  rows: readonly TooltipRow[],
1752
2015
  valueFormatter: (value: number) => string,
1753
2016
  notes: readonly string[] = [],
2017
+ locale?: string,
1754
2018
  ): string {
2019
+ const t = chartText(locale)
1755
2020
  const lines = rows.map(
1756
2021
  (row) =>
1757
2022
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1760,7 +2025,7 @@ function tooltipHtml(
1760
2025
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1761
2026
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1762
2027
  }${
1763
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2028
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1764
2029
  }</span></div>` +
1765
2030
  (row.note === undefined
1766
2031
  ? ""
@@ -1774,8 +2039,10 @@ function tooltipHtml(
1774
2039
 
1775
2040
  /* ------------------------------------------------- per-bar targets / notes */
1776
2041
 
1777
- /** Default name of the per-bar target marker (legend, tooltip). */
1778
- const BAR_TARGET_LABEL = "目標"
2042
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2043
+ function barTargetLabel(locale?: string): string {
2044
+ return chartText(locale).target
2045
+ }
1779
2046
 
1780
2047
  /** How far (px) the target marker reaches past each side of its bar. */
1781
2048
  const TARGET_MARKER_OVERHANG = 4
@@ -1826,16 +2093,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1826
2093
  return (value / target) * 100
1827
2094
  }
1828
2095
 
1829
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2096
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1830
2097
  function barTargetNote(
1831
2098
  label: string,
1832
2099
  value: number | null,
1833
2100
  target: number | null,
1834
2101
  valueFormatter: (value: number) => string,
2102
+ locale?: string,
1835
2103
  ): string | null {
1836
2104
  if (target === null) return null
1837
2105
  const rate = achievementRate(value, target)
1838
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2106
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1839
2107
  }
1840
2108
 
1841
2109
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1927,11 +2195,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1927
2195
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1928
2196
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1929
2197
 
1930
- const DEFAULT_FORECAST_LABEL = "予測"
1931
-
1932
- /** 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. */
1933
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1934
- return `${label} →`
2198
+ /**
2199
+ * 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.
2200
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2201
+ */
2202
+ function forecastBoundaryText(label?: string, locale?: string): string {
2203
+ return `${label ?? chartText(locale).forecast} →`
1935
2204
  }
1936
2205
 
1937
2206
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1971,8 +2240,10 @@ interface AxisExtent {
1971
2240
  max: number
1972
2241
  }
1973
2242
 
1974
- /** The label of a stack's total (highlight and tooltip). */
1975
- const STACK_TOTAL_LABEL = "合計"
2243
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2244
+ function stackTotalLabel(locale?: string): string {
2245
+ return chartText(locale).total
2246
+ }
1976
2247
 
1977
2248
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
1978
2249
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2094,8 +2365,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2094
2365
 
2095
2366
  /**
2096
2367
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2097
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2098
- * either value is missing or the baseline is 0.
2368
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2369
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2099
2370
  */
2100
2371
  function comparisonNote(
2101
2372
  current: number | null,
@@ -2103,16 +2374,18 @@ function comparisonNote(
2103
2374
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2104
2375
  grain?: PeriodGrain,
2105
2376
  format?: ValueFormat,
2106
- locale: string = DEFAULT_LOCALE,
2377
+ localeArg?: string,
2107
2378
  ): string {
2379
+ const locale = resolveLocale(localeArg)
2108
2380
  const result = computeComparison(
2109
2381
  current,
2110
2382
  comparison.vs === "custom"
2111
2383
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2112
2384
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2385
+ { locale },
2113
2386
  )
2114
2387
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2115
- if (text === null) return `比較なし ${result.label}`
2388
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2116
2389
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2117
2390
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2118
2391
  }
@@ -2155,7 +2428,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2155
2428
  toneColors,
2156
2429
  valueFormatter,
2157
2430
  axisFormatter = valueFormatter,
2158
- locale = DEFAULT_LOCALE,
2159
2431
  type = "default",
2160
2432
  layout = "horizontal",
2161
2433
  curveType = "linear",
@@ -2192,7 +2464,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2192
2464
  chartOptions,
2193
2465
  reserveActions = false,
2194
2466
  } = cfg
2195
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2467
+ const locale = resolveLocale(cfg.locale)
2468
+ const text = chartText(locale)
2469
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2196
2470
 
2197
2471
  const stacked = kind !== "line" && type !== "default"
2198
2472
  const percent = kind !== "line" && type === "percent"
@@ -2341,9 +2615,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2341
2615
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2342
2616
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2343
2617
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2344
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2618
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2345
2619
  const highlightText = (value: number) =>
2346
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2620
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2347
2621
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2348
2622
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2349
2623
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2369,7 +2643,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2369
2643
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2370
2644
  const barTargets: (number | null)[] | null =
2371
2645
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2372
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2646
+ const barTargetName = targetLabel ?? text.target
2373
2647
  const barTargetColor = labelColor ?? guideColor
2374
2648
 
2375
2649
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2583,7 +2857,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2583
2857
  }
2584
2858
 
2585
2859
  /* -- forecast: dashed continuation (+ optional band) of one series */
2586
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2860
+ const forecastLabel = forecast?.label ?? text.forecast
2587
2861
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2588
2862
  const forecastValues: (number | null)[] = []
2589
2863
  if (forecast) {
@@ -3059,7 +3333,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3059
3333
  })
3060
3334
  }
3061
3335
  if (stackTotals && i <= lastActual && rows.length > 1) {
3062
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3336
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3063
3337
  }
3064
3338
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3065
3339
  if (provisional) {
@@ -3075,19 +3349,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3075
3349
  ...noteFor(at),
3076
3350
  })
3077
3351
  })
3078
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3352
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3079
3353
  }
3080
3354
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3081
3355
  const lines: string[] = []
3082
3356
  if (barTargets && i <= lastActual) {
3083
3357
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3084
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3358
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3085
3359
  if (note) lines.push(note)
3086
3360
  }
3087
3361
  for (const note of notes) {
3088
3362
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3089
3363
  }
3090
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3364
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3091
3365
  },
3092
3366
  },
3093
3367
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3179,8 +3453,6 @@ interface DonutOptionSlice {
3179
3453
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3180
3454
  const DONUT_OUTSIDE_LABELS = 2
3181
3455
 
3182
- const DEFAULT_TOTAL_LABEL = "合計"
3183
-
3184
3456
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3185
3457
 
3186
3458
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3213,15 +3485,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3213
3485
  }
3214
3486
 
3215
3487
  /**
3216
- * The default center label: what is totalled (「合計」), the formatted total
3488
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3217
3489
  * (`amount`, the big line), the unit (a small line under it) and both as one
3218
3490
  * string (`value`, 「9.2万 セッション」).
3219
3491
  */
3220
3492
  function donutCenterText(
3221
3493
  total: number,
3222
3494
  valueFormatter: (value: number) => string,
3223
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3495
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3224
3496
  ): { label: string; value: string; amount: string; unit?: string } {
3497
+ totalLabel ??= chartText(locale).total
3225
3498
  const amount = valueFormatter(total)
3226
3499
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3227
3500
  }
@@ -3449,8 +3722,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3449
3722
 
3450
3723
  /* ------------------------------------------------------------ provisional */
3451
3724
 
3452
- /** The default label for points that are not final yet. */
3453
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3725
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3726
+ function defaultProvisionalLabel(locale?: string): string {
3727
+ return chartText(locale).provisional
3728
+ }
3454
3729
 
3455
3730
  /**
3456
3731
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3483,6 +3758,11 @@ function splitProvisional(
3483
3758
 
3484
3759
  /* ---------- end of inlined code ---------- */
3485
3760
 
3761
+ const TEXT = {
3762
+ ja: { item: "項目", chart: (keys: readonly string[]) => `${keys.join("・")}の推移(上段 棒、下段 線)` },
3763
+ en: { item: "Item", chart: (keys: readonly string[]) => `Trend of ${keys.join(", ")} (bars on top, line below)` },
3764
+ }
3765
+
3486
3766
  /** 1 段ぶんの設定。 */
3487
3767
  export interface PairedPanel<T extends Record<string, unknown> = Record<string, unknown>> {
3488
3768
  /** この段に描く系列の列。 */
@@ -3572,6 +3852,8 @@ export interface PairedOptionConfig {
3572
3852
  plotWidth?: number
3573
3853
  guideColor?: string
3574
3854
  chartOptions?: EChartsOption
3855
+ /** 時間軸のラベル・ツールチップの言語。既定はページのロケール(`<html lang>`)。 */
3856
+ locale?: string
3575
3857
  }
3576
3858
 
3577
3859
  /** 棒と線の `buildCartesianOption` を 1 つの option(2 つの grid・共有の横軸・連動するツールチップ)に合成する。 */
@@ -3594,6 +3876,7 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
3594
3876
  highlight: "none" as const,
3595
3877
  yAxisWidth,
3596
3878
  reducedMotion: false,
3879
+ locale: cfg.locale,
3597
3880
  }
3598
3881
  const barOpt = buildCartesianOption({
3599
3882
  ...common,
@@ -3628,7 +3911,7 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
3628
3911
  plotHeight: room - upper,
3629
3912
  }) as AnyRecord
3630
3913
 
3631
- const times = timeTooltipLabels(data.map((row) => row[index]))
3914
+ const times = timeTooltipLabels(data.map((row) => row[index]), cfg.locale)
3632
3915
  const titles = labelRows(data, index, labels).map((row, i) => String(times?.[i] ?? row[index] ?? ""))
3633
3916
  const cell = (row: Record<string, unknown>, key: string): number | null => {
3634
3917
  const v = row[key]
@@ -3638,13 +3921,13 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
3638
3921
  const rowsOf = (row: Record<string, unknown>, keys: readonly string[], palette: readonly string[]): TooltipRow[] =>
3639
3922
  keys.map((key, i) => ({ name: labelOf(key, labels), color: palette[i] || "currentColor", value: cell(row, key) }))
3640
3923
  // 1 枚のツールチップ: 見出しは 1 回、棒の行 → 線の行(段ごとの書式)。
3641
- const emptyTitle = tooltipHtml("", [], String)
3924
+ const emptyTitle = tooltipHtml("", [], String, [], cfg.locale)
3642
3925
  const tooltipFormatter = (params: unknown) => {
3643
3926
  const list = (Array.isArray(params) ? params : [params]) as { dataIndex?: number }[]
3644
3927
  const i = list[0]?.dataIndex ?? 0
3645
3928
  const row = data[i] ?? {}
3646
- const upperHtml = tooltipHtml(titles[i] ?? "", rowsOf(row, barKeys, cfg.barPalette), cfg.barFormatter)
3647
- const lowerHtml = tooltipHtml("", rowsOf(row, lineKeys, cfg.linePalette), cfg.lineFormatter).slice(emptyTitle.length)
3929
+ const upperHtml = tooltipHtml(titles[i] ?? "", rowsOf(row, barKeys, cfg.barPalette), cfg.barFormatter, [], cfg.locale)
3930
+ const lowerHtml = tooltipHtml("", rowsOf(row, lineKeys, cfg.linePalette), cfg.lineFormatter, [], cfg.locale).slice(emptyTitle.length)
3648
3931
  return `${upperHtml}${lowerHtml}`
3649
3932
  }
3650
3933
 
@@ -3707,16 +3990,17 @@ function PairedChartImpl<T extends Record<string, unknown>>(
3707
3990
  }: PairedChartProps<T>,
3708
3991
  ref: ForwardedRef<HTMLDivElement>,
3709
3992
  ) {
3710
- const tokens = useChartTokens()
3711
- const { ref: frameRef, width: plotWidth } = useChartSurface(ref, tokens)
3993
+ const { ref: frameRef, width: plotWidth, tokens } = useChartSurface(ref)
3712
3994
  const barKeys = (bars?.categories ?? NO_KEYS) as string[]
3713
3995
  const lineKeys = (line?.categories ?? NO_KEYS) as string[]
3714
- const barFormatter = useMemo(() => resolveValueFormatter(bars?.valueFormatter, bars?.format), [bars?.valueFormatter, bars?.format])
3715
- const lineFormatter = useMemo(() => resolveValueFormatter(line?.valueFormatter, line?.format), [line?.valueFormatter, line?.format])
3716
- const barAxis = useMemo(() => resolveAxisFormatter(bars?.valueFormatter, bars?.format), [bars?.valueFormatter, bars?.format])
3717
- const lineAxis = useMemo(() => resolveAxisFormatter(line?.valueFormatter, line?.format), [line?.valueFormatter, line?.format])
3718
- const barTicks = useMemo(() => resolveAxisTickFormatter(bars?.valueFormatter, bars?.format), [bars?.valueFormatter, bars?.format])
3719
- const lineTicks = useMemo(() => resolveAxisTickFormatter(line?.valueFormatter, line?.format), [line?.valueFormatter, line?.format])
3996
+ const locale = useLocale()
3997
+ const t = TEXT[uiLanguage(locale)]
3998
+ const barFormatter = useMemo(() => resolveValueFormatter(bars?.valueFormatter, bars?.format, locale), [bars?.valueFormatter, bars?.format, locale])
3999
+ const lineFormatter = useMemo(() => resolveValueFormatter(line?.valueFormatter, line?.format, locale), [line?.valueFormatter, line?.format, locale])
4000
+ const barAxis = useMemo(() => resolveAxisFormatter(bars?.valueFormatter, bars?.format, locale), [bars?.valueFormatter, bars?.format, locale])
4001
+ const lineAxis = useMemo(() => resolveAxisFormatter(line?.valueFormatter, line?.format, locale), [line?.valueFormatter, line?.format, locale])
4002
+ const barTicks = useMemo(() => resolveAxisTickFormatter(bars?.valueFormatter, bars?.format, locale), [bars?.valueFormatter, bars?.format, locale])
4003
+ const lineTicks = useMemo(() => resolveAxisTickFormatter(line?.valueFormatter, line?.format, locale), [line?.valueFormatter, line?.format, locale])
3720
4004
  const palette = useMemo(() => {
3721
4005
  const p = pairedPalette(barKeys, lineKeys, bars?.colors as ChartColors | undefined, line?.colors as ChartColors | undefined)
3722
4006
  return { bars: resolveChartColors(p.bars, tokens), line: resolveChartColors(p.line, tokens) }
@@ -3753,25 +4037,26 @@ function PairedChartImpl<T extends Record<string, unknown>>(
3753
4037
  plotWidth,
3754
4038
  guideColor: tokens?.mutedForeground ?? undefined,
3755
4039
  chartOptions,
4040
+ locale,
3756
4041
  }),
3757
4042
  [
3758
4043
  data, index, labels, barKeys, lineKeys, palette, barFormatter, lineFormatter, barAxis, lineAxis, barTicks, lineTicks,
3759
4044
  bars?.type, bars?.yAxisLabel, bars?.maxValue, line?.fitToData, line?.yAxisLabel, line?.maxValue, line?.minValue,
3760
- height, heightRatio, yAxisWidth, showLegend, showTooltip, actions, plotWidth, tokens, chartOptions,
4045
+ height, heightRatio, yAxisWidth, showLegend, showTooltip, actions, plotWidth, tokens, chartOptions, locale,
3761
4046
  ],
3762
4047
  )
3763
4048
  // スクリーンリーダー向けの値の表: 棒の列 → 線の列(段ごとの書式)。
3764
4049
  const dataTable = useMemo(() => {
3765
- const upper = chartDataTable(labelRows(data, index, labels), index, barKeys, barFormatter, labels, "項目")
3766
- const lower = chartDataTable(labelRows(data, index, labels), index, lineKeys, lineFormatter, labels, "項目")
3767
- const times = timeTooltipLabels(data.map((row) => row[index]))
4050
+ const upper = chartDataTable(labelRows(data, index, labels), index, barKeys, barFormatter, labels, t.item)
4051
+ const lower = chartDataTable(labelRows(data, index, labels), index, lineKeys, lineFormatter, labels, t.item)
4052
+ const times = timeTooltipLabels(data.map((row) => row[index]), locale)
3768
4053
  return {
3769
4054
  ...upper,
3770
4055
  columns: [...upper.columns, ...lower.columns.slice(1)],
3771
4056
  rows: upper.rows.map((r, i) => [times?.[i] ?? r[0] ?? "", ...r.slice(1), ...(lower.rows[i] ?? []).slice(1)]),
3772
4057
  }
3773
- }, [data, index, labels, barKeys, lineKeys, barFormatter, lineFormatter])
3774
- const label = ariaLabel ?? `${[...barKeys, ...lineKeys].join("・")}の推移(上段 棒、下段 線)`
4058
+ }, [data, index, labels, barKeys, lineKeys, barFormatter, lineFormatter, t, locale])
4059
+ const label = ariaLabel ?? t.chart([...barKeys, ...lineKeys])
3775
4060
  return (
3776
4061
  <ChartFrame
3777
4062
  {...chartRootProps(rest)}