@squadbase/vantage 0.6.2-dev1.48b34e1 → 0.6.2-dev1.51e401a

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 (71) hide show
  1. package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
  2. package/dist/chunk-3RJEOP3J.js.map +1 -0
  3. package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
  4. package/dist/chunk-77CISBMG.js.map +1 -0
  5. package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
  6. package/dist/chunk-EG4PX726.js.map +1 -0
  7. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  8. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  9. package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
  10. package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
  11. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  12. package/dist/chunk-ROHNXP7J.js.map +1 -0
  13. package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
  14. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  15. package/dist/components/index.d.ts +49 -29
  16. package/dist/components/index.js +5 -5
  17. package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
  18. package/dist/doc/index.d.ts +14 -14
  19. package/dist/doc/index.js +198 -84
  20. package/dist/doc/index.js.map +1 -1
  21. package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
  22. package/dist/slide/index.d.ts +2 -2
  23. package/dist/slide/index.js +95 -44
  24. package/dist/slide/index.js.map +1 -1
  25. package/dist/table/index.js +6 -6
  26. package/dist/ui/index.d.ts +1 -1
  27. package/dist/ui/index.js +5 -5
  28. package/package.json +2 -2
  29. package/registry/ui/app-shell.tsx +138 -1
  30. package/registry/ui/area-chart.tsx +344 -85
  31. package/registry/ui/attention-list.tsx +177 -15
  32. package/registry/ui/bar-chart.tsx +341 -85
  33. package/registry/ui/bar-list.tsx +332 -77
  34. package/registry/ui/box-plot-chart.tsx +405 -97
  35. package/registry/ui/category-bar.tsx +248 -40
  36. package/registry/ui/chart-frame.tsx +257 -46
  37. package/registry/ui/data-table.tsx +229 -37
  38. package/registry/ui/date-range-picker.tsx +190 -36
  39. package/registry/ui/delta.tsx +171 -12
  40. package/registry/ui/donut-chart.tsx +339 -84
  41. package/registry/ui/echart.tsx +142 -4
  42. package/registry/ui/filter-bar.tsx +179 -20
  43. package/registry/ui/funnel-steps.tsx +354 -80
  44. package/registry/ui/heatmap.tsx +293 -52
  45. package/registry/ui/histogram-chart.tsx +395 -97
  46. package/registry/ui/kpi-card.tsx +245 -44
  47. package/registry/ui/legend.tsx +330 -76
  48. package/registry/ui/line-chart.tsx +344 -85
  49. package/registry/ui/multi-select.tsx +159 -9
  50. package/registry/ui/page-shell.tsx +141 -3
  51. package/registry/ui/paired-chart.tsx +349 -90
  52. package/registry/ui/progress-circle.tsx +205 -39
  53. package/registry/ui/sample-data.tsx +153 -9
  54. package/registry/ui/scatter-chart.tsx +391 -100
  55. package/registry/ui/searchable-select.tsx +145 -7
  56. package/registry/ui/states.tsx +170 -13
  57. package/registry/ui/status-badge.tsx +193 -28
  58. package/registry/ui/tracker.tsx +205 -39
  59. package/registry/ui/trend-indicator.tsx +149 -6
  60. package/registry/ui/updated-at.tsx +170 -16
  61. package/registry/ui/waterfall-chart.tsx +415 -102
  62. package/skills/vantage-add-feature/SKILL.md +5 -0
  63. package/templates/AGENTS.md +10 -8
  64. package/templates/agents/api.md +7 -0
  65. package/templates/agents/page.md +36 -0
  66. package/dist/chunk-2N72FKLB.js.map +0 -1
  67. package/dist/chunk-57GHR5LN.js.map +0 -1
  68. package/dist/chunk-6OTTGAPO.js.map +0 -1
  69. package/dist/chunk-JXFCSF3K.js.map +0 -1
  70. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  71. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -15,15 +15,148 @@ import { forwardRef, useMemo, type ForwardedRef, type ReactElement, type Ref } f
15
15
  import { ChartFrame } from "@squadbase/vantage/components"
16
16
  import { EChart } from "@squadbase/vantage/components"
