@micha.bigler/ui-core-micha 2.42.3 → 3.0.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.
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React from 'react';
2
2
  import { Box } from '@mui/material';
3
3
  import { useTheme } from '@mui/material/styles';
4
4
  import { useTranslation } from 'react-i18next';
@@ -6,15 +6,12 @@ import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
6
6
  import { useNeutralChartPalette } from './palette';
7
7
  import {
8
8
  DEFAULT_LEGEND_POSITION,
9
+ assertRemovedChartProp,
9
10
  defaultNumericTickFormatter,
10
- resolveChartHeight,
11
- sizeYAxisForContent,
12
- spaceForRotatedTicks,
13
- warnOnHeightMismatch,
11
+ resolveChartLayout,
14
12
  withAxisDefaults,
15
13
  withChartSlotDefaults,
16
14
  withGridDefaults,
17
- withMarginDefaults,
18
15
  } from './chartDefaults';
19
16
 
20
17
  /**
@@ -27,10 +24,9 @@ import {
27
24
  * a label the caller DID set (`withAxisDefaults` only ever falls back onto
28
25
  * an axis without its own `label`).
29
26
  *
30
- * `minHeight` (CHART-8): a floor under an `aspect`-derived responsive height when `aspect` is
31
- * set and `height` is not; otherwise (no `height`, no `aspect`) it sizes the chart itself. Once
32
- * `height` is set, `height` sizes the chart and the wrapper never reserves more than that, even
33
- * if `minHeight` is also passed and larger.
27
+ * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
28
+ * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
29
+ * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
34
30
  */
