@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
|
@@ -18,15 +18,148 @@ import { forwardRef, useMemo, type ForwardedRef, type ReactElement, type Ref } f
|
|
|
18
18
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
19
19
|
import { EChart } from "@squadbase/vantage/components"
|
|
20
20
|
import type { ChartThreshold } from "@squadbase/vantage/components"
|
|
21
|
-
import {
|
|
21
|
+
import { useSyncExternalStore } from "react"
|
|
22
|
+
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
22
23
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
23
|
-
import {
|
|
24
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
24
25
|
import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
25
26
|
import { type ReactNode } from "react"
|
|
26
27
|
import type { EChartsOption } from "@squadbase/vantage/components"
|
|
27
28
|
import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
29
|
+
import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
|
|
28
30
|
import type { ChartAnnotation, ChartCommonProps, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
|
|
29
31
|
|
|
32
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The display language of the parts and the defaults that follow it.
|
|
36
|
+
*
|
|
37
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
38
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
39
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
40
|
+
*
|
|
41
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
42
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
46
|
+
type UiLanguage = "ja" | "en"
|
|
47
|
+
|
|
48
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
49
|
+
const FALLBACK_LOCALE = "en-US"
|
|
50
|
+
|
|
51
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
52
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
53
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
57
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
58
|
+
const value = tag?.trim()
|
|
59
|
+
if (!value) return FALLBACK_LOCALE
|
|
60
|
+
try {
|
|
61
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
62
|
+
} catch {
|
|
63
|
+
return FALLBACK_LOCALE
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
69
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
70
|
+
*/
|
|
71
|
+
function documentLocale(): string {
|
|
72
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
73
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
77
|
+
function resolveLocale(locale?: string | null): string {
|
|
78
|
+
return locale || documentLocale()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Picks the wording table of the locale's language. */
|
|
82
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
83
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
87
|
+
function defaultCurrency(locale?: string | null): string {
|
|
88
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
92
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
93
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
97
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
98
|
+
|
|
99
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
100
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
104
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
105
|
+
* Monday for the rest.
|
|
106
|
+
*/
|
|
107
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
108
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
109
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
110
|
+
try {
|
|
111
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
112
|
+
getWeekInfo?: () => { firstDay: number }
|
|
113
|
+
weekInfo?: { firstDay: number }
|
|
114
|
+
}
|
|
115
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
116
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
117
|
+
} catch {
|
|
118
|
+
// Fall through to the table below.
|
|
119
|
+
}
|
|
120
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
124
|
+
|
|
125
|
+
const listeners = new Set<() => void>()
|
|
126
|
+
let observer: MutationObserver | null = null
|
|
127
|
+
|
|
128
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
129
|
+
function subscribe(onChange: () => void): () => void {
|
|
130
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
131
|
+
listeners.add(onChange)
|
|
132
|
+
if (!observer) {
|
|
133
|
+
observer = new MutationObserver(() => {
|
|
134
|
+
for (const listener of [...listeners]) listener()
|
|
135
|
+
})
|
|
136
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
137
|
+
}
|
|
138
|
+
return () => {
|
|
139
|
+
listeners.delete(onChange)
|
|
140
|
+
if (listeners.size === 0 && observer) {
|
|
141
|
+
observer.disconnect()
|
|
142
|
+
observer = null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
151
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
152
|
+
*/
|
|
153
|
+
function useDocumentLocale(): string {
|
|
154
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
158
|
+
function useLocale(locale?: string | null): string {
|
|
159
|
+
const page = useDocumentLocale()
|
|
160
|
+
return locale || page
|
|
161
|
+
}
|
|
162
|
+
|
|
30
163
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
31
164
|
|
|
32
165
|
/**
|
|
@@ -61,10 +194,23 @@ interface ChartTokens {
|
|
|
61
194
|
border: string | null
|
|
62
195
|
/** Tooltip surface. */
|
|
63
196
|
popover: string | null
|
|
197
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
198
|
+
colors?: Record<string, string>
|
|
199
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
200
|
+
surface?: string | null
|
|
64
201
|
/** Tooltip text. */
|
|
65
202
|
popoverForeground: string | null
|
|
66
203
|
}
|
|
67
204
|
|
|
205
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
206
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
207
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
208
|
+
const OPTION_COLOR_TOKENS = [
|
|
209
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
210
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
211
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
212
|
+
]
|
|
213
|
+
|
|
68
214
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
69
215
|
|
|
70
216
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -120,8 +266,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
120
266
|
const color = read(token)
|
|
121
267
|
if (color) series.push(color)
|
|
122
268
|
}
|
|
123
|
-
|
|
269
|
+
const tokens: ChartTokens = {
|
|
124
270
|
series,
|
|
271
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
272
|
+
surface: readChartSurface(target),
|
|
125
273
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
126
274
|
foreground: read("--foreground"),
|
|
127
275
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -129,6 +277,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
129
277
|
popover: read("--popover"),
|
|
130
278
|
popoverForeground: read("--popover-foreground"),
|
|
131
279
|
}
|
|
280
|
+
tokenScopes.set(tokens, target)
|
|
281
|
+
return tokens
|
|
132
282
|
}
|
|
133
283
|
|
|
134
284
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -136,6 +286,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
136
286
|
if (a === b) return true
|
|
137
287
|
if (!a || !b) return false
|
|
138
288
|
return (
|
|
289
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
290
|
+
a.surface === b.surface &&
|
|
291
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
292
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
139
293
|
a.series.length === b.series.length &&
|
|
140
294
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
141
295
|
a.fontFamily === b.fontFamily &&
|
|
@@ -161,7 +315,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
161
315
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
162
316
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
163
317
|
*/
|
|
164
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
318
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
165
319
|
if (typeof document === "undefined") return () => {}
|
|
166
320
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
167
321
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -176,8 +330,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
176
330
|
|
|
177
331
|
const themeObserver = new MutationObserver(schedule)
|
|
178
332
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
179
|
-
|
|
180
|
-
|
|
333
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
334
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
335
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
336
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
337
|
+
}
|
|
181
338
|
|
|
182
339
|
const styleObserver = new MutationObserver(schedule)
|
|
183
340
|
if (document.head) {
|
|
@@ -265,22 +422,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
265
422
|
* components that build an ECharts option from token colours themselves (the
|
|
266
423
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
267
424
|
*/
|
|
268
|
-
function useChartTokens(): ChartTokens | null {
|
|
425
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
269
426
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
270
427
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
271
428
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
272
429
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
273
430
|
const tokensRef = useRef(tokens)
|
|
274
431
|
useEffect(() => {
|
|
432
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
275
433
|
const sync = () => {
|
|
276
|
-
const next = readChartTokens()
|
|
434
|
+
const next = readChartTokens(target)
|
|
277
435
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
278
436
|
tokensRef.current = next
|
|
279
437
|
setTokens(next)
|
|
280
438
|
}
|
|
281
439
|
sync()
|
|
282
|
-
return watchChartTokens(sync)
|
|
283
|
-
}, [])
|
|
440
|
+
return watchChartTokens(sync, target)
|
|
441
|
+
}, [scope])
|
|
284
442
|
return tokens
|
|
285
443
|
}
|
|
286
444
|
|
|
@@ -292,7 +450,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
292
450
|
/**
|
|
293
451
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
294
452
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
295
|
-
* variants `"positive-fill"`, …) are read
|
|
453
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
296
454
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
297
455
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
298
456
|
*/
|
|
@@ -303,12 +461,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
303
461
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
304
462
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
305
463
|
if (typeof document === "undefined") return ""
|
|
306
|
-
|
|
464
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
465
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
307
466
|
}
|
|
308
467
|
if (typeof document === "undefined") return color
|
|
309
468
|
let value = color.trim()
|
|
310
469
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
311
|
-
const styles = getComputedStyle(document.documentElement)
|
|
470
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
312
471
|
if (cssVar) {
|
|
313
472
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
314
473
|
} else if (value.includes("var(")) {
|
|
@@ -354,7 +513,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
354
513
|
if (color) return color
|
|
355
514
|
}
|
|
356
515
|
}
|
|
357
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
516
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
358
517
|
}
|
|
359
518
|
|
|
360
519
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -368,9 +527,10 @@ const WIDTH_BUCKET = 40
|
|
|
368
527
|
*/
|
|
369
528
|
function useChartSurface<E extends Element>(
|
|
370
529
|
forwarded: ForwardedRef<E>,
|
|
371
|
-
tokens
|
|
372
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
530
|
+
tokens?: ChartTokens | null,
|
|
531
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
373
532
|
const [node, setNode] = useState<E | null>(null)
|
|
533
|
+
const scopedTokens = useChartTokens(node)
|
|
374
534
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
375
535
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
376
536
|
const ref = useCallback(
|
|
@@ -384,7 +544,7 @@ function useChartSurface<E extends Element>(
|
|
|
384
544
|
useEffect(() => {
|
|
385
545
|
if (!node) return
|
|
386
546
|
setSurface(readChartSurface(node) ?? undefined)
|
|
387
|
-
}, [node, tokens])
|
|
547
|
+
}, [node, tokens, scopedTokens])
|
|
388
548
|
useEffect(() => {
|
|
389
549
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
390
550
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -393,7 +553,7 @@ function useChartSurface<E extends Element>(
|
|
|
393
553
|
observer.observe(node)
|
|
394
554
|
return () => observer.disconnect()
|
|
395
555
|
}, [node])
|
|
396
|
-
return { ref, surface, width }
|
|
556
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
397
557
|
}
|
|
398
558
|
|
|
399
559
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -503,9 +663,10 @@ function resolveValueFormatter(
|
|
|
503
663
|
function resolveAxisFormatter(
|
|
504
664
|
valueFormatter: ValueFormatter | undefined,
|
|
505
665
|
format: ValueFormat | undefined,
|
|
506
|
-
|
|
666
|
+
localeArg?: string,
|
|
507
667
|
): ValueFormatter {
|
|
508
668
|
if (valueFormatter) return valueFormatter
|
|
669
|
+
const locale = resolveLocale(localeArg)
|
|
509
670
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
510
671
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
511
672
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -514,40 +675,75 @@ function resolveAxisFormatter(
|
|
|
514
675
|
return (value) => formatValue(value, short, locale)
|
|
515
676
|
}
|
|
516
677
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
]
|
|
678
|
+
interface CompactUnit {
|
|
679
|
+
token: string
|
|
680
|
+
size: number
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
687
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
688
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
689
|
+
*/
|
|
690
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
691
|
+
const cached = compactUnitCache.get(locale)
|
|
692
|
+
if (cached !== undefined) return cached
|
|
693
|
+
let units: CompactUnit[] | null = []
|
|
694
|
+
try {
|
|
695
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
696
|
+
const seen = new Set<string>()
|
|
697
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
698
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
699
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
700
|
+
if (at === -1) continue
|
|
701
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
702
|
+
units = null
|
|
703
|
+
break
|
|
704
|
+
}
|
|
705
|
+
const token = parts[at]!.value
|
|
706
|
+
if (seen.has(token)) continue
|
|
707
|
+
seen.add(token)
|
|
708
|
+
units.push({ token, size: 10 ** exp })
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
units = null
|
|
712
|
+
}
|
|
713
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
714
|
+
compactUnitCache.set(locale, sorted)
|
|
715
|
+
return sorted
|
|
716
|
+
}
|
|
523
717
|
|
|
524
|
-
const
|
|
718
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
525
719
|
|
|
526
720
|
/**
|
|
527
721
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
528
|
-
* that largest tick, so
|
|
529
|
-
* Only the
|
|
530
|
-
* any other format —
|
|
722
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
723
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
724
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
725
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
531
726
|
*/
|
|
532
727
|
function resolveAxisTickFormatter(
|
|
533
728
|
valueFormatter: ValueFormatter | undefined,
|
|
534
729
|
format: ValueFormat | undefined,
|
|
535
|
-
|
|
730
|
+
localeArg?: string,
|
|
536
731
|
): (top: number) => ValueFormatter {
|
|
732
|
+
const locale = resolveLocale(localeArg)
|
|
537
733
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
538
734
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
if (!aligns) return () => base
|
|
735
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
736
|
+
if (!units) return () => base
|
|
542
737
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
738
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
543
739
|
return (top) => {
|
|
544
|
-
const unit =
|
|
740
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
545
741
|
if (!unit) return base
|
|
546
742
|
return (value) => {
|
|
547
743
|
const text = base(value)
|
|
548
744
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
549
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
550
|
-
return text.replace(
|
|
745
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
746
|
+
return text.replace(numberToken, () => scaled)
|
|
551
747
|
}
|
|
552
748
|
}
|
|
553
749
|
}
|
|
@@ -576,8 +772,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
576
772
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
577
773
|
const MAX_CHART_SERIES = 5
|
|
578
774
|
|
|
579
|
-
/** The series name the folded categories get. */
|
|
580
|
-
const
|
|
775
|
+
/** The series name the folded categories get, by language. */
|
|
776
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
777
|
+
|
|
778
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
779
|
+
function otherSeriesLabel(locale?: string): string {
|
|
780
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
784
|
+
|
|
785
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
786
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
787
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
788
|
+
}
|
|
581
789
|
|
|
582
790
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
583
791
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -597,7 +805,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
597
805
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
598
806
|
items: readonly Item[],
|
|
599
807
|
n: number = MAX_CHART_SERIES,
|
|
600
|
-
otherLabel: string =
|
|
808
|
+
otherLabel: string = otherSeriesLabel(),
|
|
601
809
|
): AggregatedItems<Item> {
|
|
602
810
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
603
811
|
const ranked = items
|
|
@@ -626,7 +834,7 @@ function aggregateSeries(
|
|
|
626
834
|
data: readonly Record<string, unknown>[],
|
|
627
835
|
categories: readonly string[],
|
|
628
836
|
n: number = MAX_CHART_SERIES,
|
|
629
|
-
otherLabel: string =
|
|
837
|
+
otherLabel: string = otherSeriesLabel(),
|
|
630
838
|
): AggregatedSeries {
|
|
631
839
|
const totals = categories.map((name) => ({
|
|
632
840
|
name,
|
|
@@ -658,12 +866,14 @@ function aggregateSeriesKeeping(
|
|
|
658
866
|
categories: readonly string[],
|
|
659
867
|
keep: string | undefined,
|
|
660
868
|
n: number = MAX_CHART_SERIES,
|
|
869
|
+
otherLabel: string = otherSeriesLabel(),
|
|
661
870
|
): AggregatedSeries {
|
|
662
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
871
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
663
872
|
const aggregated = aggregateSeries(
|
|
664
873
|
data,
|
|
665
874
|
categories.filter((c) => c !== keep),
|
|
666
875
|
n,
|
|
876
|
+
otherLabel,
|
|
667
877
|
)
|
|
668
878
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
669
879
|
}
|
|
@@ -699,7 +909,7 @@ function seriesColorsWithComparison(
|
|
|
699
909
|
* Colour per drawn series: the record's token for that category, else the
|
|
700
910
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
701
911
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
702
|
-
* data itself names
|
|
912
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
703
913
|
*/
|
|
704
914
|
function seriesColorsFor(
|
|
705
915
|
categories: readonly string[],
|
|
@@ -711,7 +921,7 @@ function seriesColorsFor(
|
|
|
711
921
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
712
922
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
713
923
|
return categories.map((category, i) => {
|
|
714
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
924
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
715
925
|
const fromRecord = record?.[category]
|
|
716
926
|
if (fromRecord) return fromRecord
|
|
717
927
|
if (list && list.length > 0) {
|
|
@@ -758,7 +968,7 @@ interface DonutSlicesConfig {
|
|
|
758
968
|
*/
|
|
759
969
|
function donutSlices(
|
|
760
970
|
data: readonly Record<string, unknown>[],
|
|
761
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
971
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
762
972
|
): DonutSlice[] {
|
|
763
973
|
const items = data
|
|
764
974
|
.map((row, i) => {
|
|
@@ -771,7 +981,8 @@ function donutSlices(
|
|
|
771
981
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
772
982
|
name: item.name,
|
|
773
983
|
value: item.value,
|
|
774
|
-
|
|
984
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
985
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
775
986
|
isOther: false,
|
|
776
987
|
row: item.row,
|
|
777
988
|
}))
|
|
@@ -844,7 +1055,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
844
1055
|
*/
|
|
845
1056
|
function barListRows<Row extends object>(
|
|
846
1057
|
data: readonly Row[],
|
|
847
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1058
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
848
1059
|
): BarListRow<Row>[] {
|
|
849
1060
|
const items = data.map((row): BarListRow<Row> => {
|
|
850
1061
|
const cells = row as Record<string, unknown>
|
|
@@ -890,19 +1101,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
890
1101
|
|
|
891
1102
|
/**
|
|
892
1103
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
893
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1104
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
894
1105
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
895
1106
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
896
1107
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
897
1108
|
* keeps its contrast.
|
|
898
1109
|
*/
|
|
899
1110
|
function barListFills(
|
|
900
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1111
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
901
1112
|
color: ChartColor,
|
|
902
1113
|
otherColor: string,
|
|
903
1114
|
): string[] {
|
|
904
1115
|
return rows.map((row) =>
|
|
905
|
-
row.isOther
|
|
1116
|
+
row.isOther || isOtherName(row.name)
|
|
1117
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1118
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
906
1119
|
)
|
|
907
1120
|
}
|
|
908
1121
|
|
|
@@ -1118,11 +1331,30 @@ interface ChartSummaryInput {
|
|
|
1118
1331
|
format?: ValueFormat
|
|
1119
1332
|
/** Wins over `format`. */
|
|
1120
1333
|
valueFormatter?: ValueFormatter
|
|
1334
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1335
|
+
locale?: string
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const SUMMARY_TEXT = {
|
|
1339
|
+
ja: {
|
|
1340
|
+
up: "、前の点より増加",
|
|
1341
|
+
down: "、前の点より減少",
|
|
1342
|
+
flat: "、前の点と横ばい",
|
|
1343
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1344
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1345
|
+
},
|
|
1346
|
+
en: {
|
|
1347
|
+
up: ", up from the previous point",
|
|
1348
|
+
down: ", down from the previous point",
|
|
1349
|
+
flat: ", flat from the previous point",
|
|
1350
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1351
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1352
|
+
},
|
|
1121
1353
|
}
|
|
1122
1354
|
|
|
1123
1355
|
/**
|
|
1124
1356
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1125
|
-
* 「<title>: 最新 <index> は
|
|
1357
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1126
1358
|
* computed from the last two non-empty points of the first series — never
|
|
1127
1359
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1128
1360
|
* no usable point.
|
|
@@ -1138,30 +1370,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1138
1370
|
}
|
|
1139
1371
|
const latest = points[points.length - 1]
|
|
1140
1372
|
if (!latest) return undefined
|
|
1141
|
-
const
|
|
1373
|
+
const locale = resolveLocale(input.locale)
|
|
1374
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1375
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1142
1376
|
const prev = points[points.length - 2]
|
|
1143
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1377
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1144
1378
|
const head = input.title ? `${input.title}: ` : ""
|
|
1145
|
-
return `${head}
|
|
1379
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1146
1380
|
}
|
|
1147
1381
|
|
|
1148
1382
|
/**
|
|
1149
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1150
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1383
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1384
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1151
1385
|
*/
|
|
1152
1386
|
function buildShareSummary(
|
|
1153
1387
|
slices: readonly { name: string; value: number }[],
|
|
1154
1388
|
title?: string,
|
|
1389
|
+
locale?: string,
|
|
1155
1390
|
): string | undefined {
|
|
1156
1391
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1157
1392
|
const top = slices[0]
|
|
1158
1393
|
if (!top || total <= 0) return undefined
|
|
1159
1394
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1160
|
-
return `${title ? `${title}: ` : ""}
|
|
1395
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1161
1396
|
}
|
|
1162
1397
|
|
|
1163
1398
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
1164
1399
|
|
|
1400
|
+
/** The built-in words of the option builders, by language. */
|
|
1401
|
+
const CHART_TEXT = {
|
|
1402
|
+
ja: {
|
|
1403
|
+
target: "目標",
|
|
1404
|
+
forecast: "予測",
|
|
1405
|
+
total: "合計",
|
|
1406
|
+
provisional: "速報",
|
|
1407
|
+
noComparison: "比較なし",
|
|
1408
|
+
share: (share: string) => `(${share})`,
|
|
1409
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
1410
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
1411
|
+
},
|
|
1412
|
+
en: {
|
|
1413
|
+
target: "Target",
|
|
1414
|
+
forecast: "Forecast",
|
|
1415
|
+
total: "Total",
|
|
1416
|
+
provisional: "Preliminary",
|
|
1417
|
+
noComparison: "No comparison",
|
|
1418
|
+
share: (share: string) => ` (${share})`,
|
|
1419
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
1420
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
1421
|
+
},
|
|
1422
|
+
} satisfies Record<UiLanguage, unknown>
|
|
1423
|
+
|
|
1424
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
1425
|
+
|
|
1165
1426
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
1166
1427
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
1167
1428
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -1346,7 +1607,7 @@ interface CartesianChartConfig {
|
|
|
1346
1607
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
1347
1608
|
*/
|
|
1348
1609
|
axisTicks?: (top: number) => (value: number) => string
|
|
1349
|
-
/** Locale of the time-axis labels. Default
|
|
1610
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
1350
1611
|
locale?: string
|
|
1351
1612
|
type?: CartesianStackType
|
|
1352
1613
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -1584,11 +1845,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
1584
1845
|
*/
|
|
1585
1846
|
function timeAxisLabels(
|
|
1586
1847
|
values: readonly unknown[],
|
|
1587
|
-
|
|
1848
|
+
localeArg?: string,
|
|
1588
1849
|
shown: (i: number) => boolean = () => true,
|
|
1589
1850
|
): string[] | null {
|
|
1590
1851
|
const parsed = parseTimeIndex(values)
|
|
1591
1852
|
if (!parsed) return null
|
|
1853
|
+
const locale = resolveLocale(localeArg)
|
|
1592
1854
|
const { granularity, times } = parsed
|
|
1593
1855
|
let prevYear: number | null = null
|
|
1594
1856
|
return times.map((t, i) => {
|
|
@@ -1611,9 +1873,10 @@ function timeAxisLabels(
|
|
|
1611
1873
|
}
|
|
1612
1874
|
|
|
1613
1875
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
1614
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1876
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
1615
1877
|
const parsed = parseTimeIndex(values)
|
|
1616
1878
|
if (!parsed) return null
|
|
1879
|
+
const locale = resolveLocale(localeArg)
|
|
1617
1880
|
const options: Intl.DateTimeFormatOptions =
|
|
1618
1881
|
parsed.granularity === "year"
|
|
1619
1882
|
? { year: "numeric" }
|
|
@@ -1753,7 +2016,9 @@ function tooltipHtml(
|
|
|
1753
2016
|
rows: readonly TooltipRow[],
|
|
1754
2017
|
valueFormatter: (value: number) => string,
|
|
1755
2018
|
notes: readonly string[] = [],
|
|
2019
|
+
locale?: string,
|
|
1756
2020
|
): string {
|
|
2021
|
+
const t = chartText(locale)
|
|
1757
2022
|
const lines = rows.map(
|
|
1758
2023
|
(row) =>
|
|
1759
2024
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1762,7 +2027,7 @@ function tooltipHtml(
|
|
|
1762
2027
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1763
2028
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1764
2029
|
}${
|
|
1765
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
2030
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1766
2031
|
}</span></div>` +
|
|
1767
2032
|
(row.note === undefined
|
|
1768
2033
|
? ""
|
|
@@ -1776,8 +2041,10 @@ function tooltipHtml(
|
|
|
1776
2041
|
|
|
1777
2042
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1778
2043
|
|
|
1779
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1780
|
-
|
|
2044
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
2045
|
+
function barTargetLabel(locale?: string): string {
|
|
2046
|
+
return chartText(locale).target
|
|
2047
|
+
}
|
|
1781
2048
|
|
|
1782
2049
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1783
2050
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1828,16 +2095,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1828
2095
|
return (value / target) * 100
|
|
1829
2096
|
}
|
|
1830
2097
|
|
|
1831
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
2098
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1832
2099
|
function barTargetNote(
|
|
1833
2100
|
label: string,
|
|
1834
2101
|
value: number | null,
|
|
1835
2102
|
target: number | null,
|
|
1836
2103
|
valueFormatter: (value: number) => string,
|
|
2104
|
+
locale?: string,
|
|
1837
2105
|
): string | null {
|
|
1838
2106
|
if (target === null) return null
|
|
1839
2107
|
const rate = achievementRate(value, target)
|
|
1840
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
2108
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1841
2109
|
}
|
|
1842
2110
|
|
|
1843
2111
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1929,11 +2197,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1929
2197
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1930
2198
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1931
2199
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2200
|
+
/**
|
|
2201
|
+
* 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.
|
|
2202
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
2203
|
+
*/
|
|
2204
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
2205
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1937
2206
|
}
|
|
1938
2207
|
|
|
1939
2208
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -1973,8 +2242,10 @@ interface AxisExtent {
|
|
|
1973
2242
|
max: number
|
|
1974
2243
|
}
|
|
1975
2244
|
|
|
1976
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
1977
|
-
|
|
2245
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
2246
|
+
function stackTotalLabel(locale?: string): string {
|
|
2247
|
+
return chartText(locale).total
|
|
2248
|
+
}
|
|
1978
2249
|
|
|
1979
2250
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
1980
2251
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -2096,8 +2367,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
2096
2367
|
|
|
2097
2368
|
/**
|
|
2098
2369
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
2099
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2100
|
-
* either value is missing or the baseline is 0.
|
|
2370
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2371
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
2101
2372
|
*/
|
|
2102
2373
|
function comparisonNote(
|
|
2103
2374
|
current: number | null,
|
|
@@ -2105,16 +2376,18 @@ function comparisonNote(
|
|
|
2105
2376
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
2106
2377
|
grain?: PeriodGrain,
|
|
2107
2378
|
format?: ValueFormat,
|
|
2108
|
-
|
|
2379
|
+
localeArg?: string,
|
|
2109
2380
|
): string {
|
|
2381
|
+
const locale = resolveLocale(localeArg)
|
|
2110
2382
|
const result = computeComparison(
|
|
2111
2383
|
current,
|
|
2112
2384
|
comparison.vs === "custom"
|
|
2113
2385
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
2114
2386
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
2387
|
+
{ locale },
|
|
2115
2388
|
)
|
|
2116
2389
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
2117
|
-
if (text === null) return
|
|
2390
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
2118
2391
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
2119
2392
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
2120
2393
|
}
|
|
@@ -2157,7 +2430,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2157
2430
|
toneColors,
|
|
2158
2431
|
valueFormatter,
|
|
2159
2432
|
axisFormatter = valueFormatter,
|
|
2160
|
-
locale = DEFAULT_LOCALE,
|
|
2161
2433
|
type = "default",
|
|
2162
2434
|
layout = "horizontal",
|
|
2163
2435
|
curveType = "linear",
|
|
@@ -2194,7 +2466,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2194
2466
|
chartOptions,
|
|
2195
2467
|
reserveActions = false,
|
|
2196
2468
|
} = cfg
|
|
2197
|
-
const
|
|
2469
|
+
const locale = resolveLocale(cfg.locale)
|
|
2470
|
+
const text = chartText(locale)
|
|
2471
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
2198
2472
|
|
|
2199
2473
|
const stacked = kind !== "line" && type !== "default"
|
|
2200
2474
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -2343,9 +2617,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2343
2617
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
2344
2618
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
2345
2619
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
2346
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
2620
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
2347
2621
|
const highlightText = (value: number) =>
|
|
2348
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
2622
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
2349
2623
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
2350
2624
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
2351
2625
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -2371,7 +2645,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2371
2645
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
2372
2646
|
const barTargets: (number | null)[] | null =
|
|
2373
2647
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
2374
|
-
const barTargetName = targetLabel ??
|
|
2648
|
+
const barTargetName = targetLabel ?? text.target
|
|
2375
2649
|
const barTargetColor = labelColor ?? guideColor
|
|
2376
2650
|
|
|
2377
2651
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -2585,7 +2859,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2585
2859
|
}
|
|
2586
2860
|
|
|
2587
2861
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
2588
|
-
const forecastLabel = forecast?.label ??
|
|
2862
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
2589
2863
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
2590
2864
|
const forecastValues: (number | null)[] = []
|
|
2591
2865
|
if (forecast) {
|
|
@@ -3061,7 +3335,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3061
3335
|
})
|
|
3062
3336
|
}
|
|
3063
3337
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
3064
|
-
rows.push({ name:
|
|
3338
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3065
3339
|
}
|
|
3066
3340
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
3067
3341
|
if (provisional) {
|
|
@@ -3077,19 +3351,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3077
3351
|
...noteFor(at),
|
|
3078
3352
|
})
|
|
3079
3353
|
})
|
|
3080
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
3354
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3081
3355
|
}
|
|
3082
3356
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
3083
3357
|
const lines: string[] = []
|
|
3084
3358
|
if (barTargets && i <= lastActual) {
|
|
3085
3359
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
3086
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
3360
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
3087
3361
|
if (note) lines.push(note)
|
|
3088
3362
|
}
|
|
3089
3363
|
for (const note of notes) {
|
|
3090
3364
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
3091
3365
|
}
|
|
3092
|
-
return tooltipHtml(provisional ?
|
|
3366
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
3093
3367
|
},
|
|
3094
3368
|
},
|
|
3095
3369
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -3181,8 +3455,6 @@ interface DonutOptionSlice {
|
|
|
3181
3455
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
3182
3456
|
const DONUT_OUTSIDE_LABELS = 2
|
|
3183
3457
|
|
|
3184
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
3185
|
-
|
|
3186
3458
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
3187
3459
|
|
|
3188
3460
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -3215,15 +3487,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
3215
3487
|
}
|
|
3216
3488
|
|
|
3217
3489
|
/**
|
|
3218
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
3490
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
3219
3491
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
3220
3492
|
* string (`value`, 「9.2万 セッション」).
|
|
3221
3493
|
*/
|
|
3222
3494
|
function donutCenterText(
|
|
3223
3495
|
total: number,
|
|
3224
3496
|
valueFormatter: (value: number) => string,
|
|
3225
|
-
{ totalLabel
|
|
3497
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
3226
3498
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
3499
|
+
totalLabel ??= chartText(locale).total
|
|
3227
3500
|
const amount = valueFormatter(total)
|
|
3228
3501
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
3229
3502
|
}
|
|
@@ -3451,8 +3724,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
3451
3724
|
|
|
3452
3725
|
/* ------------------------------------------------------------ provisional */
|
|
3453
3726
|
|
|
3454
|
-
/** The default label for points that are not final yet. */
|
|
3455
|
-
|
|
3727
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
3728
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
3729
|
+
return chartText(locale).provisional
|
|
3730
|
+
}
|
|
3456
3731
|
|
|
3457
3732
|
/**
|
|
3458
3733
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -3504,6 +3779,11 @@ export interface BarChartProps<T extends Record<string, unknown> = Record<string
|
|
|
3504
3779
|
barCategoryGap?: string | number
|
|
3505
3780
|
}
|
|
3506
3781
|
|
|
3782
|
+
const TEXT = {
|
|
3783
|
+
ja: { item: "項目", chart: (categories: readonly string[]) => `${categories.join("・")}の棒グラフ` },
|
|
3784
|
+
en: { item: "Item", chart: (categories: readonly string[]) => `Bar chart of ${categories.join(", ")}` },
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3507
3787
|
/** Rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
|
|
3508
3788
|
const NO_ROWS: never[] = []
|
|
3509
3789
|
|
|
@@ -3554,15 +3834,16 @@ function BarChartImpl<T extends Record<string, unknown>>(
|
|
|
3554
3834
|
}: BarChartProps<T>,
|
|
3555
3835
|
ref: ForwardedRef<HTMLDivElement>,
|
|
3556
3836
|
) {
|
|
3557
|
-
const tokens = useChartTokens()
|
|
3558
3837
|
// Highlight labels are backed by the surface the chart sits on (a card, the page).
|
|
3559
|
-
const { ref: frameRef, surface, width: plotWidth } = useChartSurface(ref
|
|
3838
|
+
const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
|
|
3839
|
+
const locale = useLocale()
|
|
3840
|
+
const t = TEXT[uiLanguage(locale)]
|
|
3560
3841
|
// Ticks and on-plot labels read short (¥1億); the tooltip reads the value as `format` says.
|
|
3561
|
-
const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3562
|
-
const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3842
|
+
const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
|
|
3843
|
+
const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
|
|
3563
3844
|
// Ticks share one unit — the largest tick's (¥0.5億 / ¥1億).
|
|
3564
|
-
const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3565
|
-
const series = useMemo(() => aggregateSeries(data, categories), [data, categories])
|
|
3845
|
+
const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
|
|
3846
|
+
const series = useMemo(() => aggregateSeries(data, categories, undefined, otherSeriesLabel(locale)), [data, categories, locale])
|
|
3566
3847
|
const palette = useMemo(
|
|
3567
3848
|
() =>
|
|
3568
3849
|
resolveChartColors(
|
|
@@ -3624,24 +3905,25 @@ function BarChartImpl<T extends Record<string, unknown>>(
|
|
|
3624
3905
|
plotWidth,
|
|
3625
3906
|
plotHeight: typeof height === "number" ? height : undefined,
|
|
3626
3907
|
chartOptions,
|
|
3908
|
+
locale,
|
|
3627
3909
|
}),
|
|
3628
3910
|
[
|
|
3629
3911
|
series, index, palette, labels, thresholds, toneColors, tooltipFormatter, axisFormatter, axisTicks, stackType, barLayout, gap, showZeroValues,
|
|
3630
3912
|
showLegend, showTooltip, showGridLines, showXAxis, showYAxis, startEndOnly, yAxisWidth,
|
|
3631
3913
|
maxValue, allowDecimals, xAxisLabel, yAxisLabel, active,
|
|
3632
|
-
provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions,
|
|
3914
|
+
provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions, locale,
|
|
3633
3915
|
],
|
|
3634
3916
|
)
|
|
3635
3917
|
// The screen-reader table: exact values (tooltip format), dates read as dates, 「その他」 as its own column.
|
|
3636
3918
|
const dataTable = useMemo(() => {
|
|
3637
|
-
const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels,
|
|
3638
|
-
const times = timeTooltipLabels(series.data.map((row) => row[index]))
|
|
3919
|
+
const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, t.item)
|
|
3920
|
+
const times = timeTooltipLabels(series.data.map((row) => row[index]), locale)
|
|
3639
3921
|
return times ? { ...table, rows: table.rows.map((row, i) => [times[i] ?? row[0] ?? "", ...row.slice(1)]) } : table
|
|
3640
|
-
}, [series, index, tooltipFormatter, labels])
|
|
3641
|
-
const label = ariaLabel ??
|
|
3922
|
+
}, [series, index, tooltipFormatter, labels, t, locale])
|
|
3923
|
+
const label = ariaLabel ?? t.chart(categories)
|
|
3642
3924
|
const resolvedSummary = useMemo(
|
|
3643
|
-
() => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter }) : summary),
|
|
3644
|
-
[summary, data, index, categories, title, format, valueFormatter],
|
|
3925
|
+
() => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter, locale }) : summary),
|
|
3926
|
+
[summary, data, index, categories, title, format, valueFormatter, locale],
|
|
3645
3927
|
)
|
|
3646
3928
|
return (
|
|
3647
3929
|
<ChartFrame
|