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