The flow previously merged _DEFAULT_GENERATOR_KWARGS={random_state:0} and
n_samples=num_points into generator_kwargs BEFORE hashing. Prefect only
records the user-supplied form, so the web app's synth_output_paths
disagreed with the flow's output name — a plain swiss_roll run showed
'embedding: n/a' in the runs list despite completing, because the web
looked for the hash that excluded those defaults.
Now we keep the user-supplied generator_kwargs around for hashing +
metadata, and use the merged dict only for the actual generator call.
n_samples is already captured in the stem as 'N<n>', and random_state=0
is a flow constant — neither belongs in the semantic identity.
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.
- run_args_hash now covers (embed_args, generator_kwargs). When gen_kwargs
is empty we still hash embed_args alone — so plain generators (s_curve,
plain swiss_roll) keep their stems and no existing plain-gen figs need
renaming. Kwargs-bearing variants (swiss_roll_hole, blobs,
gaussian_quantiles, classification) now disambiguate properly.
- Flow persists generator_kwargs into metrics.json meta AND into the
frames.json sidecar meta, so the label-enrichment path can find it
without another lookup.
- _enrich_with_labels discovers gen_kwargs in priority: payload meta -->
sibling metrics.json --> DATASET_META first-match. It matches the
DATASET_META entry by (path, kwargs) so swiss_roll_hole is no longer
confused for plain swiss_roll.
- _cached_frames overrides meta.stem with the URL-requested stem before
enrichment — after a backfill rename the sidecar's baked-in stem is
stale, and we were then failing to find the sibling metrics.json.
- Submit duplicate-check uses the new hash and keeps the hashless-legacy
check as a safety net.
- backfill_hashes.py rewritten: queries Prefect for each recent run's
full params, finds the matching fig under any of (current, legacy,
hashless) names, renames to the current scheme and patches
generator_kwargs into metrics.json.
- 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.
Replaces Prefect's auto adjective-animal names with the same stem that
addresses the run's figs on disk, so runs are hoverable/searchable in the
Prefect UI by their identifying params. flow_run_name is a callable that
reads runtime.flow_run.parameters at scheduling time.
Reads each legacy <stem>.metrics.json for its embed_args, computes the
same sha1-8 digest main.py uses, renames the .html and its sidecars in
place. Skips Reference figs (no embed_args) and any fig lacking a
metrics.json (can't recover the hash from a missing sidecar).
Stem grows an 8-hex sha1 digest of the (keys-sorted) embed_args dict, so
runs differing only in embed_args (e.g. UMAP n_neighbors=5 vs 15) now
produce distinct figs. The stem regex and parser both accept an optional
_<hash> tail so pre-hash figs still render in the runs list and compare
page; legacy filename is resolved on disk fallback.
Duplicate-submission check now rejects against BOTH the hashed and the
legacy hashless variant so users can't accidentally duplicate an old run
either.
Flow additionally writes a <stem>.frames.json sidecar next to the plotly
HTML (same shape as app/web/plotly_parse returns). Server prefers the
sidecar when present; falls back to parsing HTML for older runs. Sidecar
emission is non-critical — any failure just logs and keeps going.
Before dispatching to Prefect, check if figs/<stem>.html already exists.
If so, return 409 with a message asking to bump seed/jitter/N/T or delete
the fig. Stem is derived from (generator, embedder, N, T, J, seed), so
embed_args-only changes still collide and are blocked — users must vary
a stem-shaping param to get a distinct output file.
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.