@micha.bigler/ui-core-micha 3.2.0 → 3.3.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/CHANGELOG.md +43 -0
- package/WORK_ORDERS.md +2 -0
- package/dist/components/charts/ChartFrame.js +7 -3
- package/dist/components/charts/chartDefaults.js +34 -2
- package/dist/components/charts/exportChart.js +147 -23
- package/dist/i18n/chartsTranslations.js +17 -0
- package/docs/CHART-LAYOUT.md +9 -7
- package/package.json +1 -1
- package/src/components/charts/ChartFrame.jsx +29 -20
- package/src/components/charts/chartDefaults.js +35 -1
- package/src/components/charts/exportChart.js +157 -23
- package/src/i18n/chartsTranslations.ts +17 -0
- package/tests/ChartFrame.test.jsx +15 -0
- package/tests/chartAxisTickBand.test.jsx +95 -0
- package/tests/chartDefaults.test.js +40 -0
- package/tests/exportChart.test.js +184 -0
- package/work-orders/UCM-CHART-16.md +169 -0
- package/work-orders/UCM-CHART-17.md +195 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# UCM-CHART-16 — The x-axis band starves MUI's tick-fit check, and two consumers have patched around it
|
|
2
|
+
|
|
3
|
+
## Part A — Envelope
|
|
4
|
+
|
|
5
|
+
*Authored by the Expertenchat. Authoritative WHAT/WHY.*
|
|
6
|
+
|
|
7
|
+
### Goal
|
|
8
|
+
|
|
9
|
+
A chart with an axis title **and** horizontal tick labels renders its tick labels. No consumer needs
|
|
10
|
+
to set `xAxis[].height` by hand to make that happen.
|
|
11
|
+
|
|
12
|
+
### The defect
|
|
13
|
+
|
|
14
|
+
When the x-axis stays horizontal, `resolveXAxisGeometry` reserves one combined band from
|
|
15
|
+
`TICK_BAND_BASE_PX` + `AXIS_TITLE_BAND_PX`. Once an axis title is also taken from that budget, MUI's
|
|
16
|
+
own internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) is left **just under one line
|
|
17
|
+
of 12 px text** — so it fails for **every** tick and blanks **every** label, regardless of content.
|
|
18
|
+
|
|
19
|
+
The signature is distinctive and easy to misread: correctly positioned tick marks, zero non-empty
|
|
20
|
+
label texts. It looks like a data or formatter problem and is neither.
|
|
21
|
+
|
|
22
|
+
### The evidence: two consumers, same workaround, same magic number
|
|
23
|
+
|
|
24
|
+
In hram, independently, in two different work orders:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
StructuralReachabilityPanel.jsx:386 height: 56 (HRAM-RES-32)
|
|
28
|
+
AccessGapScatterPanel.jsx:212 height: 56 (HRAM-RES-39)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`HRAM-RES-32`'s own comment already diagnosed it correctly and called it what it is — *"a
|
|
32
|
+
pre-existing, latent **shared-core** near-miss"* — then patched locally, because a shared-core fix was
|
|
33
|
+
outside that WO. `HRAM-RES-39` hit the identical wall, read the MUI source, arrived at the same
|
|
34
|
+
cause and the same 56, and patched locally again.
|
|
35
|
+
|
|
36
|
+
Two things follow. **The third chart with a titled horizontal axis will hit it too**, and its author
|
|
37
|
+
will have to rediscover both the cause and the number. And the package is violating its own stated
|
|
38
|
+
rule: `chartDefaults.js` says *"Rule 1: nothing outside the resolver does chart arithmetic"* — a
|
|
39
|
+
caller hand-computing an axis height is exactly that, now in two places.
|
|
40
|
+
|
|
41
|
+
### A third occurrence — and it proves scope item 2
|
|
42
|
+
|
|
43
|
+
Reported by the operator on 2026-08-25, hram's `AccessibilityPanel` in its
|
|
44
|
+
`Urban/Peri-urban/Rural × Division` mode: five band categories, five rendered bar groups, **no tick
|
|
45
|
+
labels at all**. Same signature — positioned ticks, blank text.
|
|
46
|
+
|
|
47
|
+
This one is different in a way that matters. That chart has **no `xAxisLabel`**
|
|
48
|
+
(`AccessibilityPanel.jsx:164-178`), so the axis title is *not* competing for the band. What it does
|
|
49
|
+
have is `tickLabelStyle: { fontSize: 13 }` — one pixel above the 12 px the base band was sized
|
|
50
|
+
against.
|
|
51
|
+
|
|
52
|
+
**So the trigger is not "a title takes the room". It is that the band is a near-miss at all**, and
|
|
53
|
+
anything that consumes a little more — a title, a slightly larger font, presumably a taller line
|
|
54
|
+
height — pushes it over. A fixed `height: 56` patched into a third consumer would work at 13 px and
|
|
55
|
+
fail at 14. That is precisely why scope item 2 requires the resolver to **derive** the requirement
|
|
56
|
+
from the tick font size it already knows, rather than adopting the constant two consumers measured
|
|
57
|
+
empirically at 12 px.
|
|
58
|
+
|
|
59
|
+
(Signature-matched, not yet instrumented — confirm the mechanism on this third case as part of the
|
|
60
|
+
work, since it is the one that distinguishes a derived fix from a bigger constant.)
|
|
61
|
+
|
|
62
|
+
### Scope
|
|
63
|
+
|
|
64
|
+
1. **Fix the reservation in the resolver.** The band must leave MUI's fit check room for one full
|
|
65
|
+
line of tick text *after* the axis title's share, not just under it. The near-miss is small, which
|
|
66
|
+
is why it went unnoticed — the fix is a correction to the arithmetic, not a redesign.
|
|
67
|
+
2. **Derive it, do not add a second magic number.** 56 is what two consumers measured empirically
|
|
68
|
+
against a 12 px font; the resolver knows the tick font size and the title band, so it can compute
|
|
69
|
+
the requirement instead of hard-coding a constant that will be wrong at a different font size.
|
|
70
|
+
3. **Say what changed and why in the comment**, in the style the file already uses — this is the
|
|
71
|
+
third time this arithmetic has been reasoned about and the first time it is written down where it
|
|
72
|
+
belongs.
|
|
73
|
+
|
|
74
|
+
### Non-goals / do not touch
|
|
75
|
+
|
|
76
|
+
- The rotated-tick path (`xLabels: "angled"`, `spaceForRotatedTicks`). The defect is specific to the
|
|
77
|
+
horizontal case; a rotated axis reserves its own projected band and is unaffected.
|
|
78
|
+
- `CHART_SIZE_SPACING_UNITS` and the size scale — `UCM-CHART-15`, done.
|
|
79
|
+
- `ChartFrame`, margins, the legend band, `resolveChartLayout`'s other outputs.
|
|
80
|
+
- The consuming panels. Removing hram's two workarounds is `HRAM-CHT-6`, after this ships.
|
|
81
|
+
|
|
82
|
+
### Risks
|
|
83
|
+
|
|
84
|
+
- **A mocked prop-assertion test cannot catch this.** The file already records the same trap for a
|
|
85
|
+
neighbouring bug: *"a mocked prop-assertion test cannot catch this, it only reproduces on an actual
|
|
86
|
+
MUI scale computation"*. A test that asserts the computed band's number would pass whatever the
|
|
87
|
+
number is. The test has to observe **rendered label text**, or it is decoration — see test 1.
|
|
88
|
+
- **Raising the band changes every titled-horizontal-axis chart's plot area** by the difference. Small,
|
|
89
|
+
but it is a visual change to consumers, and it lands on top of `UCM-CHART-15`'s 80 px shift.
|
|
90
|
+
- Fixing it too generously wastes vertical space on every chart. The margin of the near-miss is the
|
|
91
|
+
budget; do not round it up "to be safe".
|
|
92
|
+
|
|
93
|
+
### Tests to write
|
|
94
|
+
|
|
95
|
+
1. **A rendered test**: a chart with an axis title and horizontal ticks produces **non-empty** tick
|
|
96
|
+
label texts. Written so it fails against the current resolver — a blank-label render is the
|
|
97
|
+
defect, so the test must be able to see blankness.
|
|
98
|
+
2. The rotated path is unchanged — same band as before for `xLabels: "angled"`.
|
|
99
|
+
3. An axis **without** a title still reserves only the tick band (Rule 2: a band that is not needed
|
|
100
|
+
collapses to zero).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Part B — Implementation map
|
|
105
|
+
|
|
106
|
+
`.claude/models.local.json` has no local override, so the default applies: `implementation.runtime`
|
|
107
|
+
is `claude`/`sonnet` — the Orchestrator implemented directly, no Codex dispatch.
|
|
108
|
+
|
|
109
|
+
**The defect could not be diagnosed from source reading alone** (see Risk section: "a mocked
|
|
110
|
+
prop-assertion test cannot catch this"). MUI's internal fit check (`shortenLabels`, weighed
|
|
111
|
+
against `tickLabelsMaxHeight`, in `node_modules/@mui/x-charts/ChartsXAxis/ChartsSingleXAxisTicks.mjs`
|
|
112
|
+
and `shortenLabels.mjs`) depends on `getBBox`, which jsdom does not implement — so the mechanism
|
|
113
|
+
was confirmed and the fix calibrated against a REAL browser render, not derived from reading MUI's
|
|
114
|
+
source alone:
|
|
115
|
+
|
|
116
|
+
- `dev/entries.jsx`'s dev harness (`vite`, real Chrome via the internal preview browser) was used
|
|
117
|
+
to dynamically render the actual, unmocked `BarChart` with a titled horizontal axis under this
|
|
118
|
+
package's own `createAppTheme`/DM Sans default. Confirmed the defect reproduces exactly as
|
|
119
|
+
described (blank tick labels, correctly positioned) and does NOT reproduce under MUI's default
|
|
120
|
+
Roboto stack (real glyph metrics run ~1.3-1.36× fontSize for DM Sans vs ~1.06-1.17× for Roboto —
|
|
121
|
+
explains why the defect is font-dependent and was missed until a consumer used a taller font).
|
|
122
|
+
- Binary-searched the real, minimal `xAxis[].height` needed to stop blanking, at tick font sizes
|
|
123
|
+
11-18px, with and without a title (ten measured thresholds total). The result matched
|
|
124
|
+
`ceil(fontSize * 1.3) + 9` (no title) / `+ 11` (with title, i.e. the same plus 2px) almost
|
|
125
|
+
exactly — over by at most 1px, never under, across all ten points. `9` corresponds to MUI's own
|
|
126
|
+
`tickSize`(6, default) + `TICK_LABEL_GAP`(3) internal overhead
|
|
127
|
+
(`ChartsXAxis/utilities.mjs`/`ChartsSingleXAxisTicks.mjs`); the extra `+2` when a title is
|
|
128
|
+
present was constant across every font size tested, independent of font.
|
|
129
|
+
- The fix (`resolveXAxisGeometry`'s `angle === 0` branch, `chartDefaults.js`) was re-verified
|
|
130
|
+
against the SAME real-browser harness after implementing, confirming both reproduced scenarios
|
|
131
|
+
(title + default font; no title + `tickLabelStyle.fontSize: 13`, the `AccessibilityPanel`
|
|
132
|
+
signature) now render correctly with no explicit `height` override needed.
|
|
133
|
+
|
|
134
|
+
Context package: `chartDefaults.js:322-341` (`TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX`/new
|
|
135
|
+
constants), `:366-430ish` (`resolveXAxisGeometry`, the `angle === 0` early-return that was the
|
|
136
|
+
defect site), `:450-475` (`resolveChartLayout`'s `xAxisBand`/`xTitleBand` composition, unchanged).
|
|
137
|
+
Non-goal boundary respected: the `angle !== 0` (rotated) branch below the fixed code is untouched.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Part C — Orchestrator only
|
|
142
|
+
|
|
143
|
+
> **STOP — everything below addresses the Orchestrator.**
|
|
144
|
+
|
|
145
|
+
**Tier 3 · tests: the three cases above, at least one of them rendered**
|
|
146
|
+
|
|
147
|
+
Shared-core, by the tiering table regardless of diff size.
|
|
148
|
+
|
|
149
|
+
- `reviewer` — the arithmetic, and above all whether test 1 can actually fail.
|
|
150
|
+
- `ui_reviewer` — the plot-area change this causes across consumers.
|
|
151
|
+
- `sec_reviewer` — not run; no auth, data or exposure surface.
|
|
152
|
+
|
|
153
|
+
### Release
|
|
154
|
+
|
|
155
|
+
Patch or minor per the repo's convention — this repairs existing behaviour rather than adding
|
|
156
|
+
capability, so **patch** unless the plot-area change is judged large enough to warrant a minor. State
|
|
157
|
+
which and why in the CHANGELOG; `UCM-CHART-15`'s entry is the model for describing a visual
|
|
158
|
+
consequence plainly.
|
|
159
|
+
|
|
160
|
+
### Register
|
|
161
|
+
|
|
162
|
+
Row `UCM-CHART-16`, both review notes, the published version, landing SHA(s) in a second commit
|
|
163
|
+
before the push. Record that the fix arrived **third** — two consumers patched around it first — so
|
|
164
|
+
the cost of leaving a shared-core near-miss to consumers is on the record.
|
|
165
|
+
|
|
166
|
+
### Log
|
|
167
|
+
|
|
168
|
+
- **2026-08-25** — Envelope authored after `HRAM-RES-39` hit the same defect `HRAM-RES-32` had
|
|
169
|
+
already diagnosed as shared-core, and applied the identical `height: 56` workaround independently.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# UCM-CHART-17 — The two export buttons make two different promises; make them keep them
|
|
2
|
+
|
|
3
|
+
## Part A — Envelope
|
|
4
|
+
|
|
5
|
+
*Authored by the Expertenchat. Authoritative WHAT/WHY.*
|
|
6
|
+
|
|
7
|
+
### Goal
|
|
8
|
+
|
|
9
|
+
**Export SVG** yields the chart as scalable, correctly styled vector art. **Export PNG** yields what
|
|
10
|
+
the user is looking at — legend, size key and footnotes included.
|
|
11
|
+
|
|
12
|
+
### The defect, from the source
|
|
13
|
+
|
|
14
|
+
`exportChart.js:7-11` clones the first `<svg>` in the container and serialises it raw:
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
const svg = findSvg(chartContainer).cloneNode(true);
|
|
18
|
+
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
19
|
+
return new Blob([new XMLSerializer().serializeToString(svg)], …)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Three consequences, and they compound:
|
|
23
|
+
|
|
24
|
+
1. **No styles are carried.** MUI X-Charts styles nearly everything through emotion CSS classes —
|
|
25
|
+
tick fonts and sizes, axis and grid colours, text fill, the theme's typography. The clone keeps
|
|
26
|
+
`class="MuiChartsAxis-tickLabel css-1abc"` and none of the rules behind it, so a standalone
|
|
27
|
+
viewer applies bare SVG defaults. This is the "graphically massively different" the operator
|
|
28
|
+
reported. **The PNG inherits it**, because it rasterises the same blob.
|
|
29
|
+
|
|
30
|
+
2. **The legend is gone, and that is ours.** `findSvg` takes the *first* `<svg>`, i.e. the chart.
|
|
31
|
+
Since `HRAM-RES-39`/`HRAM-VIS-2`, consuming panels hide MUI's own legend and render a hand-built
|
|
32
|
+
one as **HTML** beside the chart, with the size key as a *second* SVG. Both sit outside the
|
|
33
|
+
exported element. **Before that consolidation, MUI's legend was inside the chart SVG and did get
|
|
34
|
+
exported** — unstyled, but present. Fixing a legend overlap silently degraded the export, and
|
|
35
|
+
nobody noticed because nobody opened an exported file.
|
|
36
|
+
|
|
37
|
+
3. **"The first SVG" is fragile** regardless — a panel that renders any SVG ahead of the chart
|
|
38
|
+
exports the wrong element. It works today by accident of ordering.
|
|
39
|
+
|
|
40
|
+
### The decision: two paths, two meanings
|
|
41
|
+
|
|
42
|
+
The operator chose option C on 2026-08-25. The two buttons already promise different things and
|
|
43
|
+
should stop pretending to be one mechanism:
|
|
44
|
+
|
|
45
|
+
- **SVG — the chart, faithfully styled.** Keep the clone-and-serialise path, but **inline computed
|
|
46
|
+
styles onto the cloned nodes** before serialising. Stays true vector: scalable, editable in
|
|
47
|
+
Illustrator/Inkscape, chart only. It does **not** gain the legend, and that is correct for what
|
|
48
|
+
this format is for.
|
|
49
|
+
- **PNG — the container, as seen.** Rasterise the whole `chartRef` node rather than the chart SVG, so
|
|
50
|
+
legend, size key and footnotes are in the file and the output matches the screen by construction.
|
|
51
|
+
|
|
52
|
+
**Make the difference legible to the user, not only to us.** Two buttons that produce visibly
|
|
53
|
+
different content need labels or tooltips saying so; today they read as one thing in two formats.
|
|
54
|
+
|
|
55
|
+
### Scope
|
|
56
|
+
|
|
57
|
+
**`src/components/charts/exportChart.js`**
|
|
58
|
+
|
|
59
|
+
1. **Inline computed styles** into the SVG clone before serialisation. Walk the clone against the
|
|
60
|
+
live tree and write the resolved properties as inline style. Keep it to the properties that matter
|
|
61
|
+
for chart rendering rather than dumping every computed property — a full dump produces enormous
|
|
62
|
+
files and is its own defect.
|
|
63
|
+
2. **Rasterise the container for PNG.** The `chartRef` node ChartFrame already passes
|
|
64
|
+
(`ChartFrame.jsx:138`) is the right root — it holds the chart plus everything the panel renders
|
|
65
|
+
beside it.
|
|
66
|
+
3. **Stop relying on "the first SVG"** for the SVG path — take the chart deliberately, not
|
|
67
|
+
positionally.
|
|
68
|
+
4. **Preserve the public signatures.** `exportChartSvg(container, filename)` and
|
|
69
|
+
`exportChartPng(container, filename, backgroundColour)` are consumed by `ChartFrame` and possibly
|
|
70
|
+
directly; the change is what they produce, not how they are called.
|
|
71
|
+
|
|
72
|
+
**`src/components/charts/ChartFrame.jsx`** — labels or tooltips that distinguish the two outputs.
|
|
73
|
+
Translated via the existing `chartsTranslations` path, not hard-coded.
|
|
74
|
+
|
|
75
|
+
### Non-goals / do not touch
|
|
76
|
+
|
|
77
|
+
- The chart presets, `resolveChartLayout`, the size scale, the axis bands.
|
|
78
|
+
- Consuming apps. hram's six panels with `exportOptions` gain the fix through the pin bump; that is
|
|
79
|
+
a separate row.
|
|
80
|
+
- Adding export to panels that do not have it (`HRAM-RES-43` did that for one panel in hram).
|
|
81
|
+
|
|
82
|
+
### Risks
|
|
83
|
+
|
|
84
|
+
- **A rasteriser is a new dependency in shared-core**, and this package currently has exactly one
|
|
85
|
+
runtime dependency (`@fontsource/dm-sans`). Adding a second is a real decision — check whether the
|
|
86
|
+
job can be done with the platform's own APIs first, and if a library is warranted, say why in the
|
|
87
|
+
WO's implementation record rather than just installing one.
|
|
88
|
+
- **Canvas tainting.** DOM-to-canvas rasterisation routes through an SVG `foreignObject`; any
|
|
89
|
+
external image or font reference taints the canvas and `toBlob` throws. The existing PNG path
|
|
90
|
+
already has this exposure and may already be failing on it — worth checking before assuming the new
|
|
91
|
+
path introduces it.
|
|
92
|
+
- **Fonts in the SVG.** Inlining computed styles records `font-family: "DM Sans"`, which a machine
|
|
93
|
+
without that font will substitute. Either embed the font or accept the substitution — but decide it
|
|
94
|
+
and write it down, because a silently substituted font is exactly the "looks different" complaint
|
|
95
|
+
this WO is fixing.
|
|
96
|
+
- **Style inlining can bloat.** See scope item 1.
|
|
97
|
+
- **This has no test that can see it.** A unit test can assert that a `style` attribute exists; it
|
|
98
|
+
cannot see that the file looks right. See verification.
|
|
99
|
+
|
|
100
|
+
### Verification
|
|
101
|
+
|
|
102
|
+
**Open the exported files.** That is the whole gate, and its absence is why this defect shipped: the
|
|
103
|
+
SVG in a viewer and the PNG in an image viewer, both compared against the rendered panel.
|
|
104
|
+
|
|
105
|
+
Do it for **two** panels with different shapes — one with a hand-built legend and size key
|
|
106
|
+
(`AccessGapScatterPanel`), one without — because the legend behaviour is precisely what differs
|
|
107
|
+
between the two export paths.
|
|
108
|
+
|
|
109
|
+
Unit tests: the SVG clone carries inlined styles for tick labels and axis strokes; both entry points
|
|
110
|
+
still resolve with their existing signatures; the PNG path resolves for a container holding more than
|
|
111
|
+
one SVG.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Part B — Implementation map
|
|
116
|
+
|
|
117
|
+
`.claude/models.local.json` has no local override, so the default applies: `implementation.runtime`
|
|
118
|
+
is `claude`/`sonnet` — the Orchestrator implemented directly, no Codex dispatch.
|
|
119
|
+
|
|
120
|
+
**Dependency decision (risk section, and Part C's approval gate):** no library added. Checked
|
|
121
|
+
first, per the WO's own instruction, and it held up: the platform's own `<foreignObject>` → `Image`
|
|
122
|
+
→ `<canvas>` pipeline works for arbitrary DOM rasterisation with no dependency at all. The one
|
|
123
|
+
obstacle — **confirmed live, not assumed** — is that Chrome taints the canvas on `toBlob`
|
|
124
|
+
unconditionally the moment the source SVG contains a `<foreignObject>` with HTML content, even with
|
|
125
|
+
zero external references (a minimal repro: a `<foreignObject>` holding one `<div>` with inline
|
|
126
|
+
`color: red` styling and no images/fonts/external refs still throws `SecurityError: Tainted
|
|
127
|
+
canvases may not be exported` when the SVG is loaded via `URL.createObjectURL`). The SAME SVG loaded
|
|
128
|
+
via a `data:` URI instead does **not** taint the canvas — confirmed with an identical minimal repro,
|
|
129
|
+
then re-confirmed end-to-end against the real `exportChartPng`/`ChartFrame` (real Chrome, via the
|
|
130
|
+
dev harness). This is why the approval gate never triggered: the platform-only approach IS viable,
|
|
131
|
+
it just needed the `blob:` → `data:` URI swap in `rasterize` (`exportChart.js`), documented inline
|
|
132
|
+
there.
|
|
133
|
+
|
|
134
|
+
**Canvas tainting on the PRE-FIX path:** checked per the risk section's own instruction ("may
|
|
135
|
+
already be failing on it"). It was not — a plain SVG-only blob (no `foreignObject`) loaded via
|
|
136
|
+
`URL.createObjectURL` does **not** taint the canvas; the exposure is specific to the `foreignObject`
|
|
137
|
+
this WO's PNG path adds, not a pre-existing defect.
|
|
138
|
+
|
|
139
|
+
**Verification (both real-browser, via the dev harness's module graph — `dev/entries.jsx` never
|
|
140
|
+
committed a probe entry, all verification was done by dynamically rendering `ChartFrame` into a
|
|
141
|
+
detached container and calling the real `exportChartSvg`/`exportChartPng`):**
|
|
142
|
+
|
|
143
|
+
- Panel shape WITH a hand-built legend + size key (mirroring `AccessGapScatterPanel`: MUI's own
|
|
144
|
+
legend hidden, a `<div>` HTML legend with a coloured swatch, a second `<svg>` "size key" beside
|
|
145
|
+
the chart): PNG rasterised at 2x scale, no taint, and pixel-sampling the produced PNG at the
|
|
146
|
+
swatch's and size-key's known screen coordinates read back their EXACT source colours
|
|
147
|
+
(`rgb(25,118,210)` / `#1976d2` and `rgb(156,39,176)` / `#9c27b0`) — the legend and size key are
|
|
148
|
+
genuinely present in the raster, not just "no error thrown". SVG export of the same panel:
|
|
149
|
+
contains `MuiChartsSurface-root` (the chart), does NOT contain the legend's own text/markup —
|
|
150
|
+
chart-only as designed.
|
|
151
|
+
- Panel shape WITHOUT a hand-built legend (plain `BarChart` inside `ChartFrame`): both exports
|
|
152
|
+
resolve cleanly, no regression for the common case.
|
|
153
|
+
|
|
154
|
+
Context package used: `exportChart.js` (rewritten in full, was short), `ChartFrame.jsx:42-108`
|
|
155
|
+
(the `runExport`/`exportOptions` wiring) and `:137-151` (`chartRef`, the export root — confirmed
|
|
156
|
+
correct: it wraps `content` only, excluding the meta/export-button footer), `chartsTranslations.ts`
|
|
157
|
+
(new `EXPORT_SVG_TOOLTIP`/`EXPORT_PNG_TOOLTIP` keys, all four locales).
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Part C — Orchestrator only
|
|
162
|
+
|
|
163
|
+
> **STOP — everything below addresses the Orchestrator.**
|
|
164
|
+
|
|
165
|
+
**Tier 3 · verification: opened files for two panel shapes, plus the unit tests above**
|
|
166
|
+
|
|
167
|
+
Shared-core, and — depending on scope item 2's answer — a dependency addition, which is its own
|
|
168
|
+
approval surface under Core Behaviour. If a library is needed, **stop and get the operator's
|
|
169
|
+
approval for that specific dependency** before adding it; the approval given on 2026-08-25 was for
|
|
170
|
+
the approach, not for a named package.
|
|
171
|
+
|
|
172
|
+
- `reviewer` — the inlining's completeness and the taint/font questions.
|
|
173
|
+
- `ui_reviewer` — whether the two buttons now read as two different things to a user.
|
|
174
|
+
- `sec_reviewer` — not run; no auth, data or exposure surface.
|
|
175
|
+
|
|
176
|
+
### Release
|
|
177
|
+
|
|
178
|
+
**Minor → 3.3.0**, per the operator. The PNG's *content* changes (it now includes the legend and
|
|
179
|
+
footnotes), so the CHANGELOG entry must **lead** with that, in the style `UCM-CHART-15` established:
|
|
180
|
+
a consumer who bumps and finds their exported PNGs different should read why in the first line, not
|
|
181
|
+
find it in a bullet.
|
|
182
|
+
|
|
183
|
+
### Register
|
|
184
|
+
|
|
185
|
+
Row `UCM-CHART-17`, both review notes, the published version, landing SHA(s) in a second commit
|
|
186
|
+
before the push. Record what the opened files actually looked like — that evidence is the point.
|
|
187
|
+
|
|
188
|
+
Consumer adoption (hram's pin bump) is a separate row; a shared-core fix that is published but not
|
|
189
|
+
consumed is not finished.
|
|
190
|
+
|
|
191
|
+
### Log
|
|
192
|
+
|
|
193
|
+
- **2026-08-25** — Envelope authored after the operator reported exports producing graphically very
|
|
194
|
+
different plots. Diagnosed from source, not reproduced by the Expertenchat: the operator has seen
|
|
195
|
+
the files. Option C chosen — SVG stays vector and chart-only, PNG becomes what the screen shows.
|