@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
@@ -13,11 +13,143 @@ import { cn } from "@squadbase/vantage/ui"
13
13
  import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
14
14
  import type { ValueFormat } from "@squadbase/vantage/components"
15
15
  import type { ChartColor, ValueFormatter } from "@squadbase/vantage/components"
16
+ import { useSyncExternalStore } from "react"
16
17
  import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
17
18
  import type { ChartTone } from "@squadbase/vantage/components"
18
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
19
+ import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
19
20
  import type { ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
20
21
 
22
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
23
+
24
+ /**
25
+ * The display language of the parts and the defaults that follow it.
26
+ *
27
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
28
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
29
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
30
+ *
31
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
32
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
33
+ */
34
+
35
+ /** The language of the built-in wording: Japanese or English. */
36
+ type UiLanguage = "ja" | "en"
37
+
38
+ /** The locale used when `<html lang>` gives none (and on the server). */
39
+ const FALLBACK_LOCALE = "en-US"
40
+
41
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
42
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
43
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
44
+ }
45
+
46
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
47
+ function normalizeLocale(tag: string | null | undefined): string {
48
+ const value = tag?.trim()
49
+ if (!value) return FALLBACK_LOCALE
50
+ try {
51
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
52
+ } catch {
53
+ return FALLBACK_LOCALE
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
59
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
60
+ */
61
+ function documentLocale(): string {
62
+ if (typeof document === "undefined") return FALLBACK_LOCALE
63
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
64
+ }
65
+
66
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
67
+ function resolveLocale(locale?: string | null): string {
68
+ return locale || documentLocale()
69
+ }
70
+
71
+ /** Picks the wording table of the locale's language. */
72
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
73
+ return table[uiLanguage(resolveLocale(locale))]
74
+ }
75
+
76
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
77
+ function defaultCurrency(locale?: string | null): string {
78
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
79
+ }
80
+
81
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
82
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
83
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
84
+ }
85
+
86
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
87
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
88
+
89
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
90
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
91
+
92
+ /**
93
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
94
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
95
+ * Monday for the rest.
96
+ */
97
+ function weekStartsOn(locale?: string | null): WeekDay {
98
+ const tag = normalizeLocale(resolveLocale(locale))
99
+ if (uiLanguage(tag) === "ja") return 1
100
+ try {
101
+ const info = new Intl.Locale(tag) as Intl.Locale & {
102
+ getWeekInfo?: () => { firstDay: number }
103
+ weekInfo?: { firstDay: number }
104
+ }
105
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
106
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
107
+ } catch {
108
+ // Fall through to the table below.
109
+ }
110
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
111
+ }
112
+
113
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
114
+
115
+ const listeners = new Set<() => void>()
116
+ let observer: MutationObserver | null = null
117
+
118
+ /** One observer on `<html lang>` for every subscribed part. */
119
+ function subscribe(onChange: () => void): () => void {
120
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
121
+ listeners.add(onChange)
122
+ if (!observer) {
123
+ observer = new MutationObserver(() => {
124
+ for (const listener of [...listeners]) listener()
125
+ })
126
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
127
+ }
128
+ return () => {
129
+ listeners.delete(onChange)
130
+ if (listeners.size === 0 && observer) {
131
+ observer.disconnect()
132
+ observer = null
133
+ }
134
+ }
135
+ }
136
+
137
+ const serverLocale = () => FALLBACK_LOCALE
138
+
139
+ /**
140
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
141
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
142
+ */
143
+ function useDocumentLocale(): string {
144
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
145
+ }
146
+
147
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
148
+ function useLocale(locale?: string | null): string {
149
+ const page = useDocumentLocale()
150
+ return locale || page
151
+ }
152
+
21
153
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
22
154
 
23
155
  /**
@@ -521,9 +653,10 @@ function resolveValueFormatter(
521
653
  function resolveAxisFormatter(
522
654
  valueFormatter: ValueFormatter | undefined,
523
655
  format: ValueFormat | undefined,
524
- locale: string = DEFAULT_LOCALE,
656
+ localeArg?: string,
525
657
  ): ValueFormatter {
526
658
  if (valueFormatter) return valueFormatter
659
+ const locale = resolveLocale(localeArg)
527
660
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
528
661
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
529
662
  const currency = "currency" in spec ? spec.currency : undefined
@@ -532,40 +665,75 @@ function resolveAxisFormatter(
532
665
  return (value) => formatValue(value, short, locale)
533
666
  }
534
667
 
535
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
536
- const JA_UNITS = [
537
- { char: "兆", size: 1e12 },
538
- { char: "億", size: 1e8 },
539
- { char: "万", size: 1e4 },
540
- ] as const
668
+ interface CompactUnit {
669
+ token: string
670
+ size: number
671
+ }
541
672
 
542
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
673
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
674
+
675
+ /**
676
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
677
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
678
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
679
+ */
680
+ function compactUnits(locale: string): CompactUnit[] | null {
681
+ const cached = compactUnitCache.get(locale)
682
+ if (cached !== undefined) return cached
683
+ let units: CompactUnit[] | null = []
684
+ try {
685
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
686
+ const seen = new Set<string>()
687
+ for (let exp = 3; exp <= 15; exp++) {
688
+ const parts = compact.formatToParts(10 ** exp)
689
+ const at = parts.findIndex((p) => p.type === "compact")
690
+ if (at === -1) continue
691
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
692
+ units = null
693
+ break
694
+ }
695
+ const token = parts[at]!.value
696
+ if (seen.has(token)) continue
697
+ seen.add(token)
698
+ units.push({ token, size: 10 ** exp })
699
+ }
700
+ } catch {
701
+ units = null
702
+ }
703
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
704
+ compactUnitCache.set(locale, sorted)
705
+ return sorted
706
+ }
707
+
708
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
543
709
 
