@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
@@ -27,11 +27,143 @@ import { ChartFrame } from "@squadbase/vantage/components"
27
27
  import { EChart } from "@squadbase/vantage/components"
28
28
  import { EMPTY_VALUE, type ValueFormat } from "@squadbase/vantage/components"
29
29
  import type { ChartActionsOptions, ChartEmptyState, ChartRootAttributes, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
30
- import { createContext, useCallback, useEffect, useRef, useState } from "react"
30
+ import { useSyncExternalStore } from "react"
31
+ import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
31
32
  import type { ChartTone } from "@squadbase/vantage/components"
32
- import { DEFAULT_LOCALE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
33
+ import { formatValue, toValueFormatter } from "@squadbase/vantage/components"
33
34
  import type { ChartColor, ChartColors } from "@squadbase/vantage/components"
34
35
 
36
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
37
+
38
+ /**
39
+ * The display language of the parts and the defaults that follow it.
40
+ *
41
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
42
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
43
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
44
+ *
45
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
46
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
47
+ */
48
+
49
+ /** The language of the built-in wording: Japanese or English. */
50
+ type UiLanguage = "ja" | "en"
51
+
52
+ /** The locale used when `<html lang>` gives none (and on the server). */
53
+ const FALLBACK_LOCALE = "en-US"
54
+
55
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
56
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
57
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
58
+ }
59
+
60
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
61
+ function normalizeLocale(tag: string | null | undefined): string {
62
+ const value = tag?.trim()
63
+ if (!value) return FALLBACK_LOCALE
64
+ try {
65
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
66
+ } catch {
67
+ return FALLBACK_LOCALE
68
+ }
69
+ }
70
+
71
+ /**
72
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
73
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
74
+ */
75
+ function documentLocale(): string {
76
+ if (typeof document === "undefined") return FALLBACK_LOCALE
77
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
78
+ }
79
+
80
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
81
+ function resolveLocale(locale?: string | null): string {
82
+ return locale || documentLocale()
83
+ }
84
+
85
+ /** Picks the wording table of the locale's language. */
86
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
87
+ return table[uiLanguage(resolveLocale(locale))]
88
+ }
89
+
90
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
91
+ function defaultCurrency(locale?: string | null): string {
92
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
93
+ }
94
+
95
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
96
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
97
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
98
+ }
99
+
100
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
101
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
102
+
103
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
104
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
105
+
106
+ /**
107
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
108
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
109
+ * Monday for the rest.
110
+ */
111
+ function weekStartsOn(locale?: string | null): WeekDay {
112
+ const tag = normalizeLocale(resolveLocale(locale))
113
+ if (uiLanguage(tag) === "ja") return 1
114
+ try {
115
+ const info = new Intl.Locale(tag) as Intl.Locale & {
116
+ getWeekInfo?: () => { firstDay: number }
117
+ weekInfo?: { firstDay: number }
118
+ }
119
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
120
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
121
+ } catch {
122
+ // Fall through to the table below.
123
+ }
124
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
125
+ }
126
+
127
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
128
+
129
+ const listeners = new Set<() => void>()
130
+ let observer: MutationObserver | null = null
131
+
132
+ /** One observer on `<html lang>` for every subscribed part. */
133
+ function subscribe(onChange: () => void): () => void {
134
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
135
+ listeners.add(onChange)
136
+ if (!observer) {
137
+ observer = new MutationObserver(() => {
138
+ for (const listener of [...listeners]) listener()
139
+ })
140
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
141
+ }
142
+ return () => {
143
+ listeners.delete(onChange)
144
+ if (listeners.size === 0 && observer) {
145
+ observer.disconnect()
146
+ observer = null
147
+ }
148
+ }
149
+ }
150
+
151
+ const serverLocale = () => FALLBACK_LOCALE
152
+
153
+ /**
154
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
155
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
156
+ */
157
+ function useDocumentLocale(): string {
158
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
159
+ }
160
+
161
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
162
+ function useLocale(locale?: string | null): string {
163
+ const page = useDocumentLocale()
164
+ return locale || page
165
+ }
166
+
35
167
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
36
168
 
