@micha.bigler/ui-core-micha 3.1.2 → 3.2.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 +47 -0
- package/WORK_ORDERS.md +3 -1
- package/dist/components/charts/BarChart.js +5 -4
- package/dist/components/charts/LineChart.js +5 -4
- package/dist/components/charts/ScatterChart.js +5 -4
- package/dist/components/charts/chartDefaults.js +49 -12
- package/docs/CHART-LAYOUT.md +22 -15
- package/package.json +1 -1
- package/src/components/charts/BarChart.jsx +5 -4
- package/src/components/charts/LineChart.jsx +5 -4
- package/src/components/charts/ScatterChart.jsx +5 -4
- package/src/components/charts/chartDefaults.js +50 -11
- package/tests/BarChart.test.jsx +10 -8
- package/tests/LineChart.test.jsx +6 -4
- package/tests/ScatterChart.test.jsx +6 -4
- package/tests/chartAxisTickBand.test.jsx +95 -0
- package/tests/chartDefaults.test.js +65 -8
- package/work-orders/UCM-CHART-15.md +187 -0
- package/work-orders/UCM-CHART-16.md +169 -0
|
@@ -153,6 +153,29 @@ export const PACKAGE_DEFAULT_MARGIN = {
|
|
|
153
153
|
// as clipped even when nothing is actually cut off).
|
|
154
154
|
const ROTATED_TICK_CLEARANCE_PX = 8;
|
|
155
155
|
|
|
156
|
+
// UCM-CHART-16: the FLAT (angle 0) tick band used to be the unconditional TICK_BAND_BASE_PX
|
|
157
|
+
// regardless of the actual tick font size -- fine for MUI's own generic default, but MUI's
|
|
158
|
+
// internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) blanks EVERY tick label the
|
|
159
|
+
// moment the real rendered text is even slightly taller than what that flat floor leaves room
|
|
160
|
+
// for once MUI's own tick/gap overhead (and, when an axis title shares the band, its own gap) is
|
|
161
|
+
// subtracted. `TICK_BAND_BASE_PX` alone was a near-miss with essentially no margin at any font
|
|
162
|
+
// size above whatever MUI itself implicitly assumed -- two hram consumers independently patched
|
|
163
|
+
// around it with the same empirically-measured `height: 56`, and a third, titleless case broke at
|
|
164
|
+
// a tick font just 1px above the untitled default.
|
|
165
|
+
//
|
|
166
|
+
// These three constants are DERIVED, not guessed: binary-searched against the real, unmocked
|
|
167
|
+
// `BarChart` rendered in this repo's own dev harness (`dev/entries.jsx`'s `chart-probe` entry,
|
|
168
|
+
// Chrome, this package's own DM Sans default -- the taller of the two fonts checked; MUI's Roboto
|
|
169
|
+
// fallback needs visibly less and was never the trigger) at tick font sizes 11-18px, with and
|
|
170
|
+
// without an axis title:
|
|
171
|
+
// required flat tickBand ~= ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
|
|
172
|
+
// required tickBand+title ~= that, plus TICK_BAND_TITLE_EXTRA_PX, plus AXIS_TITLE_BAND_PX
|
|
173
|
+
// matched every measured threshold exactly or over by at most 1px, never under, across ten
|
|
174
|
+
// font-size/title combinations -- see UCM-CHART-16's work order for the full measurement table.
|
|
175
|
+
const TICK_TEXT_HEIGHT_FACTOR = 1.3;
|
|
176
|
+
const TICK_BAND_MUI_OVERHEAD_PX = 9;
|
|
177
|
+
const TICK_BAND_TITLE_EXTRA_PX = 2;
|
|
178
|
+
|
|
156
179
|
// THEME-9: MUI X-Charts' own default y-axis width is a flat 45px (ticks) +
|
|
157
180
|
// 20px (label, if present) = 65px regardless of what the ticks actually say
|
|
158
181
|
// -- confirmed against @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH /
|
|
@@ -291,13 +314,18 @@ export function defaultNumericTickFormatter(locale) {
|
|
|
291
314
|
: new Intl.NumberFormat(locale).format(value));
|
|
292
315
|
}
|
|
293
316
|
|
|
294
|
-
// UCM-CHART-
|
|
295
|
-
// (`src/theme/tokens.js`'s `spacing: 8`). `standard`
|
|
296
|
-
// default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so
|
|
297
|
-
// silently redraw every already-shipped default-sized chart
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
317
|
+
// UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
|
|
318
|
+
// (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
|
|
319
|
+
// the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
|
|
320
|
+
// migration did not also silently redraw every already-shipped default-sized chart -- that pin
|
|
321
|
+
// was a migration guardrail, not a design verdict. The migration it protected is complete, and
|
|
322
|
+
// the operator judged the whole scale too cramped (`tall` had already forced hram to use the
|
|
323
|
+
// `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
|
|
324
|
+
// adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
|
|
325
|
+
// `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
|
|
326
|
+
export const CHART_SIZE_SPACING_UNITS = {
|
|
327
|
+
compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
|
|
328
|
+
};
|
|
301
329
|
|
|
302
330
|
// UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
|
|
303
331
|
// constant taken on faith. These two ARE constants, but only for the "content is present" case;
|
|
@@ -384,7 +412,18 @@ function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextW
|
|
|
384
412
|
angle = needsRotation ? -45 : 0;
|
|
385
413
|
} // "horizontal" always stays at angle 0
|
|
386
414
|
|
|
387
|
-
if (angle === 0)
|
|
415
|
+
if (angle === 0) {
|
|
416
|
+
// UCM-CHART-16: derive the flat band from the actual tick font size instead of the
|
|
417
|
+
// unconditional floor -- `axis?.label` is the SAME signal `resolveChartLayout` uses to decide
|
|
418
|
+
// whether an axis title also shares this band (`xTitleBand`), so a titled axis reserves the
|
|
419
|
+
// extra room its own gap costs, precisely where the near-miss was.
|
|
420
|
+
const hasTitle = Boolean(axis?.label);
|
|
421
|
+
const flatTickBand = fontSize > 0
|
|
422
|
+
? Math.ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
|
|
423
|
+
+ (hasTitle ? TICK_BAND_TITLE_EXTRA_PX : 0)
|
|
424
|
+
: TICK_BAND_BASE_PX;
|
|
425
|
+
return { tickBand: Math.max(TICK_BAND_BASE_PX, flatTickBand), angle: 0 };
|
|
426
|
+
}
|
|
388
427
|
// Rotation is a visual instruction independent of whether a font size is knowable here -- an
|
|
389
428
|
// "angled" axis still rotates even when `fontSize` can't be resolved (no theme fallback given),
|
|
390
429
|
// it just can't project a tighter band than the floor without one.
|
|
@@ -404,11 +443,11 @@ function resolveChartHeightPx(size, height, spacing) {
|
|
|
404
443
|
const units = CHART_SIZE_SPACING_UNITS[size];
|
|
405
444
|
if (units == null) {
|
|
406
445
|
throw new Error(
|
|
407
|
-
`[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall"
|
|
408
|
-
+ 'or pass height (in px) for the documented escape.',
|
|
446
|
+
`[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
|
|
447
|
+
+ '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.',
|
|
409
448
|
);
|
|
410
449
|
}
|
|
411
|
-
// MUI's `theme.spacing(n)` returns a CSS length STRING ("
|
|
450
|
+
// MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
|
|
412
451
|
// height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
|
|
413
452
|
// back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
|
|
414
453
|
return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
|
package/tests/BarChart.test.jsx
CHANGED
|
@@ -31,10 +31,10 @@ describe('BarChart preset', () => {
|
|
|
31
31
|
legend: { position: { vertical: 'bottom', horizontal: 'start' } },
|
|
32
32
|
});
|
|
33
33
|
expect(props.hideLegend).toBe(true);
|
|
34
|
-
// UCM-CHART-12: size defaults to "standard" (
|
|
35
|
-
//
|
|
36
|
-
expect(props.height).toBe(
|
|
37
|
-
expect(window.getComputedStyle(screen.getByTestId('bar-chart-container')).height).toBe('
|
|
34
|
+
// UCM-CHART-12/UCM-CHART-15: size defaults to "standard" (400px) -- the wrapper always
|
|
35
|
+
// carries an explicit height now, width stays responsive.
|
|
36
|
+
expect(props.height).toBe(400);
|
|
37
|
+
expect(window.getComputedStyle(screen.getByTestId('bar-chart-container')).height).toBe('400px');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it.each([
|
|
@@ -78,11 +78,13 @@ describe('BarChart preset', () => {
|
|
|
78
78
|
// UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
|
|
79
79
|
// deleted `minHeight`/`aspect` CHART-8 trio -- see the removed-prop assertions below for the
|
|
80
80
|
// migration side of this.
|
|
81
|
-
describe('size/height resolution (UCM-CHART-12)', () => {
|
|
81
|
+
describe('size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
|
|
82
82
|
it.each([
|
|
83
|
-
['compact',
|
|
84
|
-
['standard',
|
|
85
|
-
['tall',
|
|
83
|
+
['compact', 320],
|
|
84
|
+
['standard', 400],
|
|
85
|
+
['tall', 480],
|
|
86
|
+
['extra_tall', 560],
|
|
87
|
+
['super_tall', 640],
|
|
86
88
|
])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
|
|
87
89
|
renderChart({ series: [{ data: [1] }], size });
|
|
88
90
|
expect(chartSpy.mock.calls.at(-1)[0].height).toBe(px);
|
package/tests/LineChart.test.jsx
CHANGED
|
@@ -36,7 +36,7 @@ describe('LineChart preset', () => {
|
|
|
36
36
|
|
|
37
37
|
// UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
|
|
38
38
|
// deleted `minHeight`/`aspect` CHART-8 trio.
|
|
39
|
-
describe('size/height resolution (UCM-CHART-12)', () => {
|
|
39
|
+
describe('size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
|
|
40
40
|
function renderLine(props) {
|
|
41
41
|
return render(
|
|
42
42
|
<ThemeProvider theme={createTheme()}>
|
|
@@ -48,9 +48,11 @@ describe('LineChart preset', () => {
|
|
|
48
48
|
afterEach(cleanup);
|
|
49
49
|
|
|
50
50
|
it.each([
|
|
51
|
-
['compact',
|
|
52
|
-
['standard',
|
|
53
|
-
['tall',
|
|
51
|
+
['compact', 320],
|
|
52
|
+
['standard', 400],
|
|
53
|
+
['tall', 480],
|
|
54
|
+
['extra_tall', 560],
|
|
55
|
+
['super_tall', 640],
|
|
54
56
|
])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
|
|
55
57
|
renderLine({ size });
|
|
56
58
|
expect(chartSpy.mock.calls.at(-1)[0].height).toBe(px);
|
|
@@ -235,13 +235,15 @@ describe('ScatterReferenceCurve / ScatterReferenceLine (THEME-10)', () => {
|
|
|
235
235
|
|
|
236
236
|
// UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
|
|
237
237
|
// deleted `minHeight`/`aspect` CHART-8 trio.
|
|
238
|
-
describe('ScatterChart size/height resolution (UCM-CHART-12)', () => {
|
|
238
|
+
describe('ScatterChart size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
|
|
239
239
|
afterEach(() => { cleanup(); vi.clearAllMocks(); });
|
|
240
240
|
|
|
241
241
|
it.each([
|
|
242
|
-
['compact',
|
|
243
|
-
['standard',
|
|
244
|
-
['tall',
|
|
242
|
+
['compact', 320],
|
|
243
|
+
['standard', 400],
|
|
244
|
+
['tall', 480],
|
|
245
|
+
['extra_tall', 560],
|
|
246
|
+
['super_tall', 640],
|
|
245
247
|
])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
|
|
246
248
|
renderScatter({ size });
|
|
247
249
|
expect(scatterSpy.mock.calls.at(-1)[0].height).toBe(px);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {
|
|
4
|
+
afterAll, afterEach, beforeAll, describe, expect, it,
|
|
5
|
+
} from 'vitest';
|
|
6
|
+
import { cleanup, render } from '@testing-library/react';
|
|
7
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
8
|
+
|
|
9
|
+
import { BarChart } from '../src/components/charts/BarChart';
|
|
10
|
+
|
|
11
|
+
// UCM-CHART-16: MUI's own tick-label fit check (`shortenLabels`, weighed against
|
|
12
|
+
// `tickLabelsMaxHeight`) depends on a REAL SVG text measurement (`getBBox`), which jsdom does not
|
|
13
|
+
// implement -- `SVGElement.prototype.getBBox` is not a function here, so MUI's own
|
|
14
|
+
// `getStringSize` falls back to `getBoundingClientRect`, which jsdom always reports as 0x0. Every
|
|
15
|
+
// measured height therefore comes back 0, MUI's fit check trivially "fits" any text, and the real
|
|
16
|
+
// defect (every tick label blanked) can never be observed by rendering the unmocked chart as-is --
|
|
17
|
+
// a test that skips this setup would pass whether or not the resolver's band is correct, i.e. be
|
|
18
|
+
// exactly the "mocked prop-assertion" trap this package's own tests warn against elsewhere.
|
|
19
|
+
//
|
|
20
|
+
// This installs a deterministic `getBBox` instead, so the SAME code MUI itself runs is actually
|
|
21
|
+
// exercised: height scaled off the measurement span's own `fontSize`, at a ratio (1.28) chosen to
|
|
22
|
+
// sit inside the real near-miss window this WO's own work order measured against a real browser
|
|
23
|
+
// (`resolveXAxisGeometry`'s `TICK_TEXT_HEIGHT_FACTOR` comment) -- large enough that the pre-fix
|
|
24
|
+
// unconditional floor already fails a titled axis at the theme's own default tick font, small
|
|
25
|
+
// enough that the untitled default (never broken) still passes. Width is deliberately near-zero so
|
|
26
|
+
// only the HEIGHT dimension of MUI's fit check is exercised; each chart also sets an explicit
|
|
27
|
+
// `width` so MUI's real responsive sizing (ResizeObserver, unavailable in jsdom) never enters into
|
|
28
|
+
// it.
|
|
29
|
+
function installHeightOnlyTextMeasurement() {
|
|
30
|
+
const original = SVGElement.prototype.getBBox;
|
|
31
|
+
// eslint-disable-next-line func-names
|
|
32
|
+
SVGElement.prototype.getBBox = function getBBox() {
|
|
33
|
+
const fontSize = Number.parseFloat(this.style.fontSize) || 12;
|
|
34
|
+
return { x: 0, y: 0, width: 1, height: fontSize * 1.28 };
|
|
35
|
+
};
|
|
36
|
+
return () => {
|
|
37
|
+
SVGElement.prototype.getBBox = original;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('x-axis tick band leaves MUI room for one real line of tick text (UCM-CHART-16)', () => {
|
|
42
|
+
let restoreGetBBox;
|
|
43
|
+
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
restoreGetBBox = installHeightOnlyTextMeasurement();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
restoreGetBBox();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(cleanup);
|
|
53
|
+
|
|
54
|
+
// WO scope item 1's own signature: an axis title AND horizontal tick labels sharing the same
|
|
55
|
+
// band, at the theme's own default tick font -- no explicit `height`/`tickLabelStyle` overrides
|
|
56
|
+
// anywhere, so this exercises the resolver's own default reservation end to end, the exact path
|
|
57
|
+
// two hram consumers had to work around by hand.
|
|
58
|
+
it('renders non-empty tick labels when the axis has both a title and horizontal ticks', () => {
|
|
59
|
+
const { container } = render(
|
|
60
|
+
<ThemeProvider theme={createTheme()}>
|
|
61
|
+
<BarChart
|
|
62
|
+
series={[{ data: [24, 36, 28], label: 'Opened' }]}
|
|
63
|
+
xAxis={[{ data: ['A', 'B', 'C'] }]}
|
|
64
|
+
xAxisLabel="Category"
|
|
65
|
+
width={600}
|
|
66
|
+
/>
|
|
67
|
+
</ThemeProvider>,
|
|
68
|
+
);
|
|
69
|
+
const tickTexts = Array.from(container.querySelectorAll('.MuiChartsAxis-bottom .MuiChartsAxis-tickLabel'))
|
|
70
|
+
.map((node) => node.textContent);
|
|
71
|
+
expect(tickTexts).toHaveLength(3);
|
|
72
|
+
expect(tickTexts.every((text) => text.length > 0)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Non-goal guardrail: the rotated path projects its own band from real measured/estimated text
|
|
76
|
+
// width and must stay exactly as before -- this WO only touches the flat (angle 0) case.
|
|
77
|
+
it('leaves the rotated tick path unaffected', () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<ThemeProvider theme={createTheme()}>
|
|
80
|
+
<BarChart
|
|
81
|
+
series={[{ data: [24, 36, 28, 12, 9, 7, 15] }]}
|
|
82
|
+
xAxis={[{
|
|
83
|
+
data: Array.from({ length: 7 }, (_, i) => `A rather long category label ${i}`),
|
|
84
|
+
}]}
|
|
85
|
+
xLabels="angled"
|
|
86
|
+
width={600}
|
|
87
|
+
/>
|
|
88
|
+
</ThemeProvider>,
|
|
89
|
+
);
|
|
90
|
+
const tickTexts = Array.from(container.querySelectorAll('.MuiChartsAxis-bottom .MuiChartsAxis-tickLabel'))
|
|
91
|
+
.map((node) => node.textContent);
|
|
92
|
+
expect(tickTexts).toHaveLength(7);
|
|
93
|
+
expect(tickTexts.every((text) => text.length > 0)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -115,8 +115,8 @@ describe('chart chrome defaults', () => {
|
|
|
115
115
|
expect(timeSeriesProps.grid).toEqual({ horizontal: true });
|
|
116
116
|
expect(timeSeriesProps.skipAnimation).toBe(true);
|
|
117
117
|
// UCM-CHART-12: TimeSeriesChart's inner BarChart now takes size="standard" instead of a
|
|
118
|
-
// fixed height prop
|
|
119
|
-
expect(timeSeriesProps.height).toBe(
|
|
118
|
+
// fixed height prop. UCM-CHART-15 shifted "standard" from 320px to 400px.
|
|
119
|
+
expect(timeSeriesProps.height).toBe(400);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
it('keeps caller tick font, grid, showMark, and legendPosition values instead of the defaults', () => {
|
|
@@ -551,15 +551,22 @@ describe('resolveChartLayout composition invariant (UCM-CHART-12)', () => {
|
|
|
551
551
|
});
|
|
552
552
|
});
|
|
553
553
|
|
|
554
|
-
describe('resolveChartLayout size tokens and height escape (UCM-CHART-12)', () => {
|
|
554
|
+
describe('resolveChartLayout size tokens and height escape (UCM-CHART-12, UCM-CHART-15)', () => {
|
|
555
555
|
it.each(Object.entries(CHART_SIZE_SPACING_UNITS))('resolves size="%s" through the theme spacing scale', (size, units) => {
|
|
556
556
|
const layout = resolveChartLayout({ size, spacing: theme.spacing });
|
|
557
557
|
expect(layout.chartHeight).toBe(units * 8);
|
|
558
558
|
});
|
|
559
559
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
560
|
+
// UCM-CHART-15: hardcoded independently of CHART_SIZE_SPACING_UNITS -- the test above computes
|
|
561
|
+
// its expectation from the same source under test and would pass unchanged against either the
|
|
562
|
+
// old or the new scale. This one is written to fail against the pre-CHART-15 values (240/320/400).
|
|
563
|
+
it('resolves the five size tokens to their documented pixel heights', () => {
|
|
564
|
+
const px = Object.fromEntries(['compact', 'standard', 'tall', 'extra_tall', 'super_tall'].map(
|
|
565
|
+
(size) => [size, resolveChartLayout({ size, spacing: theme.spacing }).chartHeight],
|
|
566
|
+
));
|
|
567
|
+
expect(px).toEqual({
|
|
568
|
+
compact: 320, standard: 400, tall: 480, extra_tall: 560, super_tall: 640,
|
|
569
|
+
});
|
|
563
570
|
});
|
|
564
571
|
|
|
565
572
|
it('lets height override the size token entirely', () => {
|
|
@@ -567,13 +574,63 @@ describe('resolveChartLayout size tokens and height escape (UCM-CHART-12)', () =
|
|
|
567
574
|
expect(layout.chartHeight).toBe(111);
|
|
568
575
|
});
|
|
569
576
|
|
|
570
|
-
|
|
577
|
+
// UCM-CHART-15: the height escape still wins for a token added in this WO, not only a
|
|
578
|
+
// pre-existing one.
|
|
579
|
+
it('lets height override a token that has no call site yet', () => {
|
|
580
|
+
const layout = resolveChartLayout({ size: 'super_tall', height: 111, spacing: theme.spacing });
|
|
581
|
+
expect(layout.chartHeight).toBe(111);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('throws on an unknown size token, naming all five valid tokens', () => {
|
|
571
585
|
expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(/Unknown chart size/);
|
|
586
|
+
expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(
|
|
587
|
+
/"compact" \| "standard" \| "tall" \| "extra_tall" \| "super_tall"/,
|
|
588
|
+
);
|
|
572
589
|
});
|
|
573
590
|
|
|
574
591
|
it('falls back to a numeric 8px unit when no spacing function is given', () => {
|
|
575
592
|
const layout = resolveChartLayout({ size: 'standard' });
|
|
576
|
-
expect(layout.chartHeight).toBe(
|
|
593
|
+
expect(layout.chartHeight).toBe(400);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
describe('resolveChartLayout flat x-axis tick band leaves MUI room to render (UCM-CHART-16)', () => {
|
|
598
|
+
// Third occurrence (WO Part A): no title, just a tick font 1px above the untitled default --
|
|
599
|
+
// already enough to blank every label under the pre-fix, unconditional TICK_BAND_BASE_PX=25.
|
|
600
|
+
// 26 is not invented: binary-searched against the real, unmocked `BarChart` rendered in this
|
|
601
|
+
// repo's own dev harness (Chrome, this package's own DM Sans default) -- 25 still blanked every
|
|
602
|
+
// tick, 26 was the smallest axis height that didn't. A test computing its own expectation from
|
|
603
|
+
// `CHART_TICK_TEXT_HEIGHT_FACTOR`-style constants would pass whatever those constants say,
|
|
604
|
+
// proving nothing; this number came from outside the code under test.
|
|
605
|
+
it('reserves enough room for a tick font one pixel above the untitled default (measured threshold: 26px)', () => {
|
|
606
|
+
const layout = resolveChartLayout({
|
|
607
|
+
xAxis: [{
|
|
608
|
+
scaleType: 'band',
|
|
609
|
+
data: ['Urban', 'Peri-urban', 'Rural', 'Access', 'Village'],
|
|
610
|
+
tickLabelStyle: { fontSize: 13 },
|
|
611
|
+
}],
|
|
612
|
+
yAxis: [{ scaleType: 'linear' }],
|
|
613
|
+
xLabels: 'horizontal',
|
|
614
|
+
spacing: theme.spacing,
|
|
615
|
+
});
|
|
616
|
+
expect(layout.xAxis[0].height).toBeGreaterThanOrEqual(26);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Rule 2 (WO non-goal guardrail): an axis without a title still reserves only the tick band --
|
|
620
|
+
// this WO's extra title-driven margin must not leak in when there is no title to share it with.
|
|
621
|
+
// Reviewer finding: asserting `xTitleBand === 0` alone passes whether or not the +2px title
|
|
622
|
+
// margin leaked into `xAxisBand` instead -- the band/height values themselves have to be
|
|
623
|
+
// checked against the same tick font's no-title formula (`ceil(fontSize * 1.3) + 9`).
|
|
624
|
+
it('does not add the title margin when the axis has no label', () => {
|
|
625
|
+
const layout = resolveChartLayout({
|
|
626
|
+
xAxis: [{ scaleType: 'band', data: ['A', 'B'], tickLabelStyle: { fontSize: 13 } }],
|
|
627
|
+
yAxis: [{ scaleType: 'linear' }],
|
|
628
|
+
xLabels: 'horizontal',
|
|
629
|
+
spacing: theme.spacing,
|
|
630
|
+
});
|
|
631
|
+
expect(layout.bands.xTitleBand).toBe(0);
|
|
632
|
+
expect(layout.bands.xAxisBand).toBe(26);
|
|
633
|
+
expect(layout.xAxis[0].height).toBe(26);
|
|
577
634
|
});
|
|
578
635
|
});
|
|
579
636
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# UCM-CHART-15 — Shift the chart size scale up one step, and raise the ceiling
|
|
2
|
+
|
|
3
|
+
## Part A — Envelope
|
|
4
|
+
|
|
5
|
+
*Authored by the Expertenchat. Authoritative WHAT/WHY.*
|
|
6
|
+
|
|
7
|
+
### Goal
|
|
8
|
+
|
|
9
|
+
The named chart sizes become one step taller, and the scale gains two steps above `tall`.
|
|
10
|
+
|
|
11
|
+
| token | today | new |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `compact` | 30 → 240 px | **40 → 320 px** |
|
|
14
|
+
| `standard` | 40 → 320 px | **50 → 400 px** |
|
|
15
|
+
| `tall` | 50 → 400 px | **60 → 480 px** |
|
|
16
|
+
| `extra_tall` | — | **70 → 560 px** |
|
|
17
|
+
| `super_tall` | — | **80 → 640 px** |
|
|
18
|
+
|
|
19
|
+
The 10-unit step is preserved throughout, and every value stays a clean multiple of the theme's 8 px
|
|
20
|
+
spacing unit.
|
|
21
|
+
|
|
22
|
+
### Why
|
|
23
|
+
|
|
24
|
+
Two independent pressures, both from real call sites rather than speculation.
|
|
25
|
+
|
|
26
|
+
**The ceiling is too low.** `tall` (400 px) is the largest token, and hram has already had to use the
|
|
27
|
+
documented `height` escape to get past it — `StructuralReachabilityPanel` sits at `height={480}` with
|
|
28
|
+
a comment recording exactly this: *"no size token goes taller than `tall`"*. An escape used because
|
|
29
|
+
the scale ends is a scale that ends too early.
|
|
30
|
+
|
|
31
|
+
**The whole scale is too cramped.** The operator's judgement, on existing plots and not only the new
|
|
32
|
+
ones. That judgement lands on a value that was never chosen on its merits: the comment at
|
|
33
|
+
`src/components/charts/chartDefaults.js:295` says `standard` is
|
|
34
|
+
|
|
35
|
+
> "pinned to the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this
|
|
36
|
+
> migration does not also silently redraw every already-shipped default-sized chart"
|
|
37
|
+
|
|
38
|
+
That was a **migration guardrail, not a design verdict** — 320 was kept because it was the previous
|
|
39
|
+
value. The migration it protected is complete, so the pin has served its purpose and what remains is
|
|
40
|
+
a height inherited rather than decided.
|
|
41
|
+
|
|
42
|
+
### The blast radius is one app, and that is why now
|
|
43
|
+
|
|
44
|
+
Measured across the workspace on 2026-08-24:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
hram 3.0.1 <- the only 3.x consumer
|
|
48
|
+
14 other apps 2.41.3 / 2.37.0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The size tokens are a **3.0.0 feature** (this repo's own CHANGELOG marks 3.0.0 "Breaking": `size`
|
|
52
|
+
replaced `minHeight`/`height`/`aspect`). On 2.x the scale does not exist. So no chart outside hram
|
|
53
|
+
changes, and the verification surface is one application.
|
|
54
|
+
|
|
55
|
+
**That inverts the usual caution into urgency:** fourteen apps are still to migrate onto 3.x. Every
|
|
56
|
+
app that migrates onto a scale already known to be too cramped has to be revisited afterwards.
|
|
57
|
+
Correcting the scale before they migrate is strictly cheaper than after.
|
|
58
|
+
|
|
59
|
+
### Version: minor, by operator decision — and what that obliges
|
|
60
|
+
|
|
61
|
+
Redefining what an existing public token renders is breaking under semver, and the Expertenchat
|
|
62
|
+
recommended a major. **The operator decided minor on 2026-08-24; that decision stands and this WO
|
|
63
|
+
implements it.**
|
|
64
|
+
|
|
65
|
+
The consequence has to be carried somewhere, so it is carried here: a minor version can arrive
|
|
66
|
+
through automation. Renovate's weekly run bumps minors without a human reading the diff, and on this
|
|
67
|
+
estate that triage is also the vehicle that opens `develop → main`. A silent minor could therefore
|
|
68
|
+
make every hram chart 80 px taller in a PR nobody read as a visual change.
|
|
69
|
+
|
|
70
|
+
Two obligations follow, and they are not optional decoration:
|
|
71
|
+
|
|
72
|
+
1. **The CHANGELOG entry must lead with the redraw**, in the plainest possible words: every existing
|
|
73
|
+
`compact`/`standard`/`tall` chart becomes 80 px taller. Not a bullet at the bottom — the first
|
|
74
|
+
line of the entry.
|
|
75
|
+
2. **Release and consumption are coordinated**, not left to the weekly bump. `hram`'s pin bump and
|
|
76
|
+
its own verification are `HRAM-CHT-5` and land deliberately.
|
|
77
|
+
|
|
78
|
+
### Scope
|
|
79
|
+
|
|
80
|
+
**`src/components/charts/chartDefaults.js`**
|
|
81
|
+
|
|
82
|
+
1. `CHART_SIZE_SPACING_UNITS` becomes `{ compact: 40, standard: 50, tall: 60, extra_tall: 70,
|
|
83
|
+
super_tall: 80 }`.
|
|
84
|
+
2. **Rewrite the pin comment at `:294-299`, do not delete it.** It explains why 320 was there; without
|
|
85
|
+
a successor the next reader sees an unexplained shift. The new text should say what the old pin
|
|
86
|
+
was for, that its migration is complete, and that the scale is now chosen rather than inherited.
|
|
87
|
+
|
|
88
|
+
**The enumeration sites — all of them.** The valid-size list is written out in more places than the
|
|
89
|
+
one that matters, and a half-updated enumeration is this change's most likely defect:
|
|
90
|
+
|
|
91
|
+
3. `resolveChartHeightPx`'s error message (`:407`) — `"compact" | "standard" | "tall"`.
|
|
92
|
+
4. `assertRemovedChartProp`'s guidance string in **each** chart component that carries it
|
|
93
|
+
(`LineChart.jsx:72`, `BarChart.jsx:54`, and the same call in `ScatterChart`).
|
|
94
|
+
5. The `size` JSDoc in **each** preset (`LineChart.jsx:44`, `BarChart.jsx:27`, `ScatterChart.jsx:155`).
|
|
95
|
+
6. `docs/CHART-LAYOUT.md`.
|
|
96
|
+
7. The CHANGELOG entry described above.
|
|
97
|
+
|
|
98
|
+
Grep for the literal `"tall"` before declaring this done; the list above is what was found on
|
|
99
|
+
2026-08-24 and is not guaranteed complete.
|
|
100
|
+
|
|
101
|
+
### Non-goals / do not touch
|
|
102
|
+
|
|
103
|
+
- `resolveChartLayout`'s geometry, the axis-band resolution, margins, `xLabels`, rotation logic.
|
|
104
|
+
- The `height` px escape — it stays exactly as documented.
|
|
105
|
+
- `ChartFrame`'s `minHeight` semantics.
|
|
106
|
+
- Any consuming app. hram's adoption is `HRAM-CHT-5`.
|
|
107
|
+
- Adding a sixth step. If 640 px proves too small later, the scale extends by another 10 units; do
|
|
108
|
+
not pre-add one now.
|
|
109
|
+
|
|
110
|
+
### Risks
|
|
111
|
+
|
|
112
|
+
- **A half-updated enumeration.** Adding the tokens to the resolver but not to the error message
|
|
113
|
+
produces a runtime error that lists only three valid values while five work — which reads as a bug
|
|
114
|
+
in the caller's code, not in this package.
|
|
115
|
+
- **A test that asserts the token names but not their values** would pass against both the old and
|
|
116
|
+
new scale and prove nothing. See test 1.
|
|
117
|
+
- The two new tokens have **no call site yet** in this repo. That is intended — `HRAM-CHT-5` adopts
|
|
118
|
+
them — but it means nothing here exercises them beyond the resolver test.
|
|
119
|
+
|
|
120
|
+
### Tests to write
|
|
121
|
+
|
|
122
|
+
1. `resolveChartHeightPx` returns **320 / 400 / 480 / 560 / 640** for the five tokens — asserting the
|
|
123
|
+
pixel values, not just that a number comes back. Written so it fails against the current scale.
|
|
124
|
+
2. An unknown size still throws, and the message names **all five** valid tokens.
|
|
125
|
+
3. The `height` escape still wins over `size`, for a new token as well as an old one.
|
|
126
|
+
4. Existing chart-layout tests that hard-code 240/320/400 are updated deliberately, each one seen
|
|
127
|
+
and changed rather than mass-replaced — one of them may be asserting the old value *on purpose*.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Part B — Implementation map
|
|
132
|
+
|
|
133
|
+
`.claude/models.local.json` has no local override, so the default applies: `implementation.runtime`
|
|
134
|
+
is `claude`/`sonnet` — the Orchestrator implemented directly, no Codex dispatch, no preamble/progress
|
|
135
|
+
contract needed.
|
|
136
|
+
|
|
137
|
+
Context package used:
|
|
138
|
+
|
|
139
|
+
- `src/components/charts/chartDefaults.js:294-300` (the pin comment + `CHART_SIZE_SPACING_UNITS`)
|
|
140
|
+
and `:402-419` (`resolveChartHeightPx`, its error message, and the `theme.spacing` unwrap comment).
|
|
141
|
+
- Enumeration sites confirmed by grep for the literal `"tall"` (matched the WO's own 2026-08-24
|
|
142
|
+
list exactly, nothing extra found): `LineChart.jsx:44,72`, `BarChart.jsx:27,54`,
|
|
143
|
+
`ScatterChart.jsx:155,198`, `docs/CHART-LAYOUT.md:66,73-75`.
|
|
144
|
+
- Existing hardcoded-value tests located via grep for `240|320|400` across `tests/*.test.{js,jsx}`:
|
|
145
|
+
`chartDefaults.test.js:119,562,576`, `BarChart.test.jsx:36,83-85`, `LineChart.test.jsx:51-53`,
|
|
146
|
+
`ScatterChart.test.jsx:242-244`. Distinguished from unrelated same-valued `minHeight`/`ChartFrame`
|
|
147
|
+
test fixtures (`chartDefaults.test.js:291,298,308,315,318`, `ChartFrame.test.jsx:294,311`,
|
|
148
|
+
`chart-api-census.test.js:196`), which were left untouched — they assert an arbitrary prop value,
|
|
149
|
+
not a size-token pixel height.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Part C — Orchestrator only
|
|
154
|
+
|
|
155
|
+
> **STOP — everything below addresses the Orchestrator.**
|
|
156
|
+
|
|
157
|
+
**Tier 3 · tests: the four resolver cases plus the updated layout tests**
|
|
158
|
+
|
|
159
|
+
Shared-core — a change *inside* `ui-core-micha` is Tier 3 by the tiering table regardless of diff
|
|
160
|
+
size, and this one redefines rendered output for every consumer that reaches 3.x.
|
|
161
|
+
|
|
162
|
+
- `reviewer` — the enumeration completeness and the test's ability to fail are its brief.
|
|
163
|
+
- `ui_reviewer` — the scale as a design decision: does the 10-unit step still read as a coherent
|
|
164
|
+
scale with five members, and is `super_tall` distinguishable from `extra_tall` in practice.
|
|
165
|
+
- `sec_reviewer` — not run. No auth, no data, no exposure surface.
|
|
166
|
+
- Concurrent, full context.
|
|
167
|
+
|
|
168
|
+
### Release
|
|
169
|
+
|
|
170
|
+
Minor bump per the operator's decision → **3.2.0** from the current 3.1.1. Follow this repo's release
|
|
171
|
+
procedure. The CHANGELOG obligation in Part A is a release gate, not a nicety: if the entry does not
|
|
172
|
+
lead with "every existing chart becomes 80 px taller", the release is not ready.
|
|
173
|
+
|
|
174
|
+
Do **not** bump hram from here. `HRAM-CHT-5` owns that, and it carries the visual verification.
|
|
175
|
+
|
|
176
|
+
### Register
|
|
177
|
+
|
|
178
|
+
Row `UCM-CHART-15`, review notes in the required shape, the published version, landing SHA(s) in a
|
|
179
|
+
second commit before the push. Record explicitly that a **major was recommended and minor was chosen
|
|
180
|
+
by the operator** — a future reader tracing a silent redraw should find that decision, not infer it.
|
|
181
|
+
|
|
182
|
+
### Log
|
|
183
|
+
|
|
184
|
+
- **2026-08-24** — Envelope authored. Operator judged all three existing tokens too cramped and chose
|
|
185
|
+
a full one-step shift over adding a token at the top, accepting the breaking redraw; added
|
|
186
|
+
`super_tall` to the two steps proposed. Version set to minor against the Expertenchat's
|
|
187
|
+
recommendation of major, recorded above with its consequence.
|