544
710
  /**
545
711
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
546
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
547
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
548
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
712
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
713
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
714
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
715
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
549
716
  */
550
717
  function resolveAxisTickFormatter(
551
718
  valueFormatter: ValueFormatter | undefined,
552
719
  format: ValueFormat | undefined,
553
- locale: string = DEFAULT_LOCALE,
720
+ localeArg?: string,
554
721
  ): (top: number) => ValueFormatter {
722
+ const locale = resolveLocale(localeArg)
555
723
  const base = resolveAxisFormatter(valueFormatter, format, locale)
556
724
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
557
- const aligns =
558
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
559
- if (!aligns) return () => base
725
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
726
+ if (!units) return () => base
560
727
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
728
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
561
729
  return (top) => {
562
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
730
+ const unit = units.find((u) => Math.abs(top) >= u.size)
563
731
  if (!unit) return base
564
732
  return (value) => {
565
733
  const text = base(value)
566
734
  if (value === 0 || !Number.isFinite(value)) return text
567
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
568
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
735
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
736
+ return text.replace(numberToken, () => scaled)
569
737
  }
570
738
  }
571
739
  }
@@ -594,8 +762,20 @@ function clamp(value: number, min: number, max: number): number {
594
762
  /** How many categories a chart colours individually; the rest fold into「その他」. */
595
763
  const MAX_CHART_SERIES = 5
596
764
 
597
- /** The series name the folded categories get. */
598
- const OTHER_SERIES_LABEL = "その他"
765
+ /** The series name the folded categories get, by language. */
766
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
767
+
768
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
769
+ function otherSeriesLabel(locale?: string): string {
770
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
771
+ }
772
+
773
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
774
+
775
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
776
+ function isOtherName(name: string | null | undefined): boolean {
777
+ return typeof name === "string" && OTHER_NAMES.has(name)
778
+ }
599
779
 
600
780
  interface AggregatedItems<Item extends { name: string; value: number }> {
601
781
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -615,7 +795,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
615
795
  function aggregateTopN<Item extends { name: string; value: number }>(
616
796
  items: readonly Item[],
617
797
  n: number = MAX_CHART_SERIES,
618
- otherLabel: string = OTHER_SERIES_LABEL,
798
+ otherLabel: string = otherSeriesLabel(),
619
799
  ): AggregatedItems<Item> {
620
800
  if (items.length <= n) return { top: [...items], rest: [], other: null }
621
801
  const ranked = items
@@ -644,7 +824,7 @@ function aggregateSeries(
644
824
  data: readonly Record<string, unknown>[],
645
825
  categories: readonly string[],
646
826
  n: number = MAX_CHART_SERIES,
647
- otherLabel: string = OTHER_SERIES_LABEL,
827
+ otherLabel: string = otherSeriesLabel(),
648
828
  ): AggregatedSeries {
649
829
  const totals = categories.map((name) => ({
650
830
  name,
@@ -676,12 +856,14 @@ function aggregateSeriesKeeping(
676
856
  categories: readonly string[],
677
857
  keep: string | undefined,
678
858
  n: number = MAX_CHART_SERIES,
859
+ otherLabel: string = otherSeriesLabel(),
679
860
  ): AggregatedSeries {
680
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
861
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
681
862
  const aggregated = aggregateSeries(
682
863
  data,
683
864
  categories.filter((c) => c !== keep),
684
865
  n,
866
+ otherLabel,
685
867
  )
686
868
  return { ...aggregated, categories: [...aggregated.categories, keep] }
687
869
  }
@@ -717,7 +899,7 @@ function seriesColorsWithComparison(
717
899
  * Colour per drawn series: the record's token for that category, else the
718
900
  * array's token at the category's position in `allCategories` (cycled), else
719
901
  * the token palette by drawn position. `otherCategory` — and a category the
720
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
902
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
721
903
  */
722
904
  function seriesColorsFor(
723
905
  categories: readonly string[],
@@ -729,7 +911,7 @@ function seriesColorsFor(
729
911
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
730
912
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
731
913
  return categories.map((category, i) => {
732
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
914
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
733
915
  const fromRecord = record?.[category]
734
916
  if (fromRecord) return fromRecord
735
917
  if (list && list.length > 0) {
@@ -776,7 +958,7 @@ interface DonutSlicesConfig {
776
958
  */
777
959
  function donutSlices(
778
960
  data: readonly Record<string, unknown>[],
779
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
961
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
780
962
  ): DonutSlice[] {
781
963
  const items = data
782
964
  .map((row, i) => {
@@ -789,7 +971,8 @@ function donutSlices(
789
971
  const slices: DonutSlice[] = top.map((item, rank) => ({
790
972
  name: item.name,
791
973
  value: item.value,
792
- color: palette[rank % palette.length]!,
974
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
975
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
793
976
  isOther: false,
794
977
  row: item.row,
795
978
  }))
@@ -862,7 +1045,7 @@ interface BarListRowsConfig extends BarListOptions {
862
1045
  */
863
1046
  function barListRows<Row extends object>(
864
1047
  data: readonly Row[],
865
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1048
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
866
1049
  ): BarListRow<Row>[] {
867
1050
  const items = data.map((row): BarListRow<Row> => {
868
1051
  const cells = row as Record<string, unknown>
@@ -908,19 +1091,21 @@ const BAR_LIST_OTHER_TINT = 30
908
1091
 
909
1092
  /**
910
1093
  * The fill of each BarList row: all real rows in the same tint of `color`
911
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1094
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
912
1095
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
913
1096
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
914
1097
  * for that row. A tint, not the solid token, so the label printed over the bar
915
1098
  * keeps its contrast.
916
1099
  */
917
1100
  function barListFills(
918
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1101
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
919
1102
  color: ChartColor,
920
1103
  otherColor: string,
921
1104
  ): string[] {
922
1105
  return rows.map((row) =>
923
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1106
+ row.isOther || isOtherName(row.name)
1107
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1108
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
924
1109
  )
925
1110
  }
926
1111
 
@@ -1136,11 +1321,30 @@ interface ChartSummaryInput {
1136
1321
  format?: ValueFormat
1137
1322
  /** Wins over `format`. */
1138
1323
  valueFormatter?: ValueFormatter
1324
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1325
+ locale?: string
1326
+ }
1327
+
1328
+ const SUMMARY_TEXT = {
1329
+ ja: {
1330
+ up: "、前の点より増加",
1331
+ down: "、前の点より減少",
1332
+ flat: "、前の点と横ばい",
1333
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1334
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1335
+ },
1336
+ en: {
1337
+ up: ", up from the previous point",
1338
+ down: ", down from the previous point",
1339
+ flat: ", flat from the previous point",
1340
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1341
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1342
+ },
1139
1343
  }
1140
1344
 
1141
1345
  /**
1142
1346
  * One-sentence summary built from the rows, for `summary="auto"`:
1143
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1347
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1144
1348
  * computed from the last two non-empty points of the first series — never
1145
1349
  * written by hand, so it cannot contradict the data. `undefined` when there is
1146
1350
  * no usable point.
@@ -1156,26 +1360,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1156
1360
  }
1157
1361
  const latest = points[points.length - 1]
1158
1362
  if (!latest) return undefined
1159
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1363
+ const locale = resolveLocale(input.locale)
1364
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1365
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1160
1366
  const prev = points[points.length - 2]
1161
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1367
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1162
1368
  const head = input.title ? `${input.title}: ` : ""
1163
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1369
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1164
1370
  }
1165
1371
 
1166
1372
  /**
1167
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1168
- * (largest first). `undefined` when there is nothing to describe.
1373
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1374
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1169
1375
  */
1170
1376
  function buildShareSummary(
1171
1377
  slices: readonly { name: string; value: number }[],
1172
1378
  title?: string,
1379
+ locale?: string,
1173
1380
  ): string | undefined {
1174
1381
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1175
1382
  const top = slices[0]
1176
1383
  if (!top || total <= 0) return undefined
1177
1384
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1178
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1385
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1179
1386
  }
1180
1387
 
1181
1388
  /* ---------- end of inlined code ---------- */
@@ -1215,7 +1422,8 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
1215
1422
  ) {
1216
1423
  // The bar's width thins the running totals that would print over each other.
1217
1424
  const { ref, width } = useChartSurface(forwardedRef, null)
1218
- const valueFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format), [valueFormatterProp, format])
1425
+ const locale = useLocale()
1426
+ const valueFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
1219
1427
  const segments = categoryBarSegments(values)
1220
1428
  const markerPosition = marker ? categoryBarMarkerPosition(marker.value, values) : 0
1221
1429
  const allLabels = showLabels ? categoryBarLabels(values, undefined, marker ? markerPosition : undefined) : []