@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
@@ -17,14 +17,147 @@ import { ChartFrame } from "@squadbase/vantage/components"
17
17
  import { EChart } from "@squadbase/vantage/components"
18
18
  import type { ChartCommonProps, ChartColors, NumericKeys, ValueFormatter } from "@squadbase/vantage/components"
19
19
  import type { ValueFormat } from "@squadbase/vantage/components"
20
+ import { useSyncExternalStore } from "react"
20
21
  import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
21
22
  import type { ChartTone } from "@squadbase/vantage/components"
22
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
23
+ import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
23
24
  import type { ChartColor, ChartSeriesColor } from "@squadbase/vantage/components"
24
25
  import { type ReactNode } from "react"
25
26
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
27
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
26
28
  import type { ChartThreshold, ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight } from "@squadbase/vantage/components"
27
29
 
30
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
31
+
32
+ /**
33
+ * The display language of the parts and the defaults that follow it.
34
+ *
35
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
36
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
37
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
38
+ *
39
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
40
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
41
+ */
42
+
43
+ /** The language of the built-in wording: Japanese or English. */
44
+ type UiLanguage = "ja" | "en"
45
+
46
+ /** The locale used when `<html lang>` gives none (and on the server). */
47
+ const FALLBACK_LOCALE = "en-US"
48
+
49
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
50
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
51
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
52
+ }
53
+
54
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
55
+ function normalizeLocale(tag: string | null | undefined): string {
56
+ const value = tag?.trim()
57
+ if (!value) return FALLBACK_LOCALE
58
+ try {
59
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
60
+ } catch {
61
+ return FALLBACK_LOCALE
62
+ }
63
+ }
64
+
65
+ /**
66
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
67
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
68
+ */
69
+ function documentLocale(): string {
70
+ if (typeof document === "undefined") return FALLBACK_LOCALE
71
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
72
+ }
73
+
74
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
75
+ function resolveLocale(locale?: string | null): string {
76
+ return locale || documentLocale()
77
+ }
78
+
79
+ /** Picks the wording table of the locale's language. */
80
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
81
+ return table[uiLanguage(resolveLocale(locale))]
82
+ }
83
+
84
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
85
+ function defaultCurrency(locale?: string | null): string {
86
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
87
+ }
88
+
89
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
90
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
91
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
92
+ }
93
+
94
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
95
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
96
+
97
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
98
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
99
+
100
+ /**
101
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
102
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
103
+ * Monday for the rest.
104
+ */
105
+ function weekStartsOn(locale?: string | null): WeekDay {
106
+ const tag = normalizeLocale(resolveLocale(locale))
107
+ if (uiLanguage(tag) === "ja") return 1
108
+ try {
109
+ const info = new Intl.Locale(tag) as Intl.Locale & {
110
+ getWeekInfo?: () => { firstDay: number }
111
+ weekInfo?: { firstDay: number }
112
+ }
113
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
114
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
115
+ } catch {
116
+ // Fall through to the table below.
117
+ }
118
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
119
+ }
120
+
121
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
122
+
123
+ const listeners = new Set<() => void>()
124
+ let observer: MutationObserver | null = null
125
+
126
+ /** One observer on `<html lang>` for every subscribed part. */
127
+ function subscribe(onChange: () => void): () => void {
128
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
129
+ listeners.add(onChange)
130
+ if (!observer) {
131
+ observer = new MutationObserver(() => {
132
+ for (const listener of [...listeners]) listener()
133
+ })
134
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
135
+ }
136
+ return () => {
137
+ listeners.delete(onChange)
138
+ if (listeners.size === 0 && observer) {
139
+ observer.disconnect()
140
+ observer = null
141
+ }
142
+ }
143
+ }
144
+
145
+ const serverLocale = () => FALLBACK_LOCALE
146
+
147
+ /**
148
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
149
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
150
+ */
151
+ function useDocumentLocale(): string {
152
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
153
+ }
154
+
155
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
156
+ function useLocale(locale?: string | null): string {
157
+ const page = useDocumentLocale()
158
+ return locale || page
159
+ }
160
+
28
161
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
29
162
 
