@squadbase/vantage 0.6.2-dev1.9365dcd → 0.6.2-dev1.9d7130b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-3RJEOP3J.js +174 -0
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
- package/dist/chunk-CKWPK4RO.js.map +1 -0
- package/dist/chunk-EG4PX726.js +678 -0
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
- package/dist/chunk-QNOX7WLZ.js.map +1 -0
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
- package/dist/chunk-SHZM3EXB.js.map +1 -0
- package/dist/chunk-TRMHNMMZ.js +61 -0
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +57 -273
- package/dist/components/index.js +6 -3
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +422 -4
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/query/index.js +2 -2
- package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
- package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1023 -2
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +9 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -2
- package/dist/ui/index.js +6 -6
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
- package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +2 -0
- package/docs/en/components.md +13 -2
- package/docs/en/doc/delta-text.md +40 -0
- package/docs/en/doc/doc-action-items.md +49 -0
- package/docs/en/doc/doc-callout.md +38 -0
- package/docs/en/doc/doc-data-table.md +60 -0
- package/docs/en/doc/doc-executive-summary.md +64 -0
- package/docs/en/doc/doc-figure.md +56 -0
- package/docs/en/doc/doc-key-findings.md +61 -0
- package/docs/en/doc/doc-kpi-scorecard.md +80 -0
- package/docs/en/doc/doc-notes-and-sources.md +43 -0
- package/docs/en/doc/doc-report-header.md +46 -0
- package/docs/en/pages-and-metadata.md +15 -0
- package/docs/en/parts/area-chart.md +99 -0
- package/docs/en/parts/bar-chart.md +91 -0
- package/docs/en/parts/bar-list.md +50 -0
- package/docs/en/parts/category-bar.md +37 -0
- package/docs/en/parts/chart-frame.md +39 -0
- package/docs/en/parts/delta.md +4 -3
- package/docs/en/parts/donut-chart.md +72 -0
- package/docs/en/parts/funnel-steps.md +2 -1
- package/docs/en/parts/heatmap.md +53 -0
- package/docs/en/parts/kpi-card.md +2 -1
- package/docs/en/parts/legend.md +35 -0
- package/docs/en/parts/line-chart.md +94 -0
- package/docs/en/parts/progress-circle.md +33 -0
- package/docs/en/parts/sample-data.md +39 -0
- package/docs/en/parts/scatter-chart.md +56 -0
- package/docs/en/parts/tracker.md +36 -0
- package/docs/en/parts/updated-at.md +30 -0
- package/docs/en/parts/waterfall-chart.md +56 -0
- package/docs/en/slide/slide-chart-full.md +48 -0
- package/docs/en/slide/slide-chart-insight.md +52 -0
- package/docs/en/slide/slide-columns.md +46 -0
- package/docs/en/slide/slide-comparison.md +68 -0
- package/docs/en/slide/slide-cover.md +39 -0
- package/docs/en/slide/slide-exec-summary.md +47 -0
- package/docs/en/slide/slide-heading.md +38 -0
- package/docs/en/slide/slide-kpi-grid.md +55 -0
- package/docs/en/slide/slide-kpi-hero.md +58 -0
- package/docs/en/slide/slide-next-actions.md +49 -0
- package/docs/en/slide/slide-scorecard-table.md +54 -0
- package/docs/en/slide/slide-section.md +44 -0
- package/docs/en/slide/slide-source-line.md +34 -0
- package/docs/en/ui/alert-dialog.md +42 -0
- package/docs/en/ui/avatar.md +31 -0
- package/docs/en/ui/combobox.md +74 -0
- package/docs/en/ui/drawer.md +41 -0
- package/docs/en/ui/empty.md +1 -1
- package/docs/en/ui/error-state.md +4 -3
- package/docs/en/ui/field.md +42 -0
- package/docs/en/ui/hover-card.md +21 -0
- package/docs/en/ui/kbd.md +17 -0
- package/docs/en/ui/loading.md +1 -1
- package/docs/en/ui/pagination.md +36 -0
- package/docs/en/ui/radio-group.md +21 -0
- package/docs/en/ui/toast.md +44 -0
- package/docs/en/ui/toggle-group.md +4 -3
- package/docs/index.json +857 -77
- package/docs/ja/agent-skills.md +1 -1
- package/docs/ja/changelog.md +2 -0
- package/docs/ja/components.md +12 -1
- package/docs/ja/doc/delta-text.md +40 -0
- package/docs/ja/doc/doc-action-items.md +49 -0
- package/docs/ja/doc/doc-callout.md +38 -0
- package/docs/ja/doc/doc-data-table.md +60 -0
- package/docs/ja/doc/doc-executive-summary.md +64 -0
- package/docs/ja/doc/doc-figure.md +56 -0
- package/docs/ja/doc/doc-key-findings.md +61 -0
- package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
- package/docs/ja/doc/doc-notes-and-sources.md +43 -0
- package/docs/ja/doc/doc-report-header.md +46 -0
- package/docs/ja/page-types/slide.md +1 -0
- package/docs/ja/pages-and-metadata.md +15 -0
- package/docs/ja/parts/area-chart.md +49 -25
- package/docs/ja/parts/bar-chart.md +50 -24
- package/docs/ja/parts/bar-list.md +31 -8
- package/docs/ja/parts/chart-frame.md +36 -0
- package/docs/ja/parts/delta.md +5 -4
- package/docs/ja/parts/donut-chart.md +45 -26
- package/docs/ja/parts/funnel-steps.md +27 -8
- package/docs/ja/parts/heatmap.md +45 -0
- package/docs/ja/parts/kpi-card.md +2 -1
- package/docs/ja/parts/legend.md +36 -0
- package/docs/ja/parts/line-chart.md +48 -22
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -0
- package/docs/ja/parts/updated-at.md +27 -0
- package/docs/ja/parts/waterfall-chart.md +61 -0
- package/docs/ja/slide/slide-chart-full.md +48 -0
- package/docs/ja/slide/slide-chart-insight.md +52 -0
- package/docs/ja/slide/slide-columns.md +46 -0
- package/docs/ja/slide/slide-comparison.md +68 -0
- package/docs/ja/slide/slide-cover.md +39 -0
- package/docs/ja/slide/slide-exec-summary.md +47 -0
- package/docs/ja/slide/slide-heading.md +38 -0
- package/docs/ja/slide/slide-kpi-grid.md +55 -0
- package/docs/ja/slide/slide-kpi-hero.md +58 -0
- package/docs/ja/slide/slide-next-actions.md +49 -0
- package/docs/ja/slide/slide-scorecard-table.md +54 -0
- package/docs/ja/slide/slide-section.md +44 -0
- package/docs/ja/slide/slide-source-line.md +34 -0
- package/docs/ja/ui/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- package/docs/ja/ui/loading.md +1 -1
- package/docs/ja/ui/toggle-group.md +4 -3
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +389 -104
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +386 -104
- package/registry/ui/bar-list.tsx +376 -94
- package/registry/ui/box-plot-chart.tsx +450 -116
- package/registry/ui/category-bar.tsx +292 -57
- package/registry/ui/chart-frame.tsx +301 -63
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +181 -19
- package/registry/ui/donut-chart.tsx +385 -103
- package/registry/ui/echart.tsx +313 -58
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +338 -71
- package/registry/ui/histogram-chart.tsx +440 -116
- package/registry/ui/kpi-card.tsx +254 -48
- package/registry/ui/legend.tsx +374 -93
- package/registry/ui/line-chart.tsx +389 -104
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +148 -10
- package/registry/ui/paired-chart.tsx +394 -109
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +154 -10
- package/registry/ui/scatter-chart.tsx +436 -119
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +201 -36
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +214 -42
- package/registry/ui/trend-indicator.tsx +151 -8
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +460 -121
- package/skills/vantage-add-feature/SKILL.md +5 -0
- package/skills/vantage-design/SKILL.md +54 -54
- package/skills/vantage-design/references/extract.md +26 -27
- package/skills/vantage-design/references/format.md +12 -172
- package/skills/vantage-design/references/presets.md +50 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +35 -18
- package/templates/agents/api.md +7 -0
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +59 -2
- package/templates/agents/slide.md +77 -3
- package/theme.css +23 -19
- package/dist/chunk-2ICNHUG6.js.map +0 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-WAMZWZJB.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
|
@@ -13,11 +13,143 @@ import { cn } from "@squadbase/vantage/ui"
|
|
|
13
13
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
|
|
14
14
|
import type { ValueFormat } from "@squadbase/vantage/components"
|
|
15
15
|
import type { ChartColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
16
|
-
import {
|
|
16
|
+
import { useSyncExternalStore } from "react"
|
|
17
|
+
import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
|
|
17
18
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
18
|
-
import {
|
|
19
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
|
|
19
20
|
import type { ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
20
21
|
|
|
22
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The display language of the parts and the defaults that follow it.
|
|
26
|
+
*
|
|
27
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
28
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
29
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
30
|
+
*
|
|
31
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
32
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
36
|
+
type UiLanguage = "ja" | "en"
|
|
37
|
+
|
|
38
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
39
|
+
const FALLBACK_LOCALE = "en-US"
|
|
40
|
+
|
|
41
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
42
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
43
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
47
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
48
|
+
const value = tag?.trim()
|
|
49
|
+
if (!value) return FALLBACK_LOCALE
|
|
50
|
+
try {
|
|
51
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
52
|
+
} catch {
|
|
53
|
+
return FALLBACK_LOCALE
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
59
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
60
|
+
*/
|
|
61
|
+
function documentLocale(): string {
|
|
62
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
63
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
67
|
+
function resolveLocale(locale?: string | null): string {
|
|
68
|
+
return locale || documentLocale()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Picks the wording table of the locale's language. */
|
|
72
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
73
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
77
|
+
function defaultCurrency(locale?: string | null): string {
|
|
78
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
82
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
83
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
87
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
88
|
+
|
|
89
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
90
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
94
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
95
|
+
* Monday for the rest.
|
|
96
|
+
*/
|
|
97
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
98
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
99
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
100
|
+
try {
|
|
101
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
102
|
+
getWeekInfo?: () => { firstDay: number }
|
|
103
|
+
weekInfo?: { firstDay: number }
|
|
104
|
+
}
|
|
105
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
106
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
107
|
+
} catch {
|
|
108
|
+
// Fall through to the table below.
|
|
109
|
+
}
|
|
110
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
114
|
+
|
|
115
|
+
const listeners = new Set<() => void>()
|
|
116
|
+
let observer: MutationObserver | null = null
|
|
117
|
+
|
|
118
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
119
|
+
function subscribe(onChange: () => void): () => void {
|
|
120
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
121
|
+
listeners.add(onChange)
|
|
122
|
+
if (!observer) {
|
|
123
|
+
observer = new MutationObserver(() => {
|
|
124
|
+
for (const listener of [...listeners]) listener()
|
|
125
|
+
})
|
|
126
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
127
|
+
}
|
|
128
|
+
return () => {
|
|
129
|
+
listeners.delete(onChange)
|
|
130
|
+
if (listeners.size === 0 && observer) {
|
|
131
|
+
observer.disconnect()
|
|
132
|
+
observer = null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
141
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
142
|
+
*/
|
|
143
|
+
function useDocumentLocale(): string {
|
|
144
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
148
|
+
function useLocale(locale?: string | null): string {
|
|
149
|
+
const page = useDocumentLocale()
|
|
150
|
+
return locale || page
|
|
151
|
+
}
|
|
152
|
+
|
|
21
153
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
22
154
|
|
|
23
155
|
/**
|
|
@@ -52,10 +184,23 @@ interface ChartTokens {
|
|
|
52
184
|
border: string | null
|
|
53
185
|
/** Tooltip surface. */
|
|
54
186
|
popover: string | null
|
|
187
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
188
|
+
colors?: Record<string, string>
|
|
189
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
190
|
+
surface?: string | null
|
|
55
191
|
/** Tooltip text. */
|
|
56
192
|
popoverForeground: string | null
|
|
57
193
|
}
|
|
58
194
|
|
|
195
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
196
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
197
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
198
|
+
const OPTION_COLOR_TOKENS = [
|
|
199
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
200
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
201
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
202
|
+
]
|
|
203
|
+
|
|
59
204
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
60
205
|
|
|
61
206
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -111,8 +256,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
111
256
|
const color = read(token)
|
|
112
257
|
if (color) series.push(color)
|
|
113
258
|
}
|
|
114
|
-
|
|
259
|
+
const tokens: ChartTokens = {
|
|
115
260
|
series,
|
|
261
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
262
|
+
surface: readChartSurface(target),
|
|
116
263
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
117
264
|
foreground: read("--foreground"),
|
|
118
265
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -120,6 +267,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
120
267
|
popover: read("--popover"),
|
|
121
268
|
popoverForeground: read("--popover-foreground"),
|
|
122
269
|
}
|
|
270
|
+
tokenScopes.set(tokens, target)
|
|
271
|
+
return tokens
|
|
123
272
|
}
|
|
124
273
|
|
|
125
274
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -127,6 +276,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
127
276
|
if (a === b) return true
|
|
128
277
|
if (!a || !b) return false
|
|
129
278
|
return (
|
|
279
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
280
|
+
a.surface === b.surface &&
|
|
281
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
282
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
130
283
|
a.series.length === b.series.length &&
|
|
131
284
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
132
285
|
a.fontFamily === b.fontFamily &&
|
|
@@ -152,7 +305,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
152
305
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
153
306
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
154
307
|
*/
|
|
155
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
308
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
156
309
|
if (typeof document === "undefined") return () => {}
|
|
157
310
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
158
311
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -167,8 +320,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
167
320
|
|
|
168
321
|
const themeObserver = new MutationObserver(schedule)
|
|
169
322
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
170
|
-
|
|
171
|
-
|
|
323
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
324
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
325
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
326
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
327
|
+
}
|
|
172
328
|
|
|
173
329
|
const styleObserver = new MutationObserver(schedule)
|
|
174
330
|
if (document.head) {
|
|
@@ -256,22 +412,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
256
412
|
* components that build an ECharts option from token colours themselves (the
|
|
257
413
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
258
414
|
*/
|
|
259
|
-
function useChartTokens(): ChartTokens | null {
|
|
415
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
260
416
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
261
417
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
262
418
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
263
419
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
264
420
|
const tokensRef = useRef(tokens)
|
|
265
421
|
useEffect(() => {
|
|
422
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
266
423
|
const sync = () => {
|
|
267
|
-
const next = readChartTokens()
|
|
424
|
+
const next = readChartTokens(target)
|
|
268
425
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
269
426
|
tokensRef.current = next
|
|
270
427
|
setTokens(next)
|
|
271
428
|
}
|
|
272
429
|
sync()
|
|
273
|
-
return watchChartTokens(sync)
|
|
274
|
-
}, [])
|
|
430
|
+
return watchChartTokens(sync, target)
|
|
431
|
+
}, [scope])
|
|
275
432
|
return tokens
|
|
276
433
|
}
|
|
277
434
|
|
|
@@ -283,7 +440,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
283
440
|
/**
|
|
284
441
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
285
442
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
286
|
-
* variants `"positive-fill"`, …) are read
|
|
443
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
287
444
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
288
445
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
289
446
|
*/
|
|
@@ -294,12 +451,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
294
451
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
295
452
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
296
453
|
if (typeof document === "undefined") return ""
|
|
297
|
-
|
|
454
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
455
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
298
456
|
}
|
|
299
457
|
if (typeof document === "undefined") return color
|
|
300
458
|
let value = color.trim()
|
|
301
459
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
302
|
-
const styles = getComputedStyle(document.documentElement)
|
|
460
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
303
461
|
if (cssVar) {
|
|
304
462
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
305
463
|
} else if (value.includes("var(")) {
|
|
@@ -345,7 +503,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
345
503
|
if (color) return color
|
|
346
504
|
}
|
|
347
505
|
}
|
|
348
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
506
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
349
507
|
}
|
|
350
508
|
|
|
351
509
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -359,9 +517,10 @@ const WIDTH_BUCKET = 40
|
|
|
359
517
|
*/
|
|
360
518
|
function useChartSurface<E extends Element>(
|
|
361
519
|
forwarded: ForwardedRef<E>,
|
|
362
|
-
tokens
|
|
363
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
520
|
+
tokens?: ChartTokens | null,
|
|
521
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
364
522
|
const [node, setNode] = useState<E | null>(null)
|
|
523
|
+
const scopedTokens = useChartTokens(node)
|
|
365
524
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
366
525
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
367
526
|
const ref = useCallback(
|
|
@@ -375,7 +534,7 @@ function useChartSurface<E extends Element>(
|
|
|
375
534
|
useEffect(() => {
|
|
376
535
|
if (!node) return
|
|
377
536
|
setSurface(readChartSurface(node) ?? undefined)
|
|
378
|
-
}, [node, tokens])
|
|
537
|
+
}, [node, tokens, scopedTokens])
|
|
379
538
|
useEffect(() => {
|
|
380
539
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
381
540
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -384,7 +543,7 @@ function useChartSurface<E extends Element>(
|
|
|
384
543
|
observer.observe(node)
|
|
385
544
|
return () => observer.disconnect()
|
|
386
545
|
}, [node])
|
|
387
|
-
return { ref, surface, width }
|
|
546
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
388
547
|
}
|
|
389
548
|
|
|
390
549
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -494,9 +653,10 @@ function resolveValueFormatter(
|
|
|
494
653
|
function resolveAxisFormatter(
|
|
495
654
|
valueFormatter: ValueFormatter | undefined,
|
|
496
655
|
format: ValueFormat | undefined,
|
|
497
|
-
|
|
656
|
+
localeArg?: string,
|
|
498
657
|
): ValueFormatter {
|
|
499
658
|
if (valueFormatter) return valueFormatter
|
|
659
|
+
const locale = resolveLocale(localeArg)
|
|
500
660
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
501
661
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
502
662
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -505,40 +665,75 @@ function resolveAxisFormatter(
|
|
|
505
665
|
return (value) => formatValue(value, short, locale)
|
|
506
666
|
}
|
|
507
667
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
]
|
|
668
|
+
interface CompactUnit {
|
|
669
|
+
token: string
|
|
670
|
+
size: number
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
677
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
678
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
679
|
+
*/
|
|
680
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
681
|
+
const cached = compactUnitCache.get(locale)
|
|
682
|
+
if (cached !== undefined) return cached
|
|
683
|
+
let units: CompactUnit[] | null = []
|
|
684
|
+
try {
|
|
685
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
686
|
+
const seen = new Set<string>()
|
|
687
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
688
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
689
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
690
|
+
if (at === -1) continue
|
|
691
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
692
|
+
units = null
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
const token = parts[at]!.value
|
|
696
|
+
if (seen.has(token)) continue
|
|
697
|
+
seen.add(token)
|
|
698
|
+
units.push({ token, size: 10 ** exp })
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
units = null
|
|
702
|
+
}
|
|
703
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
704
|
+
compactUnitCache.set(locale, sorted)
|
|
705
|
+
return sorted
|
|
706
|
+
}
|
|
514
707
|
|
|
515
|
-
const
|
|
708
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
516
709
|
|
|
517
710
|
/**
|
|
518
711
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
519
|
-
* that largest tick, so
|
|
520
|
-
* Only the
|
|
521
|
-
* any other format —
|
|
712
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
713
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
714
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
715
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
522
716
|
*/
|
|
523
717
|
function resolveAxisTickFormatter(
|
|
524
718
|
valueFormatter: ValueFormatter | undefined,
|
|
525
719
|
format: ValueFormat | undefined,
|
|
526
|
-
|
|
720
|
+
localeArg?: string,
|
|
527
721
|
): (top: number) => ValueFormatter {
|
|
722
|
+
const locale = resolveLocale(localeArg)
|
|
528
723
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
529
724
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
if (!aligns) return () => base
|
|
725
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
726
|
+
if (!units) return () => base
|
|
533
727
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
728
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
534
729
|
return (top) => {
|
|
535
|
-
const unit =
|
|
730
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
536
731
|
if (!unit) return base
|
|
537
732
|
return (value) => {
|
|
538
733
|
const text = base(value)
|
|
539
734
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
540
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
541
|
-
return text.replace(
|
|
735
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
736
|
+
return text.replace(numberToken, () => scaled)
|
|
542
737
|
}
|
|
543
738
|
}
|
|
544
739
|
}
|
|
@@ -567,8 +762,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
567
762
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
568
763
|
const MAX_CHART_SERIES = 5
|
|
569
764
|
|
|
570
|
-
/** The series name the folded categories get. */
|
|
571
|
-
const
|
|
765
|
+
/** The series name the folded categories get, by language. */
|
|
766
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
767
|
+
|
|
768
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
769
|
+
function otherSeriesLabel(locale?: string): string {
|
|
770
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
774
|
+
|
|
775
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
776
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
777
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
778
|
+
}
|
|
572
779
|
|
|
573
780
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
574
781
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -588,7 +795,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
588
795
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
589
796
|
items: readonly Item[],
|
|
590
797
|
n: number = MAX_CHART_SERIES,
|
|
591
|
-
otherLabel: string =
|
|
798
|
+
otherLabel: string = otherSeriesLabel(),
|
|
592
799
|
): AggregatedItems<Item> {
|
|
593
800
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
594
801
|
const ranked = items
|
|
@@ -617,7 +824,7 @@ function aggregateSeries(
|
|
|
617
824
|
data: readonly Record<string, unknown>[],
|
|
618
825
|
categories: readonly string[],
|
|
619
826
|
n: number = MAX_CHART_SERIES,
|
|
620
|
-
otherLabel: string =
|
|
827
|
+
otherLabel: string = otherSeriesLabel(),
|
|
621
828
|
): AggregatedSeries {
|
|
622
829
|
const totals = categories.map((name) => ({
|
|
623
830
|
name,
|
|
@@ -649,12 +856,14 @@ function aggregateSeriesKeeping(
|
|
|
649
856
|
categories: readonly string[],
|
|
650
857
|
keep: string | undefined,
|
|
651
858
|
n: number = MAX_CHART_SERIES,
|
|
859
|
+
otherLabel: string = otherSeriesLabel(),
|
|
652
860
|
): AggregatedSeries {
|
|
653
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
861
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
654
862
|
const aggregated = aggregateSeries(
|
|
655
863
|
data,
|
|
656
864
|
categories.filter((c) => c !== keep),
|
|
657
865
|
n,
|
|
866
|
+
otherLabel,
|
|
658
867
|
)
|
|
659
868
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
660
869
|
}
|
|
@@ -690,7 +899,7 @@ function seriesColorsWithComparison(
|
|
|
690
899
|
* Colour per drawn series: the record's token for that category, else the
|
|
691
900
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
692
901
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
693
|
-
* data itself names
|
|
902
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
694
903
|
*/
|
|
695
904
|
function seriesColorsFor(
|
|
696
905
|
categories: readonly string[],
|
|
@@ -702,7 +911,7 @@ function seriesColorsFor(
|
|
|
702
911
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
703
912
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
704
913
|
return categories.map((category, i) => {
|
|
705
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
914
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
706
915
|
const fromRecord = record?.[category]
|
|
707
916
|
if (fromRecord) return fromRecord
|
|
708
917
|
if (list && list.length > 0) {
|
|
@@ -749,7 +958,7 @@ interface DonutSlicesConfig {
|
|
|
749
958
|
*/
|
|
750
959
|
function donutSlices(
|
|
751
960
|
data: readonly Record<string, unknown>[],
|
|
752
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
961
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
753
962
|
): DonutSlice[] {
|
|
754
963
|
const items = data
|
|
755
964
|
.map((row, i) => {
|
|
@@ -762,7 +971,8 @@ function donutSlices(
|
|
|
762
971
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
763
972
|
name: item.name,
|
|
764
973
|
value: item.value,
|
|
765
|
-
|
|
974
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
975
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
766
976
|
isOther: false,
|
|
767
977
|
row: item.row,
|
|
768
978
|
}))
|
|
@@ -835,7 +1045,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
835
1045
|
*/
|
|
836
1046
|
function barListRows<Row extends object>(
|
|
837
1047
|
data: readonly Row[],
|
|
838
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1048
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
839
1049
|
): BarListRow<Row>[] {
|
|
840
1050
|
const items = data.map((row): BarListRow<Row> => {
|
|
841
1051
|
const cells = row as Record<string, unknown>
|
|
@@ -881,19 +1091,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
881
1091
|
|
|
882
1092
|
/**
|
|
883
1093
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
884
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1094
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
885
1095
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
886
1096
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
887
1097
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
888
1098
|
* keeps its contrast.
|
|
889
1099
|
*/
|
|
890
1100
|
function barListFills(
|
|
891
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1101
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
892
1102
|
color: ChartColor,
|
|
893
1103
|
otherColor: string,
|
|
894
1104
|
): string[] {
|
|
895
1105
|
return rows.map((row) =>
|
|
896
|
-
row.isOther
|
|
1106
|
+
row.isOther || isOtherName(row.name)
|
|
1107
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1108
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
897
1109
|
)
|
|
898
1110
|
}
|
|
899
1111
|
|
|
@@ -1109,11 +1321,30 @@ interface ChartSummaryInput {
|
|
|
1109
1321
|
format?: ValueFormat
|
|
1110
1322
|
/** Wins over `format`. */
|
|
1111
1323
|
valueFormatter?: ValueFormatter
|
|
1324
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1325
|
+
locale?: string
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const SUMMARY_TEXT = {
|
|
1329
|
+
ja: {
|
|
1330
|
+
up: "、前の点より増加",
|
|
1331
|
+
down: "、前の点より減少",
|
|
1332
|
+
flat: "、前の点と横ばい",
|
|
1333
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1334
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1335
|
+
},
|
|
1336
|
+
en: {
|
|
1337
|
+
up: ", up from the previous point",
|
|
1338
|
+
down: ", down from the previous point",
|
|
1339
|
+
flat: ", flat from the previous point",
|
|
1340
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1341
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1342
|
+
},
|
|
1112
1343
|
}
|
|
1113
1344
|
|
|
1114
1345
|
/**
|
|
1115
1346
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1116
|
-
* 「<title>: 最新 <index> は
|
|
1347
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1117
1348
|
* computed from the last two non-empty points of the first series — never
|
|
1118
1349
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1119
1350
|
* no usable point.
|
|
@@ -1129,26 +1360,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1129
1360
|
}
|
|
1130
1361
|
const latest = points[points.length - 1]
|
|
1131
1362
|
if (!latest) return undefined
|
|
1132
|
-
const
|
|
1363
|
+
const locale = resolveLocale(input.locale)
|
|
1364
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1365
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1133
1366
|
const prev = points[points.length - 2]
|
|
1134
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1367
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1135
1368
|
const head = input.title ? `${input.title}: ` : ""
|
|
1136
|
-
return `${head}
|
|
1369
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1137
1370
|
}
|
|
1138
1371
|
|
|
1139
1372
|
/**
|
|
1140
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1141
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1373
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1374
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1142
1375
|
*/
|
|
1143
1376
|
function buildShareSummary(
|
|
1144
1377
|
slices: readonly { name: string; value: number }[],
|
|
1145
1378
|
title?: string,
|
|
1379
|
+
locale?: string,
|
|
1146
1380
|
): string | undefined {
|
|
1147
1381
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1148
1382
|
const top = slices[0]
|
|
1149
1383
|
if (!top || total <= 0) return undefined
|
|
1150
1384
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1151
|
-
return `${title ? `${title}: ` : ""}
|
|
1385
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1152
1386
|
}
|
|
1153
1387
|
|
|
1154
1388
|
/* ---------- end of inlined code ---------- */
|
|
@@ -1188,7 +1422,8 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
|
|
|
1188
1422
|
) {
|
|
1189
1423
|
// The bar's width thins the running totals that would print over each other.
|
|
1190
1424
|
const { ref, width } = useChartSurface(forwardedRef, null)
|
|
1191
|
-
const
|
|
1425
|
+
const locale = useLocale()
|
|
1426
|
+
const valueFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
|
|
1192
1427
|
const segments = categoryBarSegments(values)
|
|
1193
1428
|
const markerPosition = marker ? categoryBarMarkerPosition(marker.value, values) : 0
|
|
1194
1429
|
const allLabels = showLabels ? categoryBarLabels(values, undefined, marker ? markerPosition : undefined) : []
|