fix(audit): chart colours + Charts-tab layout regressions #175

Merged
julien merged 1 commits from fix/audit-chart-colours-and-layout into main 2026-05-17 01:17:34 +02:00
Owner

Summary

Follow-up polish on the audit-log Charts tab shipped in #174. Three small but visible regressions surfaced once real data was loaded:

  1. "Events per day" bar chart cycled a categorical palette across the X axis — every day got a different colour, which read as "categorical meaning per day" when the X axis is just a time bucket. Switched to a single fixed fill (curated blue from the lib palette).
  2. "Outcome breakdown" donut painted slices in Set2 ordersuccess came out teal, denied came out orange. With orange = "danger" in most readers' mental model, this is the wrong way around. Added a colorMap input on <lib-donut-chart> and a curated semanticStatusColors export so the audit page can map success → green, denied → orange, failure → red.
  3. Charts tab caused a body scrollbar + empty space below the footer. Plot renders into a hidden [hidden] panel on first switch, so canvas.clientWidth is 0 and Plot falls back to its 600/720 default — the resulting fixed-width SVG was wider than the grid column, dragged horizontal overflow up the tree, and (because the grid item defaults to min-width: auto) wouldn't shrink. The shell layout broke. Fixed by constraining the chart envelope and the grid items so the SVG can scale down to the actual column width.

What lands

Bar chart — single fill, lib-owned default

  • Plot.barY mark no longer encodes colour from xKey. The fill parameter is now a literal-string colour, applied uniformly to every bar.
  • New fillColor input on <lib-bar-chart> for the rare case a consumer needs a different shade; defaults to DEFAULT_BAR_FILL = '#1d4ed8' (≈ Tailwind blue-700, picked for AA contrast against both light and dark surfaces, colour-blind-safe).
  • Removed the now-unused color: { type: 'ordinal', range: palette } scale and the colorScheme input on the bar chart — the categorical palette only made sense when fill: xKey was the default behaviour.

Donut chart — optional semantic mapping

  • New colorMap?: Readonly<Record<string, string>> input on <lib-donut-chart>. When provided, slice fill resolves to colorMap[category] first, falling back to the categorical palette for any unmapped category. Lib still owns the palette per ADR-0023; the map only constrains which colour the consumer picks for which category, not what colours exist.
  • New semanticStatusColors export from shared-charts — a curated 5-entry map (success / warning / error / info / neutral) tuned for AA contrast and deuteranopia/protanopia distinguishability. Consumers stay inside the lib's palette without authoring their own hex codes.
  • The audit page now ships outcomeColorMap = { success: green, failure: red, denied: orange } and passes it to the donut. The donut's description (screen-reader fallback) and the legend chip semantics now line up.

Chart envelope — overflow containment

libs/shared/charts/src/lib/_internal/chart-envelope.scss:

  • Force display: block + min-width: 0 on every chart host element (lib-bar-chart, lib-donut-chart, lib-stacked-bar-chart). Angular custom-element hosts default to display: inline, which lets the inner <figure> escape parent sizing constraints — the root cause of the body-scrollbar symptom.
  • .chart-canvas gets min-width: 0 and constrains every inner figure { max-width: 100% } + svg { max-width: 100%; height: auto }. The SVG keeps its viewBox aspect ratio while scaling down to the actual column width.

apps/portal-admin/src/app/pages/audit/audit.scss:

  • .chart-tile { min-width: 0; } — grid items default to min-width: auto, which prevents shrinking below the intrinsic content width. Without it, Plot's fixed-width SVG could still push the grid column past 1fr even with the lib-side fixes.

