- dataset-picker.js writes a compact query string (?ds=&n=&f=&j= plus
intro=1/picker=0 when non-default) on every change and reads it on
init. Refresh restores the page; the URL also works as a shareable
deep-link.
- To avoid a first-paint flicker of the <details> elements, the index
route pre-resolves intro_open / picker_open from the query and renders
the <details open> attribute accordingly.
- Counter shows 'N / cap' where cap flips 10→50 based on whether any
chip is active. Updates immediately on chip click and after every
htmx swap.
- compare-select.js no longer prunes selections that aren't in the
current DOM slice — server-side filtering replaces the whole list, so
absence from DOM means 'off-current-filter', not 'run deleted'.
- New runs are tagged on dispatch with dataset:<id> / algorithm:<short> /
N:<n> / T:<t> / J:<j> (single value per axis).
- /runs accepts ?dataset=&algorithm=&N=&T=&J= and applies Prefect's
tags: {all_: [...]} server-side. Without filter, fetch cap is 10; with
filter, 50 so narrow results aren't truncated. Prefect's own 200-limit
on filter queries is clamped inside recent_runs.
- New /runs/axes.json returns the universe of chip values across the last
200 deployment runs so the chip bar shows history even when the current
slice is narrow.
- runs-filter.js rewritten to cassette-style single-select: clicking the
selected chip releases it. No 'all'/'none' meta chips. Chip state feeds
#runs-slot via hx-vals; a filter-changed custom event triggers an
immediate refetch on change, in addition to the 3s poll.
- Prefect client gets an update_tags(run_id, tags) helper.
- scripts/backfill_tags.py PATCHes tags onto every existing deployment
run (dry-run by default, --apply to commit).
Disable #cc-sync in panel-grid when there's only one stem (nothing to
sync; aspect is fixed by the dialog) and hide any .compare-controls
label whose select is disabled via :has.
- Add N and T axes alongside dataset/algorithm; chips populated from runs
in the list, axis group hidden when there's a single unique value.
- Dataset+algorithm on row 1, N+T on row 2 via two explicit
.runs-filter-row flex containers (cleaner than a sentinel break elem
that double-counted the row-gap).
- 'all' and 'none' meta-chips now wrap as a unit inside .chip-meta-wrap
so one doesn't orphan to the next line.
- Row is hidden entirely when every axis in it collapses to a single
value (:has selector on .runs-filter-row).
Click a point in any panel to pin its id — highlight persists after the
cursor leaves, across all linked panels. Click the same pinned point (or
empty space) to unpin. Hover still shows the point under the cursor,
briefly overriding the pinned display. Canvas cursor is now crosshair to
hint at the interaction.
Canvas height now derives from column width via aspect-ratio (CSS custom
prop --canvas-aspect set by JS on the grid host), with --panel-h as a
ceiling. Dropdown options: scaled/locked × 1:1/3:2. Default scaled 3:2.
Legacy 'independent'/'locked' values still parse. Canvas resizes after
aspect changes via requestAnimationFrame.
- /compare accepts ?stem=…&stem=… (repeated) for 2-8 runs; legacy ?a=&b=
still works. compare.js parses multi-stem; template drops stem_a/_b
data attrs that were unused.
- compare-select.js: MAX bumped to 8, button enables at 2-8 selected.
URL emitted as ?stem=… per selection.
- runs list gets a dataset/algorithm chip filter bar above #runs-slot
(pattern ported from metrics.js). Chips reflect the union of values in
the current list; selection state persists across htmx swaps. Non-
matching rows get .filtered-out (display:none).
- _runs.html li now carries data-embedder/data-generator so the filter
can key on them.
- panel-grid.js (new): exports mountPanels({host, controls, stems}) → {destroy}.
Moved createPanel + shared control wiring + linked-hover + pad-to-match
time mapping out of compare.js. Stem-count-agnostic; works for 1, 2, or N.
- Panel DOM is cloned from <template id=compare-panel-tpl> on each page.
- compare.js is now a ~10-line shim: parse ?a=&b=, call mountPanels.
- Per-panel color is viridis-sampled by index/N (middle viridis for N=1,
ends-of-palette for N=2, linear lerp for N≤8, cycle at N≥9). Set as
--panel-color on the panel element; CSS reads it for tag/time-seg.
- Homepage <dialog id=run-modal> + run-modal.js hijack the 'embedding' link
(plain click → modal; meta/ctrl/middle-click still opens plotly HTML).
Dialog close disposes every panel's renderer/geometry/material.
- .compare-grid → repeat(auto-fit, minmax(360px, 1fr)) handles N=1..many,
replaces the <900px one-column media rule.
- Runs list: relabel Prefect's 'Late' state as 'Queued' — more honest
description of what the runner is doing at the concurrency cap.
When two runs share identical params they write to the same figs/<stem>.html,
and the most recent overwrites the earlier. Previously both got a compare
checkbox with the same data-stem, so toggling one toggled both via the JS
Set. Now we flag older duplicates server-side (first occurrence wins — Prefect
returns runs START_TIME_DESC), drop their checkbox, fade the row, and mark
'overwritten' on the outputs line.
Server enrichment regenerates the dataset deterministically (random_state=0,
matching the flow's _DEFAULT_GENERATOR_KWARGS — the stem's seed drives jitter,
not generation) and attaches per-point labels + label_kind to frames.json.
Client picks the dataset-picker's scheme: continuous ramp for s_curve/swiss_roll,
8-color categorical palette for blobs/gaussian_quantiles/classification. Jitter-
added points (id >= num_points) render black. Rainbow material is opaque with
alpha cutoff so overlapping points don't blend to the ramp midpoint.
Swiss_roll and swiss_roll_hole collide on generator_path; the plain variant
wins for now (kwargs aren't preserved through the flow's metrics.json).
Bumped Cache-Control on the frames endpoint so browsers don't cache stale
pre-enrichment payloads.
- applyU now maps u to a shared global frame index uGlobal in [0, maxT-1];
each panel clamps to its own (T-1), so a shorter timeline pads its last
frame while the longer one finishes — both advance at the same wall-clock
tempo instead of rescaling their timelines.
- tick() keeps u as a float closure variable; reading it back from the
integer-step scrubber was quantizing du to 0 at slow tempo + high T
(1600ms/frame, T=24: du ≈ 4e-4 → round to 0 on scrub), stalling playback
after one frame.
Default is smooth (lerp each point between adjacent frames); a motion:step
toggle preserves the snap-to-frame behaviour. Points missing in either
adjacent frame are hidden during the transition to avoid pop-in.
- /compare page renders two ortho-camera panels fed by /api/runs/{stem}/frames.json
- shared controls: play/pause, scrubber, speed (0.5-4x), axes sync (independent/locked)
- linked hover: picks nearest point in one panel, highlights matching point_id in other
- add/remove-jitter nulls handled via per-frame packed positions + setDrawRange
- independent error states per panel; theme-aware colors via themechange event
- per-run checkbox when embedding HTML exists; cap at 2 selected
- sticky 'compare selected' button opens /compare?a=&b= in new tab
- selection state persists across the 3s htmx poll via a Set keyed by stem
- /compare stub validates stems, renders scaffolding (three.js UI next)
Three short paragraphs framing what the notebook studies: stability of
2-D embeddings under controlled perturbation of the input over time,
the two metrics logged per run, and why the streaming/longitudinal
angle matters for both visualization and downstream classification.
Read --picker-panel from the card's computed style at scene creation
and re-apply on themechange, so the three-js canvases flip with the
rest of the UI instead of staying stuck on the light palette.
Client-side: disable the form submit button until the picker writes a
dataset_id. Server-side: reject posts where dataset_id is present but
empty, instead of silently defaulting to make_s_curve.