17
17
  import type { ChartComparison, ChartForecast, NumericKeys } from "@squadbase/vantage/components"
18
+ import { useSyncExternalStore } from "react"
18
19
  import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
19
20
  import type { ChartTone } from "@squadbase/vantage/components"
20
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
21
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
21
22
  import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
22
23
  import { type ReactNode } from "react"
23
24
  import type { EChartsOption } from "@squadbase/vantage/components"
24
25
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
26
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
25
27
  import type { ChartThreshold, ChartAnnotation, ChartCommonProps, ChartEventPayload, ChartForecastRow, ChartHighlight } from "@squadbase/vantage/components"
26
28
 
29
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
30
+
31
+ /**
32
+ * The display language of the parts and the defaults that follow it.
33
+ *
34
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
35
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
36
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
37
+ *
38
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
39
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
40
+ */
41
+
42
+ /** The language of the built-in wording: Japanese or English. */
43
+ type UiLanguage = "ja" | "en"
44
+
45
+ /** The locale used when `<html lang>` gives none (and on the server). */
46
+ const FALLBACK_LOCALE = "en-US"
47
+
48
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
49
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
50
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
51
+ }
52
+
53
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
54
+ function normalizeLocale(tag: string | null | undefined): string {
55
+ const value = tag?.trim()
56
+ if (!value) return FALLBACK_LOCALE
57
+ try {
58
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
59
+ } catch {
60
+ return FALLBACK_LOCALE
61
+ }
62
+ }
63
+
64
+ /**
65
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
66
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
67
+ */
68
+ function documentLocale(): string {
69
+ if (typeof document === "undefined") return FALLBACK_LOCALE
70
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
71
+ }
72
+
73
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
74
+ function resolveLocale(locale?: string | null): string {
75
+ return locale || documentLocale()
76
+ }
77
+
78
+ /** Picks the wording table of the locale's language. */
79
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
80
+ return table[uiLanguage(resolveLocale(locale))]
81
+ }
82
+
83
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
84
+ function defaultCurrency(locale?: string | null): string {
85
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
86
+ }
87
+
88
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
89
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
90
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
91
+ }
92
+
93
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
94
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
95
+
96
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
97
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
98
+
99
+ /**
100
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
101
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
102
+ * Monday for the rest.
103
+ */
104
+ function weekStartsOn(locale?: string | null): WeekDay {
105
+ const tag = normalizeLocale(resolveLocale(locale))
106
+ if (uiLanguage(tag) === "ja") return 1
107
+ try {
108
+ const info = new Intl.Locale(tag) as Intl.Locale & {
109
+ getWeekInfo?: () => { firstDay: number }
110
+ weekInfo?: { firstDay: number }
111
+ }
112
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
113
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
114
+ } catch {
115
+ // Fall through to the table below.
116
+ }
117
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
118
+ }
119
+
120
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
121
+
122
+ const listeners = new Set<() => void>()
123
+ let observer: MutationObserver | null = null
124
+
125
+ /** One observer on `<html lang>` for every subscribed part. */
126
+ function subscribe(onChange: () => void): () => void {
127
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
128
+ listeners.add(onChange)
129
+ if (!observer) {
130
+ observer = new MutationObserver(() => {
131
+ for (const listener of [...listeners]) listener()
132
+ })
133
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
134
+ }
135
+ return () => {
136
+ listeners.delete(onChange)
137
+ if (listeners.size === 0 && observer) {
138
+ observer.disconnect()
139
+ observer = null
140
+ }
141
+ }
142
+ }
143
+
144
+ const serverLocale = () => FALLBACK_LOCALE
145
+
146
+ /**
147
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
148
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
149
+ */
150
+ function useDocumentLocale(): string {
151
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
152
+ }
153
+
154
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
155
+ function useLocale(locale?: string | null): string {
156
+ const page = useDocumentLocale()
157
+ return locale || page
158
+ }
159
+
27
160
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
28
161
 
