@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.
package/WORK_ORDERS.md CHANGED
@@ -94,3 +94,4 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
94
94
  | UCM-CHART-9 | `ChartFrame` is a card, not a chart: it must never take a fixed height | REGRESSION in published 2.42.0, observed on hram staging. UCM-CHART-8 wired `resolveChartHeight()` through the four chart presets AND through `ChartFrame`. Correct for the presets, wrong for the frame: in the `minHeight`-alone branch the resolver returns `chartHeight = minHeight`, and `ChartFrame` applies it as `height` on its content box. A preset's box holds one chart; **`ChartFrame`'s box holds a whole card** -- title, toolbar, chart, legend, footnotes, export links. The floor became a fixed height, the content overflows, and ADJACENT CARDS VISIBLY OVERLAP (card title drawn over the chart above it, three sets of Export SVG/PNG stacked at different offsets). UCM-CHART-8's own Files note recorded that `ChartFrame` never destructures `height` -- the implementation then gave it one. | 2026-08-21 | done | 2026-08-22: `ChartFrame` fix + version bump + accurate mismatch warning | Tier 3, shared-core, live regression in `latest`. Full order: [`work-orders/UCM-CHART-9.md`](work-orders/UCM-CHART-9.md). Operator chose FIX-FORWARD over reverting hram's pin (2026-08-21), because 2.42.0 also appears to have stopped hram's Detailed-Results React #185 crash -- plausible mechanism is that deterministic sizing removed a resize-driven setState loop, but that is a HYPOTHESIS, not a finding, and this WO must re-check whether the crash returns (if it does, that is a finding for HRAM-RES-29, not a reason to undo this). Do NOT revert UCM-CHART-8: its preset behaviour is correct and includes the `aspect` branch four live call sites depend on; this WO narrows one consumer of the resolver only, and any edit needed to a preset test means the change has leaked. **Codex:** out of credits on the one allowed probe today (`.claude/codex-status.md`, 2026-08-22) -- Orchestrator implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3, so no routing change. **Reviews:** `reviewer` -- 2 findings (R1 version not bumped in-diff, fixed by bumping `package.json` to 2.42.1 in this commit so `publish.yml`'s version-increase gate fires on push; R2 jsdom-can't-measure-layout note on one test, non-blocking, covered by the live-browser check below). `ui_reviewer` -- 2 findings (U1 `warnOnHeightMismatch`'s message was left factually wrong for `ChartFrame` post-fix -- FIXED: added a `heightWins` param, `ChartFrame` now gets its own accurate wording, test coverage added in both `chartDefaults.test.js` and `ChartFrame.test.jsx`; U2 height-alone-on-ChartFrame is now a silent no-op with no dev warning -- accepted as-is, out of scope for this regression fix). Both reviews clean after fixes, no open findings. **Tests:** affected-area set (`ChartFrame`, `chartDefaults`, all four presets) -- 129/129 green, preset assertions byte-identical/unchanged. **Rendered check:** this repo's own dev harness (`npm run dev`, `dev/entries.jsx` "Charts / ScatterChart (allocation shape)") driven live in the internal preview browser at 375px and 1280px -- DOM/computed-style inspection (no screenshot capture available in this session; declared substitution per the gate's fallback clause), confirmed the content box carries no fixed `height` and, after forcing the inner chart element's rendered height up to 900px/1100px to reproduce the regression scenario, the box grew to match with zero scrollHeight/clientHeight overflow at both widths. The Envelope's full five-card check (Sensitivity, Access, Access Ladder, Allocation, Optimization, rendered in hram itself) is **NOT done by this WO** -- this repo's harness only has 3 of the 5 named cards and no live hram instance was linked against this build. Carried forward as a mandatory precondition on whichever WO next bumps hram's `ui-core-micha` pin past 2.42.0. 2.42.0 stays flagged do-not-adopt; 2.42.1 publishes automatically via `publish.yml` on this push (version-increase gate). PROCESS NOTE, recorded deliberately: 2.42.0 shipped on 188 green unit tests that say nothing about layout -- the rendered check above is the layout-level evidence this WO exists to add, even though it stops short of the full hram five-card scope. FIVE-CARD CHECK DONE 2026-08-22, hram with real data (SG Sandbox CLI / `CLI-Kilombero_2026-per_cell-network`), ucm 2.42.1 loaded, at 1280 px AND 375 px: zero boxes with an inline fixed height, zero overflowing cards, zero overlapping card pairs, no horizontal body scroll, bottom dead space 17 px (card padding) on every card. CHART-9 does what it set out to do. SUBSTITUTION DECLARED: screenshot capture was unavailable all session, so this is DOM + computed-style measurement, not images. hram pin bumped to 2.42.1 in `6dd00c1d`. NOTE: the operator's remaining "huge whitespace" complaint is NOT this WO -- it is a third, independent reservation bug, now UCM-CHART-10. |
