@squadbase/vantage 0.7.1-dev1.9d223a2 → 0.7.1-dev1.9d23147
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-AON4MAQQ.js} +3 -3
- package/dist/{build-U7GO26HR.js.map → build-AON4MAQQ.js.map} +1 -1
- package/dist/{chunk-MQUIAP66.js → chunk-5HBUBVQ3.js} +28 -7
- 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-YJQCB3UP.js → chunk-I2GWIPTD.js} +219 -67
- package/dist/chunk-I2GWIPTD.js.map +1 -0
- package/dist/{chunk-SDJTTMLK.js → chunk-SR43D6EF.js} +13 -8
- package/dist/chunk-SR43D6EF.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 +223 -6
- package/dist/components/index.js +4 -4
- package/dist/{dev-O42JFQ64.js → dev-BZ7FXDB3.js} +3 -3
- package/dist/{dev-O42JFQ64.js.map → dev-BZ7FXDB3.js.map} +1 -1
- package/dist/doc/index.js +1 -1
- package/dist/slide/index.js +1 -1
- package/dist/table/index.js +4 -4
- package/dist/ui/index.d.ts +35 -4
- package/dist/ui/index.js +174 -10
- package/dist/ui/index.js.map +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +2 -2
- package/registry/parts.json +101 -3
- 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-MQUIAP66.js.map +0 -1
- package/dist/chunk-P2KIHF65.js.map +0 -1
- package/dist/chunk-SDJTTMLK.js.map +0 -1
- package/dist/chunk-YJQCB3UP.js.map +0 -1
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squadbase/vantage",
|
|
3
|
-
"version": "0.7.1-dev1.
|
|
3
|
+
"version": "0.7.1-dev1.9d23147",
|
|
4
4
|
"description": "Data dashboard framework for Squadbase Editor. Vite-powered, config-free, one-file first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"vitest": "^5.0.5",
|
|
153
153
|
"@squadbase/squadkit": "0.0.0"
|
|
154
154
|
},
|
|
155
|
-
"gitHead": "
|
|
155
|
+
"gitHead": "9d2314799ca93035f6d229cdcf503b49d7e07198",
|
|
156
156
|
"scripts": {
|
|
157
157
|
"build": "node scripts/build-docs.mjs && node scripts/build.mjs",
|
|
158
158
|
"build:js": "tsup --no-dts",
|
package/registry/parts.json
CHANGED
|
@@ -2130,10 +2130,10 @@
|
|
|
2130
2130
|
},
|
|
2131
2131
|
{
|
|
2132
2132
|
"name": "color",
|
|
2133
|
-
"type": "
|
|
2133
|
+
"type": "ChartColor | undefined",
|
|
2134
2134
|
"required": false,
|
|
2135
2135
|
"json": true,
|
|
2136
|
-
"doc": "
|
|
2136
|
+
"doc": "Line / bar colour: a theme series token (`\"chart-1\"`〜`\"chart-5\"`), a meaning colour (`\"positive\"` / `\"negative\"` / `\"warning\"` / `\"neutral\"` / `\"info\"`), or any CSS colour (`\"#16a34a\"`, `\"var(--destr…"
|
|
2137
2137
|
},
|
|
2138
2138
|
{
|
|
2139
2139
|
"name": "area",
|
|
@@ -4830,6 +4830,17 @@
|
|
|
4830
4830
|
"json": true,
|
|
4831
4831
|
"doc": "年度の開始月(1-12)。"
|
|
4832
4832
|
},
|
|
4833
|
+
{
|
|
4834
|
+
"name": "comparison",
|
|
4835
|
+
"type": "\"previous-period\" | \"previous-year\" | undefined",
|
|
4836
|
+
"required": false,
|
|
4837
|
+
"json": true,
|
|
4838
|
+
"values": [
|
|
4839
|
+
"previous-period",
|
|
4840
|
+
"previous-year"
|
|
4841
|
+
],
|
|
4842
|
+
"doc": "比較期間。"
|
|
4843
|
+
},
|
|
4833
4844
|
{
|
|
4834
4845
|
"name": "maxDate",
|
|
4835
4846
|
"type": "Date | undefined",
|
|
@@ -5134,6 +5145,13 @@
|
|
|
5134
5145
|
"required": false,
|
|
5135
5146
|
"json": true
|
|
5136
5147
|
},
|
|
5148
|
+
{
|
|
5149
|
+
"name": "headline",
|
|
5150
|
+
"type": "ReactNode",
|
|
5151
|
+
"required": false,
|
|
5152
|
+
"json": true,
|
|
5153
|
+
"doc": "One headline number or status at the right end of the header (\"212 tasks\", a `StatusBadge`)."
|
|
5154
|
+
},
|
|
5137
5155
|
{
|
|
5138
5156
|
"name": "actions",
|
|
5139
5157
|
"type": "ReactNode",
|
|
@@ -5144,7 +5162,8 @@
|
|
|
5144
5162
|
"name": "footer",
|
|
5145
5163
|
"type": "ReactNode",
|
|
5146
5164
|
"required": false,
|
|
5147
|
-
"json": true
|
|
5165
|
+
"json": true,
|
|
5166
|
+
"doc": "One line at the bottom of the card: a summary fact on the left, an action or hint on the right."
|
|
5148
5167
|
},
|
|
5149
5168
|
{
|
|
5150
5169
|
"name": "headerClassName",
|
|
@@ -5216,6 +5235,85 @@
|
|
|
5216
5235
|
"json": true
|
|
5217
5236
|
}
|
|
5218
5237
|
]
|
|
5238
|
+
},
|
|
5239
|
+
{
|
|
5240
|
+
"name": "PageShellHeader",
|
|
5241
|
+
"module": "components/page-shell",
|
|
5242
|
+
"doc": "PageShellHeader — the page header band (title and update time, source / period, overall status); inside `PageShell` without `title`, with the title in `PageShellHeading`.",
|
|
5243
|
+
"props": [
|
|
5244
|
+
{
|
|
5245
|
+
"name": "updatedAt",
|
|
5246
|
+
"type": "string | number | Date | undefined",
|
|
5247
|
+
"required": false,
|
|
5248
|
+
"json": true,
|
|
5249
|
+
"doc": "When the data was last updated."
|
|
5250
|
+
},
|
|
5251
|
+
{
|
|
5252
|
+
"name": "updatedAtVariant",
|
|
5253
|
+
"type": "\"relative\" | \"absolute-first\" | undefined",
|
|
5254
|
+
"required": false,
|
|
5255
|
+
"json": true,
|
|
5256
|
+
"values": [
|
|
5257
|
+
"relative",
|
|
5258
|
+
"absolute-first"
|
|
5259
|
+
],
|
|
5260
|
+
"doc": "`\"relative\"` (default) or `\"absolute-first\"` (\"16:41 更新(23 分前)\") for pages where \"now\" matters."
|
|
5261
|
+
},
|
|
5262
|
+
{
|
|
5263
|
+
"name": "timeZone",
|
|
5264
|
+
"type": "string | undefined",
|
|
5265
|
+
"required": false,
|
|
5266
|
+
"json": true,
|
|
5267
|
+
"doc": "IANA time zone for the absolute time of `updatedAt`."
|
|
5268
|
+
},
|
|
5269
|
+
{
|
|
5270
|
+
"name": "source",
|
|
5271
|
+
"type": "string | undefined",
|
|
5272
|
+
"required": false,
|
|
5273
|
+
"json": true,
|
|
5274
|
+
"doc": "Where the data comes from."
|
|
5275
|
+
},
|
|
5276
|
+
{
|
|
5277
|
+
"name": "period",
|
|
5278
|
+
"type": "{ start: string | number | Date; end: string | number | Date; } | undefined",
|
|
5279
|
+
"required": false,
|
|
5280
|
+
"json": true,
|
|
5281
|
+
"doc": "The period the data covers."
|
|
5282
|
+
},
|
|
5283
|
+
{
|
|
5284
|
+
"name": "status",
|
|
5285
|
+
"type": "ReactNode",
|
|
5286
|
+
"required": false,
|
|
5287
|
+
"json": true,
|
|
5288
|
+
"doc": "The page's overall state (an alert, a status badge)."
|
|
5289
|
+
},
|
|
5290
|
+
{
|
|
5291
|
+
"name": "children",
|
|
5292
|
+
"type": "ReactNode",
|
|
5293
|
+
"required": false,
|
|
5294
|
+
"json": true
|
|
5295
|
+
}
|
|
5296
|
+
]
|
|
5297
|
+
},
|
|
5298
|
+
{
|
|
5299
|
+
"name": "SampleData",
|
|
5300
|
+
"module": "components/sample-data",
|
|
5301
|
+
"doc": "SampleData — marks the values inside it as a sample: they are not connected to real data yet (a draft laid out before its queries are wired, a screen built from column definitions only).",
|
|
5302
|
+
"props": [
|
|
5303
|
+
{
|
|
5304
|
+
"name": "label",
|
|
5305
|
+
"type": "string | undefined",
|
|
5306
|
+
"required": false,
|
|
5307
|
+
"json": true,
|
|
5308
|
+
"doc": "印の文言。"
|
|
5309
|
+
},
|
|
5310
|
+
{
|
|
5311
|
+
"name": "children",
|
|
5312
|
+
"type": "ReactNode",
|
|
5313
|
+
"required": false,
|
|
5314
|
+
"json": true
|
|
5315
|
+
}
|
|
5316
|
+
]
|
|
5219
5317
|
}
|
|
5220
5318
|
]
|
|
5221
5319
|
}
|
|
@@ -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",
|