@squadbase/vantage 0.6.2-dev1.54bde93 → 0.6.2-dev1.56da87f

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 (89) hide show
  1. package/dist/{build-A23M4GCH.js → build-U7GO26HR.js} +3 -3
  2. package/dist/{build-A23M4GCH.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-FBHT6W6J.js → check-MRVAIHG5.js} +3 -3
  4. package/dist/{check-FBHT6W6J.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
  6. package/dist/chunk-3RJEOP3J.js.map +1 -0
  7. package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
  8. package/dist/chunk-77CISBMG.js.map +1 -0
  9. package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
  10. package/dist/chunk-EG4PX726.js.map +1 -0
  11. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  12. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  13. package/dist/{chunk-5C2SEDPH.js → chunk-PJ4SPXHS.js} +4 -4
  14. package/dist/{chunk-5C2SEDPH.js.map → chunk-PJ4SPXHS.js.map} +1 -1
  15. package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
  16. package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
  17. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  18. package/dist/chunk-ROHNXP7J.js.map +1 -0
  19. package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
  20. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  21. package/dist/cli.js +4 -4
  22. package/dist/components/index.d.ts +49 -29
  23. package/dist/components/index.js +5 -5
  24. package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
  25. package/dist/{dev-H4I2JL63.js → dev-O42JFQ64.js} +3 -3
  26. package/dist/{dev-H4I2JL63.js.map → dev-O42JFQ64.js.map} +1 -1
  27. package/dist/doc/index.d.ts +14 -14
  28. package/dist/doc/index.js +198 -84
  29. package/dist/doc/index.js.map +1 -1
  30. package/dist/{doctor-B2O3GJDU.js → doctor-YD3XAAOT.js} +3 -3
  31. package/dist/{doctor-B2O3GJDU.js.map → doctor-YD3XAAOT.js.map} +1 -1
  32. package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
  33. package/dist/slide/index.d.ts +2 -2
  34. package/dist/slide/index.js +95 -44
  35. package/dist/slide/index.js.map +1 -1
  36. package/dist/table/index.js +6 -6
  37. package/dist/ui/index.d.ts +1 -1
  38. package/dist/ui/index.js +5 -5
  39. package/docs/en/agent-skills.md +1 -1
  40. package/docs/ja/agent-skills.md +1 -1
  41. package/package.json +2 -2
  42. package/registry/ui/app-shell.tsx +138 -1
  43. package/registry/ui/area-chart.tsx +344 -85
  44. package/registry/ui/attention-list.tsx +177 -15
  45. package/registry/ui/bar-chart.tsx +341 -85
  46. package/registry/ui/bar-list.tsx +332 -77
  47. package/registry/ui/box-plot-chart.tsx +405 -97
  48. package/registry/ui/category-bar.tsx +248 -40
  49. package/registry/ui/chart-frame.tsx +257 -46
  50. package/registry/ui/data-table.tsx +229 -37
  51. package/registry/ui/date-range-picker.tsx +190 -36
  52. package/registry/ui/delta.tsx +171 -12
  53. package/registry/ui/donut-chart.tsx +339 -84
  54. package/registry/ui/echart.tsx +142 -4
  55. package/registry/ui/filter-bar.tsx +179 -20
  56. package/registry/ui/funnel-steps.tsx +354 -80
  57. package/registry/ui/heatmap.tsx +293 -52
  58. package/registry/ui/histogram-chart.tsx +395 -97
  59. package/registry/ui/kpi-card.tsx +245 -44
  60. package/registry/ui/legend.tsx +330 -76
  61. package/registry/ui/line-chart.tsx +344 -85
  62. package/registry/ui/multi-select.tsx +159 -9
  63. package/registry/ui/page-shell.tsx +141 -3
  64. package/registry/ui/paired-chart.tsx +349 -90
  65. package/registry/ui/progress-circle.tsx +205 -39
  66. package/registry/ui/sample-data.tsx +153 -9
  67. package/registry/ui/scatter-chart.tsx +391 -100
  68. package/registry/ui/searchable-select.tsx +145 -7
  69. package/registry/ui/states.tsx +170 -13
  70. package/registry/ui/status-badge.tsx +193 -28
  71. package/registry/ui/tracker.tsx +205 -39
  72. package/registry/ui/trend-indicator.tsx +149 -6
  73. package/registry/ui/updated-at.tsx +170 -16
  74. package/registry/ui/waterfall-chart.tsx +415 -102
  75. package/skills/vantage-add-feature/SKILL.md +5 -0
  76. package/skills/vantage-design/SKILL.md +25 -27
  77. package/skills/vantage-design/references/extract.md +10 -13
  78. package/skills/vantage-design/references/format.md +12 -173
  79. package/skills/vantage-design/references/presets.md +0 -10
  80. package/templates/AGENTS.md +29 -15
  81. package/templates/agents/api.md +7 -0
  82. package/templates/agents/page.md +37 -1
  83. package/templates/agents/slide.md +7 -1
  84. package/dist/chunk-2N72FKLB.js.map +0 -1
  85. package/dist/chunk-57GHR5LN.js.map +0 -1
  86. package/dist/chunk-6OTTGAPO.js.map +0 -1
  87. package/dist/chunk-JXFCSF3K.js.map +0 -1
  88. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  89. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -17,15 +17,148 @@ import { forwardRef } from "react"
17
17
 
18
18
  import { cn } from "@squadbase/vantage/ui"
19
19
  import type { ChartSeriesColor } from "@squadbase/vantage/components"
20
+ import { useSyncExternalStore } from "react"
20
21
  import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
21
22
  import type { ChartTone } from "@squadbase/vantage/components"
22
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
23
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
23
24
  import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
24
25
  import { useMemo, type ReactNode } from "react"
25
26
  import type { EChartsOption } from "@squadbase/vantage/components"
26
27
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
28
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
27
29
  import type { ChartThreshold, ChartAnnotation, ChartCommonProps, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
28
30
 
31
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
32
+
33
+ /**
34
+ * The display language of the parts and the defaults that follow it.
35
+ *
36
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
37
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
38
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
39
+ *
40
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
41
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
42
+ */
43
+
44
+ /** The language of the built-in wording: Japanese or English. */
45
+ type UiLanguage = "ja" | "en"
46
+
47
+ /** The locale used when `<html lang>` gives none (and on the server). */
48
+ const FALLBACK_LOCALE = "en-US"
49
+
50
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
51
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
52
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
53
+ }
54
+
55
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
56
+ function normalizeLocale(tag: string | null | undefined): string {
57
+ const value = tag?.trim()
58
+ if (!value) return FALLBACK_LOCALE
59
+ try {
60
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
61
+ } catch {
62
+ return FALLBACK_LOCALE
63
+ }
64
+ }
65
+
66
+ /**
67
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
68
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
69
+ */
70
+ function documentLocale(): string {
71
+ if (typeof document === "undefined") return FALLBACK_LOCALE
72
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
73
+ }
74
+
75
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
76
+ function resolveLocale(locale?: string | null): string {
77
+ return locale || documentLocale()
78
+ }
79
+
80
+ /** Picks the wording table of the locale's language. */
81
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
82
+ return table[uiLanguage(resolveLocale(locale))]
83
+ }
84
+
85
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
86
+ function defaultCurrency(locale?: string | null): string {
87
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
88
+ }
89
+
90
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
91
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
92
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
93
+ }
94
+
95
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
96
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
97
+
98
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
99
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
100
+
101
+ /**
102
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
103
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
104
+ * Monday for the rest.
105
+ */
106
+ function weekStartsOn(locale?: string | null): WeekDay {
107
+ const tag = normalizeLocale(resolveLocale(locale))
108
+ if (uiLanguage(tag) === "ja") return 1
109
+ try {
110
+ const info = new Intl.Locale(tag) as Intl.Locale & {
111
+ getWeekInfo?: () => { firstDay: number }
112
+ weekInfo?: { firstDay: number }
113
+ }
114
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
115
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
116
+ } catch {
117
+ // Fall through to the table below.
118
+ }
119
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
120
+ }
121
+
122
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
123
+
124
+ const listeners = new Set<() => void>()
125
+ let observer: MutationObserver | null = null
126
+
127
+ /** One observer on `<html lang>` for every subscribed part. */
128
+ function subscribe(onChange: () => void): () => void {
129
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
130
+ listeners.add(onChange)
131
+ if (!observer) {
132
+ observer = new MutationObserver(() => {
133
+ for (const listener of [...listeners]) listener()
134
+ })
135
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
136
+ }
137
+ return () => {
138
+ listeners.delete(onChange)
139
+ if (listeners.size === 0 && observer) {
140
+ observer.disconnect()
141
+ observer = null
142
+ }
143
+ }
144
+ }
145
+
146
+ const serverLocale = () => FALLBACK_LOCALE
147
+
148
+ /**
149
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
150
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
151
+ */
152
+ function useDocumentLocale(): string {
153
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
154
+ }
155
+
156
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
157
+ function useLocale(locale?: string | null): string {
158
+ const page = useDocumentLocale()
159
+ return locale || page
160
+ }
161
+
29
162
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
30
163
 
