basalt-ui 1.31.0 → 1.32.1
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/CHANGELOG.md +34 -0
- package/MIGRATING.md +212 -20
- package/agent/rules/basalt-mantine.md +4 -4
- package/configs/oxlint-plugin.js +240 -59
- package/configs/oxlint-plugin.test.ts +329 -1
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +18 -66
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +5 -26
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +10 -68
- package/dist/charts/kinds/band-chrome.d.ts +31 -0
- package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
- package/dist/charts/kinds/band-chrome.js +71 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +49 -76
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +5 -23
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +23 -40
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +5 -133
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +16 -1
- package/dist/charts/primitives/HoverOverlay.d.ts +24 -9
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +17 -1
- package/dist/charts/primitives/chart-card-context.d.ts +17 -13
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -1
- package/dist/charts/primitives/chart-card-context.js +10 -5
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +25 -31
- package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
- package/dist/charts/primitives/legend-disclosure.js +153 -0
- package/dist/controls/controls.module.css +5 -0
- package/dist/controls/sync-button.module.css +1 -0
- package/dist/dashboard/stat-group.module.css +19 -64
- package/dist/dashboard/widget-grid.module.css +11 -38
- package/dist/data/data-table-state.d.ts +83 -0
- package/dist/data/data-table-state.d.ts.map +1 -0
- package/dist/data/data-table-state.js +139 -0
- package/dist/data/data-table.d.ts +40 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +179 -277
- package/dist/forms/form-layout.d.ts +6 -1
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.module.css +6 -13
- package/dist/guard/index.d.ts +2 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +10 -14
- package/dist/notifications/center.d.ts.map +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +12 -10
- package/dist/shell/aside-docking.d.ts +31 -0
- package/dist/shell/aside-docking.d.ts.map +1 -0
- package/dist/shell/aside-docking.js +19 -0
- package/dist/shell/index.d.ts +0 -11
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -14
- package/dist/shell/page-bar.d.ts +24 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.d.ts +1 -1
- package/dist/theme/floating.module.css +1 -0
- package/dist/theme-lab/derive-controls.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +4 -0
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/src/charts/kinds/BandStrip.tsx +18 -64
- package/src/charts/kinds/DualPanel.tsx +4 -24
- package/src/charts/kinds/MirroredBars.tsx +12 -66
- package/src/charts/kinds/band-chrome.tsx +91 -0
- package/src/charts/primitives/CartesianChart.tsx +79 -122
- package/src/charts/primitives/ChartCard.tsx +5 -27
- package/src/charts/primitives/ChartFrame.tsx +37 -76
- package/src/charts/primitives/ChartLegend.tsx +7 -154
- package/src/charts/primitives/ChartTooltip.tsx +16 -0
- package/src/charts/primitives/HoverOverlay.tsx +52 -21
- package/src/charts/primitives/chart-card-context.ts +25 -16
- package/src/charts/primitives/chart-frame-layout.ts +44 -56
- package/src/charts/primitives/legend-disclosure.tsx +177 -0
- package/src/controls/controls.module.css +5 -0
- package/src/controls/sync-button.module.css +1 -0
- package/src/dashboard/stat-group.module.css +19 -64
- package/src/dashboard/widget-grid.module.css +11 -38
- package/src/data/data-table-state.ts +218 -0
- package/src/data/data-table.tsx +134 -218
- package/src/forms/form-layout.module.css +6 -13
- package/src/forms/form-layout.tsx +6 -1
- package/src/guard/index.ts +38 -34
- package/src/notifications/center.tsx +3 -0
- package/src/provider/index.tsx +18 -17
- package/src/shell/aside-docking.ts +54 -0
- package/src/shell/index.tsx +15 -37
- package/src/shell/page-bar.tsx +24 -0
- package/src/shell/use-breakpoint.ts +2 -2
- package/src/theme/floating.module.css +1 -0
- package/src/theme-lab/derive-controls.tsx +2 -0
- package/src/tokens/index.ts +5 -0
|
@@ -8,21 +8,15 @@ import type { ChartMargin } from '../../tokens'
|
|
|
8
8
|
import type { CursorResolution } from '../cursor/resolve'
|
|
9
9
|
import { useBandPlot } from '../hooks/useBandPlot'
|
|
10
10
|
import type { BandFold, BandTooltipConfig } from '../hooks/useBandPlot'
|
|
11
|
-
import { AxisBottomDate } from '../primitives/Axes'
|
|
12
11
|
import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
|
|
13
12
|
import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
|
|
14
|
-
import {
|
|
15
|
-
ChartTooltipFloat,
|
|
16
|
-
TooltipBody,
|
|
17
|
-
TooltipHeader,
|
|
18
|
-
TooltipRow,
|
|
19
|
-
} from '../primitives/ChartTooltip'
|
|
13
|
+
import { TooltipRow } from '../primitives/ChartTooltip'
|
|
20
14
|
import { Crosshair } from '../primitives/Crosshair'
|
|
21
15
|
import { HatchPattern, hatchFill, hatchSizeFor } from '../primitives/HatchPattern'
|
|
22
|
-
import { HoverOverlay } from '../primitives/HoverOverlay'
|
|
23
16
|
import type { ChartState } from '../primitives/ChartPending'
|
|
24
17
|
import type { ChartLegendConfig, SeriesStyle } from '../series'
|
|
25
18
|
import { fmtAxisDate } from '../utils/format'
|
|
19
|
+
import { BandAxisOverlay, BandTooltip, clampFraction } from './band-chrome'
|
|
26
20
|
import { isDev } from '../../common/is-dev'
|
|
27
21
|
|
|
28
22
|
/**
|
|
@@ -256,7 +250,7 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
256
250
|
...(tooltip !== undefined && { tooltip }),
|
|
257
251
|
})
|
|
258
252
|
|
|
259
|
-
const { bands, margin, plotWidth,
|
|
253
|
+
const { bands, margin, plotWidth, point, crosshairX, step, bandWidth } = band
|
|
260
254
|
const stripHeight = Math.max(plot.height - margin.top - margin.bottom, 1)
|
|
261
255
|
const hatchId = `${chartId}-band-absent`
|
|
262
256
|
if (absentState !== undefined && !styleByKey.has(absentState)) {
|
|
@@ -351,8 +345,6 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
351
345
|
|
|
352
346
|
if (plotWidth <= 0 || bands.length === 0) return null
|
|
353
347
|
|
|
354
|
-
const cfg = tooltip === false ? undefined : tooltip
|
|
355
|
-
const badge = cfg?.label === undefined || point === null ? null : cfg.label(point)
|
|
356
348
|
const row = point === null ? null : deriveBandRow(getBand(point), styleByKey, hidden, point)
|
|
357
349
|
|
|
358
350
|
return (
|
|
@@ -383,69 +375,31 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
383
375
|
|
|
384
376
|
{crosshairX !== null && <Crosshair x={crosshairX} top={0} bottom={stripHeight} />}
|
|
385
377
|
|
|
386
|
-
<
|
|
387
|
-
|
|
388
|
-
scale={scale}
|
|
389
|
-
tickValues={band.tickValues}
|
|
390
|
-
tickFormat={(v) => formatX(String(v))}
|
|
391
|
-
anchorTerminals={band.xAnchorTerminals}
|
|
392
|
-
{...(band.xWrapWidth !== undefined && { wrapWidth: band.xWrapWidth })}
|
|
393
|
-
/>
|
|
394
|
-
|
|
395
|
-
<HoverOverlay
|
|
396
|
-
width={plotWidth}
|
|
378
|
+
<BandAxisOverlay
|
|
379
|
+
band={band}
|
|
397
380
|
height={stripHeight}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
onLeave={cursor.onPointerLeave}
|
|
402
|
-
onKeyDown={cursor.onKeyDown}
|
|
403
|
-
onBlur={cursor.onBlur}
|
|
404
|
-
valueMax={Math.max(bands.length - 1, 0)}
|
|
405
|
-
{...(point !== null && {
|
|
406
|
-
valueNow: bands.indexOf(point),
|
|
407
|
-
valueText: formatX(getX(point)),
|
|
408
|
-
})}
|
|
409
|
-
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
381
|
+
getX={getX}
|
|
382
|
+
formatX={formatX}
|
|
383
|
+
ariaLabel={ariaLabel}
|
|
410
384
|
/>
|
|
411
385
|
</Group>
|
|
412
386
|
</svg>
|
|
413
387
|
|
|
414
|
-
{band
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
{
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
{
|
|
388
|
+
<BandTooltip band={band} tooltip={tooltip} getX={getX} hidden={hidden}>
|
|
389
|
+
{row !== null && (
|
|
390
|
+
<TooltipRow
|
|
391
|
+
color={row.color}
|
|
392
|
+
label={row.label}
|
|
393
|
+
value={row.value}
|
|
394
|
+
shape={row.shape}
|
|
395
|
+
dashed={row.dashed}
|
|
422
396
|
/>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
{row !== null && (
|
|
426
|
-
<TooltipRow
|
|
427
|
-
color={row.color}
|
|
428
|
-
label={row.label}
|
|
429
|
-
value={row.value}
|
|
430
|
-
shape={row.shape}
|
|
431
|
-
dashed={row.dashed}
|
|
432
|
-
/>
|
|
433
|
-
)}
|
|
434
|
-
{cfg?.extraRows?.(point, { hidden })}
|
|
435
|
-
</TooltipBody>
|
|
436
|
-
</ChartTooltipFloat>
|
|
437
|
-
)}
|
|
397
|
+
)}
|
|
398
|
+
</BandTooltip>
|
|
438
399
|
</>
|
|
439
400
|
)
|
|
440
401
|
}
|
|
441
402
|
|
|
442
|
-
/** A 0..1 share. Non-finite (a 0/0 fold count) reads as "nothing is absent" — the conservative
|
|
443
|
-
* end, since the alternative is a band that does not render at all. */
|
|
444
|
-
function clampFraction(value: number | undefined): number {
|
|
445
|
-
if (value === undefined || !Number.isFinite(value)) return 0
|
|
446
|
-
return Math.min(Math.max(value, 0), 1)
|
|
447
|
-
}
|
|
448
|
-
|
|
449
403
|
/**
|
|
450
404
|
* The strip's one derived row: the hovered band's STATE, named and formatted by its own `series`
|
|
451
405
|
* entry. A strip cannot show a row for a state it does not draw, and cannot name a state the
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
TooltipBody,
|
|
14
14
|
TooltipHeader,
|
|
15
15
|
TooltipRow,
|
|
16
|
+
TooltipRows,
|
|
16
17
|
} from '../primitives/ChartTooltip'
|
|
17
18
|
import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
|
|
18
19
|
import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
|
|
19
20
|
import { Crosshair, SeriesDot } from '../primitives/Crosshair'
|
|
20
|
-
import { HoverOverlay } from '../primitives/HoverOverlay'
|
|
21
|
+
import { cursorSliderProps, HoverOverlay } from '../primitives/HoverOverlay'
|
|
21
22
|
import { ZoneRects } from '../primitives/ZoneRects'
|
|
22
23
|
import type { ZoneSpec } from '../primitives/ZoneRects'
|
|
23
24
|
import type { ChartState } from '../primitives/ChartPending'
|
|
@@ -595,18 +596,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
595
596
|
<HoverOverlay
|
|
596
597
|
width={xMax}
|
|
597
598
|
height={topH + PANE_GAP}
|
|
598
|
-
|
|
599
|
-
onDown={cursor.onPointerDown}
|
|
600
|
-
onUp={cursor.onPointerUp}
|
|
601
|
-
onLeave={cursor.onPointerLeave}
|
|
602
|
-
onKeyDown={cursor.onKeyDown}
|
|
603
|
-
onBlur={cursor.onBlur}
|
|
604
|
-
valueMax={Math.max(data.length - 1, 0)}
|
|
605
|
-
{...(point !== null && {
|
|
606
|
-
valueNow: data.indexOf(point),
|
|
607
|
-
valueText: formatX(getX(point)),
|
|
608
|
-
})}
|
|
609
|
-
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
599
|
+
{...cursorSliderProps({ cursor, data, getX, formatX, ariaLabel })}
|
|
610
600
|
/>
|
|
611
601
|
</Group>
|
|
612
602
|
|
|
@@ -688,17 +678,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
688
678
|
})()}
|
|
689
679
|
/>
|
|
690
680
|
<TooltipBody>
|
|
691
|
-
{deriveTooltipRows(visibleSeries, point, formatTop)
|
|
692
|
-
<TooltipRow
|
|
693
|
-
key={row.key}
|
|
694
|
-
color={row.color}
|
|
695
|
-
label={row.label}
|
|
696
|
-
value={row.value}
|
|
697
|
-
shape={row.shape}
|
|
698
|
-
dashed={row.dashed}
|
|
699
|
-
{...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
|
|
700
|
-
/>
|
|
701
|
-
))}
|
|
681
|
+
<TooltipRows rows={deriveTooltipRows(visibleSeries, point, formatTop)} />
|
|
702
682
|
{barVisible &&
|
|
703
683
|
(() => {
|
|
704
684
|
const v = getBar(point)
|
|
@@ -10,22 +10,17 @@ import type { CursorResolution } from '../cursor/resolve'
|
|
|
10
10
|
import { useBandPlot } from '../hooks/useBandPlot'
|
|
11
11
|
import type { BandFold, BandTooltipConfig } from '../hooks/useBandPlot'
|
|
12
12
|
import { probeAxisLabels } from '../layout/auto-margin'
|
|
13
|
-
import {
|
|
13
|
+
import { AxisLeftNumeric } from '../primitives/Axes'
|
|
14
14
|
import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
|
|
15
15
|
import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
|
|
16
|
-
import {
|
|
17
|
-
ChartTooltipFloat,
|
|
18
|
-
TooltipBody,
|
|
19
|
-
TooltipHeader,
|
|
20
|
-
TooltipRow,
|
|
21
|
-
} from '../primitives/ChartTooltip'
|
|
16
|
+
import { TooltipRows } from '../primitives/ChartTooltip'
|
|
22
17
|
import { Crosshair } from '../primitives/Crosshair'
|
|
23
18
|
import { HatchPattern, hatchFill, hatchSizeFor } from '../primitives/HatchPattern'
|
|
24
|
-
import { HoverOverlay } from '../primitives/HoverOverlay'
|
|
25
19
|
import type { ChartState } from '../primitives/ChartPending'
|
|
26
20
|
import { deriveTooltipRows } from '../series'
|
|
27
21
|
import type { ChartLegendConfig, ChartSeries } from '../series'
|
|
28
22
|
import { fmtAxisDate } from '../utils/format'
|
|
23
|
+
import { BandAxisOverlay, BandTooltip, clampFraction } from './band-chrome'
|
|
29
24
|
|
|
30
25
|
/** One of the two panes. Each resolves its own domain — that is the whole point of the kind. */
|
|
31
26
|
export type MirroredBarPane = {
|
|
@@ -242,7 +237,7 @@ function MirroredBarsPlot<T>(props: MirroredBarsPlotProps<T>) {
|
|
|
242
237
|
...(tooltip !== undefined && { tooltip }),
|
|
243
238
|
})
|
|
244
239
|
|
|
245
|
-
const { bands, margin, plotWidth,
|
|
240
|
+
const { bands, margin, plotWidth, point, crosshairX, step, bandWidth } = band
|
|
246
241
|
|
|
247
242
|
// ── Pass 2: the pane rects, now that the plot height is known ───────────────────────────────
|
|
248
243
|
const barBand = Math.max(plot.height - margin.top - margin.bottom, 2)
|
|
@@ -346,8 +341,6 @@ function MirroredBarsPlot<T>(props: MirroredBarsPlotProps<T>) {
|
|
|
346
341
|
|
|
347
342
|
if (plotWidth <= 0 || bands.length === 0) return null
|
|
348
343
|
|
|
349
|
-
const cfg = tooltip === false ? undefined : tooltip
|
|
350
|
-
const badge = cfg?.label === undefined || point === null ? null : cfg.label(point)
|
|
351
344
|
// Each pane's row falls back to ITS OWN axis formatter, never the other pane's — the two panes
|
|
352
345
|
// are in different units by construction (an explicit per-series `formatValue` still wins).
|
|
353
346
|
const visibleSeries = series
|
|
@@ -419,70 +412,23 @@ function MirroredBarsPlot<T>(props: MirroredBarsPlotProps<T>) {
|
|
|
419
412
|
|
|
420
413
|
{crosshairX !== null && <Crosshair x={crosshairX} top={0} bottom={barBand} />}
|
|
421
414
|
|
|
422
|
-
<
|
|
423
|
-
|
|
424
|
-
scale={scale}
|
|
425
|
-
tickValues={band.tickValues}
|
|
426
|
-
tickFormat={(v) => formatX(String(v))}
|
|
427
|
-
anchorTerminals={band.xAnchorTerminals}
|
|
428
|
-
{...(band.xWrapWidth !== undefined && { wrapWidth: band.xWrapWidth })}
|
|
429
|
-
/>
|
|
430
|
-
|
|
431
|
-
<HoverOverlay
|
|
432
|
-
width={plotWidth}
|
|
415
|
+
<BandAxisOverlay
|
|
416
|
+
band={band}
|
|
433
417
|
height={barBand}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
onLeave={cursor.onPointerLeave}
|
|
438
|
-
onKeyDown={cursor.onKeyDown}
|
|
439
|
-
onBlur={cursor.onBlur}
|
|
440
|
-
valueMax={Math.max(bands.length - 1, 0)}
|
|
441
|
-
{...(point !== null && {
|
|
442
|
-
valueNow: bands.indexOf(point),
|
|
443
|
-
valueText: formatX(getX(point)),
|
|
444
|
-
})}
|
|
445
|
-
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
418
|
+
getX={getX}
|
|
419
|
+
formatX={formatX}
|
|
420
|
+
ariaLabel={ariaLabel}
|
|
446
421
|
/>
|
|
447
422
|
</Group>
|
|
448
423
|
</svg>
|
|
449
424
|
|
|
450
|
-
{band
|
|
451
|
-
<
|
|
452
|
-
|
|
453
|
-
date={getX(point)}
|
|
454
|
-
{...(cfg?.formatHeader !== undefined && {
|
|
455
|
-
format: (key: string) => cfg.formatHeader?.(key, point) ?? key,
|
|
456
|
-
})}
|
|
457
|
-
{...(badge !== null && { label: badge.text, labelColor: badge.color })}
|
|
458
|
-
/>
|
|
459
|
-
<TooltipBody>
|
|
460
|
-
{cfg?.prependRows?.(point, { hidden })}
|
|
461
|
-
{rows.map((row) => (
|
|
462
|
-
<TooltipRow
|
|
463
|
-
key={row.key}
|
|
464
|
-
color={row.color}
|
|
465
|
-
label={row.label}
|
|
466
|
-
value={row.value}
|
|
467
|
-
shape={row.shape}
|
|
468
|
-
dashed={row.dashed}
|
|
469
|
-
{...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
|
|
470
|
-
/>
|
|
471
|
-
))}
|
|
472
|
-
{cfg?.extraRows?.(point, { hidden })}
|
|
473
|
-
</TooltipBody>
|
|
474
|
-
</ChartTooltipFloat>
|
|
475
|
-
)}
|
|
425
|
+
<BandTooltip band={band} tooltip={tooltip} getX={getX} hidden={hidden}>
|
|
426
|
+
<TooltipRows rows={rows} />
|
|
427
|
+
</BandTooltip>
|
|
476
428
|
</>
|
|
477
429
|
)
|
|
478
430
|
}
|
|
479
431
|
|
|
480
|
-
/** A 0..1 share, non-finite included — see `BandStrip`'s identical guard. */
|
|
481
|
-
function clampFraction(value: number | undefined): number {
|
|
482
|
-
if (value === undefined || !Number.isFinite(value)) return 0
|
|
483
|
-
return Math.min(Math.max(value, 0), 1)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
432
|
/** A drawable value, or null. See the call site for why NaN is not drawable. */
|
|
487
433
|
function finiteOrNull(v: number | null | undefined): number | null {
|
|
488
434
|
return v === null || v === undefined || Number.isNaN(v) ? null : v
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { BandPlot, BandTooltipConfig } from '../hooks/useBandPlot'
|
|
3
|
+
import { AxisBottomDate } from '../primitives/Axes'
|
|
4
|
+
import { ChartTooltipFloat, TooltipBody, TooltipHeader } from '../primitives/ChartTooltip'
|
|
5
|
+
import { cursorSliderProps, HoverOverlay } from '../primitives/HoverOverlay'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The chrome `BandStrip` and `MirroredBars` share around their own marks — the slot axis, the
|
|
9
|
+
* keyboard/pointer overlay and the tooltip shell — so the two `useBandPlot` kinds cannot drift in
|
|
10
|
+
* any of them. Internal; neither is exported from a barrel.
|
|
11
|
+
*
|
|
12
|
+
* theme-allow-file hand-rolled-plot — the slot axis and overlay of the two declared banded
|
|
13
|
+
* exceptions (`BandStrip`, `MirroredBars`), which carry the same waiver for the same reason.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A 0..1 share. Non-finite (a 0/0 fold count) reads as "nothing is absent" — the conservative
|
|
17
|
+
* end, since the alternative is a band that does not render at all. */
|
|
18
|
+
export function clampFraction(value: number | undefined): number {
|
|
19
|
+
if (value === undefined || !Number.isFinite(value)) return 0
|
|
20
|
+
return Math.min(Math.max(value, 0), 1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The slot axis under a `height`-tall band row, plus the hover/keyboard overlay over it. */
|
|
24
|
+
export function BandAxisOverlay<T>({
|
|
25
|
+
band,
|
|
26
|
+
height,
|
|
27
|
+
getX,
|
|
28
|
+
formatX,
|
|
29
|
+
ariaLabel,
|
|
30
|
+
}: {
|
|
31
|
+
band: BandPlot<T>
|
|
32
|
+
height: number
|
|
33
|
+
getX: (d: T) => string
|
|
34
|
+
formatX: (key: string) => string
|
|
35
|
+
ariaLabel: string | undefined
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<AxisBottomDate
|
|
40
|
+
top={height}
|
|
41
|
+
scale={band.scale}
|
|
42
|
+
tickValues={band.tickValues}
|
|
43
|
+
tickFormat={(v) => formatX(String(v))}
|
|
44
|
+
anchorTerminals={band.xAnchorTerminals}
|
|
45
|
+
{...(band.xWrapWidth !== undefined && { wrapWidth: band.xWrapWidth })}
|
|
46
|
+
/>
|
|
47
|
+
<HoverOverlay
|
|
48
|
+
width={band.plotWidth}
|
|
49
|
+
height={height}
|
|
50
|
+
{...cursorSliderProps({ cursor: band.cursor, data: band.bands, getX, formatX, ariaLabel })}
|
|
51
|
+
/>
|
|
52
|
+
</>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The tooltip shell: header (+ badge), the consumer's prepended rows, the kind's own derived
|
|
57
|
+
* `children` rows, the consumer's extra rows. Renders nothing off-hover or with `tooltip: false`. */
|
|
58
|
+
export function BandTooltip<T>({
|
|
59
|
+
band,
|
|
60
|
+
tooltip,
|
|
61
|
+
getX,
|
|
62
|
+
hidden,
|
|
63
|
+
children,
|
|
64
|
+
}: {
|
|
65
|
+
band: BandPlot<T>
|
|
66
|
+
tooltip: BandTooltipConfig<T> | false | undefined
|
|
67
|
+
getX: (d: T) => string
|
|
68
|
+
hidden: ReadonlySet<string>
|
|
69
|
+
children: ReactNode
|
|
70
|
+
}) {
|
|
71
|
+
const { point } = band
|
|
72
|
+
if (!band.showTooltip || point === null) return null
|
|
73
|
+
const cfg = tooltip === false ? undefined : tooltip
|
|
74
|
+
const badge = cfg?.label === undefined ? null : cfg.label(point)
|
|
75
|
+
return (
|
|
76
|
+
<ChartTooltipFloat anchor={band.tooltipAnchor} ariaLive={band.ariaLive}>
|
|
77
|
+
<TooltipHeader
|
|
78
|
+
date={getX(point)}
|
|
79
|
+
{...(cfg?.formatHeader !== undefined && {
|
|
80
|
+
format: (key: string) => cfg.formatHeader?.(key, point) ?? key,
|
|
81
|
+
})}
|
|
82
|
+
{...(badge !== null && { label: badge.text, labelColor: badge.color })}
|
|
83
|
+
/>
|
|
84
|
+
<TooltipBody>
|
|
85
|
+
{cfg?.prependRows?.(point, { hidden })}
|
|
86
|
+
{children}
|
|
87
|
+
{cfg?.extraRows?.(point, { hidden })}
|
|
88
|
+
</TooltipBody>
|
|
89
|
+
</ChartTooltipFloat>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -32,9 +32,9 @@ import {
|
|
|
32
32
|
} from './chart-layout'
|
|
33
33
|
import { useChartContainerClass, useChartMetrics } from './chart-tier'
|
|
34
34
|
import type { ChartState } from './ChartPending'
|
|
35
|
-
import { ChartTooltipFloat, TooltipBody, TooltipHeader,
|
|
35
|
+
import { ChartTooltipFloat, TooltipBody, TooltipHeader, TooltipRows } from './ChartTooltip'
|
|
36
36
|
import { Crosshair, SeriesDot } from './Crosshair'
|
|
37
|
-
import { HoverOverlay } from './HoverOverlay'
|
|
37
|
+
import { cursorSliderProps, HoverOverlay } from './HoverOverlay'
|
|
38
38
|
import { XZoneRects } from './XZoneRects'
|
|
39
39
|
import type { XZoneSpec } from './XZoneRects'
|
|
40
40
|
import { ZoneRects } from './ZoneRects'
|
|
@@ -405,46 +405,51 @@ function buildYScale<T>(
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
408
|
+
* The x-axis margin ladder, as one pure decision. The flat (unrotated) margin is what every case
|
|
409
|
+
* resolves to except a rotated or wrapped axis, and it is what the rotation decision itself is
|
|
410
|
+
* measured against — deciding needs an `xMax`, and `xMax` needs a margin, so the flat pass breaks
|
|
411
|
+
* the loop. An explicit `xLabelRotate` always wins — including `0`, the documented "never rotate"
|
|
412
|
+
* opt-out; unset, only a categorical (band) axis wraps, and rotates only when even wrapped it
|
|
413
|
+
* doesn't fit (`planXLabels`) — a time or number domain only thins.
|
|
414
|
+
*
|
|
415
|
+
* Per rotation: 45° reuses its pre-measured margin and measures its tick room through
|
|
416
|
+
* {@link rotatedXLabelPx} (a rotated label projects wider than its glyph box); 90° measures fresh
|
|
417
|
+
* (one line box wide, so the flat label room governs its thinning); a wrapped flat axis re-measures
|
|
418
|
+
* for its extra line(s) and thins by the wrapped width.
|
|
414
419
|
*/
|
|
415
|
-
function
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
axisFont: number
|
|
420
|
+
function resolveXAxisLayout(input: {
|
|
421
|
+
marginInput: AutoMarginInput
|
|
422
|
+
flatMargin: ChartMargin
|
|
423
|
+
rotated45Margin: ChartMargin
|
|
420
424
|
flatLabelPx: number
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* `baseMargin`'s resolver, split out of the same two-deep ternary: an unrotated, unwrapped axis
|
|
430
|
-
* keeps the already-measured flat margin; wrapping re-measures for the extra line(s)
|
|
431
|
-
* (`bottomLines`); a 45° rotation reuses its own pre-measured margin ({@link rotated45Margin}); a
|
|
432
|
-
* 90° one measures fresh (no pre-measured twin exists for it, unlike 45°).
|
|
433
|
-
*/
|
|
434
|
-
function resolveBaseMargin(input: {
|
|
425
|
+
xLabels: string[]
|
|
426
|
+
fontPx: number
|
|
427
|
+
plotWidth: number
|
|
428
|
+
categorical: boolean
|
|
429
|
+
xLabelRotate: 0 | 45 | 90 | undefined
|
|
430
|
+
}): {
|
|
431
|
+
baseMargin: ChartMargin
|
|
435
432
|
rotate: 0 | 45 | 90
|
|
436
433
|
wrapWidth: number | undefined
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
marginInput
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const {
|
|
443
|
-
|
|
444
|
-
if (rotate ===
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
434
|
+
tickLabelPx: number
|
|
435
|
+
} {
|
|
436
|
+
const { marginInput, flatMargin, rotated45Margin, flatLabelPx, xLabels, fontPx, categorical } =
|
|
437
|
+
input
|
|
438
|
+
const flatXMax = Math.max(input.plotWidth - flatMargin.left - flatMargin.right, 0)
|
|
439
|
+
const xPlan = planXLabels({ labels: xLabels, plotWidth: flatXMax, fontPx, categorical })
|
|
440
|
+
const rotate = input.xLabelRotate ?? (categorical ? xPlan.rotate : 0)
|
|
441
|
+
if (rotate === 45) {
|
|
442
|
+
const tickLabelPx = rotatedXLabelPx(xLabels, fontPx)
|
|
443
|
+
return { baseMargin: rotated45Margin, rotate, wrapWidth: undefined, tickLabelPx }
|
|
444
|
+
}
|
|
445
|
+
if (rotate === 90) {
|
|
446
|
+
const baseMargin = autoMargin({ ...marginInput, rotate })
|
|
447
|
+
return { baseMargin, rotate, wrapWidth: undefined, tickLabelPx: flatLabelPx }
|
|
448
|
+
}
|
|
449
|
+
if (!xPlan.wrap)
|
|
450
|
+
return { baseMargin: flatMargin, rotate, wrapWidth: undefined, tickLabelPx: flatLabelPx }
|
|
451
|
+
const baseMargin = autoMargin({ ...marginInput, bottomLines: xPlan.lines })
|
|
452
|
+
return { baseMargin, rotate, wrapWidth: xPlan.wrapPx, tickLabelPx: xPlan.labelPx }
|
|
448
453
|
}
|
|
449
454
|
|
|
450
455
|
/**
|
|
@@ -665,16 +670,16 @@ function CartesianPlot<T>({
|
|
|
665
670
|
// the actual law: only a band (category) domain wraps or rotates; time and number domains thin.
|
|
666
671
|
const categorical = tight && classifyDomain(keys) === 'band'
|
|
667
672
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const xLabelPx = useMemo(() => xLabelPxFor(xLabels, tier.axisFont), [xLabels, tier.axisFont])
|
|
673
|
+
// The room one flat x tick label needs at the tick font the axis will actually paint
|
|
674
|
+
// (`docs/CHARTS-SPEC.md` §8) — keyed on the labels and font alone, so a y-label or margin change
|
|
675
|
+
// never re-measures every x label.
|
|
676
|
+
const flatLabelPx = useMemo(() => xLabelPxFor(xLabels, tier.axisFont), [xLabels, tier.axisFont])
|
|
673
677
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
+
// Everything `autoMargin` measures except the rotation, measured once for every pass so a label
|
|
679
|
+
// can never differ between the one that DECIDES and the one that PAINTS — plus the 45° margin (a
|
|
680
|
+
// rotated label reaches into the left gutter).
|
|
681
|
+
const measured = useMemo(() => {
|
|
682
|
+
const marginInput: AutoMarginInput = {
|
|
678
683
|
left: yPlacement === 'outside' ? leftLabels : [],
|
|
679
684
|
right: rightLabels,
|
|
680
685
|
bottom: xLabels,
|
|
@@ -684,61 +689,34 @@ function CartesianPlot<T>({
|
|
|
684
689
|
floor: resolveMarginFloor(yPlacement, tier.margin),
|
|
685
690
|
anchorTerminals: tight,
|
|
686
691
|
...(marginOverride !== undefined && { override: marginOverride }),
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
yPlacement,
|
|
697
|
-
],
|
|
698
|
-
)
|
|
699
|
-
|
|
700
|
-
/** The margin an UNROTATED axis resolves to. It is the final margin in every case except an
|
|
701
|
-
* auto-rotated phone axis, and it is what the rotation decision itself is measured against —
|
|
702
|
-
* deciding needs an `xMax`, and `xMax` needs a margin, so the unrotated pass breaks the loop. */
|
|
703
|
-
const flatMargin = useMemo(() => autoMargin(marginInput), [marginInput])
|
|
704
|
-
|
|
705
|
-
/** And the margin a 45° axis would resolve to — the OTHER half of the decision, needed only for
|
|
706
|
-
* a categorical (band) domain that still overflows once wrapped (`planXLabels`'s own rotate
|
|
707
|
-
* case). A rotated label reaches into the left gutter, so rotating spends plot width. */
|
|
708
|
-
const rotated45Margin = useMemo(() => autoMargin({ ...marginInput, rotate: 45 }), [marginInput])
|
|
709
|
-
|
|
710
|
-
// An explicit `xLabelRotate` always wins — including `0`, which is the documented "never rotate"
|
|
711
|
-
// opt-out. Only an unset one falls through to the domain-driven default: a categorical (band)
|
|
712
|
-
// axis wraps before it rotates, and rotates only when even wrapped it doesn't fit
|
|
713
|
-
// (`planXLabels`). A time or number domain never wraps or rotates — it only thins.
|
|
714
|
-
const flatXMax = Math.max(plot.width - flatMargin.left - flatMargin.right, 0)
|
|
715
|
-
const xPlan = useMemo(
|
|
716
|
-
() => planXLabels({ labels: xLabels, plotWidth: flatXMax, fontPx: tier.axisFont, categorical }),
|
|
717
|
-
[xLabels, flatXMax, tier.axisFont, categorical],
|
|
718
|
-
)
|
|
719
|
-
const rotate = xLabelRotate ?? (categorical ? xPlan.rotate : 0)
|
|
720
|
-
const wrapWidth = rotate === 0 && xPlan.wrap ? xPlan.wrapPx : undefined
|
|
721
|
-
// A 90° label is one line box wide, so the constant floor governs it; a 45° one projects wider.
|
|
722
|
-
const tickLabelPx = resolveTickLabelPx({
|
|
723
|
-
rotate,
|
|
724
|
-
wrapWidth,
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
marginInput,
|
|
695
|
+
flatMargin: autoMargin(marginInput),
|
|
696
|
+
rotated45Margin: autoMargin({ ...marginInput, rotate: 45 }),
|
|
697
|
+
}
|
|
698
|
+
}, [
|
|
699
|
+
leftLabels,
|
|
700
|
+
rightLabels,
|
|
725
701
|
xLabels,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
702
|
+
tier.axisFont,
|
|
703
|
+
tier.margin,
|
|
704
|
+
marginOverride,
|
|
705
|
+
tight,
|
|
706
|
+
yPlacement,
|
|
707
|
+
])
|
|
708
|
+
const { baseMargin, rotate, wrapWidth, tickLabelPx } = useMemo(
|
|
732
709
|
() =>
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
710
|
+
resolveXAxisLayout({
|
|
711
|
+
...measured,
|
|
712
|
+
flatLabelPx,
|
|
713
|
+
xLabels,
|
|
714
|
+
fontPx: tier.axisFont,
|
|
715
|
+
plotWidth: plot.width,
|
|
716
|
+
categorical,
|
|
717
|
+
xLabelRotate,
|
|
740
718
|
}),
|
|
741
|
-
[
|
|
719
|
+
[measured, flatLabelPx, xLabels, tier.axisFont, plot.width, categorical, xLabelRotate],
|
|
742
720
|
)
|
|
743
721
|
|
|
744
722
|
// End-of-line labels are planned FIRST, against the base margin: the right gutter is reserved
|
|
@@ -998,18 +976,7 @@ function CartesianPlot<T>({
|
|
|
998
976
|
<HoverOverlay
|
|
999
977
|
width={xMax}
|
|
1000
978
|
height={yMax}
|
|
1001
|
-
|
|
1002
|
-
onDown={cursor.onPointerDown}
|
|
1003
|
-
onUp={cursor.onPointerUp}
|
|
1004
|
-
onLeave={cursor.onPointerLeave}
|
|
1005
|
-
onKeyDown={cursor.onKeyDown}
|
|
1006
|
-
onBlur={cursor.onBlur}
|
|
1007
|
-
valueMax={Math.max(data.length - 1, 0)}
|
|
1008
|
-
{...(point !== null && {
|
|
1009
|
-
valueNow: data.indexOf(point),
|
|
1010
|
-
valueText: formatX(getX(point)),
|
|
1011
|
-
})}
|
|
1012
|
-
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
979
|
+
{...cursorSliderProps({ cursor: cursor, data: data, getX, formatX, ariaLabel })}
|
|
1013
980
|
/>
|
|
1014
981
|
</Group>
|
|
1015
982
|
</svg>
|
|
@@ -1025,17 +992,7 @@ function CartesianPlot<T>({
|
|
|
1025
992
|
/>
|
|
1026
993
|
<TooltipBody>
|
|
1027
994
|
{tooltipCfg?.prependRows?.(point, { visible, hidden })}
|
|
1028
|
-
{rows
|
|
1029
|
-
<TooltipRow
|
|
1030
|
-
key={row.key}
|
|
1031
|
-
color={row.color}
|
|
1032
|
-
label={row.label}
|
|
1033
|
-
value={row.value}
|
|
1034
|
-
shape={row.shape}
|
|
1035
|
-
dashed={row.dashed}
|
|
1036
|
-
{...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
|
|
1037
|
-
/>
|
|
1038
|
-
))}
|
|
995
|
+
<TooltipRows rows={rows} />
|
|
1039
996
|
{tooltipCfg?.extraRows?.(point, { visible, hidden })}
|
|
1040
997
|
</TooltipBody>
|
|
1041
998
|
</ChartTooltipFloat>
|