@squadbase/vantage 0.6.2-dev1.54bde93 → 0.6.2-dev1.5c79ef5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-A23M4GCH.js → build-U7GO26HR.js} +3 -3
- package/dist/{build-A23M4GCH.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-FBHT6W6J.js → check-MRVAIHG5.js} +3 -3
- package/dist/{check-FBHT6W6J.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-5C2SEDPH.js → chunk-PJ4SPXHS.js} +4 -4
- package/dist/{chunk-5C2SEDPH.js.map → chunk-PJ4SPXHS.js.map} +1 -1
- package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
- package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/components/index.d.ts +49 -29
- package/dist/components/index.js +5 -5
- package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
- package/dist/{dev-H4I2JL63.js → dev-O42JFQ64.js} +3 -3
- package/dist/{dev-H4I2JL63.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +14 -14
- package/dist/doc/index.js +198 -84
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-B2O3GJDU.js → doctor-YD3XAAOT.js} +3 -3
- package/dist/{doctor-B2O3GJDU.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
- package/dist/slide/index.d.ts +2 -2
- package/dist/slide/index.js +95 -44
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +6 -6
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +5 -5
- package/docs/en/agent-skills.md +1 -1
- package/docs/ja/agent-skills.md +1 -1
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +344 -85
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +341 -85
- package/registry/ui/bar-list.tsx +332 -77
- package/registry/ui/box-plot-chart.tsx +405 -97
- package/registry/ui/category-bar.tsx +248 -40
- package/registry/ui/chart-frame.tsx +257 -46
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +171 -12
- package/registry/ui/donut-chart.tsx +339 -84
- package/registry/ui/echart.tsx +142 -4
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +293 -52
- package/registry/ui/histogram-chart.tsx +395 -97
- package/registry/ui/kpi-card.tsx +245 -44
- package/registry/ui/legend.tsx +330 -76
- package/registry/ui/line-chart.tsx +344 -85
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +141 -3
- package/registry/ui/paired-chart.tsx +349 -90
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +153 -9
- package/registry/ui/scatter-chart.tsx +391 -100
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +193 -28
- package/registry/ui/tracker.tsx +205 -39
- package/registry/ui/trend-indicator.tsx +149 -6
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +415 -102
- package/skills/vantage-design/SKILL.md +25 -27
- package/skills/vantage-design/references/extract.md +10 -13
- package/skills/vantage-design/references/format.md +12 -173
- package/skills/vantage-design/references/presets.md +0 -10
- package/templates/AGENTS.md +29 -15
- package/templates/agents/page.md +1 -1
- package/templates/agents/slide.md +7 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-57GHR5LN.js.map +0 -1
- package/dist/chunk-6OTTGAPO.js.map +0 -1
- package/dist/chunk-JXFCSF3K.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-QOXXOFGL.js.map +0 -1
|
@@ -32,6 +32,7 @@ import { EChart } from "@squadbase/vantage/components"
|
|
|
32
32
|
import type { Polarity } from "@squadbase/vantage/components"
|
|
33
33
|
import { EMPTY_VALUE, MINUS_SIGN } from "@squadbase/vantage/components"
|
|
34
34
|
import type { ChartCommonProps, NumericKeys, ValueFormatter } from "@squadbase/vantage/components"
|
|
35
|
+
import { useSyncExternalStore } from "react"
|
|
35
36
|
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
36
37
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
37
38
|
import { type ReactNode } from "react"
|
|
@@ -41,6 +42,137 @@ import type { ChartThreshold, ChartAnnotation, ChartEventPayload, ChartForecast,
|
|
|
41
42
|
import { formatValue, toValueFormatter } from "@squadbase/vantage/components"
|
|
42
43
|
import type { ChartColor, ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
43
44
|
|
|
45
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The display language of the parts and the defaults that follow it.
|
|
49
|
+
*
|
|
50
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
51
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
52
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
53
|
+
*
|
|
54
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
55
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
59
|
+
type UiLanguage = "ja" | "en"
|
|
60
|
+
|
|
61
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
62
|
+
const FALLBACK_LOCALE = "en-US"
|
|
63
|
+
|
|
64
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
65
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
66
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
70
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
71
|
+
const value = tag?.trim()
|
|
72
|
+
if (!value) return FALLBACK_LOCALE
|
|
73
|
+
try {
|
|
74
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
75
|
+
} catch {
|
|
76
|
+
return FALLBACK_LOCALE
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
82
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
83
|
+
*/
|
|
84
|
+
function documentLocale(): string {
|
|
85
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
86
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
90
|
+
function resolveLocale(locale?: string | null): string {
|
|
91
|
+
return locale || documentLocale()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Picks the wording table of the locale's language. */
|
|
95
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
96
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
100
|
+
function defaultCurrency(locale?: string | null): string {
|
|
101
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
105
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
106
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
110
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
111
|
+
|
|
112
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
113
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
117
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
118
|
+
* Monday for the rest.
|
|
119
|
+
*/
|
|
120
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
121
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
122
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
123
|
+
try {
|
|
124
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
125
|
+
getWeekInfo?: () => { firstDay: number }
|
|
126
|
+
weekInfo?: { firstDay: number }
|
|
127
|
+
}
|
|
128
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
129
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
130
|
+
} catch {
|
|
131
|
+
// Fall through to the table below.
|
|
132
|
+
}
|
|
133
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
137
|
+
|
|
138
|
+
const listeners = new Set<() => void>()
|
|
139
|
+
let observer: MutationObserver | null = null
|
|
140
|
+
|
|
141
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
142
|
+
function subscribe(onChange: () => void): () => void {
|
|
143
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
144
|
+
listeners.add(onChange)
|
|
145
|
+
if (!observer) {
|
|
146
|
+
observer = new MutationObserver(() => {
|
|
147
|
+
for (const listener of [...listeners]) listener()
|
|
148
|
+
})
|
|
149
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
150
|
+
}
|
|
151
|
+
return () => {
|
|
152
|
+
listeners.delete(onChange)
|
|
153
|
+
if (listeners.size === 0 && observer) {
|
|
154
|
+
observer.disconnect()
|
|
155
|
+
observer = null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
164
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
165
|
+
*/
|
|
166
|
+
function useDocumentLocale(): string {
|
|
167
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
171
|
+
function useLocale(locale?: string | null): string {
|
|
172
|
+
const page = useDocumentLocale()
|
|
173
|
+
return locale || page
|
|
174
|
+
}
|
|
175
|
+
|
|
44
176
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
45
177
|
|
|
46
178
|
/**
|
|
@@ -439,6 +571,32 @@ function useChartSurface<E extends Element>(
|
|
|
439
571
|
|
|
440
572
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
441
573
|
|
|
574
|
+
/** The built-in words of the option builders, by language. */
|
|
575
|
+
const CHART_TEXT = {
|
|
576
|
+
ja: {
|
|
577
|
+
target: "目標",
|
|
578
|
+
forecast: "予測",
|
|
579
|
+
total: "合計",
|
|
580
|
+
provisional: "速報",
|
|
581
|
+
noComparison: "比較なし",
|
|
582
|
+
share: (share: string) => `(${share})`,
|
|
583
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
584
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
585
|
+
},
|
|
586
|
+
en: {
|
|
587
|
+
target: "Target",
|
|
588
|
+
forecast: "Forecast",
|
|
589
|
+
total: "Total",
|
|
590
|
+
provisional: "Preliminary",
|
|
591
|
+
noComparison: "No comparison",
|
|
592
|
+
share: (share: string) => ` (${share})`,
|
|
593
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
594
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
595
|
+
},
|
|
596
|
+
} satisfies Record<UiLanguage, unknown>
|
|
597
|
+
|
|
598
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
599
|
+
|
|
442
600
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
443
601
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
444
602
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -623,7 +781,7 @@ interface CartesianChartConfig {
|
|
|
623
781
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
624
782
|
*/
|
|
625
783
|
axisTicks?: (top: number) => (value: number) => string
|
|
626
|
-
/** Locale of the time-axis labels. Default
|
|
784
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
627
785
|
locale?: string
|
|
628
786
|
type?: CartesianStackType
|
|
629
787
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -861,11 +1019,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
861
1019
|
*/
|
|
862
1020
|
function timeAxisLabels(
|
|
863
1021
|
values: readonly unknown[],
|
|
864
|
-
|
|
1022
|
+
localeArg?: string,
|
|
865
1023
|
shown: (i: number) => boolean = () => true,
|
|
866
1024
|
): string[] | null {
|
|
867
1025
|
const parsed = parseTimeIndex(values)
|
|
868
1026
|
if (!parsed) return null
|
|
1027
|
+
const locale = resolveLocale(localeArg)
|
|
869
1028
|
const { granularity, times } = parsed
|
|
870
1029
|
let prevYear: number | null = null
|
|
871
1030
|
return times.map((t, i) => {
|
|
@@ -888,9 +1047,10 @@ function timeAxisLabels(
|
|
|
888
1047
|
}
|
|
889
1048
|
|
|
890
1049
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
891
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1050
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
892
1051
|
const parsed = parseTimeIndex(values)
|
|
893
1052
|
if (!parsed) return null
|
|
1053
|
+
const locale = resolveLocale(localeArg)
|
|
894
1054
|
const options: Intl.DateTimeFormatOptions =
|
|
895
1055
|
parsed.granularity === "year"
|
|
896
1056
|
? { year: "numeric" }
|
|
@@ -1030,7 +1190,9 @@ function tooltipHtml(
|
|
|
1030
1190
|
rows: readonly TooltipRow[],
|
|
1031
1191
|
valueFormatter: (value: number) => string,
|
|
1032
1192
|
notes: readonly string[] = [],
|
|
1193
|
+
locale?: string,
|
|
1033
1194
|
): string {
|
|
1195
|
+
const t = chartText(locale)
|
|
1034
1196
|
const lines = rows.map(
|
|
1035
1197
|
(row) =>
|
|
1036
1198
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1039,7 +1201,7 @@ function tooltipHtml(
|
|
|
1039
1201
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1040
1202
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1041
1203
|
}${
|
|
1042
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
1204
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1043
1205
|
}</span></div>` +
|
|
1044
1206
|
(row.note === undefined
|
|
1045
1207
|
? ""
|
|
@@ -1053,8 +1215,10 @@ function tooltipHtml(
|
|
|
1053
1215
|
|
|
1054
1216
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1055
1217
|
|
|
1056
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1057
|
-
|
|
1218
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
1219
|
+
function barTargetLabel(locale?: string): string {
|
|
1220
|
+
return chartText(locale).target
|
|
1221
|
+
}
|
|
1058
1222
|
|
|
1059
1223
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1060
1224
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1105,16 +1269,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1105
1269
|
return (value / target) * 100
|
|
1106
1270
|
}
|
|
1107
1271
|
|
|
1108
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
1272
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1109
1273
|
function barTargetNote(
|
|
1110
1274
|
label: string,
|
|
1111
1275
|
value: number | null,
|
|
1112
1276
|
target: number | null,
|
|
1113
1277
|
valueFormatter: (value: number) => string,
|
|
1278
|
+
locale?: string,
|
|
1114
1279
|
): string | null {
|
|
1115
1280
|
if (target === null) return null
|
|
1116
1281
|
const rate = achievementRate(value, target)
|
|
1117
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
1282
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1118
1283
|
}
|
|
1119
1284
|
|
|
1120
1285
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1206,11 +1371,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1206
1371
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1207
1372
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1208
1373
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* 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.
|
|
1376
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
1377
|
+
*/
|
|
1378
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
1379
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1214
1380
|
}
|
|
1215
1381
|
|
|
1216
1382
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -1250,8 +1416,10 @@ interface AxisExtent {
|
|
|
1250
1416
|
max: number
|
|
1251
1417
|
}
|
|
1252
1418
|
|
|
1253
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
1254
|
-
|
|
1419
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
1420
|
+
function stackTotalLabel(locale?: string): string {
|
|
1421
|
+
return chartText(locale).total
|
|
1422
|
+
}
|
|
1255
1423
|
|
|
1256
1424
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
1257
1425
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -1373,8 +1541,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
1373
1541
|
|
|
1374
1542
|
/**
|
|
1375
1543
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
1376
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
1377
|
-
* either value is missing or the baseline is 0.
|
|
1544
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
1545
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
1378
1546
|
*/
|
|
1379
1547
|
function comparisonNote(
|
|
1380
1548
|
current: number | null,
|
|
@@ -1382,16 +1550,18 @@ function comparisonNote(
|
|
|
1382
1550
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
1383
1551
|
grain?: PeriodGrain,
|
|
1384
1552
|
format?: ValueFormat,
|
|
1385
|
-
|
|
1553
|
+
localeArg?: string,
|
|
1386
1554
|
): string {
|
|
1555
|
+
const locale = resolveLocale(localeArg)
|
|
1387
1556
|
const result = computeComparison(
|
|
1388
1557
|
current,
|
|
1389
1558
|
comparison.vs === "custom"
|
|
1390
1559
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
1391
1560
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
1561
|
+
{ locale },
|
|
1392
1562
|
)
|
|
1393
1563
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
1394
|
-
if (text === null) return
|
|
1564
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
1395
1565
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
1396
1566
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
1397
1567
|
}
|
|
@@ -1434,7 +1604,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1434
1604
|
toneColors,
|
|
1435
1605
|
valueFormatter,
|
|
1436
1606
|
axisFormatter = valueFormatter,
|
|
1437
|
-
locale = DEFAULT_LOCALE,
|
|
1438
1607
|
type = "default",
|
|
1439
1608
|
layout = "horizontal",
|
|
1440
1609
|
curveType = "linear",
|
|
@@ -1471,7 +1640,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1471
1640
|
chartOptions,
|
|
1472
1641
|
reserveActions = false,
|
|
1473
1642
|
} = cfg
|
|
1474
|
-
const
|
|
1643
|
+
const locale = resolveLocale(cfg.locale)
|
|
1644
|
+
const text = chartText(locale)
|
|
1645
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
1475
1646
|
|
|
1476
1647
|
const stacked = kind !== "line" && type !== "default"
|
|
1477
1648
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -1620,9 +1791,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1620
1791
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
1621
1792
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
1622
1793
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
1623
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
1794
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
1624
1795
|
const highlightText = (value: number) =>
|
|
1625
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
1796
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
1626
1797
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
1627
1798
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
1628
1799
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -1648,7 +1819,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1648
1819
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
1649
1820
|
const barTargets: (number | null)[] | null =
|
|
1650
1821
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
1651
|
-
const barTargetName = targetLabel ??
|
|
1822
|
+
const barTargetName = targetLabel ?? text.target
|
|
1652
1823
|
const barTargetColor = labelColor ?? guideColor
|
|
1653
1824
|
|
|
1654
1825
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -1862,7 +2033,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1862
2033
|
}
|
|
1863
2034
|
|
|
1864
2035
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
1865
|
-
const forecastLabel = forecast?.label ??
|
|
2036
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
1866
2037
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
1867
2038
|
const forecastValues: (number | null)[] = []
|
|
1868
2039
|
if (forecast) {
|
|
@@ -2338,7 +2509,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2338
2509
|
})
|
|
2339
2510
|
}
|
|
2340
2511
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
2341
|
-
rows.push({ name:
|
|
2512
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
2342
2513
|
}
|
|
2343
2514
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
2344
2515
|
if (provisional) {
|
|
@@ -2354,19 +2525,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2354
2525
|
...noteFor(at),
|
|
2355
2526
|
})
|
|
2356
2527
|
})
|
|
2357
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
2528
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
2358
2529
|
}
|
|
2359
2530
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
2360
2531
|
const lines: string[] = []
|
|
2361
2532
|
if (barTargets && i <= lastActual) {
|
|
2362
2533
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
2363
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
2534
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
2364
2535
|
if (note) lines.push(note)
|
|
2365
2536
|
}
|
|
2366
2537
|
for (const note of notes) {
|
|
2367
2538
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
2368
2539
|
}
|
|
2369
|
-
return tooltipHtml(provisional ?
|
|
2540
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
2370
2541
|
},
|
|
2371
2542
|
},
|
|
2372
2543
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -2458,8 +2629,6 @@ interface DonutOptionSlice {
|
|
|
2458
2629
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
2459
2630
|
const DONUT_OUTSIDE_LABELS = 2
|
|
2460
2631
|
|
|
2461
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
2462
|
-
|
|
2463
2632
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
2464
2633
|
|
|
2465
2634
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -2492,15 +2661,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
2492
2661
|
}
|
|
2493
2662
|
|
|
2494
2663
|
/**
|
|
2495
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
2664
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
2496
2665
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
2497
2666
|
* string (`value`, 「9.2万 セッション」).
|
|
2498
2667
|
*/
|
|
2499
2668
|
function donutCenterText(
|
|
2500
2669
|
total: number,
|
|
2501
2670
|
valueFormatter: (value: number) => string,
|
|
2502
|
-
{ totalLabel
|
|
2671
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
2503
2672
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
2673
|
+
totalLabel ??= chartText(locale).total
|
|
2504
2674
|
const amount = valueFormatter(total)
|
|
2505
2675
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
2506
2676
|
}
|
|
@@ -2728,8 +2898,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
2728
2898
|
|
|
2729
2899
|
/* ------------------------------------------------------------ provisional */
|
|
2730
2900
|
|
|
2731
|
-
/** The default label for points that are not final yet. */
|
|
2732
|
-
|
|
2901
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
2902
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
2903
|
+
return chartText(locale).provisional
|
|
2904
|
+
}
|
|
2733
2905
|
|
|
2734
2906
|
/**
|
|
2735
2907
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -2867,9 +3039,10 @@ function resolveValueFormatter(
|
|
|
2867
3039
|
function resolveAxisFormatter(
|
|
2868
3040
|
valueFormatter: ValueFormatter | undefined,
|
|
2869
3041
|
format: ValueFormat | undefined,
|
|
2870
|
-
|
|
3042
|
+
localeArg?: string,
|
|
2871
3043
|
): ValueFormatter {
|
|
2872
3044
|
if (valueFormatter) return valueFormatter
|
|
3045
|
+
const locale = resolveLocale(localeArg)
|
|
2873
3046
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
2874
3047
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
2875
3048
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -2878,40 +3051,75 @@ function resolveAxisFormatter(
|
|
|
2878
3051
|
return (value) => formatValue(value, short, locale)
|
|
2879
3052
|
}
|
|
2880
3053
|
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
]
|
|
3054
|
+
interface CompactUnit {
|
|
3055
|
+
token: string
|
|
3056
|
+
size: number
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
3060
|
+
|
|
3061
|
+
/**
|
|
3062
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
3063
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
3064
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
3065
|
+
*/
|
|
3066
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
3067
|
+
const cached = compactUnitCache.get(locale)
|
|
3068
|
+
if (cached !== undefined) return cached
|
|
3069
|
+
let units: CompactUnit[] | null = []
|
|
3070
|
+
try {
|
|
3071
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
3072
|
+
const seen = new Set<string>()
|
|
3073
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
3074
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
3075
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
3076
|
+
if (at === -1) continue
|
|
3077
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
3078
|
+
units = null
|
|
3079
|
+
break
|
|
3080
|
+
}
|
|
3081
|
+
const token = parts[at]!.value
|
|
3082
|
+
if (seen.has(token)) continue
|
|
3083
|
+
seen.add(token)
|
|
3084
|
+
units.push({ token, size: 10 ** exp })
|
|
3085
|
+
}
|
|
3086
|
+
} catch {
|
|
3087
|
+
units = null
|
|
3088
|
+
}
|
|
3089
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
3090
|
+
compactUnitCache.set(locale, sorted)
|
|
3091
|
+
return sorted
|
|
3092
|
+
}
|
|
2887
3093
|
|
|
2888
|
-
const
|
|
3094
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
2889
3095
|
|
|
2890
3096
|
/**
|
|
2891
3097
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
2892
|
-
* that largest tick, so
|
|
2893
|
-
* Only the
|
|
2894
|
-
* any other format —
|
|
3098
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
3099
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
3100
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
3101
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
2895
3102
|
*/
|
|
2896
3103
|
function resolveAxisTickFormatter(
|
|
2897
3104
|
valueFormatter: ValueFormatter | undefined,
|
|
2898
3105
|
format: ValueFormat | undefined,
|
|
2899
|
-
|
|
3106
|
+
localeArg?: string,
|
|
2900
3107
|
): (top: number) => ValueFormatter {
|
|
3108
|
+
const locale = resolveLocale(localeArg)
|
|
2901
3109
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
2902
3110
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
2903
|
-
const
|
|
2904
|
-
|
|
2905
|
-
if (!aligns) return () => base
|
|
3111
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
3112
|
+
if (!units) return () => base
|
|
2906
3113
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
3114
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
2907
3115
|
return (top) => {
|
|
2908
|
-
const unit =
|
|
3116
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
2909
3117
|
if (!unit) return base
|
|
2910
3118
|
return (value) => {
|
|
2911
3119
|
const text = base(value)
|
|
2912
3120
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
2913
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
2914
|
-
return text.replace(
|
|
3121
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
3122
|
+
return text.replace(numberToken, () => scaled)
|
|
2915
3123
|
}
|
|
2916
3124
|
}
|
|
2917
3125
|
}
|
|
@@ -2940,8 +3148,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
2940
3148
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
2941
3149
|
const MAX_CHART_SERIES = 5
|
|
2942
3150
|
|
|
2943
|
-
/** The series name the folded categories get. */
|
|
2944
|
-
const
|
|
3151
|
+
/** The series name the folded categories get, by language. */
|
|
3152
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
3153
|
+
|
|
3154
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
3155
|
+
function otherSeriesLabel(locale?: string): string {
|
|
3156
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
3160
|
+
|
|
3161
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
3162
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
3163
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
3164
|
+
}
|
|
2945
3165
|
|
|
2946
3166
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
2947
3167
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -2961,7 +3181,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
2961
3181
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
2962
3182
|
items: readonly Item[],
|
|
2963
3183
|
n: number = MAX_CHART_SERIES,
|
|
2964
|
-
otherLabel: string =
|
|
3184
|
+
otherLabel: string = otherSeriesLabel(),
|
|
2965
3185
|
): AggregatedItems<Item> {
|
|
2966
3186
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
2967
3187
|
const ranked = items
|
|
@@ -2990,7 +3210,7 @@ function aggregateSeries(
|
|
|
2990
3210
|
data: readonly Record<string, unknown>[],
|
|
2991
3211
|
categories: readonly string[],
|
|
2992
3212
|
n: number = MAX_CHART_SERIES,
|
|
2993
|
-
otherLabel: string =
|
|
3213
|
+
otherLabel: string = otherSeriesLabel(),
|
|
2994
3214
|
): AggregatedSeries {
|
|
2995
3215
|
const totals = categories.map((name) => ({
|
|
2996
3216
|
name,
|
|
@@ -3022,12 +3242,14 @@ function aggregateSeriesKeeping(
|
|
|
3022
3242
|
categories: readonly string[],
|
|
3023
3243
|
keep: string | undefined,
|
|
3024
3244
|
n: number = MAX_CHART_SERIES,
|
|
3245
|
+
otherLabel: string = otherSeriesLabel(),
|
|
3025
3246
|
): AggregatedSeries {
|
|
3026
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
3247
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
3027
3248
|
const aggregated = aggregateSeries(
|
|
3028
3249
|
data,
|
|
3029
3250
|
categories.filter((c) => c !== keep),
|
|
3030
3251
|
n,
|
|
3252
|
+
otherLabel,
|
|
3031
3253
|
)
|
|
3032
3254
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
3033
3255
|
}
|
|
@@ -3063,7 +3285,7 @@ function seriesColorsWithComparison(
|
|
|
3063
3285
|
* Colour per drawn series: the record's token for that category, else the
|
|
3064
3286
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
3065
3287
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
3066
|
-
* data itself names
|
|
3288
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
3067
3289
|
*/
|
|
3068
3290
|
function seriesColorsFor(
|
|
3069
3291
|
categories: readonly string[],
|
|
@@ -3075,7 +3297,7 @@ function seriesColorsFor(
|
|
|
3075
3297
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
3076
3298
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
3077
3299
|
return categories.map((category, i) => {
|
|
3078
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
3300
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
3079
3301
|
const fromRecord = record?.[category]
|
|
3080
3302
|
if (fromRecord) return fromRecord
|
|
3081
3303
|
if (list && list.length > 0) {
|
|
@@ -3122,7 +3344,7 @@ interface DonutSlicesConfig {
|
|
|
3122
3344
|
*/
|
|
3123
3345
|
function donutSlices(
|
|
3124
3346
|
data: readonly Record<string, unknown>[],
|
|
3125
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
3347
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
3126
3348
|
): DonutSlice[] {
|
|
3127
3349
|
const items = data
|
|
3128
3350
|
.map((row, i) => {
|
|
@@ -3135,7 +3357,8 @@ function donutSlices(
|
|
|
3135
3357
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
3136
3358
|
name: item.name,
|
|
3137
3359
|
value: item.value,
|
|
3138
|
-
|
|
3360
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
3361
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
3139
3362
|
isOther: false,
|
|
3140
3363
|
row: item.row,
|
|
3141
3364
|
}))
|
|
@@ -3208,7 +3431,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
3208
3431
|
*/
|
|
3209
3432
|
function barListRows<Row extends object>(
|
|
3210
3433
|
data: readonly Row[],
|
|
3211
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
3434
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
3212
3435
|
): BarListRow<Row>[] {
|
|
3213
3436
|
const items = data.map((row): BarListRow<Row> => {
|
|
3214
3437
|
const cells = row as Record<string, unknown>
|
|
@@ -3254,19 +3477,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
3254
3477
|
|
|
3255
3478
|
/**
|
|
3256
3479
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
3257
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
3480
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
3258
3481
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
3259
3482
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
3260
3483
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
3261
3484
|
* keeps its contrast.
|
|
3262
3485
|
*/
|
|
3263
3486
|
function barListFills(
|
|
3264
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
3487
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
3265
3488
|
color: ChartColor,
|
|
3266
3489
|
otherColor: string,
|
|
3267
3490
|
): string[] {
|
|
3268
3491
|
return rows.map((row) =>
|
|
3269
|
-
row.isOther
|
|
3492
|
+
row.isOther || isOtherName(row.name)
|
|
3493
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
3494
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
3270
3495
|
)
|
|
3271
3496
|
}
|
|
3272
3497
|
|
|
@@ -3482,11 +3707,30 @@ interface ChartSummaryInput {
|
|
|
3482
3707
|
format?: ValueFormat
|
|
3483
3708
|
/** Wins over `format`. */
|
|
3484
3709
|
valueFormatter?: ValueFormatter
|
|
3710
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
3711
|
+
locale?: string
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
const SUMMARY_TEXT = {
|
|
3715
|
+
ja: {
|
|
3716
|
+
up: "、前の点より増加",
|
|
3717
|
+
down: "、前の点より減少",
|
|
3718
|
+
flat: "、前の点と横ばい",
|
|
3719
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
3720
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
3721
|
+
},
|
|
3722
|
+
en: {
|
|
3723
|
+
up: ", up from the previous point",
|
|
3724
|
+
down: ", down from the previous point",
|
|
3725
|
+
flat: ", flat from the previous point",
|
|
3726
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
3727
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
3728
|
+
},
|
|
3485
3729
|
}
|
|
3486
3730
|
|
|
3487
3731
|
/**
|
|
3488
3732
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
3489
|
-
* 「<title>: 最新 <index> は
|
|
3733
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
3490
3734
|
* computed from the last two non-empty points of the first series — never
|
|
3491
3735
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
3492
3736
|
* no usable point.
|
|
@@ -3502,26 +3746,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
3502
3746
|
}
|
|
3503
3747
|
const latest = points[points.length - 1]
|
|
3504
3748
|
if (!latest) return undefined
|
|
3505
|
-
const
|
|
3749
|
+
const locale = resolveLocale(input.locale)
|
|
3750
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
3751
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
3506
3752
|
const prev = points[points.length - 2]
|
|
3507
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
3753
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
3508
3754
|
const head = input.title ? `${input.title}: ` : ""
|
|
3509
|
-
return `${head}
|
|
3755
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
3510
3756
|
}
|
|
3511
3757
|
|
|
3512
3758
|
/**
|
|
3513
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
3514
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
3759
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
3760
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
3515
3761
|
*/
|
|
3516
3762
|
function buildShareSummary(
|
|
3517
3763
|
slices: readonly { name: string; value: number }[],
|
|
3518
3764
|
title?: string,
|
|
3765
|
+
locale?: string,
|
|
3519
3766
|
): string | undefined {
|
|
3520
3767
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
3521
3768
|
const top = slices[0]
|
|
3522
3769
|
if (!top || total <= 0) return undefined
|
|
3523
3770
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
3524
|
-
return `${title ? `${title}: ` : ""}
|
|
3771
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
3525
3772
|
}
|
|
3526
3773
|
|
|
3527
3774
|
/* ---------- end of inlined code ---------- */
|
|
@@ -3588,6 +3835,47 @@ export interface WaterfallChartProps<T extends Record<string, unknown> = Record<
|
|
|
3588
3835
|
|
|
3589
3836
|
/* ------------------------------------------------------------------ model */
|
|
3590
3837
|
|
|
3838
|
+
const TEXT = {
|
|
3839
|
+
ja: {
|
|
3840
|
+
net: "純増",
|
|
3841
|
+
total: "合計",
|
|
3842
|
+
change: "増減",
|
|
3843
|
+
cumulative: "累計",
|
|
3844
|
+
cumulativeChange: "累計増減",
|
|
3845
|
+
item: "項目",
|
|
3846
|
+
start: "開始",
|
|
3847
|
+
end: "終了",
|
|
3848
|
+
fromTo: (from: string, to: string, net: string) => `${from} → ${to}(${net})`,
|
|
3849
|
+
from: (from: string, net: string) => `${from} から ${net}`,
|
|
3850
|
+
to: (to: string, net: string) => `${to}(${net})`,
|
|
3851
|
+
netOnly: (net: string) => `増減の合計 ${net}`,
|
|
3852
|
+
increase: (name: string, value: string) => `最大の増加は ${name}(${value})`,
|
|
3853
|
+
decrease: (name: string, value: string) => `最大の減少は ${name}(${value})`,
|
|
3854
|
+
sentenceJoin: "。",
|
|
3855
|
+
chart: (label: string) => `${label}のウォーターフォールチャート`,
|
|
3856
|
+
},
|
|
3857
|
+
en: {
|
|
3858
|
+
net: "Net change",
|
|
3859
|
+
total: "Total",
|
|
3860
|
+
change: "Change",
|
|
3861
|
+
cumulative: "Cumulative",
|
|
3862
|
+
cumulativeChange: "Cumulative change",
|
|
3863
|
+
item: "Item",
|
|
3864
|
+
start: "Start",
|
|
3865
|
+
end: "End",
|
|
3866
|
+
fromTo: (from: string, to: string, net: string) => `${from} → ${to} (${net})`,
|
|
3867
|
+
from: (from: string, net: string) => `${net} from ${from}`,
|
|
3868
|
+
to: (to: string, net: string) => `${to} (${net})`,
|
|
3869
|
+
netOnly: (net: string) => `Net change ${net}`,
|
|
3870
|
+
increase: (name: string, value: string) => `Largest increase: ${name} (${value})`,
|
|
3871
|
+
decrease: (name: string, value: string) => `Largest decrease: ${name} (${value})`,
|
|
3872
|
+
sentenceJoin: ". ",
|
|
3873
|
+
chart: (label: string) => `Waterfall chart of ${label}`,
|
|
3874
|
+
},
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
const textOf = (locale?: string) => TEXT[uiLanguage(resolveLocale(locale))]
|
|
3878
|
+
|
|
3591
3879
|
export type WaterfallKind = "total" | "increase" | "decrease" | "flat" | "gap"
|
|
3592
3880
|
|
|
3593
3881
|
export interface WaterfallStep {
|
|
@@ -3609,7 +3897,7 @@ export interface WaterfallStep {
|
|
|
3609
3897
|
*/
|
|
3610
3898
|
export function waterfallSteps(
|
|
3611
3899
|
data: readonly Record<string, unknown>[],
|
|
3612
|
-
config: { index: string; value: string; totals?: readonly string[]; mode?: WaterfallMode },
|
|
3900
|
+
config: { index: string; value: string; totals?: readonly string[]; mode?: WaterfallMode; locale?: string },
|
|
3613
3901
|
): WaterfallStep[] {
|
|
3614
3902
|
const totals = new Set(config.totals ?? [])
|
|
3615
3903
|
const delta = config.mode === "delta"
|
|
@@ -3630,13 +3918,15 @@ export function waterfallSteps(
|
|
|
3630
3918
|
})
|
|
3631
3919
|
// `delta`: the net change closes the plot as a total bar from 0 (series colour 1), so the moves add up to something.
|
|
3632
3920
|
if (delta && steps.some((s) => s.kind !== "gap")) {
|
|
3633
|
-
steps.push({ name:
|
|
3921
|
+
steps.push({ name: deltaTotalName(config.locale), kind: "total", value: running, start: 0, end: running, row: {} })
|
|
3634
3922
|
}
|
|
3635
3923
|
return steps
|
|
3636
3924
|
}
|
|
3637
3925
|
|
|
3638
|
-
/** The name of the net-change bar that `mode="delta"` appends. */
|
|
3639
|
-
export
|
|
3926
|
+
/** The name of the net-change bar that `mode="delta"` appends, in `locale` ("Net change" / 「純増」). Default the page's locale. */
|
|
3927
|
+
export function deltaTotalName(locale?: string): string {
|
|
3928
|
+
return textOf(locale).net
|
|
3929
|
+
}
|
|
3640
3930
|
|
|
3641
3931
|
/** What `mode="delta"` leaves out of the bars: the opening / closing totals and the net change of the moves. */
|
|
3642
3932
|
export interface WaterfallDeltaEnds {
|
|
@@ -3674,16 +3964,22 @@ export function waterfallDeltaEnds(
|
|
|
3674
3964
|
}
|
|
3675
3965
|
|
|
3676
3966
|
/** The note above a delta plot: `前期 ¥2.2億 → 当期 ¥2.2億(+¥130万)`; without the totals' values, the net change alone. */
|
|
3677
|
-
export function waterfallDeltaNote(
|
|
3967
|
+
export function waterfallDeltaNote(
|
|
3968
|
+
ends: WaterfallDeltaEnds,
|
|
3969
|
+
formatter: ValueFormatter,
|
|
3970
|
+
labels?: Record<string, string>,
|
|
3971
|
+
locale?: string,
|
|
3972
|
+
): string {
|
|
3973
|
+
const t = textOf(locale)
|
|
3678
3974
|
const net = signedText(ends.net, formatter)
|
|
3679
3975
|
const { opening, closing } = ends
|
|
3680
3976
|
const side = (e: { name: string; value: number | null } | null) => (e && e.value !== null ? `${labelOf(e.name, labels)} ${formatter(e.value)}` : null)
|
|
3681
3977
|
const from = side(opening)
|
|
3682
3978
|
const to = side(closing)
|
|
3683
|
-
if (from && to) return
|
|
3684
|
-
if (from) return
|
|
3685
|
-
if (to) return
|
|
3686
|
-
return
|
|
3979
|
+
if (from && to) return t.fromTo(from, to, net)
|
|
3980
|
+
if (from) return t.from(from, net)
|
|
3981
|
+
if (to) return t.to(to, net)
|
|
3982
|
+
return t.netOnly(net)
|
|
3687
3983
|
}
|
|
3688
3984
|
|
|
3689
3985
|
export interface WaterfallStack {
|
|
@@ -3767,7 +4063,9 @@ export function buildWaterfallSummary(
|
|
|
3767
4063
|
title?: string,
|
|
3768
4064
|
labels?: Record<string, string>,
|
|
3769
4065
|
delta?: WaterfallDeltaEnds,
|
|
4066
|
+
locale?: string,
|
|
3770
4067
|
): string | undefined {
|
|
4068
|
+
const t = textOf(locale)
|
|
3771
4069
|
const real = steps.filter((s) => s.kind !== "gap")
|
|
3772
4070
|
const first = real[0]
|
|
3773
4071
|
const last = real[real.length - 1]
|
|
@@ -3777,7 +4075,7 @@ export function buildWaterfallSummary(
|
|
|
3777
4075
|
// A first step starts at 0, so its end is its value; a total's end is the total. `delta`: the note (totals and net change) instead.
|
|
3778
4076
|
const parts = [
|
|
3779
4077
|
delta
|
|
3780
|
-
? waterfallDeltaNote(delta, formatter, labels)
|
|
4078
|
+
? waterfallDeltaNote(delta, formatter, labels, locale)
|
|
3781
4079
|
: real.length === 1
|
|
3782
4080
|
? `${name(first)} ${formatter(first.end)}`
|
|
3783
4081
|
: `${name(first)} ${formatter(first.end)} → ${name(last)} ${formatter(last.end)}`,
|
|
@@ -3788,9 +4086,9 @@ export function buildWaterfallSummary(
|
|
|
3788
4086
|
if (s.kind === "increase" && (!up || s.value! > up.value!)) up = s
|
|
3789
4087
|
if (s.kind === "decrease" && (!down || s.value! < down.value!)) down = s
|
|
3790
4088
|
}
|
|
3791
|
-
if (up) parts.push(
|
|
3792
|
-
if (down) parts.push(
|
|
3793
|
-
return `${head}${parts.join(
|
|
4089
|
+
if (up) parts.push(t.increase(name(up), signedText(up.value!, formatter)))
|
|
4090
|
+
if (down) parts.push(t.decrease(name(down), signedText(down.value!, formatter)))
|
|
4091
|
+
return `${head}${parts.join(t.sentenceJoin)}`
|
|
3794
4092
|
}
|
|
3795
4093
|
|
|
3796
4094
|
/** The screen-reader table: one row per step with its change and the running total. */
|
|
@@ -3800,19 +4098,21 @@ export function waterfallDataTable(
|
|
|
3800
4098
|
formatter: ValueFormatter,
|
|
3801
4099
|
labels?: Record<string, string>,
|
|
3802
4100
|
delta?: WaterfallDeltaEnds,
|
|
4101
|
+
locale?: string,
|
|
3803
4102
|
): ChartDataTable {
|
|
4103
|
+
const t = textOf(locale)
|
|
3804
4104
|
const label = (key: string) => labelOf(key, labels)
|
|
3805
4105
|
const rows = steps.map((s) => [
|
|
3806
4106
|
label(s.name),
|
|
3807
|
-
s.kind === "total" ? (delta ?
|
|
4107
|
+
s.kind === "total" ? (delta ? t.net : t.total) : s.value === null ? EMPTY_VALUE : signedText(s.value, formatter),
|
|
3808
4108
|
s.kind === "gap" ? EMPTY_VALUE : delta ? signedText(s.end, formatter) : formatter(s.end),
|
|
3809
4109
|
])
|
|
3810
4110
|
// `delta`: the totals are not bars, so the opening / closing values are rows of their own (they are not in `steps`).
|
|
3811
4111
|
const end = (e: WaterfallDeltaEnds["opening"], role: string) =>
|
|
3812
4112
|
e && e.value !== null ? [[label(e.name), role, formatter(e.value)]] : []
|
|
3813
4113
|
return {
|
|
3814
|
-
columns: [labels?.[names.index] ??
|
|
3815
|
-
rows: delta ? [...end(delta.opening,
|
|
4114
|
+
columns: [labels?.[names.index] ?? t.item, t.change, delta ? t.cumulativeChange : t.cumulative],
|
|
4115
|
+
rows: delta ? [...end(delta.opening, t.start), ...rows, ...end(delta.closing, t.end)] : rows,
|
|
3816
4116
|
}
|
|
3817
4117
|
}
|
|
3818
4118
|
|
|
@@ -3847,6 +4147,8 @@ export interface WaterfallOptionConfig {
|
|
|
3847
4147
|
noteColor?: string
|
|
3848
4148
|
/** Colour of the 1px line joining the bars' tops (the border token). `undefined` draws no connector. */
|
|
3849
4149
|
connectorColor?: string
|
|
4150
|
+
/** Language of the tooltip. Default the page's locale (`<html lang>`). */
|
|
4151
|
+
locale?: string
|
|
3850
4152
|
}
|
|
3851
4153
|
|
|
3852
4154
|
/** The bar fills 45% of its category band. ECharts reads `barCategoryGap` as a share of the band (not of the bar), so the gap is the other 55%; a gap over 100% leaves the bar at its 1px minimum. */
|
|
@@ -3987,6 +4289,7 @@ const ROW_STYLE = "display:flex;align-items:center;gap:8px;line-height:20px"
|
|
|
3987
4289
|
/** The ECharts option: a transparent base plus the part above and below 0, coloured per step. */
|
|
3988
4290
|
export function buildWaterfallOption(config: WaterfallOptionConfig): EChartsOption {
|
|
3989
4291
|
const { steps, colors, polarity, labels, valueFormatter, showValues = true, showTooltip = true } = config
|
|
4292
|
+
const t = textOf(config.locale)
|
|
3990
4293
|
const delta = config.mode === "delta"
|
|
3991
4294
|
const axisFormatter = config.axisFormatter ?? valueFormatter
|
|
3992
4295
|
const noteWidth = config.plotWidth ? Math.max(120, config.plotWidth - 16) : undefined
|
|
@@ -4068,9 +4371,13 @@ export function buildWaterfallOption(config: WaterfallOptionConfig): EChartsOpti
|
|
|
4068
4371
|
`<span style="font-weight:500;font-variant-numeric:tabular-nums;margin-left:12px">${escapeHtml(text)}</span></div>`
|
|
4069
4372
|
const rows =
|
|
4070
4373
|
step.kind === "total"
|
|
4071
|
-
? line(delta ?
|
|
4072
|
-
: line(
|
|
4073
|
-
(step.kind === "gap"
|
|
4374
|
+
? line(delta ? t.net : t.total, delta ? signedText(step.end, valueFormatter) : valueFormatter(step.end))
|
|
4375
|
+
: line(t.change, waterfallStepText(step, valueFormatter)) +
|
|
4376
|
+
(step.kind === "gap"
|
|
4377
|
+
? ""
|
|
4378
|
+
: delta
|
|
4379
|
+
? line(t.cumulativeChange, signedText(step.end, valueFormatter))
|
|
4380
|
+
: line(t.cumulative, valueFormatter(step.end)))
|
|
4074
4381
|
return `<div style="font-weight:500;margin-bottom:4px">${escapeHtml(labelOf(step.name, labels))}</div>${rows}`
|
|
4075
4382
|
},
|
|
4076
4383
|
},
|
|
@@ -4170,11 +4477,15 @@ function WaterfallChartImpl<T extends Record<string, unknown>>(
|
|
|
4170
4477
|
const valueKey: string = categories[0] ?? ""
|
|
4171
4478
|
const { ref: frameRef, width: plotWidth, tokens } = useChartSurface(ref)
|
|
4172
4479
|
// Value labels and the tooltip read in full (as `format` says); ticks read short.
|
|
4173
|
-
const
|
|
4174
|
-
const
|
|
4175
|
-
const
|
|
4480
|
+
const locale = useLocale()
|
|
4481
|
+
const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
|
|
4482
|
+
const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
|
|
4483
|
+
const steps = useMemo(
|
|
4484
|
+
() => waterfallSteps(data, { index, value: valueKey, totals, mode, locale }),
|
|
4485
|
+
[data, index, valueKey, totals, mode, locale],
|
|
4486
|
+
)
|
|
4176
4487
|
const ends = useMemo(() => (mode === "delta" ? waterfallDeltaEnds(data, { index, value: valueKey, totals }) : undefined), [data, index, valueKey, totals, mode])
|
|
4177
|
-
const deltaNote = useMemo(() => (ends ? waterfallDeltaNote(ends, valueFormatter, labels) : undefined), [ends, valueFormatter, labels])
|
|
4488
|
+
const deltaNote = useMemo(() => (ends ? waterfallDeltaNote(ends, valueFormatter, labels, locale) : undefined), [ends, valueFormatter, labels, locale])
|
|
4178
4489
|
const colors = useMemo(() => {
|
|
4179
4490
|
const fills = resolveFillColors(tokens)
|
|
4180
4491
|
const [total = ""] = resolveChartColors(["chart-1"], tokens)
|
|
@@ -4207,18 +4518,20 @@ function WaterfallChartImpl<T extends Record<string, unknown>>(
|
|
|
4207
4518
|
minValue,
|
|
4208
4519
|
maxValue,
|
|
4209
4520
|
yAxisWidth,
|
|
4521
|
+
locale,
|
|
4210
4522
|
}),
|
|
4211
|
-
[steps, colors, polarity, labels, valueFormatter, axisFormatter, showValues, showTooltip, tokens, mode, deltaNote, plotWidth, chartOptions, minValue, maxValue, yAxisWidth],
|
|
4523
|
+
[steps, colors, polarity, labels, valueFormatter, axisFormatter, showValues, showTooltip, tokens, mode, deltaNote, plotWidth, chartOptions, minValue, maxValue, yAxisWidth, locale],
|
|
4212
4524
|
)
|
|
4213
4525
|
const dataTable = useMemo(
|
|
4214
|
-
() => waterfallDataTable(steps, { index, value: valueKey }, valueFormatter, labels, ends),
|
|
4215
|
-
[steps, index, valueKey, valueFormatter, labels, ends],
|
|
4526
|
+
() => waterfallDataTable(steps, { index, value: valueKey }, valueFormatter, labels, ends, locale),
|
|
4527
|
+
[steps, index, valueKey, valueFormatter, labels, ends, locale],
|
|
4216
4528
|
)
|
|
4217
4529
|
const resolvedSummary = useMemo(
|
|
4218
|
-
() => (summary === "auto" ? buildWaterfallSummary(steps, valueFormatter, title, labels, ends) : summary),
|
|
4219
|
-
[summary, steps, valueFormatter, title, labels, ends],
|
|
4530
|
+
() => (summary === "auto" ? buildWaterfallSummary(steps, valueFormatter, title, labels, ends, locale) : summary),
|
|
4531
|
+
[summary, steps, valueFormatter, title, labels, ends, locale],
|
|
4220
4532
|
)
|
|
4221
|
-
const
|
|
4533
|
+
const t = textOf(locale)
|
|
4534
|
+
const label = ariaLabel ?? t.chart(labelOf(valueKey, labels) || t.change.toLowerCase())
|
|
4222
4535
|
return (
|
|
4223
4536
|
<ChartFrame
|
|
4224
4537
|
{...chartRootProps(rest)}
|