@micha.bigler/ui-core-micha 2.39.0 → 2.40.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/DESIGN.md CHANGED
@@ -21,6 +21,7 @@ These principles are the spec a **prototype** encodes and the implementation mat
21
21
 
22
22
  8. **Every chart earns:** axis labels (with units), a legend only when >1 series, a tooltip, theme-token colours, container-responsive sizing, and empty/loading states. **Single-hue for magnitude; diverging (blue/red around zero) ONLY for signed data.** The concrete data palette is a separate layer with a shared `ui-core-micha` default that apps override when their domain calls for it.
23
23
  **Dual-axis is a default-off exception, opt-in only** (CHART-5, operator decision): the shared `TimeSeriesChart` preset supports a second y-axis via a per-series `axis: 'secondary'` field specifically for two series sharing one axis at very different scales/units (e.g. a user count vs. hours) where series-toggles alone leave one series unreadable. Reach for it only under that same condition, not as a general layout choice — a single shared axis (or toggles) stays the default everywhere else.
24
+ 8a. **Axis titles: categorical axis gets none by default; numeric axis carries the quantity and unit, once.** A categorical axis' ticks are names (`"Session 1"`, ward names) — a title only restates their type; add one only when the ticks are codes or abbreviations whose kind is not self-evident. A numeric axis is meaningless without a title naming the quantity and its unit — unless the ticks already show the unit (`%`, a currency symbol) and the panel heading already names the quantity, in which case a title repeats both. **The unit belongs in the title once, never in every tick** (`"Accessibility (minutes) (min)"` over ticks `"0.0 min"` was measured live — the unit three times on one axis). This is a decision the caller makes per axis, not something the chart preset silently applies or suppresses (THEME-9) — a label the caller deliberately set is never auto-dropped.
24
25
  9. **Ground it in the real subject matter.** Faithful structures from the real panels (hram: Morris-tornado, Pareto/knee, CI-strip, allocation-scatter with shape-encoded sources) — no lorem, no generic charts. Hence: **scout the real subject before building.**
25
26
 
26
27
  ## Craft
