@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
package/registry/ui/heatmap.tsx
CHANGED
|
@@ -27,11 +27,143 @@ import { ChartFrame } from "@squadbase/vantage/components"
|
|
|
27
27
|
import { EChart } from "@squadbase/vantage/components"
|
|
28
28
|
import { EMPTY_VALUE, type ValueFormat } from "@squadbase/vantage/components"
|
|
29
29
|
import type { ChartActionsOptions, ChartEmptyState, ChartRootAttributes, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
30
|
-
import {
|
|
30
|
+
import { useSyncExternalStore } from "react"
|
|
31
|
+
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
31
32
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
32
|
-
import {
|
|
33
|
+
import { formatValue, toValueFormatter } from "@squadbase/vantage/components"
|
|
33
34
|
import type { ChartColor, ChartColors } from "@squadbase/vantage/components"
|
|
34
35
|
|
|
36
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The display language of the parts and the defaults that follow it.
|
|
40
|
+
*
|
|
41
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
42
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
43
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
44
|
+
*
|
|
45
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
46
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
50
|
+
type UiLanguage = "ja" | "en"
|
|
51
|
+
|
|
52
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
53
|
+
const FALLBACK_LOCALE = "en-US"
|
|
54
|
+
|
|
55
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
56
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
57
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
61
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
62
|
+
const value = tag?.trim()
|
|
63
|
+
if (!value) return FALLBACK_LOCALE
|
|
64
|
+
try {
|
|
65
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
66
|
+
} catch {
|
|
67
|
+
return FALLBACK_LOCALE
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
73
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
74
|
+
*/
|
|
75
|
+
function documentLocale(): string {
|
|
76
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
77
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
81
|
+
function resolveLocale(locale?: string | null): string {
|
|
82
|
+
return locale || documentLocale()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Picks the wording table of the locale's language. */
|
|
86
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
87
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
91
|
+
function defaultCurrency(locale?: string | null): string {
|
|
92
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
96
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
97
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
101
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
102
|
+
|
|
103
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
104
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
108
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
109
|
+
* Monday for the rest.
|
|
110
|
+
*/
|
|
111
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
112
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
113
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
114
|
+
try {
|
|
115
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
116
|
+
getWeekInfo?: () => { firstDay: number }
|
|
117
|
+
weekInfo?: { firstDay: number }
|
|
118
|
+
}
|
|
119
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
120
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
121
|
+
} catch {
|
|
122
|
+
// Fall through to the table below.
|
|
123
|
+
}
|
|
124
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
128
|
+
|
|
129
|
+
const listeners = new Set<() => void>()
|
|
130
|
+
let observer: MutationObserver | null = null
|
|
131
|
+
|
|
132
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
133
|
+
function subscribe(onChange: () => void): () => void {
|
|
134
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
135
|
+
listeners.add(onChange)
|
|
136
|
+
if (!observer) {
|
|
137
|
+
observer = new MutationObserver(() => {
|
|
138
|
+
for (const listener of [...listeners]) listener()
|
|
139
|
+
})
|
|
140
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
141
|
+
}
|
|
142
|
+
return () => {
|
|
143
|
+
listeners.delete(onChange)
|
|
144
|
+
if (listeners.size === 0 && observer) {
|
|
145
|
+
observer.disconnect()
|
|
146
|
+
observer = null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
155
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
156
|
+
*/
|
|
157
|
+
function useDocumentLocale(): string {
|
|
158
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
162
|
+
function useLocale(locale?: string | null): string {
|
|
163
|
+
const page = useDocumentLocale()
|
|
164
|
+
return locale || page
|
|
165
|
+
}
|
|
166
|
+
|
|
35
167
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
36
168
|
|
|
37
169
|
/**
|
|
@@ -66,10 +198,23 @@ interface ChartTokens {
|
|
|
66
198
|
border: string | null
|
|
67
199
|
/** Tooltip surface. */
|
|
68
200
|
popover: string | null
|
|
201
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
202
|
+
colors?: Record<string, string>
|
|
203
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
204
|
+
surface?: string | null
|
|
69
205
|
/** Tooltip text. */
|
|
70
206
|
popoverForeground: string | null
|
|
71
207
|
}
|
|
72
208
|
|
|
209
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
210
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
211
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
212
|
+
const OPTION_COLOR_TOKENS = [
|
|
213
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
214
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
215
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
216
|
+
]
|
|
217
|
+
|
|
73
218
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
74
219
|
|
|
75
220
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -125,8 +270,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
125
270
|
const color = read(token)
|
|
126
271
|
if (color) series.push(color)
|
|
127
272
|
}
|
|
128
|
-
|
|
273
|
+
const tokens: ChartTokens = {
|
|
129
274
|
series,
|
|
275
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
276
|
+
surface: readChartSurface(target),
|
|
130
277
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
131
278
|
foreground: read("--foreground"),
|
|
132
279
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -134,6 +281,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
134
281
|
popover: read("--popover"),
|
|
135
282
|
popoverForeground: read("--popover-foreground"),
|
|
136
283
|
}
|
|
284
|
+
tokenScopes.set(tokens, target)
|
|
285
|
+
return tokens
|
|
137
286
|
}
|
|
138
287
|
|
|
139
288
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -141,6 +290,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
141
290
|
if (a === b) return true
|
|
142
291
|
if (!a || !b) return false
|
|
143
292
|
return (
|
|
293
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
294
|
+
a.surface === b.surface &&
|
|
295
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
296
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
144
297
|
a.series.length === b.series.length &&
|
|
145
298
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
146
299
|
a.fontFamily === b.fontFamily &&
|
|
@@ -166,7 +319,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
166
319
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
167
320
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
168
321
|
*/
|
|
169
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
322
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
170
323
|
if (typeof document === "undefined") return () => {}
|
|
171
324
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
172
325
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -181,8 +334,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
181
334
|
|
|
182
335
|
const themeObserver = new MutationObserver(schedule)
|
|
183
336
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
184
|
-
|
|
185
|
-
|
|
337
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
338
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
339
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
340
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
341
|
+
}
|
|
186
342
|
|
|
187
343
|
const styleObserver = new MutationObserver(schedule)
|
|
188
344
|
if (document.head) {
|
|
@@ -270,22 +426,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
270
426
|
* components that build an ECharts option from token colours themselves (the
|
|
271
427
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
272
428
|
*/
|
|
273
|
-
function useChartTokens(): ChartTokens | null {
|
|
429
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
274
430
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
275
431
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
276
432
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
277
433
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
278
434
|
const tokensRef = useRef(tokens)
|
|
279
435
|
useEffect(() => {
|
|
436
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
280
437
|
const sync = () => {
|
|
281
|
-
const next = readChartTokens()
|
|
438
|
+
const next = readChartTokens(target)
|
|
282
439
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
283
440
|
tokensRef.current = next
|
|
284
441
|
setTokens(next)
|
|
285
442
|
}
|
|
286
443
|
sync()
|
|
287
|
-
return watchChartTokens(sync)
|
|
288
|
-
}, [])
|
|
444
|
+
return watchChartTokens(sync, target)
|
|
445
|
+
}, [scope])
|
|
289
446
|
return tokens
|
|
290
447
|
}
|
|
291
448
|
|
|
@@ -297,7 +454,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
297
454
|
/**
|
|
298
455
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
299
456
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
300
|
-
* variants `"positive-fill"`, …) are read
|
|
457
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
301
458
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
302
459
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
303
460
|
*/
|
|
@@ -308,12 +465,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
308
465
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
309
466
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
310
467
|
if (typeof document === "undefined") return ""
|
|
311
|
-
|
|
468
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
469
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
312
470
|
}
|
|
313
471
|
if (typeof document === "undefined") return color
|
|
314
472
|
let value = color.trim()
|
|
315
473
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
316
|
-
const styles = getComputedStyle(document.documentElement)
|
|
474
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
317
475
|
if (cssVar) {
|
|
318
476
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
319
477
|
} else if (value.includes("var(")) {
|
|
@@ -359,7 +517,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
359
517
|
if (color) return color
|
|
360
518
|
}
|
|
361
519
|
}
|
|
362
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
520
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
363
521
|
}
|
|
364
522
|
|
|
365
523
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -373,9 +531,10 @@ const WIDTH_BUCKET = 40
|
|
|
373
531
|
*/
|
|
374
532
|
function useChartSurface<E extends Element>(
|
|
375
533
|
forwarded: ForwardedRef<E>,
|
|
376
|
-
tokens
|
|
377
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
534
|
+
tokens?: ChartTokens | null,
|
|
535
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
378
536
|
const [node, setNode] = useState<E | null>(null)
|
|
537
|
+
const scopedTokens = useChartTokens(node)
|
|
379
538
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
380
539
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
381
540
|
const ref = useCallback(
|
|
@@ -389,7 +548,7 @@ function useChartSurface<E extends Element>(
|
|
|
389
548
|
useEffect(() => {
|
|
390
549
|
if (!node) return
|
|
391
550
|
setSurface(readChartSurface(node) ?? undefined)
|
|
392
|
-
}, [node, tokens])
|
|
551
|
+
}, [node, tokens, scopedTokens])
|
|
393
552
|
useEffect(() => {
|
|
394
553
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
395
554
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -398,7 +557,7 @@ function useChartSurface<E extends Element>(
|
|
|
398
557
|
observer.observe(node)
|
|
399
558
|
return () => observer.disconnect()
|
|
400
559
|
}, [node])
|
|
401
|
-
return { ref, surface, width }
|
|
560
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
402
561
|
}
|
|
403
562
|
|
|
404
563
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -508,9 +667,10 @@ function resolveValueFormatter(
|
|
|
508
667
|
function resolveAxisFormatter(
|
|
509
668
|
valueFormatter: ValueFormatter | undefined,
|
|
510
669
|
format: ValueFormat | undefined,
|
|
511
|
-
|
|
670
|
+
localeArg?: string,
|
|
512
671
|
): ValueFormatter {
|
|
513
672
|
if (valueFormatter) return valueFormatter
|
|
673
|
+
const locale = resolveLocale(localeArg)
|
|
514
674
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
515
675
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
516
676
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -519,40 +679,75 @@ function resolveAxisFormatter(
|
|
|
519
679
|
return (value) => formatValue(value, short, locale)
|
|
520
680
|
}
|
|
521
681
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
{ char: "万", size: 1e4 },
|
|
527
|
-
] as const
|
|
682
|
+
interface CompactUnit {
|
|
683
|
+
token: string
|
|
684
|
+
size: number
|
|
685
|
+
}
|
|
528
686
|
|
|
529
|
-
const
|
|
687
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
691
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
692
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
693
|
+
*/
|
|
694
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
695
|
+
const cached = compactUnitCache.get(locale)
|
|
696
|
+
if (cached !== undefined) return cached
|
|
697
|
+
let units: CompactUnit[] | null = []
|
|
698
|
+
try {
|
|
699
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
700
|
+
const seen = new Set<string>()
|
|
701
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
702
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
703
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
704
|
+
if (at === -1) continue
|
|
705
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
706
|
+
units = null
|
|
707
|
+
break
|
|
708
|
+
}
|
|
709
|
+
const token = parts[at]!.value
|
|
710
|
+
if (seen.has(token)) continue
|
|
711
|
+
seen.add(token)
|
|
712
|
+
units.push({ token, size: 10 ** exp })
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
units = null
|
|
716
|
+
}
|
|
717
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
718
|
+
compactUnitCache.set(locale, sorted)
|
|
719
|
+
return sorted
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
530
723
|
|
|
531
724
|
/**
|
|
532
725
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
533
|
-
* that largest tick, so
|
|
534
|
-
* Only the
|
|
535
|
-
* any other format —
|
|
726
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
727
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
728
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
729
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
536
730
|
*/
|
|
537
731
|
function resolveAxisTickFormatter(
|
|
538
732
|
valueFormatter: ValueFormatter | undefined,
|
|
539
733
|
format: ValueFormat | undefined,
|
|
540
|
-
|
|
734
|
+
localeArg?: string,
|
|
541
735
|
): (top: number) => ValueFormatter {
|
|
736
|
+
const locale = resolveLocale(localeArg)
|
|
542
737
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
543
738
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
if (!aligns) return () => base
|
|
739
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
740
|
+
if (!units) return () => base
|
|
547
741
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
742
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
548
743
|
return (top) => {
|
|
549
|
-
const unit =
|
|
744
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
550
745
|
if (!unit) return base
|
|
551
746
|
return (value) => {
|
|
552
747
|
const text = base(value)
|
|
553
748
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
554
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
555
|
-
return text.replace(
|
|
749
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
750
|
+
return text.replace(numberToken, () => scaled)
|
|
556
751
|
}
|
|
557
752
|
}
|
|
558
753
|
}
|
|
@@ -581,8 +776,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
581
776
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
582
777
|
const MAX_CHART_SERIES = 5
|
|
583
778
|
|
|
584
|
-
/** The series name the folded categories get. */
|
|
585
|
-
const
|
|
779
|
+
/** The series name the folded categories get, by language. */
|
|
780
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
781
|
+
|
|
782
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
783
|
+
function otherSeriesLabel(locale?: string): string {
|
|
784
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
788
|
+
|
|
789
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
790
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
791
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
792
|
+
}
|
|
586
793
|
|
|
587
794
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
588
795
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -602,7 +809,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
602
809
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
603
810
|
items: readonly Item[],
|
|
604
811
|
n: number = MAX_CHART_SERIES,
|
|
605
|
-
otherLabel: string =
|
|
812
|
+
otherLabel: string = otherSeriesLabel(),
|
|
606
813
|
): AggregatedItems<Item> {
|
|
607
814
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
608
815
|
const ranked = items
|
|
@@ -631,7 +838,7 @@ function aggregateSeries(
|
|
|
631
838
|
data: readonly Record<string, unknown>[],
|
|
632
839
|
categories: readonly string[],
|
|
633
840
|
n: number = MAX_CHART_SERIES,
|
|
634
|
-
otherLabel: string =
|
|
841
|
+
otherLabel: string = otherSeriesLabel(),
|
|
635
842
|
): AggregatedSeries {
|
|
636
843
|
const totals = categories.map((name) => ({
|
|
637
844
|
name,
|
|
@@ -663,12 +870,14 @@ function aggregateSeriesKeeping(
|
|
|
663
870
|
categories: readonly string[],
|
|
664
871
|
keep: string | undefined,
|
|
665
872
|
n: number = MAX_CHART_SERIES,
|
|
873
|
+
otherLabel: string = otherSeriesLabel(),
|
|
666
874
|
): AggregatedSeries {
|
|
667
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
875
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
668
876
|
const aggregated = aggregateSeries(
|
|
669
877
|
data,
|
|
670
878
|
categories.filter((c) => c !== keep),
|
|
671
879
|
n,
|
|
880
|
+
otherLabel,
|
|
672
881
|
)
|
|
673
882
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
674
883
|
}
|
|
@@ -704,7 +913,7 @@ function seriesColorsWithComparison(
|
|
|
704
913
|
* Colour per drawn series: the record's token for that category, else the
|
|
705
914
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
706
915
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
707
|
-
* data itself names
|
|
916
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
708
917
|
*/
|
|
709
918
|
function seriesColorsFor(
|
|
710
919
|
categories: readonly string[],
|
|
@@ -716,7 +925,7 @@ function seriesColorsFor(
|
|
|
716
925
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
717
926
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
718
927
|
return categories.map((category, i) => {
|
|
719
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
928
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
720
929
|
const fromRecord = record?.[category]
|
|
721
930
|
if (fromRecord) return fromRecord
|
|
722
931
|
if (list && list.length > 0) {
|
|
@@ -763,7 +972,7 @@ interface DonutSlicesConfig {
|
|
|
763
972
|
*/
|
|
764
973
|
function donutSlices(
|
|
765
974
|
data: readonly Record<string, unknown>[],
|
|
766
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
975
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
767
976
|
): DonutSlice[] {
|
|
768
977
|
const items = data
|
|
769
978
|
.map((row, i) => {
|
|
@@ -776,7 +985,8 @@ function donutSlices(
|
|
|
776
985
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
777
986
|
name: item.name,
|
|
778
987
|
value: item.value,
|
|
779
|
-
|
|
988
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
989
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
780
990
|
isOther: false,
|
|
781
991
|
row: item.row,
|
|
782
992
|
}))
|
|
@@ -849,7 +1059,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
849
1059
|
*/
|
|
850
1060
|
function barListRows<Row extends object>(
|
|
851
1061
|
data: readonly Row[],
|
|
852
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1062
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
853
1063
|
): BarListRow<Row>[] {
|
|
854
1064
|
const items = data.map((row): BarListRow<Row> => {
|
|
855
1065
|
const cells = row as Record<string, unknown>
|
|
@@ -895,19 +1105,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
895
1105
|
|
|
896
1106
|
/**
|
|
897
1107
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
898
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1108
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
899
1109
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
900
1110
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
901
1111
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
902
1112
|
* keeps its contrast.
|
|
903
1113
|
*/
|
|
904
1114
|
function barListFills(
|
|
905
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1115
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
906
1116
|
color: ChartColor,
|
|
907
1117
|
otherColor: string,
|
|
908
1118
|
): string[] {
|
|
909
1119
|
return rows.map((row) =>
|
|
910
|
-
row.isOther
|
|
1120
|
+
row.isOther || isOtherName(row.name)
|
|
1121
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1122
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
911
1123
|
)
|
|
912
1124
|
}
|
|
913
1125
|
|
|
@@ -1123,11 +1335,30 @@ interface ChartSummaryInput {
|
|
|
1123
1335
|
format?: ValueFormat
|
|
1124
1336
|
/** Wins over `format`. */
|
|
1125
1337
|
valueFormatter?: ValueFormatter
|
|
1338
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1339
|
+
locale?: string
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const SUMMARY_TEXT = {
|
|
1343
|
+
ja: {
|
|
1344
|
+
up: "、前の点より増加",
|
|
1345
|
+
down: "、前の点より減少",
|
|
1346
|
+
flat: "、前の点と横ばい",
|
|
1347
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1348
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1349
|
+
},
|
|
1350
|
+
en: {
|
|
1351
|
+
up: ", up from the previous point",
|
|
1352
|
+
down: ", down from the previous point",
|
|
1353
|
+
flat: ", flat from the previous point",
|
|
1354
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1355
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1356
|
+
},
|
|
1126
1357
|
}
|
|
1127
1358
|
|
|
1128
1359
|
/**
|
|
1129
1360
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1130
|
-
* 「<title>: 最新 <index> は
|
|
1361
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1131
1362
|
* computed from the last two non-empty points of the first series — never
|
|
1132
1363
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1133
1364
|
* no usable point.
|
|
@@ -1143,26 +1374,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1143
1374
|
}
|
|
1144
1375
|
const latest = points[points.length - 1]
|
|
1145
1376
|
if (!latest) return undefined
|
|
1146
|
-
const
|
|
1377
|
+
const locale = resolveLocale(input.locale)
|
|
1378
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1379
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1147
1380
|
const prev = points[points.length - 2]
|
|
1148
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1381
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1149
1382
|
const head = input.title ? `${input.title}: ` : ""
|
|
1150
|
-
return `${head}
|
|
1383
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1151
1384
|
}
|
|
1152
1385
|
|
|
1153
1386
|
/**
|
|
1154
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1155
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1387
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1388
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1156
1389
|
*/
|
|
1157
1390
|
function buildShareSummary(
|
|
1158
1391
|
slices: readonly { name: string; value: number }[],
|
|
1159
1392
|
title?: string,
|
|
1393
|
+
locale?: string,
|
|
1160
1394
|
): string | undefined {
|
|
1161
1395
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1162
1396
|
const top = slices[0]
|
|
1163
1397
|
if (!top || total <= 0) return undefined
|
|
1164
1398
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1165
|
-
return `${title ? `${title}: ` : ""}
|
|
1399
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1166
1400
|
}
|
|
1167
1401
|
|
|
1168
1402
|
/* ---------- end of inlined code ---------- */
|
|
@@ -1428,6 +1662,25 @@ export function heatmapCellFontSize(
|
|
|
1428
1662
|
return (text * 10) / 12 + HEATMAP_CELL_PADDING <= cell ? 10 : null
|
|
1429
1663
|
}
|
|
1430
1664
|
|
|
1665
|
+
const TEXT = {
|
|
1666
|
+
ja: {
|
|
1667
|
+
join: "・",
|
|
1668
|
+
flagged: "要注意",
|
|
1669
|
+
item: "項目",
|
|
1670
|
+
top: (y: string, x: string, value: string) => `最大は ${y} ${x} の ${value}`,
|
|
1671
|
+
chart: (y: string, x: string) => `${y}×${x}のヒートマップ`,
|
|
1672
|
+
},
|
|
1673
|
+
en: {
|
|
1674
|
+
join: ", ",
|
|
1675
|
+
flagged: "Needs attention",
|
|
1676
|
+
item: "Item",
|
|
1677
|
+
top: (y: string, x: string, value: string) => `Highest: ${y} ${x} (${value})`,
|
|
1678
|
+
chart: (y: string, x: string) => `Heatmap of ${y} × ${x}`,
|
|
1679
|
+
},
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const textOf = (locale?: string) => TEXT[uiLanguage(resolveLocale(locale))]
|
|
1683
|
+
|
|
1431
1684
|
export interface HeatmapOptionConfig {
|
|
1432
1685
|
model: HeatmapModel
|
|
1433
1686
|
x: string
|
|
@@ -1452,6 +1705,8 @@ export interface HeatmapOptionConfig {
|
|
|
1452
1705
|
/** The plot's width (px), when known: the numbers shrink (or go) when the cells are too narrow for them. */
|
|
1453
1706
|
plotWidth?: number | null
|
|
1454
1707
|
chartOptions?: EChartsOption
|
|
1708
|
+
/** Language of the tooltip. Default the page's locale (`<html lang>`). */
|
|
1709
|
+
locale?: string
|
|
1455
1710
|
}
|
|
1456
1711
|
|
|
1457
1712
|
/**
|
|
@@ -1463,6 +1718,7 @@ export interface HeatmapOptionConfig {
|
|
|
1463
1718
|
*/
|
|
1464
1719
|
export function buildHeatmapOption(config: HeatmapOptionConfig): EChartsOption {
|
|
1465
1720
|
const { model, labels, valueFormatter } = config
|
|
1721
|
+
const t = textOf(config.locale)
|
|
1466
1722
|
const label = (key: string) => labels?.[key] ?? key
|
|
1467
1723
|
const present = model.cells.filter((c) => c.value !== null)
|
|
1468
1724
|
const missing = model.cells.filter((c) => c.value === null)
|
|
@@ -1480,9 +1736,9 @@ export function buildHeatmapOption(config: HeatmapOptionConfig): EChartsOption {
|
|
|
1480
1736
|
const n = cell.row ? toChartNumber(cell.row[key]) : null
|
|
1481
1737
|
return `${escapeHtml(label(key))} ${n === null ? EMPTY_VALUE : escapeHtml(valueFormatter(n))}`
|
|
1482
1738
|
})
|
|
1483
|
-
.join(
|
|
1484
|
-
const flag = cell.flagged ?
|
|
1485
|
-
return `${head} ${main}${extras ?
|
|
1739
|
+
.join(t.join)
|
|
1740
|
+
const flag = cell.flagged ? `${t.join}${t.flagged}` : ""
|
|
1741
|
+
return `${head} ${main}${extras ? `${t.join}${extras}` : ""}${flag}`
|
|
1486
1742
|
}
|
|
1487
1743
|
|
|
1488
1744
|
const cellFont = config.plotWidth
|
|
@@ -1599,6 +1855,7 @@ export function heatmapDataTable(
|
|
|
1599
1855
|
names: { x: string; y: string },
|
|
1600
1856
|
formatter: ValueFormatter,
|
|
1601
1857
|
labels?: Record<string, string>,
|
|
1858
|
+
locale?: string,
|
|
1602
1859
|
): ChartDataTable {
|
|
1603
1860
|
const label = (key: string) => labels?.[key] ?? key
|
|
1604
1861
|
const rows = model.ys.map((yk, yi) => [
|
|
@@ -1608,17 +1865,26 @@ export function heatmapDataTable(
|
|
|
1608
1865
|
return v === null || v === undefined ? EMPTY_VALUE : formatter(v)
|
|
1609
1866
|
}),
|
|
1610
1867
|
])
|
|
1611
|
-
return { caption: `${label(names.y)}×${label(names.x)}`, columns: [labels?.[names.y] ??
|
|
1868
|
+
return { caption: `${label(names.y)}×${label(names.x)}`, columns: [labels?.[names.y] ?? textOf(locale).item, ...model.xs.map(label)], rows }
|
|
1612
1869
|
}
|
|
1613
1870
|
|
|
1614
|
-
/**
|
|
1615
|
-
|
|
1871
|
+
/**
|
|
1872
|
+
* `summary="auto"`: the strongest cell, e.g. 「最大は 月曜 9時台 の 12件」 / "Highest: Mon 9am (12)". `undefined` with no usable cell.
|
|
1873
|
+
* `locale` picks the language; default the page's (`<html lang>`).
|
|
1874
|
+
*/
|
|
1875
|
+
export function buildHeatmapSummary(
|
|
1876
|
+
model: HeatmapModel,
|
|
1877
|
+
formatter: ValueFormatter,
|
|
1878
|
+
title?: string,
|
|
1879
|
+
labels?: Record<string, string>,
|
|
1880
|
+
locale?: string,
|
|
1881
|
+
): string | undefined {
|
|
1616
1882
|
let best: HeatmapCell | undefined
|
|
1617
1883
|
for (const cell of model.cells) if (cell.value !== null && (!best || cell.value > (best.value as number))) best = cell
|
|
1618
1884
|
if (!best) return undefined
|
|
1619
1885
|
const label = (key: string) => labels?.[key] ?? key
|
|
1620
1886
|
const head = title ? `${title}: ` : ""
|
|
1621
|
-
return `${head}
|
|
1887
|
+
return `${head}${textOf(locale).top(label(model.ys[best.yi]!), label(model.xs[best.xi]!), formatter(best.value as number))}`
|
|
1622
1888
|
}
|
|
1623
1889
|
|
|
1624
1890
|
/* -------------------------------------------------------------- component */
|
|
@@ -1665,9 +1931,9 @@ function HeatmapImpl<T extends Record<string, unknown>>(
|
|
|
1665
1931
|
}: HeatmapProps<T>,
|
|
1666
1932
|
ref: ForwardedRef<HTMLDivElement>,
|
|
1667
1933
|
) {
|
|
1668
|
-
const tokens =
|
|
1669
|
-
const
|
|
1670
|
-
const formatter = useMemo(() => resolveValueFormatter(valueFormatter, format ?? "number"), [valueFormatter, format])
|
|
1934
|
+
const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
|
|
1935
|
+
const locale = useLocale()
|
|
1936
|
+
const formatter = useMemo(() => resolveValueFormatter(valueFormatter, format ?? "number", locale), [valueFormatter, format, locale])
|
|
1671
1937
|
const model = useMemo(
|
|
1672
1938
|
() => buildHeatmapModel({ data, x, y, value, xLabels, yLabels, flag, scale, minValue, maxValue }),
|
|
1673
1939
|
[data, x, y, value, xLabels, yLabels, flag, scale?.min, scale?.max, minValue, maxValue],
|
|
@@ -1693,15 +1959,16 @@ function HeatmapImpl<T extends Record<string, unknown>>(
|
|
|
1693
1959
|
showTooltip,
|
|
1694
1960
|
plotWidth,
|
|
1695
1961
|
chartOptions,
|
|
1962
|
+
locale,
|
|
1696
1963
|
}),
|
|
1697
|
-
[model, x, y, value, colors, surface, tokens, labels, formatter, details, showValues, showTooltip, plotWidth, chartOptions],
|
|
1964
|
+
[model, x, y, value, colors, surface, tokens, labels, formatter, details, showValues, showTooltip, plotWidth, chartOptions, locale],
|
|
1698
1965
|
)
|
|
1699
|
-
const dataTable = useMemo(() => heatmapDataTable(model, { x, y }, formatter, labels), [model, x, y, formatter, labels])
|
|
1966
|
+
const dataTable = useMemo(() => heatmapDataTable(model, { x, y }, formatter, labels, locale), [model, x, y, formatter, labels, locale])
|
|
1700
1967
|
const resolvedSummary = useMemo(
|
|
1701
|
-
() => (summary === "auto" ? buildHeatmapSummary(model, formatter, title, labels) : summary),
|
|
1702
|
-
[summary, model, formatter, title, labels],
|
|
1968
|
+
() => (summary === "auto" ? buildHeatmapSummary(model, formatter, title, labels, locale) : summary),
|
|
1969
|
+
[summary, model, formatter, title, labels, locale],
|
|
1703
1970
|
)
|
|
1704
|
-
const label = ariaLabel ??
|
|
1971
|
+
const label = ariaLabel ?? textOf(locale).chart(labels?.[y] ?? y, labels?.[x] ?? x)
|
|
1705
1972
|
const legend = showLegend ? (
|
|
1706
1973
|
<HeatmapLegend model={model} formatter={formatter} tone={tone} />
|
|
1707
1974
|
) : null
|