fix(charts): honour series opacity and dashArray on every series, not only a comparison one - #8159
Conversation
`@objectstack/spec` declares `ChartSeries.opacity` and `ChartSeries.dashArray` as unconditional per-series overrides. `normalizeSeries` read both off every series and `comparisonStyle` then discarded them for anything but a `variant: 'comparison'` overlay, so an authored key on a primary series drew exactly like no key at all. Two gaps, not one. The `variant` guard was the visible half; the second only showed on `dashArray`, which `comparisonStyle` already returned for every family while the Bar and Scatter marks passed `fillOpacity` only and dropped `strokeDasharray` / `strokeOpacity` — broken on those families even for a comparison series. `comparisonStyle` becomes `seriesStyle`: the authored values apply whatever the variant, the muted overlay DEFAULTS stay gated on `variant: 'comparison'`, and Bar / Scatter now pass all three channels. The two stroke defaults no mark ever consumed (bar, scatter — neither is stroked here) are spelled `undefined` so opening those props hands them no default they never had; every value a comparison series painted before is unchanged. The `ChartDataSeries` mirror docs and the plugin-charts reference stated the comparison-only condition as an interim measure; both are corrected here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
|
Contract review — PASS — PR #8159 (objectui#7698), head Reviewed-by: ⭐ The Zone-2 clause worked: the correction I handed over was itself incompleteMy dispatch handed the dev comment 5550238143's mark-component table explicitly as the previous seat's reading, ⛔ not as fact, and told it to rebuild the census with its own lit control. It did — and found the correction short. Re-measured by this seat on ⇒ seven, not the card's five (with stale numbers) and not the correction's four. The correction omitted both Line sites and Area The mechanism claim also confirmed at ① Derived judgmentsClause-②: NO, verified. The Option A implemented as adjudicated, and both halves closed. ⭐⭐ The sharpest thing in this PR, and it is a defect the fix could easily have introduced. The old helper computed
⇒ opening a channel can leak a previously-inert default. That is not in the card, not in triage, and not in my dispatch. It is the kind of thing that ships as "a small visual regression nobody can explain". ② semver and read-coupling
The read-coupling my dispatch made binding is satisfied in BOTH places — PR #7684's interim "comparison-only" wording is removed from the ③ Boundary flags
⭐ Out-of-scope finding declined for a measured reason, not a scope reflex. objectui#8157 — the radar mark hard-codes VerdictPASS. No Clause-② carrier is owed. ⛔ Not landing yet — CI has just started: 32 enumerated against Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…27-plugin-ai Keeps the batch-27 work on a head that carries #8159. The merged commit touches packages/plugin-charts, packages/types/src/data-display.ts and content/docs/plugins/plugin-charts.mdx -- disjoint from this branch's file surface (packages/plugin-ai/README.md, scripts/check-doc-snippet-types.mjs), so every gate reading in the PR body is pinned to the merged head. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Fixes #7698
@objectstack/specdeclaresChartSeries.opacity("Override series opacity") andChartSeries.dashArray("Override stroke dash pattern") as unconditional per-series overrides, and@object-ui/types'ChartDataSeriesSchemamirrors both.normalizeSeriesread them off every series and the renderer then honoured them on avariant: 'comparison'overlay only — so an author who wrote{ name: 'cost', opacity: 0.6 }on a primary series got a mark drawn exactly as if the key were absent. Option A per the card's adjudication: the renderer moves, not the declaration. Narrowing the published face to "comparison only" would be the renderer's tolerance dictating the contract, the direction AGENTS.md #0.1 forbids.My own measurement — the card's body is wrong about where the defect lives, and the correction holds
Re-measured on
origin/main@c14d3a090with the card's own census control rebuilt (grep -rniE 'opacity|dasharray' packages/plugin-charts/src, tests excluded). The card's body says all fivecmp?.…call sites applyfillOpacity/strokeOpacity/strokeDasharray. They do not — and there are seven of them, not five (the body's list omits the two Line sites):c14d3a090:1832fillOpacityonly:1898strokeOpacity,strokeDasharray:1905fillOpacity,strokeOpacity,strokeDasharray:1911fillOpacityonly:2037fillOpacityonly:2048strokeOpacity,strokeDasharray:2056fillOpacity,strokeOpacity,strokeDasharrayAnd
comparisonStyle:96wass.dashArray ?? (kind === 'line' || kind === 'area' ? '4 4' : undefined)— the??takes the left side whatever the kind, so the helper already returned an authored dash for bar and scatter too. Confirmed: what dropped it is the mark component, not thevariantguard, and a fix aimed only at the guard would not have repaireddashArrayat all.The ablation below is the lit control for that claim, not the reading alone. Against the pre-fix implementation, the comparison-bar pin reports
fillOpacity: '0.4'present andstrokeDasharray: null— the muted default arrived, the authored dash did not. That is the mark-component half, isolated.The fix — two halves, gated differently
comparisonStylebecomesseriesStyleand no longer returnsnull:opacity/dashArrayapply whatever thevariant.'4 4'line/area dash) stay gated onvariant === 'comparison'.strokeOpacityandstrokeDasharraythrough, so the authored dash is no longer dropped at the mark.Existing comparison series are byte-for-byte unaffected. The authored branch already won over the defaults, and the two stroke defaults no mark ever consumed (bar
0.6, scatter0.5— this renderer strokes neither family) are now spelledundefined, so openingstrokeOpacityon those marks hands them no default they never had. Pinned directly: a comparison bar carrying no keys keepsfill-opacity="0.4"and gains neitherstroke-opacitynorstroke-dasharray.A primary series carrying neither key is untouched — pinned as all three attributes absent.
Only a stroked mark can show a dash, and this renderer strokes Line and Area, so an authored
dashArrayon a bar or scatter reaches the mark and paints nothing. That is the mark's geometry, not a condition on the key, and the docs now say exactly that rather than describing it as a restriction.Read-coupling to PR #7684 (binding, discharged)
#7684 landed and stated the comparison-only condition as an interim measure. That wording is now wrong and comes out in this same change, per objectui#7070:
packages/types/src/data-display.ts— theChartDataSeries.opacityand.dashArrayJSDoc. Comment-only:git diff -U0on that file yields zero changed lines outside a block comment.content/docs/plugins/plugin-charts.mdx— both series-table rows plus the paragraph under the area example. Prose only: the three changed lines are 193, 194 and 198, and the fences in that region are 162-179, 200-216, so no compiled snippet moved.Scope held
packages/core/src/utils/chart-presentation.tswas not touched — itsseriesPresentationalready forwards both keys on the dataset path, so the dispatch's flagged out-of-plugin file stays out of this PR.Filed separately rather than folded in: objectui#8157 — the radar mark maps over
seriesbut hard-codesfillOpacity={0.6}and never calls the helper, soopacity,dashArrayand the whole comparison treatment are inert on a radar chart for every variant. Repairing it would newly mute existing comparison radar series (0.6becomes the helper's0.5), a visible change to already-drawn charts that wants its own decision. Not addressed here; it stays open.Not touched here either, all separate cards in the same measurement family: objectui#7690, objectui#7694, objectui#7682, objectui#7546.
Verification
All runs on the final commit
5d68aecbbunless stated. Heavy runs went through the container's shared verify lock; wall-clock figures are shared-box seconds.Tests
pnpm exec vitest run packages/plugin-charts/— 48 files, 447 tests passed.pnpm exec vitest run packages/types/ packages/plugin-dashboard/ packages/plugin-report/ examples/schema-catalog/— 271 files, 5611 tests passed (the chart-rendering consumers).packages/plugin-charts/src/ChartRenderer.seriesPresentation.test.tsx, 13 tests.Reverse verification (ablation). Predicted direction: RED. The one file was restored to its
c14d3a090bytes, with the mutation proven on disk before the run —git hash-objectof the working file equalled the pre-fix blob63a5e34c9and differed from the HEAD blobae52b1d01; anchored counts movedseriesStyle6 to 0 andcomparisonStyle0 to 6. Result: 8 of 13 failed, and the 5 that passed are exactly the regression pins (the comparison defaults, the untouched primary series, and an authoredopacityoverriding a comparison default — all of which held before this change and must keep holding). Restore leg proven by state, not by an exit code:git diff HEADempty,git statusclean, working blob back toae52b1d01. The script carried an absolute-pathtrap ... EXIT INT TERM.Type checking.
pnpm --filter @object-ui/plugin-charts --filter @object-ui/types type-check— exit 0. This package'stype-checkistsc --noEmit && tsc -p tsconfig.test.json, so the new test file is type-checked too rather than excluded.Gates run locally (all exit 0, quoting each gate's own verdict line):
check:control-bytes,check:vi-mock-specifiers,check:vi-mock-inherit,check:doc-fences,check:doc-types,check:doc-links,check:doc-expression-carriage,check:doc-example-readers,check:unreferenced-sources,check:spec-symbols,check:shell-escape-residue,check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check-changeset-overwrite, andcheck-governed-queue-guard --teston all five changed paths (NOT GOVERNED, 5 of 5).Two gates NOT MEASURED locally, left to CI — both exited on a stated precondition, neither is a finding:
check:doc-snippets— exit 2,PRECONDITION NOT MET: it needs 32 packages plus closures built, which does not fit this container's foreground budget under contention. It compiles documented TS blocks, and this diff changes none (see the fence arithmetic above).check:eager-closure— exit 2, noapps/console/dist/eager-closure.json, so no ceiling to weigh. The diff adds no import, so the eager payload is unchanged.Lint — a declared, measured narrowing. Repo-wide lint (
turbo run lint, 40 packages each runningeslint .) is CI's run. Locally, ESLint ran on the three changed source files: 3 files linted, 0 errors, 77 warnings, file count read from--format jsonoutput length. Every warning is pre-existing@typescript-eslint/no-explicit-anyinAdvancedChartImpl.tsx; the diff adds no newany(the replaced helper already tooks: any), and no--max-warningsis set anywhere in the lint pipeline —lint.ymlsays so in as many words. Invariance for untouched files: type-aware linting is not enabled —eslint.config.jssets noparserOptions.projectand noprojectService(the only matches in the repo are twoRuleTesterfixtures undereslint-rules/), so every verdict is a per-file function of that file's own bytes, and this diff cannot move the verdict of a file it does not touch.Changeset.
.changeset/7698-series-opacity-dasharray-unconditional.md,@object-ui/plugin-charts: minor+@object-ui/types: patch, nomajor(AGENTS.md version alignment). Not askip-changesetPR — this ships a user-visible behaviour change.🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code