Notes for the reviewer

  • Why a hardcoded hex (#1d4ed8) for DEFAULT_BAR_FILL rather than a brand token? The chart lib is intentionally decoupled from libs/shared/tokens — it has no SCSS/CSS-vars dependency and Plot writes the fill as an SVG attribute, not a CSS value, so a CSS variable wouldn't apply anyway. The hex stays inside the lib, marked as the canonical default, with the docstring promising AA contrast + colour-blind safety. If the brand wants to take it over later, swap the constant in one place.
  • semanticStatusColors is a curated map, not an open extension point. Five entries (success / warning / error / info / neutral) covers the audit module and any future status-bearing donut (user list, integrations health, etc.). Adding a sixth entry needs a small PR + an a11y-contrast check, which is the right friction.
  • The aria-label="bar" selector in the new bar-chart spec. Plot tags its bar-mark <g> with aria-label="bar" (and similarly "rule" for Plot.ruleY). Selecting on it scopes the fill-uniqueness check to actual bars, not axes / ticks / labels. If Plot changes that label upstream the spec breaks loudly — preferable to a fragile geometric selector.
  • Lazy fetch policy and the empty-state edge case from #174 are unchanged. The donut still receives colorMap even when data is empty; the lib's arcs.forEach short-circuits on zero arcs so no colour lookup happens.
  • audit.scss is now 7.5 KB, still over the 6 KB component-style warning (untouched from #174) and well under the 8 KB error. The new .chart-tile { min-width: 0 } rule is two lines.

Test plan

  • pnpm nx test shared-charts15 specs pass (was 14: +1 donut colorMap test, +1 bar single-fill test, -1 obsolete bar palette assumption).
  • pnpm nx test portal-admin62 specs pass (unchanged from #174 — semantic mapping is a template-only change, behavioural tests still hold).
  • pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-charts — clean. Same three pre-existing lint warnings, no new ones; bundle sizes unchanged within ±0.1 KB.
  • Manual smokepnpm nx serve portal-admin + pnpm nx serve portal-bff, sign in with Portal.Admin, navigate to /admin/audit:
    • Switch to Charts tab — daily-volume bars all render the same blue.
    • Donut centre matches s.total, the green slice is success, the orange slice is denied, the red slice is failure. Hover each slice — <title> shows <category>: <count> (untouched a11y contract from the lib).
    • Body scrollbar stays absent; footer stays anchored at the viewport bottom with no white-space gap.
    • Resize the window through the (max-width: 800px) breakpoint — grid collapses to one column, charts re-flow without horizontal overflow.
    • Dark mode toggle — colours stay readable, no contrast regressions.

What's next

Nothing in this chantier. The audit dashboard is now visually coherent and layout-stable. Two background items to track separately when the CMS / user-list pages land their own dashboards:

  • Tab-state URL persistence (carried forward from #174's "what's next").
  • Promote the WAI-ARIA tab pattern out of audit.html into libs/shared/ui/tabs/ once a second consumer appears.
## Summary Follow-up polish on the audit-log Charts tab shipped in #174. Three small but visible regressions surfaced once real data was loaded: 1. **"Events per day" bar chart cycled a categorical palette across the X axis** — every day got a different colour, which read as "categorical meaning per day" when the X axis is just a time bucket. Switched to a single fixed fill (curated blue from the lib palette). 2. **"Outcome breakdown" donut painted slices in Set2 order** — `success` came out teal, `denied` came out orange. With orange = "danger" in most readers' mental model, this is the wrong way around. Added a `colorMap` input on `<lib-donut-chart>` and a curated `semanticStatusColors` export so the audit page can map `success → green`, `denied → orange`, `failure → red`. 3. **Charts tab caused a body scrollbar + empty space below the footer.** Plot renders into a hidden `[hidden]` panel on first switch, so `canvas.clientWidth` is 0 and Plot falls back to its 600/720 default — the resulting fixed-width SVG was wider than the grid column, dragged horizontal overflow up the tree, and (because the grid item defaults to `min-width: auto`) wouldn't shrink. The shell layout broke. Fixed by constraining the chart envelope and the grid items so the SVG can scale down to the actual column width. ## What lands ### Bar chart — single fill, lib-owned default - `Plot.barY` mark no longer encodes colour from `xKey`. The `fill` parameter is now a literal-string colour, applied uniformly to every bar. - New `fillColor` input on `<lib-bar-chart>` for the rare case a consumer needs a different shade; defaults to `DEFAULT_BAR_FILL = '#1d4ed8'` (≈ Tailwind blue-700, picked for AA contrast against both light and dark surfaces, colour-blind-safe). - Removed the now-unused `color: { type: 'ordinal', range: palette }` scale and the `colorScheme` input on the bar chart — the categorical palette only made sense when `fill: xKey` was the default behaviour. ### Donut chart — optional semantic mapping - New `colorMap?: Readonly<Record<string, string>>` input on `<lib-donut-chart>`. When provided, slice fill resolves to `colorMap[category]` first, falling back to the categorical palette for any unmapped category. Lib still owns the palette per ADR-0023; the map only constrains *which colour the consumer picks for which category*, not what colours exist. - New `semanticStatusColors` export from `shared-charts` — a curated 5-entry map (`success / warning / error / info / neutral`) tuned for AA contrast and deuteranopia/protanopia distinguishability. Consumers stay inside the lib's palette without authoring their own hex codes. - The audit page now ships `outcomeColorMap = { success: green, failure: red, denied: orange }` and passes it to the donut. The donut's `description` (screen-reader fallback) and the legend chip semantics now line up. ### Chart envelope — overflow containment `libs/shared/charts/src/lib/_internal/chart-envelope.scss`: - Force `display: block` + `min-width: 0` on every chart host element (`lib-bar-chart`, `lib-donut-chart`, `lib-stacked-bar-chart`). Angular custom-element hosts default to `display: inline`, which lets the inner `<figure>` escape parent sizing constraints — the root cause of the body-scrollbar symptom. - `.chart-canvas` gets `min-width: 0` and constrains every inner `figure { max-width: 100% }` + `svg { max-width: 100%; height: auto }`. The SVG keeps its viewBox aspect ratio while scaling down to the actual column width. `apps/portal-admin/src/app/pages/audit/audit.scss`: - `.chart-tile { min-width: 0; }` — grid items default to `min-width: auto`, which prevents shrinking below the intrinsic content width. Without it, Plot's fixed-width SVG could still push the grid column past `1fr` even with the lib-side fixes. ## Notes for the reviewer - **Why a hardcoded hex (`#1d4ed8`) for `DEFAULT_BAR_FILL` rather than a brand token?** The chart lib is intentionally decoupled from `libs/shared/tokens` — it has no SCSS/CSS-vars dependency and Plot writes the fill as an SVG attribute, not a CSS value, so a CSS variable wouldn't apply anyway. The hex stays inside the lib, marked as the canonical default, with the docstring promising AA contrast + colour-blind safety. If the brand wants to take it over later, swap the constant in one place. - **`semanticStatusColors` is a curated map, not an open extension point.** Five entries (`success / warning / error / info / neutral`) covers the audit module and any future status-bearing donut (user list, integrations health, etc.). Adding a sixth entry needs a small PR + an a11y-contrast check, which is the right friction. - **The `aria-label="bar"` selector in the new bar-chart spec.** Plot tags its bar-mark `<g>` with `aria-label="bar"` (and similarly `"rule"` for `Plot.ruleY`). Selecting on it scopes the fill-uniqueness check to actual bars, not axes / ticks / labels. If Plot changes that label upstream the spec breaks loudly — preferable to a fragile geometric selector. - **Lazy fetch policy and the empty-state edge case** from #174 are unchanged. The donut still receives `colorMap` even when `data` is empty; the lib's `arcs.forEach` short-circuits on zero arcs so no colour lookup happens. - **`audit.scss` is now 7.5 KB**, still over the 6 KB component-style warning (untouched from #174) and well under the 8 KB error. The new `.chart-tile { min-width: 0 }` rule is two lines. ## Test plan - [x] `pnpm nx test shared-charts` — **15 specs pass** (was 14: +1 donut `colorMap` test, +1 bar single-fill test, -1 obsolete bar palette assumption). - [x] `pnpm nx test portal-admin` — **62 specs pass** (unchanged from #174 — semantic mapping is a template-only change, behavioural tests still hold). - [x] `pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-charts` — clean. Same three pre-existing lint warnings, no new ones; bundle sizes unchanged within ±0.1 KB. - [ ] **Manual smoke** — `pnpm nx serve portal-admin` + `pnpm nx serve portal-bff`, sign in with `Portal.Admin`, navigate to `/admin/audit`: - Switch to **Charts** tab — daily-volume bars all render the same blue. - Donut centre matches `s.total`, the green slice is `success`, the orange slice is `denied`, the red slice is `failure`. Hover each slice — `<title>` shows `<category>: <count>` (untouched a11y contract from the lib). - Body scrollbar stays absent; footer stays anchored at the viewport bottom with no white-space gap. - Resize the window through the `(max-width: 800px)` breakpoint — grid collapses to one column, charts re-flow without horizontal overflow. - Dark mode toggle — colours stay readable, no contrast regressions. ## What's next Nothing in this chantier. The audit dashboard is now visually coherent and layout-stable. Two background items to track separately when the CMS / user-list pages land their own dashboards: - Tab-state URL persistence (carried forward from #174's "what's next"). - Promote the WAI-ARIA tab pattern out of `audit.html` into `libs/shared/ui/tabs/` once a second consumer appears.
julien added 1 commit 2026-05-17 01:17:23 +02:00
fix(audit): chart colours + Charts-tab layout regressions
CI / scan (pull_request) Successful in 4m47s
CI / commits (pull_request) Successful in 4m48s
CI / check (pull_request) Successful in 5m9s
CI / a11y (pull_request) Successful in 4m14s
CI / perf (pull_request) Successful in 9m53s
56760bd6ba
- Bar chart now uses a single fixed fill (DEFAULT_BAR_FILL = #1d4ed8)
  instead of cycling Set2 across the X axis. Time-bucket bars carry
  no per-day semantics; one colour reads correctly.
- Donut chart accepts an optional colorMap so the audit page can map
  success → green, denied → orange, failure → red. New
  semanticStatusColors export keeps consumers inside the lib's
  curated palette per ADR-0023.
- Chart envelope and audit grid items get min-width: 0 + max-width:
  100% on the inner SVG/figure, fixing the body scrollbar + footer
  gap caused by Plot's fixed-width fallback render on a hidden tab
  panel.
julien merged commit 9e7eb4af15 into main 2026-05-17 01:17:34 +02:00
julien deleted branch fix/audit-chart-colours-and-layout 2026-05-17 01:17:35 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#175