@squadbase/vantage 0.7.0 → 0.7.1-dev1.0d13f9a
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/README.md +1 -1
- package/dist/add-AU3EDPNW.js +6 -0
- package/dist/add-AU3EDPNW.js.map +1 -0
- package/dist/{chunk-AABMKHEU.js → chunk-35B3YIBF.js} +16 -7
- package/dist/chunk-35B3YIBF.js.map +1 -0
- package/dist/{chunk-QNOX7WLZ.js → chunk-5HBUBVQ3.js} +94 -8
- package/dist/chunk-5HBUBVQ3.js.map +1 -0
- package/dist/{chunk-EG4PX726.js → chunk-GWXKENNH.js} +122 -5
- package/dist/chunk-GWXKENNH.js.map +1 -0
- package/dist/{chunk-77CISBMG.js → chunk-I2GWIPTD.js} +3685 -350
- package/dist/chunk-I2GWIPTD.js.map +1 -0
- package/dist/{chunk-TRMHNMMZ.js → chunk-LYUGDHSF.js} +3 -3
- package/dist/{chunk-TRMHNMMZ.js.map → chunk-LYUGDHSF.js.map} +1 -1
- package/dist/{chunk-2IAPIOPS.js → chunk-U6MWBCM6.js} +28 -7
- package/dist/chunk-U6MWBCM6.js.map +1 -0
- package/dist/{chunk-3RJEOP3J.js → chunk-XRKSIC5Z.js} +5 -3
- package/dist/chunk-XRKSIC5Z.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/components/index.d.ts +849 -12
- package/dist/components/index.js +5 -5
- package/dist/{delta-text-CbrOaW83.d.ts → delta-text-tav2Lh5d.d.ts} +1 -1
- package/dist/doc/index.d.ts +2 -2
- package/dist/doc/index.js +4 -4
- package/dist/{format-DihQkL1N.d.ts → format-D34piCei.d.ts} +2 -1
- package/dist/geo-japan-7BMSOJRX.js +8 -0
- package/dist/geo-japan-7BMSOJRX.js.map +1 -0
- package/dist/geo-us-GVOG7VXH.js +8 -0
- package/dist/geo-us-GVOG7VXH.js.map +1 -0
- package/dist/geo-world-35HL56UU.js +8 -0
- package/dist/geo-world-35HL56UU.js.map +1 -0
- package/dist/slide/index.d.ts +2 -2
- package/dist/slide/index.js +4 -4
- package/dist/table/index.js +5 -6
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +35 -4
- package/dist/ui/index.js +174 -11
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-6DT7ADRP.js → upgrade-MCHMELWC.js} +3 -3
- package/dist/{upgrade-6DT7ADRP.js.map → upgrade-MCHMELWC.js.map} +1 -1
- package/docs/en/agent-skills.md +13 -14
- package/docs/en/changelog.md +8 -0
- package/docs/en/parts/draft-chart.md +42 -0
- package/docs/en/parts/page-shell.md +1 -1
- package/docs/index.json +70 -50
- package/docs/ja/agent-skills.md +14 -14
- package/docs/ja/changelog.md +8 -0
- package/docs/ja/parts/draft-chart.md +40 -0
- package/docs/ja/parts/page-shell.md +1 -1
- package/package.json +2 -2
- package/registry/parts.json +5319 -0
- package/registry/ui/area-chart.tsx +43 -8
- package/registry/ui/bar-chart.tsx +43 -8
- package/registry/ui/bar-list.tsx +43 -8
- package/registry/ui/box-plot-chart.tsx +42 -7
- package/registry/ui/category-bar.tsx +25 -5
- package/registry/ui/chart-frame.tsx +55 -14
- package/registry/ui/checkbox.tsx +4 -3
- package/registry/ui/code-block.tsx +44 -0
- package/registry/ui/combo-chart.tsx +1869 -0
- package/registry/ui/combobox.tsx +7 -0
- package/registry/ui/context-menu.tsx +71 -0
- package/registry/ui/dashboard-card.tsx +23 -10
- package/registry/ui/data-table.tsx +10 -2
- package/registry/ui/date-range-picker.tsx +51 -4
- package/registry/ui/delta.tsx +39 -12
- package/registry/ui/donut-chart.tsx +43 -8
- package/registry/ui/echart.tsx +167 -7
- package/registry/ui/file-dropzone.tsx +143 -0
- package/registry/ui/filter-bar.tsx +77 -2
- package/registry/ui/forecast-chart.tsx +4769 -0
- package/registry/ui/funnel-steps.tsx +23 -5
- package/registry/ui/heatmap.tsx +20 -3
- package/registry/ui/histogram-chart.tsx +42 -7
- package/registry/ui/kpi-card.tsx +15 -9
- package/registry/ui/legend.tsx +73 -15
- package/registry/ui/line-chart.tsx +43 -8
- package/registry/ui/metric-value.tsx +2 -2
- package/registry/ui/page-shell.tsx +27 -4
- package/registry/ui/paired-chart.tsx +44 -9
- package/registry/ui/pivot-table.tsx +1260 -0
- package/registry/ui/progress-circle.tsx +2 -2
- package/registry/ui/progress.tsx +1 -1
- package/registry/ui/scatter-chart.tsx +50 -12
- package/registry/ui/sparkline.tsx +44 -9
- package/registry/ui/states.tsx +34 -1
- package/registry/ui/status-badge.tsx +7 -7
- package/registry/ui/tabs.tsx +1 -0
- package/registry/ui/treemap.tsx +1934 -0
- package/registry/ui/waterfall-chart.tsx +64 -11
- package/templates/AGENTS.md +3 -3
- package/templates/agents/page.md +5 -4
- package/dist/add-4GHF2TX2.js +0 -6
- package/dist/add-4GHF2TX2.js.map +0 -1
- package/dist/chunk-2IAPIOPS.js.map +0 -1
- package/dist/chunk-3RJEOP3J.js.map +0 -1
- package/dist/chunk-77CISBMG.js.map +0 -1
- package/dist/chunk-AABMKHEU.js.map +0 -1
- package/dist/chunk-EG4PX726.js.map +0 -1
- package/dist/chunk-NZ5QIIG4.js +0 -74
- package/dist/chunk-NZ5QIIG4.js.map +0 -1
- package/dist/chunk-QNOX7WLZ.js.map +0 -1
|
@@ -170,10 +170,25 @@ function useLocale(locale?: string | null): string {
|
|
|
170
170
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
|
-
*
|
|
174
|
-
* `role="img"`
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
174
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
175
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
176
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
177
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
178
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
179
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
180
|
+
*/
|
|
181
|
+
interface ChartFramePlot {
|
|
182
|
+
image: boolean
|
|
183
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
184
|
+
claimed: boolean
|
|
185
|
+
/** Take over the image role; returns the release. */
|
|
186
|
+
claim: () => () => void
|
|
187
|
+
ariaLabel?: string
|
|
188
|
+
ariaLabelledBy?: string
|
|
189
|
+
ariaDescribedBy?: string
|
|
190
|
+
}
|
|
191
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
177
192
|
|
|
178
193
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
179
194
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -397,6 +412,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
397
412
|
tooltip: {
|
|
398
413
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
399
414
|
transitionDuration: 0,
|
|
415
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
416
|
+
confine: true,
|
|
400
417
|
backgroundColor: tokens.popover ?? undefined,
|
|
401
418
|
borderColor: line,
|
|
402
419
|
borderWidth: 1,
|
|
@@ -2272,6 +2289,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2272
2289
|
|
|
2273
2290
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2274
2291
|
const ACTIONS_RESERVE = 80
|
|
2292
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2293
|
+
const BAR_MAX_WIDTH = 40
|
|
2294
|
+
const BAR_RADIUS = 3
|
|
2295
|
+
const STACK_GAP = 1.5
|
|
2275
2296
|
|
|
2276
2297
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2277
2298
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2485,6 +2506,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2485
2506
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2486
2507
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2487
2508
|
})()
|
|
2509
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2510
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2488
2511
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2489
2512
|
|
|
2490
2513
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2694,15 +2717,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2694
2717
|
const seriesData = pad(values[c]!)
|
|
2695
2718
|
if (kind === "bar") {
|
|
2696
2719
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2697
|
-
// Stacked:
|
|
2698
|
-
const radius = stacked
|
|
2720
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2721
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2722
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2699
2723
|
return {
|
|
2700
2724
|
id: SERIES_ID_PREFIX + category,
|
|
2701
2725
|
type: "bar" as const,
|
|
2702
2726
|
name: category,
|
|
2703
2727
|
stack: stacked ? "total" : undefined,
|
|
2704
2728
|
barCategoryGap,
|
|
2705
|
-
|
|
2729
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2730
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2706
2731
|
markPoint: markPointFor(c, solid),
|
|
2707
2732
|
markLine: undefined,
|
|
2708
2733
|
data: seriesData.map((value, i) => {
|
|
@@ -2998,6 +3023,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2998
3023
|
},
|
|
2999
3024
|
})
|
|
3000
3025
|
}
|
|
3026
|
+
if (diverging) {
|
|
3027
|
+
markLines.push({
|
|
3028
|
+
name: "zero",
|
|
3029
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3030
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3031
|
+
label: { show: false },
|
|
3032
|
+
})
|
|
3033
|
+
}
|
|
3001
3034
|
if (forecast) {
|
|
3002
3035
|
const forecastSpan =
|
|
3003
3036
|
plotWidth && labels.length > 1
|
|
@@ -3143,6 +3176,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3143
3176
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3144
3177
|
boundaryGap: kind === "bar",
|
|
3145
3178
|
inverse: horizontalBars,
|
|
3179
|
+
// Diverging: the axis stays at the plot's edge (not at 0, where labels would hit the negative bars); the zero line is a mark line.
|
|
3180
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3146
3181
|
axisTick: { show: false },
|
|
3147
3182
|
axisLabel: {
|
|
3148
3183
|
hideOverlap: true,
|
|
@@ -3245,7 +3280,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3245
3280
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3246
3281
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3247
3282
|
? { left: 0, right: 0 }
|
|
3248
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3283
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3249
3284
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3250
3285
|
type: "plain",
|
|
3251
3286
|
icon: "circle",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (`index`) and the series keys (`categories`) instead of an ECharts option.
|
|
8
8
|
* Grouped, stacked or 100%-stacked; vertical or horizontal bars. Colours are the
|
|
9
9
|
* `--chart-1..5` tokens in fixed order (more than 5 series fold into「その他」)
|
|
10
|
-
* and follow light/dark. The value axis always
|
|
10
|
+
* and follow light/dark. The value axis always reaches 0 (negative values add a solid zero line on their own). A horizontal bar chart of one
|
|
11
11
|
* series (`barOptions.layout: "vertical"`) is a light tint by default, like BarList; `barOptions.highlight`
|
|
12
12
|
* names the one bar the page is about, drawn in the full colour.
|
|
13
13
|
* Renders inside `ChartFrame` (title, summary, loading skeleton, empty state).
|
|
@@ -173,10 +173,25 @@ function useLocale(locale?: string | null): string {
|
|
|
173
173
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
177
|
-
* `role="img"`
|
|
176
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
177
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
178
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
179
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
180
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
181
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
182
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
178
183
|
*/
|
|
179
|
-
|
|
184
|
+
interface ChartFramePlot {
|
|
185
|
+
image: boolean
|
|
186
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
187
|
+
claimed: boolean
|
|
188
|
+
/** Take over the image role; returns the release. */
|
|
189
|
+
claim: () => () => void
|
|
190
|
+
ariaLabel?: string
|
|
191
|
+
ariaLabelledBy?: string
|
|
192
|
+
ariaDescribedBy?: string
|
|
193
|
+
}
|
|
194
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
180
195
|
|
|
181
196
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
182
197
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -400,6 +415,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
400
415
|
tooltip: {
|
|
401
416
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
402
417
|
transitionDuration: 0,
|
|
418
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
419
|
+
confine: true,
|
|
403
420
|
backgroundColor: tokens.popover ?? undefined,
|
|
404
421
|
borderColor: line,
|
|
405
422
|
borderWidth: 1,
|
|
@@ -2275,6 +2292,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2275
2292
|
|
|
2276
2293
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2277
2294
|
const ACTIONS_RESERVE = 80
|
|
2295
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2296
|
+
const BAR_MAX_WIDTH = 40
|
|
2297
|
+
const BAR_RADIUS = 3
|
|
2298
|
+
const STACK_GAP = 1.5
|
|
2278
2299
|
|
|
2279
2300
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2280
2301
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2488,6 +2509,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2488
2509
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2489
2510
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2490
2511
|
})()
|
|
2512
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2513
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2491
2514
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2492
2515
|
|
|
2493
2516
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2697,15 +2720,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2697
2720
|
const seriesData = pad(values[c]!)
|
|
2698
2721
|
if (kind === "bar") {
|
|
2699
2722
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2700
|
-
// Stacked:
|
|
2701
|
-
const radius = stacked
|
|
2723
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2724
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2725
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2702
2726
|
return {
|
|
2703
2727
|
id: SERIES_ID_PREFIX + category,
|
|
2704
2728
|
type: "bar" as const,
|
|
2705
2729
|
name: category,
|
|
2706
2730
|
stack: stacked ? "total" : undefined,
|
|
2707
2731
|
barCategoryGap,
|
|
2708
|
-
|
|
2732
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2733
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2709
2734
|
markPoint: markPointFor(c, solid),
|
|
2710
2735
|
markLine: undefined,
|
|
2711
2736
|
data: seriesData.map((value, i) => {
|
|
@@ -3001,6 +3026,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3001
3026
|
},
|
|
3002
3027
|
})
|
|
3003
3028
|
}
|
|
3029
|
+
if (diverging) {
|
|
3030
|
+
markLines.push({
|
|
3031
|
+
name: "zero",
|
|
3032
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3033
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3034
|
+
label: { show: false },
|
|
3035
|
+
})
|
|
3036
|
+
}
|
|
3004
3037
|
if (forecast) {
|
|
3005
3038
|
const forecastSpan =
|
|
3006
3039
|
plotWidth && labels.length > 1
|
|
@@ -3146,6 +3179,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3146
3179
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3147
3180
|
boundaryGap: kind === "bar",
|
|
3148
3181
|
inverse: horizontalBars,
|
|
3182
|
+
// Diverging: the axis stays at the plot's edge (not at 0, where labels would hit the negative bars); the zero line is a mark line.
|
|
3183
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3149
3184
|
axisTick: { show: false },
|
|
3150
3185
|
axisLabel: {
|
|
3151
3186
|
hideOverlap: true,
|
|
@@ -3248,7 +3283,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3248
3283
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3249
3284
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3250
3285
|
? { left: 0, right: 0 }
|
|
3251
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3286
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3252
3287
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3253
3288
|
type: "plain",
|
|
3254
3289
|
icon: "circle",
|
package/registry/ui/bar-list.tsx
CHANGED
|
@@ -186,10 +186,25 @@ function useLocale(locale?: string | null): string {
|
|
|
186
186
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
|
-
*
|
|
190
|
-
* `role="img"`
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
190
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
191
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
192
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
193
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
194
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
195
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
196
|
+
*/
|
|
197
|
+
interface ChartFramePlot {
|
|
198
|
+
image: boolean
|
|
199
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
200
|
+
claimed: boolean
|
|
201
|
+
/** Take over the image role; returns the release. */
|
|
202
|
+
claim: () => () => void
|
|
203
|
+
ariaLabel?: string
|
|
204
|
+
ariaLabelledBy?: string
|
|
205
|
+
ariaDescribedBy?: string
|
|
206
|
+
}
|
|
207
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
193
208
|
|
|
194
209
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
195
210
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -413,6 +428,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
413
428
|
tooltip: {
|
|
414
429
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
415
430
|
transitionDuration: 0,
|
|
431
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
432
|
+
confine: true,
|
|
416
433
|
backgroundColor: tokens.popover ?? undefined,
|
|
417
434
|
borderColor: line,
|
|
418
435
|
borderWidth: 1,
|
|
@@ -2288,6 +2305,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2288
2305
|
|
|
2289
2306
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2290
2307
|
const ACTIONS_RESERVE = 80
|
|
2308
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2309
|
+
const BAR_MAX_WIDTH = 40
|
|
2310
|
+
const BAR_RADIUS = 3
|
|
2311
|
+
const STACK_GAP = 1.5
|
|
2291
2312
|
|
|
2292
2313
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2293
2314
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2501,6 +2522,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2501
2522
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2502
2523
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2503
2524
|
})()
|
|
2525
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2526
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2504
2527
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2505
2528
|
|
|
2506
2529
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2710,15 +2733,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2710
2733
|
const seriesData = pad(values[c]!)
|
|
2711
2734
|
if (kind === "bar") {
|
|
2712
2735
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2713
|
-
// Stacked:
|
|
2714
|
-
const radius = stacked
|
|
2736
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2737
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2738
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2715
2739
|
return {
|
|
2716
2740
|
id: SERIES_ID_PREFIX + category,
|
|
2717
2741
|
type: "bar" as const,
|
|
2718
2742
|
name: category,
|
|
2719
2743
|
stack: stacked ? "total" : undefined,
|
|
2720
2744
|
barCategoryGap,
|
|
2721
|
-
|
|
2745
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2746
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2722
2747
|
markPoint: markPointFor(c, solid),
|
|
2723
2748
|
markLine: undefined,
|
|
2724
2749
|
data: seriesData.map((value, i) => {
|
|
@@ -3014,6 +3039,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3014
3039
|
},
|
|
3015
3040
|
})
|
|
3016
3041
|
}
|
|
3042
|
+
if (diverging) {
|
|
3043
|
+
markLines.push({
|
|
3044
|
+
name: "zero",
|
|
3045
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3046
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3047
|
+
label: { show: false },
|
|
3048
|
+
})
|
|
3049
|
+
}
|
|
3017
3050
|
if (forecast) {
|
|
3018
3051
|
const forecastSpan =
|
|
3019
3052
|
plotWidth && labels.length > 1
|
|
@@ -3159,6 +3192,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3159
3192
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3160
3193
|
boundaryGap: kind === "bar",
|
|
3161
3194
|
inverse: horizontalBars,
|
|
3195
|
+
// Diverging: the axis stays at the plot's edge (not at 0, where labels would hit the negative bars); the zero line is a mark line.
|
|
3196
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3162
3197
|
axisTick: { show: false },
|
|
3163
3198
|
axisLabel: {
|
|
3164
3199
|
hideOverlap: true,
|
|
@@ -3261,7 +3296,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3261
3296
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3262
3297
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3263
3298
|
? { left: 0, right: 0 }
|
|
3264
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3299
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3265
3300
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3266
3301
|
type: "plain",
|
|
3267
3302
|
icon: "circle",
|
|
@@ -174,10 +174,25 @@ function useLocale(locale?: string | null): string {
|
|
|
174
174
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
*
|
|
178
|
-
* `role="img"`
|
|
177
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
178
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
179
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
180
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
181
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
182
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
183
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
179
184
|
*/
|
|
180
|
-
|
|
185
|
+
interface ChartFramePlot {
|
|
186
|
+
image: boolean
|
|
187
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
188
|
+
claimed: boolean
|
|
189
|
+
/** Take over the image role; returns the release. */
|
|
190
|
+
claim: () => () => void
|
|
191
|
+
ariaLabel?: string
|
|
192
|
+
ariaLabelledBy?: string
|
|
193
|
+
ariaDescribedBy?: string
|
|
194
|
+
}
|
|
195
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
181
196
|
|
|
182
197
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
183
198
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -401,6 +416,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
401
416
|
tooltip: {
|
|
402
417
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
403
418
|
transitionDuration: 0,
|
|
419
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
420
|
+
confine: true,
|
|
404
421
|
backgroundColor: tokens.popover ?? undefined,
|
|
405
422
|
borderColor: line,
|
|
406
423
|
borderWidth: 1,
|
|
@@ -1437,6 +1454,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
1437
1454
|
|
|
1438
1455
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
1439
1456
|
const ACTIONS_RESERVE = 80
|
|
1457
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
1458
|
+
const BAR_MAX_WIDTH = 40
|
|
1459
|
+
const BAR_RADIUS = 3
|
|
1460
|
+
const STACK_GAP = 1.5
|
|
1440
1461
|
|
|
1441
1462
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
1442
1463
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -1650,6 +1671,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1650
1671
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
1651
1672
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
1652
1673
|
})()
|
|
1674
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
1675
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
1653
1676
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
1654
1677
|
|
|
1655
1678
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -1859,15 +1882,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1859
1882
|
const seriesData = pad(values[c]!)
|
|
1860
1883
|
if (kind === "bar") {
|
|
1861
1884
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
1862
|
-
// Stacked:
|
|
1863
|
-
const radius = stacked
|
|
1885
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
1886
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
1887
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
1864
1888
|
return {
|
|
1865
1889
|
id: SERIES_ID_PREFIX + category,
|
|
1866
1890
|
type: "bar" as const,
|
|
1867
1891
|
name: category,
|
|
1868
1892
|
stack: stacked ? "total" : undefined,
|
|
1869
1893
|
barCategoryGap,
|
|
1870
|
-
|
|
1894
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
1895
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
1871
1896
|
markPoint: markPointFor(c, solid),
|
|
1872
1897
|
markLine: undefined,
|
|
1873
1898
|
data: seriesData.map((value, i) => {
|
|
@@ -2163,6 +2188,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2163
2188
|
},
|
|
2164
2189
|
})
|
|
2165
2190
|
}
|
|
2191
|
+
if (diverging) {
|
|
2192
|
+
markLines.push({
|
|
2193
|
+
name: "zero",
|
|
2194
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
2195
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
2196
|
+
label: { show: false },
|
|
2197
|
+
})
|
|
2198
|
+
}
|
|
2166
2199
|
if (forecast) {
|
|
2167
2200
|
const forecastSpan =
|
|
2168
2201
|
plotWidth && labels.length > 1
|
|
@@ -2308,6 +2341,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2308
2341
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
2309
2342
|
boundaryGap: kind === "bar",
|
|
2310
2343
|
inverse: horizontalBars,
|
|
2344
|
+
// Diverging: the axis stays at the plot's edge (not at 0, where labels would hit the negative bars); the zero line is a mark line.
|
|
2345
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
2311
2346
|
axisTick: { show: false },
|
|
2312
2347
|
axisLabel: {
|
|
2313
2348
|
hideOverlap: true,
|
|
@@ -2410,7 +2445,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2410
2445
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
2411
2446
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2412
2447
|
? { left: 0, right: 0 }
|
|
2413
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2448
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2414
2449
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2415
2450
|
type: "plain",
|
|
2416
2451
|
icon: "circle",
|
|
@@ -163,10 +163,25 @@ function useLocale(locale?: string | null): string {
|
|
|
163
163
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
167
|
-
* `role="img"`
|
|
166
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
167
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
168
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
169
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
170
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
171
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
172
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
168
173
|
*/
|
|
169
|
-
|
|
174
|
+
interface ChartFramePlot {
|
|
175
|
+
image: boolean
|
|
176
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
177
|
+
claimed: boolean
|
|
178
|
+
/** Take over the image role; returns the release. */
|
|
179
|
+
claim: () => () => void
|
|
180
|
+
ariaLabel?: string
|
|
181
|
+
ariaLabelledBy?: string
|
|
182
|
+
ariaDescribedBy?: string
|
|
183
|
+
}
|
|
184
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
170
185
|
|
|
171
186
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
172
187
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -390,6 +405,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
390
405
|
tooltip: {
|
|
391
406
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
392
407
|
transitionDuration: 0,
|
|
408
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
409
|
+
confine: true,
|
|
393
410
|
backgroundColor: tokens.popover ?? undefined,
|
|
394
411
|
borderColor: line,
|
|
395
412
|
borderWidth: 1,
|
|
@@ -1439,8 +1456,11 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
|
|
|
1439
1456
|
/>
|
|
1440
1457
|
)
|
|
1441
1458
|
|
|
1459
|
+
// aria-label is prohibited on a role-less div: a labelled bar is a group (the label names it, the marker stays reachable).
|
|
1460
|
+
const labelled = props["aria-label"] != null || props["aria-labelledby"] != null
|
|
1461
|
+
|
|
1442
1462
|
return (
|
|
1443
|
-
<div ref={ref} data-slot="category-bar" className={cn("w-full", className)} {...props}>
|
|
1463
|
+
<div ref={ref} data-slot="category-bar" role={labelled ? "group" : undefined} className={cn("w-full", className)} {...props}>
|
|
1444
1464
|
{showLabels && labels.length > 0 && (
|
|
1445
1465
|
<div data-slot="category-bar-labels" className="relative mb-2 h-4 text-xs text-muted-foreground tabular-nums">
|
|
1446
1466
|
{labels.map((label, i) => (
|
|
@@ -1481,7 +1501,7 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
|
|
|
1481
1501
|
>
|
|
1482
1502
|
{marker.tooltip ? (
|
|
1483
1503
|
<Tooltip>
|
|
1484
|
-
<TooltipTrigger render={<
|
|
1504
|
+
<TooltipTrigger render={<button type="button" aria-label={marker.tooltip} className="block cursor-default py-1" />}>
|
|
1485
1505
|
{markerDot}
|
|
1486
1506
|
</TooltipTrigger>
|
|
1487
1507
|
<TooltipContent>{marker.tooltip}</TooltipContent>
|
|
@@ -8,20 +8,23 @@
|
|
|
8
8
|
* by the title and described by the summary, so a screen reader hears what the
|
|
9
9
|
* chart says, not just that there is one.
|
|
10
10
|
*
|
|
11
|
-
* The plot
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The plot is the frame's one `role="img"`. When it is an `EChart`, that
|
|
12
|
+
* canvas takes the role (and the frame's name and description) and the plot
|
|
13
|
+
* area drops it, so the copy / download buttons are not inside the image;
|
|
14
|
+
* any other plot keeps the role on the plot area. `dataTable` adds the numbers
|
|
15
|
+
* as a visually-hidden table beside it, so a screen reader can read the values
|
|
16
|
+
* the canvas only draws.
|
|
14
17
|
*
|
|
15
18
|
* The plot area keeps its height in every state: while loading it shows a
|
|
16
19
|
* skeleton shaped like the final chart, and with no data it says so (and offers
|
|
17
20
|
* one action) instead of drawing empty axes.
|
|
18
21
|
*/
|
|
19
|
-
import { forwardRef, useId, type HTMLAttributes, type ReactNode } from "react"
|
|
22
|
+
import { forwardRef, useCallback, useId, useMemo, useState, type HTMLAttributes, type ReactNode } from "react"
|
|
20
23
|
|
|
21
24
|
import { cn } from "@squadbase/vantage/ui"
|
|
22
25
|
import type { ChartEmptyState } from "@squadbase/vantage/components"
|
|
23
26
|
import { useSyncExternalStore } from "react"
|
|
24
|
-
import { createContext,
|
|
27
|
+
import { createContext, useEffect, useRef, type ForwardedRef, type RefObject } from "react"
|
|
25
28
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
26
29
|
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
27
30
|
import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
@@ -170,10 +173,25 @@ function useLocale(locale?: string | null): string {
|
|
|
170
173
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
171
174
|
|
|
172
175
|
/**
|
|
173
|
-
*
|
|
174
|
-
* `role="img"`
|
|
176
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
177
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
178
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
179
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
180
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
181
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
182
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
175
183
|
*/
|
|
176
|
-
|
|
184
|
+
interface ChartFramePlot {
|
|
185
|
+
image: boolean
|
|
186
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
187
|
+
claimed: boolean
|
|
188
|
+
/** Take over the image role; returns the release. */
|
|
189
|
+
claim: () => () => void
|
|
190
|
+
ariaLabel?: string
|
|
191
|
+
ariaLabelledBy?: string
|
|
192
|
+
ariaDescribedBy?: string
|
|
193
|
+
}
|
|
194
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
177
195
|
|
|
178
196
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
179
197
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -397,6 +415,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
397
415
|
tooltip: {
|
|
398
416
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
399
417
|
transitionDuration: 0,
|
|
418
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
419
|
+
confine: true,
|
|
400
420
|
backgroundColor: tokens.popover ?? undefined,
|
|
401
421
|
borderColor: line,
|
|
402
422
|
borderWidth: 1,
|
|
@@ -1422,12 +1442,15 @@ export interface ChartFrameProps extends Omit<HTMLAttributes<HTMLElement>, "titl
|
|
|
1422
1442
|
/** 見出しが無いときのプロット領域のラベル。 */
|
|
1423
1443
|
ariaLabel?: string
|
|
1424
1444
|
/**
|
|
1425
|
-
*
|
|
1445
|
+
* プロットの役割。既定 `"img"`(canvas のグラフ。`role="img"` と名前・要約は
|
|
1446
|
+
* `EChart` が canvas にだけ付け、操作ボタンは画像の外に置く)。行がボタンやリンクになる
|
|
1426
1447
|
* HTML のグラフ(BarList・FunnelSteps)は `"group"` にして、中身を読ませる。
|
|
1427
1448
|
*/
|
|
1428
1449
|
plotRole?: "img" | "group"
|
|
1429
1450
|
/** 値の表。画面には出さず(`sr-only`)、スクリーンリーダー向けに図の中に置く。`chartDataTable` で作る。 */
|
|
1430
1451
|
dataTable?: ChartDataTable
|
|
1452
|
+
/** プロットの下に常に出す小さな注記(近似値である旨など)。読み込み中・空のときも出す。 */
|
|
1453
|
+
footnote?: string
|
|
1431
1454
|
/** プロット(チャート本体)。読み込み中・空のときは描かない。 */
|
|
1432
1455
|
children?: ReactNode
|
|
1433
1456
|
}
|
|
@@ -1448,6 +1471,7 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1448
1471
|
ariaLabel,
|
|
1449
1472
|
plotRole = "img",
|
|
1450
1473
|
dataTable,
|
|
1474
|
+
footnote,
|
|
1451
1475
|
className,
|
|
1452
1476
|
children,
|
|
1453
1477
|
...props
|
|
@@ -1459,6 +1483,18 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1459
1483
|
const titleId = title ? `${id}-title` : undefined
|
|
1460
1484
|
const summaryId = summary ? `${id}-summary` : undefined
|
|
1461
1485
|
const state = isLoading ? "loading" : isEmpty ? "empty" : "ready"
|
|
1486
|
+
// An EChart inside takes the image role for its canvas (see ChartFramePlot).
|
|
1487
|
+
const [claims, setClaims] = useState(0)
|
|
1488
|
+
const claim = useCallback(() => {
|
|
1489
|
+
setClaims((n) => n + 1)
|
|
1490
|
+
return () => setClaims((n) => n - 1)
|
|
1491
|
+
}, [])
|
|
1492
|
+
const claimed = claims > 0 && plotRole === "img"
|
|
1493
|
+
const plotOwnsImage = state === "ready" && claimed
|
|
1494
|
+
const plot = useMemo(
|
|
1495
|
+
() => ({ image: plotRole === "img", claimed, claim, ariaLabel: titleId ? undefined : ariaLabel, ariaLabelledBy: titleId, ariaDescribedBy: summaryId }),
|
|
1496
|
+
[plotRole, claimed, claim, titleId, ariaLabel, summaryId],
|
|
1497
|
+
)
|
|
1462
1498
|
|
|
1463
1499
|
return (
|
|
1464
1500
|
<figure data-slot="chart-frame" className={cn("m-0 flex w-full min-w-0 flex-col gap-3", className)} {...props}>
|
|
@@ -1480,10 +1516,10 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1480
1516
|
ref={ref}
|
|
1481
1517
|
data-slot="chart-frame-body"
|
|
1482
1518
|
data-state={state}
|
|
1483
|
-
role={state === "ready" ? plotRole : "group"}
|
|
1484
|
-
aria-label={titleId ? undefined : ariaLabel}
|
|
1485
|
-
aria-labelledby={titleId}
|
|
1486
|
-
aria-describedby={summaryId}
|
|
1519
|
+
role={plotOwnsImage ? undefined : state === "ready" ? plotRole : "group"}
|
|
1520
|
+
aria-label={plotOwnsImage || titleId ? undefined : ariaLabel}
|
|
1521
|
+
aria-labelledby={plotOwnsImage ? undefined : titleId}
|
|
1522
|
+
aria-describedby={plotOwnsImage ? undefined : summaryId}
|
|
1487
1523
|
aria-busy={isLoading || undefined}
|
|
1488
1524
|
className="relative w-full min-w-0"
|
|
1489
1525
|
style={{ height: typeof height === "number" ? `${height}px` : height }}
|
|
@@ -1493,9 +1529,14 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
|
|
|
1493
1529
|
) : state === "empty" ? (
|
|
1494
1530
|
<ChartEmpty {...emptyState} defaultTitle={t.emptyTitle} />
|
|
1495
1531
|
) : (
|
|
1496
|
-
<ChartFrameContext.Provider value>{children}</ChartFrameContext.Provider>
|
|
1532
|
+
<ChartFrameContext.Provider value={plot}>{children}</ChartFrameContext.Provider>
|
|
1497
1533
|
)}
|
|
1498
1534
|
</div>
|
|
1535
|
+
{footnote && (
|
|
1536
|
+
<p data-slot="chart-frame-footnote" className="text-xs leading-snug text-muted-foreground">
|
|
1537
|
+
{footnote}
|
|
1538
|
+
</p>
|
|
1539
|
+
)}
|
|
1499
1540
|
{state === "ready" && dataTable && dataTable.rows.length > 0 && (
|
|
1500
1541
|
<ChartDataTableView table={dataTable} fallbackCaption={title ?? ariaLabel} />
|
|
1501
1542
|
)}
|