95
95
  | UCM-CHART-10 | The rotated-tick allowance is reserved twice, and the surplus is dead space | `spaceForRotatedTicks` adds the same `extraHeight` BOTH to `axis.height` and to `margin.bottom`, and in MUI v8 those are additive. The axis band already holds the rotated labels, so the margin copy is left over as an empty band below the plot. Measured in hram 2026-08-22 at 1280 px with ucm 2.42.1: Ward Metrics and Access Ladder each show a 340 px surface whose plot ends at 131 px -- **130 px, 38 %, empty**; Cost 92 px; Division Metrics 85 px; Cost-effectiveness 44 px; Expert Mode only 8 px. Two things make it conclusive: the band SCALES WITH THE LABEL LOAD (35 long rotated ward names cost 130 px, short labels cost 8), and `emptyBelowLowestLabel` equals `emptyBelowAxisLine` on every chart -- the labels sit inside the axis band and nothing occupies the space beneath them. | 2026-08-22 | done | 2026-08-22: `spaceForRotatedTicks` fix + version bump | Tier 3, shared-core. Full order: [`work-orders/UCM-CHART-10.md`](work-orders/UCM-CHART-10.md). THIRD instance of one failure class in this file: space reserved in two places that add up -- after UCM-CHART-8 (`minHeight` vs `height`) and hram's RES-28 (`margin.left` vs an unsized `yAxis`). Neither CHART-8 nor CHART-9 could have caught it: both worked on the wrapper BOX, this is the axis MARGIN arithmetic. **Fix:** kept `extraHeight` on `axis.height` only, `margin` now passes through `spaceForRotatedTicks` completely untouched (no `bottom` computed in any branch) -- a caller-set `margin.bottom` is therefore protected unconditionally, a strict superset of the removed `callerSetBottom` escape hatch, which both reviewers confirmed is a clean simplification, not a behaviour gap. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged during `UCM-CHART-9` earlier the same day) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3, so no routing change. **Reviews:** `reviewer` and `ui_reviewer` ran concurrently (Sonnet, diff inline), both converged on the same two findings, both fixed before commit -- (1) version not bumped in-diff (fixed: `package.json` 2.42.1 -> 2.42.2, so `publish.yml`'s version-increase gate fires on push); (2) the WO's "investigate in the same pass" item (HRAM-RES-29 item 2, scatter x-axis tick-label clipping) had no recorded confirm/refute. **Refuted and recorded in the WO file:** `ScatterChart.jsx` never calls `spaceForRotatedTicks` at all, and hram's own named call sites (`AccessGapScatterPanel.jsx`, `OptimizationResultsPanel.jsx`) set no `tickLabelStyle.angle` either -- this helper cannot be the cause. Matches hram's own `HRAM-RES-29` register row, which had independently traced and closed item 2 the same day to an unrelated pre-2.42.0 missing-`height`-prop bug on `ScatterChart`, already fixed by `UCM-CHART-8`/2.42.0. Both reviews clean after fixes, no open findings. **Tests:** affected-area set (`chartDefaults` incl. new `spaceForRotatedTicks (CHART-10)` unit-test block, all four presets, `ChartFrame`) -- 134/134 green, preset assertions unchanged except the two that directly asserted the bug (rewritten to assert the fix, both reviewers confirmed the rewrite is correct and non-trivial). **Rendered check -- numeric, on hram with real data** (`CLI-Kilombero_2026-per_cell-network`, SG Sandbox CLI, same candidate point, Research -> Allocation -> Basic Plots), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement, hram's own git tree untouched -- confirmed via `git status`): at 1280 px, Ward Metrics 129.6 px -> 51.6 px empty (35 labels, 340 px surface -- matches this row's own original 130 px/38% measurement almost exactly), Access Ladder 156.9 px -> 80.6 px (35 labels, 369 px), Division Metrics 85.2 px -> 41.2 px (5 labels, 340 px) -- each roughly halved, consistent with removing exactly one of two equal additive reservations. Checked again at 375 px: no clipping at either width, no regression. SUBSTITUTION DECLARED: screenshot capture was unavailable in this session (Browser pane would not composite frames) -- this is DOM/computed-style measurement (`getBoundingClientRect`/`getComputedStyle` against the live rendered SVG), which for this WO's numeric acceptance criterion is close to sufficient on its own, per the WO's own fallback clause. Cost/Cost-effectiveness/Expert Mode cards from the original six-card table were not individually re-measured (time-boxed after the first three -- Ward Metrics, Access Ladder, Division Metrics -- gave a clean, consistent, reviewer-confirmed signal); those three all use `BarChart` and its rotated-tick `xAxisAngle` path same as the measured cards (`CostMetricsPanel.jsx`, `RatioMetricsPanel.jsx` both call `spaceForRotatedTicks` via `BarChart`), so the fix is expected to reach them the same way, just not independently confirmed with numbers here. Publishes automatically via `publish.yml` on this push (version-increase gate). CONSUMER-SIDE CONFIRMATION 2026-08-22 (Expertenchat, independent of the implementing session): the PUBLISHED 2.42.2 tarball was installed normally in hram via the pin and re-measured -- this closes the one gap the implementer's node_modules swap could not, since a locally-built file is not the published artifact. (a) Artifact inspected: `dist/index.js` contains neither `MUI_CHART_MARGIN_BOTTOM` nor `largestExtraHeight`, and `src/` carries the single-reservation docstring. (b) Effect re-measured on hram Teaching -> Basic Plots at 1280 px with real data, BEFORE vs AFTER the pin: Ward Metrics 129 -> 51 px empty (implementer reported 51.6), Division Metrics 85 -> 41 (reported 41.2), Access Ladder 128 -> 51, Cost 93 -> 15, and Expert Mode unchanged at 8 -> 8 (it had almost no allowance to reclaim) -- an independent match to the implementer's numbers. No clipping at either width measured. hram pin bumped in `ae0fd956`. TWO LIMITS DECLARED, not glossed: (1) screenshots were unavailable all session, so this is DOM + computed-style measurement -- adequate here because the acceptance criterion is a pixel count, not an impression; (2) **the 375 px breakpoint was NOT reachable** -- the browser pane floors out around 418-620 px, so the second width measured was ~620 px, not 375. The implementer's own 375 px check stands; this confirmation does not duplicate it. METHOD TRAP WORTH KEEPING: the first measurement here returned the pre-fix numbers exactly (129/85/93) and looked like "the fix does not work". It was the wrong bundle -- this local stack serves from `FRONTEND_BUILD_DIR=/app/frontend_host/build_current`, which `run-dev` packs from `build_next` with an UNHASHED `index.js`, while a plain `pnpm build` writes hashed files into `frontend/build/` and never reaches it. Measure only after `run-dev --build`, or the measurement describes a version that is not loaded. |
96
96
  | UCM-CHART-11 | The rotated-tick allowance is now counted once, but estimated too high | CHART-10 removed the double reservation and that holds. What remains is a SINGLE reservation that is too large. Measured in hram 2026-08-22, ucm 2.42.2 installed via the pin, Teaching -> Basic Plots, real data, 1280 px: Ward Metrics and Access Ladder each reserve 131 px of `axis.height` while the rendered tick band is **79 px** -- 52 px unused; Division Metrics 97 vs 56 (41 unused); Cost 68 vs 53 (15). All four are -45 deg and the same preset. The unused band SCALES WITH THE ESTIMATE, not with the rendered text: `rotatedTickMetrics` derives `extraHeight` from `longestFormattedTick x AVERAGE_GLYPH_WIDTH_EM (0.6) x sin(angle)`, a character-count guess that runs consistently above the real extent. **Cost is not the well-behaved case** -- MUI truncates its labels itself (`Ching'a...`), so the estimator sees a short string; nothing about Cost is configured better. | 2026-08-22 | done | 2026-08-22: rotated-tick estimate fix + version bump | Tier 3, shared-core, third proportion change in this series. Full order: [`work-orders/UCM-CHART-11.md`](work-orders/UCM-CHART-11.md). Do NOT revert CHART-10 -- this is the SIZE of the single reservation, not whether there are two. TRAP recorded because I walked into it during the measurement: an axis TITLE also sits below the lowest tick, and `Expert Mode: Simulation Trajectory` showed 53 px there that is entirely title, not waste (`hasAxisTitle: true`, angle 0). The four cards in the table all have `hasAxisTitle: false`, which is why THEIR residual is genuinely unused -- any validating measurement must separate title from empty band or a "fix" will clip the title. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged earlier the same day during `UCM-CHART-9`) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3. **Fix, two parts:** (1) real width measurement via a transient shared invisible SVG `<text>` node (`measureTickTextWidthPx`, `getComputedTextLength`) instead of the flat `charCount x 0.6em` guess -- verified live against hram to match the real rendered tick's own measurement to the sub-pixel (91.203125px both ways for "Msolwa Station"); a per-character-width table (`estimateTextWidthPx`) is the declared ESTIMATE fallback for when no DOM is available (confirmed live: jsdom's `getComputedTextLength` throws, so this is what unit tests actually exercise). (2) Direct DOM inspection showed an accurate width alone still left ~38px unused (the old `axis.height = 45 + (projection - fontSize)` combine step double-counts the baseline) -- corrected to `axis.height = max(45, ceil(projection) + 8px clearance)`, i.e. the larger of the un-rotated floor and the real rotated extent, not always both added together. **Reviews:** `reviewer` + `ui_reviewer` ran concurrently (Sonnet, diff inline), converged on distinct real findings, both fixed before commit -- (R1) the combine step's leftover `Math.max(1, ...)` floor (carried over from the old formula) made the true floor 46px while every doc comment/test claimed 45px; changed to `Math.max(0, ...)` and added a test pinning the exact-45 case. (R2/U2, informational, accepted as-is) the estimate fallback and the DOM measurer only account for `fontSize`, not `fontFamily`/`fontWeight`/`letterSpacing` -- no current caller sets those, so no code change; named as a latent gap for a future consumer that does. (U1) only -45deg was live-verified; hram's one real -90deg call site (`RatioMetricsPanel`'s `xAxisAngle=-90`, wired to `VillageMetricsPanel.jsx`) is **not imported/reachable anywhere in the app** (confirmed by grep, dead/unwired code) so it could not be live-checked -- added a deterministic unit test instead, confirming the formula degenerates correctly at -90deg (the line-height term vanishes, textWidth alone drives the projection, no NaN/blow-up) and flagging the live-verification gap honestly rather than silently. **Tests:** affected-area set -- 139/139 green (134 CHART-10 baseline + 5 new: measured-width injection, truncated-string measurement, per-character fallback comparison, exact-floor-at-45, -90deg generalization). **Rendered check -- numeric, on hram with real data** (Teaching -> Basic Plots, `CLI-Kilombero_2026-per_cell-network`, real production-shaped data), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement -- see below): at 1280px, Ward Metrics 129.6px -> **14.6px** empty; Access Ladder 156.9px -> **14.6px**; Division Metrics 85.2px -> **14.2px**; Cost 92.3px -> **14.6px** (unchanged as expected -- its `axis.height: 60` is caller-fixed, bypasses this helper entirely, confirmed by reading `CostMetricsPanel.jsx`). All four land under the WO's ~15px target. Re-checked at 375px: identical numbers (fixed-pixel chart heights, width-independent), zero clipped x-axis tick labels at either width across all four cards (35/35/5/35 ticks checked). SUBSTITUTION DECLARED: screenshot capture was unavailable this session; this is DOM/computed-style measurement (`getComputedTextLength`/`getBoundingClientRect` against the live rendered SVG), cross-validated against the real rendered tick's own measurement (exact sub-pixel match) rather than taken on faith. **Environment note:** the verification swap briefly touched a concurrent session's idle `npm run dev` process on hram's port 5173 (stopped and restarted with forced dependency re-optimization -- the pin-bump work it supported, commit `ae0fd956`, was already landed and clean, confirmed via `git log`/`git status` before touching it) and, separately, an accidental `cp` overwrite of hram's installed `2.42.2` package file was verified NOT to have corrupted pnpm's global content-addressable store (`pnpm store status`: "Packages in the store are untouched") before being restored from a freshly-downloaded, hash-verified npm tarball. hram's own git tree confirmed untouched throughout (`git status`, only pre-existing unrelated foreign files). Publishes automatically via `publish.yml` on this push (version-increase gate). |
97
+ | UCM-CHART-12 | One layout model for charts. BREAKING (3.0.0), and the end of this series | Four WOs in a row each corrected ONE term of an equation nobody had written down: CHART-8 (`minHeight` vs `height`), CHART-9 (`ChartFrame` given a fixed height), CHART-10 (allowance added to BOTH `axis.height` and `margin.bottom`), CHART-11 (that allowance ESTIMATED above the rendered text). A fifth was already visible -- `ScatterChart`'s x-axis band stays reserved when the tick labels are empty, 58 px of blank inside hram's AccessGapScatter SVG. **Operator called a stop: no more term-by-term corrections.** Root cause is structural -- no single place owns a chart's composition; five interacting knobs (`minHeight`, `height`, `aspect`, `margin`, `xAxisAngle`) are assembled differently per preset and consumers add their own pixel arithmetic on top. THE MODEL, three rules: (1) one size source -- named `size` tokens through the theme scale, `height` px as a documented escape; `minHeight` and `aspect` REMOVED from the presets, so `resolveChartHeight` is DELETED rather than corrected a fourth time. (2) every band is its own measured content or ZERO -- no constants, no term in two addends; an empty band collapses, which is the fifth defect's fix. (3) consumers pass intent, never pixels -- no `margin` prop, and `xAxisAngle` becomes `xLabels: auto|horizontal|angled` with the model deciding rotation from measured labels and available width. | 2026-08-22 | done | `8142fa9`, `03201d7` | Tier 3, BREAKING, first major in this package's history. Full order: [`work-orders/UCM-CHART-12.md`](work-orders/UCM-CHART-12.md). THE DELIVERABLE IS THE INVARIANT, not the fix: `chartHeight === plot + xAxisBand + xTitleBand + legendBand` and `chartWidth === yAxisBand + plot + rightPad`, asserted per preset over a label matrix (none/short/long/long-and-many/empty-strings). CHART-8, -10 and -11 would EACH have failed it on day one, and the width line also catches the two findings still open elsewhere (a scatter y-axis reserving 72 px for a 33 px label; hram's tornado 220 px gutter). It must not be dropped for expedience. MEASURED SURFACE justifying breaking over continuing: 38 chart-consuming files across five apps, 14 with `minHeight`, 76 % in hram -- a sweep, not a migration programme. OPERATOR DECISIONS 2026-08-22: tokens with a pixel escape; `aspect` removed entirely (accepting that four call sites lose responsive height -- 1x hram AccessGapScatter, 3x fitness-monitor BodyHistoryPage, each converting to a token as a NAMED decision in its own migration WO, not silently); ucm first with apps migrated one at a time; NO compatibility shim, because two coexisting models are the ambiguity that caused this. `ChartFrame` KEEPS its `minHeight` -- CHART-9 established a frame is not a chart, do not sweep the frame's floor away with the presets' prop. TEST-ENVIRONMENT CONSTRAINT from CHART-11: jsdom's `getComputedTextLength` throws, so unit tests must inject a measurement stub and a BROWSER-level check is mandatory, not optional -- otherwise the invariant is only asserted against a fiction. SEQUENCING: hram has RES-28, HRAM-RES-29, FIX-19 and CHT-3 in flight on these files; hram's migration WO sequences after them and this WO must not start hram's sweep. **Codex:** first-hand out-of-credits failure this session on this exact WO's dispatch (`codex exec` returned "ERROR: Your workspace is out of credits" twice, exit 1, no PLAN/PROGRESS lines, no file touched) -- stamped `.claude/codex-status.md` 2026-08-22 and implemented directly in Claude, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at this WO's Tier 3, so the flip changed no routing. **Review runtime note:** `.claude/models.local.json` (added mid-session, per the new `35fbae3` governance commit) names `review`/`ui_review` as `codex`/`gpt-5.6-luna` -- not consulted before spawning, since this session's context predated that commit; the actual choice (spawned Claude `reviewer`+`ui_reviewer`, Sonnet) matches what the rule's own Codex-unavailable branch would have produced anyway, since Codex was independently confirmed unavailable today (this WO's own dispatch failure, plus the `UCM-CHART-9` entry earlier the same day) -- `review_fallback` is exactly `claude`/`sonnet`. Named here for the record, not as a retroactive justification. **Reviews:** `reviewer` (Sonnet) -- R1 (the height "invariant" was tautological by construction and could not have caught a CHART-10-shaped double-count; FIXED by adding independent per-term assertions -- `xAxis[0].height === xAxisBand + xTitleBand` and `margin` growth over baseline `=== legendBand` -- alongside the sum, in every invariant test case, plus a docblock correction distinguishing what each assertion actually proves), R2 (a `legendPosition.vertical: 'middle'` side-placed legend was still reserving a HEIGHT band, over-reserving height / under-reserving width; FIXED -- side-placed legends now reserve 0 height, new test added), R3 (dev harness demo's rotation angle silently drifted -25° -> -45° migrating off the old `tickLabelStyle.angle`-implies-rotation API; FIXED -- restored via `xLabels="angled"` + explicit `tickLabelStyle.angle: -25` override, commented). `ui_reviewer` (Sonnet) -- U1 (every axis, even the plain unrotated/untitled case, now gets an explicit `axis.height` where MUI used to fill in its own default -- verified against `node_modules/@mui/x-charts/constants`: `DEFAULT_AXIS_SIZE_HEIGHT=25`/`AXIS_LABEL_DEFAULT_HEIGHT=20` match `TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX` exactly, so no rendering changes for that case; comment + doc note added to make this provable, not just asserted), U2 (docs/CHART-LAYOUT.md's `margin` removal was prose-only, no before/after snippet; FIXED, snippet added), U4 (the no-`spacing`-fn fallback duplicates the 8px unit as a literal; left as a documented P3, all four presets always pass `theme.spacing` in practice). Both reviews' findings fixed; no open findings after fixes. **Tests:** affected-area set -- `chartDefaults`, `ChartFrame`, all four presets, `TimeSeriesChart` -- 152/152 green (151 + 1 new side-legend case). Full repo suite run once as an additional (non-required) sanity check: 566/566 green in isolation (one `packageTreeShaking` test timed out under concurrent-run machine load in the first pass, confirmed unrelated to this diff and passing cleanly alone). **Browser-level check, on this repo's own dev harness** (`dev/entries.jsx` via `ui-core-micha-dev`, `pnpm vite`) -- **hram itself could NOT be used**: hram is still pinned to `2.41.3` and every existing chart call site there still passes the now-removed `minHeight`/`aspect`, so a direct pin swap would throw immediately on nearly every chart (the dev-mode `assertRemovedChartProp` error) rather than render anything measurable; a real hram-with-real-data check is deferred to hram's own migration WO by design (Non-goals: "no consumer migration here"). Instead, verified against this repo's own updated specimens (BarChart, LineChart linear-x-axis, ScatterChart access-shape -- all three exercise real-shaped data, legends, and titled axes) at 1280px: DOM measurement (`getBoundingClientRect` on `.MuiChartsSurface-root`/`.MuiChartsAxis-bottom`) matched the hand-computed model EXACTLY on all three -- `xAxisHeight = 45px` (= `TICK_BAND_BASE_PX 25 + AXIS_TITLE_BAND_PX 20`, every specimen has an axis label) and `gapBelowXAxis = 40px` (= `PACKAGE_DEFAULT_MARGIN.bottom 8 + LEGEND_BAND_PX 32`, every specimen shows a legend). At 375px: chart height stayed correctly pinned (no responsive-height regression), all 22 tick labels on the LineChart specimen remained present with zero overflow past the SVG surface edge (`overflowingTickTexts: []`). SUBSTITUTION DECLARED, consistent with this series' established pattern: screenshot capture was unavailable this session (Browser pane would not composite frames) -- DOM/computed-style measurement only. One PRE-EXISTING, UNRELATED console error observed on the ScatterChart specimens (`<rect> attribute width: A negative value is not valid ("-4")`, from the marker slot's square-shape rendering) -- confirmed via `git diff` that this WO's changes never touch that code path; not investigated further, out of scope. Publishes automatically via `publish.yml` on this push (version-increase gate, `3.0.0 > 2.42.3`). **SECOND REVIEW ROUND, operator-requested, 2026-08-22:** operator asked for a `codex exec` pass over the already-landed diff, per `.claude/models.local.json` (`review`/`ui_review`: `codex`/`gpt-5.6-luna`) -- confirmed Codex available today first (`.claude/codex-status.md` had a fresh `available` line at the top of the log). Invoked directly via Bash with both mandatory flags and `-m gpt-5.6-luna`; first attempt failed (`Argument list too long` -- the 1900-line diff inlined as a CLI argument exceeded the OS limit), retried successfully via stdin. Verdict: changes requested, 6 findings, 4 P1 -- all real, none a repeat of round one. **(1)** `resolveChartLayout` only ever accounted for `sizedYAxis[0]`'s width, ignoring a secondary (right-positioned) y-axis entirely (`TimeSeriesChart`'s `axis: 'secondary'` feature) -- FIXED: `secondaryYAxisBand` now tracked separately, folded into `margin.right`, width invariant extended to four terms; unmarked axes default to the left per MUI's own rule. **(2)** `xLabels="auto"` decided rotation from a raw character-count threshold, never the measured label width the function already computes for the rotated-projection case -- FIXED: the SAME measurement (real DOM when available, per-glyph estimate otherwise) now drives the decision too (`AUTO_ROTATE_LABEL_WIDTH_PX = 50`, replacing the character threshold); the "available width" half of Rule 3's wording stays honestly unmeasured (documented) since no container width exists pre-render. **(3)** `sizeYAxisForContent` floored a y-axis's width at the font size even when every formatted tick was blank (a `valueFormatter` reducing all candidates to `''`) -- FIXED: collapses to a genuine 0 (distinct from the pre-existing "nothing to measure at all" case, which still leaves `width` untouched), unless an axis title is still set, which keeps its own reserved width. **(4)** `slotProps.legend.position` could override `legendPosition` at render time (`withChartSlotDefaults`) while the resolver only ever reserved space for `legendPosition` -- a caller passing both could get a rendered top/side legend against space reserved for bottom, the exact reserved-vs-rendered mismatch this WO exists to close -- FIXED: new exported `resolveLegendPosition` is the one merge both call sites now share; all four presets thread `slotProps` into `resolveChartLayout`. **(5, P2)** a caller-set `xAxis[0].height` bypassed the model's SIZE reservation (an accepted escape) but ALSO silently kept whatever stale `tickLabelStyle.angle` was on that axis, even under `xLabels="horizontal"` -- FIXED: rotation is now written unconditionally from `xLabels`, independent of whether `height` was caller-overridden. **(6, P2, accepted as-is)** invariant coverage is resolver-level, not exhaustively duplicated per preset (ScatterChart/TimeSeriesChart) -- all four presets already wire through the same resolver identically (confirmed by their own passing preset-level tests), so resolver-level coverage was judged proportionate rather than duplicating the full matrix four times. All four P1s and the P2 fixed; 10 new tests added (dual-axis width, measured-width auto rotation incl. two negative cases, blank-y-tick collapse at both `sizeYAxisForContent` and `resolveChartLayout` level, legend-override consistency, angle-survives-caller-height). Re-verified: 162/162 chart tests green, `tsc -p tsconfig.build.json` clean. Per Reviews (`AGENTS.md`): findings fixed, no further reviewer pass without separate approval -- not re-spawned a third time. Version bumped `3.0.0` -> `3.0.1` (patch -- correctness fixes within the same model, not a further breaking change) so `publish.yml`'s gate fires again on this push. |
package/dev/entries.jsx CHANGED
@@ -496,6 +496,11 @@ function ThemeBaselineEntry() {
496
496
  <LineChart
497
497
  xAxis={[{
498
498
  data: ['Jan', 'Feb', 'Mar', 'Apr'],
499
+ // UCM-CHART-12: rotation is now REQUESTED via xLabels="angled" below, not implied by
500
+ // this angle alone -- but an "angled" axis still honours a caller-set angle as an
501
+ // override of its own -45deg default, so this keeps the fixture's original -25deg
502
+ // shape (reviewer finding R3: an unannounced drift to -45deg here would have been a
503
+ // pure demo-visual accident, not an intended change).
499
504
  tickLabelStyle: { fontSize: 14, angle: -25 },
500
505
  }]}
501
506
  series={[
@@ -506,6 +511,7 @@ function ThemeBaselineEntry() {
506
511
  yAxisLabel="Cases"
507
512
  grid={{ vertical: true }}
508
513
  legendPosition={{ vertical: 'top', horizontal: 'end' }}
514
+ xLabels="angled"
509
515
  height={320}
510
516
  />
511
517
  </ChartFrame>
@@ -10,13 +10,13 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
- import React, { useEffect } from 'react';
13
+ import React from 'react';
14
14
  import { Box } from '@mui/material';
15
15
  import { useTheme } from '@mui/material/styles';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { BarChart as MuiBarChart } from '@mui/x-charts/BarChart';
18
18
  import { useNeutralChartPalette } from './palette';
19
- import { DEFAULT_LEGEND_POSITION, defaultNumericTickFormatter, resolveChartHeight, sizeYAxisForContent, spaceForRotatedTicks, warnOnHeightMismatch, withAxisDefaults, withChartSlotDefaults, withGridDefaults, withMarginDefaults, } from './chartDefaults';
19
+ import { DEFAULT_LEGEND_POSITION, assertRemovedChartProp, defaultNumericTickFormatter, resolveChartLayout, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
20
20
  /**
21
21
  * Responsive MUI X-Charts bar preset. It supplies labelled axes, tooltips,
22
22
  * and an automatic multi-series legend. `xAxisLabel`/`yAxisLabel` are
@@ -27,21 +27,33 @@ import { DEFAULT_LEGEND_POSITION, defaultNumericTickFormatter, resolveChartHeigh
27
27
  * a label the caller DID set (`withAxisDefaults` only ever falls back onto
28
28
  * an axis without its own `label`).
29
29
  *
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.
30
+ * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
31
+ * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
32
+ * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
34
33
  */
35
34
  export function BarChart(_a) {
36
- var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, height, aspect, grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "height", "aspect", "grid", "hideLegend", "legendPosition", "margin", "slotProps"]);
35
+ var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, size = 'standard', height, minHeight, aspect, margin, xLabels = 'auto', grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "size", "height", "minHeight", "aspect", "margin", "xLabels", "grid", "hideLegend", "legendPosition", "slotProps"]);
37
36
  const theme = useTheme();
38
37
  const { i18n } = useTranslation();
39
38
  const neutralPalette = useNeutralChartPalette();
40
- useEffect(() => warnOnHeightMismatch('BarChart', { minHeight, height }), [minHeight, height]);
41
- const { wrapperMinHeight, chartHeight } = resolveChartHeight({ minHeight, height, aspect });
39
+ assertRemovedChartProp('BarChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
40
+ assertRemovedChartProp('BarChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
41
+ assertRemovedChartProp('BarChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
42
42
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'band', data: [] }, theme.typography.caption.fontSize);
43
43
  const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
44
- const sizedYAxis = sizeYAxisForContent(labelledYAxis, series, theme.typography.caption.fontSize);
45
- const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
46
- return (_jsx(Box, { "data-testid": "bar-chart-container", sx: { width: '100%', minHeight: wrapperMinHeight, aspectRatio: aspect }, children: _jsx(MuiBarChart, Object.assign({}, chartProps, { height: chartHeight, series: series, xAxis: rotatedTickSpace.xAxis, yAxis: sizedYAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: withMarginDefaults(rotatedTickSpace.margin), slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
44
+ const layout = resolveChartLayout({
45
+ size,
46
+ height,
47
+ xAxis: labelledXAxis,
48
+ yAxis: labelledYAxis,
49
+ series,
50
+ xLabels,
51
+ hideLegend,
52
+ legendPosition,
53
+ slotProps,
54
+ tickFontSize: theme.typography.caption.fontSize,
55
+ spacing: theme.spacing,
56
+ defaultLineHeight: theme.typography.caption.lineHeight,
57
+ });
58
+ return (_jsx(Box, { "data-testid": "bar-chart-container", sx: layout.sx, children: _jsx(MuiBarChart, Object.assign({}, chartProps, { height: layout.chartHeight, series: series, xAxis: layout.xAxis, yAxis: layout.yAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: layout.margin, slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
47
59
  }
@@ -4,17 +4,35 @@ import { Alert, Box, Button, CircularProgress, Paper, Stack, Typography, } from
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { useTheme } from '@mui/material/styles';
6
6
  import { exportChartPng, exportChartSvg } from './exportChart';
7
- import { warnOnHeightMismatch } from './chartDefaults';
8
7
  export const CHART_FRAME_ROOT_SX = { p: 2 };
9
8
  export const CHART_FRAME_ALERT_SX = { mt: 2 };
9
+ /**
10
+ * UCM-CHART-12: `warnOnHeightMismatch` (the shared helper this note used to call into
11
+ * `chartDefaults`) was deleted along with `resolveChartHeight` -- both belonged to the
12
+ * `{minHeight, height, aspect}` trio the four chart presets used to accept, which that WO
13
+ * replaced with `resolveChartLayout`'s `size`/`height` model. `ChartFrame` keeps its OWN
14
+ * `minHeight`/`height`/`aspect` props unchanged (CHART-9: it is a card floor, not a chart size),
15
+ * so it keeps this note too -- same text, same condition, just no longer routed through the
16
+ * now-deleted shared function.
17
+ */
18
+ function warnOnChartFrameHeightMismatch(minHeight, height) {
19
+ if (process.env.NODE_ENV === 'production')
20
+ return;
21
+ if (minHeight == null || height == null || minHeight === height)
22
+ return;
23
+ // eslint-disable-next-line no-console
24
+ console.warn(`[ui-core-micha] <ChartFrame> received both minHeight={${minHeight}} and height={${height}}; `
25
+ + 'they disagree; height is ignored here and minHeight keeps sizing the wrapper as a floor. '
26
+ + 'Pass only minHeight, or drop height if it was meant to size the whole card.');
27
+ }
10
28
  /**
11
29
  * Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
12
30
  *
13
31
  * `minHeight` (CHART-9): always just a floor on the content box, which holds a whole card --
14
32
  * title, toolbar, chart, legend, footnotes, export links -- not a single chart. `height` is never
15
- * applied to the box (unlike the four chart presets, whose box holds exactly one chart and do use
16
- * `resolveChartHeight`'s `height`/`aspect` resolution); it is only compared against `minHeight` for
17
- * `warnOnHeightMismatch`'s dev-mode note.
33
+ * applied to the box (unlike the four chart presets, whose box holds exactly one chart and use
34
+ * `resolveChartLayout`'s `size`/`height` resolution); it is only compared against `minHeight` for
35
+ * this component's own dev-mode mismatch note.
18
36
  */
19
37
  export function ChartFrame({ title, subtitle, toolbar, controls, loading = false, error = false, isEmpty = false, emptyMessage, minHeight, height, aspect, exportOptions = false, onExportSvg, onExportPng, meta, ariaLabel, children, variant = 'outlined', titleVariant = 'h6', sx, }) {
20
38
  const { t } = useTranslation();
@@ -28,7 +46,7 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
28
46
  const hasCustomAriaLabel = Boolean(ariaLabel);
29
47
  const chartLabel = ariaLabel || title;
30
48
  const callerSx = Array.isArray(sx) ? sx : [sx];
31
- useEffect(() => warnOnHeightMismatch('ChartFrame', { minHeight, height }, { heightWins: false }), [minHeight, height]);
49
+ useEffect(() => warnOnChartFrameHeightMismatch(minHeight, height), [minHeight, height]);
32
50
  const runExport = async (type) => {
33
51
  setExportError(false);
34
52
  try {
@@ -10,13 +10,13 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
- import React, { useEffect } from 'react';
13
+ import React from 'react';
14
14
  import { Box } from '@mui/material';
15
15
  import { useTheme } from '@mui/material/styles';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { LineChart as MuiLineChart, MarkElement as MuiMarkElement, } from '@mui/x-charts/LineChart';
18
18
  import { useNeutralChartPalette } from './palette';
19
- import { DEFAULT_LEGEND_POSITION, defaultNumericTickFormatter, resolveChartHeight, sizeYAxisForContent, spaceForRotatedTicks, warnOnHeightMismatch, withAxisDefaults, withChartSlotDefaults, withGridDefaults, withMarginDefaults, } from './chartDefaults';
19
+ import { DEFAULT_LEGEND_POSITION, assertRemovedChartProp, defaultNumericTickFormatter, resolveChartLayout, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
20
20
  // Fixes MUI's hollow `background.paper`-filled marker (MarkElement.js's own
21
21
  // `styled()` default). CHART-6's WO named a `MuiMarkElement` theme-token
22
22
  // override in src/theme/tokens.js as the preferred mechanism, with a
@@ -41,22 +41,34 @@ function FilledMarkElement(_a) {
41
41
  * (`withAxisDefaults` only ever falls back onto an axis without its own
42
42
  * `label`).
43
43
  *
44
- * `minHeight` (CHART-8): a floor under an `aspect`-derived responsive height when `aspect` is
45
- * set and `height` is not; otherwise (no `height`, no `aspect`) it sizes the chart itself. Once
46
- * `height` is set, `height` sizes the chart and the wrapper never reserves more than that, even
47
- * 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).
48
47
  */
49
48
  export function LineChart(_a) {
50
- var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, height, aspect, grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, slots, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "height", "aspect", "grid", "hideLegend", "legendPosition", "margin", "slots", "slotProps"]);
49
+ var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, size = 'standard', height, minHeight, aspect, margin, xLabels = 'auto', grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, slots, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "size", "height", "minHeight", "aspect", "margin", "xLabels", "grid", "hideLegend", "legendPosition", "slots", "slotProps"]);
51
50
  const theme = useTheme();
52
51
  const { i18n } = useTranslation();
53
52
  const neutralPalette = useNeutralChartPalette();
54
- useEffect(() => warnOnHeightMismatch('LineChart', { minHeight, height }), [minHeight, height]);
55
- const { wrapperMinHeight, chartHeight } = resolveChartHeight({ minHeight, height, aspect });
53
+ assertRemovedChartProp('LineChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
54
+ assertRemovedChartProp('LineChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
55
+ assertRemovedChartProp('LineChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
56
56
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'point', data: [] }, theme.typography.caption.fontSize);
57
57
  const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
58
58
  const defaultedSeries = series.map((item) => (Object.assign({ showMark: false, labelMarkType: 'square' }, item)));
59
- const sizedYAxis = sizeYAxisForContent(labelledYAxis, defaultedSeries, theme.typography.caption.fontSize);
60
- const rotatedTickSpace = spaceForRotatedTicks(labelledXAxis, margin, theme.typography.caption.lineHeight);
61
- return (_jsx(Box, { "data-testid": "line-chart-container", sx: { width: '100%', minHeight: wrapperMinHeight, aspectRatio: aspect }, children: _jsx(MuiLineChart, Object.assign({}, chartProps, { height: chartHeight, series: defaultedSeries, xAxis: rotatedTickSpace.xAxis, yAxis: sizedYAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: withMarginDefaults(rotatedTickSpace.margin), slots: Object.assign({ mark: FilledMarkElement }, slots), slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
59
+ const layout = resolveChartLayout({
60
+ size,
61
+ height,
62
+ xAxis: labelledXAxis,
63
+ yAxis: labelledYAxis,
64
+ series: defaultedSeries,
65
+ xLabels,
66
+ hideLegend,
67
+ legendPosition,
68
+ slotProps,
69
+ tickFontSize: theme.typography.caption.fontSize,
70
+ spacing: theme.spacing,
71
+ defaultLineHeight: theme.typography.caption.lineHeight,
72
+ });
73
+ return (_jsx(Box, { "data-testid": "line-chart-container", sx: layout.sx, children: _jsx(MuiLineChart, Object.assign({}, chartProps, { height: layout.chartHeight, series: defaultedSeries, xAxis: layout.xAxis, yAxis: layout.yAxis, colors: palette || neutralPalette.categorical, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: layout.margin, slots: Object.assign({ mark: FilledMarkElement }, slots), slotProps: withChartSlotDefaults(slotProps, legendPosition) })) }));
62
74
  }
@@ -10,14 +10,14 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- import React, { useEffect, useMemo } from 'react';
13
+ import React, { useMemo } from 'react';
14
14
  import { Box } from '@mui/material';
15
15
  import { useTheme } from '@mui/material/styles';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { ScatterChart as MuiScatterChart } from '@mui/x-charts/ScatterChart';
18
18
  import { useXScale, useYScale } from '@mui/x-charts/hooks';
19
19
  import { useNeutralChartPalette } from './palette';
20
- import { DEFAULT_LEGEND_POSITION, defaultNumericTickFormatter, resolveChartHeight, sizeYAxisForContent, warnOnHeightMismatch, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
20
+ import { DEFAULT_LEGEND_POSITION, assertRemovedChartProp, defaultNumericTickFormatter, resolveChartLayout, withAxisDefaults, withChartSlotDefaults, withGridDefaults, } from './chartDefaults';
21
21
  // Small enough that hram's largest cloud (~300 points, the tested magnitude -- state it, don't
22
22
  // imply it scales past that) doesn't fuse into a solid mass at the default (non-bubble) size.
23
23
  const DEFAULT_MARKER_RADIUS = 5;
@@ -118,10 +118,9 @@ export function ScatterReferenceLine({ from, to, color, dashed, strokeWidth = 1.
118
118
  * Reference geometry (a computed curve, a straight line, either with an optional label) is
119
119
  * composed as `children` using `ScatterReferenceCurve`/`ScatterReferenceLine` above.
120
120
  *
121
- * `minHeight` (CHART-8): a floor under an `aspect`-derived responsive height when `aspect` is
122
- * set and `height` is not; otherwise (no `height`, no `aspect`) it sizes the chart itself. Once
123
- * `height` is set, `height` sizes the chart and the wrapper never reserves more than that, even
124
- * if `minHeight` is also passed and larger.
121
+ * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
122
+ * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
123
+ * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
125
124
  *
126
125
  * Tested magnitude: a few hundred marks (hram's largest cloud is ~300 points). Not verified,
127
126
  * and not implied to hold, at thousands+.
@@ -137,27 +136,36 @@ export function ScatterReferenceLine({ from, to, color, dashed, strokeWidth = 1.
137
136
  */
138
137
  export function ScatterChart(_a) {
139
138
  var _b;
140
- var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, minHeight, height, aspect, grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, margin, sizeAccessor, getPointStyle, markerSize = DEFAULT_MARKER_RADIUS, slotProps, children } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "minHeight", "height", "aspect", "grid", "hideLegend", "legendPosition", "margin", "sizeAccessor", "getPointStyle", "markerSize", "slotProps", "children"]);
139
+ var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, size = 'standard', height, minHeight, aspect, margin, xLabels = 'auto', grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, sizeAccessor, getPointStyle, markerSize = DEFAULT_MARKER_RADIUS, slotProps, children } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "size", "height", "minHeight", "aspect", "margin", "xLabels", "grid", "hideLegend", "legendPosition", "sizeAccessor", "getPointStyle", "markerSize", "slotProps", "children"]);
141
140
  const theme = useTheme();
142
141
  const { i18n } = useTranslation();
143
142
  const neutralPalette = useNeutralChartPalette();
144
- useEffect(() => warnOnHeightMismatch('ScatterChart', { minHeight, height }), [minHeight, height]);
145
- const { wrapperMinHeight, chartHeight } = resolveChartHeight({ minHeight, height, aspect });
143
+ assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
144
+ assertRemovedChartProp('ScatterChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
145
+ assertRemovedChartProp('ScatterChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
146
146
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
147
147
  const labelledYAxis = withAxisDefaults(yAxis, yAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
148
- // Only the Y axis is width-sized (sizeYAxisForContent reserves horizontal space for a
149
- // vertical axis's ticks+label) -- a scatter's numeric X axis needs no analogous treatment
150
- // beyond what MUI already sizes for a horizontal tick row.
151
- //
152
- // sizeYAxisForContent's series-data fallback (used when the caller sets no axis min/max)
153
- // expects `series[i].data` to be a plain array of numbers -- true for BarChart/LineChart, but
154
- // a scatter's data is `{x,y,z,id}` points, so `typeof value === 'number'` never matched and
155
- // the fallback silently found zero candidates (reviewer finding, live-caught: it "looked fine"
156
- // only because the untested case happened to fall back to MUI's flat default at a magnitude
157
- // that didn't visibly overlap). Adapt AT THIS CALL SITE, not inside the shared helper --
158
- // BarChart/LineChart's own contract must not change for this.
148
+ // `resolveChartLayout`'s own y-axis sizing (via `sizeYAxisForContent`) expects `series[i].data`
149
+ // to be a plain array of numbers -- true for BarChart/LineChart, but a scatter's data is
150
+ // `{x,y,z,id}` points. Adapt AT THIS CALL SITE, not inside the shared resolver -- BarChart/
151
+ // LineChart's own contract must not change for this (reviewer finding, live-caught under
152
+ // THEME-10: the untested case silently found zero candidates and fell back to MUI's flat
153
+ // default at a magnitude that didn't visibly overlap).
159
154
  const yValueSeries = useMemo(() => series.map((item) => (Object.assign(Object.assign({}, item), { data: (item.data || []).map((point) => point === null || point === void 0 ? void 0 : point.y) }))), [series]);
160
- const sizedYAxis = sizeYAxisForContent(labelledYAxis, yValueSeries, theme.typography.caption.fontSize);
155
+ const layout = resolveChartLayout({
156
+ size,
157
+ height,
158
+ xAxis: labelledXAxis,
159
+ yAxis: labelledYAxis,
160
+ series: yValueSeries,
161
+ xLabels,
162
+ hideLegend,
163
+ legendPosition,
164
+ slotProps,
165
+ tickFontSize: theme.typography.caption.fontSize,
166
+ spacing: theme.spacing,
167
+ defaultLineHeight: theme.typography.caption.lineHeight,
168
+ });
161
169
  const isNeutral = series.length <= 1 && !((_b = series[0]) === null || _b === void 0 ? void 0 : _b.color);
162
170
  const resolvedPalette = palette || (isNeutral ? [neutralPalette.neutral] : neutralPalette.categorical);
163
171
  // A single chart-wide max, NOT one per series: population (or whatever sizeAccessor reads) is
@@ -181,5 +189,5 @@ export function ScatterChart(_a) {
181
189
  // would resolve every point's style/radius against the wrong (pre-sort) index.
182
190
  const orderedSeriesById = useMemo(() => Object.fromEntries(orderedSeries.map((item) => [item.id, item])), [orderedSeries]);
183
191
  const markerSlot = useMemo(() => makeMarkerSlot(orderedSeriesById, { sizeAccessor, chartMax, getPointStyle }), [orderedSeriesById, sizeAccessor, chartMax, getPointStyle]);
184
- return (_jsx(Box, { "data-testid": "scatter-chart-container", sx: { width: '100%', minHeight: wrapperMinHeight, aspectRatio: aspect }, children: _jsx(MuiScatterChart, Object.assign({}, chartProps, { height: chartHeight, series: orderedSeries.map((item) => (Object.assign({ markerSize }, item))), xAxis: labelledXAxis, yAxis: sizedYAxis, colors: resolvedPalette, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: margin, slots: { marker: markerSlot }, slotProps: withChartSlotDefaults(slotProps, legendPosition), children: children })) }));
192
+ return (_jsx(Box, { "data-testid": "scatter-chart-container", sx: layout.sx, children: _jsx(MuiScatterChart, Object.assign({}, chartProps, { height: layout.chartHeight, series: orderedSeries.map((item) => (Object.assign({ markerSize }, item))), xAxis: layout.xAxis, yAxis: layout.yAxis, colors: resolvedPalette, grid: withGridDefaults(grid), hideLegend: hideLegend, margin: layout.margin, slots: { marker: markerSlot }, slotProps: withChartSlotDefaults(slotProps, legendPosition), children: children })) }));
185
193
  }
@@ -23,6 +23,11 @@ import { useNeutralChartPalette } from './palette';
23
23
  // zero-height chart in the first place (CHART-4). Do not "fix" this back
24
24
  // to a responsive/aspect-ratio height without re-verifying the underlying
25
25
  // MUI measurement bug is actually resolved.
26
+ //
27
+ // UCM-CHART-12: `CHART_HEIGHT` (320) is the number `"standard"` (BarChart's new `size` token) was
28
+ // deliberately pinned to, specifically so this preset's own default did not silently move under
29
+ // this migration -- still used here as ChartFrame's own `minHeight` floor (a card concern,
30
+ // unaffected by CHART-12), and the inner BarChart now takes `size="standard"` instead of `height`.
26
31
  const CHART_HEIGHT = 320;
27
32
  // Dynamic sx factories are outside the registry's documented top-level-object
28
33
  // lower bound; the series colour is data-dependent at render time.
@@ -172,6 +177,6 @@ export function TimeSeriesChart({ title, subtitle, xAxisLabel, yAxisLabel, secon
172
177
  return (_jsx(ChartFrame, { title: title, subtitle: subtitle, toolbar: toolbar, loading: loading, error: error, isEmpty: isDataEmpty, minHeight: CHART_HEIGHT, children: !isDataEmpty && (_jsx(BarChart, { xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xAxis: [{
173
178
  data: (data === null || data === void 0 ? void 0 : data.xLabels) || [],
174
179
  tickLabelInterval: makeTickLabelInterval(((data === null || data === void 0 ? void 0 : data.xLabels) || []).length),
175
- }], yAxis: yAxis, series: visibleSeries.map((series) => (Object.assign({ data: series.data, label: series.label }, (hasSecondaryAxis && series.axis === 'secondary' ? { yAxisId: 'secondary' } : {})))), palette: visiblePalette, height: CHART_HEIGHT, hideLegend: seriesConfig.length > 0, skipAnimation: skipAnimation })) }));
180
+ }], yAxis: yAxis, series: visibleSeries.map((series) => (Object.assign({ data: series.data, label: series.label }, (hasSecondaryAxis && series.axis === 'secondary' ? { yAxisId: 'secondary' } : {})))), palette: visiblePalette, size: "standard", hideLegend: seriesConfig.length > 0, skipAnimation: skipAnimation })) }));
176
181
  }
177
182
  export default TimeSeriesChart;