basalt-ui 1.30.0 → 1.30.2
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 +15 -0
- package/MIGRATING.md +284 -30
- package/dist/charts/index.d.ts +1 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.d.ts +5 -3
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.d.ts +4 -2
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +4 -2
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +4 -2
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +5 -2
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.d.ts +4 -2
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +4 -2
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +4 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +4 -2
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.d.ts +4 -1
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.d.ts +20 -11
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +39 -14
- package/dist/charts/primitives/ChartLegend.d.ts +5 -3
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.d.ts +64 -9
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +35 -6
- package/dist/common/errors.d.ts +11 -0
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +4 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/index.ts +1 -0
- package/src/charts/kinds/BandStrip.tsx +5 -3
- package/src/charts/kinds/Bars.tsx +4 -2
- package/src/charts/kinds/Donut.tsx +4 -2
- package/src/charts/kinds/DualPanel.tsx +4 -2
- package/src/charts/kinds/Heatmap.tsx +5 -2
- package/src/charts/kinds/MirroredBars.tsx +4 -2
- package/src/charts/kinds/MultiLine.tsx +4 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/kinds/ZonedLine.tsx +4 -2
- package/src/charts/primitives/CartesianChart.tsx +4 -1
- package/src/charts/primitives/ChartFrame.tsx +72 -37
- package/src/charts/primitives/ChartLegend.tsx +5 -3
- package/src/charts/primitives/chart-frame-layout.ts +123 -16
- package/src/common/errors.ts +14 -0
|
@@ -137,7 +137,7 @@ export type ChartTierMetrics = {
|
|
|
137
137
|
/** Floating tooltip `minWidth`, px. */
|
|
138
138
|
tooltipMinWidth: number
|
|
139
139
|
/** Default entry cap on the legend, or `undefined` for no default cap. An explicit
|
|
140
|
-
* `legend.maxRows`
|
|
140
|
+
* `legend.maxRows` replaces it outright ({@link resolveLegendMaxRows}). */
|
|
141
141
|
legendMaxRows: number | undefined
|
|
142
142
|
/** Per-side margin FLOORS (`VX.margin` is a floor, never a ceiling — §1). */
|
|
143
143
|
margin: ChartMargin
|
|
@@ -186,9 +186,14 @@ export function chartTierMetrics(tier: ChartTier): ChartTierMetrics {
|
|
|
186
186
|
* panel on a 1440px desktop is "a phone", so a six-series legend rolled up to two rows + `+4 more`
|
|
187
187
|
* with no way to say no. `margin` and `xLabelRotate` always had that escape; the legend did not.
|
|
188
188
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
189
|
+
* The measured {@link legendEntryCap} a `fill` frame applies is NOT a second ceiling on top of
|
|
190
|
+
* this: it resolves an ABSENT cap only. 1.30.0 ran it over the caller's number as well
|
|
191
|
+
* (`Math.min(fitted, caller)`), which made "wins outright" false again in the one place it was
|
|
192
|
+
* needed — meteo measured a 7-entry legend pinned to 2 entries at `maxRows` 3, 6 AND 99, with 5
|
|
193
|
+
* series drawn in colours the legend refused to name. The fit is not physics either: it derives
|
|
194
|
+
* from `VX.minPlotHeight`, which is a framework DEFAULT about the plot. Two defaults do not
|
|
195
|
+
* outvote the one number a caller stated, so the PLOT yields the height instead
|
|
196
|
+
* ({@link resolvePlotRect}'s `legendWins`) — visibly, in the frame the caller sized.
|
|
192
197
|
*/
|
|
193
198
|
export function resolveLegendMaxRows(input: {
|
|
194
199
|
callerMaxRows?: number | undefined
|
|
@@ -197,6 +202,48 @@ export function resolveLegendMaxRows(input: {
|
|
|
197
202
|
return input.callerMaxRows ?? chartTierMetrics(input.tier).legendMaxRows
|
|
198
203
|
}
|
|
199
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The whole legend-cap decision in one place: which cap `ChartLegend` gets, and whether the plot
|
|
207
|
+
* pays for it.
|
|
208
|
+
*
|
|
209
|
+
* It lives here rather than inline in `ChartFrame` because the COMPOSITION is what broke. 1.30.0
|
|
210
|
+
* had both halves right on their own — {@link resolveLegendMaxRows} honoured a stated cap,
|
|
211
|
+
* {@link legendEntryCap} measured a fit — and then ran the second over the first, which no test
|
|
212
|
+
* could see because neither pure function was wrong and the frame that combined them needs a
|
|
213
|
+
* `ResizeObserver` to test. Now the combination is a pure function too.
|
|
214
|
+
*
|
|
215
|
+
* Three cases, in order:
|
|
216
|
+
* 1. The caller STATED a number → it is the cap, and on a `fill` band the plot yields
|
|
217
|
+
* (`legendWins`). Nothing measured or defaulted trims it.
|
|
218
|
+
* 2. No `fill` band → the tier's default (or none), because the frame simply grows.
|
|
219
|
+
* 3. A `fill` band with nothing stated → the measured fit, bounded by the tier's default.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveLegendRollup(input: {
|
|
222
|
+
/** The caller's own `legend.maxRows`. */
|
|
223
|
+
statedMaxRows?: number | undefined
|
|
224
|
+
tier: ChartTier
|
|
225
|
+
/** A visible top/bottom legend on a `fill` frame — the one shape whose box cannot grow. */
|
|
226
|
+
fillBand: boolean
|
|
227
|
+
items: readonly LegendEntry[]
|
|
228
|
+
containerW: number
|
|
229
|
+
/** Frame height the legend may consume — `resolvedHeight - VX.minPlotHeight`. */
|
|
230
|
+
available: number
|
|
231
|
+
}): { maxRows: number | undefined; legendWins: boolean } {
|
|
232
|
+
const { statedMaxRows, tier, fillBand, items, containerW, available } = input
|
|
233
|
+
if (statedMaxRows !== undefined) return { maxRows: statedMaxRows, legendWins: fillBand }
|
|
234
|
+
const tierMaxRows = resolveLegendMaxRows({ callerMaxRows: undefined, tier })
|
|
235
|
+
if (!fillBand) return { maxRows: tierMaxRows, legendWins: false }
|
|
236
|
+
return {
|
|
237
|
+
maxRows: legendEntryCap({
|
|
238
|
+
items,
|
|
239
|
+
containerW,
|
|
240
|
+
available,
|
|
241
|
+
...(tierMaxRows !== undefined && { defaultMaxRows: tierMaxRows }),
|
|
242
|
+
}),
|
|
243
|
+
legendWins: false,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
200
247
|
/**
|
|
201
248
|
* Resolve the plot rect from the measured box and the measured legend band. Pure and exported so
|
|
202
249
|
* the two floors below are testable without a `ResizeObserver`.
|
|
@@ -211,9 +258,12 @@ export function resolveLegendMaxRows(input: {
|
|
|
211
258
|
* `height={240}` toward zero and the body stopped rendering entirely. The plot stops at
|
|
212
259
|
* `VX.minPlotHeight` and the frame's own box grows by the difference instead — it is a flex
|
|
213
260
|
* column with `height: auto`, so that growth is automatic. Under `fill` the box CANNOT grow, so
|
|
214
|
-
* the legend is capped instead ({@link legendEntryCap})
|
|
215
|
-
*
|
|
216
|
-
*
|
|
261
|
+
* the legend is capped instead ({@link legendEntryCap}) — UNLESS the caller stated
|
|
262
|
+
* `legend.maxRows`, which is `legendWins`: there the floor itself yields, because a floor is a
|
|
263
|
+
* default and the caller's number is not, but only as far as the box HAS room
|
|
264
|
+
* ({@link SELF_MEASURED_SLACK}). An unmeasured box (`resolvedHeight <= 0`, i.e. a `fill` frame
|
|
265
|
+
* before its first measurement) stays at 0 and renders nothing, exactly as before — the floor
|
|
266
|
+
* must never invent a height for a box nobody has measured.
|
|
217
267
|
*/
|
|
218
268
|
export function resolvePlotRect(input: {
|
|
219
269
|
containerW: number
|
|
@@ -221,15 +271,64 @@ export function resolvePlotRect(input: {
|
|
|
221
271
|
minWidth: number
|
|
222
272
|
sideLegendWidth: number
|
|
223
273
|
topBottomLegendHeight: number
|
|
274
|
+
/**
|
|
275
|
+
* The caller stated `legend.maxRows` on a `fill` frame, so the legend takes the rows it asked
|
|
276
|
+
* for and the plot takes what is left — `VX.minPlotHeight` stops being a floor. Without this the
|
|
277
|
+
* honoured cap would push the frame's content PAST its own cell (the plot holds 120px, the legend
|
|
278
|
+
* band adds its own) and paint over whatever sits below it; with it, the cost of the caller's
|
|
279
|
+
* number lands where they can see it. Spend the whole box on legend rows and there is no plot
|
|
280
|
+
* left to draw — that is their arithmetic, made visible rather than silently rolled up.
|
|
281
|
+
*/
|
|
282
|
+
legendWins?: boolean
|
|
224
283
|
}): { width: number; height: number } {
|
|
225
|
-
const {
|
|
284
|
+
const {
|
|
285
|
+
containerW,
|
|
286
|
+
resolvedHeight,
|
|
287
|
+
minWidth,
|
|
288
|
+
sideLegendWidth,
|
|
289
|
+
topBottomLegendHeight,
|
|
290
|
+
legendWins = false,
|
|
291
|
+
} = input
|
|
292
|
+
const room = resolvedHeight - topBottomLegendHeight
|
|
293
|
+
const plotFloor = legendWins && !isSelfMeasured(room) ? 0 : VX.minPlotHeight
|
|
226
294
|
return {
|
|
227
295
|
width: containerW === 0 ? minWidth : Math.max(containerW - sideLegendWidth, 1),
|
|
228
|
-
height:
|
|
229
|
-
resolvedHeight <= 0 ? 0 : Math.max(resolvedHeight - topBottomLegendHeight, VX.minPlotHeight),
|
|
296
|
+
height: resolvedHeight <= 0 ? 0 : Math.max(room, plotFloor),
|
|
230
297
|
}
|
|
231
298
|
}
|
|
232
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Sub-pixel slack on "the frame measures its own legend band and nothing else".
|
|
302
|
+
*
|
|
303
|
+
* MEASURED exact in the collapse this guards (both `ResizeObserver` contentRects read 29.375 in
|
|
304
|
+
* headless Chrome, `tests/layout/charts.layout.test.ts` INVARIANT 7) — the slack exists only so a
|
|
305
|
+
* future rounding or border difference between the two observers cannot re-open a fixpoint that
|
|
306
|
+
* has no other way out. A plot this thin has nothing to draw either way.
|
|
307
|
+
*/
|
|
308
|
+
const SELF_MEASURED_SLACK = 0.5
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Is the frame's height its OWN content rather than a box something else stated?
|
|
312
|
+
*
|
|
313
|
+
* `ChartFrame` measures its own node (`useChartSize` → visx `useParentSize` observes the element
|
|
314
|
+
* the ref is attached to, its name notwithstanding), and under `fill` its `height: 100%` resolves
|
|
315
|
+
* to `auto` in any parent that states no height. Its only children are the plot and the legend, so
|
|
316
|
+
* `room === 0` means the box IS the legend band — which happens exactly when no plot was drawn.
|
|
317
|
+
*
|
|
318
|
+
* That is why `legendWins` cannot yield the floor there: with the floor gone the sequence has a
|
|
319
|
+
* FIXPOINT AT ZERO — plot 0 → content is the legend → box = band → room 0 → plot 0, forever, and
|
|
320
|
+
* the chart never appears. Keeping the floor while `room` is 0 converges on the same box a `fill`
|
|
321
|
+
* frame in an unsized parent has always had (plot `VX.minPlotHeight`, box = plot + band), from
|
|
322
|
+
* which `room` is positive and `legendWins` applies normally.
|
|
323
|
+
*
|
|
324
|
+
* A NEGATIVE `room` is not this case and must keep yielding: a self-measured box can never be
|
|
325
|
+
* shorter than its own content, so a band taller than the box means the box was stated by
|
|
326
|
+
* something else, and spending all of it on legend rows is the caller's own arithmetic.
|
|
327
|
+
*/
|
|
328
|
+
function isSelfMeasured(room: number): boolean {
|
|
329
|
+
return room >= 0 && room < SELF_MEASURED_SLACK
|
|
330
|
+
}
|
|
331
|
+
|
|
233
332
|
/** Height of a legend band `rows` rows tall, wrapper padding included. */
|
|
234
333
|
const legendBandHeight = (rows: number): number =>
|
|
235
334
|
LEGEND_PAD_Y + rows * LEGEND_LINE_H + Math.max(rows - 1, 0) * VX.legendGap
|
|
@@ -272,17 +371,25 @@ function entriesWithinRows(items: readonly LegendEntry[], width: number, rows: n
|
|
|
272
371
|
* MEASURING the labels, not from assuming one entry per row — otherwise a five-entry legend that
|
|
273
372
|
* fits on one line would roll up to `+2 more` in every 240px cell, moving rendering for charts
|
|
274
373
|
* that never had the bug. Returns `undefined` when the whole legend already fits.
|
|
374
|
+
*
|
|
375
|
+
* This resolves an ABSENT cap. A caller who stated `legend.maxRows` never reaches here at all —
|
|
376
|
+
* `ChartFrame` routes around it — because `Math.min`-ing a stated number against this one made the
|
|
377
|
+
* documented escape inert in exactly the narrow `fill` panel it exists for
|
|
378
|
+
* ({@link resolveLegendMaxRows}).
|
|
275
379
|
*/
|
|
276
380
|
export function legendEntryCap(input: {
|
|
277
381
|
items: readonly LegendEntry[]
|
|
278
382
|
containerW: number
|
|
279
383
|
/** Frame height the legend may consume — `resolvedHeight - VX.minPlotHeight`. */
|
|
280
384
|
available: number
|
|
281
|
-
/**
|
|
282
|
-
|
|
385
|
+
/**
|
|
386
|
+
* The cap that applies when nothing explicit was stated — today only the tier's own default
|
|
387
|
+
* ({@link chartTierMetrics}). Two defaults contending: the smaller wins.
|
|
388
|
+
*/
|
|
389
|
+
defaultMaxRows?: number
|
|
283
390
|
}): number | undefined {
|
|
284
|
-
const { items, containerW, available,
|
|
285
|
-
if (containerW <= 0 || items.length === 0) return
|
|
391
|
+
const { items, containerW, available, defaultMaxRows } = input
|
|
392
|
+
if (containerW <= 0 || items.length === 0) return defaultMaxRows
|
|
286
393
|
const rows = Math.max(
|
|
287
394
|
1,
|
|
288
395
|
Math.floor((available - LEGEND_PAD_Y + VX.legendGap) / (LEGEND_LINE_H + VX.legendGap)),
|
|
@@ -291,8 +398,8 @@ export function legendEntryCap(input: {
|
|
|
291
398
|
legendBandHeight(rows) <= available &&
|
|
292
399
|
entriesWithinRows(items, containerW, rows) >= items.length
|
|
293
400
|
) {
|
|
294
|
-
return
|
|
401
|
+
return defaultMaxRows
|
|
295
402
|
}
|
|
296
403
|
const fitted = Math.max(1, entriesWithinRows(items, containerW, rows))
|
|
297
|
-
return
|
|
404
|
+
return defaultMaxRows === undefined ? fitted : Math.min(fitted, defaultMaxRows)
|
|
298
405
|
}
|
package/src/common/errors.ts
CHANGED
|
@@ -55,6 +55,20 @@ export function deprecatedProp(
|
|
|
55
55
|
)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A prop the component ACCEPTS, reads nothing from, and cannot report as wrong — because a SECOND
|
|
60
|
+
* prop it was passed alongside decides the same thing and won.
|
|
61
|
+
*
|
|
62
|
+
* Distinct from {@link deprecatedProp} (still read, going away) and {@link oneOf} (read, and the
|
|
63
|
+
* value is out of range): this one renders something the caller did not ask for and nothing in the
|
|
64
|
+
* type system, the linter or the console says so. `ChartFrame`'s `height` under `fill` is the
|
|
65
|
+
* reference case — meteo passed both for the life of the file and never learnt that eight height
|
|
66
|
+
* constants and a whole `compact` branch changed no pixel.
|
|
67
|
+
*/
|
|
68
|
+
export function ignoredProp(component: string, prop: string, because: string): string {
|
|
69
|
+
return message(component, `prop "${prop}" is ignored because ${because}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
/**
|
|
59
73
|
* An imperative call needs a layer that is not there — the optional peer is not installed, or the
|
|
60
74
|
* mount that subscribes to its event bus is disabled. Both halves are named because the remedy
|