@squadbase/vantage 0.6.2-dev1.5aae04e → 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-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-3RJEOP3J.js +174 -0
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
- package/dist/chunk-CKWPK4RO.js.map +1 -0
- package/dist/chunk-EG4PX726.js +678 -0
- 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-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
- package/dist/chunk-QNOX7WLZ.js.map +1 -0
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
- package/dist/chunk-SHZM3EXB.js.map +1 -0
- package/dist/chunk-TRMHNMMZ.js +61 -0
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +57 -273
- package/dist/components/index.js +6 -3
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +422 -4
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/query/index.js +2 -2
- package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
- package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1023 -2
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +9 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -2
- package/dist/ui/index.js +6 -6
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
- package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +2 -0
- package/docs/en/components.md +13 -2
- package/docs/en/doc/delta-text.md +40 -0
- package/docs/en/doc/doc-action-items.md +49 -0
- package/docs/en/doc/doc-callout.md +38 -0
- package/docs/en/doc/doc-data-table.md +60 -0
- package/docs/en/doc/doc-executive-summary.md +64 -0
- package/docs/en/doc/doc-figure.md +56 -0
- package/docs/en/doc/doc-key-findings.md +61 -0
- package/docs/en/doc/doc-kpi-scorecard.md +80 -0
- package/docs/en/doc/doc-notes-and-sources.md +43 -0
- package/docs/en/doc/doc-report-header.md +46 -0
- package/docs/en/pages-and-metadata.md +15 -0
- package/docs/en/parts/area-chart.md +99 -0
- package/docs/en/parts/bar-chart.md +91 -0
- package/docs/en/parts/bar-list.md +50 -0
- package/docs/en/parts/category-bar.md +37 -0
- package/docs/en/parts/chart-frame.md +39 -0
- package/docs/en/parts/delta.md +4 -3
- package/docs/en/parts/donut-chart.md +72 -0
- package/docs/en/parts/funnel-steps.md +2 -1
- package/docs/en/parts/heatmap.md +53 -0
- package/docs/en/parts/kpi-card.md +2 -1
- package/docs/en/parts/legend.md +35 -0
- package/docs/en/parts/line-chart.md +94 -0
- package/docs/en/parts/progress-circle.md +33 -0
- package/docs/en/parts/sample-data.md +39 -0
- package/docs/en/parts/scatter-chart.md +56 -0
- package/docs/en/parts/tracker.md +36 -0
- package/docs/en/parts/updated-at.md +30 -0
- package/docs/en/parts/waterfall-chart.md +56 -0
- package/docs/en/slide/slide-chart-full.md +48 -0
- package/docs/en/slide/slide-chart-insight.md +52 -0
- package/docs/en/slide/slide-columns.md +46 -0
- package/docs/en/slide/slide-comparison.md +68 -0
- package/docs/en/slide/slide-cover.md +39 -0
- package/docs/en/slide/slide-exec-summary.md +47 -0
- package/docs/en/slide/slide-heading.md +38 -0
- package/docs/en/slide/slide-kpi-grid.md +55 -0
- package/docs/en/slide/slide-kpi-hero.md +58 -0
- package/docs/en/slide/slide-next-actions.md +49 -0
- package/docs/en/slide/slide-scorecard-table.md +54 -0
- package/docs/en/slide/slide-section.md +44 -0
- package/docs/en/slide/slide-source-line.md +34 -0
- package/docs/en/ui/alert-dialog.md +42 -0
- package/docs/en/ui/avatar.md +31 -0
- package/docs/en/ui/combobox.md +74 -0
- package/docs/en/ui/drawer.md +41 -0
- package/docs/en/ui/empty.md +1 -1
- package/docs/en/ui/error-state.md +4 -3
- package/docs/en/ui/field.md +42 -0
- package/docs/en/ui/hover-card.md +21 -0
- package/docs/en/ui/kbd.md +17 -0
- package/docs/en/ui/loading.md +1 -1
- package/docs/en/ui/pagination.md +36 -0
- package/docs/en/ui/radio-group.md +21 -0
- package/docs/en/ui/toast.md +44 -0
- package/docs/en/ui/toggle-group.md +4 -3
- package/docs/index.json +857 -77
- package/docs/ja/agent-skills.md +1 -1
- package/docs/ja/changelog.md +2 -0
- package/docs/ja/components.md +12 -1
- package/docs/ja/doc/delta-text.md +40 -0
- package/docs/ja/doc/doc-action-items.md +49 -0
- package/docs/ja/doc/doc-callout.md +38 -0
- package/docs/ja/doc/doc-data-table.md +60 -0
- package/docs/ja/doc/doc-executive-summary.md +64 -0
- package/docs/ja/doc/doc-figure.md +56 -0
- package/docs/ja/doc/doc-key-findings.md +61 -0
- package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
- package/docs/ja/doc/doc-notes-and-sources.md +43 -0
- package/docs/ja/doc/doc-report-header.md +46 -0
- package/docs/ja/page-types/slide.md +1 -0
- package/docs/ja/pages-and-metadata.md +15 -0
- package/docs/ja/parts/area-chart.md +49 -25
- package/docs/ja/parts/bar-chart.md +50 -24
- package/docs/ja/parts/bar-list.md +31 -8
- package/docs/ja/parts/chart-frame.md +36 -0
- package/docs/ja/parts/delta.md +5 -4
- package/docs/ja/parts/donut-chart.md +45 -26
- package/docs/ja/parts/funnel-steps.md +27 -8
- package/docs/ja/parts/heatmap.md +45 -0
- package/docs/ja/parts/kpi-card.md +2 -1
- package/docs/ja/parts/legend.md +36 -0
- package/docs/ja/parts/line-chart.md +48 -22
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -0
- package/docs/ja/parts/updated-at.md +27 -0
- package/docs/ja/parts/waterfall-chart.md +61 -0
- package/docs/ja/slide/slide-chart-full.md +48 -0
- package/docs/ja/slide/slide-chart-insight.md +52 -0
- package/docs/ja/slide/slide-columns.md +46 -0
- package/docs/ja/slide/slide-comparison.md +68 -0
- package/docs/ja/slide/slide-cover.md +39 -0
- package/docs/ja/slide/slide-exec-summary.md +47 -0
- package/docs/ja/slide/slide-heading.md +38 -0
- package/docs/ja/slide/slide-kpi-grid.md +55 -0
- package/docs/ja/slide/slide-kpi-hero.md +58 -0
- package/docs/ja/slide/slide-next-actions.md +49 -0
- package/docs/ja/slide/slide-scorecard-table.md +54 -0
- package/docs/ja/slide/slide-section.md +44 -0
- package/docs/ja/slide/slide-source-line.md +34 -0
- package/docs/ja/ui/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- package/docs/ja/ui/loading.md +1 -1
- package/docs/ja/ui/toggle-group.md +4 -3
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +389 -104
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +386 -104
- package/registry/ui/bar-list.tsx +376 -94
- package/registry/ui/box-plot-chart.tsx +450 -116
- package/registry/ui/category-bar.tsx +292 -57
- package/registry/ui/chart-frame.tsx +301 -63
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +181 -19
- package/registry/ui/donut-chart.tsx +385 -103
- package/registry/ui/echart.tsx +313 -58
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +338 -71
- package/registry/ui/histogram-chart.tsx +440 -116
- package/registry/ui/kpi-card.tsx +254 -48
- package/registry/ui/legend.tsx +374 -93
- package/registry/ui/line-chart.tsx +389 -104
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +148 -10
- package/registry/ui/paired-chart.tsx +394 -109
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +154 -10
- package/registry/ui/scatter-chart.tsx +436 -119
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +201 -36
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +214 -42
- package/registry/ui/trend-indicator.tsx +151 -8
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +460 -121
- package/skills/vantage-design/SKILL.md +54 -54
- package/skills/vantage-design/references/extract.md +26 -27
- package/skills/vantage-design/references/format.md +12 -172
- package/skills/vantage-design/references/presets.md +50 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +32 -16
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +23 -2
- package/templates/agents/slide.md +77 -3
- package/theme.css +23 -19
- package/dist/chunk-2ICNHUG6.js.map +0 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-WAMZWZJB.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
|
@@ -22,14 +22,147 @@ import { forwardRef, useEffect, useMemo, useState, type ForwardedRef, type React
|
|
|
22
22
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
23
23
|
import { EChart } from "@squadbase/vantage/components"
|
|
24
24
|
import type { ChartCommonProps, ChartEventPayload, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
25
|
-
import {
|
|
25
|
+
import { useSyncExternalStore } from "react"
|
|
26
|
+
import { createContext, useCallback, useRef, type RefObject } from "react"
|
|
26
27
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
27
|
-
import {
|
|
28
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
28
29
|
import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
|
|
29
30
|
import type { EChartsOption } from "@squadbase/vantage/components"
|
|
30
31
|
import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
32
|
+
import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
|
|
31
33
|
import type { ChartThreshold, ChartAnnotation, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
|
|
32
34
|
|
|
35
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The display language of the parts and the defaults that follow it.
|
|
39
|
+
*
|
|
40
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
41
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
42
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
43
|
+
*
|
|
44
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
45
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
49
|
+
type UiLanguage = "ja" | "en"
|
|
50
|
+
|
|
51
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
52
|
+
const FALLBACK_LOCALE = "en-US"
|
|
53
|
+
|
|
54
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
55
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
56
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
60
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
61
|
+
const value = tag?.trim()
|
|
62
|
+
if (!value) return FALLBACK_LOCALE
|
|
63
|
+
try {
|
|
64
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
65
|
+
} catch {
|
|
66
|
+
return FALLBACK_LOCALE
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
72
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
73
|
+
*/
|
|
74
|
+
function documentLocale(): string {
|
|
75
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
76
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
80
|
+
function resolveLocale(locale?: string | null): string {
|
|
81
|
+
return locale || documentLocale()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Picks the wording table of the locale's language. */
|
|
85
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
86
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
90
|
+
function defaultCurrency(locale?: string | null): string {
|
|
91
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
95
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
96
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
100
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
101
|
+
|
|
102
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
103
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
107
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
108
|
+
* Monday for the rest.
|
|
109
|
+
*/
|
|
110
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
111
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
112
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
113
|
+
try {
|
|
114
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
115
|
+
getWeekInfo?: () => { firstDay: number }
|
|
116
|
+
weekInfo?: { firstDay: number }
|
|
117
|
+
}
|
|
118
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
119
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
120
|
+
} catch {
|
|
121
|
+
// Fall through to the table below.
|
|
122
|
+
}
|
|
123
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
127
|
+
|
|
128
|
+
const listeners = new Set<() => void>()
|
|
129
|
+
let observer: MutationObserver | null = null
|
|
130
|
+
|
|
131
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
132
|
+
function subscribe(onChange: () => void): () => void {
|
|
133
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
134
|
+
listeners.add(onChange)
|
|
135
|
+
if (!observer) {
|
|
136
|
+
observer = new MutationObserver(() => {
|
|
137
|
+
for (const listener of [...listeners]) listener()
|
|
138
|
+
})
|
|
139
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
140
|
+
}
|
|
141
|
+
return () => {
|
|
142
|
+
listeners.delete(onChange)
|
|
143
|
+
if (listeners.size === 0 && observer) {
|
|
144
|
+
observer.disconnect()
|
|
145
|
+
observer = null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
154
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
155
|
+
*/
|
|
156
|
+
function useDocumentLocale(): string {
|
|
157
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
161
|
+
function useLocale(locale?: string | null): string {
|
|
162
|
+
const page = useDocumentLocale()
|
|
163
|
+
return locale || page
|
|
164
|
+
}
|
|
165
|
+
|
|
33
166
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
34
167
|
|
|
35
168
|
/**
|
|
@@ -64,10 +197,23 @@ interface ChartTokens {
|
|
|
64
197
|
border: string | null
|
|
65
198
|
/** Tooltip surface. */
|
|
66
199
|
popover: string | null
|
|
200
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
201
|
+
colors?: Record<string, string>
|
|
202
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
203
|
+
surface?: string | null
|
|
67
204
|
/** Tooltip text. */
|
|
68
205
|
popoverForeground: string | null
|
|
69
206
|
}
|
|
70
207
|
|
|
208
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
209
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
210
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
211
|
+
const OPTION_COLOR_TOKENS = [
|
|
212
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
213
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
214
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
215
|
+
]
|
|
216
|
+
|
|
71
217
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
72
218
|
|
|
73
219
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -123,8 +269,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
123
269
|
const color = read(token)
|
|
124
270
|
if (color) series.push(color)
|
|
125
271
|
}
|
|
126
|
-
|
|
272
|
+
const tokens: ChartTokens = {
|
|
127
273
|
series,
|
|
274
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
275
|
+
surface: readChartSurface(target),
|
|
128
276
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
129
277
|
foreground: read("--foreground"),
|
|
130
278
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -132,6 +280,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
132
280
|
popover: read("--popover"),
|
|
133
281
|
popoverForeground: read("--popover-foreground"),
|
|
134
282
|
}
|
|
283
|
+
tokenScopes.set(tokens, target)
|
|
284
|
+
return tokens
|
|
135
285
|
}
|
|
136
286
|
|
|
137
287
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -139,6 +289,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
139
289
|
if (a === b) return true
|
|
140
290
|
if (!a || !b) return false
|
|
141
291
|
return (
|
|
292
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
293
|
+
a.surface === b.surface &&
|
|
294
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
295
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
142
296
|
a.series.length === b.series.length &&
|
|
143
297
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
144
298
|
a.fontFamily === b.fontFamily &&
|
|
@@ -164,7 +318,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
164
318
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
165
319
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
166
320
|
*/
|
|
167
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
321
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
168
322
|
if (typeof document === "undefined") return () => {}
|
|
169
323
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
170
324
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -179,8 +333,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
179
333
|
|
|
180
334
|
const themeObserver = new MutationObserver(schedule)
|
|
181
335
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
182
|
-
|
|
183
|
-
|
|
336
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
337
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
338
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
339
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
340
|
+
}
|
|
184
341
|
|
|
185
342
|
const styleObserver = new MutationObserver(schedule)
|
|
186
343
|
if (document.head) {
|
|
@@ -268,22 +425,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
268
425
|
* components that build an ECharts option from token colours themselves (the
|
|
269
426
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
270
427
|
*/
|
|
271
|
-
function useChartTokens(): ChartTokens | null {
|
|
428
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
272
429
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
273
430
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
274
431
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
275
432
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
276
433
|
const tokensRef = useRef(tokens)
|
|
277
434
|
useEffect(() => {
|
|
435
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
278
436
|
const sync = () => {
|
|
279
|
-
const next = readChartTokens()
|
|
437
|
+
const next = readChartTokens(target)
|
|
280
438
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
281
439
|
tokensRef.current = next
|
|
282
440
|
setTokens(next)
|
|
283
441
|
}
|
|
284
442
|
sync()
|
|
285
|
-
return watchChartTokens(sync)
|
|
286
|
-
}, [])
|
|
443
|
+
return watchChartTokens(sync, target)
|
|
444
|
+
}, [scope])
|
|
287
445
|
return tokens
|
|
288
446
|
}
|
|
289
447
|
|
|
@@ -295,7 +453,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
295
453
|
/**
|
|
296
454
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
297
455
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
298
|
-
* variants `"positive-fill"`, …) are read
|
|
456
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
299
457
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
300
458
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
301
459
|
*/
|
|
@@ -306,12 +464,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
306
464
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
307
465
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
308
466
|
if (typeof document === "undefined") return ""
|
|
309
|
-
|
|
467
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
468
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
310
469
|
}
|
|
311
470
|
if (typeof document === "undefined") return color
|
|
312
471
|
let value = color.trim()
|
|
313
472
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
314
|
-
const styles = getComputedStyle(document.documentElement)
|
|
473
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
315
474
|
if (cssVar) {
|
|
316
475
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
317
476
|
} else if (value.includes("var(")) {
|
|
@@ -357,7 +516,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
357
516
|
if (color) return color
|
|
358
517
|
}
|
|
359
518
|
}
|
|
360
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
519
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
361
520
|
}
|
|
362
521
|
|
|
363
522
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -371,9 +530,10 @@ const WIDTH_BUCKET = 40
|
|
|
371
530
|
*/
|
|
372
531
|
function useChartSurface<E extends Element>(
|
|
373
532
|
forwarded: ForwardedRef<E>,
|
|
374
|
-
tokens
|
|
375
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
533
|
+
tokens?: ChartTokens | null,
|
|
534
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
376
535
|
const [node, setNode] = useState<E | null>(null)
|
|
536
|
+
const scopedTokens = useChartTokens(node)
|
|
377
537
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
378
538
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
379
539
|
const ref = useCallback(
|
|
@@ -387,7 +547,7 @@ function useChartSurface<E extends Element>(
|
|
|
387
547
|
useEffect(() => {
|
|
388
548
|
if (!node) return
|
|
389
549
|
setSurface(readChartSurface(node) ?? undefined)
|
|
390
|
-
}, [node, tokens])
|
|
550
|
+
}, [node, tokens, scopedTokens])
|
|
391
551
|
useEffect(() => {
|
|
392
552
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
393
553
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -396,7 +556,7 @@ function useChartSurface<E extends Element>(
|
|
|
396
556
|
observer.observe(node)
|
|
397
557
|
return () => observer.disconnect()
|
|
398
558
|
}, [node])
|
|
399
|
-
return { ref, surface, width }
|
|
559
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
400
560
|
}
|
|
401
561
|
|
|
402
562
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -506,9 +666,10 @@ function resolveValueFormatter(
|
|
|
506
666
|
function resolveAxisFormatter(
|
|
507
667
|
valueFormatter: ValueFormatter | undefined,
|
|
508
668
|
format: ValueFormat | undefined,
|
|
509
|
-
|
|
669
|
+
localeArg?: string,
|
|
510
670
|
): ValueFormatter {
|
|
511
671
|
if (valueFormatter) return valueFormatter
|
|
672
|
+
const locale = resolveLocale(localeArg)
|
|
512
673
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
513
674
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
514
675
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -517,40 +678,75 @@ function resolveAxisFormatter(
|
|
|
517
678
|
return (value) => formatValue(value, short, locale)
|
|
518
679
|
}
|
|
519
680
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
]
|
|
681
|
+
interface CompactUnit {
|
|
682
|
+
token: string
|
|
683
|
+
size: number
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
526
687
|
|
|
527
|
-
|
|
688
|
+
/**
|
|
689
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
690
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
691
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
692
|
+
*/
|
|
693
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
694
|
+
const cached = compactUnitCache.get(locale)
|
|
695
|
+
if (cached !== undefined) return cached
|
|
696
|
+
let units: CompactUnit[] | null = []
|
|
697
|
+
try {
|
|
698
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
699
|
+
const seen = new Set<string>()
|
|
700
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
701
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
702
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
703
|
+
if (at === -1) continue
|
|
704
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
705
|
+
units = null
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
const token = parts[at]!.value
|
|
709
|
+
if (seen.has(token)) continue
|
|
710
|
+
seen.add(token)
|
|
711
|
+
units.push({ token, size: 10 ** exp })
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
units = null
|
|
715
|
+
}
|
|
716
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
717
|
+
compactUnitCache.set(locale, sorted)
|
|
718
|
+
return sorted
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
528
722
|
|
|
529
723
|
/**
|
|
530
724
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
531
|
-
* that largest tick, so
|
|
532
|
-
* Only the
|
|
533
|
-
* any other format —
|
|
725
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
726
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
727
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
728
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
534
729
|
*/
|
|
535
730
|
function resolveAxisTickFormatter(
|
|
536
731
|
valueFormatter: ValueFormatter | undefined,
|
|
537
732
|
format: ValueFormat | undefined,
|
|
538
|
-
|
|
733
|
+
localeArg?: string,
|
|
539
734
|
): (top: number) => ValueFormatter {
|
|
735
|
+
const locale = resolveLocale(localeArg)
|
|
540
736
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
541
737
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
if (!aligns) return () => base
|
|
738
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
739
|
+
if (!units) return () => base
|
|
545
740
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
741
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
546
742
|
return (top) => {
|
|
547
|
-
const unit =
|
|
743
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
548
744
|
if (!unit) return base
|
|
549
745
|
return (value) => {
|
|
550
746
|
const text = base(value)
|
|
551
747
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
552
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
553
|
-
return text.replace(
|
|
748
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
749
|
+
return text.replace(numberToken, () => scaled)
|
|
554
750
|
}
|
|
555
751
|
}
|
|
556
752
|
}
|
|
@@ -579,8 +775,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
579
775
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
580
776
|
const MAX_CHART_SERIES = 5
|
|
581
777
|
|
|
582
|
-
/** The series name the folded categories get. */
|
|
583
|
-
const
|
|
778
|
+
/** The series name the folded categories get, by language. */
|
|
779
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
780
|
+
|
|
781
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
782
|
+
function otherSeriesLabel(locale?: string): string {
|
|
783
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
787
|
+
|
|
788
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
789
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
790
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
791
|
+
}
|
|
584
792
|
|
|
585
793
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
586
794
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -600,7 +808,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
600
808
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
601
809
|
items: readonly Item[],
|
|
602
810
|
n: number = MAX_CHART_SERIES,
|
|
603
|
-
otherLabel: string =
|
|
811
|
+
otherLabel: string = otherSeriesLabel(),
|
|
604
812
|
): AggregatedItems<Item> {
|
|
605
813
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
606
814
|
const ranked = items
|
|
@@ -629,7 +837,7 @@ function aggregateSeries(
|
|
|
629
837
|
data: readonly Record<string, unknown>[],
|
|
630
838
|
categories: readonly string[],
|
|
631
839
|
n: number = MAX_CHART_SERIES,
|
|
632
|
-
otherLabel: string =
|
|
840
|
+
otherLabel: string = otherSeriesLabel(),
|
|
633
841
|
): AggregatedSeries {
|
|
634
842
|
const totals = categories.map((name) => ({
|
|
635
843
|
name,
|
|
@@ -661,12 +869,14 @@ function aggregateSeriesKeeping(
|
|
|
661
869
|
categories: readonly string[],
|
|
662
870
|
keep: string | undefined,
|
|
663
871
|
n: number = MAX_CHART_SERIES,
|
|
872
|
+
otherLabel: string = otherSeriesLabel(),
|
|
664
873
|
): AggregatedSeries {
|
|
665
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
874
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
666
875
|
const aggregated = aggregateSeries(
|
|
667
876
|
data,
|
|
668
877
|
categories.filter((c) => c !== keep),
|
|
669
878
|
n,
|
|
879
|
+
otherLabel,
|
|
670
880
|
)
|
|
671
881
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
672
882
|
}
|
|
@@ -702,7 +912,7 @@ function seriesColorsWithComparison(
|
|
|
702
912
|
* Colour per drawn series: the record's token for that category, else the
|
|
703
913
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
704
914
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
705
|
-
* data itself names
|
|
915
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
706
916
|
*/
|
|
707
917
|
function seriesColorsFor(
|
|
708
918
|
categories: readonly string[],
|
|
@@ -714,7 +924,7 @@ function seriesColorsFor(
|
|
|
714
924
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
715
925
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
716
926
|
return categories.map((category, i) => {
|
|
717
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
927
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
718
928
|
const fromRecord = record?.[category]
|
|
719
929
|
if (fromRecord) return fromRecord
|
|
720
930
|
if (list && list.length > 0) {
|
|
@@ -761,7 +971,7 @@ interface DonutSlicesConfig {
|
|
|
761
971
|
*/
|
|
762
972
|
function donutSlices(
|
|
763
973
|
data: readonly Record<string, unknown>[],
|
|
764
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
974
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
765
975
|
): DonutSlice[] {
|
|
766
976
|
const items = data
|
|
767
977
|
.map((row, i) => {
|
|
@@ -774,7 +984,8 @@ function donutSlices(
|
|
|
774
984
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
775
985
|
name: item.name,
|
|
776
986
|
value: item.value,
|
|
777
|
-
|
|
987
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
988
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
778
989
|
isOther: false,
|
|
779
990
|
row: item.row,
|
|
780
991
|
}))
|
|
@@ -847,7 +1058,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
847
1058
|
*/
|
|
848
1059
|
function barListRows<Row extends object>(
|
|
849
1060
|
data: readonly Row[],
|
|
850
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1061
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
851
1062
|
): BarListRow<Row>[] {
|
|
852
1063
|
const items = data.map((row): BarListRow<Row> => {
|
|
853
1064
|
const cells = row as Record<string, unknown>
|
|
@@ -893,19 +1104,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
893
1104
|
|
|
894
1105
|
/**
|
|
895
1106
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
896
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1107
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
897
1108
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
898
1109
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
899
1110
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
900
1111
|
* keeps its contrast.
|
|
901
1112
|
*/
|
|
902
1113
|
function barListFills(
|
|
903
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1114
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
904
1115
|
color: ChartColor,
|
|
905
1116
|
otherColor: string,
|
|
906
1117
|
): string[] {
|
|
907
1118
|
return rows.map((row) =>
|
|
908
|
-
row.isOther
|
|
1119
|
+
row.isOther || isOtherName(row.name)
|
|
1120
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1121
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
909
1122
|
)
|
|
910
1123
|
}
|
|
911
1124
|
|
|
@@ -1121,11 +1334,30 @@ interface ChartSummaryInput {
|
|
|
1121
1334
|
format?: ValueFormat
|
|
1122
1335
|
/** Wins over `format`. */
|
|
1123
1336
|
valueFormatter?: ValueFormatter
|
|
1337
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1338
|
+
locale?: string
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const SUMMARY_TEXT = {
|
|
1342
|
+
ja: {
|
|
1343
|
+
up: "、前の点より増加",
|
|
1344
|
+
down: "、前の点より減少",
|
|
1345
|
+
flat: "、前の点と横ばい",
|
|
1346
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1347
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1348
|
+
},
|
|
1349
|
+
en: {
|
|
1350
|
+
up: ", up from the previous point",
|
|
1351
|
+
down: ", down from the previous point",
|
|
1352
|
+
flat: ", flat from the previous point",
|
|
1353
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1354
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1355
|
+
},
|
|
1124
1356
|
}
|
|
1125
1357
|
|
|
1126
1358
|
/**
|
|
1127
1359
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1128
|
-
* 「<title>: 最新 <index> は
|
|
1360
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1129
1361
|
* computed from the last two non-empty points of the first series — never
|
|
1130
1362
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1131
1363
|
* no usable point.
|
|
@@ -1141,30 +1373,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1141
1373
|
}
|
|
1142
1374
|
const latest = points[points.length - 1]
|
|
1143
1375
|
if (!latest) return undefined
|
|
1144
|
-
const
|
|
1376
|
+
const locale = resolveLocale(input.locale)
|
|
1377
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1378
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1145
1379
|
const prev = points[points.length - 2]
|
|
1146
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1380
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1147
1381
|
const head = input.title ? `${input.title}: ` : ""
|
|
1148
|
-
return `${head}
|
|
1382
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1149
1383
|
}
|
|
1150
1384
|
|
|
1151
1385
|
/**
|
|
1152
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1153
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1386
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1387
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1154
1388
|
*/
|
|
1155
1389
|
function buildShareSummary(
|
|
1156
1390
|
slices: readonly { name: string; value: number }[],
|
|
1157
1391
|
title?: string,
|
|
1392
|
+
locale?: string,
|
|
1158
1393
|
): string | undefined {
|
|
1159
1394
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1160
1395
|
const top = slices[0]
|
|
1161
1396
|
if (!top || total <= 0) return undefined
|
|
1162
1397
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1163
|
-
return `${title ? `${title}: ` : ""}
|
|
1398
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1164
1399
|
}
|
|
1165
1400
|
|
|
1166
1401
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
1167
1402
|
|
|
1403
|
+
/** The built-in words of the option builders, by language. */
|
|
1404
|
+
const CHART_TEXT = {
|
|
1405
|
+
ja: {
|
|
1406
|
+
target: "目標",
|
|
1407
|
+
forecast: "予測",
|
|
1408
|
+
total: "合計",
|
|
1409
|
+
provisional: "速報",
|
|
1410
|
+
noComparison: "比較なし",
|
|
1411
|
+
share: (share: string) => `(${share})`,
|
|
1412
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
1413
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
1414
|
+
},
|
|
1415
|
+
en: {
|
|
1416
|
+
target: "Target",
|
|
1417
|
+
forecast: "Forecast",
|
|
1418
|
+
total: "Total",
|
|
1419
|
+
provisional: "Preliminary",
|
|
1420
|
+
noComparison: "No comparison",
|
|
1421
|
+
share: (share: string) => ` (${share})`,
|
|
1422
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
1423
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
1424
|
+
},
|
|
1425
|
+
} satisfies Record<UiLanguage, unknown>
|
|
1426
|
+
|
|
1427
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
1428
|
+
|
|
1168
1429
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
1169
1430
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
1170
1431
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -1349,7 +1610,7 @@ interface CartesianChartConfig {
|
|
|
1349
1610
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
1350
1611
|
*/
|
|
1351
1612
|
axisTicks?: (top: number) => (value: number) => string
|
|
1352
|
-
/** Locale of the time-axis labels. Default
|
|
1613
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
1353
1614
|
locale?: string
|
|
1354
1615
|
type?: CartesianStackType
|
|
1355
1616
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -1587,11 +1848,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
1587
1848
|
*/
|
|
1588
1849
|
function timeAxisLabels(
|
|
1589
1850
|
values: readonly unknown[],
|
|
1590
|
-
|
|
1851
|
+
localeArg?: string,
|
|
1591
1852
|
shown: (i: number) => boolean = () => true,
|
|
1592
1853
|
): string[] | null {
|
|
1593
1854
|
const parsed = parseTimeIndex(values)
|
|
1594
1855
|
if (!parsed) return null
|
|
1856
|
+
const locale = resolveLocale(localeArg)
|
|
1595
1857
|
const { granularity, times } = parsed
|
|
1596
1858
|
let prevYear: number | null = null
|
|
1597
1859
|
return times.map((t, i) => {
|
|
@@ -1614,9 +1876,10 @@ function timeAxisLabels(
|
|
|
1614
1876
|
}
|
|
1615
1877
|
|
|
1616
1878
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
1617
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1879
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
1618
1880
|
const parsed = parseTimeIndex(values)
|
|
1619
1881
|
if (!parsed) return null
|
|
1882
|
+
const locale = resolveLocale(localeArg)
|
|
1620
1883
|
const options: Intl.DateTimeFormatOptions =
|
|
1621
1884
|
parsed.granularity === "year"
|
|
1622
1885
|
? { year: "numeric" }
|
|
@@ -1756,7 +2019,9 @@ function tooltipHtml(
|
|
|
1756
2019
|
rows: readonly TooltipRow[],
|
|
1757
2020
|
valueFormatter: (value: number) => string,
|
|
1758
2021
|
notes: readonly string[] = [],
|
|
2022
|
+
locale?: string,
|
|
1759
2023
|
): string {
|
|
2024
|
+
const t = chartText(locale)
|
|
1760
2025
|
const lines = rows.map(
|
|
1761
2026
|
(row) =>
|
|
1762
2027
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1765,7 +2030,7 @@ function tooltipHtml(
|
|
|
1765
2030
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1766
2031
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1767
2032
|
}${
|
|
1768
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
2033
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1769
2034
|
}</span></div>` +
|
|
1770
2035
|
(row.note === undefined
|
|
1771
2036
|
? ""
|
|
@@ -1779,8 +2044,10 @@ function tooltipHtml(
|
|
|
1779
2044
|
|
|
1780
2045
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1781
2046
|
|
|
1782
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1783
|
-
|
|
2047
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
2048
|
+
function barTargetLabel(locale?: string): string {
|
|
2049
|
+
return chartText(locale).target
|
|
2050
|
+
}
|
|
1784
2051
|
|
|
1785
2052
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1786
2053
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1831,16 +2098,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1831
2098
|
return (value / target) * 100
|
|
1832
2099
|
}
|
|
1833
2100
|
|
|
1834
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
2101
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1835
2102
|
function barTargetNote(
|
|
1836
2103
|
label: string,
|
|
1837
2104
|
value: number | null,
|
|
1838
2105
|
target: number | null,
|
|
1839
2106
|
valueFormatter: (value: number) => string,
|
|
2107
|
+
locale?: string,
|
|
1840
2108
|
): string | null {
|
|
1841
2109
|
if (target === null) return null
|
|
1842
2110
|
const rate = achievementRate(value, target)
|
|
1843
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
2111
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1844
2112
|
}
|
|
1845
2113
|
|
|
1846
2114
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1932,11 +2200,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1932
2200
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1933
2201
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1934
2202
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2203
|
+
/**
|
|
2204
|
+
* 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.
|
|
2205
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
2206
|
+
*/
|
|
2207
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
2208
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1940
2209
|
}
|
|
1941
2210
|
|
|
1942
2211
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -1976,8 +2245,10 @@ interface AxisExtent {
|
|
|
1976
2245
|
max: number
|
|
1977
2246
|
}
|
|
1978
2247
|
|
|
1979
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
1980
|
-
|
|
2248
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
2249
|
+
function stackTotalLabel(locale?: string): string {
|
|
2250
|
+
return chartText(locale).total
|
|
2251
|
+
}
|
|
1981
2252
|
|
|
1982
2253
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
1983
2254
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -2099,8 +2370,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
2099
2370
|
|
|
2100
2371
|
/**
|
|
2101
2372
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
2102
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2103
|
-
* either value is missing or the baseline is 0.
|
|
2373
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2374
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
2104
2375
|
*/
|
|
2105
2376
|
function comparisonNote(
|
|
2106
2377
|
current: number | null,
|
|
@@ -2108,16 +2379,18 @@ function comparisonNote(
|
|
|
2108
2379
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
2109
2380
|
grain?: PeriodGrain,
|
|
2110
2381
|
format?: ValueFormat,
|
|
2111
|
-
|
|
2382
|
+
localeArg?: string,
|
|
2112
2383
|
): string {
|
|
2384
|
+
const locale = resolveLocale(localeArg)
|
|
2113
2385
|
const result = computeComparison(
|
|
2114
2386
|
current,
|
|
2115
2387
|
comparison.vs === "custom"
|
|
2116
2388
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
2117
2389
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
2390
|
+
{ locale },
|
|
2118
2391
|
)
|
|
2119
2392
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
2120
|
-
if (text === null) return
|
|
2393
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
2121
2394
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
2122
2395
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
2123
2396
|
}
|
|
@@ -2160,7 +2433,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2160
2433
|
toneColors,
|
|
2161
2434
|
valueFormatter,
|
|
2162
2435
|
axisFormatter = valueFormatter,
|
|
2163
|
-
locale = DEFAULT_LOCALE,
|
|
2164
2436
|
type = "default",
|
|
2165
2437
|
layout = "horizontal",
|
|
2166
2438
|
curveType = "linear",
|
|
@@ -2197,7 +2469,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2197
2469
|
chartOptions,
|
|
2198
2470
|
reserveActions = false,
|
|
2199
2471
|
} = cfg
|
|
2200
|
-
const
|
|
2472
|
+
const locale = resolveLocale(cfg.locale)
|
|
2473
|
+
const text = chartText(locale)
|
|
2474
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
2201
2475
|
|
|
2202
2476
|
const stacked = kind !== "line" && type !== "default"
|
|
2203
2477
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -2346,9 +2620,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2346
2620
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
2347
2621
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
2348
2622
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
2349
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
2623
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
2350
2624
|
const highlightText = (value: number) =>
|
|
2351
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
2625
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
2352
2626
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
2353
2627
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
2354
2628
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -2374,7 +2648,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2374
2648
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
2375
2649
|
const barTargets: (number | null)[] | null =
|
|
2376
2650
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
2377
|
-
const barTargetName = targetLabel ??
|
|
2651
|
+
const barTargetName = targetLabel ?? text.target
|
|
2378
2652
|
const barTargetColor = labelColor ?? guideColor
|
|
2379
2653
|
|
|
2380
2654
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -2588,7 +2862,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2588
2862
|
}
|
|
2589
2863
|
|
|
2590
2864
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
2591
|
-
const forecastLabel = forecast?.label ??
|
|
2865
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
2592
2866
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
2593
2867
|
const forecastValues: (number | null)[] = []
|
|
2594
2868
|
if (forecast) {
|
|
@@ -3064,7 +3338,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3064
3338
|
})
|
|
3065
3339
|
}
|
|
3066
3340
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
3067
|
-
rows.push({ name:
|
|
3341
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3068
3342
|
}
|
|
3069
3343
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
3070
3344
|
if (provisional) {
|
|
@@ -3080,19 +3354,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3080
3354
|
...noteFor(at),
|
|
3081
3355
|
})
|
|
3082
3356
|
})
|
|
3083
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
3357
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3084
3358
|
}
|
|
3085
3359
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
3086
3360
|
const lines: string[] = []
|
|
3087
3361
|
if (barTargets && i <= lastActual) {
|
|
3088
3362
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
3089
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
3363
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
3090
3364
|
if (note) lines.push(note)
|
|
3091
3365
|
}
|
|
3092
3366
|
for (const note of notes) {
|
|
3093
3367
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
3094
3368
|
}
|
|
3095
|
-
return tooltipHtml(provisional ?
|
|
3369
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
3096
3370
|
},
|
|
3097
3371
|
},
|
|
3098
3372
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -3184,8 +3458,6 @@ interface DonutOptionSlice {
|
|
|
3184
3458
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
3185
3459
|
const DONUT_OUTSIDE_LABELS = 2
|
|
3186
3460
|
|
|
3187
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
3188
|
-
|
|
3189
3461
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
3190
3462
|
|
|
3191
3463
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -3218,15 +3490,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
3218
3490
|
}
|
|
3219
3491
|
|
|
3220
3492
|
/**
|
|
3221
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
3493
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
3222
3494
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
3223
3495
|
* string (`value`, 「9.2万 セッション」).
|
|
3224
3496
|
*/
|
|
3225
3497
|
function donutCenterText(
|
|
3226
3498
|
total: number,
|
|
3227
3499
|
valueFormatter: (value: number) => string,
|
|
3228
|
-
{ totalLabel
|
|
3500
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
3229
3501
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
3502
|
+
totalLabel ??= chartText(locale).total
|
|
3230
3503
|
const amount = valueFormatter(total)
|
|
3231
3504
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
3232
3505
|
}
|
|
@@ -3454,8 +3727,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
3454
3727
|
|
|
3455
3728
|
/* ------------------------------------------------------------ provisional */
|
|
3456
3729
|
|
|
3457
|
-
/** The default label for points that are not final yet. */
|
|
3458
|
-
|
|
3730
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
3731
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
3732
|
+
return chartText(locale).provisional
|
|
3733
|
+
}
|
|
3459
3734
|
|
|
3460
3735
|
/**
|
|
3461
3736
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -3522,6 +3797,11 @@ export interface DonutChartProps<T extends Record<string, unknown> = Record<stri
|
|
|
3522
3797
|
loading?: boolean
|
|
3523
3798
|
}
|
|
3524
3799
|
|
|
3800
|
+
const TEXT = {
|
|
3801
|
+
ja: { item: "項目", value: "値", chart: (shares: readonly string[]) => `構成比のドーナツグラフ(${shares.join("、")})` },
|
|
3802
|
+
en: { item: "Item", value: "Value", chart: (shares: readonly string[]) => `Donut chart of shares (${shares.join(", ")})` },
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3525
3805
|
/** The element's width, kept current (`null` until measured). */
|
|
3526
3806
|
function useElementWidth(): [(el: HTMLDivElement | null) => void, number | null] {
|
|
3527
3807
|
const [node, setNode] = useState<HTMLDivElement | null>(null)
|
|
@@ -3577,12 +3857,15 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3577
3857
|
const center = resolveDonutCenter(donutOptions?.centerLabel, { showLabel: donutOptions?.showLabel ?? showLabel, label })
|
|
3578
3858
|
const maxSlices = donutOptions?.maxSlices
|
|
3579
3859
|
const showLegend = donutOptions?.showLegend ?? true
|
|
3580
|
-
const
|
|
3860
|
+
const locale = useLocale()
|
|
3861
|
+
const t = TEXT[uiLanguage(locale)]
|
|
3862
|
+
const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
|
|
3581
3863
|
|
|
3582
|
-
const tokens =
|
|
3864
|
+
const { ref: frameRef, tokens } = useChartSurface(ref)
|
|
3583
3865
|
const slices = useMemo(
|
|
3584
|
-
() =>
|
|
3585
|
-
|
|
3866
|
+
() =>
|
|
3867
|
+
donutSlices(data, { index: labelKey, value: valueKey, colors, maxSlices, otherColor: CHART_OTHER_COLOR, otherLabel: otherSeriesLabel(locale) }),
|
|
3868
|
+
[data, labelKey, valueKey, colors, maxSlices, locale],
|
|
3586
3869
|
)
|
|
3587
3870
|
const palette = useMemo(() => resolveChartColors(slices.map((s) => chartFillName(s.color)), tokens), [slices, tokens])
|
|
3588
3871
|
const rows = useMemo(() => slices.map((s) => s.row), [slices])
|
|
@@ -3632,15 +3915,14 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3632
3915
|
let centerLabel: ReactNode = null
|
|
3633
3916
|
if (center.kind === "custom") centerLabel = center.node
|
|
3634
3917
|
else if (center.kind === "total") {
|
|
3635
|
-
const text = donutCenterText(total, valueFormatter, { totalLabel: donutOptions?.totalLabel, unit: donutOptions?.unit })
|
|
3918
|
+
const text = donutCenterText(total, valueFormatter, { totalLabel: donutOptions?.totalLabel, unit: donutOptions?.unit, locale })
|
|
3636
3919
|
centerLabel = caption(text.label, text.amount, text.unit)
|
|
3637
3920
|
} else if (center.kind === "top") {
|
|
3638
3921
|
const top = donutTopText(slices, labels)
|
|
3639
3922
|
centerLabel = top && caption(top.label, top.amount)
|
|
3640
3923
|
}
|
|
3641
3924
|
// Built from the slices (names and shares), not the column keys.
|
|
3642
|
-
const a11yLabel =
|
|
3643
|
-
ariaLabel ?? `構成比のドーナツグラフ(${legend.map((item) => `${labelOf(item.name, labels)} ${item.share}`).join("、")})`
|
|
3925
|
+
const a11yLabel = ariaLabel ?? t.chart(legend.map((item) => `${labelOf(item.name, labels)} ${item.share}`))
|
|
3644
3926
|
const dataTable = useMemo(
|
|
3645
3927
|
() =>
|
|
3646
3928
|
chartDataTable(
|
|
@@ -3649,19 +3931,19 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3649
3931
|
"name",
|
|
3650
3932
|
["value"],
|
|
3651
3933
|
valueFormatter,
|
|
3652
|
-
{ name: labels?.[labelKey] ??
|
|
3934
|
+
{ name: labels?.[labelKey] ?? t.item, value: labels?.[valueKey] ?? t.value },
|
|
3653
3935
|
),
|
|
3654
|
-
[rows, labelKey, valueKey, valueFormatter, labels],
|
|
3936
|
+
[rows, labelKey, valueKey, valueFormatter, labels, t],
|
|
3655
3937
|
)
|
|
3656
3938
|
|
|
3657
3939
|
const resolvedSummary = useMemo(
|
|
3658
|
-
() => (summary === "auto" ? buildShareSummary(slices, title) : summary),
|
|
3659
|
-
[summary, slices, title],
|
|
3940
|
+
() => (summary === "auto" ? buildShareSummary(slices, title, locale) : summary),
|
|
3941
|
+
[summary, slices, title, locale],
|
|
3660
3942
|
)
|
|
3661
3943
|
return (
|
|
3662
3944
|
<ChartFrame
|
|
3663
3945
|
{...chartRootProps(rest)}
|
|
3664
|
-
ref={
|
|
3946
|
+
ref={frameRef}
|
|
3665
3947
|
title={title}
|
|
3666
3948
|
summary={resolvedSummary}
|
|
3667
3949
|
isLoading={isLoading ?? loading ?? false}
|