37
169
  /**
@@ -66,10 +198,23 @@ interface ChartTokens {
66
198
  border: string | null
67
199
  /** Tooltip surface. */
68
200
  popover: string | null
201
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
202
+ colors?: Record<string, string>
203
+ /** Actual inherited painting surface (may change without a token changing). */
204
+ surface?: string | null
69
205
  /** Tooltip text. */
70
206
  popoverForeground: string | null
71
207
  }
72
208
 
209
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
210
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
211
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
212
+ const OPTION_COLOR_TOKENS = [
213
+ "--background", "--card", "--muted", "--primary", "--destructive",
214
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
215
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
216
+ ]
217
+
73
218
  let normalizeContext: CanvasRenderingContext2D | null | undefined
74
219
 
75
220
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -125,8 +270,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
125
270
  const color = read(token)
126
271
  if (color) series.push(color)
127
272
  }
128
- return {
273
+ const tokens: ChartTokens = {
129
274
  series,
275
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
276
+ surface: readChartSurface(target),
130
277
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
131
278
  foreground: read("--foreground"),
132
279
  mutedForeground: read("--muted-foreground"),
@@ -134,6 +281,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
134
281
  popover: read("--popover"),
135
282
  popoverForeground: read("--popover-foreground"),
136
283
  }
284
+ tokenScopes.set(tokens, target)
285
+ return tokens
137
286
  }
138
287
 
139
288
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -141,6 +290,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
141
290
  if (a === b) return true
142
291
  if (!a || !b) return false
