Dashboard “Page hits — by month” — CSS fix shipped 2026-05-15, D3 reimagine still parked
DARE.CO.UK · PARKED SKETCH · 2026-05-18
Mirrored from ~/.claude/.../memory/project_dashboard_month_bar_height_parked.md. This is a design sketch parked for future build — read for context, not as a current deliverable.
Immediate CSS fix shipped 2026-05-15 (commit 9a03c2f in dare-pipeline) —
.trend-chart { aspect-ratio: 5/2 }+ SVG fills the box so Weekday + Month views render at matched physical height despite different viewBox aspects (496×200 vs 298×200). The deeper D3-style reimagine (sparkline / heat-tile / slope-chart / two-row-no-toggle alternatives) stays parked for sketch-first deliberation.
2026-05-15 EVENING — immediate CSS fix shipped
Root cause: the two views’ SVGs had different viewBox widths (Weekday 0 0 496 200, Month 0 0 298 200) rendered at width: 100%. The narrower Month viewBox at 100% width produced bars ~1.66× taller than Weekday because the SVG scaled its height up to maintain its native aspect (1.49:1 vs Weekday’s 2.48:1).
Fix applied to ~/Code/dare-pipeline/scripts/dare_cf_analytics.py (canonical) + ~/bin/dare_cf_analytics.py (local mirror):
.trend-chart { ... aspect-ratio: 5 / 2; }
.trend-chart svg { display: block; width: 100%; height: 100%; }
The container now forces a 5:2 (≈2.5:1) aspect close to Weekday’s natural viewBox. Both views fill the same physical box; Month SVG centers horizontally within it via the existing preserveAspectRatio="xMidYMid meet". No code change to bar-rendering or score logic — purely CSS.
Shipped via dare-pipeline GHA (commit 9a03c2f on origin/main, manually triggered workflow_dispatch since the scheduled run is 0 5,17 * * * UTC and the next natural fire was hours away).
2026-05-15 LATE EVENING — variant picked: two-mode bar (spike vs baseline)
Sketches shipped to devreports as dare_ab_preview_pagehits_reimagine_2026-05-15 (calendar heatmap + two-mode bar side-by-side). Dan picked Sketch B — two-mode bar, framing it as: “Seems like it brings some additional layer of context to the graph.” That’s the editorial value of the variant in one line — the bar still answers “what was the total?” but now also answers “how much was event vs floor?” within the same shape.
Open build questions to resolve on resume
- Spike threshold — sketch used 6,000 hits/day (caught 6 days: 5 in mid-March + Apr 28 Tue). Editorially defensible as top-decile (6/70 = 8.6% of active days). Make threshold a CLI/config knob:
--spike-threshold Ndefaulting to 6000; revisit once more months accumulate. - Spike-day tooltip — hovering the red top segment should surface the spike dates + values + cache% + plausible cause stub. The current sketch SVG
<title>carries the basics; the dashboard’s existingbar-tooltipelement pattern is richer — port that across so spike segments get the same drill-down affordance the baseline segments do. - Weekday view scope — sketch only applied to Month axis. Weekday two-mode (partitioning each weekday’s 13 weeks into spike-week vs baseline-week) is probably noise at that granularity. Keep Weekday as the single-bar variant; only apply two-mode to Month.
- Stack order is load-bearing — baseline at bottom, spike on top. Reads as “baseline is the floor, spike is the event.” Flipped stacks read as “spike anchors the bar” — opposite editorial signal. Don’t flip.
- Click-through future — clicking a spike segment could deep-link to a per-day investigation report (top URLs, cache%, referrers for that exact date). Surface as TODO comment in the bar_chart code; don’t block the v1 ship on it.
- Where the work lands —
bar_chart()function in~/Code/dare-pipeline/scripts/dare_cf_analytics.py(canonical) gains the two-mode partition logic. Mirror to~/bin/dare_cf_analytics.py. Test locally first; only push to GHA after the SVG diffs cleanly against the sketch.
Resume invocation
The sketch generator at ~/Downloads/dare_ab_preview_pagehits_reimagine_build.py carries the rendering logic ready to port — render_two_mode_bar(daily) is the function to lift into bar_chart(). Local test:
python3 ~/bin/dare_cf_analytics.py --output /tmp/dashboard_test.html
# open, eyeball, iterate; once clean → dare-pipeline commit + push + manual workflow_dispatch
2026-05-15 LATE EVENING — Sketch D parked: verdict-headline + traffic-light range (Google Flights pattern)
Dan shared the Google Flights “Prices are currently typical for your search” panel as exemplar; saved the underlying principle to feedback_simple_charts_disproportional_weight.md. The pattern applied to dare’s dashboard top-strip:
Each top-strip metric becomes a one-line verdict + range strip, replacing the bare number layout:
TRAFFIC IS TYPICAL 9,741 requests
[green |--|--●--|--| red] typical range 7-12K (last 30d)
CACHE HEALTH IS STRONG 62.1% hit ratio
[● green band|--|--|--| red] typical 55-70% (last 30d)
THREAT VOLUME IS LOW 146 blocked
[● green|--|--|--| red] typical 100-300 (last 30d)
ERROR SHARE IS TYPICAL 2.3% 4xx
[green |--●--| red] typical 1.5-3.5% (last 30d)
Why this slot: the current top-strip is bare numbers. The verdict-headline + traffic-light range turns each metric into a positional judgment — answers “should I care?” in <2 seconds without needing to know the historical baselines. The cumulative-volume curve (Sketch C) handles the distributional view of any single metric; Sketch D handles the typical/atypical state across many metrics at once.
Verb vocabulary (small + consistent across the dashboard): - low / typical / strong / elevated / unusual for each metric, with “good_up” and “good_down” semantics inverting which colour band each verdict lands in. Reuse the same five words across all top-strip cards.
Threshold derivation: NOT magic numbers — each metric’s “typical band” is the 25th–75th percentile of the trailing 30-day window. Updates automatically. Matches feedback_state_emerges_from_data.md — derived state, no manual flags.
Where it lands in code: new component verdict_card(metric, current, history) that emits the SVG-strip + headline + receipt-number. Top-strip section of dare_cf_analytics.py renders 4-6 of them. Probably ~80 lines added; the cards are mostly chrome.
Resume condition for Sketch D: after Sketch B (two-mode bar) lands Monday + a week of normal use confirms no regressions. Sketch D is bigger surface change than B; sequence them.
2026-05-15 LATE EVENING — editorial north star (Dan, end of session)
“we are building momentum, as we are asking what’s simple and useful, what is easy to browse and understand.”
This is the editorial frame for the dashboard reimagine queue. Every sketch direction (B / C / D, and any future additions) gets tested against four questions:
- Is it simple? — fewer elements, fewer cognitive loads, fewer learn-curves
- Is it useful? — answers a question the viewer actually has, not a question we’re proud to answer
- Is it easy to browse? — verdict-in-2-seconds; eye can scan the dashboard without reading every word
- Is it easy to understand? — no jargon, no acronyms, no chart-types-that-need-a-legend
Sketches that fail any of the four get parked or revised, not shipped. The two-mode bar (Sketch B) passes all four. Sketch D (verdict + range) is designed from these four. Sketch C (cumulative-volume curve) is the edge case — it’s not simple in vocabulary (cumulative distributions aren’t intuitive) but it IS useful for the distributional question, so it earns a slot as a companion view, not a primary surface.
The momentum Dan named is real: in one evening the dashboard reimagine queue went from one parked item → variant picked + tested → companion sketch added → verdict pattern parked → editorial north star articulated. The next session resumes from “build Monday’s pick” not “decide what to do.”
2026-05-15 LATE EVENING — Sketch E parked: metric-nugget cards (with character)
Inspiration image at ~/Desktop/CleanShot 2026-05-15 at 23.04.25@2x.png (face/object recognition dashboard, four cards). Dan: “add this to the dashboard inspiration for simplified, meaningful nuggets of info.”
Pattern elements (each card):
| Slot | Role | Example from image |
|---|---|---|
| Background | Soft pastel — gives the card character + category signal without shouting | slate-teal, sage-green, dusty-rose, sandy-tan |
| Title (top-left) | What this card is measuring | “Total Number of Faces S…” (truncates — avoid that; use full label) |
| Number (large, bold) | The focal point — single hero metric | 14,578 / 8,942 / 92.5% / 25 |
| Icon (top-right, decorative) | Adds character; reinforces category without taking from the number | face / box / dial / mosaic |
| Context (bottom-left, smaller) | Time window or category | “Last 24H” / “Overall Accuracy” / “Detected Cate…” |
| Delta (bottom-right, smaller) | Direction-of-change marker | “+358” / “+245” / “+2.3%” — colour-coded |
Compounds with what we have:
- Sketch D (Google Flights verdict + range) is the positional judgment layer — green/yellow/red strip + verdict word
- Sketch E is the nugget composition layer — how to lay out each individual metric card so it reads as one unit
- They’re not competing — D answers “is today good?” and E answers “what is today?”. A v2 top-strip could combine: verdict-headline (D) + pastel-card layout (E) + delta and time-context (E) + colour-coded category (E). One row across the top, four nuggets, each carrying a verdict + a number + a delta.
Editorial test against the north star (simple · useful · easy to browse · easy to understand):
- ✅ Simple — three text lines + one icon per card. Quieter than a chart.
- ✅ Useful — answers four questions per card (what / number / over what / moving where) at a glance
- ✅ Easy to browse — pastel backgrounds form a visual grid; eye can scan across by colour or by row
- ⚠️ Easy to understand — passes if labels don’t truncate. The screenshot’s “Total Number of Faces S…” and “Image and Video Categ…” truncations are an anti-pattern; use full labels or shorter ones.
Cautions to bake in:
- Don’t truncate labels — the screenshot’s ellipses make the cards harder to read. Either short labels or wider cards.
- Pastel palette must respect dare’s
--bg-warm/--accentranges — don’t introduce blue/pink that fights the brand. The four pastels above could be: muted green (cache), muted red (errors), muted amber (warnings), muted neutral (volume). - Icons risk feeling generic — dare’s brand voice is sparer than this dashboard’s. Either skip the icon entirely (lets the number breathe) or use a hairline glyph that matches the existing accent style.
- Delta colour-coding inherits the existing
good_up/good_downsemantics fromdelta_html()— page-views-up is green, errors-up is red, requests-up is neutral. Don’t re-invent the semantics; reuse.
Resume condition: Sketch E becomes relevant when Sketch D is being built (verdict + range). They naturally co-design — one card carries both verdict (D) and the nugget shape (E). Park together; sketch together when the time comes.
2026-05-15 LATE EVENING — Monday tidies parked alongside the two-mode bar
Surfaced at the very end of session — two small items to bundle with Monday’s two-mode-bar publish so they ship in one commit:
Tidy 1 — Dashboard timestamps in Eastern Time
Per Dan: “lets use EST going forward, I rarely travel outside of NYC.” Currently the dashboard title renders 2026-05-16 02:43 because GHA runs in UTC and dare_cf_analytics.py uses datetime.now() without TZ conversion — confusing when reading at 22:43 ET. Fix:
# In dare_cf_analytics.py wherever `now` is generated for the title:
from zoneinfo import ZoneInfo
now = datetime.now(ZoneInfo("America/New_York")).strftime("%Y-%m-%d %H:%M ET")
Use America/New_York (not a fixed UTC offset) so EST→EDT switches handle automatically through the year. Display abbreviation: ET is cleanest (doesn’t flip between EST/EDT seasonally and reads correctly year-round). Dan said “EST” colloquially — implement with the IANA zone, display “ET” unless he asks for the literal “EST” string.
Mirror to ~/bin/dare_cf_analytics.py. Same Monday commit as the two-mode bar; trivial diff.
Tidy 2 — Dashboard cache breakthrough lesson
Tonight’s incident: after pushing 9a03c2f (aspect-ratio fix), the deployed HTML at dare-dashboard.pages.dev was correct but dashboard.dare.co.uk served stale content for several hours despite hard-refresh + service worker checks. Firing a fresh workflow_dispatch (new deployment hash) broke through the cache — the new hash forces CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages to re-alias the custom domain.
The pattern worth keeping:
If a CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages deploy looks correct at
<project>.pages.devbut the custom domain shows stale, fire a freshgh workflow runso a new deployment hash forces the custom-domain alias to flip. Stale custom-domain caching outlasts hard-refresh on Pages projects.
Worth investigating Monday whether there’s a Cloudflare Cache Rule on dashboard.dare.co.uk that’s caching HTML at the edge despite CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages’s typical behaviour, or whether the custom-domain alias needs explicit purging. If it’s a Cache Rule, Cache-Control: no-cache, must-revalidate on dashboard.html would help.
2026-05-15 LATE EVENING — patch implemented + tested, awaiting Monday push
Per Dan: “park the Sketch B — two-mode bar (spike vs baseline) so that its ready to publish on Monday, meaning do a few mini tests to see if it work.”
State at end of session (2026-05-15 22:35-ish ET):
| Artefact | State |
|---|---|
~/Code/dare-pipeline/scripts/dare_cf_analytics.py |
Patched, uncommitted — bar_chart() extended with spike_threshold + spike_color params; month-chart caller passes spike_threshold=6000 |
~/bin/dare_cf_analytics.py (local mirror) |
Patched, mirrored |
~/Downloads/dare_two_mode_bar_minitest.py |
All 4 tests passing against live dashboard data |
origin/main of dare-pipeline |
Untouched — last published commit unchanged; Monday push needed |
Patch shape:
- New
bar_chart()parameters:spike_threshold: int | None = None,spike_color: str = "#c8364c". Both optional, default to legacy single-mode behaviour. - Two-mode partition logic: when
spike_thresholdANDbreakdownare supplied, each bar’s breakdown days are summed into spike (≥ threshold) and baseline (< threshold). Bars with ≥1 spike day render stacked (baseline at bottom, spike on top); bars with 0 spike days render single (current behavior). - Month chart caller:
month_chart_svg = bar_chart(..., spike_threshold=6000). Weekday chart left untouched per the editorial decision that weekly-grouped partitioning is noise. - Stack order is load-bearing: baseline at bottom, spike on top. The patch docstring captures this so future-me doesn’t flip it.
Mini-tests covered:
- Two-mode renders correctly: Feb ‘26 + May ‘26 (no spike days) → single segment; Mar ‘26 + Apr ‘26 (have spike days) → baseline + spike segments. ✓
- Legacy preserved: with
spike_threshold=None, output has zerodata-segmentattrs (regression-safe). ✓ - Segment totals match: baseline + spike sums equal the bar’s total per
<title>attrs (Mar 111,159 / Apr 98,220 = exact). ✓ - Threshold knob monotonic: lower threshold (3K) catches more spike bars than higher (6K); threshold=99999 produces zero spikes. ✓
Monday publish recipe (when Dan’s ready):
cd ~/Code/dare-pipeline
git diff scripts/dare_cf_analytics.py # sanity-check the patch
git add scripts/dare_cf_analytics.py
git commit -m "dashboard month chart: two-mode bars (baseline + spike) per editorial pick"
git push origin main
# Then trigger immediate deploy (since GHA is scheduled cron only):
gh workflow run refresh.yml
After deploy, sanity-check at https://dashboard.dare.co.uk/ — the Month view should show baseline-green at the bottom and accent-red spike segments on top for March + April; Feb (no traffic) and May (no spikes yet) stay single-mode. The Weekday view should be visually unchanged.
Rollback (if anything looks off): cd ~/Code/dare-pipeline && git revert HEAD && git push && gh workflow run refresh.yml. Mini-test script will still run against the reverted state.
Original flag — D3 reimagine still parked (the deeper question)
Flagged 2026-05-15 evening with screenshot (~/Desktop/CleanShot 2026-05-15 at 20.51.47@2x.png). Toggle on the “Page hits” chart shows:
- Weekday view — 7 bars sized in proportion to ~15-20K daily averages, comfortable height
- Month view — 2 bars at 111.2K and 98.2K, dominating the entire chart area
The chart auto-scales its y-axis per-view, so the absolute heights of Month bars feel disproportionate compared to Weekday. The bars also reach close to the top edge of their container in Month view — the labels (111.2K / 98.2K) sit ABOVE the bars rather than feeling integrated, because there’s almost no headroom.
Reimagine — not just patch (Dan 2026-05-15 evening)
“It might need to be reimagined with d3 data viz ideas as a sketch.”
The CSS tweak path (cap bar height, share padding) would fix the immediate visual disproportion but doesn’t address the deeper question: two bars in a Month view is a poor chart shape regardless of scaling. Two bars is a comparison, not a distribution; a chart is overkill for it. Seven bars in Weekday view is a distribution and earns the chart.
So the right shape is a sketch-first reimagine — pull D3 data-viz vocabulary, propose 2-3 chart treatments per the Month/Weekday data, A/B-preview them at dashboard scale, ship the winner. Mockup-before-full-build per feedback_mockup_before_full_build.md.
Sketch directions to explore
- Sparkline + delta annotation for Month view — instead of two giant bars, a horizontal sparkline-style trend line across recent months with the latest value + ↗/↘ delta from previous. Compact, comparison-friendly, scales as more months accumulate.
- Heat-tile grid for Weekday — 7 tiles in a row (or a small 5-week heat-calendar), colour-coded by intensity rather than bar-height. Closer to GitHub’s contribution graph; reads as pattern not magnitude.
- Slope chart for Month — once we have 3+ months, a slope chart shows change-over-time more honestly than bars.
- Distribution + summary stat overlays — for Weekday, the bars stay but get a horizontal “average” line + ±1σ band so visitors read variance, not just heights.
- Two-row consistent frame — Weekday on top row, Month on bottom row, no toggle; both always visible, scaled to same fractional-of-row height. Removes the toggle cognitive load entirely.
Option 5 is the “remove the choice” framing — often the right call when the choice is between two views that complement rather than substitute.
Likely CSS-tweak fallback (if reimagine is parked further)
If the sketch pass doesn’t land near-term and the current chart needs to look less jarring: - Container max-height + consistent top-padding — cap bar render height at ~70-75% of container, so labels always have ~25-30% headroom regardless of which view.
Park as the fallback; don’t ship until the reimagine is decisively dropped.
On resume
- Sketch first — produce 3-5 D3-vocabulary mockups (HTML+CSS or SVG hand-drafts) at
~/Downloads/dare_ab_preview_dashboard_pagehits_<DATE>.htmlper the A/B preview pattern - Compare against the current chart side-by-side
- Pick the winner editorially; build it; ship
- The winner becomes the canonical pattern for all dashboard time-series charts (likely needed for other dashboard cards later)
- Locate the dashboard chart-rendering code (likely
~/Code/dare-dashboard/or~/bin/dare_dashboard_*.py) only after the sketch is decided
Pair with
project_dashboard_dare_metric_card_pattern.md— sibling dashboard component patternproject_dashboard_arrows_threshold_review.md— sibling dashboard tuning task due 2026-06-08feedback_state_emerges_from_data.md— chart should respect the data, not let any one view dominate the visual frame
Aphorism
Two views of the same data should feel like the same chart, not two different ones. The visual frame is the constant; the data is the variable.