30
163
  /**
@@ -528,9 +661,10 @@ function resolveValueFormatter(
528
661
  function resolveAxisFormatter(
529
662
  valueFormatter: ValueFormatter | undefined,
530
663
  format: ValueFormat | undefined,
531
- locale: string = DEFAULT_LOCALE,
664
+ localeArg?: string,
532
665
  ): ValueFormatter {
533
666
  if (valueFormatter) return valueFormatter
667
+ const locale = resolveLocale(localeArg)
534
668
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
535
669
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
536
670
  const currency = "currency" in spec ? spec.currency : undefined
@@ -539,40 +673,75 @@ function resolveAxisFormatter(
539
673
  return (value) => formatValue(value, short, locale)
540
674
  }
541
675
 
542
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
543
- const JA_UNITS = [
544
- { char: "兆", size: 1e12 },
545
- { char: "億", size: 1e8 },
546
- { char: "万", size: 1e4 },
547
- ] as const
676
+ interface CompactUnit {
677
+ token: string
678
+ size: number
679
+ }
548
680
 
549
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
681
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
682
+
683
+ /**
684
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
685
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
686
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
687
+ */
688
+ function compactUnits(locale: string): CompactUnit[] | null {
689
+ const cached = compactUnitCache.get(locale)
690
+ if (cached !== undefined) return cached
691
+ let units: CompactUnit[] | null = []
692
+ try {
693
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
694
+ const seen = new Set<string>()
695
+ for (let exp = 3; exp <= 15; exp++) {
696
+ const parts = compact.formatToParts(10 ** exp)
697
+ const at = parts.findIndex((p) => p.type === "compact")
698
+ if (at === -1) continue
699
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
700
+ units = null
701
+ break
702
+ }
703
+ const token = parts[at]!.value
704
+ if (seen.has(token)) continue
705
+ seen.add(token)
706
+ units.push({ token, size: 10 ** exp })
707
+ }
708
+ } catch {
709
+ units = null
710
+ }
711
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
712
+ compactUnitCache.set(locale, sorted)
713
+ return sorted
714
+ }
715
+
716
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
550
717
 
551
718
  /**
552
719
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
553
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
554
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
555
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
720
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
721
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
722
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
723
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
556
724
  */
557
725
  function resolveAxisTickFormatter(
558
726
  valueFormatter: ValueFormatter | undefined,
559
727
  format: ValueFormat | undefined,
560
- locale: string = DEFAULT_LOCALE,
728
+ localeArg?: string,
561
729
  ): (top: number) => ValueFormatter {
730
+ const locale = resolveLocale(localeArg)
562
731
  const base = resolveAxisFormatter(valueFormatter, format, locale)
563
732
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
564
- const aligns =
565
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
566
- if (!aligns) return () => base
733
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
734
+ if (!units) return () => base
567
735
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
736
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
568
737
  return (top) => {
569
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
738
+ const unit = units.find((u) => Math.abs(top) >= u.size)
570
739
  if (!unit) return base
571
740
  return (value) => {
572
741
  const text = base(value)
573
742
  if (value === 0 || !Number.isFinite(value)) return text
574
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
575
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
743
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
744
+ return text.replace(numberToken, () => scaled)
576
745
  }
577
746
  }
578
747
  }
