@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
@@ -31,15 +31,148 @@ import { cn } from "@squadbase/vantage/ui"
31
31
  import { ChartFrame } from "@squadbase/vantage/components"
32
32
  import { toneForValue } from "@squadbase/vantage/components"
33
33
  import type { ChartColor, ChartCommonProps, ChartSeriesColor, ChartThreshold } from "@squadbase/vantage/components"
34
- import { createContext, useCallback, useRef } from "react"
34
+ import { useSyncExternalStore } from "react"
35
+ import { createContext, useCallback, useRef, type RefObject } from "react"
35
36
  import type { ChartTone } from "@squadbase/vantage/components"
36
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
37
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
37
38
  import type { ChartColors, ValueFormatter } from "@squadbase/vantage/components"
38
39
  import { useMemo, type ReactNode } from "react"
39
40
  import type { EChartsOption } from "@squadbase/vantage/components"
40
41
  import { computeComparison, defaultChangeDisplay, formatChange, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
42
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
41
43
  import type { ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
42
44
 
45
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
46
+
47
+ /**
48
+ * The display language of the parts and the defaults that follow it.
49
+ *
50
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
51
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
52
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
53
+ *
54
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
55
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
56
+ */
57
+
58
+ /** The language of the built-in wording: Japanese or English. */
59
+ type UiLanguage = "ja" | "en"
60
+
61
+ /** The locale used when `<html lang>` gives none (and on the server). */
62
+ const FALLBACK_LOCALE = "en-US"
63
+
64
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
65
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
66
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
67
+ }
68
+
69
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
70
+ function normalizeLocale(tag: string | null | undefined): string {
71
+ const value = tag?.trim()
72
+ if (!value) return FALLBACK_LOCALE
73
+ try {
74
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
75
+ } catch {
76
+ return FALLBACK_LOCALE
77
+ }
78
+ }
79
+
80
+ /**
81
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
82
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
83
+ */
84
+ function documentLocale(): string {
85
+ if (typeof document === "undefined") return FALLBACK_LOCALE
86
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
87
+ }
88
+
89
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
90
+ function resolveLocale(locale?: string | null): string {
91
+ return locale || documentLocale()
92
+ }
93
+
94
+ /** Picks the wording table of the locale's language. */
95
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
96
+ return table[uiLanguage(resolveLocale(locale))]
97
+ }
98
+
99
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
100
+ function defaultCurrency(locale?: string | null): string {
101
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
102
+ }
103
+
104
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
105
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
106
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
107
+ }
108
+
109
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
110
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
111
+
112
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
113
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
114
+
115
+ /**
116
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
117
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
118
+ * Monday for the rest.
119
+ */
120
+ function weekStartsOn(locale?: string | null): WeekDay {
121
+ const tag = normalizeLocale(resolveLocale(locale))
122
+ if (uiLanguage(tag) === "ja") return 1
123
+ try {
124
+ const info = new Intl.Locale(tag) as Intl.Locale & {
125
+ getWeekInfo?: () => { firstDay: number }
126
+ weekInfo?: { firstDay: number }
127
+ }
128
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
129
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
130
+ } catch {
131
+ // Fall through to the table below.
132
+ }
133
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
134
+ }
135
+
136
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
137
+
138
+ const listeners = new Set<() => void>()
139
+ let observer: MutationObserver | null = null
140
+
141
+ /** One observer on `<html lang>` for every subscribed part. */
142
+ function subscribe(onChange: () => void): () => void {
143
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
144
+ listeners.add(onChange)
145
+ if (!observer) {
146
+ observer = new MutationObserver(() => {
147
+ for (const listener of [...listeners]) listener()
148
+ })
149
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
150
+ }
151
+ return () => {
152
+ listeners.delete(onChange)
153
+ if (listeners.size === 0 && observer) {
154
+ observer.disconnect()
155
+ observer = null
156
+ }
157
+ }
158
+ }
159
+
160
+ const serverLocale = () => FALLBACK_LOCALE
161
+
162
+ /**
163
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
164
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
165
+ */
166
+ function useDocumentLocale(): string {
167
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
168
+ }
169
+
170
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
171
+ function useLocale(locale?: string | null): string {
172
+ const page = useDocumentLocale()
173
+ return locale || page
174
+ }
175
+
43
176
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
44
177
 