35
31
  export function BarChart({
36
32
  series = [],
@@ -39,13 +35,15 @@ export function BarChart({
39
35
  xAxis,
40
36
  yAxis,
41
37
  palette,
42
- minHeight,
38
+ size = 'standard',
43
39
  height,
40
+ minHeight,
44
41
  aspect,
42
+ margin,
43
+ xLabels = 'auto',
45
44
  grid,
46
45
  hideLegend = series.length <= 1,
47
46
  legendPosition = DEFAULT_LEGEND_POSITION,
48
- margin,
49
47
  slotProps,
50
48
  ...chartProps
51
49
  }) {
@@ -53,8 +51,9 @@ export function BarChart({
53
51
  const { i18n } = useTranslation();
54
52
  const neutralPalette = useNeutralChartPalette();
55
53
 
56
- useEffect(() => warnOnHeightMismatch('BarChart', { minHeight, height }), [minHeight, height]);
57
- const { wrapperMinHeight, chartHeight } = resolveChartHeight({ minHeight, height, aspect });
54
+ assertRemovedChartProp('BarChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
55
+ assertRemovedChartProp('BarChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
56
+ assertRemovedChartProp('BarChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
58
57
 
59
58
  const labelledXAxis = withAxisDefaults(
60
59
  xAxis,
@@ -68,29 +67,33 @@ export function BarChart({
68
67
  { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) },
69
68
  theme.typography.caption.fontSize,
70
69
  );
71
- const sizedYAxis = sizeYAxisForContent(
72
- labelledYAxis,
70
+ const layout = resolveChartLayout({
71
+ size,
72
+ height,
73
+ xAxis: labelledXAxis,
74
+ yAxis: labelledYAxis,
73
75
  series,
74
- theme.typography.caption.fontSize,
75
- );
76
- const rotatedTickSpace = spaceForRotatedTicks(
77
- labelledXAxis,
78
- margin,
79
- theme.typography.caption.lineHeight,
80
- );
76
+ xLabels,
77
+ hideLegend,
78
+ legendPosition,
79
+ slotProps,
80
+ tickFontSize: theme.typography.caption.fontSize,
81
+ spacing: theme.spacing,
82
+ defaultLineHeight: theme.typography.caption.lineHeight,
83
+ });
81
84
 
82
85
  return (
83
- <Box data-testid="bar-chart-container" sx={{ width: '100%', minHeight: wrapperMinHeight, aspectRatio: aspect }}>
86
+ <Box data-testid="bar-chart-container" sx={layout.sx}>
84
87
  <MuiBarChart
85
88
  {...chartProps}
86
- height={chartHeight}
89
+ height={layout.chartHeight}
87
90
  series={series}
88
- xAxis={rotatedTickSpace.xAxis}
89
- yAxis={sizedYAxis}
91
+ xAxis={layout.xAxis}
92
+ yAxis={layout.yAxis}
90
93
  colors={palette || neutralPalette.categorical}
91
94
  grid={withGridDefaults(grid)}
92
95
  hideLegend={hideLegend}
93
- margin={withMarginDefaults(rotatedTickSpace.margin)}
96
+ margin={layout.margin}
94
97
  slotProps={withChartSlotDefaults(slotProps, legendPosition)}
95
98
  />
96
99
  </Box>
@@ -11,19 +11,38 @@ import {
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { useTheme } from '@mui/material/styles';
13
13
  import { exportChartPng, exportChartSvg } from './exportChart';
14
- import { warnOnHeightMismatch } from './chartDefaults';
15
14
 
16
15
  export const CHART_FRAME_ROOT_SX = { p: 2 };
17
16
  export const CHART_FRAME_ALERT_SX = { mt: 2 };
18
17
 
18
+ /**
19
+ * UCM-CHART-12: `warnOnHeightMismatch` (the shared helper this note used to call into
20
+ * `chartDefaults`) was deleted along with `resolveChartHeight` -- both belonged to the
21
+ * `{minHeight, height, aspect}` trio the four chart presets used to accept, which that WO
22
+ * replaced with `resolveChartLayout`'s `size`/`height` model. `ChartFrame` keeps its OWN
23
+ * `minHeight`/`height`/`aspect` props unchanged (CHART-9: it is a card floor, not a chart size),
24
+ * so it keeps this note too -- same text, same condition, just no longer routed through the
25
+ * now-deleted shared function.
26
+ */
27
+ function warnOnChartFrameHeightMismatch(minHeight, height) {
28
+ if (process.env.NODE_ENV === 'production') return;
29
+ if (minHeight == null || height == null || minHeight === height) return;
30
+ // eslint-disable-next-line no-console
31
+ console.warn(
32
+ `[ui-core-micha] <ChartFrame> received both minHeight={${minHeight}} and height={${height}}; `
33
+ + 'they disagree; height is ignored here and minHeight keeps sizing the wrapper as a floor. '
34
+ + 'Pass only minHeight, or drop height if it was meant to size the whole card.',
35
+ );
36
+ }
37
+
19
38
  /**
20
39
  * Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
21
40
  *
22
41
  * `minHeight` (CHART-9): always just a floor on the content box, which holds a whole card --
23
42
  * title, toolbar, chart, legend, footnotes, export links -- not a single chart. `height` is never
24
- * applied to the box (unlike the four chart presets, whose box holds exactly one chart and do use
25
- * `resolveChartHeight`'s `height`/`aspect` resolution); it is only compared against `minHeight` for
26
- * `warnOnHeightMismatch`'s dev-mode note.
43
+ * applied to the box (unlike the four chart presets, whose box holds exactly one chart and use
44
+ * `resolveChartLayout`'s `size`/`height` resolution); it is only compared against `minHeight` for
45
+ * this component's own dev-mode mismatch note.
27
46
  */
28
47
  export function ChartFrame({
29
48
  title,
@@ -60,7 +79,7 @@ export function ChartFrame({
60
79
  const callerSx = Array.isArray(sx) ? sx : [sx];
61
80
 
62
81
  useEffect(
63
- () => warnOnHeightMismatch('ChartFrame', { minHeight, height }, { heightWins: false }),
82
+ () => warnOnChartFrameHeightMismatch(minHeight, height),
64
83
  [minHeight, height],
65
84
  );
66
85
 
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React from 'react';
2
2
  import { Box } from '@mui/material';
3
3
  import { useTheme } from '@mui/material/styles';
4
4
  import { useTranslation } from 'react-i18next';
@@ -9,15 +9,12 @@ import {
9
9
  import { useNeutralChartPalette } from './palette';
10
10
  import {
11
11
  DEFAULT_LEGEND_POSITION,
12
+ assertRemovedChartProp,
12
13
  defaultNumericTickFormatter,
13
- resolveChartHeight,
14
- sizeYAxisForContent,
15
- spaceForRotatedTicks,
16
- warnOnHeightMismatch,
14
+ resolveChartLayout,
17
15
  withAxisDefaults,
18
16
  withChartSlotDefaults,
19
17
  withGridDefaults,
20
- withMarginDefaults,
21
18
  } from './chartDefaults';
22
19
 
23
20
  // Fixes MUI's hollow `background.paper`-filled marker (MarkElement.js's own
@@ -44,10 +41,9 @@ function FilledMarkElement({ color, style, ...props }) {
44
41
  * (`withAxisDefaults` only ever falls back onto an axis without its own
45
42
  * `label`).
46
43
  *
47
- * `minHeight` (CHART-8): a floor under an `aspect`-derived responsive height when `aspect` is
48
- * set and `height` is not; otherwise (no `height`, no `aspect`) it sizes the chart itself. Once
49
- * `height` is set, `height` sizes the chart and the wrapper never reserves more than that, even
50
- * if `minHeight` is also passed and larger.
44
+ * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
45
+ * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
46
+ * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
51
47
  */
52
48
  export function LineChart({
53
49
  series = [],
@@ -56,13 +52,15 @@ export function LineChart({
56
52
  xAxis,
57
53
  yAxis,
58
54
  palette,
59
- minHeight,
55
+ size = 'standard',
60
56
  height,
57
+ minHeight,
61
58
  aspect,
59
+ margin,
60
+ xLabels = 'auto',
62
61
  grid,
63
62
  hideLegend = series.length <= 1,
64
63
  legendPosition = DEFAULT_LEGEND_POSITION,
65
- margin,
66
64
  slots,
67
65
  slotProps,
68
66
  ...chartProps
@@ -71,8 +69,9 @@ export function LineChart({
71
69
  const { i18n } = useTranslation();
72
70
  const neutralPalette = useNeutralChartPalette();
73
71
 
74
- useEffect(() => warnOnHeightMismatch('LineChart', { minHeight, height }), [minHeight, height]);
75
- const { wrapperMinHeight, chartHeight } = resolveChartHeight({ minHeight, height, aspect });
72
+ assertRemovedChartProp('LineChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
73
+ assertRemovedChartProp('LineChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
74
+ assertRemovedChartProp('LineChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
76
75
 
77
76
  const labelledXAxis = withAxisDefaults(
78
77
  xAxis,
@@ -91,29 +90,33 @@ export function LineChart({
91
90
  labelMarkType: 'square',
92
91
  ...item,
93
92
  }));
94
- const sizedYAxis = sizeYAxisForContent(
95
- labelledYAxis,
96
- defaultedSeries,
97
- theme.typography.caption.fontSize,
98
- );
99
- const rotatedTickSpace = spaceForRotatedTicks(
100
- labelledXAxis,
101
- margin,
102
- theme.typography.caption.lineHeight,
103
- );
93
+ const layout = resolveChartLayout({
94
+ size,
95
+ height,
96
+ xAxis: labelledXAxis,
97
+ yAxis: labelledYAxis,
98
+ series: defaultedSeries,
99
+ xLabels,
100
+ hideLegend,
101
+ legendPosition,
102
+ slotProps,
103
+ tickFontSize: theme.typography.caption.fontSize,
104
+ spacing: theme.spacing,
105
+ defaultLineHeight: theme.typography.caption.lineHeight,
106
+ });
104
107
 
105
108
  return (
106
- <Box data-testid="line-chart-container" sx={{ width: '100%', minHeight: wrapperMinHeight, aspectRatio: aspect }}>
109
+ <Box data-testid="line-chart-container" sx={layout.sx}>
107
110
  <MuiLineChart
108
111
  {...chartProps}
109
- height={chartHeight}
112
+ height={layout.chartHeight}
110
113
  series={defaultedSeries}
111
- xAxis={rotatedTickSpace.xAxis}
112
- yAxis={sizedYAxis}
114
+ xAxis={layout.xAxis}
115
+ yAxis={layout.yAxis}
113
116
  colors={palette || neutralPalette.categorical}
114
117
  grid={withGridDefaults(grid)}
115
118
  hideLegend={hideLegend}
116
- margin={withMarginDefaults(rotatedTickSpace.margin)}
119
+ margin={layout.margin}
117
120
  slots={{ mark: FilledMarkElement, ...slots }}
118
121
  slotProps={withChartSlotDefaults(slotProps, legendPosition)}
119
122
  />
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { Box } from '@mui/material';
3
3
  import { useTheme } from '@mui/material/styles';
4
4
  import { useTranslation } from 'react-i18next';
@@ -7,10 +7,9 @@ import { useXScale, useYScale } from '@mui/x-charts/hooks';
7
7
  import { useNeutralChartPalette } from './palette';
8
8
  import {
9
9
  DEFAULT_LEGEND_POSITION,
10
+ assertRemovedChartProp,
10
11
  defaultNumericTickFormatter,
11
- resolveChartHeight,
12
- sizeYAxisForContent,
13
- warnOnHeightMismatch,
12
+ resolveChartLayout,
14
13
  withAxisDefaults,
15
14
  withChartSlotDefaults,
16
15
  withGridDefaults,
@@ -153,10 +152,9 @@ export function ScatterReferenceLine({
153
152
  * Reference geometry (a computed curve, a straight line, either with an optional label) is
154
153
  * composed as `children` using `ScatterReferenceCurve`/`ScatterReferenceLine` above.
155
154
  *
156
- * `minHeight` (CHART-8): a floor under an `aspect`-derived responsive height when `aspect` is
157
- * set and `height` is not; otherwise (no `height`, no `aspect`) it sizes the chart itself. Once
158
- * `height` is set, `height` sizes the chart and the wrapper never reserves more than that, even
159
- * if `minHeight` is also passed and larger.
155
+ * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
156
+ * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
157
+ * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
160
158
  *
161
159
  * Tested magnitude: a few hundred marks (hram's largest cloud is ~300 points). Not verified,
162
160
  * and not implied to hold, at thousands+.
@@ -177,13 +175,15 @@ export function ScatterChart({
177
175
  xAxis,
178
176
  yAxis,
179
177
  palette,
180
- minHeight,
178
+ size = 'standard',
181
179
  height,
180
+ minHeight,
182
181
  aspect,
182
+ margin,
183
+ xLabels = 'auto',
183
184
  grid,
184
185
  hideLegend = series.length <= 1,
185
186
  legendPosition = DEFAULT_LEGEND_POSITION,
186
- margin,
187
187
  sizeAccessor,
188
188
  getPointStyle,
189
189
  markerSize = DEFAULT_MARKER_RADIUS,
@@ -195,8 +195,9 @@ export function ScatterChart({
195
195
  const { i18n } = useTranslation();
196
196
  const neutralPalette = useNeutralChartPalette();
197
197
 
198
- useEffect(() => warnOnHeightMismatch('ScatterChart', { minHeight, height }), [minHeight, height]);
199
- const { wrapperMinHeight, chartHeight } = resolveChartHeight({ minHeight, height, aspect });
198
+ assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
199
+ assertRemovedChartProp('ScatterChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
200
+ assertRemovedChartProp('ScatterChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
200
201
 
201
202
  const labelledXAxis = withAxisDefaults(
202
203
  xAxis,
@@ -210,22 +211,30 @@ export function ScatterChart({
210
211
  { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) },
211
212
  theme.typography.caption.fontSize,
212
213
  );
213
- // Only the Y axis is width-sized (sizeYAxisForContent reserves horizontal space for a
214
- // vertical axis's ticks+label) -- a scatter's numeric X axis needs no analogous treatment
215
- // beyond what MUI already sizes for a horizontal tick row.
216
- //
217
- // sizeYAxisForContent's series-data fallback (used when the caller sets no axis min/max)
218
- // expects `series[i].data` to be a plain array of numbers -- true for BarChart/LineChart, but
219
- // a scatter's data is `{x,y,z,id}` points, so `typeof value === 'number'` never matched and
220
- // the fallback silently found zero candidates (reviewer finding, live-caught: it "looked fine"
221
- // only because the untested case happened to fall back to MUI's flat default at a magnitude
222
- // that didn't visibly overlap). Adapt AT THIS CALL SITE, not inside the shared helper --
223
- // BarChart/LineChart's own contract must not change for this.
214
+ // `resolveChartLayout`'s own y-axis sizing (via `sizeYAxisForContent`) expects `series[i].data`
215
+ // to be a plain array of numbers -- true for BarChart/LineChart, but a scatter's data is
216
+ // `{x,y,z,id}` points. Adapt AT THIS CALL SITE, not inside the shared resolver -- BarChart/
217
+ // LineChart's own contract must not change for this (reviewer finding, live-caught under
218
+ // THEME-10: the untested case silently found zero candidates and fell back to MUI's flat
219
+ // default at a magnitude that didn't visibly overlap).
224
220
  const yValueSeries = useMemo(
225
221
  () => series.map((item) => ({ ...item, data: (item.data || []).map((point) => point?.y) })),
226
222
  [series],
227
223
  );
228
- const sizedYAxis = sizeYAxisForContent(labelledYAxis, yValueSeries, theme.typography.caption.fontSize);
224
+ const layout = resolveChartLayout({
225
+ size,
226
+ height,
227
+ xAxis: labelledXAxis,
228
+ yAxis: labelledYAxis,
229
+ series: yValueSeries,
230
+ xLabels,
231
+ hideLegend,
232
+ legendPosition,
233
+ slotProps,
234
+ tickFontSize: theme.typography.caption.fontSize,
235
+ spacing: theme.spacing,
236
+ defaultLineHeight: theme.typography.caption.lineHeight,
237
+ });
229
238
 
230
239
  const isNeutral = series.length <= 1 && !series[0]?.color;
231
240
  const resolvedPalette = palette || (isNeutral ? [neutralPalette.neutral] : neutralPalette.categorical);
@@ -268,17 +277,17 @@ export function ScatterChart({
268
277
  );
269
278
 
270
279
  return (
271
- <Box data-testid="scatter-chart-container" sx={{ width: '100%', minHeight: wrapperMinHeight, aspectRatio: aspect }}>
280
+ <Box data-testid="scatter-chart-container" sx={layout.sx}>
272
281
  <MuiScatterChart
273
282
  {...chartProps}
274
- height={chartHeight}
283
+ height={layout.chartHeight}
275
284
  series={orderedSeries.map((item) => ({ markerSize, ...item }))}
276
- xAxis={labelledXAxis}
277
- yAxis={sizedYAxis}
285
+ xAxis={layout.xAxis}
286
+ yAxis={layout.yAxis}
278
287
  colors={resolvedPalette}
279
288
  grid={withGridDefaults(grid)}
280
289
  hideLegend={hideLegend}
281
- margin={margin}
290
+ margin={layout.margin}
282
291
  slots={{ marker: markerSlot }}
283
292
  slotProps={withChartSlotDefaults(slotProps, legendPosition)}
284
293
  >
@@ -30,6 +30,11 @@ import { useNeutralChartPalette } from './palette';
30
30
  // zero-height chart in the first place (CHART-4). Do not "fix" this back
31
31
  // to a responsive/aspect-ratio height without re-verifying the underlying
32
32
  // MUI measurement bug is actually resolved.
33
+ //
34
+ // UCM-CHART-12: `CHART_HEIGHT` (320) is the number `"standard"` (BarChart's new `size` token) was
35
+ // deliberately pinned to, specifically so this preset's own default did not silently move under
36
+ // this migration -- still used here as ChartFrame's own `minHeight` floor (a card concern,
37
+ // unaffected by CHART-12), and the inner BarChart now takes `size="standard"` instead of `height`.
33
38
  const CHART_HEIGHT = 320;
34
39
 
35
40
  // Dynamic sx factories are outside the registry's documented top-level-object
@@ -283,7 +288,7 @@ export function TimeSeriesChart({
283
288
  ...(hasSecondaryAxis && series.axis === 'secondary' ? { yAxisId: 'secondary' } : {}),
284
289
  }))}
285
290
  palette={visiblePalette}
286
- height={CHART_HEIGHT}
291
+ size="standard"
287
292
  hideLegend={seriesConfig.length > 0}
288
293
  skipAnimation={skipAnimation}
289
294
  />