@@ -601,8 +770,20 @@ function clamp(value: number, min: number, max: number): number {
601
770
  /** How many categories a chart colours individually; the rest fold into「その他」. */
602
771
  const MAX_CHART_SERIES = 5
603
772
 
604
- /** The series name the folded categories get. */
605
- const OTHER_SERIES_LABEL = "その他"
773
+ /** The series name the folded categories get, by language. */
774
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
775
+
776
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
777
+ function otherSeriesLabel(locale?: string): string {
778
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
779
+ }
780
+
781
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
782
+
783
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
784
+ function isOtherName(name: string | null | undefined): boolean {
785
+ return typeof name === "string" && OTHER_NAMES.has(name)
786
+ }
606
787
 
607
788
  interface AggregatedItems<Item extends { name: string; value: number }> {
608
789
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -622,7 +803,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
622
803
  function aggregateTopN<Item extends { name: string; value: number }>(
623
804
  items: readonly Item[],
624
805
  n: number = MAX_CHART_SERIES,
625
- otherLabel: string = OTHER_SERIES_LABEL,
806
+ otherLabel: string = otherSeriesLabel(),
626
807
  ): AggregatedItems<Item> {
627
808
  if (items.length <= n) return { top: [...items], rest: [], other: null }
628
809
  const ranked = items
@@ -651,7 +832,7 @@ function aggregateSeries(
651
832
  data: readonly Record<string, unknown>[],
652
833
  categories: readonly string[],
653
834
  n: number = MAX_CHART_SERIES,
654
- otherLabel: string = OTHER_SERIES_LABEL,
835
+ otherLabel: string = otherSeriesLabel(),
655
836
  ): AggregatedSeries {
656
837
  const totals = categories.map((name) => ({
657
838
  name,
@@ -683,12 +864,14 @@ function aggregateSeriesKeeping(
683
864
  categories: readonly string[],
684
865
  keep: string | undefined,
685
866
  n: number = MAX_CHART_SERIES,
867
+ otherLabel: string = otherSeriesLabel(),
686
868
  ): AggregatedSeries {
687
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
869
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
688
870
  const aggregated = aggregateSeries(
689
871
  data,
690
872
  categories.filter((c) => c !== keep),
691
873
  n,
874
+ otherLabel,
692
875
  )
693
876
  return { ...aggregated, categories: [...aggregated.categories, keep] }
694
877
  }
@@ -724,7 +907,7 @@ function seriesColorsWithComparison(
724
907
  * Colour per drawn series: the record's token for that category, else the
725
908
  * array's token at the category's position in `allCategories` (cycled), else
726
909
  * the token palette by drawn position. `otherCategory` — and a category the
727
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
910
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
728
911
  */
729
912
  function seriesColorsFor(
730
913
  categories: readonly string[],
@@ -736,7 +919,7 @@ function seriesColorsFor(
736
919
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
737
920
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
738
921
  return categories.map((category, i) => {
739
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
922
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
740
923
  const fromRecord = record?.[category]
741
924
  if (fromRecord) return fromRecord
742
925
  if (list && list.length > 0) {
@@ -783,7 +966,7 @@ interface DonutSlicesConfig {
783
966
  */
784
967
  function donutSlices(
785
968
  data: readonly Record<string, unknown>[],
786
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
969
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
787
970
  ): DonutSlice[] {
788
971
  const items = data
789
972
  .map((row, i) => {
@@ -796,7 +979,8 @@ function donutSlices(
796
979
  const slices: DonutSlice[] = top.map((item, rank) => ({
797
980
  name: item.name,
798
981
  value: item.value,
799
- color: palette[rank % palette.length]!,
982
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
983
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
800
984
  isOther: false,
801
985
  row: item.row,
802
986
  }))
@@ -869,7 +1053,7 @@ interface BarListRowsConfig extends BarListOptions {
869
1053
  */
870
1054
  function barListRows<Row extends object>(
871
1055
  data: readonly Row[],
872
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1056
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
873
1057
  ): BarListRow<Row>[] {
874
1058
  const items = data.map((row): BarListRow<Row> => {
875
1059
  const cells = row as Record<string, unknown>
@@ -915,19 +1099,21 @@ const BAR_LIST_OTHER_TINT = 30
915
1099
 
916
1100
  /**
917
1101
  * The fill of each BarList row: all real rows in the same tint of `color`
918
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1102
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
919
1103
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
920
1104
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
921
1105
  * for that row. A tint, not the solid token, so the label printed over the bar
922
1106
  * keeps its contrast.
923
1107
  */
924
1108
  function barListFills(
925
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1109
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
926
1110
  color: ChartColor,
927
1111
  otherColor: string,
928
1112
  ): string[] {
929
1113
  return rows.map((row) =>
930
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1114
+ row.isOther || isOtherName(row.name)
1115
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1116
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
931
1117
  )
932
1118
  }
933
1119
 
@@ -1143,11 +1329,30 @@ interface ChartSummaryInput {
1143
1329
  format?: ValueFormat
1144
1330
  /** Wins over `format`. */
1145
1331
  valueFormatter?: ValueFormatter
1332
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1333
+ locale?: string
1334
+ }
1335
+
1336
+ const SUMMARY_TEXT = {
1337
+ ja: {
1338
+ up: "、前の点より増加",
1339
+ down: "、前の点より減少",
1340
+ flat: "、前の点と横ばい",
1341
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1342
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1343
+ },
1344
+ en: {
1345
+ up: ", up from the previous point",
1346
+ down: ", down from the previous point",
1347
+ flat: ", flat from the previous point",
1348
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1349
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1350
+ },
1146
1351
  }
1147
1352
 
1148
1353
  /**
1149
1354
  * One-sentence summary built from the rows, for `summary="auto"`:
1150
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1355
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1151
1356
  * computed from the last two non-empty points of the first series — never
1152
1357
  * written by hand, so it cannot contradict the data. `undefined` when there is
1153
1358
  * no usable point.
@@ -1163,30 +1368,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1163
1368
  }
1164
1369
  const latest = points[points.length - 1]
1165
1370
  if (!latest) return undefined
1166
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1371
+ const locale = resolveLocale(input.locale)
1372
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1373
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1167
1374
  const prev = points[points.length - 2]
1168
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1375
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1169
1376
  const head = input.title ? `${input.title}: ` : ""
1170
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1377
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1171
1378
  }
1172
1379
 
1173
1380
  /**
1174
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1175
- * (largest first). `undefined` when there is nothing to describe.
1381
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1382
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1176
1383
  */
1177
1384
  function buildShareSummary(
1178
1385
  slices: readonly { name: string; value: number }[],
1179
1386
  title?: string,
1387
+ locale?: string,
1180
1388
  ): string | undefined {
1181
1389
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1182
1390
  const top = slices[0]
1183
1391
  if (!top || total <= 0) return undefined
1184
1392
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1185
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1393
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1186
1394
  }
1187
1395
 
1188
1396
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1189
1397
 
1398
+ /** The built-in words of the option builders, by language. */
1399
+ const CHART_TEXT = {
1400
+ ja: {
1401
+ target: "目標",
1402
+ forecast: "予測",
1403
+ total: "合計",
1404
+ provisional: "速報",
1405
+ noComparison: "比較なし",
1406
+ share: (share: string) => `(${share})`,
1407
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1408
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1409
+ },
1410
+ en: {
1411
+ target: "Target",
1412
+ forecast: "Forecast",
1413
+ total: "Total",
1414
+ provisional: "Preliminary",
1415
+ noComparison: "No comparison",
1416
+ share: (share: string) => ` (${share})`,
1417
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1418
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1419
+ },
1420
+ } satisfies Record<UiLanguage, unknown>
1421
+
1422
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1423
+
1190
1424
  type CartesianChartKind = "area" | "bar" | "line"
1191
1425
  type CartesianStackType = "default" | "stacked" | "percent"
1192
1426
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1371,7 +1605,7 @@ interface CartesianChartConfig {
1371
1605
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1372
1606
  */
1373
1607
  axisTicks?: (top: number) => (value: number) => string
1374
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1608
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1375
1609
  locale?: string
1376
1610
  type?: CartesianStackType
1377
1611
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1609,11 +1843,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1609
1843
  */
1610
1844
  function timeAxisLabels(
1611
1845
  values: readonly unknown[],
1612
- locale: string = DEFAULT_LOCALE,
1846
+ localeArg?: string,
1613
1847
  shown: (i: number) => boolean = () => true,
1614
1848
  ): string[] | null {
1615
1849
  const parsed = parseTimeIndex(values)
1616
1850
  if (!parsed) return null
1851
+ const locale = resolveLocale(localeArg)
1617
1852
  const { granularity, times } = parsed
1618
1853
  let prevYear: number | null = null
1619
1854
  return times.map((t, i) => {
@@ -1636,9 +1871,10 @@ function timeAxisLabels(
1636
1871
  }
1637
1872
 
1638
1873
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1639
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1874
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1640
1875
  const parsed = parseTimeIndex(values)
1641
1876
  if (!parsed) return null
1877
+ const locale = resolveLocale(localeArg)
1642
1878
  const options: Intl.DateTimeFormatOptions =
1643
1879
  parsed.granularity === "year"
1644
1880
  ? { year: "numeric" }
@@ -1778,7 +2014,9 @@ function tooltipHtml(
1778
2014
  rows: readonly TooltipRow[],
1779
2015
  valueFormatter: (value: number) => string,
1780
2016
  notes: readonly string[] = [],
2017
+ locale?: string,
1781
2018
  ): string {
2019
+ const t = chartText(locale)
1782
2020
  const lines = rows.map(
1783
2021
  (row) =>
1784
2022
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1787,7 +2025,7 @@ function tooltipHtml(
1787
2025
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1788
2026
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1789
2027
  }${
1790
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2028
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1791
2029
  }</span></div>` +
1792
2030
  (row.note === undefined
1793
2031
  ? ""
@@ -1801,8 +2039,10 @@ function tooltipHtml(
1801
2039
 
1802
2040
  /* ------------------------------------------------- per-bar targets / notes */
1803
2041
 
1804
- /** Default name of the per-bar target marker (legend, tooltip). */
1805
- const BAR_TARGET_LABEL = "目標"
2042
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2043
+ function barTargetLabel(locale?: string): string {
2044
+ return chartText(locale).target
2045
+ }
1806
2046
 
1807
2047
  /** How far (px) the target marker reaches past each side of its bar. */
1808
2048
  const TARGET_MARKER_OVERHANG = 4
@@ -1853,16 +2093,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1853
2093
  return (value / target) * 100
1854
2094
  }
1855
2095
 
1856
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2096
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1857
2097
  function barTargetNote(
1858
2098
  label: string,
1859
2099
  value: number | null,
1860
2100
  target: number | null,
1861
2101
  valueFormatter: (value: number) => string,
2102
+ locale?: string,
1862
2103
  ): string | null {
1863
2104
  if (target === null) return null
1864
2105
  const rate = achievementRate(value, target)
1865
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2106
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1866
2107
  }
1867
2108
 
1868
2109
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1954,11 +2195,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1954
2195
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1955
2196
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1956
2197
 
1957
- const DEFAULT_FORECAST_LABEL = "予測"
1958
-
1959
- /** 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. */
1960
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1961
- return `${label} →`
2198
+ /**
2199
+ * The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side.
2200
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2201
+ */
2202
+ function forecastBoundaryText(label?: string, locale?: string): string {
2203
+ return `${label ?? chartText(locale).forecast} →`
1962
2204
  }
1963
2205
 
1964
2206
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1998,8 +2240,10 @@ interface AxisExtent {
1998
2240
  max: number
1999
2241
  }
2000
2242
 
2001
- /** The label of a stack's total (highlight and tooltip). */
2002
- const STACK_TOTAL_LABEL = "合計"
2243
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2244
+ function stackTotalLabel(locale?: string): string {
2245
+ return chartText(locale).total
2246
+ }
2003
2247
 
2004
2248
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
2005
2249
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2121,8 +2365,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2121
2365
 
2122
2366
  /**
2123
2367
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2124
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2125
- * either value is missing or the baseline is 0.
2368
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2369
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2126
2370
  */
2127
2371
  function comparisonNote(
2128
2372
  current: number | null,
@@ -2130,16 +2374,18 @@ function comparisonNote(
2130
2374
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2131
2375
  grain?: PeriodGrain,
2132
2376
  format?: ValueFormat,
2133
- locale: string = DEFAULT_LOCALE,
2377
+ localeArg?: string,
2134
2378
  ): string {
2379
+ const locale = resolveLocale(localeArg)
2135
2380
  const result = computeComparison(
2136
2381
  current,
2137
2382
  comparison.vs === "custom"
2138
2383
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2139
2384
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2385
+ { locale },
2140
2386
  )
2141
2387
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2142
- if (text === null) return `比較なし ${result.label}`
2388
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2143
2389
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2144
2390
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2145
2391
  }
@@ -2182,7 +2428,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2182
2428
  toneColors,
2183
2429
  valueFormatter,
2184
2430
  axisFormatter = valueFormatter,
2185
- locale = DEFAULT_LOCALE,
2186
2431
  type = "default",
2187
2432
  layout = "horizontal",
2188
2433
  curveType = "linear",
@@ -2219,7 +2464,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2219
2464
  chartOptions,
2220
2465
  reserveActions = false,
2221
2466
  } = cfg
2222
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2467
+ const locale = resolveLocale(cfg.locale)
2468
+ const text = chartText(locale)
2469
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2223
2470
 
2224
2471
  const stacked = kind !== "line" && type !== "default"
2225
2472
  const percent = kind !== "line" && type === "percent"
@@ -2368,9 +2615,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2368
2615
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2369
2616
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2370
2617
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2371
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2618
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2372
2619
  const highlightText = (value: number) =>
2373
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2620
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2374
2621
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2375
2622
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2376
2623
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2396,7 +2643,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2396
2643
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2397
2644
  const barTargets: (number | null)[] | null =
2398
2645
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2399
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2646
+ const barTargetName = targetLabel ?? text.target
2400
2647
  const barTargetColor = labelColor ?? guideColor
2401
2648
 
2402
2649
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2610,7 +2857,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2610
2857
  }
2611
2858
 
2612
2859
  /* -- forecast: dashed continuation (+ optional band) of one series */
2613
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2860
+ const forecastLabel = forecast?.label ?? text.forecast
2614
2861
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2615
2862
  const forecastValues: (number | null)[] = []
2616
2863
  if (forecast) {
@@ -3086,7 +3333,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3086
3333
  })
3087
3334
  }
3088
3335
  if (stackTotals && i <= lastActual && rows.length > 1) {
3089
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3336
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3090
3337
  }
3091
3338
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3092
3339
  if (provisional) {
@@ -3102,19 +3349,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3102
3349
  ...noteFor(at),
3103
3350
  })
3104
3351
  })
3105
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3352
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3106
3353
  }
3107
3354
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3108
3355
  const lines: string[] = []
3109
3356
  if (barTargets && i <= lastActual) {
3110
3357
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3111
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3358
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3112
3359
  if (note) lines.push(note)
3113
3360
  }
3114
3361
  for (const note of notes) {
3115
3362
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3116
3363
  }
3117
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3364
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3118
3365
  },
3119
3366
  },
3120
3367
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3206,8 +3453,6 @@ interface DonutOptionSlice {
3206
3453
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3207
3454
  const DONUT_OUTSIDE_LABELS = 2
3208
3455
 
3209
- const DEFAULT_TOTAL_LABEL = "合計"
3210
-
3211
3456
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3212
3457
 
3213
3458
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3240,15 +3485,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3240
3485
  }
3241
3486
 
3242
3487
  /**
3243
- * The default center label: what is totalled (「合計」), the formatted total
3488
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3244
3489
  * (`amount`, the big line), the unit (a small line under it) and both as one
3245
3490
  * string (`value`, 「9.2万 セッション」).
3246
3491
  */
3247
3492
  function donutCenterText(
3248
3493
  total: number,
3249
3494
  valueFormatter: (value: number) => string,
3250
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3495
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3251
3496
  ): { label: string; value: string; amount: string; unit?: string } {
3497
+ totalLabel ??= chartText(locale).total
3252
3498
  const amount = valueFormatter(total)
3253
3499
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3254
3500
  }
@@ -3476,8 +3722,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3476
3722
 
3477
3723
  /* ------------------------------------------------------------ provisional */
3478
3724
 
3479
- /** The default label for points that are not final yet. */
3480
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3725
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3726
+ function defaultProvisionalLabel(locale?: string): string {
3727
+ return chartText(locale).provisional
3728
+ }
3481
3729
 
3482
3730
  /**
3483
3731
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3510,6 +3758,11 @@ function splitProvisional(
3510
3758
 
3511
3759
  /* ---------- end of inlined code ---------- */
3512
3760
 
3761
+ const TEXT = {
3762
+ ja: { item: "項目", chart: (keys: readonly string[]) => `${keys.join("・")}の推移(上段 棒、下段 線)` },
3763
+ en: { item: "Item", chart: (keys: readonly string[]) => `Trend of ${keys.join(", ")} (bars on top, line below)` },
3764
+ }
3765
+
3513
3766
  /** 1 段ぶんの設定。 */
3514
3767
  export interface PairedPanel<T extends Record<string, unknown> = Record<string, unknown>> {
3515
3768
  /** この段に描く系列の列。 */
@@ -3599,6 +3852,8 @@ export interface PairedOptionConfig {
3599
3852
  plotWidth?: number
3600
3853
  guideColor?: string
3601
3854
  chartOptions?: EChartsOption
3855
+ /** 時間軸のラベル・ツールチップの言語。既定はページのロケール(`<html lang>`)。 */
3856
+ locale?: string
3602
3857
  }
3603
3858
 
3604
3859
  /** 棒と線の `buildCartesianOption` を 1 つの option(2 つの grid・共有の横軸・連動するツールチップ)に合成する。 */
@@ -3621,6 +3876,7 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
3621
3876
  highlight: "none" as const,
3622
3877
  yAxisWidth,
3623
3878
  reducedMotion: false,
3879
+ locale: cfg.locale,
3624
3880
  }
3625
3881
  const barOpt = buildCartesianOption({
3626
3882
  ...common,
@@ -3655,7 +3911,7 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
3655
3911
  plotHeight: room - upper,
3656
3912
  }) as AnyRecord
3657
3913
 
3658
- const times = timeTooltipLabels(data.map((row) => row[index]))
3914
+ const times = timeTooltipLabels(data.map((row) => row[index]), cfg.locale)
3659
3915
  const titles = labelRows(data, index, labels).map((row, i) => String(times?.[i] ?? row[index] ?? ""))
3660
3916
  const cell = (row: Record<string, unknown>, key: string): number | null => {
3661
3917
  const v = row[key]
@@ -3665,13 +3921,13 @@ export function buildPairedOption(cfg: PairedOptionConfig): EChartsOption {
3665
3921
  const rowsOf = (row: Record<string, unknown>, keys: readonly string[], palette: readonly string[]): TooltipRow[] =>
3666
3922
  keys.map((key, i) => ({ name: labelOf(key, labels), color: palette[i] || "currentColor", value: cell(row, key) }))
3667
3923
  // 1 枚のツールチップ: 見出しは 1 回、棒の行 → 線の行(段ごとの書式)。
3668
- const emptyTitle = tooltipHtml("", [], String)
3924
+ const emptyTitle = tooltipHtml("", [], String, [], cfg.locale)
3669
3925
  const tooltipFormatter = (params: unknown) => {
3670
3926
  const list = (Array.isArray(params) ? params : [params]) as { dataIndex?: number }[]
3671
3927
  const i = list[0]?.dataIndex ?? 0
3672
3928
  const row = data[i] ?? {}
3673
- const upperHtml = tooltipHtml(titles[i] ?? "", rowsOf(row, barKeys, cfg.barPalette), cfg.barFormatter)
3674
- const lowerHtml = tooltipHtml("", rowsOf(row, lineKeys, cfg.linePalette), cfg.lineFormatter).slice(emptyTitle.length)
3929
+ const upperHtml = tooltipHtml(titles[i] ?? "", rowsOf(row, barKeys, cfg.barPalette), cfg.barFormatter, [], cfg.locale)
3930
+ const lowerHtml = tooltipHtml("", rowsOf(row, lineKeys, cfg.linePalette), cfg.lineFormatter, [], cfg.locale).slice(emptyTitle.length)
3675
3931
  return `${upperHtml}${lowerHtml}`
3676
3932
  }
3677
3933
 
@@ -3737,12 +3993,14 @@ function PairedChartImpl<T extends Record<string, unknown>>(
3737
3993
  const { ref: frameRef, width: plotWidth, tokens } = useChartSurface(ref)
3738
3994
  const barKeys = (bars?.categories ?? NO_KEYS) as string[]
3739
3995
  const lineKeys = (line?.categories ?? NO_KEYS) as string[]
3740
- const barFormatter = useMemo(() => resolveValueFormatter(bars?.valueFormatter, bars?.format), [bars?.valueFormatter, bars?.format])
3741
- const lineFormatter = useMemo(() => resolveValueFormatter(line?.valueFormatter, line?.format), [line?.valueFormatter, line?.format])
3742
- const barAxis = useMemo(() => resolveAxisFormatter(bars?.valueFormatter, bars?.format), [bars?.valueFormatter, bars?.format])
3743
- const lineAxis = useMemo(() => resolveAxisFormatter(line?.valueFormatter, line?.format), [line?.valueFormatter, line?.format])
3744
- const barTicks = useMemo(() => resolveAxisTickFormatter(bars?.valueFormatter, bars?.format), [bars?.valueFormatter, bars?.format])
3745
- const lineTicks = useMemo(() => resolveAxisTickFormatter(line?.valueFormatter, line?.format), [line?.valueFormatter, line?.format])
3996
+ const locale = useLocale()
3997
+ const t = TEXT[uiLanguage(locale)]
3998
+ const barFormatter = useMemo(() => resolveValueFormatter(bars?.valueFormatter, bars?.format, locale), [bars?.valueFormatter, bars?.format, locale])
3999
+ const lineFormatter = useMemo(() => resolveValueFormatter(line?.valueFormatter, line?.format, locale), [line?.valueFormatter, line?.format, locale])
4000
+ const barAxis = useMemo(() => resolveAxisFormatter(bars?.valueFormatter, bars?.format, locale), [bars?.valueFormatter, bars?.format, locale])
4001
+ const lineAxis = useMemo(() => resolveAxisFormatter(line?.valueFormatter, line?.format, locale), [line?.valueFormatter, line?.format, locale])
4002
+ const barTicks = useMemo(() => resolveAxisTickFormatter(bars?.valueFormatter, bars?.format, locale), [bars?.valueFormatter, bars?.format, locale])
4003
+ const lineTicks = useMemo(() => resolveAxisTickFormatter(line?.valueFormatter, line?.format, locale), [line?.valueFormatter, line?.format, locale])
3746
4004
  const palette = useMemo(() => {
3747
4005
  const p = pairedPalette(barKeys, lineKeys, bars?.colors as ChartColors | undefined, line?.colors as ChartColors | undefined)
3748
4006
  return { bars: resolveChartColors(p.bars, tokens), line: resolveChartColors(p.line, tokens) }
@@ -3779,25 +4037,26 @@ function PairedChartImpl<T extends Record<string, unknown>>(
3779
4037
  plotWidth,
3780
4038
  guideColor: tokens?.mutedForeground ?? undefined,
3781
4039
  chartOptions,
4040
+ locale,
3782
4041
  }),
3783
4042
  [
3784
4043
  data, index, labels, barKeys, lineKeys, palette, barFormatter, lineFormatter, barAxis, lineAxis, barTicks, lineTicks,
3785
4044
  bars?.type, bars?.yAxisLabel, bars?.maxValue, line?.fitToData, line?.yAxisLabel, line?.maxValue, line?.minValue,
3786
- height, heightRatio, yAxisWidth, showLegend, showTooltip, actions, plotWidth, tokens, chartOptions,
4045
+ height, heightRatio, yAxisWidth, showLegend, showTooltip, actions, plotWidth, tokens, chartOptions, locale,
3787
4046
  ],
3788
4047
  )
3789
4048
  // スクリーンリーダー向けの値の表: 棒の列 → 線の列(段ごとの書式)。
3790
4049
  const dataTable = useMemo(() => {
3791
- const upper = chartDataTable(labelRows(data, index, labels), index, barKeys, barFormatter, labels, "項目")
3792
- const lower = chartDataTable(labelRows(data, index, labels), index, lineKeys, lineFormatter, labels, "項目")
3793
- const times = timeTooltipLabels(data.map((row) => row[index]))
4050
+ const upper = chartDataTable(labelRows(data, index, labels), index, barKeys, barFormatter, labels, t.item)
4051
+ const lower = chartDataTable(labelRows(data, index, labels), index, lineKeys, lineFormatter, labels, t.item)
4052
+ const times = timeTooltipLabels(data.map((row) => row[index]), locale)
3794
4053
  return {
3795
4054
  ...upper,
3796
4055
  columns: [...upper.columns, ...lower.columns.slice(1)],
3797
4056
  rows: upper.rows.map((r, i) => [times?.[i] ?? r[0] ?? "", ...r.slice(1), ...(lower.rows[i] ?? []).slice(1)]),
3798
4057
  }
3799
- }, [data, index, labels, barKeys, lineKeys, barFormatter, lineFormatter])
3800
- const label = ariaLabel ?? `${[...barKeys, ...lineKeys].join("・")}の推移(上段 棒、下段 線)`
4058
+ }, [data, index, labels, barKeys, lineKeys, barFormatter, lineFormatter, t, locale])
4059
+ const label = ariaLabel ?? t.chart([...barKeys, ...lineKeys])
3801
4060
  return (
3802
4061
  <ChartFrame
3803
4062
  {...chartRootProps(rest)}