45
178
  /**
@@ -74,10 +207,23 @@ interface ChartTokens {
74
207
  border: string | null
75
208
  /** Tooltip surface. */
76
209
  popover: string | null
210
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
211
+ colors?: Record<string, string>
212
+ /** Actual inherited painting surface (may change without a token changing). */
213
+ surface?: string | null
77
214
  /** Tooltip text. */
78
215
  popoverForeground: string | null
79
216
  }
80
217
 
218
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
219
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
220
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
221
+ const OPTION_COLOR_TOKENS = [
222
+ "--background", "--card", "--muted", "--primary", "--destructive",
223
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
224
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
225
+ ]
226
+
81
227
  let normalizeContext: CanvasRenderingContext2D | null | undefined
82
228
 
83
229
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -133,8 +279,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
133
279
  const color = read(token)
134
280
  if (color) series.push(color)
135
281
  }
136
- return {
282
+ const tokens: ChartTokens = {
137
283
  series,
284
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
285
+ surface: readChartSurface(target),
138
286
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
139
287
  foreground: read("--foreground"),
140
288
  mutedForeground: read("--muted-foreground"),
@@ -142,6 +290,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
142
290
  popover: read("--popover"),
143
291
  popoverForeground: read("--popover-foreground"),
144
292
  }
293
+ tokenScopes.set(tokens, target)
294
+ return tokens
145
295
  }
146
296
 
147
297
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -149,6 +299,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
149
299
  if (a === b) return true
150
300
  if (!a || !b) return false