package/WORK_ORDERS.md CHANGED
@@ -80,3 +80,6 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
80
80
  | AUTH-4 | The account user list on a phone | Gives `UserListComponent` a card layout below `md` — one card per user, email as the title, the two boolean columns as labelled chips, role select and delete in a divided footer, plus a sort control that replaces the column headers the table loses. Above `md` the table is byte-identical. Prototype: [assets/AUTH-4-user-list-card.html](work-orders/assets/AUTH-4-user-list-card.html). Full spec: [work-orders/AUTH-4.md](work-orders/AUTH-4.md). | 2026-08-11 | planned | | Split out of DS-11 by operator decision; fix class chosen by operator (card list, over sticky-first-column and priority-hide). Envelope only (Approval Gate #1 pending). **Authored and parked by operator decision 2026-08-11 — not queued.** It is built on demand, not as SHELL-5's successor; a fresh session should not dispatch it without the operator saying so. When it does run, it runs **after** `SHELL-5` (same screen, different half). The point of this row is that the measurements above are already done, so nobody re-measures. Prototype in scope, so the two-width rendered gate applies. **Measured 2026-08-11 in cockpit** at `innerWidth` 411 (the mobile preset gives 411, not 375): table **1053px inside a 325px container**, container `overflow-x: auto` and **the document does not overflow** — so it is not a broken layout but a desktop table on a phone, with Role and Actions, the two things the screen exists for, entirely outside the view. **The cheap fix was tested and rejected:** 253px of the width is `New` (90) + `Successful Login` (163), both default-on props that `AccountPage` already forwards and cockpit simply left on — but the four remaining columns are 800px against 325px, so configuration cannot fix it. Column defaults deliberately NOT flipped: that would be a silent estate-wide change. SHELL-2's 1045px from 2026-08-08 still holds (+8px), so THEME-4/5 did not move the density. **Four prototype decisions are load-bearing:** email as title with a muted placeholder when the name is absent (verified: a 47-char address wraps to two lines in a 357px card without overflow); the icon-only flags become labelled chips **because a Tooltip is unreachable on a touch pointer**, so that data is effectively absent today; role and delete in a footer behind a divider with the full 44px target; and a **sort control**, because a card list has no column headers — without it the mobile view silently loses a desktop capability. `extraColumns` render as label/value rows with **no new required field**; `extraRowActions` beyond three must collapse into an overflow menu. Reached via `AccountPage`, which **at least ten apps mount**, and ucm has no staging net. |
81
81
  | SHELL-6 | A controlled `SectionNav` with no way to open must not render a trigger | In `mode="mobile"` the built-in trigger renders unconditionally (`SectionNav.jsx:246`), while `handleTriggerClick` calls `onOpen?.()` — so a consumer driving `open` itself without `onOpen` gets a button that is visible, focusable and does nothing. Render the trigger only when a click can open the drawer: uncontrolled, or controlled **and** `onOpen` supplied. Plus the component's **render matrix** written into its doc comment. Full spec: [work-orders/SHELL-6.md](work-orders/SHELL-6.md). | 2026-08-11 | done | f119b70 | **Unblocks `jg-ferien/NAV-37`** (`blocked`, `c627d7b`), same shape as `SHELL-3`→`SHELL-4`. **Operator chose the semantics fix over a `showTrigger` prop** — a prop would leave the dead button as the default for every controlled consumer who has not heard of it, turning a defect into a configuration option; a focusable control that performs no action is wrong on its own terms. Fix: `const canOpenTrigger = !isControlled || Boolean(onOpen)`, wrapping only the `<ButtonBase>` trigger — `children` and `<Drawer>` keep rendering unconditionally in every case. Render-matrix doc comment added above the component per the WO's own framing ("not optional documentation; it is the artefact whose absence caused the defect"). `AccountPage`, the only shipped consumer, is uncontrolled and renders identically. Version `2.37.0` → `2.37.1` (patch, defect fix, no API change). **Codex left the diff uncommitted and did not touch this file, as instructed** — fourth consecutive WO where the explicit warning held. **Required test 1's non-vacuity confirmed**: Codex's own landing note states the new regression test was run against the pre-fix code and failed (dead trigger still in the DOM), then passed after the one-line fix — the Orchestrator did not re-verify this specific claim by checking out the prior commit, but both independent reviewers separately read the old-vs-new JSX and found the claim plausible/consistent. **Orchestrator's own verification:** scoped tests 47/47 green, `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory) ran concurrently — **both came back clean, no findings, no fix required.** `reviewer` confirmed the condition is keyed correctly (not on `open`'s value, avoiding the trap the WO's own test 3 exists to catch), `children`/`Drawer` stay unconditional, desktop path untouched, and the render-matrix comment matches the Envelope's table exactly. `ui_reviewer` confirmed the trigger's removal is genuine (conditional JSX unmount, not a CSS-hidden trick left in the accessibility tree/tab order) and no restyle occurred as a side effect; noted two non-blocking residual observations, not fixed: a hypothetical layout-shift if a consumer dynamically toggled `onOpen` on/off at runtime (no current or planned consumer does this), and `resolvedEyebrow`/`resolvedTitle` computed unconditionally even when unused (harmless). No rendered gate needed per the WO's own reasoning — the change is a DOM absence fact, not an appearance, and the test suite is the correct instrument. |
82
82
  | THEME-6 | An app-side check for `sx` values the theme already gives | `THEME-4` made shadowing mechanical for the kit's own components; apps have no equivalent — cockpit alone carries **464 `sx` props across 37 files** (measured 2026-08-11). Adds an exported, source-level, **report-only** check an app calls from its own suite, plus the cockpit count. Full spec: [work-orders/THEME-6.md](work-orders/THEME-6.md). | 2026-08-11 | done | 637d262 | DS-18's app-side half. **Codex → Claude fallback**: Codex ran out of workspace credits mid-task (first-hand failure this session, stamped today) — the Orchestrator implemented directly. Author-flip rule applied: independent `reviewer` (Sonnet, mandatory regardless of tier) ran against the Orchestrator's own diff; `ui_reviewer` correctly not spawned, per the WO's own text, since nothing renders. New export `reportRedundantThemeValues(sources, { theme })` in `src/theme/themeCompleteness.js`, reusing existing primitives (`getPath`, `sameValue`, `jsxOpeningTag`, `topLevelSxValue`, `BASELINE_STYLED_MUI_COMPONENTS`, `expandShorthandProperty`) rather than rebuilding them. Attribution by JSX tag name only (root-slot styleOverrides only), literal string/number sx values only — no template literals, identifiers, spreads, or `component={...}` resolution, the bound written open per the WO's instruction. **A real, verified discrepancy in the Envelope's own grounding, corrected here rather than silently chased:** the cited cockpit examples (`borderColor: 'divider'` ×2, `bgcolor: 'background.paper'` ×3, `borderRadius: 1` ×3) all live on `<Box component="button">` in `cockpit/frontend/src/cockpit/ChatView.jsx:75,82,95` — confirmed by direct inspection — which the check's own stated tag-name attribution method cannot catch (`Box` isn't a themed tag, and `component={...}` resolution is explicitly out of scope). The check was NOT widened to chase these numbers. **Run against cockpit's real source** (`cockpit/frontend/src`, 50 `.jsx` files, `theme.js` excluded from the sources passed per the WO's app-side-exclusion instruction — `reportOffPaletteColours`'s own skip pattern left untouched): **27 findings, all `MuiButton.textTransform`, across 4 files** (`BoardView.jsx`, `KnowledgeView.jsx`, `SettingsView.jsx`, one more) — cockpit's buttons redundantly set `sx={{ textTransform: 'none' }}`, matching the baseline's own `MuiButton.styleOverrides.root.textTransform`. This is the honest, real result, reported as a correction to the Envelope's stale grounding, not a match to it. Independent `reviewer` found one real **P2, fixed**: numeric sx values for `borderRadius` and spacing-shorthand aliases were compared raw against the theme's already-resolved CSS value, but MUI actually multiplies a bare number by `theme.shape.borderRadius`/`theme.spacing()` before rendering (e.g. `borderRadius: 8` in sx renders as `8 × shape.borderRadius`, not `8`) — comparing the two raw numbers could produce a **false positive**, exactly the failure mode the WO's own Risks section names as the one to avoid. Fixed by skipping numeric-value comparison entirely for `borderRadius` and every spacing alias (`MUI_NUMERIC_SCALE_PROPERTIES`), erring toward a false negative (the accepted lower bound) rather than a false positive; two regression tests added (the specific false-positive case, plus a corrected version of the "component the theme doesn't style" test, which had used an off-list tag rather than exercising the intended undefined-styleOverride branch — also a reviewer note, fixed). Re-verified: cockpit's 27/27 findings unchanged by the fix (all string-valued `textTransform`, not numeric). Scoped tests 37/37 green, `tsc` clean. Version `2.37.1` → `2.38.0` (minor, new exported API). No rendered gate — static/logic change, no fix shipped anywhere (report-only, per the WO's explicit non-goal). |
