@squadbase/vantage 0.6.2-dev1.9365dcd → 0.6.2-dev1.9d7130b
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-add-feature/SKILL.md +5 -0
- 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 +35 -18
- package/templates/agents/api.md +7 -0
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +59 -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
package/registry/ui/bar-list.tsx
CHANGED
|
@@ -31,15 +31,148 @@ import { cn } from "@squadbase/vantage/ui"
|
|
|
31
31
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
32
32
|
import { toneForValue } from "@squadbase/vantage/components"
|
|
33
33
|
import type { ChartColor, ChartCommonProps, ChartSeriesColor, ChartThreshold } from "@squadbase/vantage/components"
|
|
34
|
-
import {
|
|
34
|
+
import { useSyncExternalStore } from "react"
|
|
35
|
+
import { createContext, useCallback, useRef, type RefObject } from "react"
|
|
35
36
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
36
|
-
import {
|
|
37
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
37
38
|
import type { ChartColors, ValueFormatter } from "@squadbase/vantage/components"
|
|
38
39
|
import { useMemo, type ReactNode } from "react"
|
|
39
40
|
import type { EChartsOption } from "@squadbase/vantage/components"
|
|
40
41
|
import { computeComparison, defaultChangeDisplay, formatChange, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
42
|
+
import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
|
|
41
43
|
import type { ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
|
|
42
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
|
+
|
|
43
176
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
44
177
|
|
|
45
178
|
/**
|
|
@@ -74,10 +207,23 @@ interface ChartTokens {
|
|
|
74
207
|
border: string | null
|
|
75
208
|
/** Tooltip surface. */
|
|
76
209
|
popover: string | null
|
|
210
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
211
|
+
colors?: Record<string, string>
|
|
212
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
213
|
+
surface?: string | null
|
|
77
214
|
/** Tooltip text. */
|
|
78
215
|
popoverForeground: string | null
|
|
79
216
|
}
|
|
80
217
|
|
|
218
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
219
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
220
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
221
|
+
const OPTION_COLOR_TOKENS = [
|
|
222
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
223
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
224
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
225
|
+
]
|
|
226
|
+
|
|
81
227
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
82
228
|
|
|
83
229
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -133,8 +279,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
133
279
|
const color = read(token)
|
|
134
280
|
if (color) series.push(color)
|
|
135
281
|
}
|
|
136
|
-
|
|
282
|
+
const tokens: ChartTokens = {
|
|
137
283
|
series,
|
|
284
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
285
|
+
surface: readChartSurface(target),
|
|
138
286
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
139
287
|
foreground: read("--foreground"),
|
|
140
288
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -142,6 +290,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
142
290
|
popover: read("--popover"),
|
|
143
291
|
popoverForeground: read("--popover-foreground"),
|
|
144
292
|
}
|
|
293
|
+
tokenScopes.set(tokens, target)
|
|
294
|
+
return tokens
|
|
145
295
|
}
|
|
146
296
|
|
|
147
297
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -149,6 +299,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
149
299
|
if (a === b) return true
|
|
150
300
|
if (!a || !b) return false
|
|
151
301
|
return (
|
|
302
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
303
|
+
a.surface === b.surface &&
|
|
304
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
305
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
152
306
|
a.series.length === b.series.length &&
|
|
153
307
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
154
308
|
a.fontFamily === b.fontFamily &&
|
|
@@ -174,7 +328,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
174
328
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
175
329
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
176
330
|
*/
|
|
177
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
331
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
178
332
|
if (typeof document === "undefined") return () => {}
|
|
179
333
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
180
334
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -189,8 +343,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
189
343
|
|
|
190
344
|
const themeObserver = new MutationObserver(schedule)
|
|
191
345
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
192
|
-
|
|
193
|
-
|
|
346
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
347
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
348
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
349
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
350
|
+
}
|
|
194
351
|
|
|
195
352
|
const styleObserver = new MutationObserver(schedule)
|
|
196
353
|
if (document.head) {
|
|
@@ -278,22 +435,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
278
435
|
* components that build an ECharts option from token colours themselves (the
|
|
279
436
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
280
437
|
*/
|
|
281
|
-
function useChartTokens(): ChartTokens | null {
|
|
438
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
282
439
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
283
440
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
284
441
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
285
442
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
286
443
|
const tokensRef = useRef(tokens)
|
|
287
444
|
useEffect(() => {
|
|
445
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
288
446
|
const sync = () => {
|
|
289
|
-
const next = readChartTokens()
|
|
447
|
+
const next = readChartTokens(target)
|
|
290
448
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
291
449
|
tokensRef.current = next
|
|
292
450
|
setTokens(next)
|
|
293
451
|
}
|
|
294
452
|
sync()
|
|
295
|
-
return watchChartTokens(sync)
|
|
296
|
-
}, [])
|
|
453
|
+
return watchChartTokens(sync, target)
|
|
454
|
+
}, [scope])
|
|
297
455
|
return tokens
|
|
298
456
|
}
|
|
299
457
|
|
|
@@ -305,7 +463,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
305
463
|
/**
|
|
306
464
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
307
465
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
308
|
-
* variants `"positive-fill"`, …) are read
|
|
466
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
309
467
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
310
468
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
311
469
|
*/
|
|
@@ -316,12 +474,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
316
474
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
317
475
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
318
476
|
if (typeof document === "undefined") return ""
|
|
319
|
-
|
|
477
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
478
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
320
479
|
}
|
|
321
480
|
if (typeof document === "undefined") return color
|
|
322
481
|
let value = color.trim()
|
|
323
482
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
324
|
-
const styles = getComputedStyle(document.documentElement)
|
|
483
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
325
484
|
if (cssVar) {
|
|
326
485
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
327
486
|
} else if (value.includes("var(")) {
|
|
@@ -367,7 +526,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
367
526
|
if (color) return color
|
|
368
527
|
}
|
|
369
528
|
}
|
|
370
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
529
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
371
530
|
}
|
|
372
531
|
|
|
373
532
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -381,9 +540,10 @@ const WIDTH_BUCKET = 40
|
|
|
381
540
|
*/
|
|
382
541
|
function useChartSurface<E extends Element>(
|
|
383
542
|
forwarded: ForwardedRef<E>,
|
|
384
|
-
tokens
|
|
385
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
543
|
+
tokens?: ChartTokens | null,
|
|
544
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
386
545
|
const [node, setNode] = useState<E | null>(null)
|
|
546
|
+
const scopedTokens = useChartTokens(node)
|
|
387
547
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
388
548
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
389
549
|
const ref = useCallback(
|
|
@@ -397,7 +557,7 @@ function useChartSurface<E extends Element>(
|
|
|
397
557
|
useEffect(() => {
|
|
398
558
|
if (!node) return
|
|
399
559
|
setSurface(readChartSurface(node) ?? undefined)
|
|
400
|
-
}, [node, tokens])
|
|
560
|
+
}, [node, tokens, scopedTokens])
|
|
401
561
|
useEffect(() => {
|
|
402
562
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
403
563
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -406,7 +566,7 @@ function useChartSurface<E extends Element>(
|
|
|
406
566
|
observer.observe(node)
|
|
407
567
|
return () => observer.disconnect()
|
|
408
568
|
}, [node])
|
|
409
|
-
return { ref, surface, width }
|
|
569
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
410
570
|
}
|
|
411
571
|
|
|
412
572
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -516,9 +676,10 @@ function resolveValueFormatter(
|
|
|
516
676
|
function resolveAxisFormatter(
|
|
517
677
|
valueFormatter: ValueFormatter | undefined,
|
|
518
678
|
format: ValueFormat | undefined,
|
|
519
|
-
|
|
679
|
+
localeArg?: string,
|
|
520
680
|
): ValueFormatter {
|
|
521
681
|
if (valueFormatter) return valueFormatter
|
|
682
|
+
const locale = resolveLocale(localeArg)
|
|
522
683
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
523
684
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
524
685
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -527,40 +688,75 @@ function resolveAxisFormatter(
|
|
|
527
688
|
return (value) => formatValue(value, short, locale)
|
|
528
689
|
}
|
|
529
690
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
]
|
|
691
|
+
interface CompactUnit {
|
|
692
|
+
token: string
|
|
693
|
+
size: number
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
700
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
701
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
702
|
+
*/
|
|
703
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
704
|
+
const cached = compactUnitCache.get(locale)
|
|
705
|
+
if (cached !== undefined) return cached
|
|
706
|
+
let units: CompactUnit[] | null = []
|
|
707
|
+
try {
|
|
708
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
709
|
+
const seen = new Set<string>()
|
|
710
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
711
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
712
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
713
|
+
if (at === -1) continue
|
|
714
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
715
|
+
units = null
|
|
716
|
+
break
|
|
717
|
+
}
|
|
718
|
+
const token = parts[at]!.value
|
|
719
|
+
if (seen.has(token)) continue
|
|
720
|
+
seen.add(token)
|
|
721
|
+
units.push({ token, size: 10 ** exp })
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
units = null
|
|
725
|
+
}
|
|
726
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
727
|
+
compactUnitCache.set(locale, sorted)
|
|
728
|
+
return sorted
|
|
729
|
+
}
|
|
536
730
|
|
|
537
|
-
const
|
|
731
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
538
732
|
|
|
539
733
|
/**
|
|
540
734
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
541
|
-
* that largest tick, so
|
|
542
|
-
* Only the
|
|
543
|
-
* any other format —
|
|
735
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
736
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
737
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
738
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
544
739
|
*/
|
|
545
740
|
function resolveAxisTickFormatter(
|
|
546
741
|
valueFormatter: ValueFormatter | undefined,
|
|
547
742
|
format: ValueFormat | undefined,
|
|
548
|
-
|
|
743
|
+
localeArg?: string,
|
|
549
744
|
): (top: number) => ValueFormatter {
|
|
745
|
+
const locale = resolveLocale(localeArg)
|
|
550
746
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
551
747
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
if (!aligns) return () => base
|
|
748
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
749
|
+
if (!units) return () => base
|
|
555
750
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
751
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
556
752
|
return (top) => {
|
|
557
|
-
const unit =
|
|
753
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
558
754
|
if (!unit) return base
|
|
559
755
|
return (value) => {
|
|
560
756
|
const text = base(value)
|
|
561
757
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
562
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
563
|
-
return text.replace(
|
|
758
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
759
|
+
return text.replace(numberToken, () => scaled)
|
|
564
760
|
}
|
|
565
761
|
}
|
|
566
762
|
}
|
|
@@ -589,8 +785,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
589
785
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
590
786
|
const MAX_CHART_SERIES = 5
|
|
591
787
|
|
|
592
|
-
/** The series name the folded categories get. */
|
|
593
|
-
const
|
|
788
|
+
/** The series name the folded categories get, by language. */
|
|
789
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
790
|
+
|
|
791
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
792
|
+
function otherSeriesLabel(locale?: string): string {
|
|
793
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
797
|
+
|
|
798
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
799
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
800
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
801
|
+
}
|
|
594
802
|
|
|
595
803
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
596
804
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -610,7 +818,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
610
818
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
611
819
|
items: readonly Item[],
|
|
612
820
|
n: number = MAX_CHART_SERIES,
|
|
613
|
-
otherLabel: string =
|
|
821
|
+
otherLabel: string = otherSeriesLabel(),
|
|
614
822
|
): AggregatedItems<Item> {
|
|
615
823
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
616
824
|
const ranked = items
|
|
@@ -639,7 +847,7 @@ function aggregateSeries(
|
|
|
639
847
|
data: readonly Record<string, unknown>[],
|
|
640
848
|
categories: readonly string[],
|
|
641
849
|
n: number = MAX_CHART_SERIES,
|
|
642
|
-
otherLabel: string =
|
|
850
|
+
otherLabel: string = otherSeriesLabel(),
|
|
643
851
|
): AggregatedSeries {
|
|
644
852
|
const totals = categories.map((name) => ({
|
|
645
853
|
name,
|
|
@@ -671,12 +879,14 @@ function aggregateSeriesKeeping(
|
|
|
671
879
|
categories: readonly string[],
|
|
672
880
|
keep: string | undefined,
|
|
673
881
|
n: number = MAX_CHART_SERIES,
|
|
882
|
+
otherLabel: string = otherSeriesLabel(),
|
|
674
883
|
): AggregatedSeries {
|
|
675
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
884
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
676
885
|
const aggregated = aggregateSeries(
|
|
677
886
|
data,
|
|
678
887
|
categories.filter((c) => c !== keep),
|
|
679
888
|
n,
|
|
889
|
+
otherLabel,
|
|
680
890
|
)
|
|
681
891
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
682
892
|
}
|
|
@@ -712,7 +922,7 @@ function seriesColorsWithComparison(
|
|
|
712
922
|
* Colour per drawn series: the record's token for that category, else the
|
|
713
923
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
714
924
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
715
|
-
* data itself names
|
|
925
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
716
926
|
*/
|
|
717
927
|
function seriesColorsFor(
|
|
718
928
|
categories: readonly string[],
|
|
@@ -724,7 +934,7 @@ function seriesColorsFor(
|
|
|
724
934
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
725
935
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
726
936
|
return categories.map((category, i) => {
|
|
727
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
937
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
728
938
|
const fromRecord = record?.[category]
|
|
729
939
|
if (fromRecord) return fromRecord
|
|
730
940
|
if (list && list.length > 0) {
|
|
@@ -771,7 +981,7 @@ interface DonutSlicesConfig {
|
|
|
771
981
|
*/
|
|
772
982
|
function donutSlices(
|
|
773
983
|
data: readonly Record<string, unknown>[],
|
|
774
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
984
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
775
985
|
): DonutSlice[] {
|
|
776
986
|
const items = data
|
|
777
987
|
.map((row, i) => {
|
|
@@ -784,7 +994,8 @@ function donutSlices(
|
|
|
784
994
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
785
995
|
name: item.name,
|
|
786
996
|
value: item.value,
|
|
787
|
-
|
|
997
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
998
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
788
999
|
isOther: false,
|
|
789
1000
|
row: item.row,
|
|
790
1001
|
}))
|
|
@@ -857,7 +1068,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
857
1068
|
*/
|
|
858
1069
|
function barListRows<Row extends object>(
|
|
859
1070
|
data: readonly Row[],
|
|
860
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1071
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
861
1072
|
): BarListRow<Row>[] {
|
|
862
1073
|
const items = data.map((row): BarListRow<Row> => {
|
|
863
1074
|
const cells = row as Record<string, unknown>
|
|
@@ -903,19 +1114,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
903
1114
|
|
|
904
1115
|
/**
|
|
905
1116
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
906
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1117
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
907
1118
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
908
1119
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
909
1120
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
910
1121
|
* keeps its contrast.
|
|
911
1122
|
*/
|
|
912
1123
|
function barListFills(
|
|
913
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1124
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
914
1125
|
color: ChartColor,
|
|
915
1126
|
otherColor: string,
|
|
916
1127
|
): string[] {
|
|
917
1128
|
return rows.map((row) =>
|
|
918
|
-
row.isOther
|
|
1129
|
+
row.isOther || isOtherName(row.name)
|
|
1130
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1131
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
919
1132
|
)
|
|
920
1133
|
}
|
|
921
1134
|
|
|
@@ -1131,11 +1344,30 @@ interface ChartSummaryInput {
|
|
|
1131
1344
|
format?: ValueFormat
|
|
1132
1345
|
/** Wins over `format`. */
|
|
1133
1346
|
valueFormatter?: ValueFormatter
|
|
1347
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1348
|
+
locale?: string
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const SUMMARY_TEXT = {
|
|
1352
|
+
ja: {
|
|
1353
|
+
up: "、前の点より増加",
|
|
1354
|
+
down: "、前の点より減少",
|
|
1355
|
+
flat: "、前の点と横ばい",
|
|
1356
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1357
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1358
|
+
},
|
|
1359
|
+
en: {
|
|
1360
|
+
up: ", up from the previous point",
|
|
1361
|
+
down: ", down from the previous point",
|
|
1362
|
+
flat: ", flat from the previous point",
|
|
1363
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1364
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1365
|
+
},
|
|
1134
1366
|
}
|
|
1135
1367
|
|
|
1136
1368
|
/**
|
|
1137
1369
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1138
|
-
* 「<title>: 最新 <index> は
|
|
1370
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1139
1371
|
* computed from the last two non-empty points of the first series — never
|
|
1140
1372
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1141
1373
|
* no usable point.
|
|
@@ -1151,30 +1383,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1151
1383
|
}
|
|
1152
1384
|
const latest = points[points.length - 1]
|
|
1153
1385
|
if (!latest) return undefined
|
|
1154
|
-
const
|
|
1386
|
+
const locale = resolveLocale(input.locale)
|
|
1387
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1388
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1155
1389
|
const prev = points[points.length - 2]
|
|
1156
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1390
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1157
1391
|
const head = input.title ? `${input.title}: ` : ""
|
|
1158
|
-
return `${head}
|
|
1392
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1159
1393
|
}
|
|
1160
1394
|
|
|
1161
1395
|
/**
|
|
1162
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1163
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1396
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1397
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1164
1398
|
*/
|
|
1165
1399
|
function buildShareSummary(
|
|
1166
1400
|
slices: readonly { name: string; value: number }[],
|
|
1167
1401
|
title?: string,
|
|
1402
|
+
locale?: string,
|
|
1168
1403
|
): string | undefined {
|
|
1169
1404
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1170
1405
|
const top = slices[0]
|
|
1171
1406
|
if (!top || total <= 0) return undefined
|
|
1172
1407
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1173
|
-
return `${title ? `${title}: ` : ""}
|
|
1408
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1174
1409
|
}
|
|
1175
1410
|
|
|
1176
1411
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
1177
1412
|
|
|
1413
|
+
/** The built-in words of the option builders, by language. */
|
|
1414
|
+
const CHART_TEXT = {
|
|
1415
|
+
ja: {
|
|
1416
|
+
target: "目標",
|
|
1417
|
+
forecast: "予測",
|
|
1418
|
+
total: "合計",
|
|
1419
|
+
provisional: "速報",
|
|
1420
|
+
noComparison: "比較なし",
|
|
1421
|
+
share: (share: string) => `(${share})`,
|
|
1422
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
1423
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
1424
|
+
},
|
|
1425
|
+
en: {
|
|
1426
|
+
target: "Target",
|
|
1427
|
+
forecast: "Forecast",
|
|
1428
|
+
total: "Total",
|
|
1429
|
+
provisional: "Preliminary",
|
|
1430
|
+
noComparison: "No comparison",
|
|
1431
|
+
share: (share: string) => ` (${share})`,
|
|
1432
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
1433
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
1434
|
+
},
|
|
1435
|
+
} satisfies Record<UiLanguage, unknown>
|
|
1436
|
+
|
|
1437
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
1438
|
+
|
|
1178
1439
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
1179
1440
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
1180
1441
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -1359,7 +1620,7 @@ interface CartesianChartConfig {
|
|
|
1359
1620
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
1360
1621
|
*/
|
|
1361
1622
|
axisTicks?: (top: number) => (value: number) => string
|
|
1362
|
-
/** Locale of the time-axis labels. Default
|
|
1623
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
1363
1624
|
locale?: string
|
|
1364
1625
|
type?: CartesianStackType
|
|
1365
1626
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -1597,11 +1858,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
1597
1858
|
*/
|
|
1598
1859
|
function timeAxisLabels(
|
|
1599
1860
|
values: readonly unknown[],
|
|
1600
|
-
|
|
1861
|
+
localeArg?: string,
|
|
1601
1862
|
shown: (i: number) => boolean = () => true,
|
|
1602
1863
|
): string[] | null {
|
|
1603
1864
|
const parsed = parseTimeIndex(values)
|
|
1604
1865
|
if (!parsed) return null
|
|
1866
|
+
const locale = resolveLocale(localeArg)
|
|
1605
1867
|
const { granularity, times } = parsed
|
|
1606
1868
|
let prevYear: number | null = null
|
|
1607
1869
|
return times.map((t, i) => {
|
|
@@ -1624,9 +1886,10 @@ function timeAxisLabels(
|
|
|
1624
1886
|
}
|
|
1625
1887
|
|
|
1626
1888
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
1627
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1889
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
1628
1890
|
const parsed = parseTimeIndex(values)
|
|
1629
1891
|
if (!parsed) return null
|
|
1892
|
+
const locale = resolveLocale(localeArg)
|
|
1630
1893
|
const options: Intl.DateTimeFormatOptions =
|
|
1631
1894
|
parsed.granularity === "year"
|
|
1632
1895
|
? { year: "numeric" }
|
|
@@ -1766,7 +2029,9 @@ function tooltipHtml(
|
|
|
1766
2029
|
rows: readonly TooltipRow[],
|
|
1767
2030
|
valueFormatter: (value: number) => string,
|
|
1768
2031
|
notes: readonly string[] = [],
|
|
2032
|
+
locale?: string,
|
|
1769
2033
|
): string {
|
|
2034
|
+
const t = chartText(locale)
|
|
1770
2035
|
const lines = rows.map(
|
|
1771
2036
|
(row) =>
|
|
1772
2037
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1775,7 +2040,7 @@ function tooltipHtml(
|
|
|
1775
2040
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1776
2041
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1777
2042
|
}${
|
|
1778
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
2043
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1779
2044
|
}</span></div>` +
|
|
1780
2045
|
(row.note === undefined
|
|
1781
2046
|
? ""
|
|
@@ -1789,8 +2054,10 @@ function tooltipHtml(
|
|
|
1789
2054
|
|
|
1790
2055
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1791
2056
|
|
|
1792
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1793
|
-
|
|
2057
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
2058
|
+
function barTargetLabel(locale?: string): string {
|
|
2059
|
+
return chartText(locale).target
|
|
2060
|
+
}
|
|
1794
2061
|
|
|
1795
2062
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1796
2063
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1841,16 +2108,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1841
2108
|
return (value / target) * 100
|
|
1842
2109
|
}
|
|
1843
2110
|
|
|
1844
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
2111
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1845
2112
|
function barTargetNote(
|
|
1846
2113
|
label: string,
|
|
1847
2114
|
value: number | null,
|
|
1848
2115
|
target: number | null,
|
|
1849
2116
|
valueFormatter: (value: number) => string,
|
|
2117
|
+
locale?: string,
|
|
1850
2118
|
): string | null {
|
|
1851
2119
|
if (target === null) return null
|
|
1852
2120
|
const rate = achievementRate(value, target)
|
|
1853
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
2121
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1854
2122
|
}
|
|
1855
2123
|
|
|
1856
2124
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1942,11 +2210,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1942
2210
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1943
2211
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1944
2212
|
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2213
|
+
/**
|
|
2214
|
+
* 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.
|
|
2215
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
2216
|
+
*/
|
|
2217
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
2218
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1950
2219
|
}
|
|
1951
2220
|
|
|
1952
2221
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -1986,8 +2255,10 @@ interface AxisExtent {
|
|
|
1986
2255
|
max: number
|
|
1987
2256
|
}
|
|
1988
2257
|
|
|
1989
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
1990
|
-
|
|
2258
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
2259
|
+
function stackTotalLabel(locale?: string): string {
|
|
2260
|
+
return chartText(locale).total
|
|
2261
|
+
}
|
|
1991
2262
|
|
|
1992
2263
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
1993
2264
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -2109,8 +2380,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
2109
2380
|
|
|
2110
2381
|
/**
|
|
2111
2382
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
2112
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2113
|
-
* either value is missing or the baseline is 0.
|
|
2383
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2384
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
2114
2385
|
*/
|
|
2115
2386
|
function comparisonNote(
|
|
2116
2387
|
current: number | null,
|
|
@@ -2118,16 +2389,18 @@ function comparisonNote(
|
|
|
2118
2389
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
2119
2390
|
grain?: PeriodGrain,
|
|
2120
2391
|
format?: ValueFormat,
|
|
2121
|
-
|
|
2392
|
+
localeArg?: string,
|
|
2122
2393
|
): string {
|
|
2394
|
+
const locale = resolveLocale(localeArg)
|
|
2123
2395
|
const result = computeComparison(
|
|
2124
2396
|
current,
|
|
2125
2397
|
comparison.vs === "custom"
|
|
2126
2398
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
2127
2399
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
2400
|
+
{ locale },
|
|
2128
2401
|
)
|
|
2129
2402
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
2130
|
-
if (text === null) return
|
|
2403
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
2131
2404
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
2132
2405
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
2133
2406
|
}
|
|
@@ -2170,7 +2443,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2170
2443
|
toneColors,
|
|
2171
2444
|
valueFormatter,
|
|
2172
2445
|
axisFormatter = valueFormatter,
|
|
2173
|
-
locale = DEFAULT_LOCALE,
|
|
2174
2446
|
type = "default",
|
|
2175
2447
|
layout = "horizontal",
|
|
2176
2448
|
curveType = "linear",
|
|
@@ -2207,7 +2479,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2207
2479
|
chartOptions,
|
|
2208
2480
|
reserveActions = false,
|
|
2209
2481
|
} = cfg
|
|
2210
|
-
const
|
|
2482
|
+
const locale = resolveLocale(cfg.locale)
|
|
2483
|
+
const text = chartText(locale)
|
|
2484
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
2211
2485
|
|
|
2212
2486
|
const stacked = kind !== "line" && type !== "default"
|
|
2213
2487
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -2356,9 +2630,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2356
2630
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
2357
2631
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
2358
2632
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
2359
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
2633
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
2360
2634
|
const highlightText = (value: number) =>
|
|
2361
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
2635
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
2362
2636
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
2363
2637
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
2364
2638
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -2384,7 +2658,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2384
2658
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
2385
2659
|
const barTargets: (number | null)[] | null =
|
|
2386
2660
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
2387
|
-
const barTargetName = targetLabel ??
|
|
2661
|
+
const barTargetName = targetLabel ?? text.target
|
|
2388
2662
|
const barTargetColor = labelColor ?? guideColor
|
|
2389
2663
|
|
|
2390
2664
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -2598,7 +2872,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2598
2872
|
}
|
|
2599
2873
|
|
|
2600
2874
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
2601
|
-
const forecastLabel = forecast?.label ??
|
|
2875
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
2602
2876
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
2603
2877
|
const forecastValues: (number | null)[] = []
|
|
2604
2878
|
if (forecast) {
|
|
@@ -3074,7 +3348,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3074
3348
|
})
|
|
3075
3349
|
}
|
|
3076
3350
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
3077
|
-
rows.push({ name:
|
|
3351
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3078
3352
|
}
|
|
3079
3353
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
3080
3354
|
if (provisional) {
|
|
@@ -3090,19 +3364,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3090
3364
|
...noteFor(at),
|
|
3091
3365
|
})
|
|
3092
3366
|
})
|
|
3093
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
3367
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3094
3368
|
}
|
|
3095
3369
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
3096
3370
|
const lines: string[] = []
|
|
3097
3371
|
if (barTargets && i <= lastActual) {
|
|
3098
3372
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
3099
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
3373
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
3100
3374
|
if (note) lines.push(note)
|
|
3101
3375
|
}
|
|
3102
3376
|
for (const note of notes) {
|
|
3103
3377
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
3104
3378
|
}
|
|
3105
|
-
return tooltipHtml(provisional ?
|
|
3379
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
3106
3380
|
},
|
|
3107
3381
|
},
|
|
3108
3382
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -3194,8 +3468,6 @@ interface DonutOptionSlice {
|
|
|
3194
3468
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
3195
3469
|
const DONUT_OUTSIDE_LABELS = 2
|
|
3196
3470
|
|
|
3197
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
3198
|
-
|
|
3199
3471
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
3200
3472
|
|
|
3201
3473
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -3228,15 +3500,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
3228
3500
|
}
|
|
3229
3501
|
|
|
3230
3502
|
/**
|
|
3231
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
3503
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
3232
3504
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
3233
3505
|
* string (`value`, 「9.2万 セッション」).
|
|
3234
3506
|
*/
|
|
3235
3507
|
function donutCenterText(
|
|
3236
3508
|
total: number,
|
|
3237
3509
|
valueFormatter: (value: number) => string,
|
|
3238
|
-
{ totalLabel
|
|
3510
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
3239
3511
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
3512
|
+
totalLabel ??= chartText(locale).total
|
|
3240
3513
|
const amount = valueFormatter(total)
|
|
3241
3514
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
3242
3515
|
}
|
|
@@ -3464,8 +3737,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
3464
3737
|
|
|
3465
3738
|
/* ------------------------------------------------------------ provisional */
|
|
3466
3739
|
|
|
3467
|
-
/** The default label for points that are not final yet. */
|
|
3468
|
-
|
|
3740
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
3741
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
3742
|
+
return chartText(locale).provisional
|
|
3743
|
+
}
|
|
3469
3744
|
|
|
3470
3745
|
/**
|
|
3471
3746
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -3545,6 +3820,11 @@ export interface BarListProps<T extends object = BarListItem>
|
|
|
3545
3820
|
ariaLabel?: string
|
|
3546
3821
|
}
|
|
3547
3822
|
|
|
3823
|
+
const TEXT = {
|
|
3824
|
+
ja: { item: "項目", value: "値" },
|
|
3825
|
+
en: { item: "Item", value: "Value" },
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3548
3828
|
/** Row height (h-8) + gap (space-y-1.5): the skeleton / empty height for `n` rows. */
|
|
3549
3829
|
const listHeight = (n: number) => n * 32 + Math.max(0, n - 1) * 6
|
|
3550
3830
|
|
|
@@ -3587,17 +3867,19 @@ function BarListImpl<T extends object>(
|
|
|
3587
3867
|
return () => cancelAnimationFrame(frame)
|
|
3588
3868
|
}, [showAnimation])
|
|
3589
3869
|
|
|
3870
|
+
const locale = useLocale()
|
|
3871
|
+
const t = TEXT[uiLanguage(locale)]
|
|
3590
3872
|
const maxItems = barListOptions?.maxItems ?? MAX_CHART_SERIES
|
|
3591
3873
|
const token = Array.isArray(colors) ? colors[0] : colors
|
|
3592
3874
|
const barColor = token ?? color ?? "chart-1"
|
|
3593
3875
|
|
|
3594
|
-
const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format)
|
|
3876
|
+
const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format, locale)
|
|
3595
3877
|
const baseRows = barListRows(data, {
|
|
3596
3878
|
index,
|
|
3597
3879
|
category: categories?.[0],
|
|
3598
3880
|
sortOrder: barListOptions?.sortOrder ?? sortOrder ?? "descending",
|
|
3599
3881
|
maxItems,
|
|
3600
|
-
otherLabel: barListOptions?.otherLabel,
|
|
3882
|
+
otherLabel: barListOptions?.otherLabel ?? otherSeriesLabel(locale),
|
|
3601
3883
|
exclude: barListOptions?.exclude,
|
|
3602
3884
|
})
|
|
3603
3885
|
const rows = baseRows.map((row) => {
|
|
@@ -3616,7 +3898,7 @@ function BarListImpl<T extends object>(
|
|
|
3616
3898
|
"name",
|
|
3617
3899
|
["value"],
|
|
3618
3900
|
formatter,
|
|
3619
|
-
{ name: (index && labels?.[index]) ||
|
|
3901
|
+
{ name: (index && labels?.[index]) || t.item, value: (categories?.[0] && labels?.[categories[0]]) || t.value },
|
|
3620
3902
|
)
|
|
3621
3903
|
|
|
3622
3904
|
const list = (
|