151
301
  return (
302
+ tokenScopes.get(a) === tokenScopes.get(b) &&
303
+ a.surface === b.surface &&
304
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
305
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
152
306
  a.series.length === b.series.length &&
153
307
  a.series.every((color, i) => color === b.series[i]) &&
154
308
  a.fontFamily === b.fontFamily &&
@@ -174,7 +328,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
174
328
  * attributes alone leaves the chart painted in the previous palette until a
175
329
  * reload — the tokens in the DOM are new, the canvas is not.
176
330
  */
177
- function watchChartTokens(onChange: () => void): () => void {
331
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
178
332
  if (typeof document === "undefined") return () => {}
179
333
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
180
334
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -189,8 +343,11 @@ function watchChartTokens(onChange: () => void): () => void {
189
343
 
190
344
  const themeObserver = new MutationObserver(schedule)
191
345
  const attributeFilter = ["class", "style", "data-theme"]
192
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
193
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
346
+ // Only the chart and its ancestors can change its inherited theme. Never observe
347
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
348
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
349
+ themeObserver.observe(node, { attributes: true, attributeFilter })
350
+ }
194
351
 
195
352
  const styleObserver = new MutationObserver(schedule)
196
353
  if (document.head) {
@@ -278,22 +435,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
278
435
  * components that build an ECharts option from token colours themselves (the
279
436
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
280
437
  */
281
- function useChartTokens(): ChartTokens | null {
438
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
282
439
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
283
440
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
284
441
  // replayed by React on every rebase and returns a fresh object each time, so the state
285
442
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
286
443
  const tokensRef = useRef(tokens)
287
444
  useEffect(() => {
445
+ const target = scope && "current" in scope ? scope.current : scope
288
446
  const sync = () => {
289
- const next = readChartTokens()
447
+ const next = readChartTokens(target)
290
448
  if (sameChartTokens(tokensRef.current, next)) return
291
449
  tokensRef.current = next
292
450
  setTokens(next)
293
451
  }
294
452
  sync()
295
- return watchChartTokens(sync)
296
- }, [])
453
+ return watchChartTokens(sync, target)
454
+ }, [scope])
297
455
  return tokens
298
456
  }
299
457
 
@@ -305,7 +463,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
305
463
  /**
306
464
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
307
465
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
308
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
466
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
309
467
  * other CSS colour is normalized like the tokens are. A colour that can't be
310
468
  * resolved comes back as `""`, which ECharts treats as "use the palette".
311
469
  */
@@ -316,12 +474,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
316
474
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
317
475
  if (TONE_NAME_RE.test(color.trim())) {
318
476
  if (typeof document === "undefined") return ""
319
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
477
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
478
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
320
479
  }
321
480
  if (typeof document === "undefined") return color
322
481
  let value = color.trim()
323
482
  const cssVar = CSS_VAR_RE.exec(value)
324
- const styles = getComputedStyle(document.documentElement)
483
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
325
484
  if (cssVar) {
326
485
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
327
486
  } else if (value.includes("var(")) {
@@ -367,7 +526,7 @@ function readChartSurface(el: Element | null): string | null {
367
526
  if (color) return color
368
527
  }
369
528
  }
370
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
529
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
371
530
  }
372
531
 
373
532
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -381,9 +540,10 @@ const WIDTH_BUCKET = 40
381
540
  */
382
541
  function useChartSurface<E extends Element>(
383
542
  forwarded: ForwardedRef<E>,
384
- tokens: ChartTokens | null,
385
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
543
+ tokens?: ChartTokens | null,
544
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
386
545
  const [node, setNode] = useState<E | null>(null)
546
+ const scopedTokens = useChartTokens(node)
387
547
  const [surface, setSurface] = useState<string | undefined>(undefined)
388
548
  const [width, setWidth] = useState<number | undefined>(undefined)
389
549
  const ref = useCallback(
@@ -397,7 +557,7 @@ function useChartSurface<E extends Element>(
397
557
  useEffect(() => {
398
558
  if (!node) return
399
559
  setSurface(readChartSurface(node) ?? undefined)
400
- }, [node, tokens])
560
+ }, [node, tokens, scopedTokens])
401
561
  useEffect(() => {
402
562
  if (!node || typeof ResizeObserver === "undefined") return
403
563
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -406,7 +566,7 @@ function useChartSurface<E extends Element>(
406
566
  observer.observe(node)
407
567
  return () => observer.disconnect()
408
568
  }, [node])
409
- return { ref, surface, width }
569
+ return { ref, surface, width, tokens: scopedTokens }
410
570
  }
411
571
 
412
572
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -516,9 +676,10 @@ function resolveValueFormatter(
516
676
  function resolveAxisFormatter(
517
677
  valueFormatter: ValueFormatter | undefined,
518
678
  format: ValueFormat | undefined,
519
- locale: string = DEFAULT_LOCALE,
679
+ localeArg?: string,
520
680
  ): ValueFormatter {
521
681
  if (valueFormatter) return valueFormatter
682
+ const locale = resolveLocale(localeArg)
522
683
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
523
684
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
524
685
  const currency = "currency" in spec ? spec.currency : undefined
@@ -527,40 +688,75 @@ function resolveAxisFormatter(
527
688
  return (value) => formatValue(value, short, locale)
528
689
  }
529
690
 
530
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
531
- const JA_UNITS = [
532
- { char: "兆", size: 1e12 },
533
- { char: "億", size: 1e8 },
534
- { char: "万", size: 1e4 },
535
- ] as const
691
+ interface CompactUnit {
692
+ token: string
693
+ size: number
694
+ }
695
+
696
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
697
+
698
+ /**
699
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
700
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
701
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
702
+ */
703
+ function compactUnits(locale: string): CompactUnit[] | null {
704
+ const cached = compactUnitCache.get(locale)
705
+ if (cached !== undefined) return cached
706
+ let units: CompactUnit[] | null = []
707
+ try {
708
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
709
+ const seen = new Set<string>()
710
+ for (let exp = 3; exp <= 15; exp++) {
711
+ const parts = compact.formatToParts(10 ** exp)
712
+ const at = parts.findIndex((p) => p.type === "compact")
713
+ if (at === -1) continue
714
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
715
+ units = null
716
+ break
717
+ }
718
+ const token = parts[at]!.value
719
+ if (seen.has(token)) continue
720
+ seen.add(token)
721
+ units.push({ token, size: 10 ** exp })
722
+ }
723
+ } catch {
724
+ units = null
725
+ }
726
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
727
+ compactUnitCache.set(locale, sorted)
728
+ return sorted
729
+ }
536
730
 
537
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
731
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
538
732
 
539
733
  /**
540
734
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
541
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
542
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
543
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
735
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
736
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
737
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
738
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
544
739
  */
545
740
  function resolveAxisTickFormatter(
546
741
  valueFormatter: ValueFormatter | undefined,
547
742
  format: ValueFormat | undefined,
548
- locale: string = DEFAULT_LOCALE,
743
+ localeArg?: string,
549
744
  ): (top: number) => ValueFormatter {
745
+ const locale = resolveLocale(localeArg)
550
746
  const base = resolveAxisFormatter(valueFormatter, format, locale)
551
747
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
552
- const aligns =
553
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
554
- if (!aligns) return () => base
748
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
749
+ if (!units) return () => base
555
750
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
751
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
556
752
  return (top) => {
557
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
753
+ const unit = units.find((u) => Math.abs(top) >= u.size)
558
754
  if (!unit) return base
559
755
  return (value) => {
560
756
  const text = base(value)
561
757
  if (value === 0 || !Number.isFinite(value)) return text
562
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
563
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
758
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
759
+ return text.replace(numberToken, () => scaled)
564
760
  }
565
761
  }
566
762
  }
@@ -589,8 +785,20 @@ function clamp(value: number, min: number, max: number): number {
589
785
  /** How many categories a chart colours individually; the rest fold into「その他」. */
590
786
  const MAX_CHART_SERIES = 5
591
787
 
592
- /** The series name the folded categories get. */
593
- const OTHER_SERIES_LABEL = "その他"
788
+ /** The series name the folded categories get, by language. */
789
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
790
+
791
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
792
+ function otherSeriesLabel(locale?: string): string {
793
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
794
+ }
795
+
796
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
797
+
798
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
799
+ function isOtherName(name: string | null | undefined): boolean {
800
+ return typeof name === "string" && OTHER_NAMES.has(name)
801
+ }
594
802
 
595
803
  interface AggregatedItems<Item extends { name: string; value: number }> {
596
804
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -610,7 +818,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
610
818
  function aggregateTopN<Item extends { name: string; value: number }>(
611
819
  items: readonly Item[],
612
820
  n: number = MAX_CHART_SERIES,
613
- otherLabel: string = OTHER_SERIES_LABEL,
821
+ otherLabel: string = otherSeriesLabel(),
614
822
  ): AggregatedItems<Item> {
615
823
  if (items.length <= n) return { top: [...items], rest: [], other: null }
616
824
  const ranked = items
@@ -639,7 +847,7 @@ function aggregateSeries(
639
847
  data: readonly Record<string, unknown>[],
640
848
  categories: readonly string[],
641
849
  n: number = MAX_CHART_SERIES,
642
- otherLabel: string = OTHER_SERIES_LABEL,
850
+ otherLabel: string = otherSeriesLabel(),
643
851
  ): AggregatedSeries {
644
852
  const totals = categories.map((name) => ({
645
853
  name,
@@ -671,12 +879,14 @@ function aggregateSeriesKeeping(
671
879
  categories: readonly string[],
672
880
  keep: string | undefined,
673
881
  n: number = MAX_CHART_SERIES,
882
+ otherLabel: string = otherSeriesLabel(),
674
883
  ): AggregatedSeries {
675
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
884
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
676
885
  const aggregated = aggregateSeries(
677
886
  data,
678
887
  categories.filter((c) => c !== keep),
679
888
  n,
889
+ otherLabel,
680
890
  )
681
891
  return { ...aggregated, categories: [...aggregated.categories, keep] }
682
892
  }
@@ -712,7 +922,7 @@ function seriesColorsWithComparison(
712
922
  * Colour per drawn series: the record's token for that category, else the
713
923
  * array's token at the category's position in `allCategories` (cycled), else
714
924
  * the token palette by drawn position. `otherCategory` — and a category the
715
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
925
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
716
926
  */
717
927
  function seriesColorsFor(
718
928
  categories: readonly string[],
@@ -724,7 +934,7 @@ function seriesColorsFor(
724
934
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
725
935
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
726
936
  return categories.map((category, i) => {
727
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
937
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
728
938
  const fromRecord = record?.[category]
729
939
  if (fromRecord) return fromRecord
730
940
  if (list && list.length > 0) {
@@ -771,7 +981,7 @@ interface DonutSlicesConfig {
771
981
  */
772
982
  function donutSlices(
773
983
  data: readonly Record<string, unknown>[],
774
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
984
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
775
985
  ): DonutSlice[] {
776
986
  const items = data
777
987
  .map((row, i) => {
@@ -784,7 +994,8 @@ function donutSlices(
784
994
  const slices: DonutSlice[] = top.map((item, rank) => ({
785
995
  name: item.name,
786
996
  value: item.value,
787
- color: palette[rank % palette.length]!,
997
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
998
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
788
999
  isOther: false,
789
1000
  row: item.row,
790
1001
  }))
@@ -857,7 +1068,7 @@ interface BarListRowsConfig extends BarListOptions {
857
1068
  */
858
1069
  function barListRows<Row extends object>(
859
1070
  data: readonly Row[],
860
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1071
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
861
1072
  ): BarListRow<Row>[] {
862
1073
  const items = data.map((row): BarListRow<Row> => {
863
1074
  const cells = row as Record<string, unknown>
@@ -903,19 +1114,21 @@ const BAR_LIST_OTHER_TINT = 30
903
1114
 
904
1115
  /**
905
1116
  * The fill of each BarList row: all real rows in the same tint of `color`
906
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1117
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
907
1118
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
908
1119
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
909
1120
  * for that row. A tint, not the solid token, so the label printed over the bar
910
1121
  * keeps its contrast.
911
1122
  */
912
1123
  function barListFills(
913
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1124
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
914
1125
  color: ChartColor,
915
1126
  otherColor: string,
916
1127
  ): string[] {
917
1128
  return rows.map((row) =>
918
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1129
+ row.isOther || isOtherName(row.name)
1130
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1131
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
919
1132
  )
920
1133
  }
921
1134
 
@@ -1131,11 +1344,30 @@ interface ChartSummaryInput {
1131
1344
  format?: ValueFormat
1132
1345
  /** Wins over `format`. */
1133
1346
  valueFormatter?: ValueFormatter
1347
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1348
+ locale?: string
1349
+ }
1350
+
1351
+ const SUMMARY_TEXT = {
1352
+ ja: {
1353
+ up: "、前の点より増加",
1354
+ down: "、前の点より減少",
1355
+ flat: "、前の点と横ばい",
1356
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1357
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1358
+ },
1359
+ en: {
1360
+ up: ", up from the previous point",
1361
+ down: ", down from the previous point",
1362
+ flat: ", flat from the previous point",
1363
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1364
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1365
+ },
1134
1366
  }
1135
1367
 
1136
1368
  /**
1137
1369
  * One-sentence summary built from the rows, for `summary="auto"`:
1138
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1370
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1139
1371
  * computed from the last two non-empty points of the first series — never
1140
1372
  * written by hand, so it cannot contradict the data. `undefined` when there is
1141
1373
  * no usable point.
@@ -1151,30 +1383,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1151
1383
  }
1152
1384
  const latest = points[points.length - 1]
1153
1385
  if (!latest) return undefined
1154
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1386
+ const locale = resolveLocale(input.locale)
1387
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1388
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1155
1389
  const prev = points[points.length - 2]
1156
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1390
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1157
1391
  const head = input.title ? `${input.title}: ` : ""
1158
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1392
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1159
1393
  }
1160
1394
 
1161
1395
  /**
1162
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1163
- * (largest first). `undefined` when there is nothing to describe.
1396
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1397
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1164
1398
  */
1165
1399
  function buildShareSummary(
1166
1400
  slices: readonly { name: string; value: number }[],
1167
1401
  title?: string,
1402
+ locale?: string,
1168
1403
  ): string | undefined {
1169
1404
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1170
1405
  const top = slices[0]
1171
1406
  if (!top || total <= 0) return undefined
1172
1407
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1173
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1408
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1174
1409
  }
1175
1410
 
1176
1411
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1177
1412
 
1413
+ /** The built-in words of the option builders, by language. */
1414
+ const CHART_TEXT = {
1415
+ ja: {
1416
+ target: "目標",
1417
+ forecast: "予測",
1418
+ total: "合計",
1419
+ provisional: "速報",
1420
+ noComparison: "比較なし",
1421
+ share: (share: string) => `(${share})`,
1422
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1423
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1424
+ },
1425
+ en: {
1426
+ target: "Target",
1427
+ forecast: "Forecast",
1428
+ total: "Total",
1429
+ provisional: "Preliminary",
1430
+ noComparison: "No comparison",
1431
+ share: (share: string) => ` (${share})`,
1432
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1433
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1434
+ },
1435
+ } satisfies Record<UiLanguage, unknown>
1436
+
1437
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1438
+
1178
1439
  type CartesianChartKind = "area" | "bar" | "line"
1179
1440
  type CartesianStackType = "default" | "stacked" | "percent"
1180
1441
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1359,7 +1620,7 @@ interface CartesianChartConfig {
1359
1620
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1360
1621
  */
1361
1622
  axisTicks?: (top: number) => (value: number) => string
1362
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1623
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1363
1624
  locale?: string
1364
1625
  type?: CartesianStackType
1365
1626
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1597,11 +1858,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1597
1858
  */
1598
1859
  function timeAxisLabels(
1599
1860
  values: readonly unknown[],
1600
- locale: string = DEFAULT_LOCALE,
1861
+ localeArg?: string,
1601
1862
  shown: (i: number) => boolean = () => true,
1602
1863
  ): string[] | null {
1603
1864
  const parsed = parseTimeIndex(values)
1604
1865
  if (!parsed) return null
1866
+ const locale = resolveLocale(localeArg)
1605
1867
  const { granularity, times } = parsed
1606
1868
  let prevYear: number | null = null
1607
1869
  return times.map((t, i) => {
@@ -1624,9 +1886,10 @@ function timeAxisLabels(
1624
1886
  }
1625
1887
 
1626
1888
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1627
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1889
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1628
1890
  const parsed = parseTimeIndex(values)
1629
1891
  if (!parsed) return null
1892
+ const locale = resolveLocale(localeArg)
1630
1893
  const options: Intl.DateTimeFormatOptions =
1631
1894
  parsed.granularity === "year"
1632
1895
  ? { year: "numeric" }
@@ -1766,7 +2029,9 @@ function tooltipHtml(
1766
2029
  rows: readonly TooltipRow[],
1767
2030
  valueFormatter: (value: number) => string,
1768
2031
  notes: readonly string[] = [],
2032
+ locale?: string,
1769
2033
  ): string {
2034
+ const t = chartText(locale)
1770
2035
  const lines = rows.map(
1771
2036
  (row) =>
1772
2037
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1775,7 +2040,7 @@ function tooltipHtml(
1775
2040
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1776
2041
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1777
2042
  }${
1778
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2043
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1779
2044
  }</span></div>` +
1780
2045
  (row.note === undefined
1781
2046
  ? ""
@@ -1789,8 +2054,10 @@ function tooltipHtml(
1789
2054
 
1790
2055
  /* ------------------------------------------------- per-bar targets / notes */
1791
2056
 
1792
- /** Default name of the per-bar target marker (legend, tooltip). */
1793
- const BAR_TARGET_LABEL = "目標"
2057
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2058
+ function barTargetLabel(locale?: string): string {
2059
+ return chartText(locale).target
2060
+ }
1794
2061
 
1795
2062
  /** How far (px) the target marker reaches past each side of its bar. */
1796
2063
  const TARGET_MARKER_OVERHANG = 4
@@ -1841,16 +2108,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1841
2108
  return (value / target) * 100
1842
2109
  }
1843
2110
 
1844
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2111
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1845
2112
  function barTargetNote(
1846
2113
  label: string,
1847
2114
  value: number | null,
1848
2115
  target: number | null,
1849
2116
  valueFormatter: (value: number) => string,
2117
+ locale?: string,
1850
2118
  ): string | null {
1851
2119
  if (target === null) return null
1852
2120
  const rate = achievementRate(value, target)
1853
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2121
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1854
2122
  }
1855
2123
 
1856
2124
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1942,11 +2210,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1942
2210
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1943
2211
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1944
2212
 
1945
- const DEFAULT_FORECAST_LABEL = "予測"
1946
-
1947
- /** 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. */
1948
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1949
- return `${label} →`
2213
+ /**
2214
+ * 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.
2215
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2216
+ */
2217
+ function forecastBoundaryText(label?: string, locale?: string): string {
2218
+ return `${label ?? chartText(locale).forecast} →`
1950
2219
  }
1951
2220
 
1952
2221
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1986,8 +2255,10 @@ interface AxisExtent {
1986
2255
  max: number
1987
2256
  }
1988
2257
 
1989
- /** The label of a stack's total (highlight and tooltip). */
1990
- const STACK_TOTAL_LABEL = "合計"
2258
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2259
+ function stackTotalLabel(locale?: string): string {
2260
+ return chartText(locale).total
2261
+ }
1991
2262
 
1992
2263
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
1993
2264
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2109,8 +2380,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2109
2380
 
2110
2381
  /**
2111
2382
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2112
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2113
- * either value is missing or the baseline is 0.
2383
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2384
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2114
2385
  */
2115
2386
  function comparisonNote(
2116
2387
  current: number | null,
@@ -2118,16 +2389,18 @@ function comparisonNote(
2118
2389
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2119
2390
  grain?: PeriodGrain,
2120
2391
  format?: ValueFormat,
2121
- locale: string = DEFAULT_LOCALE,
2392
+ localeArg?: string,
2122
2393
  ): string {
2394
+ const locale = resolveLocale(localeArg)
2123
2395
  const result = computeComparison(
2124
2396
  current,
2125
2397
  comparison.vs === "custom"
2126
2398
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2127
2399
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2400
+ { locale },
2128
2401
  )
2129
2402
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2130
- if (text === null) return `比較なし ${result.label}`
2403
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2131
2404
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2132
2405
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2133
2406
  }
@@ -2170,7 +2443,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2170
2443
  toneColors,
2171
2444
  valueFormatter,
2172
2445
  axisFormatter = valueFormatter,
2173
- locale = DEFAULT_LOCALE,
2174
2446
  type = "default",
2175
2447
  layout = "horizontal",
2176
2448
  curveType = "linear",
@@ -2207,7 +2479,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2207
2479
  chartOptions,
2208
2480
  reserveActions = false,
2209
2481
  } = cfg
2210
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2482
+ const locale = resolveLocale(cfg.locale)
2483
+ const text = chartText(locale)
2484
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2211
2485
 
2212
2486
  const stacked = kind !== "line" && type !== "default"
2213
2487
  const percent = kind !== "line" && type === "percent"
@@ -2356,9 +2630,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2356
2630
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2357
2631
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2358
2632
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2359
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2633
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2360
2634
  const highlightText = (value: number) =>
2361
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2635
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2362
2636
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2363
2637
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2364
2638
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2384,7 +2658,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2384
2658
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2385
2659
  const barTargets: (number | null)[] | null =
2386
2660
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2387
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2661
+ const barTargetName = targetLabel ?? text.target
2388
2662
  const barTargetColor = labelColor ?? guideColor
2389
2663
 
2390
2664
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2598,7 +2872,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2598
2872
  }
2599
2873
 
2600
2874
  /* -- forecast: dashed continuation (+ optional band) of one series */
2601
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2875
+ const forecastLabel = forecast?.label ?? text.forecast
2602
2876
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2603
2877
  const forecastValues: (number | null)[] = []
2604
2878
  if (forecast) {
@@ -3074,7 +3348,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3074
3348
  })
3075
3349
  }
3076
3350
  if (stackTotals && i <= lastActual && rows.length > 1) {
3077
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3351
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3078
3352
  }
3079
3353
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3080
3354
  if (provisional) {
@@ -3090,19 +3364,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3090
3364
  ...noteFor(at),
3091
3365
  })
3092
3366
  })
3093
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3367
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3094
3368
  }
3095
3369
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3096
3370
  const lines: string[] = []
3097
3371
  if (barTargets && i <= lastActual) {
3098
3372
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3099
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3373
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3100
3374
  if (note) lines.push(note)
3101
3375
  }
3102
3376
  for (const note of notes) {
3103
3377
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3104
3378
  }
3105
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3379
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3106
3380
  },
3107
3381
  },
3108
3382
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3194,8 +3468,6 @@ interface DonutOptionSlice {
3194
3468
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3195
3469
  const DONUT_OUTSIDE_LABELS = 2
3196
3470
 
3197
- const DEFAULT_TOTAL_LABEL = "合計"
3198
-
3199
3471
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3200
3472
 
3201
3473
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3228,15 +3500,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3228
3500
  }
3229
3501
 
3230
3502
  /**
3231
- * The default center label: what is totalled (「合計」), the formatted total
3503
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3232
3504
  * (`amount`, the big line), the unit (a small line under it) and both as one
3233
3505
  * string (`value`, 「9.2万 セッション」).
3234
3506
  */
3235
3507
  function donutCenterText(
3236
3508
  total: number,
3237
3509
  valueFormatter: (value: number) => string,
3238
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3510
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3239
3511
  ): { label: string; value: string; amount: string; unit?: string } {
3512
+ totalLabel ??= chartText(locale).total
3240
3513
  const amount = valueFormatter(total)
3241
3514
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3242
3515
  }
@@ -3464,8 +3737,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3464
3737
 
3465
3738
  /* ------------------------------------------------------------ provisional */
3466
3739
 
3467
- /** The default label for points that are not final yet. */
3468
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3740
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3741
+ function defaultProvisionalLabel(locale?: string): string {
3742
+ return chartText(locale).provisional
3743
+ }
3469
3744
 
3470
3745
  /**
3471
3746
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3545,6 +3820,11 @@ export interface BarListProps<T extends object = BarListItem>
3545
3820
  ariaLabel?: string
3546
3821
  }
3547
3822
 
3823
+ const TEXT = {
3824
+ ja: { item: "項目", value: "値" },
3825
+ en: { item: "Item", value: "Value" },
3826
+ }
3827
+
3548
3828
  /** Row height (h-8) + gap (space-y-1.5): the skeleton / empty height for `n` rows. */
3549
3829
  const listHeight = (n: number) => n * 32 + Math.max(0, n - 1) * 6
3550
3830
 
@@ -3587,17 +3867,19 @@ function BarListImpl<T extends object>(
3587
3867
  return () => cancelAnimationFrame(frame)
3588
3868
  }, [showAnimation])
3589
3869
 
3870
+ const locale = useLocale()
3871
+ const t = TEXT[uiLanguage(locale)]
3590
3872
  const maxItems = barListOptions?.maxItems ?? MAX_CHART_SERIES
3591
3873
  const token = Array.isArray(colors) ? colors[0] : colors
3592
3874
  const barColor = token ?? color ?? "chart-1"
3593
3875
 
3594
- const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format)
3876
+ const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format, locale)
3595
3877
  const baseRows = barListRows(data, {
3596
3878
  index,
3597
3879
  category: categories?.[0],
3598
3880
  sortOrder: barListOptions?.sortOrder ?? sortOrder ?? "descending",
3599
3881
  maxItems,
3600
- otherLabel: barListOptions?.otherLabel,
3882
+ otherLabel: barListOptions?.otherLabel ?? otherSeriesLabel(locale),
3601
3883
  exclude: barListOptions?.exclude,
3602
3884
  })
3603
3885
  const rows = baseRows.map((row) => {
@@ -3616,7 +3898,7 @@ function BarListImpl<T extends object>(
3616
3898
  "name",
3617
3899
  ["value"],
3618
3900
  formatter,
3619
- { name: (index && labels?.[index]) || "項目", value: (categories?.[0] && labels?.[categories[0]]) || "値" },
3901
+ { name: (index && labels?.[index]) || t.item, value: (categories?.[0] && labels?.[categories[0]]) || t.value },
3620
3902
  )
3621
3903
 
3622
3904
  const list = (