Skip to content

feat(analytics): cross-filter hover between chart, countries, and files - #465

Draft
alukach wants to merge 8 commits into
mainfrom
feat/analytics-hover-day
Draft

feat(analytics): cross-filter hover between chart, countries, and files#465
alukach wants to merge 8 commits into
mainfrom
feat/analytics-hover-day

Conversation

@alukach

@alukach alukach commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Hovering any of the three product-analytics panels narrows the other two to that slice, and clicking pins a slice for deeper interrogation:

  • Hover a chart day → stats row, country list, and files table show that UTC day's values.
  • Hover a country (incl. the "others" row) → stats row shows that country's window totals, the chart shows its daily series, and the files table shows its per-file values.
  • Hover a file → stats row shows that file's totals (incl. its distinct-country count), the chart shows its daily series, and the country list shows its per-country values.
  • Click to pin — one pin per dimension. Clicking a country row, a file row, or a chart bar pins that value; pins compose into an intersection filter (country ∩ file ∩ day, all served by the cross/cube data). Hovering substitutes for its own dimension's pin while the cursor is there, so you can sweep siblings without dropping the other pins (e.g. pin a country and a file, then sweep the chart to read that pair's downloads by date). Clicking a pinned value again unpins it.
  • While a slice is active, the country/file lists re-rank descending by the shown values — but a list never re-sorts on its own hover, so rows can't shuffle under the cursor. Rows stay pinned to the window's top 5 countries / top 10 files; the chart caption names the active slice.

How

  • getProductBreakdowns runs a second query wave after the window ranking, scoped to the top entries with IN (…) — bounded row counts, no 10k-cap risk: per-day series (requests + bytes) for countries/others/files, a single country×file cross query (≤ 5×10 rows) serving both hover directions, and an others-per-file aggregate. Window queries also gain per-country bytes and per-file COUNT(DISTINCT blob6) so the stats row can follow non-day slices.
  • Per-day values are keyed by the day's ISO timestamp (UsagePoint.date) rather than array index, so the independently cached usage grid and breakdowns can never misalign.
  • ProductAnalyticsView holds one hover state (day | country | file) plus a pinned slice; each panel derives its shown values from them. HoverCaption gains an optional filterLabel.

Testing

  • Data-layer test extended: mocks all five second-wave queries, asserts byDay/byCountry/otherCountries construction and the quoted IN/NOT IN clauses.
  • tsc --noEmit, lint, and all analytics suites green (29 tests).
  • Worth a spin on the Vercel preview: the new IN-list SQL follows AE's documented syntax but couldn't be run against live AE locally.

🤖 Generated with Claude Code

Hovering a bar on the product downloads chart now narrows the by-country
list and top-files table to that UTC day, matching the stats row. The
breakdown fetch gains a second query wave scoped to the window's top
countries/files via IN (bounded rows), keyed by ISO day so independently
cached grids can't misalign.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
source-cooperative Ready Ready Preview, Comment Jul 29, 2026 5:12pm

Request Review

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 4m 46s —— View job


Review of PR #465

  • Gather context (diff, changed files, CLAUDE.md)
  • Review src/lib/clients/analytics/index.ts
  • Review src/components/features/analytics/ProductAnalyticsView.tsx
  • Review src/components/features/analytics/panels.tsx
  • Review test file changes
  • Post findings

✅ No blocking issues — safe to merge.

Traced the cross-filter/pin logic end-to-end (hover/pin state → entity/countryEntity/fileEntity derivation → byDay/byCountry lookups) against the new second-wave SQL, and it holds up:

  • SQL injection: all dynamic IN (...) values (countryIn, fileIn) go through sqlQuote() (src/lib/clients/analytics/index.ts:471-472), which escapes backslashes/quotes/control chars — consistent with the rest of the file. Good, since blob3 (file path) is attacker-influenced (arbitrary object keys).
  • Map-key construction for the day×country×file cube (`${file} ${country || "??"}`, index.ts:558-575) looks collision-prone at first glance since file paths can contain spaces, but country codes are always exactly 2 chars (ISO code or the "??" fallback), so the concatenation is actually unambiguous — no real collision risk.
  • parseDateTime keys used to join UsagePoint.date (grid) with the breakdowns' byDay maps produce identical ISO strings on both sides (index.ts:211-213 vs index.ts:329-334), so lookups won't silently miss.
  • All second-wave queries are correctly guarded by topCountries.length/rest.length/topFiles.length before firing, avoiding malformed empty IN () clauses.
  • Front-end shown/shownCountries/entity branching in ProductAnalyticsView.tsx:76-120 correctly falls back to zeroEntry for missing cross entries and handles the "others" aggregate's window-only distinct-country count (NaN"—") as documented.

Minor, non-blocking note: the new pin interactions (chart bar click, country row click, file row click) are mouse-only — no onKeyDown/role="button" — but this matches the existing pattern elsewhere in the codebase (e.g. DirectoryRow.tsx), so not a regression introduced by this PR.

Hovering a country or file now narrows the other panels too: the chart
shows that slice's daily series, and the opposite list re-ranks to its
values via a bounded country-x-file cross query. Hovered slices sort the
country/file lists descending (a list never re-sorts on its own hover,
so rows can't shuffle under the cursor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alukach alukach changed the title feat(analytics): hover a day to filter country and file breakdowns feat(analytics): cross-filter hover between chart, countries, and files Jul 29, 2026
The Downloads / Daily avg / Data served / Countries stats now update for
a hovered country or file (window queries gain per-country bytes and
per-file distinct-country counts). Clicking a country/file row pins that
slice: the chart and stats stay scoped to it, and hovering a chart day
then reads that day's values within the pin. Clicking again unpins;
hovering another row previews it over the pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconciles the table with the file-traffic total: a footer row shows the
distinct-file count, requests, and bytes outside the top list, making
long-tail traffic (e.g. bulk mirrors) visible instead of alarming. The
remainder is window-wide only, so it blanks to an em dash while a
day/country slice re-values the table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hovering the opposite type of an active pin no longer replaces the pin
(which snapped both lists back to window values and re-sorted them under
the cursor). The pin keeps scoping the lists and chart; the stats row
narrows to pin ∩ hover from the country×file cross data, and the caption
shows both. Same-type hover still previews that row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pinning a country and hovering a file (or vice versa) now redraws the
chart with that intersection's daily series. Two more bounded queries
fetch the day-x-country-x-file cube for the top 5 countries x top 10
files plus an others-x-file-x-day aggregate, nested under each file's
byCountry/otherCountries entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the single pin with one pin per dimension, toggled by clicking a
row or a chart bar; pins compose into an intersection filter (all combos
are served by the existing cross/cube data). A hover substitutes for its
own dimension's pin while the cursor is there, so sweeping rows previews
siblings without dropping the other pins. This also collapses the
special-cased slice/cross logic into one effective-filter model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins get their own persistent line above the chart (green square,
'PINNED: …' with the pinned country/file/date), and the line below is
purely transient hover state (hovered date or row preview), so the two
no longer overwrite each other in the same slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant