@micha.bigler/ui-core-micha 2.40.0 → 2.41.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.
@@ -0,0 +1,208 @@
1
+ # THEME-11 — The kit has no opinion on chart margins, so every chart in the estate inherits MUI's
2
+
3
+ ## Part A — Envelope
4
+
5
+ **Goal.** Give the chart presets a margin default of their own, so charts stop falling back to
6
+ MUI X's `DEFAULT_MARGINS` of 20 px on all four sides, and trim the one comparable block in
7
+ `ChartFrame`. The whitespace around plots shrinks everywhere at once, without each consuming
8
+ panel having to hand-tune a `margin` prop.
9
+
10
+ **Why — measured, not estimated.** hram's four KPI cards were taken apart pixel by pixel. The
11
+ chart is 270 px tall and its plot area is 202 px; the remaining 167 px is furniture, and the
12
+ budget accounts for the card's observed ~370 px exactly:
13
+
14
+ | Block | px | Owner |
15
+ |---|---|---|
16
+ | `ChartFrame` root padding (`p: 2`) top | 16 | this package |
17
+ | Title text | ~24 | this package |
18
+ | Title box `mb: 2` | 16 | this package |
19
+ | `margin.top` | 20 | **MUI default** |
20
+ | **Plot area** | **202** | — |
21
+ | x-axis height (`DEFAULT_AXIS_SIZE_HEIGHT`) | 25 | MUI default |
22
+ | `margin.bottom` | 20 | **MUI default** |
23
+ | Forced blank (`minHeight 300` over `height 270`) | 30 | hram — its own `FIX-16` |
24
+
25
+ The forced blank is **centred, not stacked at the bottom**: `ChartFrame`'s content box is
26
+ `display: flex` with `alignItems: center`, so a `minHeight` above the chart's own height splits
27
+ evenly — 15 px above the chart and 15 px below. It is listed once here for the total.
28
+ | `ChartFrame` root padding bottom | 16 | this package |
29
+
30
+ **The MUI defaults apply because this package never forms an opinion.** `spaceForRotatedTicks`
31
+ returns the caller's `margin` untouched unless the x-ticks are rotated *and* the caller left
32
+ `bottom` unset — so for the ordinary case it hands `margin: undefined` straight to
33
+ `MuiBarChart`, and `DEFAULT_MARGINS = { top: 20, bottom: 20, left: 20, right: 20 }` takes over.
34
+ THEME-9 gave the kit axis *sizing*; margins were never in its scope. Every chart in every
35
+ consuming app has been paying those 20 px since.
36
+
37
+ **What each side is actually for**, which is what makes the trim safe rather than a guess:
38
+ - `margin.left` sits **outside** the y-axis width — this package's own comment records
39
+ `drawingArea.left = margin.left + axisSizeLeft`. The tick labels live in `axisSizeLeft`, so
40
+ `margin.left` is pure padding.
41
+ - `margin.bottom` sits **below** the x-axis block; `DEFAULT_AXIS_SIZE_HEIGHT` already contains
42
+ the tick label.
43
+ - `margin.top` is the only one carrying real content: the topmost y tick label is centred on its
44
+ gridline and overhangs the plot by roughly half a caption line-height (~9 px). This is why the
45
+ top cannot simply be trimmed to match the others.
46
+ - `margin.right` is the risky one — see the scope note.
47
+
48
+ **Scope.**
49
+
50
+ 1. **A margin default in the shared chart path**, applied when the caller passes no margin and
51
+ merged per-side (not wholesale) when the caller passes a partial one, so an explicit
52
+ `{ left: 60 }` does not silently reinstate 20 px on the other three sides:
53
+ - `top: 10` — leaves the ~9 px overhang of the topmost tick label its room. **Not 8**: at 8
54
+ that label sits on the edge.
55
+ - `bottom: 8`
56
+ - `left: 8`
57
+ - `right: 16` — **deliberately a smaller trim than the other sides.** On a band x-axis the
58
+ tick labels are centred and 8 would be fine, but on a **linear** x-axis the last tick label
59
+ sits *at* the edge and overhangs by half its width (~13 px for a four-character label at
60
+ caption size). 16 is the value that survives both. Do **not** make this conditional on
61
+ scale type: that is inferred behaviour serving no named consumer, and a chart that genuinely
62
+ needs more can pass `margin` explicitly.
63
+ 2. **`ChartFrame`'s title box `mb: 2` → `mb: 1`** (16 → 8 px).
64
+ 3. Apply consistently across the presets that share the path (`BarChart`, `LineChart`,
65
+ `TimeSeriesChart`), so consumers do not have to know which chart type trims what.
66
+
67
+ **Non-goals / do-not-touch.**
68
+ - **`CHART_FRAME_ROOT_SX = { p: 2 }` stays.** Trimming it would help the left and right edges
69
+ too, but `ChartFrame` wraps non-chart content as well, and its padding is what makes a panel
70
+ read as a card. That is a separate proposal with a different blast radius, not a rider here.
71
+ - No change to axis sizing, tick formatting or label placement — THEME-9 owns those and they
72
+ are working.
73
+ - No new props. This is a default, not a feature.
74
+
75
+ **Tier 3** — shared-core. `reviewer` mandatory; `ui_reviewer` too, the deliverable being visual.
76
+
77
+ **Two traps this WO must not walk into.**
78
+
79
+ 1. **`spaceForRotatedTicks` decides "did the caller set bottom?" by `margin?.bottom != null`.**
80
+ If a package-level default margin object is injected *before* that check, every rotated-tick
81
+ chart sees it as caller-set and loses its extra bottom space — hram's `RatioMetricsPanel`
82
+ runs x-labels at −45° and −90° and would clip them. The default must be applied so that
83
+ `spaceForRotatedTicks` still distinguishes a genuine caller margin from the package default,
84
+ or the rotated path must be computed first and the default merged under it.
85
+ 2. **`MUI_CHART_MARGIN_BOTTOM = 20` in `chartDefaults.js` is a hard-coded mirror of MUI's
86
+ default**, used as the base when the rotated path computes extra bottom space. It must move
87
+ with the new default, or rotated charts get their extra height added to the wrong base.
88
+
89
+ **Tests to write.** Narrow, but these three are the ones that catch a real break:
90
+ - A rotated-tick chart still receives its enlarged bottom margin (trap 1 — the regression this
91
+ change is most likely to cause).
92
+ - A caller-supplied partial margin keeps its own value on the side it set and gets the new
93
+ default on the others (the merge semantics in scope 1).
94
+ - A chart with a linear x-axis does not clip its last tick label at the new right margin.
95
+
96
+ **Risks.**
97
+ - **The blast radius is every chart in every consuming app**, and it lands the moment each app
98
+ bumps its pin — not at publish. A single wrong side value is therefore an estate-wide visual
99
+ regression rather than a local one. That is also the point of doing it here rather than in
100
+ each panel, but it raises the bar on the rendered check.
101
+ - **Panels that already hand-tune `margin`** (hram's `RatioMetricsPanel` passes
102
+ `{ left: 60, right: 8, top: 20, bottom: … }`) must be unaffected on the sides they set. This
103
+ is what scope 1's per-side merge is for; whole-object replacement would break them.
104
+ - The numbers above are derived from a caption-sized tick label. A consuming app with a
105
+ markedly larger chart font would need more top margin. Do not build for that — no consumer
106
+ has it — but state the assumption in the docs.
107
+
108
+ **Delivery is not done at publish.** hram is the consumer that reported this, and its pins for
109
+ THEME-8, THEME-9 and THEME-10 are all already open. Take this one in the same bump rather than
110
+ adding a fourth pending hop.
111
+
112
+ ## Part B — Implementation map — ADDRESSED TO THE IMPLEMENTER
113
+
114
+ Codex not dispatched: `.claude/codex-status.md` already carried a same-day `unavailable` line —
115
+ implemented directly in Claude, authorship flip, `reviewer` + `ui_reviewer` mandatory (already
116
+ required at Tier 3).
117
+
118
+ ### What landed
119
+
120
+ - `chartDefaults.js`: `PACKAGE_DEFAULT_MARGIN = { top: 10, bottom: 8, left: 8, right: 16 }`
121
+ (exported) and `withMarginDefaults(margin)` (per-side merge, number-shorthand expansion).
122
+ `MUI_CHART_MARGIN_BOTTOM` moved `20 → 8` to match the new baseline (trap 2).
123
+ - `BarChart.jsx`/`LineChart.jsx`: `spaceForRotatedTicks` still runs FIRST, on the caller's
124
+ UNMODIFIED `margin` — its own `callerSetBottom` check (`margin?.bottom != null`) therefore
125
+ still tells a genuine caller margin from "nothing set" correctly (trap 1 avoided by ordering,
126
+ not by threading a flag through the shared helper). `withMarginDefaults` is applied AFTER,
127
+ wrapping `rotatedTickSpace.margin` on the way into `MuiBarChart`/`MuiLineChart` — so a
128
+ rotation-computed `bottom` (already present on that object) is preserved by the merge, and
129
+ only genuinely-unset sides get the package default.
130
+ - `TimeSeriesChart.jsx`: no change needed — it never sets its own `margin`, so it inherits
131
+ `BarChart`'s new default automatically through composition.
132
+ - `ChartFrame.jsx`: title row `mb: 2 → mb: 1`. Controls row `mb: 2` left untouched (not named).
133
+ **Deliberately unconditional** (`ui_reviewer` U3 asked whether this should be scoped to only
134
+ chart-preset consumers, matching the margin trim's scope): `ChartFrame` wraps non-chart
135
+ content too, and scope item 2 names the title box specifically, with no "only when a chart
136
+ preset is the child" qualifier anywhere in Part A — a scatter-backed or bespoke-SVG panel
137
+ gets the tighter title spacing but keeps whatever margin its own content uses, which is
138
+ correct: the title row and the plot margin are two independent pieces of furniture, not one
139
+ setting that should move together.
140
+
141
+ ### Scope gap surfaced, not silently folded in
142
+
143
+ `ScatterChart` (THEME-10, same day, not yet published) passes its `margin` prop straight
144
+ through with no default handling at all — it would keep MUI's flat 20px default while
145
+ `BarChart`/`LineChart`/`TimeSeriesChart` get 8/10/16. THEME-11's own scope item 3 names exactly
146
+ three presets, not four, and `ScatterChart` didn't exist when this WO's Part A was authored.
147
+ Flagging this explicitly rather than extending scope silently: `ScatterChart` should likely get
148
+ `withMarginDefaults` too, in a follow-up (or when `ScatterChart`'s own version bumps). **This is
149
+ NOT the same seam as the right-margin note below** — `ScatterChart`'s X axis is always linear by
150
+ construction (THEME-10 scope), so if/when it does adopt `withMarginDefaults`, its own rendered
151
+ check needs to re-examine the right-margin question specifically, not inherit this WO's
152
+ "no consumer has it" conclusion by assumption.
153
+
154
+ ### Verification detail beyond the WO's own risk note — corrected after review
155
+
156
+ The rendered check found that a wide, locale-grouped tick value (`"1,000"`, 6 characters)
157
+ leaves only ~1px of clearance at the new `right: 16` before MUI's own internal `shortenLabels`
158
+ truncates it to `"1,0…"` — never actual overflow-clipping past the SVG boundary in either the
159
+ narrow or wide case. Two corrections to the original note, from `reviewer`/`ui_reviewer`:
160
+
161
+ - **The grouped formatting is MUI's OWN default tick rendering, not THEME-9's
162
+ `defaultNumericTickFormatter`** (`reviewer`/`ui_reviewer` finding, independently confirmed:
163
+ neither `BarChart.jsx` nor `LineChart.jsx` wires a `valueFormatter` onto the x-axis — only the
164
+ y-axis gets THEME-9's default — and the dev fixture that produced `"1,000"` set none either).
165
+ This means the tight case is not a THEME-9-introduced interaction; it is how MUI itself has
166
+ always formatted a linear axis's ticks, now simply more VISIBLE because `right` shrank from
167
+ 20 to 16.
168
+ - **No real consumer hits this today** (`reviewer`, verified by grepping every hram panel that
169
+ actually consumes `BarChart`/`LineChart`/`TimeSeriesChart`: `CostMetricsPanel`,
170
+ `RatioMetricsPanel`, `AccessibilityPanel`, `TotalMetricsPanel`, `ReferralMetricsPanel`,
171
+ `GroupMetricsPanel`, `GenericAnalysisPlot` — every one uses `scaleType: 'band'`, none
172
+ `'linear'`. `AllocationPerformancePanel`, the panel this note originally worried about,
173
+ imports only `ChartFrame` from this package and draws its own bespoke SVG axes — it does not
174
+ go through `BarChart`/`LineChart` at all, so it is unaffected by this WO regardless.) The WO's
175
+ own explicit "do not build for a case no consumer has; state the assumption" therefore applies
176
+ cleanly, not just plausibly. The residual risk (`ui_reviewer`) is real but one hop removed: the
177
+ first FUTURE consumer that renders a linear x-axis through `BarChart`/`LineChart` with wide
178
+ values should re-check this margin before shipping, and there is no code-level guard that would
179
+ catch it automatically — recorded here so that check doesn't get skipped.
180
+
181
+ ### Target repo
182
+
183
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha`
184
+
185
+ ## Part C — Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
186
+
187
+ > **If you are the implementer reading this work order as your own specification: STOP at this
188
+ > line.**
189
+
190
+ - **Execution.** Codex known-unavailable today — implemented directly in Claude.
191
+ - **Review routing.** `reviewer` + `ui_reviewer`, concurrent, one background batch.
192
+ - **Verification.** Package suite: `chartDefaults.test.js` gained a dedicated
193
+ `withMarginDefaults` describe block (pure-function contract) plus integration tests against
194
+ `BarChart`/`LineChart` (package default applied, rotated-tick chart still gets its enlarged
195
+ bottom margin — not the flat package default, partial-margin per-side merge, full-margin
196
+ passthrough, right > left/bottom by construction); full affected set (`BarChart.test.jsx`,
197
+ `LineChart.test.jsx`, `TimeSeriesChart.test.jsx`, `timeSeriesLegend.test.js`,
198
+ `ScatterChart.test.jsx`, `chartsPalette.test.js`) — 91 tests, green.
199
+ **Rendered check, done live** in the package's own dev harness: (1) the existing "Caller
200
+ overrides" `LineChart` fixture (−25° rotated ticks, no explicit margin — exactly trap 1's
201
+ shape) measured with NO overlap between the rotated tick labels and the axis title (15px
202
+ clear gap) — trap 1 confirmed avoided; (2) a new `LineChart (linear x-axis)` fixture added
203
+ and measured at two magnitudes — narrow (`"100"`) with 5px clearance from the SVG edge, wide
204
+ locale-grouped (`"1,000"`) with MUI's own truncation kicking in at ~1px clearance but never
205
+ actual clipping past the boundary (see the "beyond the WO's risk note" section above).
206
+ - **Register & commit.** Advance the THEME-11 row with the reviewer verdicts. Coordinate the
207
+ consumer pin bump with THEME-8, THEME-9 and THEME-10 — all still open at publish time,
208
+ pending this WO's own reviews and a bundled version bump.