@micha.bigler/ui-core-micha 2.42.2 → 3.0.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/WORK_ORDERS.md +3 -1
- package/dev/entries.jsx +6 -0
- package/dist/components/charts/BarChart.js +23 -12
- package/dist/components/charts/ChartFrame.js +23 -5
- package/dist/components/charts/LineChart.js +23 -12
- package/dist/components/charts/ScatterChart.js +29 -22
- package/dist/components/charts/TimeSeriesChart.js +6 -1
- package/dist/components/charts/chartDefaults.js +290 -110
- package/docs/CHART-LAYOUT.md +129 -0
- package/package.json +1 -1
- package/src/components/charts/BarChart.jsx +30 -28
- package/src/components/charts/ChartFrame.jsx +24 -5
- package/src/components/charts/LineChart.jsx +31 -29
- package/src/components/charts/ScatterChart.jsx +37 -29
- package/src/components/charts/TimeSeriesChart.jsx +6 -1
- package/src/components/charts/chartDefaults.js +320 -111
- package/tests/BarChart.test.jsx +27 -39
- package/tests/LineChart.test.jsx +21 -24
- package/tests/ScatterChart.test.jsx +21 -24
- package/tests/chartDefaults.test.js +313 -186
- package/work-orders/UCM-CHART-11.md +132 -0
- package/work-orders/UCM-CHART-12.md +348 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# UCM-CHART-11 — The rotated-tick allowance is now counted once, but estimated too high
|
|
2
|
+
|
|
3
|
+
- **Repo:** `ui-core-micha`, branch `main` (shared-core; publishes from `main`, no staging step)
|
|
4
|
+
- **Tier:** 3 — shared-core; every consumer's chart proportions change.
|
|
5
|
+
- **Status:** planned
|
|
6
|
+
- **Workstream:** `CHART-*`
|
|
7
|
+
- **Files:** `src/components/charts/chartDefaults.js` — `rotatedTickMetrics`, `longestFormattedTick`,
|
|
8
|
+
`AVERAGE_GLYPH_WIDTH_EM` — and its tests. The presets only call the helper; leave them alone.
|
|
9
|
+
|
|
10
|
+
> **Why — measured in hram on 2026-08-22, ucm 2.42.2 installed via the pin, Teaching → Basic Plots,
|
|
11
|
+
> real data, 1280 px.** `UCM-CHART-10` removed the double reservation and that holds. What is left is
|
|
12
|
+
> a **single reservation that is too large**:
|
|
13
|
+
>
|
|
14
|
+
> | card | longest x label | `axis.height` reserves | rendered tick band | unused | axis title? |
|
|
15
|
+
> |---|---|---|---|---|---|
|
|
16
|
+
> | Ward Metrics | `Msolwa Station` (14 ch, full) | 131 px | **79 px** | **52 px** | no |
|
|
17
|
+
> | Access Ladder | `Msolwa Station` (14 ch, full) | 131 px | **79 px** | **52 px** | no |
|
|
18
|
+
> | Division Metrics | `mang-ula` (8 ch) | 97 px | **56 px** | **41 px** | no |
|
|
19
|
+
> | Cost | `Ching'a…` (**MUI-truncated**) | 68 px | **53 px** | 15 px | no |
|
|
20
|
+
>
|
|
21
|
+
> All four are `-45°`, all four are the same preset. **The unused band scales with the estimate, not
|
|
22
|
+
> with the rendered text.** `rotatedTickMetrics` derives `extraHeight` from
|
|
23
|
+
> `longestFormattedTick × AVERAGE_GLYPH_WIDTH_EM (0.6) × sin(angle)` — a character-count estimate of
|
|
24
|
+
> how wide the longest label *would* be. The rendered text is consistently shorter than that estimate,
|
|
25
|
+
> and the surplus becomes an empty band inside the axis.
|
|
26
|
+
>
|
|
27
|
+
> **Cost is not the well-behaved case — it is the case where the estimate happens to be small.** MUI
|
|
28
|
+
> truncates its labels itself (`Ching'a…`), so the estimator sees a short string. Nothing about Cost
|
|
29
|
+
> is configured better; it just has less to overestimate.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Part A — Envelope (authoritative WHAT/WHY)
|
|
34
|
+
|
|
35
|
+
### Goal
|
|
36
|
+
|
|
37
|
+
The rotated-tick allowance reflects the text that is actually rendered. A chart with long rotated
|
|
38
|
+
labels reserves what those labels occupy — not what a character-count estimate predicts.
|
|
39
|
+
|
|
40
|
+
### Definition of Done
|
|
41
|
+
|
|
42
|
+
- [x] **`extraHeight` is derived from measured text extent, not from character count.** Chose a
|
|
43
|
+
transient, invisible, shared SVG `<text>` node (`measureTickTextWidthPx`), not canvas: it is
|
|
44
|
+
the same element type MUI's own axis ticks render as, so it inherits font metrics via the
|
|
45
|
+
page's own cascade with no extra plumbing. Verified live against hram: a hidden node measuring
|
|
46
|
+
"Msolwa Station" at 13px returned 91.203125, and the REAL rendered tick's own
|
|
47
|
+
`getComputedTextLength()` for the same string was 91.203125 — exact match. `AVERAGE_GLYPH_WIDTH_EM`
|
|
48
|
+
stays (still used by the unrelated `sizeYAxisForContent`, Y-axis width, THEME-9) but is no
|
|
49
|
+
longer read by the rotated-tick path; a new per-character-width table
|
|
50
|
+
(`estimateTextWidthPx`) is the explicitly-declared ESTIMATE fallback for when no DOM is
|
|
51
|
+
available (confirmed live: jsdom's `getComputedTextLength` throws "not a function", so the
|
|
52
|
+
fallback is what the unit tests actually exercise; the real measurement only runs in a browser).
|
|
53
|
+
`measureTextWidth` is also injectable (`spaceForRotatedTicks`'s new optional 4th param) so
|
|
54
|
+
tests stub it for determinism instead of depending on either environment.
|
|
55
|
+
- [x] **MUI's own truncation is accounted for.** The estimate is always taken from
|
|
56
|
+
`longestFormattedTick`'s OUTPUT (i.e. whatever `axis.valueFormatter` produces), never the raw
|
|
57
|
+
`axis.data` value — already true structurally before this WO, confirmed and locked in by a new
|
|
58
|
+
test (`measures the formatted (already-truncated) tick text...`) using a formatter that
|
|
59
|
+
truncates. Cost's own axis sets an explicit `height: 60` (bypasses this helper entirely,
|
|
60
|
+
confirmed by reading `CostMetricsPanel.jsx`), so it was never affected either way.
|
|
61
|
+
- [x] **Numeric acceptance, on the same four live cards**, Teaching → Basic Plots, 1280 px, real data:
|
|
62
|
+
Ward Metrics 129.6 px → **14.6 px**; Access Ladder 156.9 px → **14.6 px**; Division Metrics
|
|
63
|
+
85.2 px → **14.2 px**; Cost 92.3 px → **14.6 px** (unchanged, as expected — its axis.height is
|
|
64
|
+
caller-fixed). All four land under the ~15 px target. Getting there needed correcting not just
|
|
65
|
+
the width term but how it combines with the `MUI_LABELLED_X_AXIS_HEIGHT` baseline — see the
|
|
66
|
+
code comment on `ROTATED_TICK_CLEARANCE_PX` for why an accurate width alone left ~38 px
|
|
67
|
+
unused, verified by direct DOM inspection before adding the second half of the fix.
|
|
68
|
+
- [x] **Nothing is clipped, at any width.** Checked at 1280 px and 375 px on all four cards: 0 of the
|
|
69
|
+
35 (Ward Metrics/Access Ladder/Cost) or 5 (Division Metrics) x-axis tick labels exceeded their
|
|
70
|
+
SVG's bottom edge at either width.
|
|
71
|
+
- [x] **Angle 0 stays byte-identical.** `UCM-CHART-10`'s own byte-identical-by-reference test for
|
|
72
|
+
this is unchanged and still green (137/137 total, no rewrite needed).
|
|
73
|
+
|
|
74
|
+
### One trap, and I walked into it myself
|
|
75
|
+
|
|
76
|
+
**An axis *title* also sits below the lowest tick label.** In the same measurement pass, `Expert
|
|
77
|
+
Mode: Simulation Trajectory` showed 53 px below its lowest tick — and every pixel of it is the
|
|
78
|
+
x-axis title (`hasAxisTitle: true`, angle 0). I first read that as waste. It is not.
|
|
79
|
+
|
|
80
|
+
The four cards in the table above all have **`hasAxisTitle: false`**, which is why their residual is
|
|
81
|
+
genuinely unused. **Any measurement used to validate this fix must separate the axis title from the
|
|
82
|
+
empty band**, or a "fix" will reclaim space the title needs and clip it.
|
|
83
|
+
|
|
84
|
+
### Non-goals
|
|
85
|
+
|
|
86
|
+
- Do **not** revert `UCM-CHART-10`. Its single-reservation change is correct and verified; this is
|
|
87
|
+
the size of that single reservation, not whether there are two.
|
|
88
|
+
- Do not touch `ScatterChart` — it never calls this helper (established under `UCM-CHART-10`).
|
|
89
|
+
- Do not shrink `MUI_LABELLED_X_AXIS_HEIGHT`. That constant is the un-rotated baseline and is not
|
|
90
|
+
where the error is.
|
|
91
|
+
- Do not "fix" this by forcing truncation on every consumer. Ward names rendering in full is correct
|
|
92
|
+
behaviour; the bug is that the reservation does not match them.
|
|
93
|
+
|
|
94
|
+
### Risks
|
|
95
|
+
|
|
96
|
+
- **Every consumer's chart proportions change again**, the third time in this series. Plots get
|
|
97
|
+
taller. That is the intent, and the numeric acceptance above is what proves it landed.
|
|
98
|
+
- Text measurement is environment-sensitive: a canvas or hidden-node measurement must run with the
|
|
99
|
+
same font that will render, or it trades one estimate for another. The font is loaded from the
|
|
100
|
+
consumer's theme, not from this package — check what is available at call time.
|
|
101
|
+
- If measurement is not reliably available at that point in the render, an *improved* estimate
|
|
102
|
+
(per-character widths rather than a flat 0.6 em) is an acceptable fallback — but say so explicitly
|
|
103
|
+
rather than leaving it looking like a measurement.
|
|
104
|
+
|
|
105
|
+
### Tests to WRITE — narrow
|
|
106
|
+
|
|
107
|
+
- A rotated axis whose longest label is long: the reserved height matches the measured extent within
|
|
108
|
+
a stated tolerance, asserted against a stubbed measurement so the test is deterministic.
|
|
109
|
+
- A rotated axis whose labels MUI truncates: the reservation follows the **truncated** string.
|
|
110
|
+
- Angle 0: identical to input, by reference — `UCM-CHART-10`'s existing assertion, unchanged.
|
|
111
|
+
- A caller-set `margin.bottom`: still untouched (`UCM-CHART-10`'s guarantee, unchanged).
|
|
112
|
+
|
|
113
|
+
**Rendered check before publish — numeric, on hram with real data**, at 1280 px, the four cards
|
|
114
|
+
above, before and after, with the axis-title separation described in the trap section. Declare a DOM
|
|
115
|
+
measurement as a substitution if capture is unavailable.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Part B — Implementation map
|
|
120
|
+
|
|
121
|
+
> **TO BE FILLED BY THE ORCHESTRATOR** on `git pull`. Must carry `rotatedTickMetrics`,
|
|
122
|
+
> `longestFormattedTick`, `AVERAGE_GLYPH_WIDTH_EM` and `spaceForRotatedTicks` with line anchors, the
|
|
123
|
+
> absolute working directory, the progress contract, and the preamble. **Do not dispatch while this
|
|
124
|
+
> placeholder stands.**
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Part C — Orchestrator-only
|
|
129
|
+
|
|
130
|
+
> **TO BE FILLED BY THE ORCHESTRATOR.** Review routing (`reviewer` + `ui_reviewer`, full context —
|
|
131
|
+
> Tier 3 shared-core), the numeric rendered check as a hard commit gate, register maintenance,
|
|
132
|
+
> registry publish verification, commit, and the execution directive with its self-address guard.
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# UCM-CHART-12 — One layout model for charts. Breaking, and the end of this series.
|
|
2
|
+
|
|
3
|
+
- **Repo:** `ui-core-micha`, branch `main` (shared-core; publishes from `main`, no staging step)
|
|
4
|
+
- **Tier:** 3 — shared-core, **BREAKING**: major version, `3.0.0`.
|
|
5
|
+
- **Status:** planned
|
|
6
|
+
- **Workstream:** `CHART-*`
|
|
7
|
+
- **Files:** `src/components/charts/chartDefaults.js` (the new resolver; `resolveChartHeight`,
|
|
8
|
+
`spaceForRotatedTicks`, `warnOnHeightMismatch` are **deleted**), the four presets, `ChartFrame.jsx`,
|
|
9
|
+
their tests, and a new `docs/CHART-LAYOUT.md` migration guide.
|
|
10
|
+
- **Consumers are NOT migrated here.** Operator decision 2026-08-22: ucm first, apps one at a time,
|
|
11
|
+
each its own WO gated on its pin bump.
|
|
12
|
+
|
|
13
|
+
> **Why — four work orders is the evidence, not the anecdote.**
|
|
14
|
+
>
|
|
15
|
+
> | WO | what it corrected |
|
|
16
|
+
> |---|---|
|
|
17
|
+
> | `UCM-CHART-8` | `minHeight` vs `height` — wrapper reserved space the chart did not use |
|
|
18
|
+
> | `UCM-CHART-9` | `ChartFrame` given a fixed `height` it must never have |
|
|
19
|
+
> | `UCM-CHART-10` | rotated-tick allowance added to **both** `axis.height` and `margin.bottom` |
|
|
20
|
+
> | `UCM-CHART-11` | that same allowance **estimated** above the rendered text |
|
|
21
|
+
>
|
|
22
|
+
> Each fixed exactly one term of an equation nobody had written down. A fifth is already visible
|
|
23
|
+
> (`ScatterChart`'s x-axis band stays reserved when the tick labels are empty — hram's
|
|
24
|
+
> `AccessGapScatterPanel` shows 58 px of blank inside the SVG where the labels should be), and the
|
|
25
|
+
> operator has called a stop: **no more term-by-term corrections.**
|
|
26
|
+
>
|
|
27
|
+
> **The root cause is structural: no single place owns a chart's composition.** Five interacting
|
|
28
|
+
> knobs — `minHeight`, `height`, `aspect`, `margin`, `xAxisAngle` — are assembled differently by each
|
|
29
|
+
> preset, and consumers add pixel arithmetic of their own on top (`margin.bottom: 58` in
|
|
30
|
+
> `AccessGapScatterPanel`, `margin.left: 220` in hram's tornado). Every one of those is the same
|
|
31
|
+
> disease: **space reserved in one place and again in another, or reserved as a constant where the
|
|
32
|
+
> content is variable.**
|
|
33
|
+
>
|
|
34
|
+
> **Measured consumer surface, 2026-08-22** — this is why breaking is cheaper than continuing:
|
|
35
|
+
>
|
|
36
|
+
> | app | chart files | of which use `minHeight` |
|
|
37
|
+
> |---|---|---|
|
|
38
|
+
> | hram | 29 | 12 |
|
|
39
|
+
> | fitness-monitor | 5 | 2 |
|
|
40
|
+
> | cockpit / jg-ferien / spesix | 4 | 0 |
|
|
41
|
+
> | **total** | **38** | **14** |
|
|
42
|
+
>
|
|
43
|
+
> 76 % sits in one app. This is a sweep, not a migration programme.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Part A — Envelope (authoritative WHAT/WHY)
|
|
48
|
+
|
|
49
|
+
### Goal
|
|
50
|
+
|
|
51
|
+
One function owns a chart's geometry. Every reserved band is derived from the content it holds and
|
|
52
|
+
collapses to zero when that content is empty. Consumers express intent, never pixels. And a test
|
|
53
|
+
asserts the composition adds up, so this class of defect cannot recur.
|
|
54
|
+
|
|
55
|
+
### The model — three rules
|
|
56
|
+
|
|
57
|
+
**Rule 1 — one size source, no interaction.**
|
|
58
|
+
A chart's height comes from a **named size token**: `size="compact" | "standard" | "tall"`, resolved
|
|
59
|
+
through the theme's spacing scale. `height` in pixels survives as a **documented escape** for the
|
|
60
|
+
justified special case, and the JSDoc says it requires a reason.
|
|
61
|
+
- **`minHeight` is removed from all four presets.** A floor is a *card* concern, not a chart one.
|
|
62
|
+
- **`aspect` is removed entirely** (operator decision 2026-08-22). One size source, system-wide.
|
|
63
|
+
- **`resolveChartHeight` is therefore deleted, not amended** — with `minHeight` and `aspect` both
|
|
64
|
+
gone, its three-way resolution has nothing left to resolve. Do not preserve it out of caution.
|
|
65
|
+
|
|
66
|
+
**Rule 2 — every band is its own measured content, or zero.**
|
|
67
|
+
Height decomposes into `plot + xAxisBand + xTitleBand + legendBand`; width into
|
|
68
|
+
`yAxisBand + plot + rightPad`. Each band is sized from what it actually contains, measured in **one**
|
|
69
|
+
place. **An empty band is zero.** That single sentence is the fifth defect's fix: reserved space for
|
|
70
|
+
tick labels that turn out empty must collapse, not persist.
|
|
71
|
+
- No band is ever a constant.
|
|
72
|
+
- No term appears in two addends. That is what `UCM-CHART-10` was.
|
|
73
|
+
|
|
74
|
+
**Rule 3 — consumers pass intent, never pixels.**
|
|
75
|
+
- **No `margin` prop on the presets.** The model owns margins completely. Spacing between a chart and
|
|
76
|
+
its neighbours is the card's job.
|
|
77
|
+
- **`xAxisAngle` is replaced by `xLabels: "auto" | "horizontal" | "angled"`.** Under `"auto"` the
|
|
78
|
+
model decides from the *measured* labels and the available width whether rotation is needed. A
|
|
79
|
+
consumer no longer states a rotation and separately pays for its geometry.
|
|
80
|
+
|
|
81
|
+
### The invariant — the actual deliverable
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
chartHeight === plotHeight + xAxisBand + xTitleBand + legendBand
|
|
85
|
+
chartWidth === yAxisBand + plotWidth + rightPad
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Asserted as a test, per preset, over a matrix of label loads: none, short, long, long-and-many,
|
|
89
|
+
empty-strings.
|
|
90
|
+
|
|
91
|
+
**`UCM-CHART-8`, `-10` and `-11` would each have failed this on day one.** The width line catches the
|
|
92
|
+
two findings still open elsewhere: a scatter y-axis reserving 72 px for a 33 px label, and the
|
|
93
|
+
tornado's 220 px gutter. **This assertion, not the fix, is what ends the series** — and it is the one
|
|
94
|
+
thing in this WO that must not be dropped for expedience.
|
|
95
|
+
|
|
96
|
+
### Definition of Done
|
|
97
|
+
|
|
98
|
+
- [ ] `resolveChartLayout({ size | height, xLabels, xTitle, yTitle, legend, ticks, measureText })`
|
|
99
|
+
returns the **complete** geometry — wrapper `sx`, chart height, margins, and both axis sizes.
|
|
100
|
+
Nothing outside it does chart arithmetic.
|
|
101
|
+
- [ ] `size` tokens resolve through the theme spacing scale; `height` documented as the escape.
|
|
102
|
+
- [ ] `minHeight`, `aspect` and `margin` are gone from the four presets. Passing one is a **dev-mode
|
|
103
|
+
error naming the replacement**, not a silent ignore — 38 call sites will hit this and the
|
|
104
|
+
message is the migration guide.
|
|
105
|
+
- [ ] `ChartFrame` **keeps** its `minHeight` as a card floor. `UCM-CHART-9` established that a frame
|
|
106
|
+
is not a chart; do not sweep the frame's floor away with the presets' prop.
|
|
107
|
+
- [ ] `resolveChartHeight`, `spaceForRotatedTicks`, `warnOnHeightMismatch` deleted.
|
|
108
|
+
- [ ] Both invariants asserted, over the label matrix above.
|
|
109
|
+
- [ ] **Empty tick labels produce a zero-height axis band**, tested explicitly. This is the
|
|
110
|
+
Access-scatter case and it is the reason the model exists.
|
|
111
|
+
- [ ] `docs/CHART-LAYOUT.md`: the model in one page, the removed props with their replacements, and a
|
|
112
|
+
before/after per prop. Five apps have to read this.
|
|
113
|
+
|
|
114
|
+
### Non-goals
|
|
115
|
+
|
|
116
|
+
- No consumer migration here. Five per-app WOs follow, each gated on its pin bump.
|
|
117
|
+
- **No compatibility shim** (operator decision): two models coexisting is the ambiguity that caused
|
|
118
|
+
this. One breaking version, clear error messages.
|
|
119
|
+
- No change to palettes, colours, series rendering, tooltips or interaction.
|
|
120
|
+
- Do not "improve" the estimate again. Under Rule 2 there is no estimate — bands are measured.
|
|
121
|
+
|
|
122
|
+
### Risks
|
|
123
|
+
|
|
124
|
+
- **Four call sites lose responsive height** with `aspect` gone: `hram/AccessGapScatterPanel.jsx:182`
|
|
125
|
+
and three in `fitness-monitor/BodyHistoryPage.jsx`. fitness-monitor's `960/380` ratios were a
|
|
126
|
+
design decision, not an accident. **Each converts to a size token as a named, visible decision in
|
|
127
|
+
its own migration WO** — not silently. Charts still reflow horizontally (the wrapper stays
|
|
128
|
+
`width: 100%`); only the height stops tracking width. Operator accepted this trade on 2026-08-22.
|
|
129
|
+
- **The invariant test cannot run on estimates in jsdom.** `UCM-CHART-11` established that jsdom's
|
|
130
|
+
`getComputedTextLength` throws, so the unit tests must inject a measurement stub — and a
|
|
131
|
+
**browser-level check is therefore mandatory, not optional**, or the invariant is only asserted
|
|
132
|
+
against a fiction.
|
|
133
|
+
- **hram has several WOs in flight touching these files** (`RES-28`, `HRAM-RES-29`, `FIX-19`,
|
|
134
|
+
`CHT-3`). hram's migration WO must sequence after them, and this WO must not start hram's sweep.
|
|
135
|
+
- A `3.0.0` major is the first in this package's history. Check `publish.yml`'s version gate handles a
|
|
136
|
+
major, and that no consumer pins with a caret that would silently adopt it.
|
|
137
|
+
|
|
138
|
+
### Tests to WRITE
|
|
139
|
+
|
|
140
|
+
- The two invariants, per preset, over the label matrix — the centrepiece.
|
|
141
|
+
- Empty tick labels → zero-height band.
|
|
142
|
+
- `size` token → height mapping, stable and theme-derived.
|
|
143
|
+
- `height` escape overrides the token.
|
|
144
|
+
- Each removed prop raises the dev-mode error naming its replacement.
|
|
145
|
+
- `ChartFrame`'s `minHeight` floor still behaves as `UCM-CHART-9` fixed it — that test stays green
|
|
146
|
+
unchanged.
|
|
147
|
+
|
|
148
|
+
**Browser-level check before publish**, on hram with real data at 1280 px and one narrow width: the
|
|
149
|
+
invariant holds on rendered geometry, and the Access scatter's blank band is gone. DOM measurement is
|
|
150
|
+
acceptable and must be declared; here it is sufficient, because the acceptance is arithmetic.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Part B — Implementation map
|
|
155
|
+
|
|
156
|
+
### Context package
|
|
157
|
+
|
|
158
|
+
**Named files to change**
|
|
159
|
+
|
|
160
|
+
- `src/components/charts/chartDefaults.js` — the resolver lives here today as three separate,
|
|
161
|
+
interacting functions. **Delete** `resolveChartHeight` (:385-394), `warnOnHeightMismatch`
|
|
162
|
+
(:408-421), and `spaceForRotatedTicks` (:229-238) plus its private helper `rotatedTickMetrics`
|
|
163
|
+
(:183-210) — but **keep** the text-measurement primitives they call: `measureTickTextWidthPx`
|
|
164
|
+
(:120-131, real DOM measurement via a hidden shared SVG `<text>` node — this is the "measured, not
|
|
165
|
+
estimated" plumbing Rule 2 needs) and `estimateTextWidthPx`/`glyphWidthEm` (:71-86, the no-DOM
|
|
166
|
+
fallback — SSR / jsdom without an injected stub). Also keep `sizeYAxisForContent` (:311-348, THEME-9,
|
|
167
|
+
y-axis width from tick content — the new resolver's `yAxisBand` should call this, not reinvent it),
|
|
168
|
+
`withGridDefaults`, `withAxisDefaults`, `withChartSlotDefaults`, `defaultNumericTickFormatter`,
|
|
169
|
+
`PACKAGE_DEFAULT_MARGIN`/`withMarginDefaults` (margins are now fully owned by the resolver per Rule
|
|
170
|
+
3 — decide whether `withMarginDefaults` survives as an internal helper the resolver calls, or is
|
|
171
|
+
folded into it; either is fine, it must not stay a separate exported knob a consumer can pass into).
|
|
172
|
+
Add the new `resolveChartLayout` export here.
|
|
173
|
+
- `src/components/charts/BarChart.jsx` (:35-98), `LineChart.jsx` (:52-123), `ScatterChart.jsx`
|
|
174
|
+
(:173-289), all four accept `{ minHeight, height, aspect, margin }` in their destructuring and call
|
|
175
|
+
`resolveChartHeight`/`warnOnHeightMismatch`/`spaceForRotatedTicks` — every one of those call sites
|
|
176
|
+
is replaced by a single `resolveChartLayout(...)` call. `xAxisAngle` is not currently a named prop on
|
|
177
|
+
any of the three (rotation today comes in via `tickLabelStyle.angle` on a caller-supplied `xAxis`
|
|
178
|
+
entry, see `rotatedTickMetrics`'s `axis.tickLabelStyle?.angle`) — introducing `xLabels: "auto" |
|
|
179
|
+
"horizontal" | "angled"` as a first-class prop is new surface, not a rename; decide how `"angled"`
|
|
180
|
+
maps onto what MUI needs (`tickLabelStyle.angle` + the axis height the resolver computes) and how
|
|
181
|
+
`"auto"` decides using the *measured* label widths against `plotWidth` (Rule 3) — this is the one
|
|
182
|
+
piece of real algorithmic design in this WO, budget time for it.
|
|
183
|
+
- `TimeSeriesChart.jsx` (:269, :286) passes `minHeight={CHART_HEIGHT}` to `ChartFrame` (stays — that's
|
|
184
|
+
the frame floor, Rule/DoD says `ChartFrame`'s `minHeight` survives) and `height={CHART_HEIGHT}` to the
|
|
185
|
+
inner `BarChart` (:286, must migrate to `size="standard"` or equivalent — decide the token that
|
|
186
|
+
reproduces 320px, see size-token note below).
|
|
187
|
+
- `src/components/charts/ChartFrame.jsx` (:28-49, :112-124) — **unchanged behaviourally**: keeps
|
|
188
|
+
`minHeight`/`height`/`aspect` as its own card-floor props (DoD: "`ChartFrame` keeps its `minHeight`
|
|
189
|
+
as a card floor"). It currently also imports `warnOnHeightMismatch` (:14, :62-65) for its own
|
|
190
|
+
mismatch note — that function is deleted, so `ChartFrame` needs either an inline replacement of the
|
|
191
|
+
same dev-mode console warning (same text/condition, just no longer imported from `chartDefaults`) or
|
|
192
|
+
a decision that the note is no longer worth keeping now that the presets' own version of it is gone
|
|
193
|
+
— **do not delete `ChartFrame`'s warning silently**; if dropping it, say so in `CHART-LAYOUT.md`.
|
|
194
|
+
- New file `docs/CHART-LAYOUT.md` — the model in one page, each removed prop paired with its
|
|
195
|
+
replacement and a before/after code snippet. Look at `docs/` (if it exists) or the package README for
|
|
196
|
+
the house doc style/tone to match; five apps' implementers read this cold.
|
|
197
|
+
- Tests: `tests/chartDefaults.test.js` (imports the now-deleted functions at :17-26 — every one of
|
|
198
|
+
those imports must be replaced) and `tests/ChartFrame.test.jsx` (the `minHeight` floor regression
|
|
199
|
+
test named in the DoD must stay green, unchanged in intent).
|
|
200
|
+
|
|
201
|
+
**The invariant, concretely**
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
chartHeight === plotHeight + xAxisBand + xTitleBand + legendBand
|
|
205
|
+
chartWidth === yAxisBand + plotWidth + rightPad
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`resolveChartLayout` must return every term on the right as its own named field (not just the totals)
|
|
209
|
+
so the test can sum them and assert equality against the height/width it also returns — a test that
|
|
210
|
+
can only compare final totals cannot catch a term double-counted against another (which is exactly
|
|
211
|
+
what `UCM-CHART-10` was). `xAxisBand` comes from `axis.height` (today's `MUI_LABELLED_X_AXIS_HEIGHT` +
|
|
212
|
+
`rotatedTickMetrics`'s extra, now unconditional on `xLabels`, not just on caller-supplied
|
|
213
|
+
`tickLabelStyle.angle`), `yAxisBand` from `sizeYAxisForContent`'s width, `legendBand` from whether
|
|
214
|
+
`hideLegend`/`legendPosition` puts a legend on the vertical axis (0 when legend is off or horizontal —
|
|
215
|
+
this band existed nowhere before this WO; MUI's own legend today just grows the SVG uncounted), `plot`
|
|
216
|
+
is whatever remains. Empty tick labels (Rule 2 / DoD, the Access-scatter case) must resolve
|
|
217
|
+
`xAxisBand`/`yAxisBand` to 0, not the MUI/package floor — trace `longestFormattedTick` (:271-284)
|
|
218
|
+
returning `''` through to the band, since today's `rotatedTickMetrics` floors width at `fontSize`
|
|
219
|
+
(:193-194) rather than letting empty collapse to zero.
|
|
220
|
+
|
|
221
|
+
**Size tokens**
|
|
222
|
+
|
|
223
|
+
`theme.spacing` is MUI's 8px unit (`src/theme/tokens.js:182`). `TimeSeriesChart.jsx`'s existing
|
|
224
|
+
`CHART_HEIGHT = 320` (its own comment at :18-33 explains *why* a fixed height exists — read it, the
|
|
225
|
+
reason still applies) is the one deployed number every current consumer has already implicitly agreed
|
|
226
|
+
looks right; make `"standard"` resolve to exactly that (`theme.spacing(40)`) so this migration doesn't
|
|
227
|
+
also silently redraw every existing default-sized chart. Pick `"compact"`/`"tall"` proportionally
|
|
228
|
+
(e.g. `theme.spacing(30)` / `theme.spacing(50)`) and document the exact numbers in `CHART-LAYOUT.md` —
|
|
229
|
+
these are visible product decisions, not internal plumbing, so state them plainly rather than burying
|
|
230
|
+
them in the resolver.
|
|
231
|
+
|
|
232
|
+
**Removed-prop dev-mode error**
|
|
233
|
+
|
|
234
|
+
Two existing precedents pull in different directions: `warnOnHeightMismatch` (being deleted) was a
|
|
235
|
+
`console.warn`, gated on `process.env.NODE_ENV !== 'production'`; `TimeSeriesChart.jsx:195`
|
|
236
|
+
(`hasSecondaryAxis && !secondaryYAxisLabel`) is a real `throw new Error(...)`, ungated, in production
|
|
237
|
+
too. The DoD text ("a dev-mode error naming the replacement, not a silent ignore") reads as: gate on
|
|
238
|
+
`NODE_ENV !== 'production'` like the deleted warning did (a shared package must not crash a
|
|
239
|
+
consumer's production page over a layout prop), but make it a `throw`, not a `console.warn`, inside
|
|
240
|
+
that gate — loud in dev, inert in prod. Apply it to `minHeight`/`aspect`/`margin` on the four presets;
|
|
241
|
+
each error message names the specific replacement (`size`/removed-with-no-replacement/removed
|
|
242
|
+
respectively — `margin` has no successor prop at all, the model owns it completely, say that plainly
|
|
243
|
+
in the message).
|
|
244
|
+
|
|
245
|
+
**Do-not-touch / invariants that must survive unchanged**
|
|
246
|
+
|
|
247
|
+
- Palette, series colours, tooltips, legend *content*, interaction — none of this WO's concern (Non-
|
|
248
|
+
goals). Only the geometry that decides *how much room* things get.
|
|
249
|
+
- `sizeYAxisForContent`'s existing behaviour and its own tests (:311-348) — reuse, do not rewrite.
|
|
250
|
+
- `ChartFrame`'s `minHeight` floor test (`UCM-CHART-9`'s regression) — must stay green, unmodified in
|
|
251
|
+
intent, even though its `warnOnHeightMismatch` import needs to change.
|
|
252
|
+
- `ScatterChart.jsx` keeps its own y-axis-only sizing rationale (:213-223, the `data.map(point => y)`
|
|
253
|
+
adapter) — this WO's `resolveChartLayout` change must not silently break that adapter's contract.
|
|
254
|
+
|
|
255
|
+
**Known pitfall**
|
|
256
|
+
|
|
257
|
+
`UCM-CHART-11`'s own register note: jsdom's `getComputedTextLength` throws / is unimplemented, so
|
|
258
|
+
`measureTickTextWidthPx` returns `null` under test and every unit test that needs a real number must
|
|
259
|
+
inject a measurement stub (the existing `measureTextWidth` injection point in `spaceForRotatedTicks`
|
|
260
|
+
is the pattern — carry the same injectability into `resolveChartLayout`). This is *why* the WO also
|
|
261
|
+
requires a **mandatory browser-level check** before publish (see Verification, Part C) — the unit
|
|
262
|
+
suite alone cannot prove the invariant holds against real rendered text.
|
|
263
|
+
|
|
264
|
+
Directive: work from this package; open only the named files to verify. If you need the full
|
|
265
|
+
`docs/` directory listing or README style, or need to search for other `chartDefaults` imports across
|
|
266
|
+
the package (there may be call sites in `src/index.js` exports or `dev/` harness files not listed
|
|
267
|
+
above), delegate that lookup to a read-only Explore sub-agent rather than open-ended browsing.
|
|
268
|
+
|
|
269
|
+
### Target repo working directory (absolute)
|
|
270
|
+
|
|
271
|
+
`C:\Users\biglmi\Documents\webapps\ui-core-micha`
|
|
272
|
+
|
|
273
|
+
### Preamble — REQUIRED, do not dispatch without it
|
|
274
|
+
|
|
275
|
+
> The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
|
|
276
|
+
> is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`,
|
|
277
|
+
> and this repo's `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/
|
|
278
|
+
> deps/schema/CI unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` —
|
|
279
|
+
> the register row and the review verdicts are the orchestrator's alone.** Do NOT `git add`/`commit`/
|
|
280
|
+
> `push` — leave every change uncommitted in the working tree for the orchestrator's independent
|
|
281
|
+
> review. WRITE the tests the "Tests to WRITE" section (Part A) calls for AND **RUN the tests you just
|
|
282
|
+
> wrote** to confirm they execute and pass — that is the ONLY test run you do (NOT the app's
|
|
283
|
+
> affected/full suite, NOT any review, NOT the mandatory browser-level check — that is the
|
|
284
|
+
> orchestrator's, after you finish). The orchestrator re-runs the authoritative set, does the
|
|
285
|
+
> independent review, and drives the browser-level check after you finish — those are the gate; your
|
|
286
|
+
> own run does not count as it.
|
|
287
|
+
>
|
|
288
|
+
> Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
|
|
289
|
+
> `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
|
|
290
|
+
> completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
|
|
291
|
+
> `RESULT: DONE|BLOCKED <reason>`.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Part C — Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
|
|
296
|
+
|
|
297
|
+
> **If you are the implementer reading this work order as your own specification: STOP at this line.
|
|
298
|
+
> Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
|
|
299
|
+
> reviewers, no verification run, no register edit, no commit.** You ARE the invocation described
|
|
300
|
+
> below; do NOT shell out to `codex exec`.
|
|
301
|
+
|
|
302
|
+
### Execution directive
|
|
303
|
+
|
|
304
|
+
Implement through `codex exec` in the background, invoked directly via Bash (never the
|
|
305
|
+
`debugger`/`*_coder` Agent wrappers), with BOTH `--skip-git-repo-check` and
|
|
306
|
+
`--dangerously-bypass-approvals-and-sandbox`. `cwd` = this repo's root (never `…/webapps`). Fallback
|
|
307
|
+
to direct Claude implementation only on Codex quota/rate-limit/non-zero exit — check
|
|
308
|
+
`.claude/codex-status.md` first per `AGENTS.md`; the fallback flips authorship, making an independent
|
|
309
|
+
`reviewer` mandatory regardless of the routing below.
|
|
310
|
+
|
|
311
|
+
### Review routing
|
|
312
|
+
|
|
313
|
+
Tier 3, shared-core, **breaking**: `reviewer` AND `ui_reviewer` both run, full context, concurrently
|
|
314
|
+
in one background batch (not sequential, not after commit) — model Sonnet for `reviewer` given Tier 3.
|
|
315
|
+
The Orchestrator additionally runs its own targeted pass for: the invariant test's actual assertion
|
|
316
|
+
strength (does it sum the named bands, or only compare totals — the thing that would let a
|
|
317
|
+
`UCM-CHART-10`-shaped bug back in), whether the dev-mode error truly stays inert in production, and
|
|
318
|
+
whether `ChartFrame`'s dropped/kept mismatch warning was a silent behaviour change.
|
|
319
|
+
|
|
320
|
+
### Verification
|
|
321
|
+
|
|
322
|
+
1. The affected-areas test gate: `tests/chartDefaults.test.js`, `tests/ChartFrame.test.jsx`, and any
|
|
323
|
+
new chart-preset test files, via the package's test runner (`pnpm test` scoped to these files/dirs
|
|
324
|
+
— check `package.json`'s test script for the right invocation, do not run the full suite unless it
|
|
325
|
+
turns out these are the only test files in the package).
|
|
326
|
+
2. **Mandatory browser-level check, not optional** (Part A "Tests to WRITE" + Risks): render on hram
|
|
327
|
+
with real data at 1280px and one narrow width (375px), confirm the invariant holds against actually
|
|
328
|
+
rendered geometry and that the Access-scatter blank band is gone. DOM measurement is acceptable
|
|
329
|
+
here and must be declared as such in the register Notiz (per AGENTS.md → Reviews, prototype-style
|
|
330
|
+
substitution disclosure) — this WO's own text calls the browser check "sufficient... because the
|
|
331
|
+
acceptance is arithmetic," so a declared DOM-measurement substitution meets the gate; a screenshot
|
|
332
|
+
is not required, but the specific measured numbers must be reported, not just "looks fine."
|
|
333
|
+
3. Check `publish.yml`'s version gate (`.github/workflows/publish.yml:72-96`, a plain per-segment
|
|
334
|
+
integer `greaterThan` compare) — it already handles a major bump correctly (`3.0.0 > 2.42.3`), no
|
|
335
|
+
change needed there. Bump `package.json`'s `"version"` (:3, currently `2.42.3`) to `3.0.0` as part
|
|
336
|
+
of this diff — the publish workflow is push-triggered off `main` and gates only on the version
|
|
337
|
+
string increasing, so the version bump IS the publish trigger; do not push before everything else
|
|
338
|
+
here is green. Per the Risk note, do NOT chase down consumer caret pins in this WO — that is each
|
|
339
|
+
consumer's own migration WO's problem (Non-goals: no consumer migration here).
|
|
340
|
+
|
|
341
|
+
### Register + commit
|
|
342
|
+
|
|
343
|
+
`WORK_ORDERS.md` row `UCM-CHART-12`: `done`, both reviewers named with verdict, the browser-level
|
|
344
|
+
check's declared method (DOM measurement or real screenshots) and its numbers, commit SHA. Single
|
|
345
|
+
concise commit subject, e.g. `UCM-CHART-12: one layout model for charts (breaking, 3.0.0)`. Push to
|
|
346
|
+
`main` (this repo's trunk — no `develop` step, per the Envelope header). This is a **major, breaking,
|
|
347
|
+
shared-core** publish: after push, confirm in the Actions log that `publish.yml` actually ran and
|
|
348
|
+
published `3.0.0` — a silent publish failure here is invisible to every consumer's future pin bump.
|