basalt-ui 1.15.0 → 1.17.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 +14 -0
- package/agent/rules/basalt-charts.md +74 -13
- package/agent/templates/CLAUDE-block.md.tpl +9 -6
- package/dist/charts/cursor/resolve.d.ts +29 -5
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +25 -4
- package/dist/charts/hooks/useChartCursor.d.ts +8 -1
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +6 -2
- package/dist/charts/index.d.ts +2 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.d.ts +30 -8
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +10 -3
- package/dist/charts/kinds/Bars.test.d.ts +2 -0
- package/dist/charts/kinds/Bars.test.d.ts.map +1 -0
- package/dist/charts/kinds/DualPanel.d.ts +30 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +82 -12
- package/dist/charts/kinds/DualPanel.test.d.ts +2 -0
- package/dist/charts/kinds/DualPanel.test.d.ts.map +1 -0
- package/dist/charts/kinds/MultiLine.d.ts +9 -0
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +45 -33
- package/dist/charts/kinds/MultiLine.test.d.ts +2 -0
- package/dist/charts/kinds/MultiLine.test.d.ts.map +1 -0
- package/dist/charts/kinds/StackedArea.d.ts +9 -0
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +16 -1
- package/dist/charts/kinds/StackedArea.test.d.ts +2 -0
- package/dist/charts/kinds/StackedArea.test.d.ts.map +1 -0
- package/dist/charts/kinds/ZonedLine.d.ts +9 -0
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +5 -0
- package/dist/charts/kinds/ZonedLine.test.d.ts +2 -0
- package/dist/charts/kinds/ZonedLine.test.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.d.ts +45 -6
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +29 -12
- package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.d.ts +8 -1
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +1 -1
- package/dist/charts/primitives/ChartLegend.d.ts +3 -0
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +4 -0
- package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +3 -2
- package/dist/charts/primitives/ChartTooltip.test.d.ts +2 -0
- package/dist/charts/primitives/ChartTooltip.test.d.ts.map +1 -0
- package/dist/charts/primitives/XZoneRects.d.ts +22 -8
- package/dist/charts/primitives/XZoneRects.d.ts.map +1 -1
- package/dist/charts/primitives/XZoneRects.js +39 -2
- package/dist/charts/series.d.ts +16 -3
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +2 -1
- package/dist/charts/utils/domain.d.ts +15 -0
- package/dist/charts/utils/domain.d.ts.map +1 -1
- package/dist/charts/utils/domain.js +6 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/cursor/resolve.test.ts +62 -0
- package/src/charts/cursor/resolve.ts +71 -7
- package/src/charts/hooks/useChartCursor.ts +12 -1
- package/src/charts/index.ts +2 -0
- package/src/charts/kinds/Bars.test.tsx +183 -0
- package/src/charts/kinds/Bars.tsx +41 -12
- package/src/charts/kinds/DualPanel.test.tsx +441 -0
- package/src/charts/kinds/DualPanel.tsx +127 -11
- package/src/charts/kinds/MultiLine.test.tsx +218 -0
- package/src/charts/kinds/MultiLine.tsx +64 -36
- package/src/charts/kinds/StackedArea.test.tsx +114 -0
- package/src/charts/kinds/StackedArea.tsx +25 -1
- package/src/charts/kinds/ZonedLine.test.tsx +147 -0
- package/src/charts/kinds/ZonedLine.tsx +14 -0
- package/src/charts/primitives/CartesianChart.test.tsx +269 -0
- package/src/charts/primitives/CartesianChart.tsx +79 -19
- package/src/charts/primitives/ChartFrame.test.tsx +42 -0
- package/src/charts/primitives/ChartFrame.tsx +9 -2
- package/src/charts/primitives/ChartLegend.test.tsx +30 -0
- package/src/charts/primitives/ChartLegend.tsx +7 -0
- package/src/charts/primitives/ChartTooltip.test.tsx +23 -0
- package/src/charts/primitives/ChartTooltip.tsx +7 -1
- package/src/charts/primitives/XZoneRects.test.tsx +60 -0
- package/src/charts/primitives/XZoneRects.tsx +75 -10
- package/src/charts/series.test.ts +51 -0
- package/src/charts/series.ts +18 -6
- package/src/charts/utils/domain.test.ts +31 -1
- package/src/charts/utils/domain.ts +19 -0
- package/src/charts/primitives/CartesianChart.test.ts +0 -99
package/src/charts/series.ts
CHANGED
|
@@ -60,6 +60,14 @@ export type SeriesStyle = {
|
|
|
60
60
|
strokeWidth?: number
|
|
61
61
|
/** bar/area: the swatch honors this so it cannot lie. */
|
|
62
62
|
fillOpacity?: number
|
|
63
|
+
/**
|
|
64
|
+
* A MARK property — dims the plotted stroke, e.g. a faint moving-average companion line. The
|
|
65
|
+
* legend swatch honors it too (parity with `fillOpacity`, which it already honors — a swatch
|
|
66
|
+
* that lies is the thing the derived-legend rule exists to prevent). The TOOLTIP ROW swatch and
|
|
67
|
+
* the CROSSHAIR DOT deliberately do NOT honor it: those are 12px value-readout chips where a
|
|
68
|
+
* sub-1 opacity reads as a rendering bug, not as data.
|
|
69
|
+
*/
|
|
70
|
+
strokeOpacity?: number
|
|
63
71
|
/** Which y-axis this series is measured against. Default 'left'. `'right'` is what makes a
|
|
64
72
|
* chart dual-axis — `CartesianChart` reads it for the crosshair dots and the tooltip. */
|
|
65
73
|
axis?: 'left' | 'right'
|
|
@@ -84,10 +92,13 @@ export type SeriesStyle = {
|
|
|
84
92
|
export type ChartSeries<T> = SeriesStyle & {
|
|
85
93
|
/** null = line gap + skipped tooltip row. */
|
|
86
94
|
getValue: (d: T) => number | null
|
|
87
|
-
/** Per-series override of the shared tooltip/value formatter.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
/** Per-series override of the shared tooltip/value formatter. Receives the hovered datum
|
|
96
|
+
* alongside the value so a row can read fields beyond the plotted number (e.g. `97.5 kg (92.5 ×
|
|
97
|
+
* 3)`). */
|
|
98
|
+
formatValue?: (v: number, d: T) => string
|
|
99
|
+
/** PR star / status dot. Return null for no marker at that point. `ring` defaults true (today's
|
|
100
|
+
* punched-out stroke, unchanged); `false` omits the stroke entirely. `fillOpacity` defaults 1. */
|
|
101
|
+
getMarker?: (d: T) => { color?: string; r?: number; fillOpacity?: number; ring?: boolean } | null
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
/** A single derived tooltip row, ready for `TooltipRow`. */
|
|
@@ -112,6 +123,7 @@ function legendEntryFor(s: SeriesStyle): LegendEntry {
|
|
|
112
123
|
dashed: s.dash === 'dashed',
|
|
113
124
|
...(s.strokeWidth !== undefined && { strokeWidth: s.strokeWidth }),
|
|
114
125
|
...(s.fillOpacity !== undefined && { fillOpacity: s.fillOpacity }),
|
|
126
|
+
...(s.strokeOpacity !== undefined && { strokeOpacity: s.strokeOpacity }),
|
|
115
127
|
...(s.role !== undefined && { role: s.role }),
|
|
116
128
|
...(s.note !== undefined && { note: s.note }),
|
|
117
129
|
}
|
|
@@ -165,12 +177,12 @@ export function deriveTooltipRows<T>(
|
|
|
165
177
|
if (s.tooltip === false) continue
|
|
166
178
|
const value = s.getValue(datum)
|
|
167
179
|
if (value === null) continue
|
|
168
|
-
const format = s.formatValue ?? fallbackFormat
|
|
180
|
+
const format: (v: number, d: T) => string = s.formatValue ?? fallbackFormat
|
|
169
181
|
rows.push({
|
|
170
182
|
key: s.key,
|
|
171
183
|
label: s.label,
|
|
172
184
|
color: s.color,
|
|
173
|
-
value: format(value),
|
|
185
|
+
value: format(value, datum),
|
|
174
186
|
shape: shapeFor(s.mark),
|
|
175
187
|
dashed: s.dash === 'dashed',
|
|
176
188
|
...(s.strokeWidth !== undefined && { strokeWidth: s.strokeWidth }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { padAutoLower } from './domain'
|
|
2
|
+
import { padAutoLower, padAutoUpper } from './domain'
|
|
3
3
|
|
|
4
4
|
describe('padAutoLower', () => {
|
|
5
5
|
test('stays at 0 for the default zero-baseline case (never goes negative)', () => {
|
|
@@ -22,3 +22,33 @@ describe('padAutoLower', () => {
|
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
describe('padAutoUpper', () => {
|
|
27
|
+
test('stays at 0 (never pushed positive)', () => {
|
|
28
|
+
expect(padAutoUpper(0, 1.1)).toBe(0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('pads UP above a positive candidate', () => {
|
|
32
|
+
expect(padAutoUpper(78, 1.1)).toBeCloseTo(85.8, 5)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('pads further up (toward and past zero) for a negative candidate', () => {
|
|
36
|
+
expect(padAutoUpper(-5, 1.1)).toBeCloseTo(-4.5454, 3)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('upper never lands below the candidate it was derived from', () => {
|
|
40
|
+
for (const candidate of [-10, -0.5, 0, 0.5, 10, 78]) {
|
|
41
|
+
expect(padAutoUpper(candidate, 1.1)).toBeGreaterThanOrEqual(candidate)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('the autoMaxFloor case: dataMax 3.2, pad 1.1, floor 6 -> 6.6', () => {
|
|
46
|
+
expect(padAutoUpper(Math.max(3.2, 6), 1.1)).toBeCloseTo(6.6, 5)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('is the exact mirror of padAutoLower — same magnitude, opposite direction', () => {
|
|
50
|
+
for (const candidate of [-10, -0.5, 0.5, 10, 78]) {
|
|
51
|
+
expect(padAutoUpper(candidate, 1.1)).toBeCloseTo(-padAutoLower(-candidate, 1.1), 5)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -20,3 +20,22 @@ export function padAutoLower(candidate: number, pad: number): number {
|
|
|
20
20
|
if (candidate === 0) return 0
|
|
21
21
|
return candidate > 0 ? candidate / pad : candidate * pad
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Auto y-domain upper-bound padding — the exact mirror of {@link padAutoLower}, sharing its
|
|
26
|
+
* sign-safety: pad AWAY from zero (`upper >= candidate`), never toward it. `candidate` is the
|
|
27
|
+
* caller's `Math.max(dataMax, floor)` — the `autoMaxFloor` clamp has already been applied, so
|
|
28
|
+
* padding runs AFTER the floor, not before it. Applying the floor post-pad (the pre-existing bug)
|
|
29
|
+
* lands a floored axis top exactly on the floor value with zero headroom — a target line pinned to
|
|
30
|
+
* that floor then sits glued to the plot edge instead of getting the same breathing room every
|
|
31
|
+
* other bound gets.
|
|
32
|
+
*
|
|
33
|
+
* - `candidate === 0` stays exactly 0 — never pushed positive.
|
|
34
|
+
* - `candidate > 0` multiplies by `pad`, moving the ceiling further UP (away from zero).
|
|
35
|
+
* - `candidate < 0` divides by `pad`, moving the ceiling UP toward (and past) zero — the mirror of
|
|
36
|
+
* `padAutoLower`'s `candidate > 0` branch.
|
|
37
|
+
*/
|
|
38
|
+
export function padAutoUpper(candidate: number, pad: number): number {
|
|
39
|
+
if (candidate === 0) return 0
|
|
40
|
+
return candidate > 0 ? candidate * pad : candidate / pad
|
|
41
|
+
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { ChartSeries } from '../series'
|
|
3
|
-
import { resolveAxisDomain } from './CartesianChart'
|
|
4
|
-
|
|
5
|
-
type Row = { date: string; a: number; b: number }
|
|
6
|
-
|
|
7
|
-
const rows: Row[] = [
|
|
8
|
-
{ date: '2026-08-01', a: 10, b: 4 },
|
|
9
|
-
{ date: '2026-08-02', a: 40, b: 6 },
|
|
10
|
-
{ date: '2026-08-03', a: 25, b: 5 },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
const seriesFor = (key: 'a' | 'b'): ChartSeries<Row> => ({
|
|
14
|
-
key,
|
|
15
|
-
label: key,
|
|
16
|
-
color: '#000',
|
|
17
|
-
mark: 'line',
|
|
18
|
-
getValue: (d) => d[key],
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const both = [seriesFor('a'), seriesFor('b')]
|
|
22
|
-
|
|
23
|
-
describe('resolveAxisDomain', () => {
|
|
24
|
-
test('a fixed tuple passes through untouched', () => {
|
|
25
|
-
expect(resolveAxisDomain({ domain: [0, 100] }, rows, both)).toEqual([0, 100])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('auto pads the upper bound away from zero and keeps the zero baseline', () => {
|
|
29
|
-
const [min, max] = resolveAxisDomain(undefined, rows, both)
|
|
30
|
-
expect(min).toBe(0)
|
|
31
|
-
expect(max).toBeCloseTo(44, 5)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('auto over an ALL-NEGATIVE series keeps the largest value inside the plot', () => {
|
|
35
|
-
// `max * pad` would push the upper bound to -5.5, i.e. below the largest datum, clipping it.
|
|
36
|
-
const negative: ChartSeries<Row>[] = [
|
|
37
|
-
{ ...seriesFor('a'), getValue: (d) => -d.a },
|
|
38
|
-
{ ...seriesFor('b'), getValue: (d) => -d.b },
|
|
39
|
-
]
|
|
40
|
-
const [min, max] = resolveAxisDomain({ autoMinCeil: Infinity }, rows, negative)
|
|
41
|
-
expect(max).toBeGreaterThanOrEqual(-4)
|
|
42
|
-
expect(min).toBeLessThanOrEqual(-40)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('autoMaxFloor raises a low upper bound', () => {
|
|
46
|
-
const [, max] = resolveAxisDomain({ autoMaxFloor: 500 }, rows, both)
|
|
47
|
-
expect(max).toBe(500)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('the domain follows the VISIBLE series — hiding the tall one shrinks the axis', () => {
|
|
51
|
-
const [, withBoth] = resolveAxisDomain(undefined, rows, both)
|
|
52
|
-
const [, onlyB] = resolveAxisDomain(undefined, rows, [seriesFor('b')])
|
|
53
|
-
expect(onlyB).toBeLessThan(withBoth)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('a domain function receives the data AND the visible series', () => {
|
|
57
|
-
const domain = resolveAxisDomain(
|
|
58
|
-
{ domain: (data, visible) => [0, data.length * visible.length] },
|
|
59
|
-
rows,
|
|
60
|
-
both,
|
|
61
|
-
)
|
|
62
|
-
expect(domain).toEqual([0, 6])
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test('empty data falls back to a usable unit domain', () => {
|
|
66
|
-
expect(resolveAxisDomain(undefined, [], both)).toEqual([0, 1])
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('resolveAxisDomain — overlay bounds', () => {
|
|
71
|
-
test('a zone past the data stretches the axis instead of clipping', () => {
|
|
72
|
-
const [, max] = resolveAxisDomain(undefined, rows, both, [0, 200])
|
|
73
|
-
expect(max).toBeGreaterThanOrEqual(200)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('a reference line below the data lowers the floor', () => {
|
|
77
|
-
const [min] = resolveAxisDomain({ autoMinCeil: Infinity }, rows, both, [-30])
|
|
78
|
-
expect(min).toBeLessThanOrEqual(-30)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('infinite bounds ("top/bottom of axis") are ignored, not blown up', () => {
|
|
82
|
-
const plain = resolveAxisDomain(undefined, rows, both)
|
|
83
|
-
expect(resolveAxisDomain(undefined, rows, both, [-Infinity, Infinity])).toEqual(plain)
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('resolveAxisDomain — degenerate input', () => {
|
|
88
|
-
test('a flat all-zero series still gets a usable axis, not a zero-extent scale', () => {
|
|
89
|
-
const flat: ChartSeries<Row>[] = [{ ...seriesFor('a'), getValue: () => 0 }]
|
|
90
|
-
const [min, max] = resolveAxisDomain(undefined, rows, flat)
|
|
91
|
-
expect(max).toBeGreaterThan(min)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('a flat non-zero series is not collapsed onto its own value', () => {
|
|
95
|
-
const flat: ChartSeries<Row>[] = [{ ...seriesFor('a'), getValue: () => 7 }]
|
|
96
|
-
const [min, max] = resolveAxisDomain({ autoMinCeil: Infinity }, rows, flat)
|
|
97
|
-
expect(max).toBeGreaterThan(min)
|
|
98
|
-
})
|
|
99
|
-
})
|