31
164
  /**
@@ -529,9 +662,10 @@ function resolveValueFormatter(
529
662
  function resolveAxisFormatter(
530
663
  valueFormatter: ValueFormatter | undefined,
531
664
  format: ValueFormat | undefined,
532
- locale: string = DEFAULT_LOCALE,
665
+ localeArg?: string,
533
666
  ): ValueFormatter {
534
667
  if (valueFormatter) return valueFormatter
668
+ const locale = resolveLocale(localeArg)
535
669
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
536
670
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
537
671
  const currency = "currency" in spec ? spec.currency : undefined
@@ -540,40 +674,75 @@ function resolveAxisFormatter(
540
674
  return (value) => formatValue(value, short, locale)
541
675
  }
542
676
 
543
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
544
- const JA_UNITS = [
545
- { char: "兆", size: 1e12 },
546
- { char: "億", size: 1e8 },
547
- { char: "万", size: 1e4 },
548
- ] as const
677
+ interface CompactUnit {
678
+ token: string
679
+ size: number
680
+ }
681
+
682
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
683
+
684
+ /**
685
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
686
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
687
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
688
+ */
689
+ function compactUnits(locale: string): CompactUnit[] | null {
690
+ const cached = compactUnitCache.get(locale)
691
+ if (cached !== undefined) return cached
692
+ let units: CompactUnit[] | null = []
693
+ try {
694
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
695
+ const seen = new Set<string>()
696
+ for (let exp = 3; exp <= 15; exp++) {
697
+ const parts = compact.formatToParts(10 ** exp)
698
+ const at = parts.findIndex((p) => p.type === "compact")
699
+ if (at === -1) continue
700
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
701
+ units = null
702
+ break
703
+ }
704
+ const token = parts[at]!.value
705
+ if (seen.has(token)) continue
706
+ seen.add(token)
707
+ units.push({ token, size: 10 ** exp })
708
+ }
709
+ } catch {
710
+ units = null
711
+ }
712
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
713
+ compactUnitCache.set(locale, sorted)
714
+ return sorted
715
+ }
549
716
 
