@squadbase/vantage 0.6.2-dev1.d594c56 → 0.6.2-dev1.e85faec
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/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-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-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/components/index.d.ts +11 -247
- package/dist/components/index.js +5 -2
- package/dist/delta-text-CvwBGJ95.d.ts +15 -0
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +300 -3
- package/dist/doc/index.js.map +1 -1
- package/dist/format-U0cQl069.d.ts +243 -0
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +966 -1
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +5 -2
- 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/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/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/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/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-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +2 -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 +16 -15
- package/dist/chunk-HKSVYMYJ.js.map +0 -1
- package/dist/chunk-XPQSTZUU.js.map +0 -1
package/registry/ui/kpi-card.tsx
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* line takes its name from it ("昨日 617"), the same way a `label` on any `vs` does.
|
|
44
44
|
* - **Two or more comparisons sit under the value**, not beside it, so the value
|
|
45
45
|
* keeps its place and its width whatever the card holds.
|
|
46
|
-
* -
|
|
46
|
+
* - **`betterWhen` drives everything.** With `betterWhen="down"` the target
|
|
47
47
|
* is met at or below it, and the record badge marks the lowest value.
|
|
48
48
|
* - **Rates change in points.** With a `percent` format the delta and
|
|
49
49
|
* `{difference}` are in percentage points ("-0.5pt"); `change="relative"` opts
|
|
@@ -147,7 +147,9 @@ export interface KpiCardProps extends Omit<ComponentProps<"div">, "children"> {
|
|
|
147
147
|
* @example provisional / provisional={{ label: "暫定" }}
|
|
148
148
|
*/
|
|
149
149
|
provisional?: boolean | { label?: string }
|
|
150
|
-
/**
|
|
150
|
+
/** Which direction is good: `"up"` (default) or `"down"` (churn, error rate). Wins over `polarity`. */
|
|
151
|
+
betterWhen?: "up" | "down"
|
|
152
|
+
/** @deprecated Older name of `betterWhen` (`"higher-is-better"` / `"lower-is-better"`). `"neutral"` (never colours the change, skips the target and record logic) is only reachable here. `betterWhen` wins if both are given. */
|
|
151
153
|
polarity?: Polarity
|
|
152
154
|
/** `"relative"` ("+12.4%"), `"points"` ("-0.5pt") or `"difference"` ("+¥530万", in `format`). Default: points for a `percent` format, else relative. */
|
|
153
155
|
change?: ChangeDisplay
|
|
@@ -160,7 +162,7 @@ export interface KpiCardProps extends Omit<ComponentProps<"div">, "children"> {
|
|
|
160
162
|
/**
|
|
161
163
|
* Target for the current period. Under the value it adds a 6px bar (actual, the pace
|
|
162
164
|
* projection, a tick at the target) and one line "目標の 49%・このペースなら 98%"
|
|
163
|
-
* (the projection only while `period` is in progress). With `
|
|
165
|
+
* (the projection only while `period` is in progress). With `betterWhen="down"`
|
|
164
166
|
* or a rate target (points) there is no bar: "目標 … まであと …" in the footer.
|
|
165
167
|
*/
|
|
166
168
|
target?: number
|
|
@@ -235,6 +237,11 @@ const DELTA_PLACEMENT =
|
|
|
235
237
|
* (32px; 24px for `sm`), so 218px / 288px here are the 250px / 320px card widths. It also owns the gap above the footer.
|
|
236
238
|
*/
|
|
237
239
|
const HEADER_CLASS = "@container/card pb-(--card-spacing)"
|
|
240
|
+
/** A value this long (a huge standard-notation currency) may wrap between digit groups, and its delta then moves under it so the number gets the full row. Ordinary values stay on one line beside the delta. */
|
|
241
|
+
const LONG_VALUE_CHARS = 10
|
|
242
|
+
const DELTA_UNDER_PLACEMENT = "col-start-1 row-span-1 row-start-3 justify-self-start"
|
|
243
|
+
// No grey footer band: value, comparison and notes share one surface, 8px apart (the footer only keeps its subgrid row).
|
|
244
|
+
const KPI_FOOTER_CLASS = "flex-col items-start gap-2 rounded-none border-t-0 bg-transparent pt-0 text-sm"
|
|
238
245
|
|
|
239
246
|
const PLACEHOLDER = /\{(\w+)\}/g
|
|
240
247
|
|
|
@@ -307,6 +314,11 @@ function warnShallowSummary(template: string) {
|
|
|
307
314
|
)
|
|
308
315
|
}
|
|
309
316
|
|
|
317
|
+
/** Splits formatted text after each thousands separator so a huge standard-notation value wraps between digit groups instead of clipping. The text itself is unchanged. */
|
|
318
|
+
function digitGroups(text: string): string[] {
|
|
319
|
+
return text.match(/[^,,]*[,,]|[^,,]+$/g) ?? [text]
|
|
320
|
+
}
|
|
321
|
+
|
|
310
322
|
export function KpiCard({
|
|
311
323
|
label,
|
|
312
324
|
value,
|
|
@@ -315,7 +327,8 @@ export function KpiCard({
|
|
|
315
327
|
comparison: comparisonProp,
|
|
316
328
|
comparisons,
|
|
317
329
|
provisional,
|
|
318
|
-
|
|
330
|
+
betterWhen,
|
|
331
|
+
polarity: polarityProp,
|
|
319
332
|
change,
|
|
320
333
|
neutralBand,
|
|
321
334
|
showPrevious = true,
|
|
@@ -342,6 +355,8 @@ export function KpiCard({
|
|
|
342
355
|
const isProvisional = provisional !== undefined && provisional !== false
|
|
343
356
|
const provisionalLabel = typeof provisional === "object" && provisional.label ? provisional.label : "速報"
|
|
344
357
|
const sample = useSampleMark()
|
|
358
|
+
// `betterWhen` is the documented name and wins over the older `polarity`.
|
|
359
|
+
const polarity: Polarity | undefined = betterWhen ? (betterWhen === "down" ? "lower-is-better" : "higher-is-better") : polarityProp
|
|
345
360
|
const neutral = polarity === "neutral"
|
|
346
361
|
const hasPrevious = comparison !== undefined && showPrevious
|
|
347
362
|
// Two or more badges would squeeze the value from the right; they get their own row under it instead.
|
|
@@ -417,7 +432,7 @@ export function KpiCard({
|
|
|
417
432
|
) : null}
|
|
418
433
|
</CardHeader>
|
|
419
434
|
{(summary || record || (hasTarget && !targetBar) || sourceParts.length > 0 || hasUpdated || showSparkline) && (
|
|
420
|
-
<CardFooter className=
|
|
435
|
+
<CardFooter className={KPI_FOOTER_CLASS}>
|
|
421
436
|
{summary && <Skeleton aria-hidden="true" className="h-5 w-4/5" />}
|
|
422
437
|
{record && <Skeleton aria-hidden="true" className="h-5 w-28 rounded-4xl" />}
|
|
423
438
|
{hasTarget && !targetBar && <Skeleton aria-hidden="true" className="h-5 w-3/5" />}
|
|
@@ -535,22 +550,34 @@ export function KpiCard({
|
|
|
535
550
|
/>
|
|
536
551
|
))
|
|
537
552
|
|
|
553
|
+
const valueText = fmt(value)
|
|
554
|
+
const longValue = valueText.length > LONG_VALUE_CHARS
|
|
555
|
+
|
|
538
556
|
return (
|
|
539
557
|
<Card data-slot="kpi-card" size={size} className={cardClass} {...props}>
|
|
540
558
|
<CardHeader className={HEADER_CLASS}>
|
|
541
|
-
<CardDescription data-slot="kpi-card-label" title={label} className="col-start-1 min-w-0 truncate text-[13px] font-medium text-muted-foreground">
|
|
559
|
+
<CardDescription data-slot="kpi-card-label" title={label} className="col-start-1 min-w-0 truncate text-[13px] leading-snug font-medium text-muted-foreground">
|
|
542
560
|
{label}
|
|
543
561
|
</CardDescription>
|
|
544
562
|
<CardTitle
|
|
545
563
|
data-slot="kpi-card-value"
|
|
546
|
-
title={
|
|
564
|
+
title={valueText}
|
|
547
565
|
className={cn(
|
|
548
|
-
"col-start-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 font-semibold tracking-tight tabular-nums",
|
|
566
|
+
"col-start-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 leading-none font-semibold tracking-tight tabular-nums",
|
|
549
567
|
// Fixed size: the step follows the card width only, never the length of the value.
|
|
550
568
|
valueSize,
|
|
551
569
|
)}
|
|
552
570
|
>
|
|
553
|
-
<span className="whitespace-nowrap"
|
|
571
|
+
<span data-slot="kpi-card-value-text" className={longValue ? "min-w-0 break-words" : "whitespace-nowrap"}>
|
|
572
|
+
{longValue
|
|
573
|
+
? digitGroups(valueText).map((group, i) => (
|
|
574
|
+
<span key={i} className="whitespace-nowrap">
|
|
575
|
+
{group}
|
|
576
|
+
<wbr />
|
|
577
|
+
</span>
|
|
578
|
+
))
|
|
579
|
+
: valueText}
|
|
580
|
+
</span>
|
|
554
581
|
{isProvisional && (
|
|
555
582
|
<Badge data-slot="kpi-card-provisional" variant="outline" className="align-middle">
|
|
556
583
|
{provisionalLabel}
|
|
@@ -564,7 +591,7 @@ export function KpiCard({
|
|
|
564
591
|
{deltas}
|
|
565
592
|
</div>
|
|
566
593
|
) : (
|
|
567
|
-
<CardAction className={DELTA_PLACEMENT}>{deltas}</CardAction>
|
|
594
|
+
<CardAction className={longValue ? DELTA_UNDER_PLACEMENT : DELTA_PLACEMENT}>{deltas}</CardAction>
|
|
568
595
|
))}
|
|
569
596
|
{hasPrevious && comparison && (
|
|
570
597
|
<div data-slot="kpi-card-previous" className="col-span-full text-sm text-muted-foreground tabular-nums">
|
|
@@ -614,9 +641,9 @@ export function KpiCard({
|
|
|
614
641
|
)}
|
|
615
642
|
</CardHeader>
|
|
616
643
|
{hasFooter && (
|
|
617
|
-
<CardFooter className=
|
|
644
|
+
<CardFooter className={KPI_FOOTER_CLASS}>
|
|
618
645
|
{summaryText && (
|
|
619
|
-
<p data-slot="kpi-card-summary" title={summaryText} className="line-clamp-2
|
|
646
|
+
<p data-slot="kpi-card-summary" title={summaryText} className="line-clamp-2 text-sm leading-relaxed text-muted-foreground tabular-nums">
|
|
620
647
|
{summaryText}
|
|
621
648
|
</p>
|
|
622
649
|
)}
|
|
@@ -634,13 +661,13 @@ export function KpiCard({
|
|
|
634
661
|
<p
|
|
635
662
|
data-slot="kpi-card-source"
|
|
636
663
|
title={sourceParts.join("・")}
|
|
637
|
-
className="line-clamp-2 w-full text-xs text-muted-foreground tabular-nums"
|
|
664
|
+
className="line-clamp-2 w-full text-xs leading-normal text-muted-foreground tabular-nums"
|
|
638
665
|
>
|
|
639
666
|
{sourceParts.join("・")}
|
|
640
667
|
</p>
|
|
641
668
|
)}
|
|
642
669
|
{hasUpdated && (
|
|
643
|
-
<p data-slot="kpi-card-updated" className="text-xs text-muted-foreground">
|
|
670
|
+
<p data-slot="kpi-card-updated" className="text-xs leading-normal text-muted-foreground">
|
|
644
671
|
{updatedAtText(updatedAt, updatedAtDisplay, locale, now, timeZone)}
|
|
645
672
|
</p>
|
|
646
673
|
)}
|
package/registry/ui/legend.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { forwardRef } from "react"
|
|
|
17
17
|
|
|
18
18
|
import { cn } from "@squadbase/vantage/ui"
|
|
19
19
|
import type { ChartSeriesColor } from "@squadbase/vantage/components"
|
|
20
|
-
import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef } from "react"
|
|
20
|
+
import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
|
|
21
21
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
22
22
|
import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
23
23
|
import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
|
|
@@ -50,6 +50,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
|
|
|
50
50
|
interface ChartTokens {
|
|
51
51
|
/** Resolved `--chart-1..5`, in order. */
|
|
52
52
|
series: string[]
|
|
53
|
+
/** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
|
|
54
|
+
fontFamily?: string
|
|
53
55
|
/** Body text: titles and the default `textStyle`. */
|
|
54
56
|
foreground: string | null
|
|
55
57
|
/** Secondary text: axis labels, legend, subtitles. */
|
|
@@ -58,10 +60,23 @@ interface ChartTokens {
|
|
|
58
60
|
border: string | null
|
|
59
61
|
/** Tooltip surface. */
|
|
60
62
|
popover: string | null
|
|
63
|
+
/** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
|
|
64
|
+
colors?: Record<string, string>
|
|
65
|
+
/** Actual inherited painting surface (may change without a token changing). */
|
|
66
|
+
surface?: string | null
|
|
61
67
|
/** Tooltip text. */
|
|
62
68
|
popoverForeground: string | null
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
// Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
|
|
72
|
+
// var(--token) expressions must be read, without retaining unmounted charts globally.
|
|
73
|
+
const tokenScopes = new WeakMap<ChartTokens, Element>()
|
|
74
|
+
const OPTION_COLOR_TOKENS = [
|
|
75
|
+
"--background", "--card", "--muted", "--primary", "--destructive",
|
|
76
|
+
...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
|
|
77
|
+
["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
|
|
78
|
+
]
|
|
79
|
+
|
|
65
80
|
let normalizeContext: CanvasRenderingContext2D | null | undefined
|
|
66
81
|
|
|
67
82
|
/** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
|
|
@@ -117,14 +132,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
|
|
|
117
132
|
const color = read(token)
|
|
118
133
|
if (color) series.push(color)
|
|
119
134
|
}
|
|
120
|
-
|
|
135
|
+
const tokens: ChartTokens = {
|
|
121
136
|
series,
|
|
137
|
+
colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
|
|
138
|
+
surface: readChartSurface(target),
|
|
139
|
+
...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
|
|
122
140
|
foreground: read("--foreground"),
|
|
123
141
|
mutedForeground: read("--muted-foreground"),
|
|
124
142
|
border: read("--border"),
|
|
125
143
|
popover: read("--popover"),
|
|
126
144
|
popoverForeground: read("--popover-foreground"),
|
|
127
145
|
}
|
|
146
|
+
tokenScopes.set(tokens, target)
|
|
147
|
+
return tokens
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
/** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
|
|
@@ -132,8 +152,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
132
152
|
if (a === b) return true
|
|
133
153
|
if (!a || !b) return false
|
|
134
154
|
return (
|
|
155
|
+
tokenScopes.get(a) === tokenScopes.get(b) &&
|
|
156
|
+
a.surface === b.surface &&
|
|
157
|
+
Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
|
|
158
|
+
Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
|
|
135
159
|
a.series.length === b.series.length &&
|
|
136
160
|
a.series.every((color, i) => color === b.series[i]) &&
|
|
161
|
+
a.fontFamily === b.fontFamily &&
|
|
137
162
|
a.foreground === b.foreground &&
|
|
138
163
|
a.mutedForeground === b.mutedForeground &&
|
|
139
164
|
a.border === b.border &&
|
|
@@ -156,7 +181,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
|
|
|
156
181
|
* attributes alone leaves the chart painted in the previous palette until a
|
|
157
182
|
* reload — the tokens in the DOM are new, the canvas is not.
|
|
158
183
|
*/
|
|
159
|
-
function watchChartTokens(onChange: () => void): () => void {
|
|
184
|
+
function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
|
|
160
185
|
if (typeof document === "undefined") return () => {}
|
|
161
186
|
// Style edits arrive as bursts of mutations; coalesce them into one read, and
|
|
162
187
|
// take it after the next frame so a swapped stylesheet has been applied.
|
|
@@ -171,8 +196,11 @@ function watchChartTokens(onChange: () => void): () => void {
|
|
|
171
196
|
|
|
172
197
|
const themeObserver = new MutationObserver(schedule)
|
|
173
198
|
const attributeFilter = ["class", "style", "data-theme"]
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
// Only the chart and its ancestors can change its inherited theme. Never observe
|
|
200
|
+
// the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
|
|
201
|
+
for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
|
|
202
|
+
themeObserver.observe(node, { attributes: true, attributeFilter })
|
|
203
|
+
}
|
|
176
204
|
|
|
177
205
|
const styleObserver = new MutationObserver(schedule)
|
|
178
206
|
if (document.head) {
|
|
@@ -228,7 +256,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
228
256
|
// Let the page background show through: charts sit on cards, not on a canvas.
|
|
229
257
|
backgroundColor: "transparent",
|
|
230
258
|
...(tokens.series.length > 0 ? { color: tokens.series } : {}),
|
|
231
|
-
textStyle: { color: text },
|
|
259
|
+
textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
|
|
232
260
|
title: { textStyle: { color: text }, subtextStyle: { color: muted } },
|
|
233
261
|
legend: { textStyle: { color: muted } },
|
|
234
262
|
categoryAxis: axisTheme(tokens, false),
|
|
@@ -244,7 +272,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
244
272
|
padding: [8, 12],
|
|
245
273
|
// Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
|
|
246
274
|
extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
|
|
247
|
-
textStyle: {
|
|
275
|
+
textStyle: {
|
|
276
|
+
color: tokens.popoverForeground ?? undefined,
|
|
277
|
+
fontSize: 12,
|
|
278
|
+
...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
|
|
279
|
+
},
|
|
248
280
|
axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
|
|
249
281
|
},
|
|
250
282
|
}
|
|
@@ -256,22 +288,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
256
288
|
* components that build an ECharts option from token colours themselves (the
|
|
257
289
|
* typed charts), so their palette follows light/dark like `EChart`'s theme.
|
|
258
290
|
*/
|
|
259
|
-
function useChartTokens(): ChartTokens | null {
|
|
291
|
+
function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
|
|
260
292
|
const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
|
|
261
293
|
// Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
|
|
262
294
|
// replayed by React on every rebase and returns a fresh object each time, so the state
|
|
263
295
|
// never settles ("Maximum update depth exceeded" once the tokens change after mount).
|
|
264
296
|
const tokensRef = useRef(tokens)
|
|
265
297
|
useEffect(() => {
|
|
298
|
+
const target = scope && "current" in scope ? scope.current : scope
|
|
266
299
|
const sync = () => {
|
|
267
|
-
const next = readChartTokens()
|
|
300
|
+
const next = readChartTokens(target)
|
|
268
301
|
if (sameChartTokens(tokensRef.current, next)) return
|
|
269
302
|
tokensRef.current = next
|
|
270
303
|
setTokens(next)
|
|
271
304
|
}
|
|
272
305
|
sync()
|
|
273
|
-
return watchChartTokens(sync)
|
|
274
|
-
}, [])
|
|
306
|
+
return watchChartTokens(sync, target)
|
|
307
|
+
}, [scope])
|
|
275
308
|
return tokens
|
|
276
309
|
}
|
|
277
310
|
|
|
@@ -283,7 +316,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
|
|
|
283
316
|
/**
|
|
284
317
|
* Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
|
|
285
318
|
* the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
|
|
286
|
-
* variants `"positive-fill"`, …) are read
|
|
319
|
+
* variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
|
|
287
320
|
* other CSS colour is normalized like the tokens are. A colour that can't be
|
|
288
321
|
* resolved comes back as `""`, which ECharts treats as "use the palette".
|
|
289
322
|
*/
|
|
@@ -294,12 +327,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
|
|
|
294
327
|
// Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
|
|
295
328
|
if (TONE_NAME_RE.test(color.trim())) {
|
|
296
329
|
if (typeof document === "undefined") return ""
|
|
297
|
-
|
|
330
|
+
const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
|
|
331
|
+
return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
|
|
298
332
|
}
|
|
299
333
|
if (typeof document === "undefined") return color
|
|
300
334
|
let value = color.trim()
|
|
301
335
|
const cssVar = CSS_VAR_RE.exec(value)
|
|
302
|
-
const styles = getComputedStyle(document.documentElement)
|
|
336
|
+
const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
|
|
303
337
|
if (cssVar) {
|
|
304
338
|
value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
|
|
305
339
|
} else if (value.includes("var(")) {
|
|
@@ -345,7 +379,7 @@ function readChartSurface(el: Element | null): string | null {
|
|
|
345
379
|
if (color) return color
|
|
346
380
|
}
|
|
347
381
|
}
|
|
348
|
-
return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
|
|
382
|
+
return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
|
|
349
383
|
}
|
|
350
384
|
|
|
351
385
|
/** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
|
|
@@ -359,9 +393,10 @@ const WIDTH_BUCKET = 40
|
|
|
359
393
|
*/
|
|
360
394
|
function useChartSurface<E extends Element>(
|
|
361
395
|
forwarded: ForwardedRef<E>,
|
|
362
|
-
tokens
|
|
363
|
-
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
|
|
396
|
+
tokens?: ChartTokens | null,
|
|
397
|
+
): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
|
|
364
398
|
const [node, setNode] = useState<E | null>(null)
|
|
399
|
+
const scopedTokens = useChartTokens(node)
|
|
365
400
|
const [surface, setSurface] = useState<string | undefined>(undefined)
|
|
366
401
|
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
367
402
|
const ref = useCallback(
|
|
@@ -375,7 +410,7 @@ function useChartSurface<E extends Element>(
|
|
|
375
410
|
useEffect(() => {
|
|
376
411
|
if (!node) return
|
|
377
412
|
setSurface(readChartSurface(node) ?? undefined)
|
|
378
|
-
}, [node, tokens])
|
|
413
|
+
}, [node, tokens, scopedTokens])
|
|
379
414
|
useEffect(() => {
|
|
380
415
|
if (!node || typeof ResizeObserver === "undefined") return
|
|
381
416
|
const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
|
|
@@ -384,7 +419,7 @@ function useChartSurface<E extends Element>(
|
|
|
384
419
|
observer.observe(node)
|
|
385
420
|
return () => observer.disconnect()
|
|
386
421
|
}, [node])
|
|
387
|
-
return { ref, surface, width }
|
|
422
|
+
return { ref, surface, width, tokens: scopedTokens }
|
|
388
423
|
}
|
|
389
424
|
|
|
390
425
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
|
|
@@ -1538,6 +1573,10 @@ const toDate = (t: ParsedTime) => new Date(t.year, t.month - 1, t.day)
|
|
|
1538
1573
|
|
|
1539
1574
|
/** How many x labels a time axis shows at most (the step is picked so the labels never crowd). */
|
|
1540
1575
|
const TIME_AXIS_MAX_LABELS = 6
|
|
1576
|
+
/** Under this plot width (px) the axis, legend and labels switch to their compact form. */
|
|
1577
|
+
const NARROW_PLOT_WIDTH = 400
|
|
1578
|
+
/** The most x labels a narrow plot shows (a 262px plot reads 4 at most). */
|
|
1579
|
+
const NARROW_TIME_AXIS_MAX_LABELS = 4
|
|
1541
1580
|
|
|
1542
1581
|
/** Calendar-friendly label steps per granularity (every 3 months, every week …). */
|
|
1543
1582
|
const TIME_AXIS_STEPS: Record<TimeGranularity, readonly number[]> = {
|
|
@@ -1983,7 +2022,9 @@ const LEGEND_LINE_BUDGET = 320
|
|
|
1983
2022
|
/** How many lines the plain legend takes, estimated from its labels (12px text + 8px dot + gaps). */
|
|
1984
2023
|
function legendLines(items: readonly string[], plotWidth?: number, reserve = 0): number {
|
|
1985
2024
|
const width = items.reduce((sum, name) => sum + [...name].length * 12 + 8 + 5 + 16, 0)
|
|
1986
|
-
|
|
2025
|
+
// A narrow plot has no room for the PNG buttons beside the legend: they move off its line, so no reserve.
|
|
2026
|
+
const narrow = plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2027
|
+
const budget = Math.max(80, (plotWidth ? (narrow ? plotWidth : Math.min(LEGEND_LINE_BUDGET, plotWidth)) : LEGEND_LINE_BUDGET) - (narrow ? 0 : reserve))
|
|
1987
2028
|
return Math.min(3, Math.max(1, Math.ceil(width / budget)))
|
|
1988
2029
|
}
|
|
1989
2030
|
|
|
@@ -2238,7 +2279,15 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2238
2279
|
? null
|
|
2239
2280
|
: timeAxisLabelStep(
|
|
2240
2281
|
rawIndex,
|
|
2241
|
-
plotWidth
|
|
2282
|
+
plotWidth
|
|
2283
|
+
? Math.max(
|
|
2284
|
+
2,
|
|
2285
|
+
Math.min(
|
|
2286
|
+
plotWidth < NARROW_PLOT_WIDTH ? NARROW_TIME_AXIS_MAX_LABELS : TIME_AXIS_MAX_LABELS,
|
|
2287
|
+
Math.floor(plotWidth / TIME_LABEL_WIDTH),
|
|
2288
|
+
),
|
|
2289
|
+
)
|
|
2290
|
+
: undefined,
|
|
2242
2291
|
)
|
|
2243
2292
|
// Plain (non-date) x labels of a line chart: thinned to every n-th so they never touch, never rotated.
|
|
2244
2293
|
const plainStep = (() => {
|
|
@@ -2284,8 +2333,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2284
2333
|
: []
|
|
2285
2334
|
// On a narrow plot the max and the latest label would print over each other: keep the latest.
|
|
2286
2335
|
const pointGap = plotWidth && labels.length > 1 ? plotWidth / (kind === "bar" ? labels.length : labels.length - 1) : null
|
|
2336
|
+
// By default only the latest value is labelled; an explicit `highlight` keeps its max + latest.
|
|
2337
|
+
const latestOnly = cfg.highlight === undefined && !shareHighlight && picked.length === 2
|
|
2287
2338
|
const highlightPoints =
|
|
2288
|
-
|
|
2339
|
+
latestOnly
|
|
2340
|
+
? [picked[1]!]
|
|
2341
|
+
: picked.length === 2 && pointGap !== null && Math.abs(picked[1]!.index - picked[0]!.index) * pointGap < HIGHLIGHT_LABEL_WIDTH
|
|
2289
2342
|
? [picked[1]!]
|
|
2290
2343
|
: picked
|
|
2291
2344
|
|
|
@@ -2496,6 +2549,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2496
2549
|
? { width: 0, color, opacity: 0 }
|
|
2497
2550
|
: { width: 2, color, opacity: dimSeries ? DIMMED : 1 },
|
|
2498
2551
|
itemStyle: { color, opacity: dimSeries ? DIMMED : 1 },
|
|
2552
|
+
// Hovering a series fades the others (not for a stack, where the bands read together).
|
|
2553
|
+
emphasis: stacked ? undefined : { focus: "series" as const },
|
|
2499
2554
|
areaStyle: areaStyle && dimSeries ? { ...areaStyle, opacity: DIMMED * 0.3 } : areaStyle,
|
|
2500
2555
|
markPoint: markPointFor(c, color),
|
|
2501
2556
|
markLine: undefined,
|
|
@@ -2942,7 +2997,10 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2942
2997
|
legend: {
|
|
2943
2998
|
show: showLegend,
|
|
2944
2999
|
top: 0,
|
|
2945
|
-
|
|
3000
|
+
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3001
|
+
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3002
|
+
? { left: 0, right: 0 }
|
|
3003
|
+
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2946
3004
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2947
3005
|
type: "plain",
|
|
2948
3006
|
icon: "circle",
|
|
@@ -3003,7 +3061,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3003
3061
|
}
|
|
3004
3062
|
}
|
|
3005
3063
|
const shares = percent ? cartesianSeriesValues(i <= lastActual ? [row] : [], categories, "percent") : null
|
|
3006
|
-
|
|
3064
|
+
// Rows follow the legend order (categories first), not ECharts' own series order.
|
|
3065
|
+
const legendRank = (p: AxisTooltipParam) => {
|
|
3066
|
+
const at = categories.indexOf(p.seriesName ?? "")
|
|
3067
|
+
return at === -1 ? categories.length : at
|
|
3068
|
+
}
|
|
3069
|
+
for (const p of [...list].sort((a, b) => legendRank(a) - legendRank(b))) {
|
|
3007
3070
|
const color = typeof p.color === "string" ? p.color : "currentColor"
|
|
3008
3071
|
if (p.seriesId === FORECAST_SERIES_ID) {
|
|
3009
3072
|
// At and before the boundary the forecast repeats the actual value.
|