basalt-ui 2.3.0 → 2.5.0
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 +79 -12
- package/agent/rules/basalt-state.md +1 -1
- package/configs/lefthook.yml +4 -1
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +4 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/run-records.d.ts +12 -3
- package/dist/agent/run-records.d.ts.map +1 -1
- package/dist/agent/run-records.js +16 -3
- package/dist/agent/transport.d.ts +10 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +23 -7
- package/dist/charts/CartesianChart.d.ts.map +1 -1
- package/dist/charts/CartesianChart.js +6 -2
- package/dist/charts/DualPanel.d.ts +5 -1
- package/dist/charts/DualPanel.d.ts.map +1 -1
- package/dist/charts/DualPanel.js +46 -19
- package/dist/charts/Heatmap.js +2 -2
- package/dist/charts/Lines.js +2 -2
- package/dist/charts/MirroredBars.d.ts.map +1 -1
- package/dist/charts/MirroredBars.js +14 -11
- package/dist/charts/compile.d.ts +19 -2
- package/dist/charts/compile.d.ts.map +1 -1
- package/dist/charts/compile.js +192 -42
- package/dist/charts/marks.d.ts +8 -0
- package/dist/charts/marks.d.ts.map +1 -1
- package/dist/charts/marks.js +22 -1
- package/dist/charts/panel.d.ts +6 -0
- package/dist/charts/panel.d.ts.map +1 -1
- package/dist/charts/panel.js +12 -3
- package/dist/charts/types.d.ts +39 -1
- package/dist/charts/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +8 -2
- package/dist/state/fields.d.ts +14 -3
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +41 -9
- package/dist/telemetry.d.ts +58 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +13 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/agent/ai-sdk-transport.ts +5 -2
- package/src/agent/index.ts +1 -1
- package/src/agent/run-records.ts +27 -3
- package/src/agent/transport.ts +11 -5
- package/src/agent/use-agent-thread-runs.ts +35 -8
- package/src/charts/CartesianChart.tsx +7 -2
- package/src/charts/DualPanel.tsx +50 -19
- package/src/charts/Heatmap.tsx +2 -2
- package/src/charts/Lines.tsx +2 -2
- package/src/charts/MirroredBars.tsx +13 -10
- package/src/charts/compile.ts +261 -46
- package/src/charts/marks.ts +35 -1
- package/src/charts/panel.tsx +18 -3
- package/src/charts/types.ts +37 -1
- package/src/index.ts +1 -1
- package/src/provider/index.tsx +8 -2
- package/src/state/fields.ts +79 -21
- package/src/telemetry.ts +70 -7
package/src/charts/compile.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { crosshair } from '@tanstack/charts/crosshair'
|
|
9
9
|
import { decorative } from '@tanstack/charts/mark/decorative'
|
|
10
10
|
import { rect } from '@tanstack/charts/rect'
|
|
11
|
-
import { ruleY } from '@tanstack/charts/rule'
|
|
11
|
+
import { ruleX, ruleY } from '@tanstack/charts/rule'
|
|
12
12
|
import { scaleLinear } from '@tanstack/charts/scales/linear'
|
|
13
13
|
import { scaleUtc } from 'd3-scale'
|
|
14
14
|
import { tooltip as tooltipHost } from '@tanstack/charts/tooltip'
|
|
@@ -23,10 +23,12 @@ import type {
|
|
|
23
23
|
ResponsiveChartDefinition,
|
|
24
24
|
StoredChartSpec,
|
|
25
25
|
} from '@tanstack/charts/types/core'
|
|
26
|
+
import { SHORT_CHART_HEIGHT } from './box'
|
|
26
27
|
import { fmtCompact } from '../format/number'
|
|
27
28
|
import { VX } from '../tokens'
|
|
28
29
|
import type { ContainerClass } from '../tokens/size-classes'
|
|
29
30
|
import { cursorBinding, sourceOnlyFocus } from './cursor'
|
|
31
|
+
import { cursorDots } from './marks'
|
|
30
32
|
import { resolveContainerClass } from '../tokens/size-classes'
|
|
31
33
|
import { resolveYDomain, xScaleFor, xZoneFractions, yScaleFor } from './scales'
|
|
32
34
|
import type { XScaleModel } from './scales'
|
|
@@ -39,6 +41,8 @@ import type {
|
|
|
39
41
|
ChartTooltipRow,
|
|
40
42
|
ChartX,
|
|
41
43
|
PlotRow,
|
|
44
|
+
RefLine,
|
|
45
|
+
ZoneEdge,
|
|
42
46
|
ZoneSpec,
|
|
43
47
|
} from './types'
|
|
44
48
|
|
|
@@ -94,6 +98,14 @@ const LADDER = {
|
|
|
94
98
|
const INSIDE_LABEL_DX = 4
|
|
95
99
|
const INSIDE_LABEL_DY = -6
|
|
96
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Y labels go inside the plot only for the compact class under `SHORT_CHART_HEIGHT` (the box's own
|
|
103
|
+
* short signal); otherwise they sit in a left gutter. `height` is the whole chart's, `0` before it
|
|
104
|
+
* is measured, which keeps the gutter rather than flipping a first paint inside.
|
|
105
|
+
*/
|
|
106
|
+
const labelsInside = (cls: ContainerClass, height: number): boolean =>
|
|
107
|
+
cls === 'compact' && height > 0 && height < SHORT_CHART_HEIGHT
|
|
108
|
+
|
|
97
109
|
export function defaultNumberFormat(v: number): string {
|
|
98
110
|
return fmtCompact(v, { digits: Math.abs(v) >= 1000 ? 1 : 2 })
|
|
99
111
|
}
|
|
@@ -136,6 +148,83 @@ export function defaultFormatX(v: ChartValue): string {
|
|
|
136
148
|
const clampTo = (v: number, [lo, hi]: readonly [number, number]): number =>
|
|
137
149
|
Math.min(Math.max(v, Math.min(lo, hi)), Math.max(lo, hi))
|
|
138
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A tooltip swatch is the engine's `background` shorthand, so a dashed one is a gradient: a 2px
|
|
153
|
+
* dashed line across the swatch's middle, in the row color.
|
|
154
|
+
*/
|
|
155
|
+
export const dashedSwatch = (color: string): string =>
|
|
156
|
+
`repeating-linear-gradient(90deg, ${color} 0 2px, transparent 2px 4px) center / 100% 2px no-repeat`
|
|
157
|
+
|
|
158
|
+
/** A basalt tooltip row as the engine's: the swatch shape folded into the row color. */
|
|
159
|
+
export function engineRow(
|
|
160
|
+
row: ChartTooltipRow & { active?: boolean | undefined },
|
|
161
|
+
): EngineTooltipRow {
|
|
162
|
+
const { label, value, color, swatch, active } = row
|
|
163
|
+
return {
|
|
164
|
+
label,
|
|
165
|
+
value,
|
|
166
|
+
...(color !== undefined && { color: swatch === 'dashed' ? dashedSwatch(color) : color }),
|
|
167
|
+
...(active && { active }),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A series row of a tooltip: the label with its note, the swatch following the series' dash. */
|
|
172
|
+
export function seriesRow(
|
|
173
|
+
s: Pick<ChartSeries<never>, 'label' | 'note' | 'color' | 'dash'>,
|
|
174
|
+
value: string,
|
|
175
|
+
active: boolean,
|
|
176
|
+
): EngineTooltipRow {
|
|
177
|
+
return engineRow({
|
|
178
|
+
label: s.note === undefined ? s.label : `${s.label} · ${s.note}`,
|
|
179
|
+
value,
|
|
180
|
+
color: s.color,
|
|
181
|
+
swatch: s.dash === 'dashed' ? 'dashed' : 'solid',
|
|
182
|
+
active,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Keys the compiler reserves for the legend entries it derives from `refLines`. */
|
|
187
|
+
const REF_KEY_PREFIX = '__ref-'
|
|
188
|
+
|
|
189
|
+
/** The legend key of a reference line that names itself (`RefLine.legend`). */
|
|
190
|
+
const refLegendKey = (index: number): string => `${REF_KEY_PREFIX}${index}`
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A reference line is drawn translucent (`*-ref`, 20-30% alpha) so it recedes behind the data, but
|
|
194
|
+
* a 16x8 swatch at that alpha vanishes on the dark surface. The legend takes the status solid,
|
|
195
|
+
* which the color gate holds to 3:1 on the panel in both schemes, like a series swatch.
|
|
196
|
+
*/
|
|
197
|
+
const SWATCH_SOLID: ReadonlyMap<string, string> = new Map([
|
|
198
|
+
[VX.goodRef, VX.goodSolid],
|
|
199
|
+
[VX.badRef, VX.badSolid],
|
|
200
|
+
[VX.warnRef, VX.warnSolid],
|
|
201
|
+
// The tinted status fills are as translucent, and a consumer may pass either as a line color.
|
|
202
|
+
[VX.good, VX.goodSolid],
|
|
203
|
+
[VX.goodSoft, VX.goodSolid],
|
|
204
|
+
[VX.bad, VX.badSolid],
|
|
205
|
+
[VX.warn, VX.warnSolid],
|
|
206
|
+
])
|
|
207
|
+
|
|
208
|
+
/** Legend-only entries for the reference lines that name themselves, in line order. */
|
|
209
|
+
export function refLegendSeries<T>(refLines: readonly RefLine[] | undefined): ChartSeries<T>[] {
|
|
210
|
+
return (refLines ?? []).flatMap((r, i) =>
|
|
211
|
+
r.legend === undefined
|
|
212
|
+
? []
|
|
213
|
+
: [
|
|
214
|
+
{
|
|
215
|
+
key: refLegendKey(i),
|
|
216
|
+
label: r.legend.label,
|
|
217
|
+
color: SWATCH_SOLID.get(r.color) ?? r.color,
|
|
218
|
+
role: 'reference' as const,
|
|
219
|
+
tooltip: false,
|
|
220
|
+
getValue: () => null,
|
|
221
|
+
...(r.legend.mark !== undefined && { mark: r.legend.mark }),
|
|
222
|
+
...(r.dashed && { dash: 'dashed' as const }),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
139
228
|
/** The visible series: not toggled off, and not a companion whose parent is. */
|
|
140
229
|
export function visibleSeries<T>(
|
|
141
230
|
series: readonly ChartSeries<T>[],
|
|
@@ -154,8 +243,10 @@ export function yGuide<T>(opts: {
|
|
|
154
243
|
cls: ContainerClass
|
|
155
244
|
side: 'left' | 'right'
|
|
156
245
|
grid: boolean
|
|
246
|
+
/** The whole chart's height: decides inside or gutter labels. */
|
|
247
|
+
height: number
|
|
157
248
|
}): Omit<ChartPositionScaleOptions<number>, 'scale'> {
|
|
158
|
-
const { cfg, axis, cls, side, grid } = opts
|
|
249
|
+
const { cfg, axis, cls, side, grid, height } = opts
|
|
159
250
|
const ladder = LADDER[cls]
|
|
160
251
|
const ticks =
|
|
161
252
|
typeof cfg?.ticks === 'object'
|
|
@@ -163,8 +254,8 @@ export function yGuide<T>(opts: {
|
|
|
163
254
|
: { count: typeof cfg?.ticks === 'number' ? cfg.ticks : ladder.yTicks }
|
|
164
255
|
const gridLine = grid ? { grid: GRID_LINE } : {}
|
|
165
256
|
if (cls === 'micro') return { ...gridLine, axis: false }
|
|
166
|
-
//
|
|
167
|
-
const inside = cls
|
|
257
|
+
// Labels sit INSIDE the plot, above their grid line, so the plot keeps the full width.
|
|
258
|
+
const inside = labelsInside(cls, height)
|
|
168
259
|
return {
|
|
169
260
|
...gridLine,
|
|
170
261
|
...(side === 'right' && { side: 'right' as const }),
|
|
@@ -212,9 +303,12 @@ export function xGuide<X extends ChartX>(opts: {
|
|
|
212
303
|
xTicks: number | readonly X[] | undefined
|
|
213
304
|
format: (v: ChartValue) => string
|
|
214
305
|
cls: ContainerClass
|
|
306
|
+
/** The whole chart's height: decides whether the left margin is locked to zero. */
|
|
307
|
+
height: number
|
|
215
308
|
}): Omit<ChartPositionScaleOptions<ChartValue>, 'scale'> {
|
|
216
|
-
const { model, xTicks, format, cls } = opts
|
|
309
|
+
const { model, xTicks, format, cls, height } = opts
|
|
217
310
|
const ladder = LADDER[cls]
|
|
311
|
+
const inside = labelsInside(cls, height)
|
|
218
312
|
const ticks = xTickPolicy(model, xTicks ?? ladder.xTicks)
|
|
219
313
|
return {
|
|
220
314
|
axis: {
|
|
@@ -224,8 +318,8 @@ export function xGuide<X extends ChartX>(opts: {
|
|
|
224
318
|
fontSize: ladder.font,
|
|
225
319
|
opacity: 1,
|
|
226
320
|
thin: true,
|
|
227
|
-
// A locked-zero left margin (
|
|
228
|
-
...(
|
|
321
|
+
// A locked-zero left margin (inside labels) must not clip the first label.
|
|
322
|
+
...(inside && {
|
|
229
323
|
anchor: ({ index }: { index: number }) => (index === 0 ? ('start' as const) : undefined),
|
|
230
324
|
}),
|
|
231
325
|
},
|
|
@@ -435,32 +529,93 @@ function resolveAxes<T, X extends ChartX>(
|
|
|
435
529
|
}
|
|
436
530
|
}
|
|
437
531
|
|
|
438
|
-
/**
|
|
532
|
+
/** The sides a `ZoneEdge` names, in a fixed order: the lower bound first. */
|
|
533
|
+
const edgeSides = <A extends string, B extends string>(
|
|
534
|
+
edge: ZoneEdge<A | B> | undefined,
|
|
535
|
+
lower: A,
|
|
536
|
+
upper: B,
|
|
537
|
+
): { lower: boolean; upper: boolean } => ({
|
|
538
|
+
lower: edge !== undefined && (edge.side === lower || edge.side === 'both'),
|
|
539
|
+
upper: edge !== undefined && (edge.side === upper || edge.side === 'both'),
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const edgePaint = (edge: ZoneEdge<string>) => ({
|
|
543
|
+
stroke: edge.color,
|
|
544
|
+
strokeOpacity: 1,
|
|
545
|
+
strokeWidth: edge.width ?? 1,
|
|
546
|
+
...(edge.dashed && { strokeDasharray: DASH }),
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* An edge rule at `position` when it lies strictly inside `(lo, hi)`: a bound at or past the plot
|
|
551
|
+
* edge (infinite, outside the domain, exactly on it) has no rule, for y- and x-zones alike.
|
|
552
|
+
*/
|
|
553
|
+
const edgeRule = (
|
|
554
|
+
position: number | null,
|
|
555
|
+
[lo, hi]: readonly [number, number],
|
|
556
|
+
make: (at: number) => ChartMarks[number],
|
|
557
|
+
): ChartMarks => (position !== null && position > lo && position < hi ? [make(position)] : [])
|
|
558
|
+
|
|
559
|
+
/** Fixed paint under the kind's marks: zones, then x-zones, each with its edge rules. */
|
|
439
560
|
function underlayMarks<T, X extends ChartX>(
|
|
440
561
|
input: CartesianInput<T, X>,
|
|
441
562
|
model: XScaleModel<X>,
|
|
442
563
|
axes: Axes<T>,
|
|
443
564
|
): ChartMarks {
|
|
444
565
|
const fractionRows = [{ f0: 0, f1: 1 }]
|
|
445
|
-
const zones = (input.zones ?? []).
|
|
566
|
+
const zones = (input.zones ?? []).flatMap((z, i) => {
|
|
446
567
|
const domain = axes.axisOf(z.axisSide).domain
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
568
|
+
const [lo, hi] = [Math.min(...domain), Math.max(...domain)]
|
|
569
|
+
const rule = (value: number, name: string) =>
|
|
570
|
+
edgeRule(value, [lo, hi], (at) =>
|
|
571
|
+
decorative(
|
|
572
|
+
ruleY([at], {
|
|
573
|
+
id: `zone-${i}-${name}`,
|
|
574
|
+
yScale: axes.scaleId(z.axisSide),
|
|
575
|
+
...(z.edge && edgePaint(z.edge)),
|
|
576
|
+
}),
|
|
577
|
+
),
|
|
578
|
+
)
|
|
579
|
+
const sides = edgeSides(z.edge, 'bottom', 'top')
|
|
580
|
+
return [
|
|
581
|
+
decorative(
|
|
582
|
+
rect(fractionRows, {
|
|
583
|
+
id: `zone-${i}`,
|
|
584
|
+
x1: 'f0',
|
|
585
|
+
x2: 'f1',
|
|
586
|
+
y1: () => clampTo(z.from, domain),
|
|
587
|
+
y2: () => clampTo(z.to, domain),
|
|
588
|
+
xScale: 'zx',
|
|
589
|
+
yScale: axes.scaleId(z.axisSide),
|
|
590
|
+
fill: z.fill,
|
|
591
|
+
inset: 0,
|
|
592
|
+
}),
|
|
593
|
+
),
|
|
594
|
+
...(sides.lower ? rule(Math.min(z.from, z.to), 'bottom') : []),
|
|
595
|
+
...(sides.upper ? rule(Math.max(z.from, z.to), 'top') : []),
|
|
596
|
+
]
|
|
460
597
|
})
|
|
461
598
|
const xZones = (input.xZones ?? []).flatMap((z, i) => {
|
|
462
599
|
const f = xZoneFractions(z, model)
|
|
463
600
|
if (!f) return []
|
|
601
|
+
const sides = edgeSides(z.edge, 'start', 'end')
|
|
602
|
+
// The bound's own plot fraction (widened like the band), unclamped: `f` is clamped to the plot.
|
|
603
|
+
const widen = z.align === 'edge' ? model.halfStep : 0
|
|
604
|
+
const bound = (value: X | undefined, sign: 1 | -1): number | null => {
|
|
605
|
+
const fraction = value === undefined ? null : model.fraction(value)
|
|
606
|
+
return fraction === null ? null : fraction + sign * widen
|
|
607
|
+
}
|
|
608
|
+
const rule = (value: X | undefined, sign: 1 | -1, name: string) =>
|
|
609
|
+
edgeRule(bound(value, sign), [0, 1], (at) =>
|
|
610
|
+
decorative(
|
|
611
|
+
ruleX([at], {
|
|
612
|
+
id: `xzone-${i}-${name}`,
|
|
613
|
+
x: (v: number) => v,
|
|
614
|
+
xScale: 'zx',
|
|
615
|
+
...(z.edge && edgePaint(z.edge)),
|
|
616
|
+
}),
|
|
617
|
+
),
|
|
618
|
+
)
|
|
464
619
|
return [
|
|
465
620
|
decorative(
|
|
466
621
|
rect([{ f0: f[0], f1: f[1] }], {
|
|
@@ -474,6 +629,9 @@ function underlayMarks<T, X extends ChartX>(
|
|
|
474
629
|
inset: 0,
|
|
475
630
|
}),
|
|
476
631
|
),
|
|
632
|
+
// An omitted bound is the plot edge: nothing to draw a rule on.
|
|
633
|
+
...(sides.lower ? rule(z.from, -1, 'start') : []),
|
|
634
|
+
...(sides.upper ? rule(z.to, 1, 'end') : []),
|
|
477
635
|
]
|
|
478
636
|
})
|
|
479
637
|
return [...zones, ...xZones]
|
|
@@ -484,15 +642,20 @@ const refLineMarks = <T, X extends ChartX>(
|
|
|
484
642
|
input: CartesianInput<T, X>,
|
|
485
643
|
axes: Axes<T>,
|
|
486
644
|
): ChartMarks =>
|
|
487
|
-
(input.refLines ?? []).
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
645
|
+
(input.refLines ?? []).flatMap((r, i) =>
|
|
646
|
+
// A line that names itself in the legend can be toggled off there.
|
|
647
|
+
input.hidden?.has(refLegendKey(i))
|
|
648
|
+
? []
|
|
649
|
+
: [
|
|
650
|
+
ruleY([r.value], {
|
|
651
|
+
id: `ref-${i}`,
|
|
652
|
+
stroke: r.color,
|
|
653
|
+
strokeOpacity: 1,
|
|
654
|
+
strokeWidth: 1,
|
|
655
|
+
yScale: axes.scaleId(r.axis),
|
|
656
|
+
...(r.dashed && { strokeDasharray: DASH }),
|
|
657
|
+
}),
|
|
658
|
+
],
|
|
496
659
|
)
|
|
497
660
|
|
|
498
661
|
function buildScales<T, X extends ChartX>(opts: {
|
|
@@ -501,18 +664,26 @@ function buildScales<T, X extends ChartX>(opts: {
|
|
|
501
664
|
axes: Axes<T>
|
|
502
665
|
formatX: (v: ChartValue) => string
|
|
503
666
|
cls: ContainerClass
|
|
667
|
+
height: number
|
|
504
668
|
zoneScale: boolean
|
|
505
669
|
}): StoredChartSpec['scales'] {
|
|
506
|
-
const { input, model, axes, formatX, cls, zoneScale } = opts
|
|
670
|
+
const { input, model, axes, formatX, cls, height, zoneScale } = opts
|
|
507
671
|
const { left, right } = axes
|
|
508
672
|
return {
|
|
509
673
|
x: {
|
|
510
674
|
scale: model.scale,
|
|
511
|
-
...xGuide({ model, xTicks: input.xTicks, format: formatX, cls }),
|
|
675
|
+
...xGuide({ model, xTicks: input.xTicks, format: formatX, cls, height }),
|
|
512
676
|
},
|
|
513
677
|
y: {
|
|
514
678
|
scale: yScaleFor(input.y, left.domain),
|
|
515
|
-
...yGuide({
|
|
679
|
+
...yGuide({
|
|
680
|
+
cfg: input.y,
|
|
681
|
+
axis: left,
|
|
682
|
+
cls,
|
|
683
|
+
height,
|
|
684
|
+
side: 'left',
|
|
685
|
+
grid: input.y?.grid ?? true,
|
|
686
|
+
}),
|
|
516
687
|
},
|
|
517
688
|
...(right && {
|
|
518
689
|
y2: {
|
|
@@ -522,6 +693,7 @@ function buildScales<T, X extends ChartX>(opts: {
|
|
|
522
693
|
cfg: input.y2,
|
|
523
694
|
axis: right,
|
|
524
695
|
cls,
|
|
696
|
+
height,
|
|
525
697
|
side: 'right',
|
|
526
698
|
grid: input.y2?.grid ?? false,
|
|
527
699
|
}),
|
|
@@ -568,6 +740,25 @@ function rowsOf<T, X extends ChartX>(
|
|
|
568
740
|
}
|
|
569
741
|
}
|
|
570
742
|
|
|
743
|
+
/** The cursor dots of the visible series that opt in with `cursorValue`. */
|
|
744
|
+
function cursorDotMarks<T, X extends ChartX>(
|
|
745
|
+
input: CartesianInput<T, X>,
|
|
746
|
+
visible: readonly ChartSeries<T>[],
|
|
747
|
+
axes: Axes<T>,
|
|
748
|
+
): ChartMarks {
|
|
749
|
+
return visible.flatMap((s) => {
|
|
750
|
+
const { cursorValue } = s
|
|
751
|
+
if (!cursorValue) return []
|
|
752
|
+
const rows = input.data.flatMap((d): PlotRow<T, X>[] => {
|
|
753
|
+
const y = cursorValue(d)
|
|
754
|
+
return y === null || !Number.isFinite(y)
|
|
755
|
+
? []
|
|
756
|
+
: [{ x: input.x(d), y, series: s.key, datum: d }]
|
|
757
|
+
})
|
|
758
|
+
return [cursorDots(s, rows, axes.onRight(s) ? 'y2' : undefined)]
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
|
|
571
762
|
function tooltipSpec<T, X extends ChartX>(opts: {
|
|
572
763
|
input: CartesianInput<T, X>
|
|
573
764
|
axes: Axes<T>
|
|
@@ -599,25 +790,40 @@ function tooltipSpec<T, X extends ChartX>(opts: {
|
|
|
599
790
|
for (const s of rowSeries) {
|
|
600
791
|
const v = s.getValue(d)
|
|
601
792
|
if (v === null || !Number.isFinite(v)) continue
|
|
602
|
-
rows.push(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
793
|
+
rows.push(
|
|
794
|
+
seriesRow(
|
|
795
|
+
s,
|
|
796
|
+
s.formatValue
|
|
797
|
+
? s.formatValue(v, d)
|
|
798
|
+
: axes.axisOf(axes.onRight(s) ? 'right' : 'left').format(v),
|
|
799
|
+
input.highlighted === s.key,
|
|
800
|
+
),
|
|
801
|
+
)
|
|
610
802
|
}
|
|
611
803
|
const extra: readonly ChartTooltipRow[] =
|
|
612
804
|
(input.tooltip || undefined)?.rows?.(d, { visible, hidden }) ?? []
|
|
613
|
-
return {
|
|
805
|
+
return {
|
|
806
|
+
title: tooltipHeader(input, formatX, x, d),
|
|
807
|
+
rows: [...rows, ...extra.map(engineRow)],
|
|
808
|
+
}
|
|
614
809
|
},
|
|
615
810
|
}
|
|
616
811
|
}
|
|
617
812
|
|
|
813
|
+
/** A consumer key in the legend's reserved `__ref-` namespace would collide with a reference line. */
|
|
814
|
+
function guardReservedKeys<T>(series: readonly ChartSeries<T>[]): void {
|
|
815
|
+
if (process.env.NODE_ENV === 'production') return
|
|
816
|
+
const clash = series.find((s) => s.key.startsWith(REF_KEY_PREFIX))
|
|
817
|
+
if (clash)
|
|
818
|
+
throw new Error(
|
|
819
|
+
`[basalt] CartesianChart: the series key "${clash.key}" is reserved (\`${REF_KEY_PREFIX}\` prefix names reference-line legend entries)`,
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
|
|
618
823
|
export function compileCartesian<T, X extends ChartX>(
|
|
619
824
|
input: CartesianInput<T, X>,
|
|
620
825
|
): CartesianDefinition {
|
|
826
|
+
guardReservedKeys(input.series)
|
|
621
827
|
const visible = visibleSeries(input.series, input.hidden ?? new Set<string>())
|
|
622
828
|
const visibleKeys = new Set(visible.map((s) => s.key))
|
|
623
829
|
const keys = new Set(input.series.map((s) => s.key))
|
|
@@ -625,6 +831,7 @@ export function compileCartesian<T, X extends ChartX>(
|
|
|
625
831
|
const axes = resolveAxes(input, visible)
|
|
626
832
|
const under = underlayMarks(input, model, axes)
|
|
627
833
|
const over = refLineMarks(input, axes)
|
|
834
|
+
const dots = cursorDotMarks(input, visible, axes)
|
|
628
835
|
const rows = rowsOf(input, visibleKeys)
|
|
629
836
|
const formatX = (v: ChartValue): string =>
|
|
630
837
|
input.formatX ? input.formatX(v as X) : defaultFormatX(v)
|
|
@@ -647,12 +854,20 @@ export function compileCartesian<T, X extends ChartX>(
|
|
|
647
854
|
y2Domain: axes.right?.domain,
|
|
648
855
|
})
|
|
649
856
|
return {
|
|
650
|
-
marks: [...under, ...kindMarks, ...over, crosshair({ x: CROSSHAIR, y: false })],
|
|
651
|
-
scales: buildScales({
|
|
857
|
+
marks: [...under, ...kindMarks, ...dots, ...over, crosshair({ x: CROSSHAIR, y: false })],
|
|
858
|
+
scales: buildScales({
|
|
859
|
+
input,
|
|
860
|
+
model,
|
|
861
|
+
axes,
|
|
862
|
+
formatX,
|
|
863
|
+
cls,
|
|
864
|
+
height,
|
|
865
|
+
zoneScale: under.length > 0,
|
|
866
|
+
}),
|
|
652
867
|
theme: CHART_THEME,
|
|
653
868
|
...(gradients.length > 0 && { gradients }),
|
|
654
869
|
clip: true,
|
|
655
|
-
...(cls
|
|
870
|
+
...(labelsInside(cls, height) && { margin: { left: 0, ...(axes.right && { right: 0 }) } }),
|
|
656
871
|
}
|
|
657
872
|
},
|
|
658
873
|
// Followers paint the crosshair only, unless the chart opts into following tooltips.
|
package/src/charts/marks.ts
CHANGED
|
@@ -30,12 +30,21 @@ export function emphasis<T>(
|
|
|
30
30
|
return highlighted === null || highlighted === s.key || highlighted === s.parent ? 1 : DIMMED
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** The rows a series' line and area are drawn through: all of them, or only the non-null ones. */
|
|
34
|
+
export function drawnRows<T, X extends ChartX>(
|
|
35
|
+
plot: PlotContext<T, X>,
|
|
36
|
+
s: Pick<ChartSeries<T>, 'key' | 'gaps'>,
|
|
37
|
+
): PlotRow<T, X>[] {
|
|
38
|
+
const rows = plot.rows(s.key)
|
|
39
|
+
return s.gaps === 'connect' ? rows.filter((r) => r.y !== null) : rows
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function lineMark<T, X extends ChartX>(
|
|
34
43
|
plot: PlotContext<T, X>,
|
|
35
44
|
s: ChartSeries<T>,
|
|
36
45
|
): ChartMarks[number] {
|
|
37
46
|
const yScale = yScaleOf(plot, s)
|
|
38
|
-
return lineY(plot
|
|
47
|
+
return lineY(drawnRows(plot, s), {
|
|
39
48
|
id: s.key,
|
|
40
49
|
x: rowX,
|
|
41
50
|
y: 'y',
|
|
@@ -96,6 +105,31 @@ export function markersOf<T, X extends ChartX>(
|
|
|
96
105
|
)
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The cursor dot of a series the consumer draws itself (`ChartSeries.cursorValue`): a dot per datum,
|
|
110
|
+
* invisible until the cursor is on that datum's x. It is NOT decorative, so the engine counts it as
|
|
111
|
+
* a focus point and the shared cursor, the focus group and the tooltip reach the series.
|
|
112
|
+
*/
|
|
113
|
+
export function cursorDots<T, X extends ChartX>(
|
|
114
|
+
s: ChartSeries<T>,
|
|
115
|
+
rows: readonly PlotRow<T, X>[],
|
|
116
|
+
yScale: 'y2' | undefined,
|
|
117
|
+
): ChartMarks[number] {
|
|
118
|
+
return dot(rows, {
|
|
119
|
+
id: `${s.key}-cursor`,
|
|
120
|
+
x: rowX,
|
|
121
|
+
y: 'y',
|
|
122
|
+
r: VX.dotR,
|
|
123
|
+
fill: s.color,
|
|
124
|
+
fillOpacity: 0,
|
|
125
|
+
stroke: VX.surface.panel,
|
|
126
|
+
strokeWidth: 1.5,
|
|
127
|
+
strokeOpacity: 0,
|
|
128
|
+
states: [{ when: { focus: 'x' }, style: { fillOpacity: 1, strokeOpacity: 1 } }],
|
|
129
|
+
...(yScale !== undefined && { yScale }),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
/**
|
|
100
134
|
* A single-series `barY`. The engine stacks every `barY` that has no explicit extent, and a row with
|
|
101
135
|
* no `z`/`color` channel is its own series: an N-by-N matrix (a 5k-row `DualPanel` took 10 s). So
|
package/src/charts/panel.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import { useCallback } from 'react'
|
|
|
21
21
|
import type { ReactElement } from 'react'
|
|
22
22
|
import { ChartShell } from './CartesianChart'
|
|
23
23
|
import type { ChartShellProps, ShellContext } from './CartesianChart'
|
|
24
|
-
import { defaultFormatX, utcHeaderDate, visibleSeries, xGuide, yGuide } from './compile'
|
|
24
|
+
import { defaultFormatX, engineRow, utcHeaderDate, visibleSeries, xGuide, yGuide } from './compile'
|
|
25
25
|
import type { CartesianDefinition } from './compile'
|
|
26
26
|
import { cursorBinding, sourceOnlyFocus } from './cursor'
|
|
27
27
|
import type { XScaleModel } from './scales'
|
|
@@ -83,12 +83,15 @@ export const paneY = (opts: {
|
|
|
83
83
|
format: (v: number) => string
|
|
84
84
|
ticks: number
|
|
85
85
|
cls: ContainerClass
|
|
86
|
+
/** The whole chart's height ({@link chartHeightOf}). */
|
|
87
|
+
height: number
|
|
86
88
|
}): StoredChartSpec['scales'][string] => ({
|
|
87
89
|
scale: opts.scale,
|
|
88
90
|
...yGuide({
|
|
89
91
|
cfg: { ticks: opts.ticks },
|
|
90
92
|
axis: { domain: opts.domain, format: opts.format },
|
|
91
93
|
cls: opts.cls,
|
|
94
|
+
height: opts.height,
|
|
92
95
|
side: 'left',
|
|
93
96
|
grid: true,
|
|
94
97
|
}),
|
|
@@ -102,11 +105,23 @@ export const paneX = <X extends ChartX>(opts: {
|
|
|
102
105
|
model: XScaleModel<X>
|
|
103
106
|
format: (v: ChartValue) => string
|
|
104
107
|
cls: ContainerClass
|
|
108
|
+
/** The whole chart's height ({@link chartHeightOf}). */
|
|
109
|
+
height: number
|
|
105
110
|
}): StoredChartSpec['scales'][string] => ({
|
|
106
111
|
scale: opts.scale,
|
|
107
|
-
...xGuide({
|
|
112
|
+
...xGuide({
|
|
113
|
+
model: opts.model,
|
|
114
|
+
xTicks: undefined,
|
|
115
|
+
format: opts.format,
|
|
116
|
+
cls: opts.cls,
|
|
117
|
+
height: opts.height,
|
|
118
|
+
}),
|
|
108
119
|
})
|
|
109
120
|
|
|
121
|
+
/** The whole chart's height from one pane's: a pane gets `share` of the height less the gap. */
|
|
122
|
+
export const chartHeightOf = (paneHeight: number, share: number, gap: number): number =>
|
|
123
|
+
paneHeight > 0 && share > 0 ? paneHeight / share + gap : 0
|
|
124
|
+
|
|
110
125
|
/** One row per datum for a series: a `null` or non-finite value is kept as a gap. */
|
|
111
126
|
export function rowsOf<T, X extends ChartX>(
|
|
112
127
|
data: readonly T[],
|
|
@@ -205,7 +220,7 @@ function panelDefinition<T, X extends ChartX>(
|
|
|
205
220
|
const { x, d } = sourceOf<T>(primary, owned)
|
|
206
221
|
const extra: readonly ChartTooltipRow[] =
|
|
207
222
|
tooltip?.rows?.(d, { visible, hidden: hiddenSeries }) ?? []
|
|
208
|
-
return { title: header(x, d), rows: [...plan.rows(d), ...extra] }
|
|
223
|
+
return { title: header(x, d), rows: [...plan.rows(d), ...extra.map(engineRow)] }
|
|
209
224
|
},
|
|
210
225
|
},
|
|
211
226
|
}
|
package/src/charts/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export type ChartSeries<T> = {
|
|
|
31
31
|
* tier, a config value) is a step: a smoothed curve draws values that never existed. */
|
|
32
32
|
curve?: CurveName | undefined
|
|
33
33
|
strokeWidth?: number | undefined
|
|
34
|
+
/**
|
|
35
|
+
* What a `null` value does to the line (and its `area`). `'break'` (default) leaves a gap;
|
|
36
|
+
* `'connect'` draws through the series' non-null points only, skipping the null rows, so a series
|
|
37
|
+
* that is null on most shared x values (one lift in a wide table) is one continuous line. Points,
|
|
38
|
+
* markers, the cursor and the tooltip still read real values: a null row has no tooltip row.
|
|
39
|
+
*/
|
|
40
|
+
gaps?: 'break' | 'connect' | undefined
|
|
34
41
|
/** Dims the plotted stroke (a faint companion line). Strokes only. */
|
|
35
42
|
strokeOpacity?: number | undefined
|
|
36
43
|
/** Bars and areas. */
|
|
@@ -53,6 +60,12 @@ export type ChartSeries<T> = {
|
|
|
53
60
|
note?: string | undefined
|
|
54
61
|
/** Per-series tooltip value format; receives the datum so a row can read more than the value. */
|
|
55
62
|
formatValue?: ((v: number, d: T) => string) | undefined
|
|
63
|
+
/**
|
|
64
|
+
* A cursor dot for a series whose mark the consumer draws itself (`marks`, so `getValue` stays
|
|
65
|
+
* `null`): the y of the dot at each datum, `null` for none. The dot shows while the cursor is on
|
|
66
|
+
* that datum's x and takes the series color. A series drawn from `plot.rows` already has one.
|
|
67
|
+
*/
|
|
68
|
+
cursorValue?: ((d: T) => number | null) | undefined
|
|
56
69
|
/** A marker at a point (a PR star, a status dot); `null` for none. */
|
|
57
70
|
getMarker?:
|
|
58
71
|
| ((d: T) => {
|
|
@@ -95,12 +108,24 @@ export type AxisConfig<T> = {
|
|
|
95
108
|
scale?: 'linear' | 'log' | undefined
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
/** A rule along a band's boundary, painted with the band. A bound at the plot edge draws none. */
|
|
112
|
+
export type ZoneEdge<S extends string> = {
|
|
113
|
+
side: S | 'both'
|
|
114
|
+
color: string
|
|
115
|
+
dashed?: boolean | undefined
|
|
116
|
+
/** Default `1`. */
|
|
117
|
+
width?: number | undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
/** A horizontal band on one y axis. `±Infinity` reaches the plot edge. */
|
|
99
121
|
export type ZoneSpec = {
|
|
100
122
|
from: number
|
|
101
123
|
to: number
|
|
102
124
|
fill: string
|
|
103
125
|
axisSide?: 'left' | 'right' | undefined
|
|
126
|
+
/** A rule on the band's visual `top` (the larger bound) and/or `bottom` (the smaller), whichever
|
|
127
|
+
* of `from`/`to` that is. */
|
|
128
|
+
edge?: ZoneEdge<'top' | 'bottom'> | undefined
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
/**
|
|
@@ -113,6 +138,8 @@ export type XZoneSpec<X extends ChartX = ChartX> = {
|
|
|
113
138
|
to?: X | undefined
|
|
114
139
|
fill: string
|
|
115
140
|
align?: 'center' | 'edge' | undefined
|
|
141
|
+
/** A rule on the band's `start` (`from`) and/or `end` (`to`) boundary; an omitted bound has none. */
|
|
142
|
+
edge?: ZoneEdge<'start' | 'end'> | undefined
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
/** A horizontal reference line — annotation only, no domain role beyond staying inside the plot. */
|
|
@@ -121,6 +148,13 @@ export type RefLine = {
|
|
|
121
148
|
color: string
|
|
122
149
|
dashed?: boolean | undefined
|
|
123
150
|
axis?: 'left' | 'right' | undefined
|
|
151
|
+
/**
|
|
152
|
+
* Names the line in the legend (its color, dashed as `dashed`; a toggle hides the line), so a
|
|
153
|
+
* threshold needs no always-`null` series. `mark` is the swatch shape. Default `'line'`. The
|
|
154
|
+
* toggle state is keyed by the line's index in `refLines`: reordering the array while a line is
|
|
155
|
+
* toggled off moves the "off" to whichever line now sits at that index.
|
|
156
|
+
*/
|
|
157
|
+
legend?: { label: string; mark?: 'line' | 'bar' | undefined } | undefined
|
|
124
158
|
}
|
|
125
159
|
|
|
126
160
|
/** A fill between the line and a value, on one side of it, tracking the line. */
|
|
@@ -138,7 +172,9 @@ export type ChartTooltipRow = {
|
|
|
138
172
|
label: string
|
|
139
173
|
value: string
|
|
140
174
|
/** The row swatch; omitted, the row has none. */
|
|
141
|
-
color?: string
|
|
175
|
+
color?: string | undefined
|
|
176
|
+
/** The swatch shape; `'dashed'` for a reference or dashed series. Default `'solid'`. */
|
|
177
|
+
swatch?: 'solid' | 'dashed' | undefined
|
|
142
178
|
}
|
|
143
179
|
|
|
144
180
|
export type ChartTooltipConfig<T, X extends ChartX = ChartX> = {
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export {
|
|
|
12
12
|
type BasaltRouter,
|
|
13
13
|
type BasaltRouterContext,
|
|
14
14
|
} from './provider/create-router'
|
|
15
|
-
export type { BasaltEvent } from './telemetry'
|
|
15
|
+
export type { BasaltEvent, ChatTurnEnd, ChatTurnTrace, Telemetry, TraceHeaders } from './telemetry'
|
|
16
16
|
export {
|
|
17
17
|
defineNav,
|
|
18
18
|
navGroup,
|