@squadbase/vantage 0.6.2-dev1.d594c56 → 0.6.2-dev1.e30f2cc
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-A23M4GCH.js} +7 -7
- package/dist/{build-X5NVCQKT.js.map → build-A23M4GCH.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-FBHT6W6J.js} +5 -5
- package/dist/{check-ZBIH422T.js.map → check-FBHT6W6J.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-57GHR5LN.js +163 -0
- package/dist/chunk-57GHR5LN.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-5C2SEDPH.js} +21 -4
- package/dist/chunk-5C2SEDPH.js.map +1 -0
- package/dist/chunk-6OTTGAPO.js +44 -0
- package/dist/chunk-6OTTGAPO.js.map +1 -0
- package/dist/{chunk-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
- package/dist/chunk-7S52M2SL.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-JXFCSF3K.js +673 -0
- package/dist/chunk-JXFCSF3K.js.map +1 -0
- package/dist/{chunk-HKSVYMYJ.js → chunk-QOXXOFGL.js} +61 -929
- package/dist/chunk-QOXXOFGL.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-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 +11 -247
- package/dist/components/index.js +5 -2
- package/dist/delta-text-CvwBGJ95.d.ts +15 -0
- package/dist/{dev-4XEDTELV.js → dev-H4I2JL63.js} +7 -7
- package/dist/{dev-4XEDTELV.js.map → dev-H4I2JL63.js.map} +1 -1
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +307 -3
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-5E2VEZPK.js → doctor-B2O3GJDU.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-B2O3GJDU.js.map} +1 -1
- package/dist/format-U0cQl069.d.ts +243 -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 +972 -2
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +7 -4
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +4 -1
- package/dist/ui/index.js +3 -3
- 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/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/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/area-chart.tsx +88 -26
- package/registry/ui/attention-list.tsx +303 -0
- package/registry/ui/bar-chart.tsx +88 -26
- package/registry/ui/bar-list.tsx +87 -24
- package/registry/ui/box-plot-chart.tsx +3906 -0
- package/registry/ui/category-bar.tsx +54 -19
- package/registry/ui/chart-frame.tsx +56 -21
- package/registry/ui/dashboard-card.tsx +2 -2
- package/registry/ui/delta.tsx +11 -8
- package/registry/ui/donut-chart.tsx +89 -26
- package/registry/ui/echart.tsx +194 -44
- package/registry/ui/funnel-steps.tsx +33 -5
- package/registry/ui/heatmap.tsx +55 -21
- package/registry/ui/histogram-chart.tsx +3986 -0
- package/registry/ui/kpi-card.tsx +41 -14
- package/registry/ui/legend.tsx +87 -24
- package/registry/ui/line-chart.tsx +88 -26
- package/registry/ui/page-shell.tsx +9 -9
- package/registry/ui/paired-chart.tsx +3827 -0
- package/registry/ui/sample-data.tsx +1 -1
- package/registry/ui/scatter-chart.tsx +88 -26
- package/registry/ui/section-header.tsx +2 -2
- package/registry/ui/slider.tsx +1 -1
- package/registry/ui/status-badge.tsx +8 -8
- package/registry/ui/tooltip.tsx +6 -2
- package/registry/ui/tracker.tsx +9 -3
- package/registry/ui/trend-indicator.tsx +2 -2
- package/registry/ui/waterfall-chart.tsx +88 -26
- package/skills/vantage-design/SKILL.md +27 -25
- package/skills/vantage-design/references/extract.md +16 -14
- package/skills/vantage-design/references/format.md +6 -5
- package/skills/vantage-design/references/presets.md +60 -110
- package/skills/vantage-pitfalls/SKILL.md +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +4 -2
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +22 -1
- package/templates/agents/slide.md +70 -2
- package/theme.css +23 -19
- package/dist/chunk-2ICNHUG6.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-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/echart.tsx
CHANGED
|
@@ -58,7 +58,7 @@ import { Check, Copy, Download } from "lucide-react"
|
|
|
58
58
|
import { cn } from "@squadbase/vantage/ui"
|
|
59
59
|
import { Button } from "@squadbase/vantage/ui"
|
|
60
60
|
import type { ChartActionsOptions } from "@squadbase/vantage/components"
|
|
61
|
-
import { createContext, type ForwardedRef } from "react"
|
|
61
|
+
import { createContext, type ForwardedRef, type RefObject } from "react"
|
|
62
62
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
63
63
|
|
|
64
64
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -85,6 +85,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
|
|
|
85
85
|
interface ChartTokens {
|
|
86
86
|
/** Resolved `--chart-1..5`, in order. */
|
|
87
87
|
series: string[]
|
|
88
|
+
/** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
|
|
89
|
+
fontFamily?: string
|
|
88
90
|
/** Body text: titles and the default `textStyle`. */
|
|
89
91
|
foreground: string | null
|
|
90
92
|
/** Secondary text: axis labels, legend, subtitles. */
|
|
@@ -93,10 +95,23 @@ interface ChartTokens {
|
|
|
93
95
|
border: string | null
|
|
94
96
|
/** Tooltip surface. */
|
|
95
97
|
popover: string | null
|
|
98
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
99
|
+
colors?: Record<string, string>
|
|
100
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
101
|
+
surface?: string | null
|
|
96
102
|
/** Tooltip text. */
|
|
97
103
|
popoverForeground: string | null
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
107
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
108
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
109
|
+
const OPTION_COLOR_TOKENS = [
|
|
110
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
111
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
112
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
113
|
+
]
|
|
114
|
+
|
|
100
115
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
101
116
|
|
|
102
117
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -152,14 +167,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
152
167
|
const color = read(token)
|
|
153
168
|
if (color) series.push(color)
|
|
154
169
|
}
|
|
155
|
-
|
|
170
|
+
const tokens: ChartTokens = {
|
|
156
171
|
series,
|
|
172
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
173
|
+
surface: readChartSurface(target),
|
|
174
|
+
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
157
175
|
foreground: read("--foreground"),
|
|
158
176
|
mutedForeground: read("--muted-foreground"),
|
|
159
177
|
border: read("--border"),
|
|
160
178
|
popover: read("--popover"),
|
|
161
179
|
popoverForeground: read("--popover-foreground"),
|
|
162
180
|
}
|
|
181
|
+
tokenScopes.set(tokens, target)
|
|
182
|
+
return tokens
|
|
163
183
|
}
|
|
164
184
|
|
|
165
185
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -167,8 +187,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
167
187
|
if (a === b) return true
|
|
168
188
|
if (!a || !b) return false
|
|
169
189
|
return (
|
|
190
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
191
|
+
a.surface === b.surface &&
|
|
192
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
193
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
170
194
|
a.series.length === b.series.length &&
|
|
171
195
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
196
|
+
a.fontFamily === b.fontFamily &&
|
|
172
197
|
a.foreground === b.foreground &&
|
|
173
198
|
a.mutedForeground === b.mutedForeground &&
|
|
174
199
|
a.border === b.border &&
|
|
@@ -191,7 +216,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
191
216
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
192
217
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
193
218
|
*/
|
|
194
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
219
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
195
220
|
if (typeof document === "undefined") return () => {}
|
|
196
221
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
197
222
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -206,8 +231,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
206
231
|
|
|
207
232
|
const themeObserver = new MutationObserver(schedule)
|
|
208
233
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
235
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
236
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
237
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
238
|
+
}
|
|
211
239
|
|
|
212
240
|
const styleObserver = new MutationObserver(schedule)
|
|
213
241
|
if (document.head) {
|
|
@@ -263,7 +291,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
263
291
|
// Let the page background show through: charts sit on cards, not on a canvas.
|
|
264
292
|
backgroundColor: "transparent",
|
|
265
293
|
...(tokens.series.length > 0 ? { color: tokens.series } : {}),
|
|
266
|
-
textStyle: { color: text },
|
|
294
|
+
textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
|
|
267
295
|
title: { textStyle: { color: text }, subtextStyle: { color: muted } },
|
|
268
296
|
legend: { textStyle: { color: muted } },
|
|
269
297
|
categoryAxis: axisTheme(tokens, false),
|
|
@@ -279,7 +307,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
279
307
|
padding: [8, 12],
|
|
280
308
|
// Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
|
|
281
309
|
extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
|
|
282
|
-
textStyle: {
|
|
310
|
+
textStyle: {
|
|
311
|
+
color: tokens.popoverForeground ?? undefined,
|
|
312
|
+
fontSize: 12,
|
|
313
|
+
...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
|
|
314
|
+
},
|
|
283
315
|
axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
|
|
284
316
|
},
|
|
285
317
|
}
|
|
@@ -291,22 +323,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
291
323
|
* components that build an ECharts option from token colours themselves (the
|
|
292
324
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
293
325
|
*/
|
|
294
|
-
function useChartTokens(): ChartTokens | null {
|
|
326
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
295
327
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
296
328
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
297
329
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
298
330
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
299
331
|
const tokensRef = useRef(tokens)
|
|
300
332
|
useEffect(() => {
|
|
333
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
301
334
|
const sync = () => {
|
|
302
|
-
const next = readChartTokens()
|
|
335
|
+
const next = readChartTokens(target)
|
|
303
336
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
304
337
|
tokensRef.current = next
|
|
305
338
|
setTokens(next)
|
|
306
339
|
}
|
|
307
340
|
sync()
|
|
308
|
-
return watchChartTokens(sync)
|
|
309
|
-
}, [])
|
|
341
|
+
return watchChartTokens(sync, target)
|
|
342
|
+
}, [scope])
|
|
310
343
|
return tokens
|
|
311
344
|
}
|
|
312
345
|
|
|
@@ -318,7 +351,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
318
351
|
/**
|
|
319
352
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
320
353
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
321
|
-
* variants `"positive-fill"`, …) are read
|
|
354
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
322
355
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
323
356
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
324
357
|
*/
|
|
@@ -329,12 +362,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
329
362
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
330
363
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
331
364
|
if (typeof document === "undefined") return ""
|
|
332
|
-
|
|
365
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
366
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
333
367
|
}
|
|
334
368
|
if (typeof document === "undefined") return color
|
|
335
369
|
let value = color.trim()
|
|
336
370
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
337
|
-
const styles = getComputedStyle(document.documentElement)
|
|
371
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
338
372
|
if (cssVar) {
|
|
339
373
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
340
374
|
} else if (value.includes("var(")) {
|
|
@@ -380,7 +414,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
380
414
|
if (color) return color
|
|
381
415
|
}
|
|
382
416
|
}
|
|
383
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
417
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
384
418
|
}
|
|
385
419
|
|
|
386
420
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -394,9 +428,10 @@ const WIDTH_BUCKET = 40
|
|
|
394
428
|
*/
|
|
395
429
|
function useChartSurface<E extends Element>(
|
|
396
430
|
forwarded: ForwardedRef<E>,
|
|
397
|
-
tokens
|
|
398
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
431
|
+
tokens?: ChartTokens | null,
|
|
432
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
399
433
|
const [node, setNode] = useState<E | null>(null)
|
|
434
|
+
const scopedTokens = useChartTokens(node)
|
|
400
435
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
401
436
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
402
437
|
const ref = useCallback(
|
|
@@ -410,7 +445,7 @@ function useChartSurface<E extends Element>(
|
|
|
410
445
|
useEffect(() => {
|
|
411
446
|
if (!node) return
|
|
412
447
|
setSurface(readChartSurface(node) ?? undefined)
|
|
413
|
-
}, [node, tokens])
|
|
448
|
+
}, [node, tokens, scopedTokens])
|
|
414
449
|
useEffect(() => {
|
|
415
450
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
416
451
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -419,7 +454,7 @@ function useChartSurface<E extends Element>(
|
|
|
419
454
|
observer.observe(node)
|
|
420
455
|
return () => observer.disconnect()
|
|
421
456
|
}, [node])
|
|
422
|
-
return { ref, surface, width }
|
|
457
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
423
458
|
}
|
|
424
459
|
|
|
425
460
|
/* ---------- end of inlined code ---------- */
|
|
@@ -656,6 +691,128 @@ export function echartSetOptionOpts(prev: EChartsOption | null, next: EChartsOpt
|
|
|
656
691
|
return { notMerge: false, replaceMerge: [...types], lazyUpdate: true }
|
|
657
692
|
}
|
|
658
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Component types that get a generated `id` when the option has none, so replaceMerge can match them across updates.
|
|
696
|
+
* Not `dataset` / `graphic` / `tooltip`: they are rebuilt on every update (there is nothing to transition).
|
|
697
|
+
*/
|
|
698
|
+
const ID_TYPES = [
|
|
699
|
+
"title", "legend", "grid", "xAxis", "yAxis", "polar", "radiusAxis", "angleAxis", "radar",
|
|
700
|
+
"dataZoom", "visualMap", "geo", "singleAxis", "calendar",
|
|
701
|
+
] as const
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* What `withStableIds` remembers between updates: per generated id, the shape last applied and how many
|
|
705
|
+
* times it changed (the id's `~v<n>` suffix); per component type, the order of its ids.
|
|
706
|
+
*/
|
|
707
|
+
export interface StableIdState {
|
|
708
|
+
ids: Map<string, { shape: string; version: number }>
|
|
709
|
+
order: Map<string, string[]>
|
|
710
|
+
}
|
|
711
|
+
export const createStableIdState = (): StableIdState => ({ ids: new Map(), order: new Map() })
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Shape of a component: which keys it has, recursively (arrays only as "array", values not at all —
|
|
715
|
+
* except `type`). A merge keeps keys the new option dropped, so a changed shape must not be merged.
|
|
716
|
+
*/
|
|
717
|
+
function shapeOf(v: unknown): string {
|
|
718
|
+
if (Array.isArray(v)) return "[]"
|
|
719
|
+
if (!v || typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype) return "."
|
|
720
|
+
const o = v as Record<string, unknown>
|
|
721
|
+
return `{${Object.keys(o)
|
|
722
|
+
.filter((k) => o[k] !== undefined)
|
|
723
|
+
.sort()
|
|
724
|
+
.map((k) => (k === "type" || k === "id" ? `${k}=${String(o[k])}` : `${k}:${shapeOf(o[k])}`))
|
|
725
|
+
.join(",")}}`
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Gives every series (and the common components) an `id` it does not have yet —
|
|
730
|
+
* series: `series:<name ?? index>`, others `<type>:<index>` — so an update that
|
|
731
|
+
* only changes values is matched to the existing component and transitions
|
|
732
|
+
* instead of being rebuilt. If the component's shape changed since `state` last
|
|
733
|
+
* saw it (a key was removed or added, the type changed), the id gets a new
|
|
734
|
+
* `~v<n>` suffix so replaceMerge builds it from scratch: the result then always
|
|
735
|
+
* equals applying the option with `notMerge`. Explicit ids are kept; the input is not mutated.
|
|
736
|
+
*/
|
|
737
|
+
export function withStableIds(option: EChartsOption, state: StableIdState = createStableIdState()): EChartsOption {
|
|
738
|
+
const out: Record<string, unknown> = { ...(option as Record<string, unknown>) }
|
|
739
|
+
const taken = new Set<string>()
|
|
740
|
+
const list = (v: unknown): Record<string, unknown>[] | null =>
|
|
741
|
+
Array.isArray(v) ? (v as Record<string, unknown>[]) : v && typeof v === "object" ? [v as Record<string, unknown>] : null
|
|
742
|
+
const give = (key: string, pick: (c: Record<string, unknown>, i: number) => string) => {
|
|
743
|
+
const items = list(out[key])
|
|
744
|
+
if (!items) return
|
|
745
|
+
const used = new Set<string>(items.flatMap((c) => (c && c.id != null ? [String(c.id)] : [])))
|
|
746
|
+
const bases = items.map((c, i) => {
|
|
747
|
+
if (!c || typeof c !== "object" || c.id != null) return null
|
|
748
|
+
let base = pick(c, i)
|
|
749
|
+
for (let n = 2; used.has(base) || taken.has(`${key}\u0000${base}`); n++) base = `${pick(c, i)}#${n}`
|
|
750
|
+
used.add(base)
|
|
751
|
+
taken.add(`${key}\u0000${base}`)
|
|
752
|
+
return base
|
|
753
|
+
})
|
|
754
|
+
// A merge fills the empty slots a removal left and keeps the slot order of what exists, so after any
|
|
755
|
+
// change in which components there are (or their order) it would not match a fresh render: rebuild them all.
|
|
756
|
+
const prevOrder = state.order.get(key) ?? []
|
|
757
|
+
const cur = bases.filter((b): b is string => b !== null)
|
|
758
|
+
const inOrder = cur.length === prevOrder.length && cur.every((b, i) => b === prevOrder[i])
|
|
759
|
+
state.order.set(key, cur)
|
|
760
|
+
out[key] = items.map((c, i) => {
|
|
761
|
+
const base = bases[i]
|
|
762
|
+
if (base == null) return c
|
|
763
|
+
const shape = shapeOf(c)
|
|
764
|
+
const prev = state.ids.get(base)
|
|
765
|
+
const version = !prev ? 0 : prev.shape === shape && inOrder ? prev.version : prev.version + 1
|
|
766
|
+
state.ids.set(base, { shape, version })
|
|
767
|
+
return { ...c, id: version ? `${base}~v${version}` : base }
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
give("series", (c, i) => `series:${typeof c.name === "string" && c.name ? c.name : i}`)
|
|
771
|
+
for (const key of ID_TYPES) give(key, (_c, i) => `${key}:${i}`)
|
|
772
|
+
return out as EChartsOption
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Copy of the plain objects / arrays of an option (functions, class instances, Dates stay shared), to compare against later in-place edits. */
|
|
776
|
+
export function snapshotOption<T>(v: T): T {
|
|
777
|
+
if (Array.isArray(v)) return v.map(snapshotOption) as T
|
|
778
|
+
if (v && typeof v === "object") {
|
|
779
|
+
const proto = Object.getPrototypeOf(v)
|
|
780
|
+
if (proto === Object.prototype || proto === null) {
|
|
781
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, snapshotOption(x)])) as T
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return v
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** Structural equality for option trees; functions and other non-plain objects compare by reference. */
|
|
788
|
+
export function optionEquals(a: unknown, b: unknown): boolean {
|
|
789
|
+
if (Object.is(a, b)) return true
|
|
790
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false
|
|
791
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
792
|
+
if (Array.isArray(a)) {
|
|
793
|
+
const bb = b as unknown[]
|
|
794
|
+
return a.length === bb.length && a.every((v, i) => optionEquals(v, bb[i]))
|
|
795
|
+
}
|
|
796
|
+
if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
|
|
797
|
+
const pa = Object.getPrototypeOf(a)
|
|
798
|
+
if ((pa !== Object.prototype && pa !== null) || Object.getPrototypeOf(b) !== pa) return false
|
|
799
|
+
const ka = Object.keys(a)
|
|
800
|
+
const kb = Object.keys(b)
|
|
801
|
+
return ka.length === kb.length && ka.every((k) => Object.hasOwn(b, k) && optionEquals((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]))
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** `graphic` text doesn't inherit the global `textStyle`, so give it the inherited font unless it sets its own. */
|
|
805
|
+
function withGraphicFont(option: EChartsOption, fontFamily: string | undefined): EChartsOption {
|
|
806
|
+
const graphic = (option as { graphic?: unknown }).graphic
|
|
807
|
+
if (!fontFamily || !Array.isArray(graphic)) return option
|
|
808
|
+
return {
|
|
809
|
+
...option,
|
|
810
|
+
graphic: graphic.map((g: { type?: string; style?: Record<string, unknown> }) =>
|
|
811
|
+
g && g.type === "text" && g.style && !g.style.fontFamily ? { ...g, style: { ...g.style, fontFamily } } : g,
|
|
812
|
+
),
|
|
813
|
+
} as EChartsOption
|
|
814
|
+
}
|
|
815
|
+
|
|
659
816
|
/** `option` as applied: reduced motion forces `animation: false`; otherwise animation stays explicit so it can flip back. */
|
|
660
817
|
export function withMotionPreference(option: EChartsOption, reducedMotion: boolean): EChartsOption {
|
|
661
818
|
return { ...option, animation: reducedMotion ? false : (option.animation ?? true) }
|
|
@@ -697,29 +854,13 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
697
854
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
698
855
|
const [instance, setInstance] = useState<ECharts | null>(null)
|
|
699
856
|
const [copied, setCopied] = useState(false)
|
|
700
|
-
|
|
701
|
-
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
857
|
+
const tokens = useChartTokens(containerRef)
|
|
702
858
|
const reducedMotion = usePrefersReducedMotion()
|
|
703
859
|
// What the current instance last received, to pick the merge mode of the next update.
|
|
704
860
|
const appliedRef = useRef<EChartsOption | null>(null)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
// ないときは state を更新せず、再 init を防ぐ。比較は updater 関数の中ではなく ref で行う:
|
|
709
|
-
// DOM を読む updater は React に何度も再生され、そのたびに新しいオブジェクトを返して
|
|
710
|
-
// 再描画が止まらなくなる(変数を後から当てた直後に Maximum update depth exceeded)。
|
|
711
|
-
const tokensRef = useRef(tokens)
|
|
712
|
-
useEffect(() => {
|
|
713
|
-
if (theme !== undefined) return
|
|
714
|
-
const sync = () => {
|
|
715
|
-
const next = readChartTokens()
|
|
716
|
-
if (sameChartTokens(tokensRef.current, next)) return
|
|
717
|
-
tokensRef.current = next
|
|
718
|
-
setTokens(next)
|
|
719
|
-
}
|
|
720
|
-
sync()
|
|
721
|
-
return watchChartTokens(sync)
|
|
722
|
-
}, [theme])
|
|
861
|
+
// The option (before ids) as it was when applied, deep-copied; and the shape history behind the generated ids.
|
|
862
|
+
const snapshotRef = useRef<EChartsOption | null>(null)
|
|
863
|
+
const idStateRef = useRef<StableIdState>(createStableIdState())
|
|
723
864
|
|
|
724
865
|
const autoTheme = useMemo(() => (tokens ? buildChartTheme(tokens) : undefined), [tokens])
|
|
725
866
|
const resolvedTheme = theme ?? autoTheme
|
|
@@ -731,6 +872,7 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
731
872
|
if (!container) return
|
|
732
873
|
const inst = init(container, resolvedTheme, { renderer: "canvas" })
|
|
733
874
|
appliedRef.current = null
|
|
875
|
+
snapshotRef.current = null
|
|
734
876
|
// `data-rendering` stays on until the (animated) paint is done, so anything
|
|
735
877
|
// capturing the page — printing a document — can wait for real pixels.
|
|
736
878
|
container.dataset.rendering = ""
|
|
@@ -757,21 +899,29 @@ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
|
|
|
757
899
|
}, [resolvedTheme])
|
|
758
900
|
|
|
759
901
|
useEffect(() => {
|
|
760
|
-
if (!instance) return
|
|
902
|
+
if (!instance || instance.isDisposed()) return
|
|
903
|
+
const prepared = withGraphicFont(
|
|
904
|
+
withMotionPreference(option, reducedMotion),
|
|
905
|
+
theme === undefined ? tokens?.fontFamily : undefined,
|
|
906
|
+
)
|
|
907
|
+
// Same content as last time (a re-render that built an equal option): nothing to redraw.
|
|
908
|
+
// Compared with a snapshot, so an in-place edit of a nested array of the caller's option still counts as a change.
|
|
909
|
+
if (snapshotRef.current && optionEquals(snapshotRef.current, prepared)) return
|
|
761
910
|
if (containerRef.current) containerRef.current.dataset.rendering = ""
|
|
762
|
-
const next =
|
|
911
|
+
const next = withStableIds(prepared, idStateRef.current)
|
|
763
912
|
instance.setOption(next, echartSetOptionOpts(appliedRef.current, next))
|
|
764
913
|
appliedRef.current = next
|
|
765
|
-
|
|
914
|
+
snapshotRef.current = snapshotOption(prepared)
|
|
915
|
+
}, [instance, option, reducedMotion, theme, tokens?.fontFamily])
|
|
766
916
|
|
|
767
917
|
useEffect(() => {
|
|
768
|
-
if (!instance) return
|
|
918
|
+
if (!instance || instance.isDisposed()) return
|
|
769
919
|
if (loading) instance.showLoading()
|
|
770
920
|
else instance.hideLoading()
|
|
771
921
|
}, [instance, loading])
|
|
772
922
|
|
|
773
923
|
useEffect(() => {
|
|
774
|
-
if (!instance || !onEvents) return
|
|
924
|
+
if (!instance || instance.isDisposed() || !onEvents) return
|
|
775
925
|
const entries = Object.entries(onEvents)
|
|
776
926
|
for (const [event, handler] of entries) {
|
|
777
927
|
instance.on(event, handler as EChartsEventHandler)
|
|
@@ -1190,6 +1190,10 @@ const toDate = (t: ParsedTime) => new Date(t.year, t.month - 1, t.day)
|
|
|
1190
1190
|
|
|
1191
1191
|
/** How many x labels a time axis shows at most (the step is picked so the labels never crowd). */
|
|
1192
1192
|
const TIME_AXIS_MAX_LABELS = 6
|
|
1193
|
+
/** Under this plot width (px) the axis, legend and labels switch to their compact form. */
|
|
1194
|
+
const NARROW_PLOT_WIDTH = 400
|
|
1195
|
+
/** The most x labels a narrow plot shows (a 262px plot reads 4 at most). */
|
|
1196
|
+
const NARROW_TIME_AXIS_MAX_LABELS = 4
|
|
1193
1197
|
|
|
1194
1198
|
/** Calendar-friendly label steps per granularity (every 3 months, every week …). */
|
|
1195
1199
|
const TIME_AXIS_STEPS: Record<TimeGranularity, readonly number[]> = {
|
|
@@ -1635,7 +1639,9 @@ const LEGEND_LINE_BUDGET = 320
|
|
|
1635
1639
|
/** How many lines the plain legend takes, estimated from its labels (12px text + 8px dot + gaps). */
|
|
1636
1640
|
function legendLines(items: readonly string[], plotWidth?: number, reserve = 0): number {
|
|
1637
1641
|
const width = items.reduce((sum, name) => sum + [...name].length * 12 + 8 + 5 + 16, 0)
|
|
1638
|
-
|
|
1642
|
+
// A narrow plot has no room for the PNG buttons beside the legend: they move off its line, so no reserve.
|
|
1643
|
+
const narrow = plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
1644
|
+
const budget = Math.max(80, (plotWidth ? (narrow ? plotWidth : Math.min(LEGEND_LINE_BUDGET, plotWidth)) : LEGEND_LINE_BUDGET) - (narrow ? 0 : reserve))
|
|
1639
1645
|
return Math.min(3, Math.max(1, Math.ceil(width / budget)))
|
|
1640
1646
|
}
|
|
1641
1647
|
|
|
@@ -1890,7 +1896,15 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1890
1896
|
? null
|
|
1891
1897
|
: timeAxisLabelStep(
|
|
1892
1898
|
rawIndex,
|
|
1893
|
-
plotWidth
|
|
1899
|
+
plotWidth
|
|
1900
|
+
? Math.max(
|
|
1901
|
+
2,
|
|
1902
|
+
Math.min(
|
|
1903
|
+
plotWidth < NARROW_PLOT_WIDTH ? NARROW_TIME_AXIS_MAX_LABELS : TIME_AXIS_MAX_LABELS,
|
|
1904
|
+
Math.floor(plotWidth / TIME_LABEL_WIDTH),
|
|
1905
|
+
),
|
|
1906
|
+
)
|
|
1907
|
+
: undefined,
|
|
1894
1908
|
)
|
|
1895
1909
|
// Plain (non-date) x labels of a line chart: thinned to every n-th so they never touch, never rotated.
|
|
1896
1910
|
const plainStep = (() => {
|
|
@@ -1936,8 +1950,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1936
1950
|
: []
|
|
1937
1951
|
// On a narrow plot the max and the latest label would print over each other: keep the latest.
|
|
1938
1952
|
const pointGap = plotWidth && labels.length > 1 ? plotWidth / (kind === "bar" ? labels.length : labels.length - 1) : null
|
|
1953
|
+
// By default only the latest value is labelled; an explicit `highlight` keeps its max + latest.
|
|
1954
|
+
const latestOnly = cfg.highlight === undefined && !shareHighlight && picked.length === 2
|
|
1939
1955
|
const highlightPoints =
|
|
1940
|
-
|
|
1956
|
+
latestOnly
|
|
1957
|
+
? [picked[1]!]
|
|
1958
|
+
: picked.length === 2 && pointGap !== null && Math.abs(picked[1]!.index - picked[0]!.index) * pointGap < HIGHLIGHT_LABEL_WIDTH
|
|
1941
1959
|
? [picked[1]!]
|
|
1942
1960
|
: picked
|
|
1943
1961
|
|
|
@@ -2148,6 +2166,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2148
2166
|
? { width: 0, color, opacity: 0 }
|
|
2149
2167
|
: { width: 2, color, opacity: dimSeries ? DIMMED : 1 },
|
|
2150
2168
|
itemStyle: { color, opacity: dimSeries ? DIMMED : 1 },
|
|
2169
|
+
// Hovering a series fades the others (not for a stack, where the bands read together).
|
|
2170
|
+
emphasis: stacked ? undefined : { focus: "series" as const },
|
|
2151
2171
|
areaStyle: areaStyle && dimSeries ? { ...areaStyle, opacity: DIMMED * 0.3 } : areaStyle,
|
|
2152
2172
|
markPoint: markPointFor(c, color),
|
|
2153
2173
|
markLine: undefined,
|
|
@@ -2594,7 +2614,10 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2594
2614
|
legend: {
|
|
2595
2615
|
show: showLegend,
|
|
2596
2616
|
top: 0,
|
|
2597
|
-
|
|
2617
|
+
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
2618
|
+
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2619
|
+
? { left: 0, right: 0 }
|
|
2620
|
+
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2598
2621
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2599
2622
|
type: "plain",
|
|
2600
2623
|
icon: "circle",
|
|
@@ -2655,7 +2678,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2655
2678
|
}
|
|
2656
2679
|
}
|
|
2657
2680
|
const shares = percent ? cartesianSeriesValues(i <= lastActual ? [row] : [], categories, "percent") : null
|
|
2658
|
-
|
|
2681
|
+
// Rows follow the legend order (categories first), not ECharts' own series order.
|
|
2682
|
+
const legendRank = (p: AxisTooltipParam) => {
|
|
2683
|
+
const at = categories.indexOf(p.seriesName ?? "")
|
|
2684
|
+
return at === -1 ? categories.length : at
|
|
2685
|
+
}
|
|
2686
|
+
for (const p of [...list].sort((a, b) => legendRank(a) - legendRank(b))) {
|
|
2659
2687
|
const color = typeof p.color === "string" ? p.color : "currentColor"
|
|
2660
2688
|
if (p.seriesId === FORECAST_SERIES_ID) {
|
|
2661
2689
|
// At and before the boundary the forecast repeats the actual value.
|