@squadbase/vantage 0.6.2-dev1.9365dcd → 0.6.2-dev1.9d7130b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-3RJEOP3J.js +174 -0
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
- package/dist/chunk-CKWPK4RO.js.map +1 -0
- package/dist/chunk-EG4PX726.js +678 -0
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
- package/dist/chunk-QNOX7WLZ.js.map +1 -0
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
- package/dist/chunk-SHZM3EXB.js.map +1 -0
- package/dist/chunk-TRMHNMMZ.js +61 -0
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +57 -273
- package/dist/components/index.js +6 -3
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +422 -4
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/query/index.js +2 -2
- package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
- package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1023 -2
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +9 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -2
- package/dist/ui/index.js +6 -6
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
- package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +2 -0
- package/docs/en/components.md +13 -2
- package/docs/en/doc/delta-text.md +40 -0
- package/docs/en/doc/doc-action-items.md +49 -0
- package/docs/en/doc/doc-callout.md +38 -0
- package/docs/en/doc/doc-data-table.md +60 -0
- package/docs/en/doc/doc-executive-summary.md +64 -0
- package/docs/en/doc/doc-figure.md +56 -0
- package/docs/en/doc/doc-key-findings.md +61 -0
- package/docs/en/doc/doc-kpi-scorecard.md +80 -0
- package/docs/en/doc/doc-notes-and-sources.md +43 -0
- package/docs/en/doc/doc-report-header.md +46 -0
- package/docs/en/pages-and-metadata.md +15 -0
- package/docs/en/parts/area-chart.md +99 -0
- package/docs/en/parts/bar-chart.md +91 -0
- package/docs/en/parts/bar-list.md +50 -0
- package/docs/en/parts/category-bar.md +37 -0
- package/docs/en/parts/chart-frame.md +39 -0
- package/docs/en/parts/delta.md +4 -3
- package/docs/en/parts/donut-chart.md +72 -0
- package/docs/en/parts/funnel-steps.md +2 -1
- package/docs/en/parts/heatmap.md +53 -0
- package/docs/en/parts/kpi-card.md +2 -1
- package/docs/en/parts/legend.md +35 -0
- package/docs/en/parts/line-chart.md +94 -0
- package/docs/en/parts/progress-circle.md +33 -0
- package/docs/en/parts/sample-data.md +39 -0
- package/docs/en/parts/scatter-chart.md +56 -0
- package/docs/en/parts/tracker.md +36 -0
- package/docs/en/parts/updated-at.md +30 -0
- package/docs/en/parts/waterfall-chart.md +56 -0
- package/docs/en/slide/slide-chart-full.md +48 -0
- package/docs/en/slide/slide-chart-insight.md +52 -0
- package/docs/en/slide/slide-columns.md +46 -0
- package/docs/en/slide/slide-comparison.md +68 -0
- package/docs/en/slide/slide-cover.md +39 -0
- package/docs/en/slide/slide-exec-summary.md +47 -0
- package/docs/en/slide/slide-heading.md +38 -0
- package/docs/en/slide/slide-kpi-grid.md +55 -0
- package/docs/en/slide/slide-kpi-hero.md +58 -0
- package/docs/en/slide/slide-next-actions.md +49 -0
- package/docs/en/slide/slide-scorecard-table.md +54 -0
- package/docs/en/slide/slide-section.md +44 -0
- package/docs/en/slide/slide-source-line.md +34 -0
- package/docs/en/ui/alert-dialog.md +42 -0
- package/docs/en/ui/avatar.md +31 -0
- package/docs/en/ui/combobox.md +74 -0
- package/docs/en/ui/drawer.md +41 -0
- package/docs/en/ui/empty.md +1 -1
- package/docs/en/ui/error-state.md +4 -3
- package/docs/en/ui/field.md +42 -0
- package/docs/en/ui/hover-card.md +21 -0
- package/docs/en/ui/kbd.md +17 -0
- package/docs/en/ui/loading.md +1 -1
- package/docs/en/ui/pagination.md +36 -0
- package/docs/en/ui/radio-group.md +21 -0
- package/docs/en/ui/toast.md +44 -0
- package/docs/en/ui/toggle-group.md +4 -3
- package/docs/index.json +857 -77
- package/docs/ja/agent-skills.md +1 -1
- package/docs/ja/changelog.md +2 -0
- package/docs/ja/components.md +12 -1
- package/docs/ja/doc/delta-text.md +40 -0
- package/docs/ja/doc/doc-action-items.md +49 -0
- package/docs/ja/doc/doc-callout.md +38 -0
- package/docs/ja/doc/doc-data-table.md +60 -0
- package/docs/ja/doc/doc-executive-summary.md +64 -0
- package/docs/ja/doc/doc-figure.md +56 -0
- package/docs/ja/doc/doc-key-findings.md +61 -0
- package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
- package/docs/ja/doc/doc-notes-and-sources.md +43 -0
- package/docs/ja/doc/doc-report-header.md +46 -0
- package/docs/ja/page-types/slide.md +1 -0
- package/docs/ja/pages-and-metadata.md +15 -0
- package/docs/ja/parts/area-chart.md +49 -25
- package/docs/ja/parts/bar-chart.md +50 -24
- package/docs/ja/parts/bar-list.md +31 -8
- package/docs/ja/parts/chart-frame.md +36 -0
- package/docs/ja/parts/delta.md +5 -4
- package/docs/ja/parts/donut-chart.md +45 -26
- package/docs/ja/parts/funnel-steps.md +27 -8
- package/docs/ja/parts/heatmap.md +45 -0
- package/docs/ja/parts/kpi-card.md +2 -1
- package/docs/ja/parts/legend.md +36 -0
- package/docs/ja/parts/line-chart.md +48 -22
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -0
- package/docs/ja/parts/updated-at.md +27 -0
- package/docs/ja/parts/waterfall-chart.md +61 -0
- package/docs/ja/slide/slide-chart-full.md +48 -0
- package/docs/ja/slide/slide-chart-insight.md +52 -0
- package/docs/ja/slide/slide-columns.md +46 -0
- package/docs/ja/slide/slide-comparison.md +68 -0
- package/docs/ja/slide/slide-cover.md +39 -0
- package/docs/ja/slide/slide-exec-summary.md +47 -0
- package/docs/ja/slide/slide-heading.md +38 -0
- package/docs/ja/slide/slide-kpi-grid.md +55 -0
- package/docs/ja/slide/slide-kpi-hero.md +58 -0
- package/docs/ja/slide/slide-next-actions.md +49 -0
- package/docs/ja/slide/slide-scorecard-table.md +54 -0
- package/docs/ja/slide/slide-section.md +44 -0
- package/docs/ja/slide/slide-source-line.md +34 -0
- package/docs/ja/ui/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- package/docs/ja/ui/loading.md +1 -1
- package/docs/ja/ui/toggle-group.md +4 -3
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +389 -104
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +386 -104
- package/registry/ui/bar-list.tsx +376 -94
- package/registry/ui/box-plot-chart.tsx +450 -116
- package/registry/ui/category-bar.tsx +292 -57
- package/registry/ui/chart-frame.tsx +301 -63
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +181 -19
- package/registry/ui/donut-chart.tsx +385 -103
- package/registry/ui/echart.tsx +313 -58
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +338 -71
- package/registry/ui/histogram-chart.tsx +440 -116
- package/registry/ui/kpi-card.tsx +254 -48
- package/registry/ui/legend.tsx +374 -93
- package/registry/ui/line-chart.tsx +389 -104
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +148 -10
- package/registry/ui/paired-chart.tsx +394 -109
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +154 -10
- package/registry/ui/scatter-chart.tsx +436 -119
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +201 -36
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +214 -42
- package/registry/ui/trend-indicator.tsx +151 -8
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +460 -121
- package/skills/vantage-add-feature/SKILL.md +5 -0
- package/skills/vantage-design/SKILL.md +54 -54
- package/skills/vantage-design/references/extract.md +26 -27
- package/skills/vantage-design/references/format.md +12 -172
- package/skills/vantage-design/references/presets.md +50 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +35 -18
- package/templates/agents/api.md +7 -0
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +59 -2
- package/templates/agents/slide.md +77 -3
- package/theme.css +23 -19
- package/dist/chunk-2ICNHUG6.js.map +0 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-WAMZWZJB.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
|
@@ -19,7 +19,8 @@ import { ChartFrame } from "@squadbase/vantage/components"
|
|
|
19
19
|
import { EChart } from "@squadbase/vantage/components"
|
|
20
20
|
import { toValueFormatter } from "@squadbase/vantage/components"
|
|
21
21
|
import type { ChartCommonProps, ValueFormatter } from "@squadbase/vantage/components"
|
|
22
|
-
import {
|
|
22
|
+
import { useSyncExternalStore } from "react"
|
|
23
|
+
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
23
24
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
24
25
|
import { type ReactNode } from "react"
|
|
25
26
|
import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
@@ -28,6 +29,137 @@ import type { ChartThreshold, ChartAnnotation, ChartEventPayload, ChartForecast,
|
|
|
28
29
|
import { formatValue } from "@squadbase/vantage/components"
|
|
29
30
|
import type { ChartColor, ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
30
31
|
|
|
32
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The display language of the parts and the defaults that follow it.
|
|
36
|
+
*
|
|
37
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
38
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
39
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
40
|
+
*
|
|
41
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
42
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
46
|
+
type UiLanguage = "ja" | "en"
|
|
47
|
+
|
|
48
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
49
|
+
const FALLBACK_LOCALE = "en-US"
|
|
50
|
+
|
|
51
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
52
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
53
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
57
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
58
|
+
const value = tag?.trim()
|
|
59
|
+
if (!value) return FALLBACK_LOCALE
|
|
60
|
+
try {
|
|
61
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
62
|
+
} catch {
|
|
63
|
+
return FALLBACK_LOCALE
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
69
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
70
|
+
*/
|
|
71
|
+
function documentLocale(): string {
|
|
72
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
73
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
77
|
+
function resolveLocale(locale?: string | null): string {
|
|
78
|
+
return locale || documentLocale()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Picks the wording table of the locale's language. */
|
|
82
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
83
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
87
|
+
function defaultCurrency(locale?: string | null): string {
|
|
88
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
92
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
93
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
97
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
98
|
+
|
|
99
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
100
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
104
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
105
|
+
* Monday for the rest.
|
|
106
|
+
*/
|
|
107
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
108
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
109
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
110
|
+
try {
|
|
111
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
112
|
+
getWeekInfo?: () => { firstDay: number }
|
|
113
|
+
weekInfo?: { firstDay: number }
|
|
114
|
+
}
|
|
115
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
116
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
117
|
+
} catch {
|
|
118
|
+
// Fall through to the table below.
|
|
119
|
+
}
|
|
120
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
124
|
+
|
|
125
|
+
const listeners = new Set<() => void>()
|
|
126
|
+
let observer: MutationObserver | null = null
|
|
127
|
+
|
|
128
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
129
|
+
function subscribe(onChange: () => void): () => void {
|
|
130
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
131
|
+
listeners.add(onChange)
|
|
132
|
+
if (!observer) {
|
|
133
|
+
observer = new MutationObserver(() => {
|
|
134
|
+
for (const listener of [...listeners]) listener()
|
|
135
|
+
})
|
|
136
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
137
|
+
}
|
|
138
|
+
return () => {
|
|
139
|
+
listeners.delete(onChange)
|
|
140
|
+
if (listeners.size === 0 && observer) {
|
|
141
|
+
observer.disconnect()
|
|
142
|
+
observer = null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
151
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
152
|
+
*/
|
|
153
|
+
function useDocumentLocale(): string {
|
|
154
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
158
|
+
function useLocale(locale?: string | null): string {
|
|
159
|
+
const page = useDocumentLocale()
|
|
160
|
+
return locale || page
|
|
161
|
+
}
|
|
162
|
+
|
|
31
163
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
32
164
|
|
|
33
165
|
/**
|
|
@@ -62,10 +194,23 @@ interface ChartTokens {
|
|
|
62
194
|
border: string | null
|
|
63
195
|
/** Tooltip surface. */
|
|
64
196
|
popover: string | null
|
|
197
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
198
|
+
colors?: Record<string, string>
|
|
199
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
200
|
+
surface?: string | null
|
|
65
201
|
/** Tooltip text. */
|
|
66
202
|
popoverForeground: string | null
|
|
67
203
|
}
|
|
68
204
|
|
|
205
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
206
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
207
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
208
|
+
const OPTION_COLOR_TOKENS = [
|
|
209
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
210
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
211
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
212
|
+
]
|
|
213
|
+
|
|
69
214
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
70
215
|
|
|
71
216
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -121,8 +266,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
121
266
|
const color = read(token)
|
|
122
267
|
if (color) series.push(color)
|
|
123
268
|
}
|
|
124
|
-
|
|
269
|
+
const tokens: ChartTokens = {
|
|
125
270
|
series,
|
|
271
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
272
|
+
surface: readChartSurface(target),
|
|
126
273
|
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
127
274
|
foreground: read("--foreground"),
|
|
128
275
|
mutedForeground: read("--muted-foreground"),
|
|
@@ -130,6 +277,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
130
277
|
popover: read("--popover"),
|
|
131
278
|
popoverForeground: read("--popover-foreground"),
|
|
132
279
|
}
|
|
280
|
+
tokenScopes.set(tokens, target)
|
|
281
|
+
return tokens
|
|
133
282
|
}
|
|
134
283
|
|
|
135
284
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -137,6 +286,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
137
286
|
if (a === b) return true
|
|
138
287
|
if (!a || !b) return false
|
|
139
288
|
return (
|
|
289
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
290
|
+
a.surface === b.surface &&
|
|
291
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
292
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
140
293
|
a.series.length === b.series.length &&
|
|
141
294
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
142
295
|
a.fontFamily === b.fontFamily &&
|
|
@@ -162,7 +315,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
162
315
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
163
316
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
164
317
|
*/
|
|
165
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
318
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
166
319
|
if (typeof document === "undefined") return () => {}
|
|
167
320
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
168
321
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -177,8 +330,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
177
330
|
|
|
178
331
|
const themeObserver = new MutationObserver(schedule)
|
|
179
332
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
180
|
-
|
|
181
|
-
|
|
333
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
334
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
335
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
336
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
337
|
+
}
|
|
182
338
|
|
|
183
339
|
const styleObserver = new MutationObserver(schedule)
|
|
184
340
|
if (document.head) {
|
|
@@ -266,22 +422,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
266
422
|
* components that build an ECharts option from token colours themselves (the
|
|
267
423
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
268
424
|
*/
|
|
269
|
-
function useChartTokens(): ChartTokens | null {
|
|
425
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
270
426
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
271
427
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
272
428
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
273
429
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
274
430
|
const tokensRef = useRef(tokens)
|
|
275
431
|
useEffect(() => {
|
|
432
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
276
433
|
const sync = () => {
|
|
277
|
-
const next = readChartTokens()
|
|
434
|
+
const next = readChartTokens(target)
|
|
278
435
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
279
436
|
tokensRef.current = next
|
|
280
437
|
setTokens(next)
|
|
281
438
|
}
|
|
282
439
|
sync()
|
|
283
|
-
return watchChartTokens(sync)
|
|
284
|
-
}, [])
|
|
440
|
+
return watchChartTokens(sync, target)
|
|
441
|
+
}, [scope])
|
|
285
442
|
return tokens
|
|
286
443
|
}
|
|
287
444
|
|
|
@@ -293,7 +450,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
293
450
|
/**
|
|
294
451
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
295
452
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
296
|
-
* variants `"positive-fill"`, …) are read
|
|
453
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
297
454
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
298
455
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
299
456
|
*/
|
|
@@ -304,12 +461,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
304
461
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
305
462
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
306
463
|
if (typeof document === "undefined") return ""
|
|
307
|
-
|
|
464
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
465
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
308
466
|
}
|
|
309
467
|
if (typeof document === "undefined") return color
|
|
310
468
|
let value = color.trim()
|
|
311
469
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
312
|
-
const styles = getComputedStyle(document.documentElement)
|
|
470
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
313
471
|
if (cssVar) {
|
|
314
472
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
315
473
|
} else if (value.includes("var(")) {
|
|
@@ -355,7 +513,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
355
513
|
if (color) return color
|
|
356
514
|
}
|
|
357
515
|
}
|
|
358
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
516
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
359
517
|
}
|
|
360
518
|
|
|
361
519
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -369,9 +527,10 @@ const WIDTH_BUCKET = 40
|
|
|
369
527
|
*/
|
|
370
528
|
function useChartSurface<E extends Element>(
|
|
371
529
|
forwarded: ForwardedRef<E>,
|
|
372
|
-
tokens
|
|
373
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
530
|
+
tokens?: ChartTokens | null,
|
|
531
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
374
532
|
const [node, setNode] = useState<E | null>(null)
|
|
533
|
+
const scopedTokens = useChartTokens(node)
|
|
375
534
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
376
535
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
377
536
|
const ref = useCallback(
|
|
@@ -385,7 +544,7 @@ function useChartSurface<E extends Element>(
|
|
|
385
544
|
useEffect(() => {
|
|
386
545
|
if (!node) return
|
|
387
546
|
setSurface(readChartSurface(node) ?? undefined)
|
|
388
|
-
}, [node, tokens])
|
|
547
|
+
}, [node, tokens, scopedTokens])
|
|
389
548
|
useEffect(() => {
|
|
390
549
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
391
550
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -394,11 +553,37 @@ function useChartSurface<E extends Element>(
|
|
|
394
553
|
observer.observe(node)
|
|
395
554
|
return () => observer.disconnect()
|
|
396
555
|
}, [node])
|
|
397
|
-
return { ref, surface, width }
|
|
556
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
398
557
|
}
|
|
399
558
|
|
|
400
559
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
401
560
|
|
|
561
|
+
/** The built-in words of the option builders, by language. */
|
|
562
|
+
const CHART_TEXT = {
|
|
563
|
+
ja: {
|
|
564
|
+
target: "目標",
|
|
565
|
+
forecast: "予測",
|
|
566
|
+
total: "合計",
|
|
567
|
+
provisional: "速報",
|
|
568
|
+
noComparison: "比較なし",
|
|
569
|
+
share: (share: string) => `(${share})`,
|
|
570
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
571
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
572
|
+
},
|
|
573
|
+
en: {
|
|
574
|
+
target: "Target",
|
|
575
|
+
forecast: "Forecast",
|
|
576
|
+
total: "Total",
|
|
577
|
+
provisional: "Preliminary",
|
|
578
|
+
noComparison: "No comparison",
|
|
579
|
+
share: (share: string) => ` (${share})`,
|
|
580
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
581
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
582
|
+
},
|
|
583
|
+
} satisfies Record<UiLanguage, unknown>
|
|
584
|
+
|
|
585
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
586
|
+
|
|
402
587
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
403
588
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
404
589
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -583,7 +768,7 @@ interface CartesianChartConfig {
|
|
|
583
768
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
584
769
|
*/
|
|
585
770
|
axisTicks?: (top: number) => (value: number) => string
|
|
586
|
-
/** Locale of the time-axis labels. Default
|
|
771
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
587
772
|
locale?: string
|
|
588
773
|
type?: CartesianStackType
|
|
589
774
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -821,11 +1006,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
821
1006
|
*/
|
|
822
1007
|
function timeAxisLabels(
|
|
823
1008
|
values: readonly unknown[],
|
|
824
|
-
|
|
1009
|
+
localeArg?: string,
|
|
825
1010
|
shown: (i: number) => boolean = () => true,
|
|
826
1011
|
): string[] | null {
|
|
827
1012
|
const parsed = parseTimeIndex(values)
|
|
828
1013
|
if (!parsed) return null
|
|
1014
|
+
const locale = resolveLocale(localeArg)
|
|
829
1015
|
const { granularity, times } = parsed
|
|
830
1016
|
let prevYear: number | null = null
|
|
831
1017
|
return times.map((t, i) => {
|
|
@@ -848,9 +1034,10 @@ function timeAxisLabels(
|
|
|
848
1034
|
}
|
|
849
1035
|
|
|
850
1036
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
851
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1037
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
852
1038
|
const parsed = parseTimeIndex(values)
|
|
853
1039
|
if (!parsed) return null
|
|
1040
|
+
const locale = resolveLocale(localeArg)
|
|
854
1041
|
const options: Intl.DateTimeFormatOptions =
|
|
855
1042
|
parsed.granularity === "year"
|
|
856
1043
|
? { year: "numeric" }
|
|
@@ -990,7 +1177,9 @@ function tooltipHtml(
|
|
|
990
1177
|
rows: readonly TooltipRow[],
|
|
991
1178
|
valueFormatter: (value: number) => string,
|
|
992
1179
|
notes: readonly string[] = [],
|
|
1180
|
+
locale?: string,
|
|
993
1181
|
): string {
|
|
1182
|
+
const t = chartText(locale)
|
|
994
1183
|
const lines = rows.map(
|
|
995
1184
|
(row) =>
|
|
996
1185
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -999,7 +1188,7 @@ function tooltipHtml(
|
|
|
999
1188
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1000
1189
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1001
1190
|
}${
|
|
1002
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
1191
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1003
1192
|
}</span></div>` +
|
|
1004
1193
|
(row.note === undefined
|
|
1005
1194
|
? ""
|
|
@@ -1013,8 +1202,10 @@ function tooltipHtml(
|
|
|
1013
1202
|
|
|
1014
1203
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1015
1204
|
|
|
1016
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1017
|
-
|
|
1205
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
1206
|
+
function barTargetLabel(locale?: string): string {
|
|
1207
|
+
return chartText(locale).target
|
|
1208
|
+
}
|
|
1018
1209
|
|
|
1019
1210
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1020
1211
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1065,16 +1256,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1065
1256
|
return (value / target) * 100
|
|
1066
1257
|
}
|
|
1067
1258
|
|
|
1068
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
1259
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1069
1260
|
function barTargetNote(
|
|
1070
1261
|
label: string,
|
|
1071
1262
|
value: number | null,
|
|
1072
1263
|
target: number | null,
|
|
1073
1264
|
valueFormatter: (value: number) => string,
|
|
1265
|
+
locale?: string,
|
|
1074
1266
|
): string | null {
|
|
1075
1267
|
if (target === null) return null
|
|
1076
1268
|
const rate = achievementRate(value, target)
|
|
1077
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
1269
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1078
1270
|
}
|
|
1079
1271
|
|
|
1080
1272
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1166,11 +1358,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1166
1358
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1167
1359
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1168
1360
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1361
|
+
/**
|
|
1362
|
+
* 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.
|
|
1363
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
1364
|
+
*/
|
|
1365
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
1366
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1174
1367
|
}
|
|
1175
1368
|
|
|
1176
1369
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -1210,8 +1403,10 @@ interface AxisExtent {
|
|
|
1210
1403
|
max: number
|
|
1211
1404
|
}
|
|
1212
1405
|
|
|
1213
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
1214
|
-
|
|
1406
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
1407
|
+
function stackTotalLabel(locale?: string): string {
|
|
1408
|
+
return chartText(locale).total
|
|
1409
|
+
}
|
|
1215
1410
|
|
|
1216
1411
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
1217
1412
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -1333,8 +1528,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
1333
1528
|
|
|
1334
1529
|
/**
|
|
1335
1530
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
1336
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
1337
|
-
* either value is missing or the baseline is 0.
|
|
1531
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
1532
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
1338
1533
|
*/
|
|
1339
1534
|
function comparisonNote(
|
|
1340
1535
|
current: number | null,
|
|
@@ -1342,16 +1537,18 @@ function comparisonNote(
|
|
|
1342
1537
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
1343
1538
|
grain?: PeriodGrain,
|
|
1344
1539
|
format?: ValueFormat,
|
|
1345
|
-
|
|
1540
|
+
localeArg?: string,
|
|
1346
1541
|
): string {
|
|
1542
|
+
const locale = resolveLocale(localeArg)
|
|
1347
1543
|
const result = computeComparison(
|
|
1348
1544
|
current,
|
|
1349
1545
|
comparison.vs === "custom"
|
|
1350
1546
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
1351
1547
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
1548
|
+
{ locale },
|
|
1352
1549
|
)
|
|
1353
1550
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
1354
|
-
if (text === null) return
|
|
1551
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
1355
1552
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
1356
1553
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
1357
1554
|
}
|
|
@@ -1394,7 +1591,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1394
1591
|
toneColors,
|
|
1395
1592
|
valueFormatter,
|
|
1396
1593
|
axisFormatter = valueFormatter,
|
|
1397
|
-
locale = DEFAULT_LOCALE,
|
|
1398
1594
|
type = "default",
|
|
1399
1595
|
layout = "horizontal",
|
|
1400
1596
|
curveType = "linear",
|
|
@@ -1431,7 +1627,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1431
1627
|
chartOptions,
|
|
1432
1628
|
reserveActions = false,
|
|
1433
1629
|
} = cfg
|
|
1434
|
-
const
|
|
1630
|
+
const locale = resolveLocale(cfg.locale)
|
|
1631
|
+
const text = chartText(locale)
|
|
1632
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
1435
1633
|
|
|
1436
1634
|
const stacked = kind !== "line" && type !== "default"
|
|
1437
1635
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -1580,9 +1778,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1580
1778
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
1581
1779
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
1582
1780
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
1583
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
1781
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
1584
1782
|
const highlightText = (value: number) =>
|
|
1585
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
1783
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
1586
1784
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
1587
1785
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
1588
1786
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -1608,7 +1806,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1608
1806
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
1609
1807
|
const barTargets: (number | null)[] | null =
|
|
1610
1808
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
1611
|
-
const barTargetName = targetLabel ??
|
|
1809
|
+
const barTargetName = targetLabel ?? text.target
|
|
1612
1810
|
const barTargetColor = labelColor ?? guideColor
|
|
1613
1811
|
|
|
1614
1812
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -1822,7 +2020,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1822
2020
|
}
|
|
1823
2021
|
|
|
1824
2022
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
1825
|
-
const forecastLabel = forecast?.label ??
|
|
2023
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
1826
2024
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
1827
2025
|
const forecastValues: (number | null)[] = []
|
|
1828
2026
|
if (forecast) {
|
|
@@ -2298,7 +2496,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2298
2496
|
})
|
|
2299
2497
|
}
|
|
2300
2498
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
2301
|
-
rows.push({ name:
|
|
2499
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
2302
2500
|
}
|
|
2303
2501
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
2304
2502
|
if (provisional) {
|
|
@@ -2314,19 +2512,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2314
2512
|
...noteFor(at),
|
|
2315
2513
|
})
|
|
2316
2514
|
})
|
|
2317
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
2515
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
2318
2516
|
}
|
|
2319
2517
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
2320
2518
|
const lines: string[] = []
|
|
2321
2519
|
if (barTargets && i <= lastActual) {
|
|
2322
2520
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
2323
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
2521
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
2324
2522
|
if (note) lines.push(note)
|
|
2325
2523
|
}
|
|
2326
2524
|
for (const note of notes) {
|
|
2327
2525
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
2328
2526
|
}
|
|
2329
|
-
return tooltipHtml(provisional ?
|
|
2527
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
2330
2528
|
},
|
|
2331
2529
|
},
|
|
2332
2530
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -2418,8 +2616,6 @@ interface DonutOptionSlice {
|
|
|
2418
2616
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
2419
2617
|
const DONUT_OUTSIDE_LABELS = 2
|
|
2420
2618
|
|
|
2421
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
2422
|
-
|
|
2423
2619
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
2424
2620
|
|
|
2425
2621
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -2452,15 +2648,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
2452
2648
|
}
|
|
2453
2649
|
|
|
2454
2650
|
/**
|
|
2455
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
2651
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
2456
2652
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
2457
2653
|
* string (`value`, 「9.2万 セッション」).
|
|
2458
2654
|
*/
|
|
2459
2655
|
function donutCenterText(
|
|
2460
2656
|
total: number,
|
|
2461
2657
|
valueFormatter: (value: number) => string,
|
|
2462
|
-
{ totalLabel
|
|
2658
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
2463
2659
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
2660
|
+
totalLabel ??= chartText(locale).total
|
|
2464
2661
|
const amount = valueFormatter(total)
|
|
2465
2662
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
2466
2663
|
}
|
|
@@ -2688,8 +2885,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
2688
2885
|
|
|
2689
2886
|
/* ------------------------------------------------------------ provisional */
|
|
2690
2887
|
|
|
2691
|
-
/** The default label for points that are not final yet. */
|
|
2692
|
-
|
|
2888
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
2889
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
2890
|
+
return chartText(locale).provisional
|
|
2891
|
+
}
|
|
2693
2892
|
|
|
2694
2893
|
/**
|
|
2695
2894
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -2827,9 +3026,10 @@ function resolveValueFormatter(
|
|
|
2827
3026
|
function resolveAxisFormatter(
|
|
2828
3027
|
valueFormatter: ValueFormatter | undefined,
|
|
2829
3028
|
format: ValueFormat | undefined,
|
|
2830
|
-
|
|
3029
|
+
localeArg?: string,
|
|
2831
3030
|
): ValueFormatter {
|
|
2832
3031
|
if (valueFormatter) return valueFormatter
|
|
3032
|
+
const locale = resolveLocale(localeArg)
|
|
2833
3033
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
2834
3034
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
2835
3035
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -2838,40 +3038,75 @@ function resolveAxisFormatter(
|
|
|
2838
3038
|
return (value) => formatValue(value, short, locale)
|
|
2839
3039
|
}
|
|
2840
3040
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
{ char: "万", size: 1e4 },
|
|
2846
|
-
] as const
|
|
3041
|
+
interface CompactUnit {
|
|
3042
|
+
token: string
|
|
3043
|
+
size: number
|
|
3044
|
+
}
|
|
2847
3045
|
|
|
2848
|
-
const
|
|
3046
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
3047
|
+
|
|
3048
|
+
/**
|
|
3049
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
3050
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
3051
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
3052
|
+
*/
|
|
3053
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
3054
|
+
const cached = compactUnitCache.get(locale)
|
|
3055
|
+
if (cached !== undefined) return cached
|
|
3056
|
+
let units: CompactUnit[] | null = []
|
|
3057
|
+
try {
|
|
3058
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
3059
|
+
const seen = new Set<string>()
|
|
3060
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
3061
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
3062
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
3063
|
+
if (at === -1) continue
|
|
3064
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
3065
|
+
units = null
|
|
3066
|
+
break
|
|
3067
|
+
}
|
|
3068
|
+
const token = parts[at]!.value
|
|
3069
|
+
if (seen.has(token)) continue
|
|
3070
|
+
seen.add(token)
|
|
3071
|
+
units.push({ token, size: 10 ** exp })
|
|
3072
|
+
}
|
|
3073
|
+
} catch {
|
|
3074
|
+
units = null
|
|
3075
|
+
}
|
|
3076
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
3077
|
+
compactUnitCache.set(locale, sorted)
|
|
3078
|
+
return sorted
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
2849
3082
|
|
|
2850
3083
|
/**
|
|
2851
3084
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
2852
|
-
* that largest tick, so
|
|
2853
|
-
* Only the
|
|
2854
|
-
* any other format —
|
|
3085
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
3086
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
3087
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
3088
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
2855
3089
|
*/
|
|
2856
3090
|
function resolveAxisTickFormatter(
|
|
2857
3091
|
valueFormatter: ValueFormatter | undefined,
|
|
2858
3092
|
format: ValueFormat | undefined,
|
|
2859
|
-
|
|
3093
|
+
localeArg?: string,
|
|
2860
3094
|
): (top: number) => ValueFormatter {
|
|
3095
|
+
const locale = resolveLocale(localeArg)
|
|
2861
3096
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
2862
3097
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
if (!aligns) return () => base
|
|
3098
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
3099
|
+
if (!units) return () => base
|
|
2866
3100
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
3101
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
2867
3102
|
return (top) => {
|
|
2868
|
-
const unit =
|
|
3103
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
2869
3104
|
if (!unit) return base
|
|
2870
3105
|
return (value) => {
|
|
2871
3106
|
const text = base(value)
|
|
2872
3107
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
2873
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
2874
|
-
return text.replace(
|
|
3108
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
3109
|
+
return text.replace(numberToken, () => scaled)
|
|
2875
3110
|
}
|
|
2876
3111
|
}
|
|
2877
3112
|
}
|
|
@@ -2900,8 +3135,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
2900
3135
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
2901
3136
|
const MAX_CHART_SERIES = 5
|
|
2902
3137
|
|
|
2903
|
-
/** The series name the folded categories get. */
|
|
2904
|
-
const
|
|
3138
|
+
/** The series name the folded categories get, by language. */
|
|
3139
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
3140
|
+
|
|
3141
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
3142
|
+
function otherSeriesLabel(locale?: string): string {
|
|
3143
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
3147
|
+
|
|
3148
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
3149
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
3150
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
3151
|
+
}
|
|
2905
3152
|
|
|
2906
3153
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
2907
3154
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -2921,7 +3168,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
2921
3168
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
2922
3169
|
items: readonly Item[],
|
|
2923
3170
|
n: number = MAX_CHART_SERIES,
|
|
2924
|
-
otherLabel: string =
|
|
3171
|
+
otherLabel: string = otherSeriesLabel(),
|
|
2925
3172
|
): AggregatedItems<Item> {
|
|
2926
3173
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
2927
3174
|
const ranked = items
|
|
@@ -2950,7 +3197,7 @@ function aggregateSeries(
|
|
|
2950
3197
|
data: readonly Record<string, unknown>[],
|
|
2951
3198
|
categories: readonly string[],
|
|
2952
3199
|
n: number = MAX_CHART_SERIES,
|
|
2953
|
-
otherLabel: string =
|
|
3200
|
+
otherLabel: string = otherSeriesLabel(),
|
|
2954
3201
|
): AggregatedSeries {
|
|
2955
3202
|
const totals = categories.map((name) => ({
|
|
2956
3203
|
name,
|
|
@@ -2982,12 +3229,14 @@ function aggregateSeriesKeeping(
|
|
|
2982
3229
|
categories: readonly string[],
|
|
2983
3230
|
keep: string | undefined,
|
|
2984
3231
|
n: number = MAX_CHART_SERIES,
|
|
3232
|
+
otherLabel: string = otherSeriesLabel(),
|
|
2985
3233
|
): AggregatedSeries {
|
|
2986
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
3234
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
2987
3235
|
const aggregated = aggregateSeries(
|
|
2988
3236
|
data,
|
|
2989
3237
|
categories.filter((c) => c !== keep),
|
|
2990
3238
|
n,
|
|
3239
|
+
otherLabel,
|
|
2991
3240
|
)
|
|
2992
3241
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
2993
3242
|
}
|
|
@@ -3023,7 +3272,7 @@ function seriesColorsWithComparison(
|
|
|
3023
3272
|
* Colour per drawn series: the record's token for that category, else the
|
|
3024
3273
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
3025
3274
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
3026
|
-
* data itself names
|
|
3275
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
3027
3276
|
*/
|
|
3028
3277
|
function seriesColorsFor(
|
|
3029
3278
|
categories: readonly string[],
|
|
@@ -3035,7 +3284,7 @@ function seriesColorsFor(
|
|
|
3035
3284
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
3036
3285
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
3037
3286
|
return categories.map((category, i) => {
|
|
3038
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
3287
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
3039
3288
|
const fromRecord = record?.[category]
|
|
3040
3289
|
if (fromRecord) return fromRecord
|
|
3041
3290
|
if (list && list.length > 0) {
|
|
@@ -3082,7 +3331,7 @@ interface DonutSlicesConfig {
|
|
|
3082
3331
|
*/
|
|
3083
3332
|
function donutSlices(
|
|
3084
3333
|
data: readonly Record<string, unknown>[],
|
|
3085
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
3334
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
3086
3335
|
): DonutSlice[] {
|
|
3087
3336
|
const items = data
|
|
3088
3337
|
.map((row, i) => {
|
|
@@ -3095,7 +3344,8 @@ function donutSlices(
|
|
|
3095
3344
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
3096
3345
|
name: item.name,
|
|
3097
3346
|
value: item.value,
|
|
3098
|
-
|
|
3347
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
3348
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
3099
3349
|
isOther: false,
|
|
3100
3350
|
row: item.row,
|
|
3101
3351
|
}))
|
|
@@ -3168,7 +3418,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
3168
3418
|
*/
|
|
3169
3419
|
function barListRows<Row extends object>(
|
|
3170
3420
|
data: readonly Row[],
|
|
3171
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
3421
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
3172
3422
|
): BarListRow<Row>[] {
|
|
3173
3423
|
const items = data.map((row): BarListRow<Row> => {
|
|
3174
3424
|
const cells = row as Record<string, unknown>
|
|
@@ -3214,19 +3464,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
3214
3464
|
|
|
3215
3465
|
/**
|
|
3216
3466
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
3217
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
3467
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
3218
3468
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
3219
3469
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
3220
3470
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
3221
3471
|
* keeps its contrast.
|
|
3222
3472
|
*/
|
|
3223
3473
|
function barListFills(
|
|
3224
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
3474
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
3225
3475
|
color: ChartColor,
|
|
3226
3476
|
otherColor: string,
|
|
3227
3477
|
): string[] {
|
|
3228
3478
|
return rows.map((row) =>
|
|
3229
|
-
row.isOther
|
|
3479
|
+
row.isOther || isOtherName(row.name)
|
|
3480
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
3481
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
3230
3482
|
)
|
|
3231
3483
|
}
|
|
3232
3484
|
|
|
@@ -3442,11 +3694,30 @@ interface ChartSummaryInput {
|
|
|
3442
3694
|
format?: ValueFormat
|
|
3443
3695
|
/** Wins over `format`. */
|
|
3444
3696
|
valueFormatter?: ValueFormatter
|
|
3697
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
3698
|
+
locale?: string
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
const SUMMARY_TEXT = {
|
|
3702
|
+
ja: {
|
|
3703
|
+
up: "、前の点より増加",
|
|
3704
|
+
down: "、前の点より減少",
|
|
3705
|
+
flat: "、前の点と横ばい",
|
|
3706
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
3707
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
3708
|
+
},
|
|
3709
|
+
en: {
|
|
3710
|
+
up: ", up from the previous point",
|
|
3711
|
+
down: ", down from the previous point",
|
|
3712
|
+
flat: ", flat from the previous point",
|
|
3713
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
3714
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
3715
|
+
},
|
|
3445
3716
|
}
|
|
3446
3717
|
|
|
3447
3718
|
/**
|
|
3448
3719
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
3449
|
-
* 「<title>: 最新 <index> は
|
|
3720
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
3450
3721
|
* computed from the last two non-empty points of the first series — never
|
|
3451
3722
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
3452
3723
|
* no usable point.
|
|
@@ -3462,26 +3733,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
3462
3733
|
}
|
|
3463
3734
|
const latest = points[points.length - 1]
|
|
3464
3735
|
if (!latest) return undefined
|
|
3465
|
-
const
|
|
3736
|
+
const locale = resolveLocale(input.locale)
|
|
3737
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
3738
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
3466
3739
|
const prev = points[points.length - 2]
|
|
3467
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
3740
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
3468
3741
|
const head = input.title ? `${input.title}: ` : ""
|
|
3469
|
-
return `${head}
|
|
3742
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
3470
3743
|
}
|
|
3471
3744
|
|
|
3472
3745
|
/**
|
|
3473
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
3474
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
3746
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
3747
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
3475
3748
|
*/
|
|
3476
3749
|
function buildShareSummary(
|
|
3477
3750
|
slices: readonly { name: string; value: number }[],
|
|
3478
3751
|
title?: string,
|
|
3752
|
+
locale?: string,
|
|
3479
3753
|
): string | undefined {
|
|
3480
3754
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
3481
3755
|
const top = slices[0]
|
|
3482
3756
|
if (!top || total <= 0) return undefined
|
|
3483
3757
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
3484
|
-
return `${title ? `${title}: ` : ""}
|
|
3758
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
3485
3759
|
}
|
|
3486
3760
|
|
|
3487
3761
|
/* ---------- inlined from @squadbase/squadkit registry/lib/distribution.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -3660,7 +3934,7 @@ export interface HistogramChartProps
|
|
|
3660
3934
|
max?: number
|
|
3661
3935
|
/** しきい値の線(任意)。階級の途中にも引ける。 */
|
|
3662
3936
|
threshold?: HistogramThreshold
|
|
3663
|
-
/**
|
|
3937
|
+
/** 件数の単位。既定は日本語のページで「件」、それ以外は単位なし。 */
|
|
3664
3938
|
countUnit?: string
|
|
3665
3939
|
/** 横軸の値の名前(データ表の先頭列)。既定「値」。例「解決日数」 */
|
|
3666
3940
|
valueLabel?: string
|
|
@@ -3672,9 +3946,45 @@ export interface HistogramChartProps
|
|
|
3672
3946
|
|
|
3673
3947
|
/* ------------------------------------------------------------------ model */
|
|
3674
3948
|
|
|
3949
|
+
const TEXT = {
|
|
3950
|
+
ja: {
|
|
3951
|
+
countUnit: "件",
|
|
3952
|
+
atOrAbove: (from: string) => `${from}以上`,
|
|
3953
|
+
range: (from: string, to: string) => `${from}〜${to}`,
|
|
3954
|
+
count: (n: number, unit: string) => `${n}${unit}`,
|
|
3955
|
+
countHeader: (unit: string) => `${unit}数`,
|
|
3956
|
+
share: "構成比",
|
|
3957
|
+
value: "値",
|
|
3958
|
+
valueInSentence: "値",
|
|
3959
|
+
threshold: "しきい値",
|
|
3960
|
+
peak: (total: number, bin: string, count: number, unit: string) => `${total}${unit}のうち最も多いのは ${bin}(${count}${unit})`,
|
|
3961
|
+
overThreshold: (name: string, value: string, count: string, share: string) => `${name}(${value})以上は ${count}(${share})`,
|
|
3962
|
+
sentenceJoin: "。",
|
|
3963
|
+
chart: (label: string) => `${label}のヒストグラム`,
|
|
3964
|
+
},
|
|
3965
|
+
en: {
|
|
3966
|
+
countUnit: "",
|
|
3967
|
+
atOrAbove: (from: string) => `${from}+`,
|
|
3968
|
+
range: (from: string, to: string) => `${from}–${to}`,
|
|
3969
|
+
count: (n: number, unit: string) => (unit ? `${n} ${unit}` : `${n}`),
|
|
3970
|
+
countHeader: () => "Count",
|
|
3971
|
+
share: "Share",
|
|
3972
|
+
value: "Value",
|
|
3973
|
+
valueInSentence: "values",
|
|
3974
|
+
threshold: "Threshold",
|
|
3975
|
+
peak: (total: number, bin: string, count: number, unit: string) => `Most common: ${bin} (${unit ? `${count} ${unit}` : count} of ${total})`,
|
|
3976
|
+
overThreshold: (name: string, value: string, count: string, share: string) => `At or above ${name} (${value}): ${count} (${share})`,
|
|
3977
|
+
sentenceJoin: ". ",
|
|
3978
|
+
chart: (label: string) => `Histogram of ${label}`,
|
|
3979
|
+
},
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
const textOf = (locale?: string) => TEXT[uiLanguage(resolveLocale(locale))]
|
|
3983
|
+
|
|
3675
3984
|
/** 階級の名前。`10〜15`、上限超えは `30以上`。 */
|
|
3676
|
-
export function binLabel(bin: HistogramBin, formatter: ValueFormatter): string {
|
|
3677
|
-
|
|
3985
|
+
export function binLabel(bin: HistogramBin, formatter: ValueFormatter, locale?: string): string {
|
|
3986
|
+
const t = textOf(locale)
|
|
3987
|
+
return bin.overflow ? t.atOrAbove(formatter(bin.from)) : t.range(formatter(bin.from), formatter(bin.to))
|
|
3678
3988
|
}
|
|
3679
3989
|
|
|
3680
3990
|
/** しきい値が来る位置を階級の添字(小数。階級の左端 = 添字 − 0.5)で返す。範囲の外は `null`。 */
|
|
@@ -3700,27 +4010,30 @@ const percent = (n: number, total: number) => (total === 0 ? "0%" : `${Math.roun
|
|
|
3700
4010
|
export function buildHistogramSummary(
|
|
3701
4011
|
bins: readonly HistogramBin[],
|
|
3702
4012
|
formatter: ValueFormatter,
|
|
3703
|
-
config: { total: number; countUnit: string; atOrAbove?: number; threshold?: HistogramThreshold },
|
|
4013
|
+
config: { total: number; countUnit: string; atOrAbove?: number; threshold?: HistogramThreshold; locale?: string },
|
|
3704
4014
|
): string | undefined {
|
|
3705
4015
|
if (bins.length === 0 || config.total === 0) return undefined
|
|
4016
|
+
const t = textOf(config.locale)
|
|
4017
|
+
const count = (n: number) => t.count(n, config.countUnit)
|
|
3706
4018
|
const peak = bins.reduce((a, b) => (b.count > a.count ? b : a))
|
|
3707
|
-
const parts = [
|
|
4019
|
+
const parts = [t.peak(config.total, binLabel(peak, formatter, config.locale), peak.count, config.countUnit)]
|
|
3708
4020
|
if (config.threshold && config.atOrAbove !== undefined) {
|
|
3709
|
-
const name = config.threshold.label ??
|
|
3710
|
-
parts.push(
|
|
4021
|
+
const name = config.threshold.label ?? t.threshold
|
|
4022
|
+
parts.push(t.overThreshold(name, formatter(config.threshold.value), count(config.atOrAbove), percent(config.atOrAbove, config.total)))
|
|
3711
4023
|
}
|
|
3712
|
-
return parts.join(
|
|
4024
|
+
return parts.join(t.sentenceJoin)
|
|
3713
4025
|
}
|
|
3714
4026
|
|
|
3715
4027
|
/** The screen-reader table: one row per class. */
|
|
3716
4028
|
export function histogramDataTable(
|
|
3717
4029
|
bins: readonly HistogramBin[],
|
|
3718
4030
|
formatter: ValueFormatter,
|
|
3719
|
-
config: { total: number; countUnit: string; valueLabel?: string },
|
|
4031
|
+
config: { total: number; countUnit: string; valueLabel?: string; locale?: string },
|
|
3720
4032
|
): ChartDataTable {
|
|
4033
|
+
const t = textOf(config.locale)
|
|
3721
4034
|
return {
|
|
3722
|
-
columns: [config.valueLabel ??
|
|
3723
|
-
rows: bins.map((b) => [binLabel(b, formatter), `${b.count}`, percent(b.count, config.total)]),
|
|
4035
|
+
columns: [config.valueLabel ?? t.value, t.countHeader(config.countUnit), t.share],
|
|
4036
|
+
rows: bins.map((b) => [binLabel(b, formatter, config.locale), `${b.count}`, percent(b.count, config.total)]),
|
|
3724
4037
|
}
|
|
3725
4038
|
}
|
|
3726
4039
|
|
|
@@ -3783,13 +4096,17 @@ export interface HistogramOptionConfig {
|
|
|
3783
4096
|
labelColor?: string
|
|
3784
4097
|
lineColor?: string
|
|
3785
4098
|
chartOptions?: EChartsOption
|
|
4099
|
+
/** Language of the class names, tooltip and threshold label. Default the page's locale (`<html lang>`). */
|
|
4100
|
+
locale?: string
|
|
3786
4101
|
}
|
|
3787
4102
|
|
|
3788
4103
|
const ROW_STYLE = "display:flex;align-items:center;gap:8px;line-height:20px"
|
|
3789
4104
|
|
|
3790
4105
|
/** The ECharts option: gap-less bars over the classes, the overflow class in the neutral fill, an optional threshold line. */
|
|
3791
4106
|
export function buildHistogramOption(config: HistogramOptionConfig): EChartsOption {
|
|
3792
|
-
const {
|
|
4107
|
+
const { locale } = config
|
|
4108
|
+
const t = textOf(locale)
|
|
4109
|
+
const { bins, total, colors, valueFormatter, threshold, showTooltip = true, countUnit = t.countUnit } = config
|
|
3793
4110
|
const pos = threshold ? thresholdPosition(bins, threshold.value) : null
|
|
3794
4111
|
const option = {
|
|
3795
4112
|
...chartMotion(false),
|
|
@@ -3809,15 +4126,15 @@ export function buildHistogramOption(config: HistogramOptionConfig): EChartsOpti
|
|
|
3809
4126
|
`<span style="flex:1;opacity:.8">${escapeHtml(name)}</span>` +
|
|
3810
4127
|
`<span style="font-weight:500;font-variant-numeric:tabular-nums;margin-left:12px">${escapeHtml(text)}</span></div>`
|
|
3811
4128
|
return (
|
|
3812
|
-
`<div style="font-weight:500;margin-bottom:4px">${escapeHtml(binLabel(bin, valueFormatter))}</div>` +
|
|
3813
|
-
line(
|
|
3814
|
-
line(
|
|
4129
|
+
`<div style="font-weight:500;margin-bottom:4px">${escapeHtml(binLabel(bin, valueFormatter, locale))}</div>` +
|
|
4130
|
+
line(t.countHeader(countUnit), t.count(bin.count, countUnit)) +
|
|
4131
|
+
line(t.share, percent(bin.count, total))
|
|
3815
4132
|
)
|
|
3816
4133
|
},
|
|
3817
4134
|
},
|
|
3818
4135
|
xAxis: {
|
|
3819
4136
|
type: "category" as const,
|
|
3820
|
-
data: bins.map((b) => binLabel(b, valueFormatter)),
|
|
4137
|
+
data: bins.map((b) => binLabel(b, valueFormatter, locale)),
|
|
3821
4138
|
axisTick: { show: false },
|
|
3822
4139
|
axisLabel: { hideOverlap: true },
|
|
3823
4140
|
},
|
|
@@ -3852,7 +4169,7 @@ export function buildHistogramOption(config: HistogramOptionConfig): EChartsOpti
|
|
|
3852
4169
|
// [階級の添字, 0(y の位置は使わない), 階級の中での位置(左端 -0.5 〜 右端 0.5)]
|
|
3853
4170
|
data: [thresholdPoint(bins, threshold.value)],
|
|
3854
4171
|
renderItem: thresholdRenderer({
|
|
3855
|
-
text: `${threshold.label ??
|
|
4172
|
+
text: `${threshold.label ?? t.threshold} ${valueFormatter(threshold.value)}`,
|
|
3856
4173
|
lineColor: config.lineColor,
|
|
3857
4174
|
labelColor: config.labelColor,
|
|
3858
4175
|
}),
|
|
@@ -3875,7 +4192,7 @@ export const HistogramChart = forwardRef(function HistogramChart(
|
|
|
3875
4192
|
start,
|
|
3876
4193
|
max,
|
|
3877
4194
|
threshold,
|
|
3878
|
-
countUnit
|
|
4195
|
+
countUnit: countUnitProp,
|
|
3879
4196
|
valueLabel,
|
|
3880
4197
|
format,
|
|
3881
4198
|
valueFormatter: valueFormatterProp,
|
|
@@ -3894,12 +4211,14 @@ export const HistogramChart = forwardRef(function HistogramChart(
|
|
|
3894
4211
|
}: HistogramChartProps,
|
|
3895
4212
|
ref: ForwardedRef<HTMLDivElement>,
|
|
3896
4213
|
) {
|
|
3897
|
-
const tokens =
|
|
3898
|
-
const
|
|
4214
|
+
const { ref: frameRef, tokens } = useChartSurface(ref)
|
|
4215
|
+
const locale = useLocale()
|
|
4216
|
+
const t = TEXT[uiLanguage(locale)]
|
|
4217
|
+
const countUnit = countUnitProp ?? t.countUnit
|
|
3899
4218
|
// Class names read in full (`10〜15日`); the format default is a plain number, not the compact form.
|
|
3900
4219
|
const valueFormatter = useMemo(
|
|
3901
|
-
() => (valueFormatterProp ?? (format === undefined ? toValueFormatter("number") : resolveValueFormatter(undefined, format))),
|
|
3902
|
-
[valueFormatterProp, format],
|
|
4220
|
+
() => (valueFormatterProp ?? (format === undefined ? toValueFormatter("number", locale) : resolveValueFormatter(undefined, format, locale))),
|
|
4221
|
+
[valueFormatterProp, format, locale],
|
|
3903
4222
|
)
|
|
3904
4223
|
const bins = useMemo(() => histogramBins(values, { binWidth, start, max }), [values, binWidth, start, max])
|
|
3905
4224
|
const total = useMemo(() => bins.reduce((sum, b) => sum + b.count, 0), [bins])
|
|
@@ -3920,10 +4239,14 @@ export const HistogramChart = forwardRef(function HistogramChart(
|
|
|
3920
4239
|
labelColor: tokens?.foreground ?? undefined,
|
|
3921
4240
|
lineColor: tokens?.mutedForeground ?? undefined,
|
|
3922
4241
|
chartOptions,
|
|
4242
|
+
locale,
|
|
3923
4243
|
}),
|
|
3924
|
-
[bins, total, colors, valueFormatter, countUnit, threshold, showTooltip, tokens, chartOptions],
|
|
4244
|
+
[bins, total, colors, valueFormatter, countUnit, threshold, showTooltip, tokens, chartOptions, locale],
|
|
4245
|
+
)
|
|
4246
|
+
const dataTable = useMemo(
|
|
4247
|
+
() => histogramDataTable(bins, valueFormatter, { total, countUnit, valueLabel, locale }),
|
|
4248
|
+
[bins, valueFormatter, total, countUnit, valueLabel, locale],
|
|
3925
4249
|
)
|
|
3926
|
-
const dataTable = useMemo(() => histogramDataTable(bins, valueFormatter, { total, countUnit, valueLabel }), [bins, valueFormatter, total, countUnit, valueLabel])
|
|
3927
4250
|
const resolvedSummary = useMemo(
|
|
3928
4251
|
() =>
|
|
3929
4252
|
summary === "auto"
|
|
@@ -3932,11 +4255,12 @@ export const HistogramChart = forwardRef(function HistogramChart(
|
|
|
3932
4255
|
countUnit,
|
|
3933
4256
|
threshold,
|
|
3934
4257
|
atOrAbove: threshold ? countAtOrAbove(values, threshold.value, bins[0]?.from) : undefined,
|
|
4258
|
+
locale,
|
|
3935
4259
|
})
|
|
3936
4260
|
: summary,
|
|
3937
|
-
[summary, bins, valueFormatter, total, countUnit, threshold, values],
|
|
4261
|
+
[summary, bins, valueFormatter, total, countUnit, threshold, values, locale],
|
|
3938
4262
|
)
|
|
3939
|
-
const label = ariaLabel ??
|
|
4263
|
+
const label = ariaLabel ?? t.chart(valueLabel ?? t.valueInSentence)
|
|
3940
4264
|
return (
|
|
3941
4265
|
<ChartFrame
|
|
3942
4266
|
{...chartRootProps(rest)}
|