29
162
  /**
@@ -527,9 +660,10 @@ function resolveValueFormatter(
527
660
  function resolveAxisFormatter(
528
661
  valueFormatter: ValueFormatter | undefined,
529
662
  format: ValueFormat | undefined,
530
- locale: string = DEFAULT_LOCALE,
663
+ localeArg?: string,
531
664
  ): ValueFormatter {
532
665
  if (valueFormatter) return valueFormatter
666
+ const locale = resolveLocale(localeArg)
533
667
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
534
668
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
535
669
  const currency = "currency" in spec ? spec.currency : undefined
@@ -538,40 +672,75 @@ function resolveAxisFormatter(
538
672
  return (value) => formatValue(value, short, locale)
539
673
  }
540
674
 
541
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
542
- const JA_UNITS = [
543
- { char: "兆", size: 1e12 },
544
- { char: "億", size: 1e8 },
545
- { char: "万", size: 1e4 },
546
- ] as const
675
+ interface CompactUnit {
676
+ token: string
677
+ size: number
678
+ }
547
679
 
548
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
680
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
681
+
682
+ /**
683
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
684
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
685
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
686
+ */
687
+ function compactUnits(locale: string): CompactUnit[] | null {
688
+ const cached = compactUnitCache.get(locale)
689
+ if (cached !== undefined) return cached
690
+ let units: CompactUnit[] | null = []
691
+ try {
692
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
693
+ const seen = new Set<string>()
694
+ for (let exp = 3; exp <= 15; exp++) {
695
+ const parts = compact.formatToParts(10 ** exp)
696
+ const at = parts.findIndex((p) => p.type === "compact")
697
+ if (at === -1) continue
698
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
699
+ units = null
700
+ break
701
+ }
702
+ const token = parts[at]!.value
703
+ if (seen.has(token)) continue
704
+ seen.add(token)
705
+ units.push({ token, size: 10 ** exp })
706
+ }
707
+ } catch {
708
+ units = null
709
+ }
710
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
711
+ compactUnitCache.set(locale, sorted)
712
+ return sorted
713
+ }
714
+
715
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
549
716
 
550
717
  /**
551
718
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
552
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
553
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
554
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
719
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
720
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
721
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
722
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
555
723
  */
556
724
  function resolveAxisTickFormatter(
557
725
  valueFormatter: ValueFormatter | undefined,
558
726
  format: ValueFormat | undefined,
559
- locale: string = DEFAULT_LOCALE,
727
+ localeArg?: string,
560
728
  ): (top: number) => ValueFormatter {
729
+ const locale = resolveLocale(localeArg)
561
730
  const base = resolveAxisFormatter(valueFormatter, format, locale)
562
731
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
563
- const aligns =
564
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
565
- if (!aligns) return () => base
732
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
733
+ if (!units) return () => base
566
734
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
735
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
567
736
  return (top) => {
568
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
737
+ const unit = units.find((u) => Math.abs(top) >= u.size)
569
738
  if (!unit) return base
570
739
  return (value) => {
571
740
  const text = base(value)
572
741
  if (value === 0 || !Number.isFinite(value)) return text
573
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
574
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
742
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
743
+ return text.replace(numberToken, () => scaled)
575
744
  }
576
745
  }
577
746
  }