83
+ | THEME-7 | Guidance: a series slot that collides with an app's own accent | The baseline ships six `dataSeries.categorical` colours and no guidance for the case where a slot sits too close to something the consuming app owns. Measured case, not hypothesis (hram `IMARA-2`, frozen 2026-08-13, Lab ΔE calibrated against that operator's own rejected pair at ΔE 16.6): series 1 `#3D5A99` vs hram's primary `#2F4F96` = **ΔE 7.2**, and series 3 `#2E8F8A` vs hram's DALYs identity `#1F7A72` = **ΔE 8.3** (same hue, one lightness step). hram resolved both app-side by dropping slot 1 and replacing slot 3 with a navy. This WO adds the missing guidance to `DESIGN.md` and the token docs — how to check an accent against the palette, and why a colliding slot should be DROPPED rather than nudged — plus an explicit decide-and-record on whether to ship anything beyond guidance (ΔE helper / documented extension sequence / nothing). **Explicitly not a defect report: the six shipped values stay unchanged**, since they are correct for any app without a muted-indigo accent or a teal domain identity. Full order [`work-orders/THEME-7.md`](work-orders/THEME-7.md) | 2026-08-13 | planned | | Tier 3 (shared-core surface) though documentation-led; independent `reviewer` mandatory. Raised by the hram/Imara design-language work at the operator's explicit instruction to take the finding upstream rather than keep it app-local. Stated limitation to keep in the text: the guidance derives from ONE app's experience and its threshold is calibrated on one operator's judgement — a starting point, not a standard. |
84
+ | THEME-8 | Touch-Ziel des Selects sitzt am falschen Element und drückt den Text aus der Mitte | `src/theme/tokens.js` setzt die Touch-Mindesthöhe (40, im `@media (any-pointer: coarse)`-Zweig 44) auf `MuiSelect.styleOverrides.**select**` — also auf das INNERE Element, das MUIs variantenabhängiges Padding trägt. Beim `standard`-Select ist dieses Padding oben-verankert (`1px 24px 5px 0`), der Zusatzraum landet komplett unten, der Text reitet oben. **Von den sechs Komponenten mit Touch-Ziel (`MuiButton`, `MuiIconButton`, `MuiSelect`, `MuiCheckbox`, `MuiFormControlLabel`, `MuiTextField`) ist `MuiSelect` die EINZIGE, die einen Nicht-Root-Slot adressiert** — eine Inkonsistenz in der Baseline selbst, keine Designentscheidung. **Live gemessen** (hram, Administration → Users, Select in einer Tabellenzelle): heute 0 px über / 0 px unter dem Text (Inneres auf 44 px gestreckt); nach Verschieben der Regel auf die Wurzel **9 px / 9 px** — exakt zentriert, weil `MuiInputBase-root` bereits `display:flex; align-items:center` ist. Voller Auftrag [`work-orders/THEME-8.md`](work-orders/THEME-8.md) | 2026-08-14 | done (npm publish; consumer pin bumps still open) | ae0745d | Tier 3 (Shared-Core). Codex out of credits (2026-08-14, `.claude/codex-status.md`), Orchestrator implemented directly — authorship flip made `reviewer` + `ui_reviewer` mandatory (already required at Tier 3): both ran, no blocking findings. `reviewer`: config-level test doesn't render/measure the DOM (P3, accepted — Envelope's own proof is the live measurement, already done), no version bump at review time (P2, resolved by 2.39.0→2.39.1 in the same commit). `ui_reviewer`: MuiSelect's coarse branch still hand-writes `{minHeight:44}` instead of the shared `coarseHitArea` helper (P3, pre-existing, not introduced here). Both independently traced MUI source (`Select.js`/`InputBase.js`) and confirmed the root-slot fix reaches the right DOM node and centres via the inherited flex layout. No live browser re-render done (Claude-Code preview-scoping override: small/medium frontend change, code reasoning + tests suffice). **Wert bleibt 40/44 — nur der Anker ist falsch.** Verstärkt auf Touch-Geräten: der `coarse`-Zweig greift auch auf Laptops mit Touchscreen (Operator-Rechner meldet `any-pointer: coarse`), dort 44 statt 40 px — gleicher Fehler, 4 px grösser. **Estate-weite Sichtveränderung**: alle konsumierenden Apps bekommen re-zentrierte Selects; in den Release Notes nennen. **Nicht am npm-Publish abschliessen** — die Pin-Bumps der Konsumenten (zuerst hram, wo es gefunden wurde) gehören dazu. Gefunden beim Messen einer vermeintlichen hram-Regression; die App-seitige Schwester ist hram FIX-11 (anderer Fehler, gleiche Felder). |
85
+ | THEME-9 | Diagramme geben mehr Platz an Achsen-Mobiliar als an Daten | **In einer konsumierenden App (hram) gemessen, nicht geschätzt.** Eine KPI-Karte „Coverage“: Karte 470 × 372 → Chart-Box 436 × **300** (`minHeight:300`, das SVG darin ist 270 hoch → **30 px tot**) → SVG 436 × 270 → **Zeichenfläche 301 × 175**. Die Zeichenfläche ist damit **45 % des SVG und 30 % der Karte**; innerhalb des SVG gehen 135 px Breite und 95 px Höhe verloren. Vom Kartenrand bis zur ersten Tick-Zahl sind es 104 px, davon **60 px blanker Raum zwischen SVG-Rand und Achsentitel**; unter dem Achsentitel liegen 30 px leer im SVG plus 32 px bis zur Karte. **Drei weitere gemessene Befunde:** (1) `withAxisDefaults` endet mit `label: axis.label || label` und setzt damit EIN `yAxisLabel` auf JEDE Achse — in hrams Doppelachsen-Chart tragen beide Y-Achsen dieselbe 43-Zeichen-Zeile, je 65 px eines 206-px-Charts. (2) Der Achsentitel wird IN das Tick-Band gesetzt: auf der Accessibility-Ansicht überlappen **4 von 4** Y-Ticks den Titel, dasselbe passiert mit um 45° gedrehten Ortsnamen über dem Titel „Ward“. (3) Ticks tragen Einheit und Zier-Nachkommastellen (`0.0 min`, `50.0 min`) — dieselbe Einheit steht nochmals doppelt im Titel (`Accessibility (minutes) (min)`); anderswo kürzt MUI die Ticks zu `0.0…`. `BarChart` setzt gar keinen Standard-Formatter, `TimeSeriesChart` schon — also löst es jede App neu. Voller Auftrag [`work-orders/THEME-9.md`](work-orders/THEME-9.md) | 2026-08-14 | done (npm publish; consumer pin bumps still open) | 3fd6097 | Tier 3 (Shared-Core). Codex bereits als `unavailable` für heute vermerkt (`.claude/codex-status.md`, THEME-8) — direkt in Claude implementiert, Autorenschaft geflippt, `reviewer` + `ui_reviewer` liefen parallel (ohnehin Pflicht bei Tier 3). **`reviewer` R1 (P1, behoben):** die Serie-zu-Achse-Zuordnung in `sizeYAxisForContent` traf `undefined===undefined` statt MUI's echter Default-Regel (unmarkierte Serie → erste Achse im Array) — machte den Sizer zum stillen No-op genau auf der Primärachse des Doppelachsen-Falls, der diesen WO motiviert hat; Regressionstest ergänzt. **`ui_reviewer` U1 (P1, behoben):** `DESIGN.md` #8a versprach "keine Titel-Pflicht bei kategorialer Achse", aber `BarChart`/`LineChart` erzwangen beide Labels per `throw` — Labels jetzt optional, sonst wäre die Regel für FIX-13 unbrauchbar gewesen. **U3 (P2, behoben):** Sizer nutzte den Theme-Default-Font statt der per Achse gesetzten `tickLabelStyle.fontSize`. **U5 (P3, behoben):** Label-Dicke war an `theme.typography.body1` gekoppelt, obwohl MUI intern hart 14px setzt — jetzt eigene benannte Konstante. **U2 (P2, akzeptierter Rest):** Schätzung kennt MUI's "nice-tick"-Rundung nicht exakt; bestehender 1.1×-Sicherheitsfaktor federt kleine Abweichungen ab, der gerenderte Check ist der eigentliche Beweis. **U4:** Behauptung, `TimeSeriesChart` umgehe `withAxisDefaults`/`sizeYAxisForContent`, am Quellcode widerlegt (rendert über die eigene `<BarChart>`-Komponente). Affected-Set grün (60 Tests). **Live-Nachmessung in hram nicht möglich** — Browser-Pane liefert nur für den vom Operator angezeigten Tab reale Viewport-Masse (neue Tabs 0×0, negative-Width-SVG-Fehler als direkte Folge, kein Codefehler); die "vorher"-Zahlen der Coverage-Karte wurden LIVE aus dem korrekt angezeigten hram-Tab gemessen (siehe WO) und bestätigten exakt die im Envelope genannten 60px/104px — zurückgeführt auf hrams eigenes `margin.left=60`, das sich ADDITIV mit MUIs Achsenbreiten-Reservierung addiert (kein ucm-Fehler). Die "nachher"-Messung folgt mit FIX-13 nach dem Pin-Bump (ohnehin dort vorgesehen). Fünf Posten: Ränder/Achsenreservierung verkleinern · Achsentitel nicht mehr broadcasten (mit Test, genau der Defekt der ausgeliefert wurde) · Titel aus dem Tick-Band halten · Standard-Tick-Formatter · Entscheidungsregel in `DESIGN.md`. **Gemessene Warnung gegen naives `Intl`-Compact:** Deutsch kürzt Tausender GAR NICHT (`12'500` bleibt), Swahili stellt ein Wort voran (`elfu 12.5`, breiter als die Rohzahl) — kompakte Notation allein löst das Breitenproblem also nicht und verschlimmert es in einer Sprache; ein handgeschriebenes `B` wäre auf Deutsch zudem FALSCH (Billion = 10¹², `Intl` sagt korrekt `Mrd.`). **Regel:** kategoriale Achse → kein Titel (Ticks sind Namen); numerische Achse → Titel trägt Grösse und Einheit, ausser die Ticks zeigen die Einheit schon (`%`); Einheit einmal im Titel, nie in jedem Tick. **Ränder kürzen und Überlappung beheben ziehen gegeneinander** — zusammen machen und messen, sonst erzeugt das eine das andere neu. Nicht am Publish abschliessen: App-Pins nachziehen, hram zuerst (dortiger Nachzug = FIX-13). |
@@ -13,22 +13,28 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import React from 'react';
14
14
  import { Box } from '@mui/material';
