History Graph
`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 undermaxBlocksPerColumn), 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'svalidate_palette.jsfor both modes.app/data/history-graph-fixtures.ts—HistorySeries/HistoryPoint/HistoryGraphDatatypes + 5 fixtures (7-day/3-series, 30-day/2-series, 60-day/1-series, sparse week, single-spike).app/data/component-registry.ts—DataHistoryGraphentry.app/pages/components/_demos/DataHistoryGraph.vue— interactive adminProps demo + scenarios grid.
Key Changes
- 2026-09-11: Built the
history-graphnamespace on top ofDonutChart'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 fromcolorScheme.value(needed since these are literal hexes, notqp()CSS vars) hit the exact SSR/hydration-mismatch caseThemeToggle.vuealready documents — fixed with the samemounted-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-1on 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-formatcolumnWidth(28/24/42px for weekday/day/date) onflex: 0 1 <width>px. Hit a second, subtler bug getting there: the columns wrapper's ownflex-basis: autodidn'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 withmin-w-0in 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 trustingauto. 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 — addedblockSize/blockGap(defaults 6px/2px, down from the previous fixed 8px/4px) threaded fromDataHistoryGraphthroughColumntoBlock, with the unit-scaling pitch (blockSize + blockGap) now derived from them instead of a hardcoded 12, and droppedrounded-smfrom 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, andColumnnow 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 adayGapprop (default unchanged at 8px) alongsideblockSize/blockGap, replacing the staticgap-2Tailwind class with an inline style bound to the prop. Mark then reportedblockSizecausing "massive top padding" when increased on the demo's slider — root cause was the shareddev/meta/PropsMachine.vuecontrol's range/textv-modellacking a.numbermodifier, so an edited numeric adminProp arrives as a string;Column'spitch = props.blockSize + props.blockGapthen did string concatenation ("16" + 2 -> "162") instead of addition, andareaHeight = maxUnits × "162"re-coerced through multiplication to 972px for a chart that should have been 108px.columnWidth/columnsAreaWidthwere already safe (their+only ever combines two products, which forces numeric coercion first) — fixed the one unsafe line with explicitNumber(...)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 withlabelSize(default 11px, matching the previous hardcodedtext-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 underlegendBelow— the columns wrapper sized itself withflex: 0 1 Xpx, and flex-basis follows the flex container's main axis; togglinglegendBelowswitches 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 usingwidth: Xpxdirectly, 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 tolabelSize + 1rather than a hardcodedtext-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