@@ -600,8 +769,20 @@ function clamp(value: number, min: number, max: number): number {
600
769
  /** How many categories a chart colours individually; the rest fold into「その他」. */
601
770
  const MAX_CHART_SERIES = 5
602
771
 
603
- /** The series name the folded categories get. */
604
- const OTHER_SERIES_LABEL = "その他"
772
+ /** The series name the folded categories get, by language. */
773
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
774
+
775
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
776
+ function otherSeriesLabel(locale?: string): string {
777
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
778
+ }
779
+
780
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
781
+
782
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
783
+ function isOtherName(name: string | null | undefined): boolean {
784
+ return typeof name === "string" && OTHER_NAMES.has(name)
785
+ }
605
786
 
606
787
  interface AggregatedItems<Item extends { name: string; value: number }> {
607
788
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -621,7 +802,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
621
802
  function aggregateTopN<Item extends { name: string; value: number }>(
622
803
  items: readonly Item[],
623
804
  n: number = MAX_CHART_SERIES,
624
- otherLabel: string = OTHER_SERIES_LABEL,
805
+ otherLabel: string = otherSeriesLabel(),
625
806
  ): AggregatedItems<Item> {
626
807
  if (items.length <= n) return { top: [...items], rest: [], other: null }
627
808
  const ranked = items
@@ -650,7 +831,7 @@ function aggregateSeries(
650
831
  data: readonly Record<string, unknown>[],
651
832
  categories: readonly string[],
652
833
  n: number = MAX_CHART_SERIES,
653
- otherLabel: string = OTHER_SERIES_LABEL,
834
+ otherLabel: string = otherSeriesLabel(),
654
835
  ): AggregatedSeries {
655
836
  const totals = categories.map((name) => ({
656
837
  name,
@@ -682,12 +863,14 @@ function aggregateSeriesKeeping(
682
863
  categories: readonly string[],
683
864
  keep: string | undefined,
684
865
  n: number = MAX_CHART_SERIES,
866
+ otherLabel: string = otherSeriesLabel(),
685
867
  ): AggregatedSeries {
686
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
868
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
687
869
  const aggregated = aggregateSeries(
688
870
  data,
689
871
  categories.filter((c) => c !== keep),
690
872
  n,
873
+ otherLabel,
691
874
  )
692
875
  return { ...aggregated, categories: [...aggregated.categories, keep] }
693
876
  }
@@ -723,7 +906,7 @@ function seriesColorsWithComparison(
723
906
  * Colour per drawn series: the record's token for that category, else the
724
907
  * array's token at the category's position in `allCategories` (cycled), else
725
908
  * the token palette by drawn position. `otherCategory` — and a category the
726
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
909
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
727
910
  */
728
911
  function seriesColorsFor(
729
912
  categories: readonly string[],
@@ -735,7 +918,7 @@ function seriesColorsFor(
735
918
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
736
919
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
737
920
  return categories.map((category, i) => {
738
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
921
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
739
922
  const fromRecord = record?.[category]
740
923
  if (fromRecord) return fromRecord
741
924
  if (list && list.length > 0) {
@@ -782,7 +965,7 @@ interface DonutSlicesConfig {
782
965
  */
783
966
  function donutSlices(
784
967
  data: readonly Record<string, unknown>[],
785
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
968
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
786
969
  ): DonutSlice[] {
787
970
  const items = data
788
971
  .map((row, i) => {
@@ -795,7 +978,8 @@ function donutSlices(
795
978
  const slices: DonutSlice[] = top.map((item, rank) => ({
796
979
  name: item.name,
797
980
  value: item.value,
798
- color: palette[rank % palette.length]!,
981
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
982
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
799
983
  isOther: false,
800
984
  row: item.row,
801
985
  }))
@@ -868,7 +1052,7 @@ interface BarListRowsConfig extends BarListOptions {
868
1052
  */
869
1053
  function barListRows<Row extends object>(
870
1054
  data: readonly Row[],
871
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1055
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
872
1056
  ): BarListRow<Row>[] {
873
1057
  const items = data.map((row): BarListRow<Row> => {
874
1058
  const cells = row as Record<string, unknown>
@@ -914,19 +1098,21 @@ const BAR_LIST_OTHER_TINT = 30
914
1098
 
915
1099
  /**
916
1100
  * The fill of each BarList row: all real rows in the same tint of `color`
917
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1101
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
918
1102
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
919
1103
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
920
1104
  * for that row. A tint, not the solid token, so the label printed over the bar
921
1105
  * keeps its contrast.
922
1106
  */
923
1107
  function barListFills(
924
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1108
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
925
1109
  color: ChartColor,
926
1110
  otherColor: string,
927
1111
  ): string[] {
928
1112
  return rows.map((row) =>
929
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1113
+ row.isOther || isOtherName(row.name)
1114
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1115
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
930
1116
  )
931
1117
  }
932
1118
 
@@ -1142,11 +1328,30 @@ interface ChartSummaryInput {
1142
1328
  format?: ValueFormat
1143
1329
  /** Wins over `format`. */
1144
1330
  valueFormatter?: ValueFormatter
1331
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1332
+ locale?: string
1333
+ }
1334
+
1335
+ const SUMMARY_TEXT = {
1336
+ ja: {
1337
+ up: "、前の点より増加",
1338
+ down: "、前の点より減少",
1339
+ flat: "、前の点と横ばい",
1340
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1341
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1342
+ },
1343
+ en: {
1344
+ up: ", up from the previous point",
1345
+ down: ", down from the previous point",
1346
+ flat: ", flat from the previous point",
1347
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1348
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1349
+ },
1145
1350
  }
1146
1351
 
1147
1352
  /**
1148
1353
  * One-sentence summary built from the rows, for `summary="auto"`:
1149
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1354
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1150
1355
  * computed from the last two non-empty points of the first series — never
1151
1356
  * written by hand, so it cannot contradict the data. `undefined` when there is
1152
1357
  * no usable point.
@@ -1162,30 +1367,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1162
1367
  }
1163
1368
  const latest = points[points.length - 1]
1164
1369
  if (!latest) return undefined
1165
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1370
+ const locale = resolveLocale(input.locale)
1371
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1372
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1166
1373
  const prev = points[points.length - 2]
1167
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1374
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1168
1375
  const head = input.title ? `${input.title}: ` : ""
1169
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1376
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1170
1377
  }
1171
1378
 
1172
1379
  /**
1173
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1174
- * (largest first). `undefined` when there is nothing to describe.
1380
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1381
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1175
1382
  */
1176
1383
  function buildShareSummary(
1177
1384
  slices: readonly { name: string; value: number }[],
1178
1385
  title?: string,
1386
+ locale?: string,
1179
1387
  ): string | undefined {
1180
1388
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1181
1389
  const top = slices[0]
1182
1390
  if (!top || total <= 0) return undefined
1183
1391
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1184
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1392
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1185
1393
  }
1186
1394
 
1187
1395
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1188
1396
 
1397
+ /** The built-in words of the option builders, by language. */
1398
+ const CHART_TEXT = {
1399
+ ja: {
1400
+ target: "目標",
1401
+ forecast: "予測",
1402
+ total: "合計",
1403
+ provisional: "速報",
1404
+ noComparison: "比較なし",
1405
+ share: (share: string) => `(${share})`,
1406
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1407
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1408
+ },
1409
+ en: {
1410
+ target: "Target",
1411
+ forecast: "Forecast",
1412
+ total: "Total",
1413
+ provisional: "Preliminary",
1414
+ noComparison: "No comparison",
1415
+ share: (share: string) => ` (${share})`,
1416
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1417
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1418
+ },
1419
+ } satisfies Record<UiLanguage, unknown>
1420
+
1421
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1422
+
1189
1423
  type CartesianChartKind = "area" | "bar" | "line"
1190
1424
  type CartesianStackType = "default" | "stacked" | "percent"
1191
1425
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1370,7 +1604,7 @@ interface CartesianChartConfig {
1370
1604
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1371
1605
  */
1372
1606
  axisTicks?: (top: number) => (value: number) => string
1373
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1607
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1374
1608
  locale?: string
1375
1609
  type?: CartesianStackType
1376
1610
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1608,11 +1842,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1608
1842
  */
1609
1843
  function timeAxisLabels(
1610
1844
  values: readonly unknown[],
1611
- locale: string = DEFAULT_LOCALE,
1845
+ localeArg?: string,
1612
1846
  shown: (i: number) => boolean = () => true,
1613
1847
  ): string[] | null {
1614
1848
  const parsed = parseTimeIndex(values)
1615
1849
  if (!parsed) return null
1850
+ const locale = resolveLocale(localeArg)
1616
1851
  const { granularity, times } = parsed
1617
1852
  let prevYear: number | null = null
1618
1853
  return times.map((t, i) => {
@@ -1635,9 +1870,10 @@ function timeAxisLabels(
1635
1870
  }
1636
1871
 
1637
1872
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1638
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1873
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1639
1874
  const parsed = parseTimeIndex(values)
1640
1875
  if (!parsed) return null
1876
+ const locale = resolveLocale(localeArg)
1641
1877
  const options: Intl.DateTimeFormatOptions =
1642
1878
  parsed.granularity === "year"
1643
1879
  ? { year: "numeric" }
@@ -1777,7 +2013,9 @@ function tooltipHtml(
1777
2013
  rows: readonly TooltipRow[],
1778
2014
  valueFormatter: (value: number) => string,
1779
2015
  notes: readonly string[] = [],
2016
+ locale?: string,
1780
2017
  ): string {
2018
+ const t = chartText(locale)
1781
2019
  const lines = rows.map(
1782
2020
  (row) =>
1783
2021
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1786,7 +2024,7 @@ function tooltipHtml(
1786
2024
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1787
2025
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1788
2026
  }${
1789
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2027
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1790
2028
  }</span></div>` +
1791
2029
  (row.note === undefined
1792
2030
  ? ""
@@ -1800,8 +2038,10 @@ function tooltipHtml(
1800
2038
 
1801
2039
  /* ------------------------------------------------- per-bar targets / notes */
1802
2040
 
1803
- /** Default name of the per-bar target marker (legend, tooltip). */
1804
- const BAR_TARGET_LABEL = "目標"
2041
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2042
+ function barTargetLabel(locale?: string): string {
2043
+ return chartText(locale).target
2044
+ }
1805
2045
 
1806
2046
  /** How far (px) the target marker reaches past each side of its bar. */
1807
2047
  const TARGET_MARKER_OVERHANG = 4
@@ -1852,16 +2092,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1852
2092
  return (value / target) * 100
1853
2093
  }
1854
2094
 
1855
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2095
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1856
2096
  function barTargetNote(
1857
2097
  label: string,
1858
2098
  value: number | null,
1859
2099
  target: number | null,
1860
2100
  valueFormatter: (value: number) => string,
2101
+ locale?: string,
1861
2102
  ): string | null {
1862
2103
  if (target === null) return null
1863
2104
  const rate = achievementRate(value, target)
1864
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2105
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1865
2106
  }
1866
2107
 
1867
2108
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1953,11 +2194,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1953
2194
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1954
2195
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1955
2196
 
1956
- const DEFAULT_FORECAST_LABEL = "予測"
1957
-
1958
- /** The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side. */
1959
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1960
- return `${label} →`
2197
+ /**
2198
+ * The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side.
2199
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2200
+ */
2201
+ function forecastBoundaryText(label?: string, locale?: string): string {
2202
+ return `${label ?? chartText(locale).forecast} →`
1961
2203
  }
1962
2204
 
1963
2205
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1997,8 +2239,10 @@ interface AxisExtent {
1997
2239
  max: number
1998
2240
  }
1999
2241
 
2000
- /** The label of a stack's total (highlight and tooltip). */
2001
- const STACK_TOTAL_LABEL = "合計"
2242
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2243
+ function stackTotalLabel(locale?: string): string {
2244
+ return chartText(locale).total
2245
+ }
2002
2246
 
2003
2247
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
2004
2248
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2120,8 +2364,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2120
2364
 
2121
2365
  /**
2122
2366
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2123
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2124
- * either value is missing or the baseline is 0.
2367
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2368
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2125
2369
  */
2126
2370
  function comparisonNote(
2127
2371
  current: number | null,
@@ -2129,16 +2373,18 @@ function comparisonNote(
2129
2373
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2130
2374
  grain?: PeriodGrain,
2131
2375
  format?: ValueFormat,
2132
- locale: string = DEFAULT_LOCALE,
2376
+ localeArg?: string,
2133
2377
  ): string {
2378
+ const locale = resolveLocale(localeArg)
2134
2379
  const result = computeComparison(
2135
2380
  current,
2136
2381
  comparison.vs === "custom"
2137
2382
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2138
2383
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2384
+ { locale },
2139
2385
  )
2140
2386
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2141
- if (text === null) return `比較なし ${result.label}`
2387
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2142
2388
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2143
2389
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2144
2390
  }
@@ -2181,7 +2427,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2181
2427
  toneColors,
2182
2428
  valueFormatter,
2183
2429
  axisFormatter = valueFormatter,
2184
- locale = DEFAULT_LOCALE,
2185
2430
  type = "default",
2186
2431
  layout = "horizontal",
2187
2432
  curveType = "linear",
@@ -2218,7 +2463,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2218
2463
  chartOptions,
2219
2464
  reserveActions = false,
2220
2465
  } = cfg
2221
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2466
+ const locale = resolveLocale(cfg.locale)
2467
+ const text = chartText(locale)
2468
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2222
2469
 
2223
2470
  const stacked = kind !== "line" && type !== "default"
2224
2471
  const percent = kind !== "line" && type === "percent"
@@ -2367,9 +2614,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2367
2614
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2368
2615
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2369
2616
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2370
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2617
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2371
2618
  const highlightText = (value: number) =>
2372
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2619
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2373
2620
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2374
2621
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2375
2622
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2395,7 +2642,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2395
2642
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2396
2643
  const barTargets: (number | null)[] | null =
2397
2644
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2398
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2645
+ const barTargetName = targetLabel ?? text.target
2399
2646
  const barTargetColor = labelColor ?? guideColor
2400
2647
 
2401
2648
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2609,7 +2856,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2609
2856
  }
2610
2857
 
2611
2858
  /* -- forecast: dashed continuation (+ optional band) of one series */
2612
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2859
+ const forecastLabel = forecast?.label ?? text.forecast
2613
2860
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2614
2861
  const forecastValues: (number | null)[] = []
2615
2862
  if (forecast) {
@@ -3085,7 +3332,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3085
3332
  })
3086
3333
  }
3087
3334
  if (stackTotals && i <= lastActual && rows.length > 1) {
3088
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3335
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3089
3336
  }
3090
3337
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3091
3338
  if (provisional) {
@@ -3101,19 +3348,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3101
3348
  ...noteFor(at),
3102
3349
  })
3103
3350
  })
3104
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3351
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3105
3352
  }
3106
3353
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3107
3354
  const lines: string[] = []
3108
3355
  if (barTargets && i <= lastActual) {
3109
3356
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3110
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3357
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3111
3358
  if (note) lines.push(note)
3112
3359
  }
3113
3360
  for (const note of notes) {
3114
3361
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3115
3362
  }
3116
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3363
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3117
3364
  },
3118
3365
  },
3119
3366
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3205,8 +3452,6 @@ interface DonutOptionSlice {
3205
3452
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3206
3453
  const DONUT_OUTSIDE_LABELS = 2
3207
3454
 
3208
- const DEFAULT_TOTAL_LABEL = "合計"
3209
-
3210
3455
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3211
3456
 
3212
3457
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3239,15 +3484,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3239
3484
  }
3240
3485
 
3241
3486
  /**
3242
- * The default center label: what is totalled (「合計」), the formatted total
3487
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3243
3488
  * (`amount`, the big line), the unit (a small line under it) and both as one
3244
3489
  * string (`value`, 「9.2万 セッション」).
3245
3490
  */
3246
3491
  function donutCenterText(
3247
3492
  total: number,
3248
3493
  valueFormatter: (value: number) => string,
3249
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3494
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3250
3495
  ): { label: string; value: string; amount: string; unit?: string } {
3496
+ totalLabel ??= chartText(locale).total
3251
3497
  const amount = valueFormatter(total)
3252
3498
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3253
3499
  }
@@ -3475,8 +3721,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3475
3721
 
3476
3722
  /* ------------------------------------------------------------ provisional */
3477
3723
 
3478
- /** The default label for points that are not final yet. */
3479
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3724
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3725
+ function defaultProvisionalLabel(locale?: string): string {
3726
+ return chartText(locale).provisional
3727
+ }
3480
3728
 
3481
3729
  /**
3482
3730
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3532,6 +3780,11 @@ export interface AreaChartProps<T extends Record<string, unknown> = Record<strin
3532
3780
  connectNulls?: boolean
3533
3781
  }
3534
3782
 
3783
+ const TEXT = {
3784
+ ja: { item: "項目", chart: (categories: readonly string[]) => `${categories.join("・")}の面グラフ` },
3785
+ en: { item: "Item", chart: (categories: readonly string[]) => `Area chart of ${categories.join(", ")}` },
3786
+ }
3787
+
3535
3788
  /** Rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
3536
3789
  const NO_ROWS: never[] = []
3537
3790
 
@@ -3586,15 +3839,20 @@ function AreaChartImpl<T extends Record<string, unknown>>(
3586
3839
  ) {
3587
3840
  // Highlight labels are backed by the surface the chart sits on (a card, the page).
3588
3841
  const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
3842
+ const locale = useLocale()
3843
+ const t = TEXT[uiLanguage(locale)]
3589
3844
  // Ticks and on-plot labels read short (¥1億); the tooltip reads the value as `format` says.
3590
- const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format), [valueFormatter, format])
3591
- const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format), [valueFormatter, format])
3845
+ const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3846
+ const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3592
3847
  // Ticks share one unit — the largest tick's (¥0.5億 / ¥1億).
3593
- const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format), [valueFormatter, format])
3848
+ const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3594
3849
  // The comparison series is kept out of the「その他」fold and out of the `--chart-*` order.
3595
3850
  const comparisonCategory = comparison && categories.includes(comparison.category) ? comparison.category : undefined
3596
3851
  const comparisonConfig = comparisonCategory === undefined ? undefined : comparison
3597
- const series = useMemo(() => aggregateSeriesKeeping(data, categories, comparisonCategory), [data, categories, comparisonCategory])
3852
+ const series = useMemo(
3853
+ () => aggregateSeriesKeeping(data, categories, comparisonCategory, undefined, otherSeriesLabel(locale)),
3854
+ [data, categories, comparisonCategory, locale],
3855
+ )
3598
3856
  const palette = useMemo(
3599
3857
  () =>
3600
3858
  resolveChartColors(
@@ -3653,24 +3911,25 @@ function AreaChartImpl<T extends Record<string, unknown>>(
3653
3911
  plotWidth,
3654
3912
  plotHeight: typeof height === "number" ? height : undefined,
3655
3913
  chartOptions,
3914
+ locale,
3656
3915
  }),
3657
3916
  [
3658
3917
  series, index, palette, labels, tooltipFormatter, axisFormatter, axisTicks, stackType, curve, areaFill, joinNulls,
3659
3918
  showLegend, showTooltip, showGridLines, showXAxis, showYAxis, startEndOnly, yAxisWidth,
3660
3919
  maxValue, allowDecimals, xAxisLabel, yAxisLabel, onValueChange, active,
3661
- provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, annotations, forecast, comparisonConfig, format, height, tokens, surface, plotWidth, chartOptions,
3920
+ provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, annotations, forecast, comparisonConfig, format, height, tokens, surface, plotWidth, chartOptions, locale,
3662
3921
  ],
3663
3922
  )
3664
3923
  // The screen-reader table: exact values (tooltip format), dates read as dates, 「その他」 as its own column.
3665
3924
  const dataTable = useMemo(() => {
3666
- const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, "項目")
3667
- const times = timeTooltipLabels(series.data.map((row) => row[index]))
3925
+ const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, t.item)
3926
+ const times = timeTooltipLabels(series.data.map((row) => row[index]), locale)
3668
3927
  return times ? { ...table, rows: table.rows.map((row, i) => [times[i] ?? row[0] ?? "", ...row.slice(1)]) } : table
3669
- }, [series, index, tooltipFormatter, labels])
3670
- const label = ariaLabel ?? `${categories.join("・")}の面グラフ`
3928
+ }, [series, index, tooltipFormatter, labels, t, locale])
3929
+ const label = ariaLabel ?? t.chart(categories)
3671
3930
  const resolvedSummary = useMemo(
3672
- () => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter }) : summary),
3673
- [summary, data, index, categories, title, format, valueFormatter],
3931
+ () => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter, locale }) : summary),
3932
+ [summary, data, index, categories, title, format, valueFormatter, locale],
3674
3933
  )
3675
3934
  return (
3676
3935
  <ChartFrame