Cycles
Design · c87

History Graph

activecreated 2026-09-11· last activity 2026-09-11· mode: assemblage (new pictogram form composed from DonutChart's

`DataHistoryGraph` component family (q-nuxt-layer) — a pictogram-style

Why

The initial prompt asked for a history chart whose columns are built from stacked, color-coded unit blocks (see history-graph.png) — flexible enough to show weekday words, day numbers, or dates across 7/30/60-day spans, one or more series, and to switch how many items a block represents (or fall back to continuous bars) depending on scale. It asked for the component and sub-components to be built in q-nuxt-layer, documented on /components with a wide range of demo datasets, and for chart colors to live in a small, separately-editable file rather than as props.

Decisions made with Mark before building: colors are a generic categorical palette, not agent-bound (no coupling to components/agent/registry.js); the component owns only the plot + legend, matching how DataChartUI/ DataDonutChart already work — the consuming page composes its own card/title/ dropdown chrome; and this cycle is exploratory/demo-only, with a rich /components demo across many synthetic datasets and no specific qdash page wiring yet.

Objective

A history-graph component namespace in q-nuxt-layer (DataHistoryGraph, DataHistoryGraphColumn, DataHistoryGraphLegend, DataHistoryGraphBlock) plus an editable palette.js, documented on /components with adminProps and a scenarios grid covering 7/30/60-day ranges, sparse/empty, and single-spike datasets.

Scope

  • q-nuxt-layer/components/data/HistoryGraph.vue — public component: label auto-detection, unit-scaling ("nice" 1/2/5×10ⁿ step so the tallest column stays under maxBlocksPerColumn), auto block/bar mode switch past 40 columns, tick thinning past 15 columns, theme-reactive series color resolution.
  • q-nuxt-layer/components/data/history-graph/Column.vue, Legend.vue, _/Block.vue — one time-bucket's stacked units, the swatch legend, and a single unit square.
  • q-nuxt-layer/components/data/history-graph/palette.js — the editable categorical palette (light/dark hex pairs), validated with the dataviz skill's validate_palette.js for both modes.
  • app/data/history-graph-fixtures.tsHistorySeries/HistoryPoint/ HistoryGraphData types + 5 fixtures (7-day/3-series, 30-day/2-series, 60-day/1-series, sparse week, single-spike).
  • app/data/component-registry.tsDataHistoryGraph entry.
  • app/pages/components/_demos/DataHistoryGraph.vue — interactive adminProps demo + scenarios grid.

Key Changes

  • 2026-09-11: Built the history-graph namespace on top of DonutChart's palette/legend precedent. The mock's black/blue/grey don't map to existing semantic tokens (no true blue token; a literal neutral gray always fails the dataviz validator's chroma floor by construction — that check exists specifically to keep "reads as gray" colors out of categorical data, so the third slot is a validated muted sage rather than literal gray). Landed the stacked-unit algorithm: a shared "nice" unit value keeps the tallest column under the block cap across any range, with auto-fallback to continuous bars past 40 columns (verified against the 60-day fixture) — reusing the same px-per-value scale in both modes so bar mode reads as the continuous version of the same chart, not a different one. Found and fixed two real bugs via Playwright verification against the running dev server: (1) a date-only ISO string ("2026-09-07") parses as UTC midnight, which showed the previous weekday in any timezone behind UTC — fixed by treating bare date strings as local calendar days; (2) resolving series color reactively from colorScheme.value (needed since these are literal hexes, not qp() CSS vars) hit the exact SSR/hydration-mismatch case ThemeToggle.vue already documents — fixed with the same mounted-gated pattern, verified by loading the page fresh with dark mode already stored (not just toggling live). Mark then flagged that columns were stretching to fill wide containers (flex-1 on both the columns row and each column) instead of sitting at their natural pictogram size — discussed two options and chose "fixed width that can shrink but never grow" over "always fixed" so a 7-day chart stays compact while a 60-day one still compresses to fit. Landed as an explicit per-format columnWidth (28/24/42px for weekday/day/date) on flex: 0 1 <width>px. Hit a second, subtler bug getting there: the columns wrapper's own flex-basis: auto didn't reliably resolve to the sum of its children's explicit flex-basis values — a flex item that's itself a flex container doesn't reliably auto-size that way, worse with min-w-0 in play — so columns silently shrank to ~19px with truncated labels even with acres of free space. Fixed by computing the wrapper's own width explicitly (n × columnWidth + gaps) instead of trusting auto. Mark also asked for square (not rounded) blocks and for block size / vertical gap between stacked blocks to be real props rather than baked-in constants — added blockSize/blockGap (defaults 6px/2px, down from the previous fixed 8px/4px) threaded from DataHistoryGraph through Column to Block, with the unit-scaling pitch (blockSize + blockGap) now derived from them instead of a hardcoded 12, and dropped rounded-sm from both blocks and bar-mode segments. Finally, Mark flagged (with a corrected reference image) that series should render as independent side-by-side lanes per day, not summed into one stacked column — reversing this session's earlier read of the mock. Reworked the scaling to key off the tallest single-series value (not the per-day sum), since lanes no longer share a column's height budget, and Column now renders one fixed-width lane per series (flex-col-reverse, its own baseline-aligned stack) inside a centered row, instead of one interleaved stack. Column width now also accounts for the lanes' combined width (seriesCount × blockSize + gaps), not just the label's needs, so charts with more series than a short label can fit don't crowd their lanes. Last, promoted the previously-hardcoded 8px gap between day columns to a dayGap prop (default unchanged at 8px) alongside blockSize/blockGap, replacing the static gap-2 Tailwind class with an inline style bound to the prop. Mark then reported blockSize causing "massive top padding" when increased on the demo's slider — root cause was the shared dev/meta/PropsMachine.vue control's range/text v-model lacking a .number modifier, so an edited numeric adminProp arrives as a string; Column's pitch = props.blockSize + props.blockGap then did string concatenation ("16" + 2 -> "162") instead of addition, and areaHeight = maxUnits × "162" re-coerced through multiplication to 972px for a chart that should have been 108px. columnWidth/columnsAreaWidth were already safe (their + only ever combines two products, which forces numeric coercion first) — fixed the one unsafe line with explicit Number(...) rather than touching the shared dev tool, which other consumers apparently avoid tripping since they use -/*// on their numeric adminProps, never a bare +. Rounded out the sizing props with labelSize (default 11px, matching the previous hardcoded text-11) — the label-driven minimum column width (LABEL_COLUMN_WIDTH) now scales proportionally with it so a bigger label doesn't truncate against a width calibrated for the old fixed 11px. Mark then caught a large empty gap under legendBelow — the columns wrapper sized itself with flex: 0 1 Xpx, and flex-basis follows the flex container's main axis; toggling legendBelow switches that axis from row to column, so the same Xpx (a width) was being applied as a height instead once the layout went vertical. Fixed by using width: Xpx directly, which is axis-independent and (via flex-basis:auto falling back to the width property) behaves identically to the old row-mode shrink behavior while also being correct in column mode. Last, linked the legend's label size to labelSize + 1 rather than a hardcoded text-15.

Outcome

Shipped and documented. All five fixtures render correctly in both themes; unit-scaling holds the tallest column under the cap from the 7-day to the 60-day range; block/bar auto-switch, tick thinning, and per-block/bar hover tooltips verified live against the dev server. No qdash page wiring — this cycle was exploratory/demo-only per the pre-build decision.

Artifacts

  • initial-prompt.md
  • initial-prompt.md

Qpoint Brand Style Guide