Commit Graph

69 Commits

Author SHA1 Message Date
Michael Pilosov
25776c12d2 metrics: hover tooltip + click-to-select on stability plots 2026-04-23 10:20:41 -06:00
Michael Pilosov
4f6e900c05 runs filter: persist chip state in URL + server-render initial slice
- runs-filter.js mirrors its chip selections into the query string
  (dataset/algorithm/N/T/J). Empty selections are omitted entirely.
  Reads URL on init; triggers an immediate /runs refresh if any filter
  was present so the polled slice catches up instantly.
- dataset-picker.js's updateUrlState now merges into the existing query
  instead of rebuilding, so the two scripts don't stomp each other's
  keys.
- Index route applies the same chip filter to its initial server-side
  run listing, so a filter-bearing deep-link renders the right slice on
  first paint — no flash of unfiltered runs.
2026-04-22 18:21:51 -06:00
Michael Pilosov
3a951b387a homepage: persist intro/picker open state + dataset/N/T/J in URL query
- 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.
2026-04-22 18:16:42 -06:00
Michael Pilosov
ba7eef9df0 runs: subtle 'clear' button next to compare selected 2026-04-22 18:04:24 -06:00
Michael Pilosov
59a6bece2e runs: live counter + keep compare selections across filter swaps
- 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'.
2026-04-22 18:02:35 -06:00
Michael Pilosov
e94d28b8fc filenames + run names: J in sci notation (5E-3 not 0.005)
Periods in filenames are avoidable and the Prefect UI dislikes them in
run names. Uses a shared sci_notation helper in main.py mirrored in the
flow. Stem regex (main + parser) now matches J<digits.Ee+-> to accept
both old decimal-J and new sci-J filenames so the two transition
together. J tag in Prefect tag list also uses the sci form, so chip
filters stay consistent.

Backfill script extended to find pre-transition (decimal-J) files on
disk via a second base-stem variant, then rename them to the sci form.
backfill_tags re-patches existing runs so their J tag matches the new
canonical form.

All 13 existing figs + runs renamed / retagged in-place.
2026-04-22 17:54:46 -06:00
Michael Pilosov
56279dbb1b runs: server-side chip filter via Prefect tags + cassette chip UX
- 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).
2026-04-22 17:48:35 -06:00
Michael Pilosov
aa1303e373 runs filter: add J (jitter) chip group alongside N and T 2026-04-22 17:28:55 -06:00
Michael Pilosov
4ecdc7f586 modal: hide axes dropdown for single-panel runs
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.
2026-04-22 17:23:51 -06:00
Michael Pilosov
d70eff3704 runs filter: N + T chip rows; group all/none meta chips; explicit row layout
- 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).
2026-04-22 17:20:08 -06:00
Michael Pilosov
4576088c73 labels: distinguish swiss_roll vs swiss_roll_hole in the UI
- _run_view uses a new _dataset_id(path, kwargs) that matches DATASET_META
  by (path, cleaned kwargs) and returns the catalogue key — so the runs
  list / filter chips show 'swiss_roll' vs 'swiss_roll_hole' rather than
  collapsing both to 'make_swiss_roll'.
- _enrich_with_labels replaces the stem-derived meta.generator with the
  matched DATASET_META key, so the compare panel header + diff-highlight
  also distinguish the two variants.
