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