550
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
717
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
551
718
 
552
719
  /**
553
720
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
554
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
555
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
556
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
721
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
722
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
723
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
724
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
557
725
  */
558
726
  function resolveAxisTickFormatter(
559
727
  valueFormatter: ValueFormatter | undefined,
560
728
  format: ValueFormat | undefined,
561
- locale: string = DEFAULT_LOCALE,
729
+ localeArg?: string,
562
730
  ): (top: number) => ValueFormatter {
731
+ const locale = resolveLocale(localeArg)
563
732
  const base = resolveAxisFormatter(valueFormatter, format, locale)
564
733
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
565
- const aligns =
566
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
567
- if (!aligns) return () => base
734
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
735
+ if (!units) return () => base
568
736
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
737
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
569
738
  return (top) => {
570
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
739
+ const unit = units.find((u) => Math.abs(top) >= u.size)
571
740
  if (!unit) return base
572
741
  return (value) => {
573
742
  const text = base(value)
574
743
  if (value === 0 || !Number.isFinite(value)) return text
575
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
576
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
744
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
745
+ return text.replace(numberToken, () => scaled)
577
746
  }
578
747
  }
579
748
  }
@@ -602,8 +771,20 @@ function clamp(value: number, min: number, max: number): number {
602
771
  /** How many categories a chart colours individually; the rest fold into「その他」. */
603
772
  const MAX_CHART_SERIES = 5
604
773
 
605
- /** The series name the folded categories get. */
606
- const OTHER_SERIES_LABEL = "その他"
774
+ /** The series name the folded categories get, by language. */
775
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
776
+
777
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
778
+ function otherSeriesLabel(locale?: string): string {
779
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
780
+ }
781
+
782
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
783
+
784
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
785
+ function isOtherName(name: string | null | undefined): boolean {
786
+ return typeof name === "string" && OTHER_NAMES.has(name)
787
+ }
607
788
 
608
789
  interface AggregatedItems<Item extends { name: string; value: number }> {
609
790
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -623,7 +804,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
623
804
  function aggregateTopN<Item extends { name: string; value: number }>(
624
805
  items: readonly Item[],
625
806
  n: number = MAX_CHART_SERIES,
626
- otherLabel: string = OTHER_SERIES_LABEL,
807
+ otherLabel: string = otherSeriesLabel(),
627
808
  ): AggregatedItems<Item> {
628
809
  if (items.length <= n) return { top: [...items], rest: [], other: null }
629
810
  const ranked = items
@@ -652,7 +833,7 @@ function aggregateSeries(
652
833
  data: readonly Record<string, unknown>[],
653
834
  categories: readonly string[],
654
835
  n: number = MAX_CHART_SERIES,
655
- otherLabel: string = OTHER_SERIES_LABEL,
836
+ otherLabel: string = otherSeriesLabel(),
656
837
  ): AggregatedSeries {
657
838
  const totals = categories.map((name) => ({
658
839
  name,
@@ -684,12 +865,14 @@ function aggregateSeriesKeeping(
684
865
  categories: readonly string[],
685
866
  keep: string | undefined,
686
867
  n: number = MAX_CHART_SERIES,
868
+ otherLabel: string = otherSeriesLabel(),
687
869
  ): AggregatedSeries {
688
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
870
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
689
871
  const aggregated = aggregateSeries(
690
872
  data,
691
873
  categories.filter((c) => c !== keep),
692
874
  n,
875
+ otherLabel,
693
876
  )
694
877
  return { ...aggregated, categories: [...aggregated.categories, keep] }
695
878
  }
@@ -725,7 +908,7 @@ function seriesColorsWithComparison(
725
908
  * Colour per drawn series: the record's token for that category, else the
726
909
  * array's token at the category's position in `allCategories` (cycled), else
727
910
  * the token palette by drawn position. `otherCategory` — and a category the
728
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
911
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
729
912
  */
730
913
  function seriesColorsFor(
731
914
  categories: readonly string[],
@@ -737,7 +920,7 @@ function seriesColorsFor(
737
920
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
738
921
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
739
922
  return categories.map((category, i) => {
740
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
923
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
741
924
  const fromRecord = record?.[category]
742
925
  if (fromRecord) return fromRecord
743
926
  if (list && list.length > 0) {
@@ -784,7 +967,7 @@ interface DonutSlicesConfig {
784
967
  */
785
968
  function donutSlices(
786
969
  data: readonly Record<string, unknown>[],
787
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
970
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
788
971
  ): DonutSlice[] {
789
972
  const items = data
790
973
  .map((row, i) => {
@@ -797,7 +980,8 @@ function donutSlices(
797
980
  const slices: DonutSlice[] = top.map((item, rank) => ({
798
981
  name: item.name,
799
982
  value: item.value,
800
- color: palette[rank % palette.length]!,
983
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
984
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
801
985
  isOther: false,
802
986
  row: item.row,
803
987
  }))
@@ -870,7 +1054,7 @@ interface BarListRowsConfig extends BarListOptions {
870
1054
  */
871
1055
  function barListRows<Row extends object>(
872
1056
  data: readonly Row[],
873
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1057
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
874
1058
  ): BarListRow<Row>[] {
875
1059
  const items = data.map((row): BarListRow<Row> => {
876
1060
  const cells = row as Record<string, unknown>
@@ -916,19 +1100,21 @@ const BAR_LIST_OTHER_TINT = 30
916
1100
 
917
1101
  /**
918
1102
  * The fill of each BarList row: all real rows in the same tint of `color`
919
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1103
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
920
1104
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
921
1105
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
922
1106
  * for that row. A tint, not the solid token, so the label printed over the bar
923
1107
  * keeps its contrast.
924
1108
  */
925
1109
  function barListFills(
926
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1110
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
927
1111
  color: ChartColor,
928
1112
  otherColor: string,
929
1113
  ): string[] {
930
1114
  return rows.map((row) =>
931
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1115
+ row.isOther || isOtherName(row.name)
1116
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1117
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
932
1118
  )
933
1119
  }
934
1120
 
@@ -1144,11 +1330,30 @@ interface ChartSummaryInput {
1144
1330
  format?: ValueFormat
1145
1331
  /** Wins over `format`. */
1146
1332
  valueFormatter?: ValueFormatter
1333
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1334
+ locale?: string
1335
+ }
1336
+
1337
+ const SUMMARY_TEXT = {
1338
+ ja: {
1339
+ up: "、前の点より増加",
1340
+ down: "、前の点より減少",
1341
+ flat: "、前の点と横ばい",
1342
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1343
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1344
+ },
1345
+ en: {
1346
+ up: ", up from the previous point",
1347
+ down: ", down from the previous point",
1348
+ flat: ", flat from the previous point",
1349
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1350
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1351
+ },
1147
1352
  }
1148
1353
 
1149
1354
  /**
1150
1355
  * One-sentence summary built from the rows, for `summary="auto"`:
1151
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1356
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1152
1357
  * computed from the last two non-empty points of the first series — never
1153
1358
  * written by hand, so it cannot contradict the data. `undefined` when there is
1154
1359
  * no usable point.
@@ -1164,30 +1369,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1164
1369
  }
1165
1370
  const latest = points[points.length - 1]
1166
1371
  if (!latest) return undefined
1167
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1372
+ const locale = resolveLocale(input.locale)
1373
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1374
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1168
1375
  const prev = points[points.length - 2]
1169
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1376
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1170
1377
  const head = input.title ? `${input.title}: ` : ""
1171
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1378
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1172
1379
  }
1173
1380
 
1174
1381
  /**
1175
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1176
- * (largest first). `undefined` when there is nothing to describe.
1382
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1383
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1177
1384
  */
1178
1385
  function buildShareSummary(
1179
1386
  slices: readonly { name: string; value: number }[],
1180
1387
  title?: string,
1388
+ locale?: string,
1181
1389
  ): string | undefined {
1182
1390
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1183
1391
  const top = slices[0]
1184
1392
  if (!top || total <= 0) return undefined
1185
1393
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1186
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1394
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1187
1395
  }
1188
1396
 
1189
1397
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1190
1398
 
1399
+ /** The built-in words of the option builders, by language. */
1400
+ const CHART_TEXT = {
1401
+ ja: {
1402
+ target: "目標",
1403
+ forecast: "予測",
1404
+ total: "合計",
1405
+ provisional: "速報",
1406
+ noComparison: "比較なし",
1407
+ share: (share: string) => `(${share})`,
1408
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1409
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1410
+ },
1411
+ en: {
1412
+ target: "Target",
1413
+ forecast: "Forecast",
1414
+ total: "Total",
1415
+ provisional: "Preliminary",
1416
+ noComparison: "No comparison",
1417
+ share: (share: string) => ` (${share})`,
1418
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1419
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1420
+ },
1421
+ } satisfies Record<UiLanguage, unknown>
1422
+
1423
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1424
+
1191
1425
  type CartesianChartKind = "area" | "bar" | "line"
1192
1426
  type CartesianStackType = "default" | "stacked" | "percent"
1193
1427
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1372,7 +1606,7 @@ interface CartesianChartConfig {
1372
1606
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1373
1607
  */
1374
1608
  axisTicks?: (top: number) => (value: number) => string
1375
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1609
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1376
1610
  locale?: string
1377
1611
  type?: CartesianStackType
1378
1612
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1610,11 +1844,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1610
1844
  */
1611
1845
  function timeAxisLabels(
1612
1846
  values: readonly unknown[],
1613
- locale: string = DEFAULT_LOCALE,
1847
+ localeArg?: string,
1614
1848
  shown: (i: number) => boolean = () => true,
1615
1849
  ): string[] | null {
1616
1850
  const parsed = parseTimeIndex(values)
1617
1851
  if (!parsed) return null
1852
+ const locale = resolveLocale(localeArg)
1618
1853
  const { granularity, times } = parsed
1619
1854
  let prevYear: number | null = null
1620
1855
  return times.map((t, i) => {
@@ -1637,9 +1872,10 @@ function timeAxisLabels(
1637
1872
  }
1638
1873
 
1639
1874
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1640
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1875
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1641
1876
  const parsed = parseTimeIndex(values)
1642
1877
  if (!parsed) return null
1878
+ const locale = resolveLocale(localeArg)
1643
1879
  const options: Intl.DateTimeFormatOptions =
1644
1880
  parsed.granularity === "year"
1645
1881
  ? { year: "numeric" }
@@ -1779,7 +2015,9 @@ function tooltipHtml(
1779
2015
  rows: readonly TooltipRow[],
1780
2016
  valueFormatter: (value: number) => string,
1781
2017
  notes: readonly string[] = [],
2018
+ locale?: string,
1782
2019
  ): string {
2020
+ const t = chartText(locale)
1783
2021
  const lines = rows.map(
1784
2022
  (row) =>
1785
2023
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1788,7 +2026,7 @@ function tooltipHtml(
1788
2026
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1789
2027
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1790
2028
  }${
1791
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2029
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1792
2030
  }</span></div>` +
1793
2031
  (row.note === undefined
1794
2032
  ? ""
@@ -1802,8 +2040,10 @@ function tooltipHtml(
1802
2040
 
1803
2041
  /* ------------------------------------------------- per-bar targets / notes */
1804
2042
 
1805
- /** Default name of the per-bar target marker (legend, tooltip). */
1806
- const BAR_TARGET_LABEL = "目標"
2043
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2044
+ function barTargetLabel(locale?: string): string {
2045
+ return chartText(locale).target
2046
+ }
1807
2047
 
1808
2048
  /** How far (px) the target marker reaches past each side of its bar. */
1809
2049
  const TARGET_MARKER_OVERHANG = 4
@@ -1854,16 +2094,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1854
2094
  return (value / target) * 100
1855
2095
  }
1856
2096
 
1857
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2097
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1858
2098
  function barTargetNote(
1859
2099
  label: string,
1860
2100
  value: number | null,
1861
2101
  target: number | null,
1862
2102
  valueFormatter: (value: number) => string,
2103
+ locale?: string,
1863
2104
  ): string | null {
1864
2105
  if (target === null) return null
1865
2106
  const rate = achievementRate(value, target)
1866
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2107
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1867
2108
  }
1868
2109
 
1869
2110
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1955,11 +2196,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1955
2196
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1956
2197
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1957
2198
 
1958
- const DEFAULT_FORECAST_LABEL = "予測"
1959
-
1960
- /** 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. */
1961
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1962
- return `${label} →`
2199
+ /**
2200
+ * 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.
2201
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2202
+ */
2203
+ function forecastBoundaryText(label?: string, locale?: string): string {
2204
+ return `${label ?? chartText(locale).forecast} →`
1963
2205
  }
1964
2206
 
1965
2207
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1999,8 +2241,10 @@ interface AxisExtent {
1999
2241
  max: number
2000
2242
  }
2001
2243
 
2002
- /** The label of a stack's total (highlight and tooltip). */
2003
- const STACK_TOTAL_LABEL = "合計"
2244
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2245
+ function stackTotalLabel(locale?: string): string {
2246
+ return chartText(locale).total
2247
+ }
2004
2248
 
2005
2249
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
2006
2250
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2122,8 +2366,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2122
2366
 
2123
2367
  /**
2124
2368
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2125
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2126
- * either value is missing or the baseline is 0.
2369
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2370
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2127
2371
  */
2128
2372
  function comparisonNote(
2129
2373
  current: number | null,
@@ -2131,16 +2375,18 @@ function comparisonNote(
2131
2375
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2132
2376
  grain?: PeriodGrain,
2133
2377
  format?: ValueFormat,
2134
- locale: string = DEFAULT_LOCALE,
2378
+ localeArg?: string,
2135
2379
  ): string {
2380
+ const locale = resolveLocale(localeArg)
2136
2381
  const result = computeComparison(
2137
2382
  current,
2138
2383
  comparison.vs === "custom"
2139
2384
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2140
2385
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2386
+ { locale },
2141
2387
  )
2142
2388
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2143
- if (text === null) return `比較なし ${result.label}`
2389
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2144
2390
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2145
2391
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2146
2392
  }
@@ -2183,7 +2429,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2183
2429
  toneColors,
2184
2430
  valueFormatter,
2185
2431
  axisFormatter = valueFormatter,
2186
- locale = DEFAULT_LOCALE,
2187
2432
  type = "default",
2188
2433
  layout = "horizontal",
2189
2434
  curveType = "linear",
@@ -2220,7 +2465,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2220
2465
  chartOptions,
2221
2466
  reserveActions = false,
2222
2467
  } = cfg
2223
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2468
+ const locale = resolveLocale(cfg.locale)
2469
+ const text = chartText(locale)
2470
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2224
2471
 
2225
2472
  const stacked = kind !== "line" && type !== "default"
2226
2473
  const percent = kind !== "line" && type === "percent"
@@ -2369,9 +2616,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2369
2616
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2370
2617
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2371
2618
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2372
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2619
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2373
2620
  const highlightText = (value: number) =>
2374
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2621
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2375
2622
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2376
2623
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2377
2624
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2397,7 +2644,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2397
2644
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2398
2645
  const barTargets: (number | null)[] | null =
2399
2646
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2400
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2647
+ const barTargetName = targetLabel ?? text.target
2401
2648
  const barTargetColor = labelColor ?? guideColor
2402
2649
 
2403
2650
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2611,7 +2858,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2611
2858
  }
2612
2859
 
2613
2860
  /* -- forecast: dashed continuation (+ optional band) of one series */
2614
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2861
+ const forecastLabel = forecast?.label ?? text.forecast
2615
2862
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2616
2863
  const forecastValues: (number | null)[] = []
2617
2864
  if (forecast) {
@@ -3087,7 +3334,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3087
3334
  })
3088
3335
  }
3089
3336
  if (stackTotals && i <= lastActual && rows.length > 1) {
3090
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3337
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3091
3338
  }
3092
3339
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3093
3340
  if (provisional) {
@@ -3103,19 +3350,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3103
3350
  ...noteFor(at),
3104
3351
  })
3105
3352
  })
3106
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3353
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3107
3354
  }
3108
3355
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3109
3356
  const lines: string[] = []
3110
3357
  if (barTargets && i <= lastActual) {
3111
3358
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3112
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3359
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3113
3360
  if (note) lines.push(note)
3114
3361
  }
3115
3362
  for (const note of notes) {
3116
3363
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3117
3364
  }
3118
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3365
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3119
3366
  },
3120
3367
  },
3121
3368
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3207,8 +3454,6 @@ interface DonutOptionSlice {
3207
3454
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3208
3455
  const DONUT_OUTSIDE_LABELS = 2
3209
3456
 
3210
- const DEFAULT_TOTAL_LABEL = "合計"
3211
-
3212
3457
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3213
3458
 
3214
3459
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3241,15 +3486,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3241
3486
  }
3242
3487
 
3243
3488
  /**
3244
- * The default center label: what is totalled (「合計」), the formatted total
3489
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3245
3490
  * (`amount`, the big line), the unit (a small line under it) and both as one
3246
3491
  * string (`value`, 「9.2万 セッション」).
3247
3492
  */
3248
3493
  function donutCenterText(
3249
3494
  total: number,
3250
3495
  valueFormatter: (value: number) => string,
3251
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3496
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3252
3497
  ): { label: string; value: string; amount: string; unit?: string } {
3498
+ totalLabel ??= chartText(locale).total
3253
3499
  const amount = valueFormatter(total)
3254
3500
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3255
3501
  }
@@ -3477,8 +3723,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3477
3723
 
3478
3724
  /* ------------------------------------------------------------ provisional */
3479
3725
 
3480
- /** The default label for points that are not final yet. */
3481
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3726
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3727
+ function defaultProvisionalLabel(locale?: string): string {
3728
+ return chartText(locale).provisional
3729
+ }
3482
3730
 
3483
3731
  /**
3484
3732
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3536,11 +3784,17 @@ export function legendColors(
3536
3784
  return seriesColorsFor(categories, colors, CHART_OTHER_COLOR).map((color) => chartFillToCss(color))
3537
3785
  }
3538
3786
 
3787
+ const TEXT = {
3788
+ ja: { legend: "凡例" },
3789
+ en: { legend: "Legend" },
3790
+ }
3791
+
3539
3792
  /** Plain-HTML legend. Renders nothing for an empty `categories`. */
3540
3793
  export const Legend = forwardRef<HTMLUListElement, LegendProps>(function Legend(
3541
- { categories, colors, labels, layout = "horizontal", ariaLabel = "凡例", className },
3794
+ { categories, colors, labels, layout = "horizontal", ariaLabel, className },
3542
3795
  ref,
3543
3796
  ) {
3797
+ const t = TEXT[uiLanguage(useLocale())]
3544
3798
  if (categories.length === 0) return null
3545
3799
  const swatches = legendColors(categories, colors)
3546
3800
  return (
@@ -3548,7 +3802,7 @@ export const Legend = forwardRef<HTMLUListElement, LegendProps>(function Legend(
3548
3802
  ref={ref}
3549
3803
  data-slot="legend"
3550
3804
  data-layout={layout}
3551
- aria-label={ariaLabel}
3805
+ aria-label={ariaLabel ?? t.legend}
3552
3806
  className={cn("m-0 flex list-none gap-x-4 gap-y-1 p-0 text-xs", layout === "vertical" ? "flex-col" : "flex-row flex-wrap", className)}
3553
3807
  >
3554
3808
  {categories.map((category, i) => (