@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
|
@@ -20,11 +20,143 @@ import { forwardRef, useId, type HTMLAttributes, type ReactNode } from "react"
|
|
|
20
20
|
|
|
21
21
|
import { cn } from "@squadbase/vantage/ui"
|
|
22
22
|
import type { ChartEmptyState } from "@squadbase/vantage/components"
|
|
23
|
-
import {
|
|
23
|
+
import { useSyncExternalStore } from "react"
|
|
24
|
+
import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
|
|
24
25
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
25
|
-
import {
|
|
26
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
26
27
|
import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
27
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
|
+
|
|
28
160
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
29
161
|
|
|
30
162
|
/**
|
|
@@ -59,10 +191,23 @@ interface ChartTokens {
|
|
|
59
191
|
border: string | null
|
|
60
192
|
/** Tooltip surface. */
|
|
61
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
|
|
62
198
|
/** Tooltip text. */
|
|
63
199
|
popoverForeground: string | null
|
|
64
200
|
}
|
|
65
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
|
+
|
|
66
211
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
67
212
|
|
|
68
213
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -118,8 +263,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
118
263
|
const color = read(token)
|
|
119
264
|
if (color) series.push(color)
|
|
120
265
|
}
|
|
121
|
-
|
|
266
|
+
const tokens: ChartTokens = {
|
|
122
267
|
series,
|
|
268
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
269
|
+
surface: readChartSurface(target),
|
|
123
270
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
124
271
|
foreground: read("--foreground"),
|
|
125
272
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -127,6 +274,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
127
274
|
popover: read("--popover"),
|
|
128
275
|
popoverForeground: read("--popover-foreground"),
|
|
129
276
|
}
|
|
277
|
+
tokenScopes.set(tokens, target)
|
|
278
|
+
return tokens
|
|
130
279
|
}
|
|
131
280
|
|
|
132
281
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -134,6 +283,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
134
283
|
if (a === b) return true
|
|
135
284
|
if (!a || !b) return false
|
|
136
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) &&
|
|
137
290
|
a.series.length === b.series.length &&
|
|
138
291
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
139
292
|
a.fontFamily === b.fontFamily &&
|
|
@@ -159,7 +312,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
159
312
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
160
313
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
161
314
|
*/
|
|
162
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
315
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
163
316
|
if (typeof document === "undefined") return () => {}
|
|
164
317
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
165
318
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -174,8 +327,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
174
327
|
|
|
175
328
|
const themeObserver = new MutationObserver(schedule)
|
|
176
329
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
335
|
|
|
180
336
|
const styleObserver = new MutationObserver(schedule)
|
|
181
337
|
if (document.head) {
|
|
@@ -263,22 +419,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
263
419
|
* components that build an ECharts option from token colours themselves (the
|
|
264
420
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
265
421
|
*/
|
|
266
|
-
function useChartTokens(): ChartTokens | null {
|
|
422
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
267
423
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
268
424
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
269
425
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
270
426
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
271
427
|
const tokensRef = useRef(tokens)
|
|
272
428
|
useEffect(() => {
|
|
429
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
273
430
|
const sync = () => {
|
|
274
|
-
const next = readChartTokens()
|
|
431
|
+
const next = readChartTokens(target)
|
|
275
432
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
276
433
|
tokensRef.current = next
|
|
277
434
|
setTokens(next)
|
|
278
435
|
}
|
|
279
436
|
sync()
|
|
280
|
-
return watchChartTokens(sync)
|
|
281
|
-
}, [])
|
|
437
|
+
return watchChartTokens(sync, target)
|
|
438
|
+
}, [scope])
|
|
282
439
|
return tokens
|
|
283
440
|
}
|
|
284
441
|
|
|
@@ -290,7 +447,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
290
447
|
/**
|
|
291
448
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
292
449
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
293
|
-
* 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
|
|
294
451
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
295
452
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
296
453
|
*/
|
|
@@ -301,12 +458,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
301
458
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
302
459
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
303
460
|
if (typeof document === "undefined") return ""
|
|
304
|
-
|
|
461
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
462
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
305
463
|
}
|
|
306
464
|
if (typeof document === "undefined") return color
|
|
307
465
|
let value = color.trim()
|
|
308
466
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
309
|
-
const styles = getComputedStyle(document.documentElement)
|
|
467
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
310
468
|
if (cssVar) {
|
|
311
469
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
312
470
|
} else if (value.includes("var(")) {
|
|
@@ -352,7 +510,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
352
510
|
if (color) return color
|
|
353
511
|
}
|
|
354
512
|
}
|
|
355
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
513
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
356
514
|
}
|
|
357
515
|
|
|
358
516
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -366,9 +524,10 @@ const WIDTH_BUCKET = 40
|
|
|
366
524
|
*/
|
|
367
525
|
function useChartSurface<E extends Element>(
|
|
368
526
|
forwarded: ForwardedRef<E>,
|
|
369
|
-
tokens
|
|
370
|
-
): { 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 } {
|
|
371
529
|
const [node, setNode] = useState<E | null>(null)
|
|
530
|
+
const scopedTokens = useChartTokens(node)
|
|
372
531
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
373
532
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
374
533
|
const ref = useCallback(
|
|
@@ -382,7 +541,7 @@ function useChartSurface<E extends Element>(
|
|
|
382
541
|
useEffect(() => {
|
|
383
542
|
if (!node) return
|
|
384
543
|
setSurface(readChartSurface(node) ?? undefined)
|
|
385
|
-
}, [node, tokens])
|
|
544
|
+
}, [node, tokens, scopedTokens])
|
|
386
545
|
useEffect(() => {
|
|
387
546
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
388
547
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -391,7 +550,7 @@ function useChartSurface<E extends Element>(
|
|
|
391
550
|
observer.observe(node)
|
|
392
551
|
return () => observer.disconnect()
|
|
393
552
|
}, [node])
|
|
394
|
-
return { ref, surface, width }
|
|
553
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
395
554
|
}
|
|
396
555
|
|
|
397
556
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -501,9 +660,10 @@ function resolveValueFormatter(
|
|
|
501
660
|
function resolveAxisFormatter(
|
|
502
661
|
valueFormatter: ValueFormatter | undefined,
|
|
503
662
|
format: ValueFormat | undefined,
|
|
504
|
-
|
|
663
|
+
localeArg?: string,
|
|
505
664
|
): ValueFormatter {
|
|
506
665
|
if (valueFormatter) return valueFormatter
|
|
666
|
+
const locale = resolveLocale(localeArg)
|
|
507
667
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
508
668
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
509
669
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -512,40 +672,75 @@ function resolveAxisFormatter(
|
|
|
512
672
|
return (value) => formatValue(value, short, locale)
|
|
513
673
|
}
|
|
514
674
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
{ char: "万", size: 1e4 },
|
|
520
|
-
] as const
|
|
675
|
+
interface CompactUnit {
|
|
676
|
+
token: string
|
|
677
|
+
size: number
|
|
678
|
+
}
|
|
521
679
|
|
|
522
|
-
const
|
|
680
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
681
|
+
|
|
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, "\\$&")
|
|
523
716
|
|
|
524
717
|
/**
|
|
525
718
|
* 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 —
|
|
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.
|
|
529
723
|
*/
|
|
530
724
|
function resolveAxisTickFormatter(
|
|
531
725
|
valueFormatter: ValueFormatter | undefined,
|
|
532
726
|
format: ValueFormat | undefined,
|
|
533
|
-
|
|
727
|
+
localeArg?: string,
|
|
534
728
|
): (top: number) => ValueFormatter {
|
|
729
|
+
const locale = resolveLocale(localeArg)
|
|
535
730
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
536
731
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
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
|
|
540
734
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
735
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
541
736
|
return (top) => {
|
|
542
|
-
const unit =
|
|
737
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
543
738
|
if (!unit) return base
|
|
544
739
|
return (value) => {
|
|
545
740
|
const text = base(value)
|
|
546
741
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
547
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
548
|
-
return text.replace(
|
|
742
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
743
|
+
return text.replace(numberToken, () => scaled)
|
|
549
744
|
}
|
|
550
745
|
}
|
|
551
746
|
}
|
|
@@ -574,8 +769,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
574
769
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
575
770
|
const MAX_CHART_SERIES = 5
|
|
576
771
|
|
|
577
|
-
/** The series name the folded categories get. */
|
|
578
|
-
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
|
+
}
|
|
579
786
|
|
|
580
787
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
581
788
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -595,7 +802,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
595
802
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
596
803
|
items: readonly Item[],
|
|
597
804
|
n: number = MAX_CHART_SERIES,
|
|
598
|
-
otherLabel: string =
|
|
805
|
+
otherLabel: string = otherSeriesLabel(),
|
|
599
806
|
): AggregatedItems<Item> {
|
|
600
807
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
601
808
|
const ranked = items
|
|
@@ -624,7 +831,7 @@ function aggregateSeries(
|
|
|
624
831
|
data: readonly Record<string, unknown>[],
|
|
625
832
|
categories: readonly string[],
|
|
626
833
|
n: number = MAX_CHART_SERIES,
|
|
627
|
-
otherLabel: string =
|
|
834
|
+
otherLabel: string = otherSeriesLabel(),
|
|
628
835
|
): AggregatedSeries {
|
|
629
836
|
const totals = categories.map((name) => ({
|
|
630
837
|
name,
|
|
@@ -656,12 +863,14 @@ function aggregateSeriesKeeping(
|
|
|
656
863
|
categories: readonly string[],
|
|
657
864
|
keep: string | undefined,
|
|
658
865
|
n: number = MAX_CHART_SERIES,
|
|
866
|
+
otherLabel: string = otherSeriesLabel(),
|
|
659
867
|
): AggregatedSeries {
|
|
660
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
868
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
661
869
|
const aggregated = aggregateSeries(
|
|
662
870
|
data,
|
|
663
871
|
categories.filter((c) => c !== keep),
|
|
664
872
|
n,
|
|
873
|
+
otherLabel,
|
|
665
874
|
)
|
|
666
875
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
667
876
|
}
|
|
@@ -697,7 +906,7 @@ function seriesColorsWithComparison(
|
|
|
697
906
|
* Colour per drawn series: the record's token for that category, else the
|
|
698
907
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
699
908
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
700
|
-
* data itself names
|
|
909
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
701
910
|
*/
|
|
702
911
|
function seriesColorsFor(
|
|
703
912
|
categories: readonly string[],
|
|
@@ -709,7 +918,7 @@ function seriesColorsFor(
|
|
|
709
918
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
710
919
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
711
920
|
return categories.map((category, i) => {
|
|
712
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
921
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
713
922
|
const fromRecord = record?.[category]
|
|
714
923
|
if (fromRecord) return fromRecord
|
|
715
924
|
if (list && list.length > 0) {
|
|
@@ -756,7 +965,7 @@ interface DonutSlicesConfig {
|
|
|
756
965
|
*/
|
|
757
966
|
function donutSlices(
|
|
758
967
|
data: readonly Record<string, unknown>[],
|
|
759
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
968
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
760
969
|
): DonutSlice[] {
|
|
761
970
|
const items = data
|
|
762
971
|
.map((row, i) => {
|
|
@@ -769,7 +978,8 @@ function donutSlices(
|
|
|
769
978
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
770
979
|
name: item.name,
|
|
771
980
|
value: item.value,
|
|
772
|
-
|
|
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]!,
|
|
773
983
|
isOther: false,
|
|
774
984
|
row: item.row,
|
|
775
985
|
}))
|
|
@@ -842,7 +1052,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
842
1052
|
*/
|
|
843
1053
|
function barListRows<Row extends object>(
|
|
844
1054
|
data: readonly Row[],
|
|
845
|
-
{ 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 = {},
|
|
846
1056
|
): BarListRow<Row>[] {
|
|
847
1057
|
const items = data.map((row): BarListRow<Row> => {
|
|
848
1058
|
const cells = row as Record<string, unknown>
|
|
@@ -888,19 +1098,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
888
1098
|
|
|
889
1099
|
/**
|
|
890
1100
|
* 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`
|
|
1101
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
892
1102
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
893
1103
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
894
1104
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
895
1105
|
* keeps its contrast.
|
|
896
1106
|
*/
|
|
897
1107
|
function barListFills(
|
|
898
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1108
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
899
1109
|
color: ChartColor,
|
|
900
1110
|
otherColor: string,
|
|
901
1111
|
): string[] {
|
|
902
1112
|
return rows.map((row) =>
|
|
903
|
-
row.isOther
|
|
1113
|
+
row.isOther || isOtherName(row.name)
|
|
1114
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1115
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
904
1116
|
)
|
|
905
1117
|
}
|
|
906
1118
|
|
|
@@ -1116,11 +1328,30 @@ interface ChartSummaryInput {
|
|
|
1116
1328
|
format?: ValueFormat
|
|
1117
1329
|
/** Wins over `format`. */
|
|
1118
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
|
+
},
|
|
1119
1350
|
}
|
|
1120
1351
|
|
|
1121
1352
|
/**
|
|
1122
1353
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1123
|
-
* 「<title>: 最新 <index> は
|
|
1354
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1124
1355
|
* computed from the last two non-empty points of the first series — never
|
|
1125
1356
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1126
1357
|
* no usable point.
|
|
@@ -1136,26 +1367,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1136
1367
|
}
|
|
1137
1368
|
const latest = points[points.length - 1]
|
|
1138
1369
|
if (!latest) return undefined
|
|
1139
|
-
const
|
|
1370
|
+
const locale = resolveLocale(input.locale)
|
|
1371
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1372
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1140
1373
|
const prev = points[points.length - 2]
|
|
1141
|
-
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
|
|
1142
1375
|
const head = input.title ? `${input.title}: ` : ""
|
|
1143
|
-
return `${head}
|
|
1376
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1144
1377
|
}
|
|
1145
1378
|
|
|
1146
1379
|
/**
|
|
1147
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1148
|
-
* (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.
|
|
1149
1382
|
*/
|
|
1150
1383
|
function buildShareSummary(
|
|
1151
1384
|
slices: readonly { name: string; value: number }[],
|
|
1152
1385
|
title?: string,
|
|
1386
|
+
locale?: string,
|
|
1153
1387
|
): string | undefined {
|
|
1154
1388
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1155
1389
|
const top = slices[0]
|
|
1156
1390
|
if (!top || total <= 0) return undefined
|
|
1157
1391
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1158
|
-
return `${title ? `${title}: ` : ""}
|
|
1392
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1159
1393
|
}
|
|
1160
1394
|
|
|
1161
1395
|
/* ---------- end of inlined code ---------- */
|
|
@@ -1166,7 +1400,10 @@ export type ChartSkeletonVariant = "line" | "area" | "bar" | "donut" | "list"
|
|
|
1166
1400
|
/** The plot height of every typed chart unless `height` says otherwise (px). */
|
|
1167
1401
|
export const DEFAULT_CHART_HEIGHT = 288
|
|
1168
1402
|
|
|
1169
|
-
const
|
|
1403
|
+
const TEXT = {
|
|
1404
|
+
ja: { emptyTitle: "表示できるデータがありません", loading: "読み込み中" },
|
|
1405
|
+
en: { emptyTitle: "No data to display", loading: "Loading" },
|
|
1406
|
+
}
|
|
1170
1407
|
|
|
1171
1408
|
export interface ChartFrameProps extends Omit<HTMLAttributes<HTMLElement>, "title" | "children"> {
|
|
1172
1409
|
/** グラフの見出し。プロット領域の `aria-labelledby` になる。 */
|
|
@@ -1218,6 +1455,7 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1218
1455
|
ref,
|
|
1219
1456
|
) {
|
|
1220
1457
|
const id = useId()
|
|
1458
|
+
const t = TEXT[uiLanguage(useLocale())]
|
|
1221
1459
|
const titleId = title ? `${id}-title` : undefined
|
|
1222
1460
|
const summaryId = summary ? `${id}-summary` : undefined
|
|
1223
1461
|
const state = isLoading ? "loading" : isEmpty ? "empty" : "ready"
|
|
@@ -1251,9 +1489,9 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1251
1489
|
style={{ height: typeof height === "number" ? `${height}px` : height }}
|
|
1252
1490
|
>
|
|
1253
1491
|
{state === "loading" ? (
|
|
1254
|
-
<ChartSkeleton variant={variant} />
|
|
1492
|
+
<ChartSkeleton variant={variant} loadingText={t.loading} />
|
|
1255
1493
|
) : state === "empty" ? (
|
|
1256
|
-
<ChartEmpty {...emptyState} />
|
|
1494
|
+
<ChartEmpty {...emptyState} defaultTitle={t.emptyTitle} />
|
|
1257
1495
|
) : (
|
|
1258
1496
|
<ChartFrameContext.Provider value>{children}</ChartFrameContext.Provider>
|
|
1259
1497
|
)}
|
|
@@ -1272,7 +1510,7 @@ const BAR_HEIGHTS = [58, 74, 46, 88, 64, 80, 52]
|
|
|
1272
1510
|
const LIST_WIDTHS = [92, 76, 64, 48, 36]
|
|
1273
1511
|
const LINE_POINTS = "0,70 16,58 33,62 50,40 66,46 83,28 100,34"
|
|
1274
1512
|
|
|
1275
|
-
function ChartSkeleton({ variant }: { variant: ChartSkeletonVariant }) {
|
|
1513
|
+
function ChartSkeleton({ variant, loadingText }: { variant: ChartSkeletonVariant; loadingText: string }) {
|
|
1276
1514
|
const pulse = "animate-pulse motion-reduce:animate-none"
|
|
1277
1515
|
return (
|
|
1278
1516
|
<div data-slot="chart-skeleton" data-variant={variant} className="absolute inset-0 flex flex-col gap-3">
|
|
@@ -1306,7 +1544,7 @@ function ChartSkeleton({ variant }: { variant: ChartSkeletonVariant }) {
|
|
|
1306
1544
|
</div>
|
|
1307
1545
|
)}
|
|
1308
1546
|
</div>
|
|
1309
|
-
<span className="sr-only"
|
|
1547
|
+
<span className="sr-only">{loadingText}</span>
|
|
1310
1548
|
</div>
|
|
1311
1549
|
)
|
|
1312
1550
|
}
|
|
@@ -1352,13 +1590,13 @@ function ChartDataTableView({ table, fallbackCaption }: { table: ChartDataTable;
|
|
|
1352
1590
|
|
|
1353
1591
|
/* ------------------------------------------------------------------- empty */
|
|
1354
1592
|
|
|
1355
|
-
function ChartEmpty({ title, description, action }: ChartEmptyState) {
|
|
1593
|
+
function ChartEmpty({ title, description, action, defaultTitle }: ChartEmptyState & { defaultTitle: string }) {
|
|
1356
1594
|
return (
|
|
1357
1595
|
<div
|
|
1358
1596
|
data-slot="chart-empty"
|
|
1359
1597
|
className="absolute inset-0 flex flex-col items-center justify-center gap-1 rounded-md border border-dashed border-border p-6 text-center"
|
|
1360
1598
|
>
|
|
1361
|
-
<p className="text-sm font-medium text-balance">{title ??
|
|
1599
|
+
<p className="text-sm font-medium text-balance">{title ?? defaultTitle}</p>
|
|
1362
1600
|
{description && <p className="max-w-prose text-sm text-balance text-muted-foreground">{description}</p>}
|
|
1363
1601
|
{action && <div className="mt-3">{action}</div>}
|
|
1364
1602
|
</div>
|