2026-04-22 17:12:46 -06:00
Michael Pilosov
d052ec4223 labels: strip transient n_samples/random_state from generator_kwargs before regen
Sidecars written by the pre-fix flow contain merged generator_kwargs
(n_samples + random_state=0 mixed in with the user-supplied form). The
enrichment call passes n_samples/random_state explicitly, so an old
sidecar's gk caused a TypeError (duplicate kwarg) that the try/except
swallowed — leaving labels empty and coloring falling back to a plain
ramp. Strip those keys before DATASET_META matching and the regen
call; matches work naturally against the stripped dict.
2026-04-22 17:09:26 -06:00
Michael Pilosov
bdbebaa7e8 compare: click to pin a point's highlight; hover temporarily overrides
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.
2026-04-22 17:00:29 -06:00
Michael Pilosov
a4fc36352d compare: axes dropdown combines sync (scaled/locked) × aspect (1:1/3:2)
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.
2026-04-22 16:57:15 -06:00
Michael Pilosov
9b178dad38 runs: filter chips + compare selection up to 8
- /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.
2026-04-22 16:41:06 -06:00
Michael Pilosov
b744c48348 stems: fold generator_kwargs into the hash; fix swiss_roll vs hole ambiguity
- 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.
2026-04-22 16:30:42 -06:00
Michael Pilosov
44de8deeeb viz: extract N-panel-agnostic module; homepage modal reuses it for single-run view
- 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.
2026-04-22 16:17:01 -06:00
Michael Pilosov
fe49565651 stems: include embed_args hash in output filename + emit frames.json 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.
2026-04-22 15:52:39 -06:00
Michael Pilosov
36e217f51e submit: reject runs whose output would overwrite an existing fig
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.
2026-04-22 15:39:05 -06:00
Michael Pilosov
a5614ac371 runs list: mark older runs with duplicate-stem output as stale, hide their compare checkbox
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.
2026-04-22 15:35:05 -06:00
Michael Pilosov
fc1ae9dbc9 compare: default color mode to 'original' 2026-04-22 15:30:50 -06:00
Michael Pilosov
9277229024 compare: color points by their original-dataset label (mono|original toggle)
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.
2026-04-22 15:29:03 -06:00
Michael Pilosov
d3f5088233 compare: pad-to-match time mapping + fix stalled play at small du
- 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.
2026-04-22 14:44:08 -06:00
Michael Pilosov
89401e3aee compare: extend mismatch highlighting to embedder and generator slugs 2026-04-22 14:38:36 -06:00
Michael Pilosov
41ce5ee88a compare: highlight mismatched N/T/J/s params in red between the two panels 2026-04-22 14:37:04 -06:00
Michael Pilosov
c9868ff83e compare: wrap N/T/J/seed params onto its own line in each panel header 2026-04-22 14:35:24 -06:00
Michael Pilosov
dd01638110 compare: replace stem-link filename text with a new-tab arrow glyph 2026-04-22 14:33:18 -06:00
Michael Pilosov
8bc8b801dc compare: move control bar above the panels 2026-04-22 14:32:32 -06:00
Michael Pilosov
a976ba893a compare: slow base playback 4x (1x = 1600ms/frame, was 400ms) 2026-04-22 14:31:20 -06:00
Michael Pilosov
d0b026734a compare: interpolate between frames for smooth point-trajectory motion
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.
2026-04-22 14:29:24 -06:00
Michael Pilosov
fc6aad5516 compare: side-by-side three.js animation with linked scrub, hover, and theme
- /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
2026-04-22 14:26:45 -06:00
Michael Pilosov
e680867f8b compare: selection UX on runs list + /compare placeholder page
- 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)
2026-04-22 14:19:26 -06:00
Michael Pilosov
b016dbdaee compare: parse plotly HTML into frames JSON, expose at /api/runs/{stem}/frames.json 2026-04-22 14:16:30 -06:00
Michael Pilosov
acb596743a reducer list: right-align pkg column so wrapped class-path lines stay on the right 2026-04-22 13:58:01 -06:00
Michael Pilosov
4ee78dd466 reducer list: break long class paths on '.' so they wrap cleanly on narrow (non-mobile) viewports 2026-04-22 11:48:52 -06:00
Michael Pilosov
958fa019ea reducers: add KernelPCA, Isomap, MDS, SpectralEmbedding, GaussianRandomProjection 2026-04-22 11:45:02 -06:00
Michael Pilosov
97ee3d4db6 reducer list: blurb spans full width on narrow viewports 2026-04-22 11:34:24 -06:00
Michael Pilosov
f524dcce51 clean up / clarify labels 2026-04-22 11:30:52 -06:00
Michael Pilosov
b108204289 fix min height 2026-04-22 11:25:30 -06:00
Michael Pilosov
8db92089a1 theme toggle: absolute (not fixed) so it doesn't follow scroll 2026-04-22 11:16:20 -06:00
Michael Pilosov
f07fa567d0 anchor theme toggle to top-right of viewport 2026-04-22 11:14:16 -06:00
Michael Pilosov
7039b39d9d nowrap nav link and accordion toggles on small viewports 2026-04-22 11:13:03 -06:00
Michael Pilosov
2e76cda332 page titles: drop web1 and em-dashes 2026-04-22 11:10:41 -06:00
Michael Pilosov
03309e28a6 move prefect badge from masthead to footer 2026-04-22 11:09:44 -06:00
Michael Pilosov
8bb186269b header: 'deployment' -> 'ver' 2026-04-22 11:08:14 -06:00
Michael Pilosov
5ce69f2ef0 inline metrics into home page; footer copyright; drop prefect-api URL
Migrates the metrics view from a separate /metrics page into an inline
§ 5 collapsible section on the home page (swept in from pre-existing
unstaged edits). Also updates the footer to © 2026 Mind the Math LLC
and drops the prefect-api URL from both mastheads.
2026-04-22 11:08:14 -06:00
Michael Pilosov
86e11a6a11 restyle sec 0 + incl fig 2026-04-22 10:57:24 -06:00
Michael Pilosov
bb46e5a18d add §0 introduction (default collapsed)
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.
2026-04-22 10:41:24 -06:00
Michael Pilosov
1807b3ac4e add pacmap.LocalMAP to the reducer catalogue 2026-04-21 22:13:37 -06:00
Michael Pilosov
158f3fdefa theme picker canvas backgrounds
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.
2026-04-21 21:56:14 -06:00