@squadbase/vantage 0.7.1-dev1.c17b6ee → 0.7.1-dev1.c83f631
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-U7GO26HR.js → build-YQGJBR3G.js} +3 -3
- package/dist/{build-U7GO26HR.js.map → build-YQGJBR3G.js.map} +1 -1
- package/dist/{chunk-EG4PX726.js → chunk-GWXKENNH.js} +122 -5
- package/dist/chunk-GWXKENNH.js.map +1 -0
- package/dist/{chunk-SC7K2RD6.js → chunk-I2GWIPTD.js} +101 -27
- package/dist/chunk-I2GWIPTD.js.map +1 -0
- package/dist/{chunk-SDJTTMLK.js → chunk-K2ZA547X.js} +3 -3
- package/dist/chunk-K2ZA547X.js.map +1 -0
- package/dist/{chunk-P2KIHF65.js → chunk-U6MWBCM6.js} +28 -7
- package/dist/chunk-U6MWBCM6.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/components/index.d.ts +206 -4
- package/dist/components/index.js +3 -3
- package/dist/{dev-O42JFQ64.js → dev-5PHW3KNJ.js} +3 -3
- package/dist/{dev-O42JFQ64.js.map → dev-5PHW3KNJ.js.map} +1 -1
- package/dist/doc/index.js +1 -1
- package/dist/slide/index.js +1 -1
- package/dist/table/index.js +3 -3
- package/dist/ui/index.d.ts +28 -1
- package/dist/ui/index.js +172 -9
- package/dist/ui/index.js.map +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +2 -2
- package/registry/parts.json +81 -2
- 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 +47 -14
- package/registry/ui/checkbox.tsx +4 -3
- package/registry/ui/code-block.tsx +44 -0
- package/registry/ui/combo-chart.tsx +20 -3
- package/registry/ui/combobox.tsx +7 -0
- package/registry/ui/context-menu.tsx +71 -0
- package/registry/ui/dashboard-card.tsx +20 -9
- package/registry/ui/data-table.tsx +7 -1
- package/registry/ui/date-range-picker.tsx +51 -4
- package/registry/ui/delta.tsx +36 -11
- 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 +74 -1
- package/registry/ui/forecast-chart.tsx +42 -7
- package/registry/ui/funnel-steps.tsx +22 -4
- package/registry/ui/heatmap.tsx +20 -3
- package/registry/ui/histogram-chart.tsx +42 -7
- package/registry/ui/kpi-card.tsx +12 -8
- package/registry/ui/legend.tsx +73 -15
- package/registry/ui/line-chart.tsx +43 -8
- package/registry/ui/page-shell.tsx +2 -0
- package/registry/ui/paired-chart.tsx +43 -8
- package/registry/ui/progress.tsx +1 -1
- package/registry/ui/scatter-chart.tsx +50 -12
- package/registry/ui/sparkline.tsx +43 -8
- 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 +20 -3
- package/registry/ui/waterfall-chart.tsx +64 -11
- package/dist/chunk-EG4PX726.js.map +0 -1
- package/dist/chunk-P2KIHF65.js.map +0 -1
- package/dist/chunk-SC7K2RD6.js.map +0 -1
- package/dist/chunk-SDJTTMLK.js.map +0 -1
|
@@ -171,10 +171,25 @@ function useLocale(locale?: string | null): string {
|
|
|
171
171
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
175
|
-
* `role="img"`
|
|
174
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
175
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
176
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
177
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
178
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
179
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
180
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
176
181
|
*/
|
|
177
|
-
|
|
182
|
+
interface ChartFramePlot {
|
|
183
|
+
image: boolean
|
|
184
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
185
|
+
claimed: boolean
|
|
186
|
+
/** Take over the image role; returns the release. */
|
|
187
|
+
claim: () => () => void
|
|
188
|
+
ariaLabel?: string
|
|
189
|
+
ariaLabelledBy?: string
|
|
190
|
+
ariaDescribedBy?: string
|
|
191
|
+
}
|
|
192
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
178
193
|
|
|
179
194
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
180
195
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -398,6 +413,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
398
413
|
tooltip: {
|
|
399
414
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
400
415
|
transitionDuration: 0,
|
|
416
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
417
|
+
confine: true,
|
|
401
418
|
backgroundColor: tokens.popover ?? undefined,
|
|
402
419
|
borderColor: line,
|
|
403
420
|
borderWidth: 1,
|
|
@@ -2273,6 +2290,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2273
2290
|
|
|
2274
2291
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2275
2292
|
const ACTIONS_RESERVE = 80
|
|
2293
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2294
|
+
const BAR_MAX_WIDTH = 40
|
|
2295
|
+
const BAR_RADIUS = 3
|
|
2296
|
+
const STACK_GAP = 1.5
|
|
2276
2297
|
|
|
2277
2298
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2278
2299
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2486,6 +2507,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2486
2507
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2487
2508
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2488
2509
|
})()
|
|
2510
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2511
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2489
2512
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2490
2513
|
|
|
2491
2514
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2695,15 +2718,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2695
2718
|
const seriesData = pad(values[c]!)
|
|
2696
2719
|
if (kind === "bar") {
|
|
2697
2720
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2698
|
-
// Stacked:
|
|
2699
|
-
const radius = stacked
|
|
2721
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2722
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2723
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2700
2724
|
return {
|
|
2701
2725
|
id: SERIES_ID_PREFIX + category,
|
|
2702
2726
|
type: "bar" as const,
|
|
2703
2727
|
name: category,
|
|
2704
2728
|
stack: stacked ? "total" : undefined,
|
|
2705
2729
|
barCategoryGap,
|
|
2706
|
-
|
|
2730
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2731
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2707
2732
|
markPoint: markPointFor(c, solid),
|
|
2708
2733
|
markLine: undefined,
|
|
2709
2734
|
data: seriesData.map((value, i) => {
|
|
@@ -2999,6 +3024,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2999
3024
|
},
|
|
3000
3025
|
})
|
|
3001
3026
|
}
|
|
3027
|
+
if (diverging) {
|
|
3028
|
+
markLines.push({
|
|
3029
|
+
name: "zero",
|
|
3030
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3031
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3032
|
+
label: { show: false },
|
|
3033
|
+
})
|
|
3034
|
+
}
|
|
3002
3035
|
if (forecast) {
|
|
3003
3036
|
const forecastSpan =
|
|
3004
3037
|
plotWidth && labels.length > 1
|
|
@@ -3144,6 +3177,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3144
3177
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3145
3178
|
boundaryGap: kind === "bar",
|
|
3146
3179
|
inverse: horizontalBars,
|
|
3180
|
+
// 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.
|
|
3181
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3147
3182
|
axisTick: { show: false },
|
|
3148
3183
|
axisLabel: {
|
|
3149
3184
|
hideOverlap: true,
|
|
@@ -3246,7 +3281,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3246
3281
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3247
3282
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3248
3283
|
? { left: 0, right: 0 }
|
|
3249
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3284
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3250
3285
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3251
3286
|
type: "plain",
|
|
3252
3287
|
icon: "circle",
|
|
@@ -1891,6 +1891,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
1891
1891
|
|
|
1892
1892
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
1893
1893
|
const ACTIONS_RESERVE = 80
|
|
1894
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
1895
|
+
const BAR_MAX_WIDTH = 40
|
|
1896
|
+
const BAR_RADIUS = 3
|
|
1897
|
+
const STACK_GAP = 1.5
|
|
1894
1898
|
|
|
1895
1899
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
1896
1900
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2104,6 +2108,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2104
2108
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2105
2109
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2106
2110
|
})()
|
|
2111
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2112
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2107
2113
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2108
2114
|
|
|
2109
2115
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2313,15 +2319,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2313
2319
|
const seriesData = pad(values[c]!)
|
|
2314
2320
|
if (kind === "bar") {
|
|
2315
2321
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2316
|
-
// Stacked:
|
|
2317
|
-
const radius = stacked
|
|
2322
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2323
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2324
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2318
2325
|
return {
|
|
2319
2326
|
id: SERIES_ID_PREFIX + category,
|
|
2320
2327
|
type: "bar" as const,
|
|
2321
2328
|
name: category,
|
|
2322
2329
|
stack: stacked ? "total" : undefined,
|
|
2323
2330
|
barCategoryGap,
|
|
2324
|
-
|
|
2331
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2332
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2325
2333
|
markPoint: markPointFor(c, solid),
|
|
2326
2334
|
markLine: undefined,
|
|
2327
2335
|
data: seriesData.map((value, i) => {
|
|
@@ -2617,6 +2625,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2617
2625
|
},
|
|
2618
2626
|
})
|
|
2619
2627
|
}
|
|
2628
|
+
if (diverging) {
|
|
2629
|
+
markLines.push({
|
|
2630
|
+
name: "zero",
|
|
2631
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
2632
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
2633
|
+
label: { show: false },
|
|
2634
|
+
})
|
|
2635
|
+
}
|
|
2620
2636
|
if (forecast) {
|
|
2621
2637
|
const forecastSpan =
|
|
2622
2638
|
plotWidth && labels.length > 1
|
|
@@ -2762,6 +2778,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2762
2778
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
2763
2779
|
boundaryGap: kind === "bar",
|
|
2764
2780
|
inverse: horizontalBars,
|
|
2781
|
+
// 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.
|
|
2782
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
2765
2783
|
axisTick: { show: false },
|
|
2766
2784
|
axisLabel: {
|
|
2767
2785
|
hideOverlap: true,
|
|
@@ -2864,7 +2882,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2864
2882
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
2865
2883
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2866
2884
|
? { left: 0, right: 0 }
|
|
2867
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2885
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2868
2886
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2869
2887
|
type: "plain",
|
|
2870
2888
|
icon: "circle",
|
package/registry/ui/heatmap.tsx
CHANGED
|
@@ -177,10 +177,25 @@ function useLocale(locale?: string | null): string {
|
|
|
177
177
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
|
-
*
|
|
181
|
-
* `role="img"`
|
|
180
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
181
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
182
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
183
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
184
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
185
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
186
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
182
187
|
*/
|
|
183
|
-
|
|
188
|
+
interface ChartFramePlot {
|
|
189
|
+
image: boolean
|
|
190
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
191
|
+
claimed: boolean
|
|
192
|
+
/** Take over the image role; returns the release. */
|
|
193
|
+
claim: () => () => void
|
|
194
|
+
ariaLabel?: string
|
|
195
|
+
ariaLabelledBy?: string
|
|
196
|
+
ariaDescribedBy?: string
|
|
197
|
+
}
|
|
198
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
184
199
|
|
|
185
200
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
186
201
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -404,6 +419,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
404
419
|
tooltip: {
|
|
405
420
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
406
421
|
transitionDuration: 0,
|
|
422
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
423
|
+
confine: true,
|
|
407
424
|
backgroundColor: tokens.popover ?? undefined,
|
|
408
425
|
borderColor: line,
|
|
409
426
|
borderWidth: 1,
|
|
@@ -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,
|
|
@@ -1436,6 +1453,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
1436
1453
|
|
|
1437
1454
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
1438
1455
|
const ACTIONS_RESERVE = 80
|
|
1456
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
1457
|
+
const BAR_MAX_WIDTH = 40
|
|
1458
|
+
const BAR_RADIUS = 3
|
|
1459
|
+
const STACK_GAP = 1.5
|
|
1439
1460
|
|
|
1440
1461
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
1441
1462
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -1649,6 +1670,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1649
1670
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
1650
1671
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
1651
1672
|
})()
|
|
1673
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
1674
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
1652
1675
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
1653
1676
|
|
|
1654
1677
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -1858,15 +1881,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
1858
1881
|
const seriesData = pad(values[c]!)
|
|
1859
1882
|
if (kind === "bar") {
|
|
1860
1883
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
1861
|
-
// Stacked:
|
|
1862
|
-
const radius = stacked
|
|
1884
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
1885
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
1886
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
1863
1887
|
return {
|
|
1864
1888
|
id: SERIES_ID_PREFIX + category,
|
|
1865
1889
|
type: "bar" as const,
|
|
1866
1890
|
name: category,
|
|
1867
1891
|
stack: stacked ? "total" : undefined,
|
|
1868
1892
|
barCategoryGap,
|
|
1869
|
-
|
|
1893
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
1894
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
1870
1895
|
markPoint: markPointFor(c, solid),
|
|
1871
1896
|
markLine: undefined,
|
|
1872
1897
|
data: seriesData.map((value, i) => {
|
|
@@ -2162,6 +2187,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2162
2187
|
},
|
|
2163
2188
|
})
|
|
2164
2189
|
}
|
|
2190
|
+
if (diverging) {
|
|
2191
|
+
markLines.push({
|
|
2192
|
+
name: "zero",
|
|
2193
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
2194
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
2195
|
+
label: { show: false },
|
|
2196
|
+
})
|
|
2197
|
+
}
|
|
2165
2198
|
if (forecast) {
|
|
2166
2199
|
const forecastSpan =
|
|
2167
2200
|
plotWidth && labels.length > 1
|
|
@@ -2307,6 +2340,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2307
2340
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
2308
2341
|
boundaryGap: kind === "bar",
|
|
2309
2342
|
inverse: horizontalBars,
|
|
2343
|
+
// 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.
|
|
2344
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
2310
2345
|
axisTick: { show: false },
|
|
2311
2346
|
axisLabel: {
|
|
2312
2347
|
hideOverlap: true,
|
|
@@ -2409,7 +2444,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2409
2444
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
2410
2445
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
2411
2446
|
? { left: 0, right: 0 }
|
|
2412
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2447
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
2413
2448
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
2414
2449
|
type: "plain",
|
|
2415
2450
|
icon: "circle",
|
package/registry/ui/kpi-card.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Five tiers, following shadcn dashboard-01 SectionCards:
|
|
9
9
|
*
|
|
10
10
|
* 1. label (CardDescription, muted)
|
|
11
|
-
* 2. value (CardTitle, tabular-nums; text-
|
|
11
|
+
* 2. value (CardTitle, tabular-nums; text-[26px] once the card is ≥ 320px wide (a comparison label is ~180px, so narrower cards would overlap the value))
|
|
12
12
|
* 3. delta (CardAction: arrow + sign + colour + comparison label — `Delta`)
|
|
13
13
|
* 4. previous actual ("前月 ¥4,290万"; on by default, `showPrevious={false}` hides it)
|
|
14
14
|
* 5. conclusion (`summary`: a *template* whose placeholders the card fills)
|
|
@@ -590,7 +590,7 @@ export function KpiCard({
|
|
|
590
590
|
if (summary !== undefined) warnShallowSummary(summary, t)
|
|
591
591
|
// Two rows of the parent grid (body, footer), shared by subgrid; a plain two-row grid when standalone.
|
|
592
592
|
const cardClass = cn("row-span-2 grid content-start gap-y-0 grid-rows-subgrid", className)
|
|
593
|
-
const valueSize = small ? "text-xl @[218px]/card:text-2xl" : "text-2xl @[218px]/card:text-
|
|
593
|
+
const valueSize = small ? "text-xl @[218px]/card:text-2xl" : "text-2xl @[218px]/card:text-[26px]"
|
|
594
594
|
|
|
595
595
|
if (isLoading) {
|
|
596
596
|
return (
|
|
@@ -748,6 +748,7 @@ export function KpiCard({
|
|
|
748
748
|
change={display}
|
|
749
749
|
digits={digits}
|
|
750
750
|
locale={locale}
|
|
751
|
+
className="min-w-0 shrink whitespace-normal"
|
|
751
752
|
/>
|
|
752
753
|
))
|
|
753
754
|
|
|
@@ -757,16 +758,18 @@ export function KpiCard({
|
|
|
757
758
|
return (
|
|
758
759
|
<Card data-slot="kpi-card" size={size} className={cardClass} {...props}>
|
|
759
760
|
<CardHeader className={HEADER_CLASS}>
|
|
760
|
-
<CardDescription data-slot="kpi-card-label" title={label} className="col-start-1 min-w-0
|
|
761
|
-
{label}
|
|
761
|
+
<CardDescription data-slot="kpi-card-label" title={label} className="col-start-1 flex min-w-0 items-center gap-2 text-[13px] leading-snug font-medium text-muted-foreground">
|
|
762
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
763
|
+
{sample && <SampleBadge label={sample.label} className="shrink-0" />}
|
|
762
764
|
</CardDescription>
|
|
763
765
|
<CardTitle
|
|
764
766
|
data-slot="kpi-card-value"
|
|
765
767
|
title={valueText}
|
|
766
768
|
className={cn(
|
|
767
|
-
"col-start-1 flex min-w-0
|
|
769
|
+
"col-start-1 flex min-w-0 items-center gap-x-2 gap-y-1 leading-none font-medium tracking-tight tabular-nums",
|
|
768
770
|
// Fixed size: the step follows the card width only, never the length of the value.
|
|
769
771
|
valueSize,
|
|
772
|
+
(longValue || isProvisional) && "flex-wrap",
|
|
770
773
|
)}
|
|
771
774
|
>
|
|
772
775
|
<span data-slot="kpi-card-value-text" className={longValue ? "min-w-0 break-words" : "whitespace-nowrap"}>
|
|
@@ -784,7 +787,8 @@ export function KpiCard({
|
|
|
784
787
|
{provisionalLabel}
|
|
785
788
|
</Badge>
|
|
786
789
|
)}
|
|
787
|
-
{
|
|
790
|
+
{/* The delta sits right next to the value; a long value or several comparisons put it under. */}
|
|
791
|
+
{comparison && !stacked && !longValue && deltas}
|
|
788
792
|
</CardTitle>
|
|
789
793
|
{comparison &&
|
|
790
794
|
(stacked ? (
|
|
@@ -792,7 +796,7 @@ export function KpiCard({
|
|
|
792
796
|
{deltas}
|
|
793
797
|
</div>
|
|
794
798
|
) : (
|
|
795
|
-
<CardAction className={
|
|
799
|
+
longValue && <CardAction className={DELTA_UNDER_PLACEMENT}>{deltas}</CardAction>
|
|
796
800
|
))}
|
|
797
801
|
{hasPrevious && comparison && (
|
|
798
802
|
<div data-slot="kpi-card-previous" className="col-span-full text-sm text-muted-foreground tabular-nums">
|
|
@@ -879,7 +883,7 @@ export function KpiCard({
|
|
|
879
883
|
<Sparkline
|
|
880
884
|
data={historyPoints.map((v) => ({ value: v }))}
|
|
881
885
|
height={SPARKLINE_HEIGHT}
|
|
882
|
-
color="
|
|
886
|
+
color="chart-1"
|
|
883
887
|
highlight="latest"
|
|
884
888
|
aria-label={t.history(label, historyPoints.length)}
|
|
885
889
|
/>
|
package/registry/ui/legend.tsx
CHANGED
|
@@ -172,10 +172,25 @@ function useLocale(locale?: string | null): string {
|
|
|
172
172
|
const CHART_OTHER_COLOR = "color-mix(in oklab, var(--neutral) 50%, var(--background))"
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
176
|
-
* `role="img"`
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
* What a `ChartFrame` tells the plot inside it. A frame whose plot is an image
|
|
176
|
+
* (`image`) puts `role="img"` on its plot area, unless an `EChart` inside it
|
|
177
|
+
* `claim`s the image: then the canvas alone carries `role="img"` with this name
|
|
178
|
+
* and description (`claimed`), so the action buttons beside the canvas are not
|
|
179
|
+
* inside the image (an interactive control inside `role="img"` is a
|
|
180
|
+
* nested-interactive violation and is not exposed). A `group` frame never
|
|
181
|
+
* hands the role to the canvas. `false` outside a frame.
|
|
182
|
+
*/
|
|
183
|
+
interface ChartFramePlot {
|
|
184
|
+
image: boolean
|
|
185
|
+
/** An `EChart` inside has taken over the `role="img"`. */
|
|
186
|
+
claimed: boolean
|
|
187
|
+
/** Take over the image role; returns the release. */
|
|
188
|
+
claim: () => () => void
|
|
189
|
+
ariaLabel?: string
|
|
190
|
+
ariaLabelledBy?: string
|
|
191
|
+
ariaDescribedBy?: string
|
|
192
|
+
}
|
|
193
|
+
const ChartFrameContext = createContext<ChartFramePlot | false>(false)
|
|
179
194
|
|
|
180
195
|
/** Series palette, in order. Missing tokens are skipped, not defaulted. */
|
|
181
196
|
const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
|
|
@@ -399,6 +414,8 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
|
|
|
399
414
|
tooltip: {
|
|
400
415
|
// ECharts' default 0.4s transform lags the tooltip ~300ms behind the cursor.
|
|
401
416
|
transitionDuration: 0,
|
|
417
|
+
// Stay inside the chart: cards are often narrow and overflow-hidden.
|
|
418
|
+
confine: true,
|
|
402
419
|
backgroundColor: tokens.popover ?? undefined,
|
|
403
420
|
borderColor: line,
|
|
404
421
|
borderWidth: 1,
|
|
@@ -2274,6 +2291,10 @@ function legendLines(items: readonly string[], plotWidth?: number, reserve = 0):
|
|
|
2274
2291
|
|
|
2275
2292
|
/** Width (px) kept free at the legend's right for the PNG action buttons (two 32px buttons + gaps). */
|
|
2276
2293
|
const ACTIONS_RESERVE = 80
|
|
2294
|
+
/** Bars stay slim on wide cards (px); stacked segments round all corners and are parted by a hairline of the surface colour (px). */
|
|
2295
|
+
const BAR_MAX_WIDTH = 40
|
|
2296
|
+
const BAR_RADIUS = 3
|
|
2297
|
+
const STACK_GAP = 1.5
|
|
2277
2298
|
|
|
2278
2299
|
/** Room (px) one time label (`2026年1月`) needs, gap included. */
|
|
2279
2300
|
const TIME_LABEL_WIDTH = 80
|
|
@@ -2487,6 +2508,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2487
2508
|
const compared = cartesianSeriesValues(data, [categories[compIdx]!], "default")[0]!
|
|
2488
2509
|
return categories.map((_, i) => (i === compIdx ? compared : main[mainIdx.indexOf(i)]!))
|
|
2489
2510
|
})()
|
|
2511
|
+
// Any negative bar: the part lays out the diverging view itself (a solid zero line, category labels kept at the axis end).
|
|
2512
|
+
const diverging = kind === "bar" && values.some((s) => s.some((v) => typeof v === "number" && v < 0))
|
|
2490
2513
|
const comparisonColor = guideColor ? withAlpha(guideColor, COMPARISON_OPACITY) : undefined
|
|
2491
2514
|
|
|
2492
2515
|
/* -- x labels: the actual rows, then the forecast rows that extend them */
|
|
@@ -2696,15 +2719,17 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2696
2719
|
const seriesData = pad(values[c]!)
|
|
2697
2720
|
if (kind === "bar") {
|
|
2698
2721
|
const rounded = horizontalBars ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
2699
|
-
// Stacked:
|
|
2700
|
-
const radius = stacked
|
|
2722
|
+
// Stacked: every segment is rounded, with a hairline of the surface colour between segments (the gap).
|
|
2723
|
+
const radius = stacked ? BAR_RADIUS : rounded
|
|
2724
|
+
const gapStyle = stacked && labelBackground ? { borderColor: labelBackground, borderWidth: STACK_GAP } : {}
|
|
2701
2725
|
return {
|
|
2702
2726
|
id: SERIES_ID_PREFIX + category,
|
|
2703
2727
|
type: "bar" as const,
|
|
2704
2728
|
name: category,
|
|
2705
2729
|
stack: stacked ? "total" : undefined,
|
|
2706
2730
|
barCategoryGap,
|
|
2707
|
-
|
|
2731
|
+
barMaxWidth: BAR_MAX_WIDTH,
|
|
2732
|
+
itemStyle: { color, borderRadius: radius, ...gapStyle },
|
|
2708
2733
|
markPoint: markPointFor(c, solid),
|
|
2709
2734
|
markLine: undefined,
|
|
2710
2735
|
data: seriesData.map((value, i) => {
|
|
@@ -3000,6 +3025,14 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3000
3025
|
},
|
|
3001
3026
|
})
|
|
3002
3027
|
}
|
|
3028
|
+
if (diverging) {
|
|
3029
|
+
markLines.push({
|
|
3030
|
+
name: "zero",
|
|
3031
|
+
[horizontalBars ? "xAxis" : "yAxis"]: 0,
|
|
3032
|
+
lineStyle: { type: "solid", width: 1, color: guideColor },
|
|
3033
|
+
label: { show: false },
|
|
3034
|
+
})
|
|
3035
|
+
}
|
|
3003
3036
|
if (forecast) {
|
|
3004
3037
|
const forecastSpan =
|
|
3005
3038
|
plotWidth && labels.length > 1
|
|
@@ -3145,6 +3178,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3145
3178
|
show: horizontalBars ? showYAxis : showXAxis,
|
|
3146
3179
|
boundaryGap: kind === "bar",
|
|
3147
3180
|
inverse: horizontalBars,
|
|
3181
|
+
// 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.
|
|
3182
|
+
axisLine: diverging ? { show: false, onZero: false } : undefined,
|
|
3148
3183
|
axisTick: { show: false },
|
|
3149
3184
|
axisLabel: {
|
|
3150
3185
|
hideOverlap: true,
|
|
@@ -3247,7 +3282,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3247
3282
|
// Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
|
|
3248
3283
|
...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
|
|
3249
3284
|
? { left: 0, right: 0 }
|
|
3250
|
-
: { right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3285
|
+
: { left: 0, right: reserveActions ? ACTIONS_RESERVE : 0 }),
|
|
3251
3286
|
// Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
|
|
3252
3287
|
type: "plain",
|
|
3253
3288
|
icon: "circle",
|
|
@@ -3768,6 +3803,10 @@ export interface LegendProps {
|
|
|
3768
3803
|
labels?: Record<string, string>
|
|
3769
3804
|
/** 並べ方。`"horizontal"`(既定。折り返す横並び)か `"vertical"`(縦並び)。 */
|
|
3770
3805
|
layout?: "horizontal" | "vertical"
|
|
3806
|
+
/** 隠している系列名(`onHiddenChange` と対)。グラフに渡す `categories` からこの系列を外す。色は凡例の全 `categories` で決まるので、外しても残りの色は変わらない。 */
|
|
3807
|
+
hidden?: string[]
|
|
3808
|
+
/** 渡すと凡例のチップが押せるボタンになり、押すたびに系列を隠す・戻す(キーボードで操作でき、隠れたチップは薄くなる)。新しい `hidden` が渡される。 */
|
|
3809
|
+
onHiddenChange?: (hidden: string[]) => void
|
|
3771
3810
|
/** 凡例の読み上げ名。省略時は「凡例」。 */
|
|
3772
3811
|
ariaLabel?: string
|
|
3773
3812
|
className?: string
|
|
@@ -3791,7 +3830,7 @@ const TEXT = {
|
|
|
3791
3830
|
|
|
3792
3831
|
/** Plain-HTML legend. Renders nothing for an empty `categories`. */
|
|
3793
3832
|
export const Legend = forwardRef<HTMLUListElement, LegendProps>(function Legend(
|
|
3794
|
-
{ categories, colors, labels, layout = "horizontal", ariaLabel, className },
|
|
3833
|
+
{ categories, colors, labels, layout = "horizontal", hidden = [], onHiddenChange, ariaLabel, className },
|
|
3795
3834
|
ref,
|
|
3796
3835
|
) {
|
|
3797
3836
|
const t = TEXT[uiLanguage(useLocale())]
|
|
@@ -3805,12 +3844,31 @@ export const Legend = forwardRef<HTMLUListElement, LegendProps>(function Legend(
|
|
|
3805
3844
|
aria-label={ariaLabel ?? t.legend}
|
|
3806
3845
|
className={cn("m-0 flex list-none gap-x-4 gap-y-1 p-0 text-xs", layout === "vertical" ? "flex-col" : "flex-row flex-wrap", className)}
|
|
3807
3846
|
>
|
|
3808
|
-
{categories.map((category, i) =>
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3847
|
+
{categories.map((category, i) => {
|
|
3848
|
+
const off = hidden.includes(category)
|
|
3849
|
+
const chip = (
|
|
3850
|
+
<>
|
|
3851
|
+
<span aria-hidden="true" className={cn("size-2.5 shrink-0 rounded-[2px]", off && "opacity-30")} style={{ background: swatches[i] }} />
|
|
3852
|
+
<span className={cn("min-w-0 truncate text-muted-foreground", off && "line-through opacity-60")}>{labelOf(category, labels)}</span>
|
|
3853
|
+
</>
|
|
3854
|
+
)
|
|
3855
|
+
return (
|
|
3856
|
+
<li key={category} data-slot="legend-item" data-hidden={off || undefined} className="flex min-w-0 items-center gap-1.5">
|
|
3857
|
+
{onHiddenChange ? (
|
|
3858
|
+
<button
|
|
3859
|
+
type="button"
|
|
3860
|
+
aria-pressed={!off}
|
|
3861
|
+
onClick={() => onHiddenChange(off ? hidden.filter((c) => c !== category) : [...hidden, category])}
|
|
3862
|
+
className="flex min-w-0 cursor-pointer items-center gap-1.5 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
3863
|
+
>
|
|
3864
|
+
{chip}
|
|
3865
|
+
</button>
|
|
3866
|
+
) : (
|
|
3867
|
+
chip
|
|
3868
|
+
)}
|
|
3869
|
+
</li>
|
|
3870
|
+
)
|
|
3871
|
+
})}
|
|
3814
3872
|
</ul>
|
|
3815
3873
|
)
|
|
3816
3874
|
})
|