15
15
  import { useTheme } from '@mui/material/styles';
16
+ import { useTranslation } from 'react-i18next';
16
17
  import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
17
18
  import { useNeutralChartPalette } from './palette';
18
- import { DEFAULT_LEGEND_POSITION, spaceForRotatedTicks, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
19
+ import { DEFAULT_LEGEND_POSITION, defaultNumericTickFormatter, sizeYAxisForContent, spaceForRotatedTicks, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
19
20
  /**
20
- * Responsive MUI X-Charts bar preset. xAxisLabel and yAxisLabel (including units) are required.
21
- * It supplies labelled axes, tooltips, and an automatic multi-series legend.
21
+ * Responsive MUI X-Charts bar preset. It supplies labelled axes, tooltips,
22
+ * and an automatic multi-series legend. `xAxisLabel`/`yAxisLabel` are
23
+ * optional, per DESIGN.md #8a: a categorical axis (BarChart's default x-axis
24
+ * shape) needs no title by default -- its ticks are names -- and a numeric
25
+ * axis needs one only unless its unit is already visible in the ticks and
26
+ * the panel heading already names the quantity. Omitting one never suppresses
27
+ * a label the caller DID set (`withAxisDefaults` only ever falls back onto
28
+ * an axis without its own `label`).
22
29
  */
23
30
  export function BarChart(_a) {
24
31
  var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, aspect, grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "aspect", "grid", "hideLegend", "legendPosition", "margin", "slotProps"]);
25
32
  const theme = useTheme();
33
+ const { i18n } = useTranslation();
26
34
  const neutralPalette = useNeutralChartPalette();
27
- if (!xAxisLabel || !yAxisLabel) {
28
- throw new Error('BarChart requires xAxisLabel and yAxisLabel.');
29
- }
30
35
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'band', data: [] }, theme.typography.caption.fontSize);
31
- const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear' }, theme.typography.caption.fontSize);
36
+ const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
37
+ const sizedYAxis = sizeYAxisForContent(labelledYAxis, series, theme.typography.caption.fontSize);
32
38
  const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