143
292
  return (
293
+ tokenScopes.get(a) === tokenScopes.get(b) &&
294
+ a.surface === b.surface &&
295
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
296
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
144
297
  a.series.length === b.series.length &&
145
298
  a.series.every((color, i) => color === b.series[i]) &&
146
299
  a.fontFamily === b.fontFamily &&
@@ -166,7 +319,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
166
319
  * attributes alone leaves the chart painted in the previous palette until a
167
320
  * reload — the tokens in the DOM are new, the canvas is not.
168
321
  */
169
- function watchChartTokens(onChange: () => void): () => void {
322
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
170
323
  if (typeof document === "undefined") return () => {}
171
324
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
172
325
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -181,8 +334,11 @@ function watchChartTokens(onChange: () => void): () => void {
181
334
 
182
335
  const themeObserver = new MutationObserver(schedule)
183
336
  const attributeFilter = ["class", "style", "data-theme"]
184
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
185
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
337
+ // Only the chart and its ancestors can change its inherited theme. Never observe
338
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
339
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
340
+ themeObserver.observe(node, { attributes: true, attributeFilter })
341
+ }
186
342
 
187
343
  const styleObserver = new MutationObserver(schedule)
188
344
  if (document.head) {
@@ -270,22 +426,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
270
426
  * components that build an ECharts option from token colours themselves (the
271
427
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
272
428
  */
273
- function useChartTokens(): ChartTokens | null {
429
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
274
430
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
275
431
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
276
432
  // replayed by React on every rebase and returns a fresh object each time, so the state
277
433
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
278
434
  const tokensRef = useRef(tokens)
279
435
  useEffect(() => {
436
+ const target = scope && "current" in scope ? scope.current : scope
280
437
  const sync = () => {
281
- const next = readChartTokens()
438
+ const next = readChartTokens(target)
282
439
  if (sameChartTokens(tokensRef.current, next)) return
283
440
  tokensRef.current = next
284
441
  setTokens(next)
285
442
  }
286
443
  sync()
287
- return watchChartTokens(sync)
288
- }, [])
444
+ return watchChartTokens(sync, target)
445
+ }, [scope])
289
446
  return tokens
290
447
  }
291
448
 
@@ -297,7 +454,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
297
454
  /**
298
455
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
299
456
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
300
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
457
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
301
458
  * other CSS colour is normalized like the tokens are. A colour that can't be
302
459
  * resolved comes back as `""`, which ECharts treats as "use the palette".
303
460
  */
@@ -308,12 +465,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
308
465
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
309
466
  if (TONE_NAME_RE.test(color.trim())) {
310
467
  if (typeof document === "undefined") return ""
311
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
468
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
469
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
312
470
  }
313
471
  if (typeof document === "undefined") return color
314
472
  let value = color.trim()
315
473
  const cssVar = CSS_VAR_RE.exec(value)
316
- const styles = getComputedStyle(document.documentElement)
474
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
317
475
  if (cssVar) {
318
476
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
319
477
  } else if (value.includes("var(")) {
@@ -359,7 +517,7 @@ function readChartSurface(el: Element | null): string | null {
359
517
  if (color) return color
360
518
  }
361
519
  }
362
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
520
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
363
521
  }
364
522
 
365
523
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -373,9 +531,10 @@ const WIDTH_BUCKET = 40
373
531
  */
374
532
  function useChartSurface<E extends Element>(
375
533
  forwarded: ForwardedRef<E>,
376
- tokens: ChartTokens | null,
377
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
534
+ tokens?: ChartTokens | null,
535
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
378
536
  const [node, setNode] = useState<E | null>(null)
537
+ const scopedTokens = useChartTokens(node)
379
538
  const [surface, setSurface] = useState<string | undefined>(undefined)
380
539
  const [width, setWidth] = useState<number | undefined>(undefined)
381
540
  const ref = useCallback(
@@ -389,7 +548,7 @@ function useChartSurface<E extends Element>(
389
548
  useEffect(() => {
390
549
  if (!node) return
391
550
  setSurface(readChartSurface(node) ?? undefined)
392
- }, [node, tokens])
551
+ }, [node, tokens, scopedTokens])
393
552
  useEffect(() => {
394
553
  if (!node || typeof ResizeObserver === "undefined") return
395
554
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -398,7 +557,7 @@ function useChartSurface<E extends Element>(
398
557
  observer.observe(node)
399
558
  return () => observer.disconnect()
400
559
  }, [node])
401
- return { ref, surface, width }
560
+ return { ref, surface, width, tokens: scopedTokens }
402
561
  }
403
562
 
404
563
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -508,9 +667,10 @@ function resolveValueFormatter(
508
667
  function resolveAxisFormatter(
509
668
  valueFormatter: ValueFormatter | undefined,
510
669
  format: ValueFormat | undefined,
511
- locale: string = DEFAULT_LOCALE,
670
+ localeArg?: string,
512
671
  ): ValueFormatter {
513
672
  if (valueFormatter) return valueFormatter
673
+ const locale = resolveLocale(localeArg)
514
674
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
515
675
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
516
676
  const currency = "currency" in spec ? spec.currency : undefined
@@ -519,40 +679,75 @@ function resolveAxisFormatter(
519
679
  return (value) => formatValue(value, short, locale)
520
680
  }
521
681
 
522
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
523
- const JA_UNITS = [
524
- { char: "兆", size: 1e12 },
525
- { char: "億", size: 1e8 },
526
- { char: "万", size: 1e4 },
527
- ] as const
682
+ interface CompactUnit {
683
+ token: string
684
+ size: number
685
+ }
528
686
 
529
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
687
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
688
+
689
+ /**
690
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
691
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
692
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
693
+ */
694
+ function compactUnits(locale: string): CompactUnit[] | null {
695
+ const cached = compactUnitCache.get(locale)
696
+ if (cached !== undefined) return cached
697
+ let units: CompactUnit[] | null = []
698
+ try {
699
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
700
+ const seen = new Set<string>()
701
+ for (let exp = 3; exp <= 15; exp++) {
702
+ const parts = compact.formatToParts(10 ** exp)
703
+ const at = parts.findIndex((p) => p.type === "compact")
704
+ if (at === -1) continue
705
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
706
+ units = null
707
+ break
708
+ }
709
+ const token = parts[at]!.value
710
+ if (seen.has(token)) continue
711
+ seen.add(token)
712
+ units.push({ token, size: 10 ** exp })
713
+ }
714
+ } catch {
715
+ units = null
716
+ }
717
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
718
+ compactUnitCache.set(locale, sorted)
719
+ return sorted
720
+ }
721
+
722
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
530
723
 
531
724
  /**
532
725
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
533
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
534
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
535
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
726
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
727
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
728
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
729
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
536
730
  */
537
731
  function resolveAxisTickFormatter(
538
732
  valueFormatter: ValueFormatter | undefined,
539
733
  format: ValueFormat | undefined,
540
- locale: string = DEFAULT_LOCALE,
734
+ localeArg?: string,
541
735
  ): (top: number) => ValueFormatter {
736
+ const locale = resolveLocale(localeArg)
542
737
  const base = resolveAxisFormatter(valueFormatter, format, locale)
543
738
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
544
- const aligns =
545
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
546
- if (!aligns) return () => base
739
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
740
+ if (!units) return () => base
547
741
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
742
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
548
743
  return (top) => {
549
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
744
+ const unit = units.find((u) => Math.abs(top) >= u.size)
550
745
  if (!unit) return base
551
746
  return (value) => {
552
747
  const text = base(value)
553
748
  if (value === 0 || !Number.isFinite(value)) return text
554
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
555
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
749
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
750
+ return text.replace(numberToken, () => scaled)
556
751
  }
557
752
  }
558
753
  }
@@ -581,8 +776,20 @@ function clamp(value: number, min: number, max: number): number {
581
776
  /** How many categories a chart colours individually; the rest fold into「その他」. */
582
777
  const MAX_CHART_SERIES = 5
583
778
 
584
- /** The series name the folded categories get. */
585
- const OTHER_SERIES_LABEL = "その他"
779
+ /** The series name the folded categories get, by language. */
780
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
781
+
782
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
783
+ function otherSeriesLabel(locale?: string): string {
784
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
785
+ }
786
+
787
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
788
+
789
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
790
+ function isOtherName(name: string | null | undefined): boolean {
791
+ return typeof name === "string" && OTHER_NAMES.has(name)
792
+ }
586
793
 
587
794
  interface AggregatedItems<Item extends { name: string; value: number }> {
588
795
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -602,7 +809,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
602
809
  function aggregateTopN<Item extends { name: string; value: number }>(
603
810
  items: readonly Item[],
604
811
  n: number = MAX_CHART_SERIES,
605
- otherLabel: string = OTHER_SERIES_LABEL,
812
+ otherLabel: string = otherSeriesLabel(),
606
813
  ): AggregatedItems<Item> {
607
814
  if (items.length <= n) return { top: [...items], rest: [], other: null }
608
815
  const ranked = items
@@ -631,7 +838,7 @@ function aggregateSeries(
631
838
  data: readonly Record<string, unknown>[],
632
839
  categories: readonly string[],
633
840
  n: number = MAX_CHART_SERIES,
634
- otherLabel: string = OTHER_SERIES_LABEL,
841
+ otherLabel: string = otherSeriesLabel(),
635
842
  ): AggregatedSeries {
636
843
  const totals = categories.map((name) => ({
637
844
  name,
@@ -663,12 +870,14 @@ function aggregateSeriesKeeping(
663
870
  categories: readonly string[],
664
871
  keep: string | undefined,
665
872
  n: number = MAX_CHART_SERIES,
873
+ otherLabel: string = otherSeriesLabel(),
666
874
  ): AggregatedSeries {
667
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
875
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
668
876
  const aggregated = aggregateSeries(
669
877
  data,
670
878
  categories.filter((c) => c !== keep),
671
879
  n,
880
+ otherLabel,
672
881
  )
673
882
  return { ...aggregated, categories: [...aggregated.categories, keep] }
674
883
  }
@@ -704,7 +913,7 @@ function seriesColorsWithComparison(
704
913
  * Colour per drawn series: the record's token for that category, else the
705
914
  * array's token at the category's position in `allCategories` (cycled), else
706
915
  * the token palette by drawn position. `otherCategory` — and a category the
707
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
916
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
708
917
  */
709
918
  function seriesColorsFor(
710
919
  categories: readonly string[],
@@ -716,7 +925,7 @@ function seriesColorsFor(
716
925
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
717
926
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
718
927
  return categories.map((category, i) => {
719
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
928
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
720
929
  const fromRecord = record?.[category]
721
930
  if (fromRecord) return fromRecord
722
931
  if (list && list.length > 0) {
@@ -763,7 +972,7 @@ interface DonutSlicesConfig {
763
972
  */
764
973
  function donutSlices(
765
974
  data: readonly Record<string, unknown>[],
766
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
975
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
767
976
  ): DonutSlice[] {
768
977
  const items = data
769
978
  .map((row, i) => {
@@ -776,7 +985,8 @@ function donutSlices(
776
985
  const slices: DonutSlice[] = top.map((item, rank) => ({
777
986
  name: item.name,
778
987
  value: item.value,
779
- color: palette[rank % palette.length]!,
988
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
989
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
780
990
  isOther: false,
781
991
  row: item.row,
782
992
  }))
@@ -849,7 +1059,7 @@ interface BarListRowsConfig extends BarListOptions {
849
1059
  */
850
1060
  function barListRows<Row extends object>(
851
1061
  data: readonly Row[],
852
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1062
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
853
1063
  ): BarListRow<Row>[] {
854
1064
  const items = data.map((row): BarListRow<Row> => {
855
1065
  const cells = row as Record<string, unknown>
@@ -895,19 +1105,21 @@ const BAR_LIST_OTHER_TINT = 30
895
1105
 
896
1106
  /**
897
1107
  * The fill of each BarList row: all real rows in the same tint of `color`
898
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1108
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
899
1109
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
900
1110
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
901
1111
  * for that row. A tint, not the solid token, so the label printed over the bar
902
1112
  * keeps its contrast.
903
1113
  */
904
1114
  function barListFills(
905
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1115
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
906
1116
  color: ChartColor,
907
1117
  otherColor: string,
908
1118
  ): string[] {
909
1119
  return rows.map((row) =>
910
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1120
+ row.isOther || isOtherName(row.name)
1121
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1122
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
911
1123
  )
912
1124
  }
913
1125
 
@@ -1123,11 +1335,30 @@ interface ChartSummaryInput {
1123
1335
  format?: ValueFormat
1124
1336
  /** Wins over `format`. */
1125
1337
  valueFormatter?: ValueFormatter
1338
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1339
+ locale?: string
1340
+ }
1341
+
1342
+ const SUMMARY_TEXT = {
1343
+ ja: {
1344
+ up: "、前の点より増加",
1345
+ down: "、前の点より減少",
1346
+ flat: "、前の点と横ばい",
1347
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1348
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1349
+ },
1350
+ en: {
1351
+ up: ", up from the previous point",
1352
+ down: ", down from the previous point",
1353
+ flat: ", flat from the previous point",
1354
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1355
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1356
+ },
1126
1357
  }
1127
1358
 
1128
1359
  /**
1129
1360
  * One-sentence summary built from the rows, for `summary="auto"`:
1130
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1361
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1131
1362
  * computed from the last two non-empty points of the first series — never
1132
1363
  * written by hand, so it cannot contradict the data. `undefined` when there is
1133
1364
  * no usable point.
@@ -1143,26 +1374,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1143
1374
  }
1144
1375
  const latest = points[points.length - 1]
1145
1376
  if (!latest) return undefined
1146
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1377
+ const locale = resolveLocale(input.locale)
1378
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1379
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1147
1380
  const prev = points[points.length - 2]
1148
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1381
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1149
1382
  const head = input.title ? `${input.title}: ` : ""
1150
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1383
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1151
1384
  }
1152
1385
 
1153
1386
  /**
1154
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1155
- * (largest first). `undefined` when there is nothing to describe.
1387
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1388
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1156
1389
  */
1157
1390
  function buildShareSummary(
1158
1391
  slices: readonly { name: string; value: number }[],
1159
1392
  title?: string,
1393
+ locale?: string,
1160
1394
  ): string | undefined {
1161
1395
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1162
1396
  const top = slices[0]
1163
1397
  if (!top || total <= 0) return undefined
1164
1398
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1165
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1399
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1166
1400
  }
1167
1401
 
1168
1402
  /* ---------- end of inlined code ---------- */
@@ -1428,6 +1662,25 @@ export function heatmapCellFontSize(
1428
1662
  return (text * 10) / 12 + HEATMAP_CELL_PADDING <= cell ? 10 : null
1429
1663
  }
1430
1664
 
1665
+ const TEXT = {
1666
+ ja: {
1667
+ join: "・",
1668
+ flagged: "要注意",
1669
+ item: "項目",
1670
+ top: (y: string, x: string, value: string) => `最大は ${y} ${x} の ${value}`,
1671
+ chart: (y: string, x: string) => `${y}×${x}のヒートマップ`,
1672
+ },
1673
+ en: {
1674
+ join: ", ",
1675
+ flagged: "Needs attention",
1676
+ item: "Item",
1677
+ top: (y: string, x: string, value: string) => `Highest: ${y} ${x} (${value})`,
1678
+ chart: (y: string, x: string) => `Heatmap of ${y} × ${x}`,
1679
+ },
1680
+ }
1681
+
1682
+ const textOf = (locale?: string) => TEXT[uiLanguage(resolveLocale(locale))]
1683
+
1431
1684
  export interface HeatmapOptionConfig {
1432
1685
  model: HeatmapModel
1433
1686
  x: string
@@ -1452,6 +1705,8 @@ export interface HeatmapOptionConfig {
1452
1705
  /** The plot's width (px), when known: the numbers shrink (or go) when the cells are too narrow for them. */
1453
1706
  plotWidth?: number | null
1454
1707
  chartOptions?: EChartsOption
1708
+ /** Language of the tooltip. Default the page's locale (`<html lang>`). */
1709
+ locale?: string
1455
1710
  }
1456
1711
 
1457
1712
  /**
@@ -1463,6 +1718,7 @@ export interface HeatmapOptionConfig {
1463
1718
  */
1464
1719
  export function buildHeatmapOption(config: HeatmapOptionConfig): EChartsOption {
1465
1720
  const { model, labels, valueFormatter } = config
1721
+ const t = textOf(config.locale)
1466
1722
  const label = (key: string) => labels?.[key] ?? key
1467
1723
  const present = model.cells.filter((c) => c.value !== null)
1468
1724
  const missing = model.cells.filter((c) => c.value === null)
@@ -1480,9 +1736,9 @@ export function buildHeatmapOption(config: HeatmapOptionConfig): EChartsOption {
1480
1736
  const n = cell.row ? toChartNumber(cell.row[key]) : null
1481
1737
  return `${escapeHtml(label(key))} ${n === null ? EMPTY_VALUE : escapeHtml(valueFormatter(n))}`
1482
1738
  })
1483
- .join("・")
1484
- const flag = cell.flagged ? "・要注意" : ""
1485
- return `${head} ${main}${extras ? `・${extras}` : ""}${flag}`
1739
+ .join(t.join)
1740
+ const flag = cell.flagged ? `${t.join}${t.flagged}` : ""
1741
+ return `${head} ${main}${extras ? `${t.join}${extras}` : ""}${flag}`
1486
1742
  }
1487
1743
 
1488
1744
  const cellFont = config.plotWidth
@@ -1599,6 +1855,7 @@ export function heatmapDataTable(
1599
1855
  names: { x: string; y: string },
1600
1856
  formatter: ValueFormatter,
1601
1857
  labels?: Record<string, string>,
1858
+ locale?: string,
1602
1859
  ): ChartDataTable {
1603
1860
  const label = (key: string) => labels?.[key] ?? key
1604
1861
  const rows = model.ys.map((yk, yi) => [
@@ -1608,17 +1865,26 @@ export function heatmapDataTable(
1608
1865
  return v === null || v === undefined ? EMPTY_VALUE : formatter(v)
1609
1866
  }),
1610
1867
  ])
1611
- return { caption: `${label(names.y)}×${label(names.x)}`, columns: [labels?.[names.y] ?? "項目", ...model.xs.map(label)], rows }
1868
+ return { caption: `${label(names.y)}×${label(names.x)}`, columns: [labels?.[names.y] ?? textOf(locale).item, ...model.xs.map(label)], rows }
1612
1869
  }
1613
1870
 
1614
- /** `summary="auto"`: the strongest cell, e.g. 「最大は 月曜 9時台 の 12件」. `undefined` with no usable cell. */
1615
- export function buildHeatmapSummary(model: HeatmapModel, formatter: ValueFormatter, title?: string, labels?: Record<string, string>): string | undefined {
1871
+ /**
1872
+ * `summary="auto"`: the strongest cell, e.g. 「最大は 月曜 9時台 の 12件」 / "Highest: Mon 9am (12)". `undefined` with no usable cell.
1873
+ * `locale` picks the language; default the page's (`<html lang>`).
1874
+ */
1875
+ export function buildHeatmapSummary(
1876
+ model: HeatmapModel,
1877
+ formatter: ValueFormatter,
1878
+ title?: string,
1879
+ labels?: Record<string, string>,
1880
+ locale?: string,
1881
+ ): string | undefined {
1616
1882
  let best: HeatmapCell | undefined
1617
1883
  for (const cell of model.cells) if (cell.value !== null && (!best || cell.value > (best.value as number))) best = cell
1618
1884
  if (!best) return undefined
1619
1885
  const label = (key: string) => labels?.[key] ?? key
1620
1886
  const head = title ? `${title}: ` : ""
1621
- return `${head}最大は ${label(model.ys[best.yi]!)} ${label(model.xs[best.xi]!)} の ${formatter(best.value as number)}`
1887
+ return `${head}${textOf(locale).top(label(model.ys[best.yi]!), label(model.xs[best.xi]!), formatter(best.value as number))}`
1622
1888
  }
1623
1889
 
1624
1890
  /* -------------------------------------------------------------- component */
@@ -1665,9 +1931,9 @@ function HeatmapImpl<T extends Record<string, unknown>>(
1665
1931
  }: HeatmapProps<T>,
1666
1932
  ref: ForwardedRef<HTMLDivElement>,
1667
1933
  ) {
1668
- const tokens = useChartTokens()
1669
- const { ref: frameRef, surface, width: plotWidth } = useChartSurface(ref, tokens)
1670
- const formatter = useMemo(() => resolveValueFormatter(valueFormatter, format ?? "number"), [valueFormatter, format])
1934
+ const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
1935
+ const locale = useLocale()
1936
+ const formatter = useMemo(() => resolveValueFormatter(valueFormatter, format ?? "number", locale), [valueFormatter, format, locale])
1671
1937
  const model = useMemo(
1672
1938
  () => buildHeatmapModel({ data, x, y, value, xLabels, yLabels, flag, scale, minValue, maxValue }),
1673
1939
  [data, x, y, value, xLabels, yLabels, flag, scale?.min, scale?.max, minValue, maxValue],
@@ -1693,15 +1959,16 @@ function HeatmapImpl<T extends Record<string, unknown>>(
1693
1959
  showTooltip,
1694
1960
  plotWidth,
1695
1961
  chartOptions,
1962
+ locale,
1696
1963
  }),
1697
- [model, x, y, value, colors, surface, tokens, labels, formatter, details, showValues, showTooltip, plotWidth, chartOptions],
1964
+ [model, x, y, value, colors, surface, tokens, labels, formatter, details, showValues, showTooltip, plotWidth, chartOptions, locale],
1698
1965
  )
1699
- const dataTable = useMemo(() => heatmapDataTable(model, { x, y }, formatter, labels), [model, x, y, formatter, labels])
1966
+ const dataTable = useMemo(() => heatmapDataTable(model, { x, y }, formatter, labels, locale), [model, x, y, formatter, labels, locale])
1700
1967
  const resolvedSummary = useMemo(
1701
- () => (summary === "auto" ? buildHeatmapSummary(model, formatter, title, labels) : summary),
1702
- [summary, model, formatter, title, labels],
1968
+ () => (summary === "auto" ? buildHeatmapSummary(model, formatter, title, labels, locale) : summary),
1969
+ [summary, model, formatter, title, labels, locale],
1703
1970
  )
1704
- const label = ariaLabel ?? `${labels?.[y] ?? y}×${labels?.[x] ?? x}のヒートマップ`
1971
+ const label = ariaLabel ?? textOf(locale).chart(labels?.[y] ?? y, labels?.[x] ?? x)
1705
1972
  const legend = showLegend ? (
1706
1973
  <HeatmapLegend model={model} formatter={formatter} tone={tone} />
1707
1974
  ) : null