Latest Results
feat(benchmarks-website): per-chart UX rebuild ā zoom-as-scope, collapsible groups
Replaces the page-level toolbar (which controlled every chart together)
with a per-chart toolbar that the user reported as the main UX
complaint, and switches the scope mechanism from "refetch on change" to
"zoom over a single fetched slice" so the slider is fluid at 60fps.
## Per-chart toolbar
Every `.chart-card` now carries its own compact `.toolbar.toolbar--card`
with Show / Y / Mode controls. There is no page-level toolbar on `/`,
`/chart`, or `/group`. Toolbar buttons are `<button type="button">`
(not `<a>`): they manipulate Chart.js state in place rather than
navigating.
## Zoom-as-scope
Each chart fetches up to 1000 commits once. The "Show" buttons and
slider set `chart.options.scales.x.min/max` to a window of the
fetched slice; no refetch on scope change. The slider fires on
`input` throttled to 16ms (~60fps, matches v2's `ZOOM_THROTTLE_DELAY`)
so dragging is continuous. Drag-pan and drag-rectangle-zoom are wired
through `chartjs-plugin-zoom`; mouse wheel pans horizontally via a
manual canvas listener calling `chart.pan()` because the plugin
doesn't expose pan-on-wheel.
The zoom plugin UMD is bundled locally
(`static/chartjs-plugin-zoom.umd.min.js`, MIT-licensed). hammerjs is
intentionally not bundled ā touch gestures are nice-to-have, the
plugin's mouse path works without it (guarded by `if (Hammer)`).
## Tooltip flicker fix + crosshair
The tooltip host is now permanently `pointer-events: none`. The
previous code flipped it to `auto` while visible, which produced a
flicker loop: cursor on tooltip ā mouseout on canvas ā tooltip hides
ā mousein on canvas ā tooltip shows. Cost: tooltip-internal links are
no longer clickable; the chart-card title already links to the
permalink.
The tooltip is offset 12px from the cursor and flips to the left when
within 24px of the right edge. Interaction mode is
`{ mode: "index", intersect: false, axis: "x" }` so hover anywhere
over the chart snaps to the nearest commit. A custom inline plugin
(`afterDatasetsDraw`) draws a 1px dashed `--muted` vertical crosshair
at the active hover index.
## Collapsible groups, v2 ordering
Landing page wraps each group in `<details>` with a `<summary>` that
shows the group name + chart-count badge. Only the first group is
`open` by default; closed groups render only the chart-card shells
(no inline JSON), and `chart-init.js` fetches their payloads via
`/api/chart/{slug}?n=1000` on the first `details.toggle` event.
Group naming was rewritten to match v2's hard-coded list:
- `tpch sf=1 [nvme]` ā `TPC-H (NVMe) (SF=1)`
- `tpcds sf=10 [nvme]` ā `TPC-DS (NVMe) (SF=10)`
- `clickbench [nvme]` ā `Clickbench`
A new `pub const GROUP_ORDER` + `pub fn group_sort_key` in `api.rs`
sort discovered groups into the canonical order; unknown groups sort
last by alphabetical fallback. Option (1) from the task brief ā the
rename was a clean change inside `group_name_query` only, no need for
the option-(2) sort-key fallback.
## URL state
URL writeback for per-chart toolbar state was deliberately dropped.
The user's feedback emphasised local-and-immediate UX, not "share a
perfect view via URL"; permalinks (`/chart/{slug}`, `/group/{slug}`)
are the sharing mechanism. `?n=` on the landing route is still
honoured as a power-user override on the initial fetch size.
## Tests
- Snapshots refreshed for all three pages (markup change is large).
- Added: `landing_groups_render_in_v2_order` ā fixture covers Random
Access / Compression / Compression Size / TPC-H / vector-search and
the rendered order matches the canonical list.
- Added: `details_first_group_open_others_closed`.
- Added: `chart_card_carries_per_chart_toolbar` (every card).
- Updated `static_assets_are_served` to cover the new
`/static/chartjs-plugin-zoom.umd.min.js` route.
## Out of scope (per task brief)
- Zoom-sync across charts in a group (v2's `zoom-sync.js` pattern) ā
follow-up PR.
- LTTB downsampling.
- "Compare to main" delta mode.
- The `collect_group_charts` N+1 in `api.rs`.
- Mobile legend resize handler.
- Replacing the inline crosshair plugin with `chartjs-plugin-crosshair`.
Signed-off-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01NhtGnaLstPEAh7cRJ4qDFt [claude] feat(benchmarks-website): historical comparison UX + mobile (#7681)
## Summary
Brings the v3 benchmarks website to a demo-ready state focused on the
historical-comparison use case (Vortex vs other engines on the same
commit, HEAD vs N commits ago, latest vs first as % delta). Single
process, single binary; SSR `maud` + inline JSON `<script>` +
Chart.js ā no client-side framework, no build step, no post-load API
round-trips.
> Branch note: this PR was developed on the harness-assigned branch
> `claude/demo-ready-benchmarks-v3-H5ECI` rather than the
> `claude/benchmarks-v3-ui-historical-comparison` branch the task
> request mentioned, because the session's harness pins the working
> branch (`Develop on branch ā¦`, `NEVER push to a different branch
> without explicit permission`).
## CI note
The `Rust tests (windows-x64)` job is failing on this PR but the
**same job is also failing on the merge commit at the tip of
`ct/benchmarks-v3`** (PR #7671's run, job id `73229326105`, the
commit `8697731` we branched from). The base branch shipped with
that failure tolerated, and our diff only touches
`benchmarks-website/server/` (no Windows-specific paths, no FFI, no
new dependencies on Windows-fragile crates), so this failure is
pre-existing and not caused by the PR. CodSpeed flagged two
`varbinview_zip` regressions in `vortex-array/` ā also untouched by
this PR.
## What's new
* **Scoped commit window** ā `?n=25|50|100|250|all`, default 100,
server-side clamp to `[1, 1000]`. SQL splices in a `LIMIT ?` filter
and binds the value as a parameter (consistent with the rest of
the file's `params!`-style use); the unbounded path is a separate
query so the plan stays clean.
* **Group page** ā `GET /group/{slug}` renders every chart in one
group on a single screen. Each card embeds its own
`<script id="chart-data-N">` payload + sibling `<canvas
data-chart-index="N">`. `IntersectionObserver` defers `Chart`
construction until the canvas scrolls into view (mobile-friendly
+ cheap for 22-chart TPC-H groups).
* **Toolbar** ā same component on `/chart/{slug}` and `/group/{slug}`.
Scope buttons + slider, linear/log Y-axis, absolute / `% of
baseline` mode. URL query string is canonical state; subtitle
mirrors active state. Slider step is `5` so it can land on every
preset value (`25`, `50`, `100`, `250`).
* **Rich tooltip** ā custom external HTML tooltip with `<short-sha> Ā·
YYYY-MM-DD` title; per-series rows render value with friendly unit
(nsāµsāmsās, BāKiBāMiBāGiB) and a coloured `% delta` vs the prior
visible commit; footer carries the truncated commit message + a
GitHub link. Document-level click closes.
* **Legend ā URL** ā clicking a legend item rewrites
`?hidden=engine:format|ā¦` via `history.replaceState` (no back-button
hostility). Permalinks reproduce the view. Delimiter is `|` so
series names can contain `:` and `,` without escaping.
* **Mobile** ā `@media (max-width: 768px)`: single-column chart grid,
toolbar wraps with ā„ 40 px touch targets, slider expands to fill
the row, legend pops to the *top* of the chart so it doesn't push
the chart off-screen on a phone.
* **Landing search** ā client-side filter input above the group list.
* **/api/group/{slug}** ā JSON sibling to the HTML route, returns
every chart in the group with payloads inlined.
## What was *not* picked up from `planning/components/web-ui.md`'s
deferred list
Done now (moved out of deferred):
- mobile redesign basics (single column, ā„ 40 px tap targets,
toolbar wrap)
- engine + series toggling (legend ā URL)
- deep-link state (every toolbar control is URL-canonical)
- group landing with the start of "filters" (client-side search)
Still deferred (intentional):
- per-commit drill-down page
- ad-hoc SQL page
- LTTB downsampling
- engine name lookup table + curated colour palettes
- summary cards (geomean ratios, rankings)
- full-screen modal / zoom-pan
- `?mode=delta` (compare-to-main) ā parser branch dropped pending
data shape work; toolbar surface today is only `abs / rel`
## Repro
INGEST_BEARER_TOKEN=$(openssl rand -hex 32) \
VORTEX_BENCH_DB=./bench.duckdb \
cargo run --release -p vortex-bench-server
Then open `http://localhost:3000/`, click any group name (now a link
to `/group/{slug}`), or any chart inside, and play with the toolbar.
Toggle a series in the legend and notice `?hidden=ā¦` appear in the
URL. Resize to phone width to confirm single-column layout, sticky
toolbar wrapping, and legend-on-top.
## Snapshot diffs
Three `.snap` files refreshed by this PR:
- `landing_page.snap` ā group names now link to `/group/{slug}`,
search input added, `data-group-name` for client filter.
- `chart_page_query.snap` ā toolbar + indexed
`<script id="chart-data-0">` + tooltip host element.
- `group_page_query.snap` (new) ā group page rendered against the
fixture DB, `?n=100` pinned for stability.
Run `INSTA_UPDATE=always cargo test -p vortex-bench-server` (or
`cargo insta accept`) to refresh.
## Test plan
- [x] `cargo build -p vortex-bench-server`
- [x] `cargo test -p vortex-bench-server` ā 41 tests pass (22 unit +
10 ingest + 9 web_ui)
- [x] `cargo clippy -p vortex-bench-server --all-targets -- -D
warnings` ā clean
- [x] `cargo +nightly fmt` ā no diff
- [ ] `./scripts/public-api.sh` ā skipped per CLAUDE.md (leaf binary,
not in workspace public-api lockfile set)
- [ ] Manual screenshots ā couldn't capture from the sandbox; the
reviewer or follow-up should record landing / single chart with
toolbar / group desktop / group mobile / tooltip open / log+rel.
## Follow-up review fixes (commits `7042f0d` ⦠`da668a4`)
- `7042f0d` ā `LIMIT` value travels as a bound parameter (`LIMIT ?`)
via `params_from_iter` instead of being interpolated into SQL.
- `9c80bce` ā drop the unused `?mode=delta` parser branch in both
`UiQuery::mode` and `chart-init.js::parseUrl`.
- `d156ab8` ā `?hidden=` delimiter is now `|`; new test pins the
server/client wire agreement.
- `da668a4` ā slider `step` lowered to 5 so it can land on every
preset (`25/50/100/250`).
## Things explicitly NOT changed
- `/api/ingest`, auth, schema, write paths.
- DB migration (none added).
- Existing routes (no renames).
- v2 site at `benchmarks-website/server.js` etc ā untouched.
- Single-chart page still works; reuses the same `chart-init.js`.
https://claude.ai/code/session_015Nc73ihs9TUdx7QzLUZudK
---------
Signed-off-by: Claude <claude@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com> Latest Branches
0%
0%
+11%
Ā© 2026 CodSpeed Technology