@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
@@ -13,11 +13,143 @@ import { cn } from "@squadbase/vantage/ui"
13
13
  import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
14
14
  import type { ValueFormat } from "@squadbase/vantage/components"
15
15
  import type { ChartColor, ValueFormatter } from "@squadbase/vantage/components"
16
- import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef } from "react"
16
+ import { useSyncExternalStore } from "react"
17
+ import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
17
18
  import type { ChartTone } from "@squadbase/vantage/components"
18
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
19
+ import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
19
20
  import type { ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
20
21
 
22
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
23
+
24
+ /**
25
+ * The display language of the parts and the defaults that follow it.
26
+ *
27
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
28
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
29
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
30
+ *
31
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
32
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
33
+ */
34
+
35
+ /** The language of the built-in wording: Japanese or English. */
36
+ type UiLanguage = "ja" | "en"
37
+
38
+ /** The locale used when `<html lang>` gives none (and on the server). */
39
+ const FALLBACK_LOCALE = "en-US"
40
+
41
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
42
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
43
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
44
+ }
45
+
46
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
47
+ function normalizeLocale(tag: string | null | undefined): string {
48
+ const value = tag?.trim()
49
+ if (!value) return FALLBACK_LOCALE
50
+ try {
51
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
52
+ } catch {
53
+ return FALLBACK_LOCALE
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
59
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
60
+ */
61
+ function documentLocale(): string {
62
+ if (typeof document === "undefined") return FALLBACK_LOCALE
63
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
64
+ }
65
+
66
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
67
+ function resolveLocale(locale?: string | null): string {
68
+ return locale || documentLocale()
69
+ }
70
+
71
+ /** Picks the wording table of the locale's language. */
72
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
73
+ return table[uiLanguage(resolveLocale(locale))]
74
+ }
75
+
76
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
77
+ function defaultCurrency(locale?: string | null): string {
78
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
79
+ }
80
+
81
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
82
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
83
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
84
+ }
85
+
86
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
87
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
88
+
89
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
90
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
91
+
92
+ /**
93
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
94
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
95
+ * Monday for the rest.
96
+ */
97
+ function weekStartsOn(locale?: string | null): WeekDay {
98
+ const tag = normalizeLocale(resolveLocale(locale))
99
+ if (uiLanguage(tag) === "ja") return 1
100
+ try {
101
+ const info = new Intl.Locale(tag) as Intl.Locale & {
102
+ getWeekInfo?: () => { firstDay: number }
103
+ weekInfo?: { firstDay: number }
104
+ }
105
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
106
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
107
+ } catch {
108
+ // Fall through to the table below.
109
+ }
110
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
111
+ }
112
+
113
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
114
+
115
+ const listeners = new Set<() => void>()
116
+ let observer: MutationObserver | null = null
117
+
118
+ /** One observer on `<html lang>` for every subscribed part. */
119
+ function subscribe(onChange: () => void): () => void {
120
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
121
+ listeners.add(onChange)
122
+ if (!observer) {
123
+ observer = new MutationObserver(() => {
124
+ for (const listener of [...listeners]) listener()
125
+ })
126
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
127
+ }
128
+ return () => {
129
+ listeners.delete(onChange)
130
+ if (listeners.size === 0 && observer) {
131
+ observer.disconnect()
132
+ observer = null
133
+ }
134
+ }
135
+ }
136
+
137
+ const serverLocale = () => FALLBACK_LOCALE
138
+
139
+ /**
140
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
141
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
142
+ */
143
+ function useDocumentLocale(): string {
144
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
145
+ }
146
+
147
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
148
+ function useLocale(locale?: string | null): string {
149
+ const page = useDocumentLocale()
150
+ return locale || page
151
+ }
152
+
21
153
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
22
154
 
23
155
  /**
@@ -52,10 +184,23 @@ interface ChartTokens {
52
184
  border: string | null
53
185
  /** Tooltip surface. */
54
186
  popover: string | null
187
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
188
+ colors?: Record<string, string>
189
+ /** Actual inherited painting surface (may change without a token changing). */
190
+ surface?: string | null
55
191
  /** Tooltip text. */
56
192
  popoverForeground: string | null
57
193
  }
58
194
 
195
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
196
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
197
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
198
+ const OPTION_COLOR_TOKENS = [
199
+ "--background", "--card", "--muted", "--primary", "--destructive",
200
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
201
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
202
+ ]
203
+
59
204
  let normalizeContext: CanvasRenderingContext2D | null | undefined
60
205
 
61
206
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -111,8 +256,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
111
256
  const color = read(token)
112
257
  if (color) series.push(color)
113
258
  }
114
- return {
259
+ const tokens: ChartTokens = {
115
260
  series,
261
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
262
+ surface: readChartSurface(target),
116
263
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
117
264
  foreground: read("--foreground"),
118
265
  mutedForeground: read("--muted-foreground"),
@@ -120,6 +267,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
120
267
  popover: read("--popover"),
121
268
  popoverForeground: read("--popover-foreground"),
122
269
  }
270
+ tokenScopes.set(tokens, target)
271
+ return tokens
123
272
  }
124
273
 
125
274
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -127,6 +276,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
127
276
  if (a === b) return true
128
277
  if (!a || !b) return false
129
278
  return (
279
+ tokenScopes.get(a) === tokenScopes.get(b) &&
280
+ a.surface === b.surface &&
281
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
282
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
130
283
  a.series.length === b.series.length &&
131
284
  a.series.every((color, i) => color === b.series[i]) &&
132
285
  a.fontFamily === b.fontFamily &&
@@ -152,7 +305,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
152
305
  * attributes alone leaves the chart painted in the previous palette until a
153
306
  * reload — the tokens in the DOM are new, the canvas is not.
154
307
  */
155
- function watchChartTokens(onChange: () => void): () => void {
308
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
156
309
  if (typeof document === "undefined") return () => {}
157
310
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
158
311
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -167,8 +320,11 @@ function watchChartTokens(onChange: () => void): () => void {
167
320
 
168
321
  const themeObserver = new MutationObserver(schedule)
169
322
  const attributeFilter = ["class", "style", "data-theme"]
170
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
171
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
323
+ // Only the chart and its ancestors can change its inherited theme. Never observe
324
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
325
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
326
+ themeObserver.observe(node, { attributes: true, attributeFilter })
327
+ }
172
328
 
173
329
  const styleObserver = new MutationObserver(schedule)
174
330
  if (document.head) {
@@ -256,22 +412,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
256
412
  * components that build an ECharts option from token colours themselves (the
257
413
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
258
414
  */
259
- function useChartTokens(): ChartTokens | null {
415
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
260
416
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
261
417
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
262
418
  // replayed by React on every rebase and returns a fresh object each time, so the state
263
419
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
264
420
  const tokensRef = useRef(tokens)
265
421
  useEffect(() => {
422
+ const target = scope && "current" in scope ? scope.current : scope
266
423
  const sync = () => {
267
- const next = readChartTokens()
424
+ const next = readChartTokens(target)
268
425
  if (sameChartTokens(tokensRef.current, next)) return
269
426
  tokensRef.current = next
270
427
  setTokens(next)
271
428
  }
272
429
  sync()
273
- return watchChartTokens(sync)
274
- }, [])
430
+ return watchChartTokens(sync, target)
431
+ }, [scope])
275
432
  return tokens
276
433
  }
277
434
 
@@ -283,7 +440,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
283
440
  /**
284
441
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
285
442
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
286
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
443
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
287
444
  * other CSS colour is normalized like the tokens are. A colour that can't be
288
445
  * resolved comes back as `""`, which ECharts treats as "use the palette".
289
446
  */
@@ -294,12 +451,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
294
451
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
295
452
  if (TONE_NAME_RE.test(color.trim())) {
296
453
  if (typeof document === "undefined") return ""
297
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
454
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
455
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
298
456
  }
299
457
  if (typeof document === "undefined") return color
300
458
  let value = color.trim()
301
459
  const cssVar = CSS_VAR_RE.exec(value)
302
- const styles = getComputedStyle(document.documentElement)
460
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
303
461
  if (cssVar) {
304
462
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
305
463
  } else if (value.includes("var(")) {
@@ -345,7 +503,7 @@ function readChartSurface(el: Element | null): string | null {
345
503
  if (color) return color
346
504
  }
347
505
  }
348
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
506
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
349
507
  }
350
508
 
351
509
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -359,9 +517,10 @@ const WIDTH_BUCKET = 40
359
517
  */
360
518
  function useChartSurface<E extends Element>(
361
519
  forwarded: ForwardedRef<E>,
362
- tokens: ChartTokens | null,
363
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
520
+ tokens?: ChartTokens | null,
521
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
364
522
  const [node, setNode] = useState<E | null>(null)
523
+ const scopedTokens = useChartTokens(node)
365
524
  const [surface, setSurface] = useState<string | undefined>(undefined)
366
525
  const [width, setWidth] = useState<number | undefined>(undefined)
367
526
  const ref = useCallback(
@@ -375,7 +534,7 @@ function useChartSurface<E extends Element>(
375
534
  useEffect(() => {
376
535
  if (!node) return
377
536
  setSurface(readChartSurface(node) ?? undefined)
378
- }, [node, tokens])
537
+ }, [node, tokens, scopedTokens])
379
538
  useEffect(() => {
380
539
  if (!node || typeof ResizeObserver === "undefined") return
381
540
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -384,7 +543,7 @@ function useChartSurface<E extends Element>(
384
543
  observer.observe(node)
385
544
  return () => observer.disconnect()
386
545
  }, [node])
387
- return { ref, surface, width }
546
+ return { ref, surface, width, tokens: scopedTokens }
388
547
  }
389
548
 
390
549
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -494,9 +653,10 @@ function resolveValueFormatter(
494
653
  function resolveAxisFormatter(
495
654
  valueFormatter: ValueFormatter | undefined,
496
655
  format: ValueFormat | undefined,
497
- locale: string = DEFAULT_LOCALE,
656
+ localeArg?: string,
498
657
  ): ValueFormatter {
499
658
  if (valueFormatter) return valueFormatter
659
+ const locale = resolveLocale(localeArg)
500
660
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
501
661
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
502
662
  const currency = "currency" in spec ? spec.currency : undefined
@@ -505,40 +665,75 @@ function resolveAxisFormatter(
505
665
  return (value) => formatValue(value, short, locale)
506
666
  }
507
667
 
508
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
509
- const JA_UNITS = [
510
- { char: "兆", size: 1e12 },
511
- { char: "億", size: 1e8 },
512
- { char: "万", size: 1e4 },
513
- ] as const
668
+ interface CompactUnit {
669
+ token: string
670
+ size: number
671
+ }
672
+
673
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
674
+
675
+ /**
676
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
677
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
678
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
679
+ */
680
+ function compactUnits(locale: string): CompactUnit[] | null {
681
+ const cached = compactUnitCache.get(locale)
682
+ if (cached !== undefined) return cached
683
+ let units: CompactUnit[] | null = []
684
+ try {
685
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
686
+ const seen = new Set<string>()
687
+ for (let exp = 3; exp <= 15; exp++) {
688
+ const parts = compact.formatToParts(10 ** exp)
689
+ const at = parts.findIndex((p) => p.type === "compact")
690
+ if (at === -1) continue
691
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
692
+ units = null
693
+ break
694
+ }
695
+ const token = parts[at]!.value
696
+ if (seen.has(token)) continue
697
+ seen.add(token)
698
+ units.push({ token, size: 10 ** exp })
699
+ }
700
+ } catch {
701
+ units = null
702
+ }
703
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
704
+ compactUnitCache.set(locale, sorted)
705
+ return sorted
706
+ }
514
707
 
515
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
708
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
516
709
 
517
710
  /**
518
711
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
519
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
520
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
521
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
712
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
713
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
714
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
715
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
522
716
  */
523
717
  function resolveAxisTickFormatter(
524
718
  valueFormatter: ValueFormatter | undefined,
525
719
  format: ValueFormat | undefined,
526
- locale: string = DEFAULT_LOCALE,
720
+ localeArg?: string,
527
721
  ): (top: number) => ValueFormatter {
722
+ const locale = resolveLocale(localeArg)
528
723
  const base = resolveAxisFormatter(valueFormatter, format, locale)
529
724
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
530
- const aligns =
531
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
532
- if (!aligns) return () => base
725
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
726
+ if (!units) return () => base
533
727
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
728
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
534
729
  return (top) => {
535
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
730
+ const unit = units.find((u) => Math.abs(top) >= u.size)
536
731
  if (!unit) return base
537
732
  return (value) => {
538
733
  const text = base(value)
539
734
  if (value === 0 || !Number.isFinite(value)) return text
540
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
541
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
735
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
736
+ return text.replace(numberToken, () => scaled)
542
737
  }
543
738
  }
544
739
  }
@@ -567,8 +762,20 @@ function clamp(value: number, min: number, max: number): number {
567
762
  /** How many categories a chart colours individually; the rest fold into「その他」. */
568
763
  const MAX_CHART_SERIES = 5
569
764
 
570
- /** The series name the folded categories get. */
571
- const OTHER_SERIES_LABEL = "その他"
765
+ /** The series name the folded categories get, by language. */
766
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
767
+
768
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
769
+ function otherSeriesLabel(locale?: string): string {
770
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
771
+ }
772
+
773
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
774
+
775
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
776
+ function isOtherName(name: string | null | undefined): boolean {
777
+ return typeof name === "string" && OTHER_NAMES.has(name)
778
+ }
572
779
 
573
780
  interface AggregatedItems<Item extends { name: string; value: number }> {
574
781
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -588,7 +795,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
588
795
  function aggregateTopN<Item extends { name: string; value: number }>(
589
796
  items: readonly Item[],
590
797
  n: number = MAX_CHART_SERIES,
591
- otherLabel: string = OTHER_SERIES_LABEL,
798
+ otherLabel: string = otherSeriesLabel(),
592
799
  ): AggregatedItems<Item> {
593
800
  if (items.length <= n) return { top: [...items], rest: [], other: null }
594
801
  const ranked = items
@@ -617,7 +824,7 @@ function aggregateSeries(
617
824
  data: readonly Record<string, unknown>[],
618
825
  categories: readonly string[],
619
826
  n: number = MAX_CHART_SERIES,
620
- otherLabel: string = OTHER_SERIES_LABEL,
827
+ otherLabel: string = otherSeriesLabel(),
621
828
  ): AggregatedSeries {
622
829
  const totals = categories.map((name) => ({
623
830
  name,
@@ -649,12 +856,14 @@ function aggregateSeriesKeeping(
649
856
  categories: readonly string[],
650
857
  keep: string | undefined,
651
858
  n: number = MAX_CHART_SERIES,
859
+ otherLabel: string = otherSeriesLabel(),
652
860
  ): AggregatedSeries {
653
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
861
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
654
862
  const aggregated = aggregateSeries(
655
863
  data,
656
864
  categories.filter((c) => c !== keep),
657
865
  n,
866
+ otherLabel,
658
867
  )
659
868
  return { ...aggregated, categories: [...aggregated.categories, keep] }
660
869
  }
@@ -690,7 +899,7 @@ function seriesColorsWithComparison(
690
899
  * Colour per drawn series: the record's token for that category, else the
691
900
  * array's token at the category's position in `allCategories` (cycled), else
692
901
  * the token palette by drawn position. `otherCategory` — and a category the
693
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
902
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
694
903
  */
695
904
  function seriesColorsFor(
696
905
  categories: readonly string[],
@@ -702,7 +911,7 @@ function seriesColorsFor(
702
911
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
703
912
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
704
913
  return categories.map((category, i) => {
705
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
914
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
706
915
  const fromRecord = record?.[category]
707
916
  if (fromRecord) return fromRecord
708
917
  if (list && list.length > 0) {
@@ -749,7 +958,7 @@ interface DonutSlicesConfig {
749
958
  */
750
959
  function donutSlices(
751
960
  data: readonly Record<string, unknown>[],
752
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
961
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
753
962
  ): DonutSlice[] {
754
963
  const items = data
755
964
  .map((row, i) => {
@@ -762,7 +971,8 @@ function donutSlices(
762
971
  const slices: DonutSlice[] = top.map((item, rank) => ({
763
972
  name: item.name,
764
973
  value: item.value,
765
- color: palette[rank % palette.length]!,
974
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
975
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
766
976
  isOther: false,
767
977
  row: item.row,
768
978
  }))
@@ -835,7 +1045,7 @@ interface BarListRowsConfig extends BarListOptions {
835
1045
  */
836
1046
  function barListRows<Row extends object>(
837
1047
  data: readonly Row[],
838
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1048
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
839
1049
  ): BarListRow<Row>[] {
840
1050
  const items = data.map((row): BarListRow<Row> => {
841
1051
  const cells = row as Record<string, unknown>
@@ -881,19 +1091,21 @@ const BAR_LIST_OTHER_TINT = 30
881
1091
 
882
1092
  /**
883
1093
  * The fill of each BarList row: all real rows in the same tint of `color`
884
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1094
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
885
1095
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
886
1096
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
887
1097
  * for that row. A tint, not the solid token, so the label printed over the bar
888
1098
  * keeps its contrast.
889
1099
  */
890
1100
  function barListFills(
891
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1101
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
892
1102
  color: ChartColor,
893
1103
  otherColor: string,
894
1104
  ): string[] {
895
1105
  return rows.map((row) =>
896
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1106
+ row.isOther || isOtherName(row.name)
1107
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1108
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
897
1109
  )
898
1110
  }
899
1111
 
@@ -1109,11 +1321,30 @@ interface ChartSummaryInput {
1109
1321
  format?: ValueFormat
1110
1322
  /** Wins over `format`. */
1111
1323
  valueFormatter?: ValueFormatter
1324
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1325
+ locale?: string
1326
+ }
1327
+
1328
+ const SUMMARY_TEXT = {
1329
+ ja: {
1330
+ up: "、前の点より増加",
1331
+ down: "、前の点より減少",
1332
+ flat: "、前の点と横ばい",
1333
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1334
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1335
+ },
1336
+ en: {
1337
+ up: ", up from the previous point",
1338
+ down: ", down from the previous point",
1339
+ flat: ", flat from the previous point",
1340
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1341
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1342
+ },
1112
1343
  }
1113
1344
 
1114
1345
  /**
1115
1346
  * One-sentence summary built from the rows, for `summary="auto"`:
1116
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1347
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1117
1348
  * computed from the last two non-empty points of the first series — never
1118
1349
  * written by hand, so it cannot contradict the data. `undefined` when there is
1119
1350
  * no usable point.
@@ -1129,26 +1360,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1129
1360
  }
1130
1361
  const latest = points[points.length - 1]
1131
1362
  if (!latest) return undefined
1132
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1363
+ const locale = resolveLocale(input.locale)
1364
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1365
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1133
1366
  const prev = points[points.length - 2]
1134
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1367
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1135
1368
  const head = input.title ? `${input.title}: ` : ""
1136
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1369
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1137
1370
  }
1138
1371
 
1139
1372
  /**
1140
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1141
- * (largest first). `undefined` when there is nothing to describe.
1373
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1374
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1142
1375
  */
1143
1376
  function buildShareSummary(
1144
1377
  slices: readonly { name: string; value: number }[],
1145
1378
  title?: string,
1379
+ locale?: string,
1146
1380
  ): string | undefined {
1147
1381
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1148
1382
  const top = slices[0]
1149
1383
  if (!top || total <= 0) return undefined
1150
1384
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1151
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1385
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1152
1386
  }
1153
1387
 
1154
1388
  /* ---------- end of inlined code ---------- */
@@ -1188,7 +1422,8 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
1188
1422
  ) {
1189
1423
  // The bar's width thins the running totals that would print over each other.
1190
1424
  const { ref, width } = useChartSurface(forwardedRef, null)
1191
- const valueFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format), [valueFormatterProp, format])
1425
+ const locale = useLocale()
1426
+ const valueFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
1192
1427
  const segments = categoryBarSegments(values)
1193
1428
  const markerPosition = marker ? categoryBarMarkerPosition(marker.value, values) : 0
1194
1429
  const allLabels = showLabels ? categoryBarLabels(values, undefined, marker ? markerPosition : undefined) : []