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