33
- return (_jsx(Box, { "data-testid": "bar-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiBarChart, Object.assign({}, chartProps, { series: series, xAxis: rotatedTickSpace.xAxis, yAxis: labelledYAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: rotatedTickSpace.margin, slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
39
+ return (_jsx(Box, { "data-testid": "bar-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiBarChart, Object.assign({}, chartProps, { series: series, xAxis: rotatedTickSpace.xAxis, yAxis: sizedYAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: rotatedTickSpace.margin, slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
34
40
  }
@@ -13,9 +13,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import React from 'react';
14
14
  import { Box } from '@mui/material';
15
15
  import { useTheme } from '@mui/material/styles';
16
+ import { useTranslation } from 'react-i18next';
16
17
  import { LineChart as MuiLineChart, MarkElement as MuiMarkElement, } from '@mui/x-charts/LineChart';
17
18
  import { useNeutralChartPalette } from './palette';
18
- import { DEFAULT_LEGEND_POSITION, spaceForRotatedTicks, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
19
+ import { DEFAULT_LEGEND_POSITION, defaultNumericTickFormatter, sizeYAxisForContent, spaceForRotatedTicks, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
19
20
  // Fixes MUI's hollow `background.paper`-filled marker (MarkElement.js's own
20
21
  // `styled()` default). CHART-6's WO named a `MuiMarkElement` theme-token
21
22
  // override in src/theme/tokens.js as the preferred mechanism, with a
@@ -31,19 +32,24 @@ function FilledMarkElement(_a) {
31
32
  return _jsx(MuiMarkElement, Object.assign({}, props, { color: color, style: Object.assign({ fill: color }, style) }));
32
33
  }
33
34
  /**
34
- * Responsive MUI X-Charts line preset. xAxisLabel and yAxisLabel (including units) are required.
35
- * It supplies labelled axes, tooltips, and an automatic multi-series legend.
35
+ * Responsive MUI X-Charts line preset. It supplies labelled axes, tooltips,
36
+ * and an automatic multi-series legend. `xAxisLabel`/`yAxisLabel` are
37
+ * optional, per DESIGN.md #8a: a categorical axis needs no title by default
38
+ * -- its ticks are names -- and a numeric axis needs one only unless its
39
+ * unit is already visible in the ticks and the panel heading already names
40
+ * the quantity. Omitting one never suppresses a label the caller DID set
41
+ * (`withAxisDefaults` only ever falls back onto an axis without its own
42
+ * `label`).
36
43
  */
37
44
  export function LineChart(_a) {
38
45
  var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, aspect, grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, slots, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "aspect", "grid", "hideLegend", "legendPosition", "margin", "slots", "slotProps"]);
39
46
  const theme = useTheme();
47
+ const { i18n } = useTranslation();
40
48
  const neutralPalette = useNeutralChartPalette();
41
- if (!xAxisLabel || !yAxisLabel) {
42
- throw new Error('LineChart requires xAxisLabel and yAxisLabel.');
43
- }
44
49
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'point', data: [] }, theme.typography.caption.fontSize);
45
- const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear' }, theme.typography.caption.fontSize);
46
- const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
50
+ const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
47
51
  const defaultedSeries = series.map((item) => (Object.assign({ showMark: false, labelMarkType: 'square' }, item)));
48
- return (_jsx(Box, { "data-testid": "line-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiLineChart, Object.assign({}, chartProps, { series: defaultedSeries, xAxis: rotatedTickSpace.xAxis, yAxis: labelledYAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: rotatedTickSpace.margin, slots: Object.assign({ mark: FilledMarkElement }, slots), slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
52
+ const sizedYAxis = sizeYAxisForContent(labelledYAxis, defaultedSeries, theme.typography.caption.fontSize);
53
+ const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
54
+ return (_jsx(Box, { "data-testid": "line-chart-container", sx: { width: '100%', minHeight, aspectRatio: aspect }, children: _jsx(MuiLineChart, Object.assign({}, chartProps, { series: defaultedSeries, xAxis: rotatedTickSpace.xAxis, yAxis: sizedYAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: rotatedTickSpace.margin, slots: Object.assign({ mark: FilledMarkElement }, slots), slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
49
55
  }
@@ -7,7 +7,18 @@ export function withGridDefaults(grid) {
7
7
  }
8
8
  export function withAxisDefaults(axes, label, defaults, tickFontSize) {
9
9
  const values = (axes === null || axes === void 0 ? void 0 : axes.length) ? axes : [defaults];
10
- return values.map((axis) => (Object.assign(Object.assign(Object.assign({}, defaults), axis), { tickLabelStyle: Object.assign({ fontSize: tickFontSize }, axis.tickLabelStyle), label: axis.label || label })));
10
+ return values.map((axis, index) => (Object.assign(Object.assign(Object.assign({}, defaults), axis), { tickLabelStyle: Object.assign({ fontSize: tickFontSize }, axis.tickLabelStyle),
11
+ // THEME-9: the single `label` argument is the caller's ONE string for
12
+ // this whole axis family (xAxisLabel/yAxisLabel) -- it may only ever
13
+ // fall back onto ONE axis. Applying it via .map() to every entry
14
+ // broadcast the identical label onto every axis in a multi-axis array
15
+ // (measured live: hram's dual-axis chart carried the same 43-character
16
+ // label on both y-axes). Only the first entry -- the common single-axis
17
+ // case is index 0 -- gets the fallback; a caller building a genuine
18
+ // multi-axis array must label each entry itself (as TimeSeriesChart's
19
+ // own secondary-axis support already does), exactly like MUI's own
20
+ // default `position: 'left'` (only the first entry, MUI docs).
21
+ label: axis.label || (index === 0 ? label : undefined) })));
11
22
  }
12
23
  function cssLengthToPixels(value) {
13
24
  if (typeof value === 'number')
@@ -78,6 +89,133 @@ export function spaceForRotatedTicks(xAxis, margin, defaultLineHeight = 1) {
78
89
  ? Object.assign(Object.assign({}, margin), { bottom: MUI_CHART_MARGIN_BOTTOM + largestExtraHeight }) : margin;
79
90
  return { xAxis: spacedXAxis, margin: spacedMargin };
80
91
  }
92
+ // THEME-9: MUI X-Charts' own default y-axis width is a flat 45px (ticks) +
93
+ // 20px (label, if present) = 65px regardless of what the ticks actually say
94
+ // -- confirmed against @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH /
95
+ // AXIS_LABEL_DEFAULT_HEIGHT constants and its `useChartDimensions` selector
96
+ // (drawingArea.left = margin.left + axisSizeLeft, i.e. margin and axis size
97
+ // are ADDITIVE, not one containing the other). That flat default is both too
98
+ // generous for short ticks ("0%"/"50%"/"100%") and too stingy for wide ones
99
+ // ("50.0 min"), which is exactly the measured overlap on the Accessibility
100
+ // y-axis. Mirrors spaceForRotatedTicks' existing text-width heuristic below.
101
+ const Y_AXIS_TICK_SIZE = 6;
102
+ const Y_AXIS_TICK_LABEL_GAP = 2;
103
+ const Y_AXIS_LABEL_TICK_GAP = 2;
104
+ // A rotated (90deg) axis-label's line-box "thickness" -- what it costs in
105
+ // WIDTH once rotated -- runs a bit past its raw font size once ascender/
106
+ // descender room is included; matches the ~18px measured live for a 14px
107
+ // label (14 * 1.3 ~= 18).
108
+ const Y_AXIS_LABEL_THICKNESS_FACTOR = 1.3;
109
+ // A small safety margin on the estimate: `spaceForRotatedTicks`'s own risk
110
+ // note applies here too -- a too-tight cut recreates the overlap it was
111
+ // meant to fix, and glyph-width heuristics are approximate by nature.
112
+ const Y_AXIS_WIDTH_SAFETY_FACTOR = 1.1;
113
+ // MUI X-Charts' ChartsYAxisImpl hardcodes the axis-label font at 14px
114
+ // (`additionalProps.style: {...theme.typography.body1, fontSize: 14, ...}`
115
+ // -- fontSize:14 is set AFTER the theme.typography.body1 spread, so it wins
116
+ // regardless of what body1 actually resolves to). Matching that literal
117
+ // constant here -- rather than reading it off `theme.typography.body1` --
118
+ // keeps this sizing correct even if this package's body1 size ever changes;
119
+ // coupling it to body1 was only ever coincidentally right because body1
120
+ // happens to be 14px today.
121
+ const MUI_AXIS_LABEL_FONT_SIZE_PX = 14;
122
+ function longestFormattedTick(values, valueFormatter) {
123
+ return values.reduce((longest, value) => {
124
+ let text;
125
+ try {
126
+ text = valueFormatter ? String(valueFormatter(value, { location: 'tick' })) : String(value);
127
+ }
128
+ catch (_a) {
129
+ // Some formatters require MUI's complete scale context; falling back
130
+ // to the raw value is conservative -- it reserves at least as much
131
+ // space as the unformatted number would need.
132
+ text = String(value);
133
+ }
134
+ return text.length > longest.length ? text : longest;
135
+ }, '');
136
+ }
137
+ /**
138
+ * Sizes a linear y-axis to its actual tick content instead of MUI's flat
139
+ * default. Only touches axes the caller left unsized (`axis.width == null`)
140
+ * and only linear/numeric ones -- a categorical axis's ticks are names, not
141
+ * numbers, and sizing those is not this function's job. Candidate values come
142
+ * from the axis' own `min`/`max` when the caller set them (as every current
143
+ * consumer does), falling back to the series actually plotted against this
144
+ * axis; if neither yields a candidate, the axis is left untouched rather than
145
+ * guessed at.
146
+ *
147
+ * `tickFontSize` is only the FALLBACK -- an axis' own `tickLabelStyle.fontSize`
148
+ * (set by `withAxisDefaults`, itself overridable per-axis by the caller) is
149
+ * read first, so a caller-tuned tick font is sized correctly rather than
150
+ * against the package-wide default. The axis LABEL's font is not a caller
151
+ * knob at all (`MUI_AXIS_LABEL_FONT_SIZE_PX` -- MUI hardcodes it).
152
+ *
153
+ * The series-to-axis match for the fallback deliberately mirrors MUI's own
154
+ * default-assignment rule, not a naive `undefined === undefined`: a series
155
+ * with no explicit `yAxisId` is assigned to the FIRST axis in the array
156
+ * (`yAxisIds[0]` in MUI's own selector), never to "no id". Matching only
157
+ * `undefined === undefined` left the primary axis of a real dual-axis chart
158
+ * (id `'primary'`, its series never set `yAxisId`) with zero matched series
159
+ * and therefore MUI's untouched flat default -- a silent no-op on exactly
160
+ * the dual-axis case this WO was measured against.
161
+ */
162
+ export function sizeYAxisForContent(yAxis, series, tickFontSize) {
163
+ var _a;
164
+ const fallbackTickFontSizePx = cssLengthToPixels(tickFontSize);
165
+ const defaultAxisId = (_a = yAxis[0]) === null || _a === void 0 ? void 0 : _a.id;
166
+ return yAxis.map((axis) => {
167
+ var _a;
168
+ if (axis.width != null)
169
+ return axis;
170
+ if (axis.scaleType && axis.scaleType !== 'linear')
171
+ return axis;
172
+ const tickFontSizePx = cssLengthToPixels((_a = axis.tickLabelStyle) === null || _a === void 0 ? void 0 : _a.fontSize) || fallbackTickFontSizePx;
173
+ if (tickFontSizePx === 0)
174
+ return axis;
175
+ const candidates = [];
176
+ if (typeof axis.min === 'number')
177
+ candidates.push(axis.min);
178
+ if (typeof axis.max === 'number')
179
+ candidates.push(axis.max);
180
+ if (candidates.length === 0) {
181
+ (series || [])
182
+ .filter((item) => { var _a; return ((_a = item.yAxisId) !== null && _a !== void 0 ? _a : defaultAxisId) === axis.id; })
183
+ .forEach((item) => (item.data || []).forEach((value) => {
184
+ if (typeof value === 'number')
185
+ candidates.push(value);
186
+ }));
187
+ }
188
+ if (candidates.length === 0)
189
+ return axis;
190
+ const longestTick = longestFormattedTick(candidates, axis.valueFormatter);
191
+ const tickTextWidth = Math.max(tickFontSizePx, longestTick.length * tickFontSizePx * AVERAGE_GLYPH_WIDTH_EM);
192
+ const labelThickness = axis.label
193
+ ? Math.ceil(MUI_AXIS_LABEL_FONT_SIZE_PX * Y_AXIS_LABEL_THICKNESS_FACTOR) + Y_AXIS_LABEL_TICK_GAP
194
+ : 0;
195
+ const width = Math.ceil((tickTextWidth + Y_AXIS_TICK_SIZE + Y_AXIS_TICK_LABEL_GAP + labelThickness)
196
+ * Y_AXIS_WIDTH_SAFETY_FACTOR);
197
+ return Object.assign(Object.assign({}, axis), { width });
198
+ });
199
+ }
200
+ /**
201
+ * Locale-correct, unit-free tick numbers as the default -- BarChart set none
202
+ * at all (raw numbers straight from the domain), and a hand-rolled `Intl`
203
+ * compact-notation default was measured to be actively wrong: German does
204
+ * not compact thousands at all (`12'500` stays `12'500`) and Swahili prepends
205
+ * a word that is WIDER than the raw number (`elfu 12.5`) -- compact notation
206
+ * does not solve the width problem and worsens it in one locale. Plain
207
+ * grouped formatting (no `notation: 'compact'`) is safe across all four
208
+ * locales this package's consumers use. Ticks get a short, rounded form;
209
+ * tooltip/legend keep full precision -- teaching screens must not round away
210
+ * a meaningful difference. A caller's own `valueFormatter` always wins (this
211
+ * is only ever used as a default, per `withAxisDefaults`' `{...defaults,
212
+ * ...axis}` merge).
213
+ */
214
+ export function defaultNumericTickFormatter(locale) {
215
+ return (value, context) => ((context === null || context === void 0 ? void 0 : context.location) === 'tick'
216
+ ? new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)
217
+ : new Intl.NumberFormat(locale).format(value));
218
+ }
81
219
  export function withChartSlotDefaults(slotProps, legendPosition) {
82
220
  var _a;
83
221
  const position = Object.assign(Object.assign(Object.assign({}, DEFAULT_LEGEND_POSITION), legendPosition), (_a = slotProps === null || slotProps === void 0 ? void 0 : slotProps.legend) === null || _a === void 0 ? void 0 : _a.position);
@@ -45,8 +45,8 @@ const COMPONENT_SURFACES = [
45
45
  componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.enter'),
46
46
  componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.exit'),
47
47
  componentPath('MuiSelect', 'defaultProps.variant'),
48
- componentPath('MuiSelect', 'styleOverrides.select.minHeight'),
49
- componentKeyLeaf('MuiSelect', 'styleOverrides.select', '@media (any-pointer: coarse)', 'minHeight'),
48
+ componentPath('MuiSelect', 'styleOverrides.root.minHeight'),
49
+ componentKeyLeaf('MuiSelect', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
50
50
  componentPath('MuiCheckbox', 'styleOverrides.root.color'),
51
51
  componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
52
52
  componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
@@ -209,7 +209,7 @@ export const BASELINE_STATIC = {
209
209
  MuiSelect: {
210
210
  defaultProps: { variant: 'outlined' },
211
211
  styleOverrides: {
212
- select: {
212
+ root: {
213
213
  minHeight: 40,
214
214
  boxSizing: 'border-box',
215
215
  '@media (any-pointer: coarse)': { minHeight: 44 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.39.0",
3
+ "version": "2.40.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
@@ -1,10 +1,13 @@
1
1
  import React from 'react';
2
2
  import { Box } from '@mui/material';
3
3
  import { useTheme } from '@mui/material/styles';
4
+ import { useTranslation } from 'react-i18next';
4
5
  import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
5
6
  import { useNeutralChartPalette } from './palette';
6
7
  import {
7
8
  DEFAULT_LEGEND_POSITION,
9
+ defaultNumericTickFormatter,
10
+ sizeYAxisForContent,
8
11
  spaceForRotatedTicks,
9
12
  withAxisDefaults,
10
13
  withChartSlotDefaults,
@@ -12,8 +15,14 @@ import {
12
15
  } from './chartDefaults';
13
16
 
14
17
  /**
15
- * Responsive MUI X-Charts bar preset. xAxisLabel and yAxisLabel (including units) are required.
16
- * It supplies labelled axes, tooltips, and an automatic multi-series legend.
18
+ * Responsive MUI X-Charts bar preset. It supplies labelled axes, tooltips,
19
+ * and an automatic multi-series legend. `xAxisLabel`/`yAxisLabel` are
20
+ * optional, per DESIGN.md #8a: a categorical axis (BarChart's default x-axis
21
+ * shape) needs no title by default -- its ticks are names -- and a numeric
22
+ * axis needs one only unless its unit is already visible in the ticks and
23
+ * the panel heading already names the quantity. Omitting one never suppresses
24
+ * a label the caller DID set (`withAxisDefaults` only ever falls back onto
25
+ * an axis without its own `label`).
17
26
  */
18
27
  export function BarChart({
19
28
  series = [],
@@ -32,10 +41,8 @@ export function BarChart({
32
41
  ...chartProps
33
42
  }) {
34
43
  const theme = useTheme();
44
+ const { i18n } = useTranslation();
35
45
  const neutralPalette = useNeutralChartPalette();
36
- if (!xAxisLabel || !yAxisLabel) {
37
- throw new Error('BarChart requires xAxisLabel and yAxisLabel.');
38
- }
39
46
 
40
47
  const labelledXAxis = withAxisDefaults(
41
48
  xAxis,
@@ -46,7 +53,12 @@ export function BarChart({
46
53
  const labelledYAxis = withAxisDefaults(
47
54
  yAxis,
48
55
  yAxisLabel,
49
- { scaleType: 'linear' },
56
+ { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) },
57
+ theme.typography.caption.fontSize,
58
+ );
59
+ const sizedYAxis = sizeYAxisForContent(
60
+ labelledYAxis,
61
+ series,
50
62
  theme.typography.caption.fontSize,
51
63
  );
52
64
  const rotatedTickSpace = spaceForRotatedTicks(
@@ -61,7 +73,7 @@ export function BarChart({
61
73
  {...chartProps}
62
74
  series={series}
63
75
  xAxis={rotatedTickSpace.xAxis}
64
- yAxis={labelledYAxis}
76
+ yAxis={sizedYAxis}
65
77
  colors={palette || neutralPalette.categorical}
66
78
  grid={withGridDefaults(grid)}
67
79
  hideLegend={hideLegend}
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Box } from '@mui/material';
3
3
  import { useTheme } from '@mui/material/styles';
4
+ import { useTranslation } from 'react-i18next';
4
5
  import {
5
6
  LineChart as MuiLineChart,
6
7
  MarkElement as MuiMarkElement,
@@ -8,6 +9,8 @@ import {
8
9
  import { useNeutralChartPalette } from './palette';
9
10
  import {
10
11
  DEFAULT_LEGEND_POSITION,
12
+ defaultNumericTickFormatter,
13
+ sizeYAxisForContent,
11
14
  spaceForRotatedTicks,
12
15
  withAxisDefaults,
13
16
  withChartSlotDefaults,
@@ -29,8 +32,14 @@ function FilledMarkElement({ color, style, ...props }) {
29
32
  }
30
33
 
31
34
  /**
32
- * Responsive MUI X-Charts line preset. xAxisLabel and yAxisLabel (including units) are required.
33
- * It supplies labelled axes, tooltips, and an automatic multi-series legend.
35
+ * Responsive MUI X-Charts line preset. It supplies labelled axes, tooltips,
36
+ * and an automatic multi-series legend. `xAxisLabel`/`yAxisLabel` are
37
+ * optional, per DESIGN.md #8a: a categorical axis needs no title by default
38
+ * -- its ticks are names -- and a numeric axis needs one only unless its
39
+ * unit is already visible in the ticks and the panel heading already names
40
+ * the quantity. Omitting one never suppresses a label the caller DID set
41
+ * (`withAxisDefaults` only ever falls back onto an axis without its own
42
+ * `label`).
34
43
  */
35
44
  export function LineChart({
36
45
  series = [],
@@ -50,10 +59,8 @@ export function LineChart({
50
59
  ...chartProps
51
60
  }) {
52
61
  const theme = useTheme();
62
+ const { i18n } = useTranslation();
53
63
  const neutralPalette = useNeutralChartPalette();
54
- if (!xAxisLabel || !yAxisLabel) {
55
- throw new Error('LineChart requires xAxisLabel and yAxisLabel.');
56
- }
57
64
 
58
65
  const labelledXAxis = withAxisDefaults(
59
66
  xAxis,
@@ -64,19 +71,24 @@ export function LineChart({
64
71
  const labelledYAxis = withAxisDefaults(
65
72
  yAxis,
66
73
  yAxisLabel,
67
- { scaleType: 'linear' },
74
+ { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) },
68
75
  theme.typography.caption.fontSize,
69
76
  );
70
- const rotatedTickSpace = spaceForRotatedTicks(
71
- labelledXAxis,
72
- margin,
73
- theme.typography.caption.lineHeight,
74
- );
75
77
  const defaultedSeries = series.map((item) => ({
76
78
  showMark: false,
77
79
  labelMarkType: 'square',
78
80
  ...item,
79
81
  }));
82
+ const sizedYAxis = sizeYAxisForContent(
83
+ labelledYAxis,
84
+ defaultedSeries,
85
+ theme.typography.caption.fontSize,
86
+ );
87
+ const rotatedTickSpace = spaceForRotatedTicks(
88
+ labelledXAxis,
89
+ margin,
90
+ theme.typography.caption.lineHeight,
91
+ );
80
92
 
81
93
  return (
82
94
  <Box data-testid="line-chart-container" sx={{ width: '100%', minHeight, aspectRatio: aspect }}>
@@ -84,7 +96,7 @@ export function LineChart({
84
96
  {...chartProps}
85
97
  series={defaultedSeries}
86
98
  xAxis={rotatedTickSpace.xAxis}
87
- yAxis={labelledYAxis}
99
+ yAxis={sizedYAxis}
88
100
  colors={palette || neutralPalette.categorical}
89
101
  grid={withGridDefaults(grid)}
90
102
  hideLegend={hideLegend}