dr-sandbox/app/web/templates/index.html
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

500 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>embedding notebook</title>
<link rel="stylesheet" href="/static/style.css?v=31" />
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%231f4e5f'/%3E%3C/svg%3E" />
<script>
(function(){try{
var t=localStorage.getItem('theme');
if(!t)t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
document.documentElement.setAttribute('data-theme',t);
}catch(e){}})();
</script>
</head>
<body>
<header class="masthead">
<div>
<h1 class="title">embedding notebook <em>&mdash; drift &amp; projection</em></h1>
</div>
<div class="meta">
<a href="#metrics" class="nav-link">metrics &darr;</a>
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="toggle theme"></button>
</div>
</header>
<details class="dataset-picker intro" id="intro">
<summary>
<span class="picker-meta">
<span class="section-number">§ 0</span>
<span class="picker-title">introduction</span>
<span class="picker-selection">
<span class="lbl">scope</span>
<code>impact of data drift on dimension reduction</code>
</span>
</span>
<span class="picker-toggle" aria-hidden="true"></span>
</summary>
<div class="picker-body">
<div class="intro-body">
<div class="intro-prose">
<p>
<strong>What this is.</strong> Dimensionality reduction is a workhorse
for both exploratory visualization and downstream prediction, yet the
stability of its output under small perturbations of the input is
rarely examined directly. This notebook takes a narrow, empirical
approach: a three-dimensional point cloud (§&nbsp;1) is perturbed by a
controlled amount at each of a short sequence of timesteps, the
selected reducer (§&nbsp;2) is applied independently to every snapshot,
and the resulting trajectory of two-dimensional embeddings is recorded.
</p>
<p>
<strong>What it measures.</strong> Two stability views are logged
alongside each run and plotted on the
<a href="/metrics">metrics page</a>. Per-timestep travel —
&thinsp;y(t)&thinsp;&minus;&thinsp;y(t&minus;1)&thinsp;‖ —
captures how much the 2-D layout moves between consecutive frames.
<em>k</em>NN retention captures how much of the input-space neighborhood
graph survives projection. Together they separate reducers that are
globally stable but locally noisy from those with the opposite failure
mode.
</p>
<p>
<strong>Why this matters.</strong> A reducer that looks well-behaved on
a single snapshot is not automatically the right tool for a streaming
or longitudinal setting. Used as the substrate for a visualization,
frame-to-frame motion will read as change the user did not request;
used as a feature-extraction step inside a classification pipeline,
drift between training and inference will quietly erode accuracy. The
aim here is to build intuition for those regimes before committing the
reducer to either role.
</p>
</div>
<figure class="intro-figure">
<svg class="intro-schema" viewBox="0 0 480 230" xmlns="http://www.w3.org/2000/svg"
role="img" aria-label="schematic: 3-D perturbed snapshots reduced independently to a 2-D trajectory">
<!-- LEFT: stack of snapshots -->
<g>
<rect class="frame depth" x="40" y="55" width="130" height="130"/>
<rect class="frame depth" x="30" y="45" width="130" height="130"/>
<rect class="frame" x="20" y="35" width="130" height="130"/>
<!-- tiny axis triad in the front frame -->
<g class="axes" transform="translate(32 156)">
<line x1="0" y1="0" x2="14" y2="0"/>
<line x1="0" y1="0" x2="0" y2="-14"/>
<line x1="0" y1="0" x2="-9" y2="7"/>
</g>
<g class="dots">
<circle cx="72" cy="112" r="2.1"/>
<circle cx="86" cy="60" r="2.1"/>
<circle cx="100" cy="92" r="2.1"/>
<circle cx="62" cy="142" r="2.1"/>
<circle cx="112" cy="124" r="2.1"/>
<circle cx="118" cy="54" r="2.1"/>
<circle cx="134" cy="148" r="2.1"/>
</g>
<g class="dots-hi">
<circle class="track-a" cx="54" cy="72" r="3.2"/>
<circle class="track-b" cx="132" cy="80" r="3.2"/>
<circle class="track-c" cx="90" cy="134" r="3.2"/>
</g>
<text class="t-hint" x="150" y="30" text-anchor="end">t = 0, 1, … T</text>
</g>
<!-- ARROW + reducer label -->
<g transform="translate(182 100)">
<line class="arrow-line" x1="0" y1="0" x2="92" y2="0"/>
<polyline class="arrow-head" points="84,-5 94,0 84,5" fill="none"/>
<text class="phi" x="47" y="-12" text-anchor="middle">φ</text>
<text class="arrow-sub" x="47" y="22" text-anchor="middle">per snapshot</text>
</g>
<!-- RIGHT: single 2-D frame with trajectories -->
<g>
<rect class="frame" x="290" y="35" width="170" height="130"/>
<g class="trajs">
<polyline class="track-a" points="308,72 316,88 326,106 336,124"/>
<polyline class="track-b" points="370,52 380,68 390,82 398,100"/>
<polyline class="track-c" points="430,152 428,132 430,108 426,84"/>
</g>
<g class="dot-start">
<circle class="track-a" cx="308" cy="72" r="3"/>
<circle class="track-b" cx="370" cy="52" r="3"/>
<circle class="track-c" cx="430" cy="152" r="3"/>
</g>
<g class="dot-end">
<circle class="track-a" cx="336" cy="124" r="2.9"/>
<circle class="track-b" cx="398" cy="100" r="2.9"/>
<circle class="track-c" cx="426" cy="84" r="2.9"/>
</g>
<g class="dots">
<circle cx="316" cy="112" r="2.1"/>
<circle cx="352" cy="136" r="2.1"/>
<circle cx="358" cy="78" r="2.1"/>
<circle cx="392" cy="58" r="2.1"/>
<circle cx="412" cy="118" r="2.1"/>
<circle cx="440" cy="140" r="2.1"/>
<circle cx="348" cy="158" r="2.1"/>
<circle cx="380" cy="142" r="2.1"/>
</g>
</g>
<!-- captions -->
<g class="cap">
<text x="85" y="200" text-anchor="middle">snapshots · Xₜ ⊂ ℝ³</text>
<text x="375" y="200" text-anchor="middle">embedded trajectory · Yₜ ⊂ ℝ²</text>
</g>
</svg>
</figure>
</div>
</div>
</details>
<details class="dataset-picker" id="picker" open>
<summary>
<span class="picker-meta">
<span class="section-number">§ 1</span>
<span class="picker-title">input dataset</span>
<span class="picker-selection">
<span class="lbl">generator</span>
<code id="picker-summary-path"></code>
</span>
</span>
<span class="picker-toggle" aria-hidden="true"></span>
</summary>
<div class="picker-body">
<p class="lede">
Six candidate generators for the embedding pipeline. Drag to rotate, scroll to zoom,
<kbd></kbd>&nbsp;<kbd></kbd> or <kbd>1</kbd>&thinsp;&hellip;&thinsp;<kbd>6</kbd> to select.
</p>
<div class="picker-controls">
<span class="ctl-label">n samples</span>
<div class="segmented count-5" role="radiogroup" aria-label="number of samples">
<label><input type="radio" name="n" value="100"><span>100</span></label>
<label><input type="radio" name="n" value="500" checked><span>500</span></label>
<label><input type="radio" name="n" value="1000"><span>1,000</span></label>
<label><input type="radio" name="n" value="2500"><span>2,500</span></label>
<label><input type="radio" name="n" value="5000"><span>5,000</span></label>
</div>
<span class="ctl-label">noise σ</span>
<div class="segmented count-3" role="radiogroup" aria-label="noise σ">
<label><input type="radio" name="j" value="0.001"><span>0.001</span></label>
<label><input type="radio" name="j" value="0.005" checked><span>0.005</span></label>
<label><input type="radio" name="j" value="0.01"><span>0.010</span></label>
</div>
<span class="ctl-label">timesteps</span>
<div class="segmented count-3" role="radiogroup" aria-label="number of timesteps">
<label><input type="radio" name="f" value="12"><span>12</span></label>
<label><input type="radio" name="f" value="24" checked><span>24</span></label>
<label><input type="radio" name="f" value="48"><span>48</span></label>
</div>
</div>
<div class="gallery" id="gallery">
<div class="picker-loading">loading samples&hellip;</div>
</div>
<div class="picker-footer">
<div class="selection">
<span class="lbl">generator</span>
<code id="selected-path"></code>
</div>
<button type="button" class="continue" id="continue-btn" disabled>Continue &rarr;</button>
</div>
</div>
</details>
<main>
<!-- ==================== LEFT: parameter notebook ==================== -->
<section class="col-left">
<form
id="run-form"
hx-post="/submit"
hx-target="#runs-slot"
hx-swap="innerHTML"
hx-indicator="#busy"
>
<!-- Picker-driven hidden fields. Values are written by dataset-picker.js. -->
<input type="hidden" name="dataset_id" id="dataset_id" value="" />
<input type="hidden" name="num_points" id="num_points" value="500" />
<input type="hidden" name="num_timesteps" id="num_timesteps" value="24" />
<input type="hidden" name="jitter_scale" id="jitter_scale" value="0.005" />
<input type="hidden" name="seed" id="seed" value="42" />
<!-- §2 reducer -->
<div class="section">
<div class="section-label">
<span>§ 2 &nbsp; reducer</span><span class="ordinal">method</span>
</div>
<p class="lead">Dimensionality reduction applied to each snapshot. Only reducers whose Python package is importable are shown.</p>
<ul class="reducer-list">
{% for r in reducers %}
<li>
<input type="radio" name="reducer" id="red-{{ loop.index }}" value="{{ r.key }}"
{% if r.key == default_reducer %}checked{% endif %}
hx-get="/reducer-form?name={{ r.key }}"
hx-target="#reducer-params"
hx-swap="innerHTML"
hx-trigger="change" />
<label for="red-{{ loop.index }}">
<span class="mark">{{ "%02d"|format(loop.index) }}</span>
<span class="text">
<span class="name">{{ r.label }}</span>
<span class="blurb">{{ r.blurb }}</span>
</span>
<span class="pkg">{{ r.key.replace('.', '.<wbr>')|safe }}</span>
</label>
</li>
{% endfor %}
</ul>
</div>
<!-- §3 reducer params -->
<div class="section">
<div class="section-label">
<span>§ 3 &nbsp; parameters</span><span class="ordinal">kwargs</span>
</div>
<div id="reducer-params">
{% include "_reducer_form.html" with context %}
</div>
</div>
<div class="actions">
<button type="submit" class="submit">submit run</button>
<span id="busy" class="htmx-indicator">dispatching&hellip;</span>
</div>
</form>
</section>
<!-- ==================== RIGHT: runs log ============================== -->
<section class="col-right">
<div class="section-label">
<span>§ 4 &nbsp; recent runs</span>
<span class="run-count">
<span id="runs-count">{{ runs|length }}</span> / 10 · refresh 3s
<span id="poll-ind" class="htmx-indicator" style="margin-left:6px">&#9679;</span>
</span>
</div>
<div class="compare-bar">
<button type="button" id="compare-btn" disabled>
compare selected <span id="compare-count">(0/8)</span>
</button>
<span class="compare-hint muted">pick 2&ndash;8 embeddings &rarr; side-by-side animation in a new tab</span>
</div>
<div class="runs-filter" id="runs-filter">
<div class="runs-filter-group">
<span class="ctl-label">dataset</span>
<div class="chips" id="runs-flt-dataset" aria-label="filter by dataset"></div>
</div>
<div class="runs-filter-group">
<span class="ctl-label">algorithm</span>
<div class="chips" id="runs-flt-algo" aria-label="filter by algorithm"></div>
</div>
</div>
<div
id="runs-slot"
hx-get="/runs"
hx-trigger="load delay:3s, every 3s"
hx-swap="innerHTML"
hx-indicator="#poll-ind"
>
{% include "_runs.html" with context %}
</div>
</section>
</main>
<details class="dataset-picker metrics-inline" id="metrics">
<summary>
<span class="picker-meta">
<span class="section-number">§ 5</span>
<span class="picker-title">stability metrics</span>
<span class="picker-selection">
<span class="lbl">view</span>
<code>travel · drift · kNN@10</code>
</span>
</span>
<span class="picker-toggle" aria-hidden="true"></span>
</summary>
<div class="picker-body">
<div class="filter-bar">
<div class="filter-group">
<span class="ctl-label">dataset</span>
<div class="chips" id="flt-dataset" aria-label="filter by dataset"></div>
</div>
<div class="filter-group">
<span class="ctl-label">algorithm</span>
<div class="chips" id="flt-algo" aria-label="filter by algorithm"></div>
</div>
<div class="filter-group stat-group">
<span class="ctl-label">travel stat</span>
<div class="segmented count-4" role="radiogroup" aria-label="travel stat">
<label><input type="radio" name="stat" value="mean" checked><span>mean</span></label>
<label><input type="radio" name="stat" value="median"><span>median</span></label>
<label><input type="radio" name="stat" value="p95"><span>p95</span></label>
<label><input type="radio" name="stat" value="max"><span>max</span></label>
</div>
</div>
<div class="filter-count">
<span id="match-count">0</span>&thinsp;/&thinsp;<span id="total-count">0</span> <span class="muted">runs</span>
</div>
</div>
<div class="plots">
<figure class="plot">
<figcaption>
<span class="plot-title">frame-to-frame travel</span>
<span class="plot-sub">&thinsp;y(t) y(t1)&thinsp;&nbsp;·&nbsp; output 2-D space</span>
</figcaption>
<div id="plot-ff" class="plot-area"></div>
</figure>
<figure class="plot">
<figcaption>
<span class="plot-title">vs-initial travel</span>
<span class="plot-sub">&thinsp;y(t) y(0)&thinsp;&nbsp;·&nbsp; drift from first timestep</span>
</figcaption>
<div id="plot-vi" class="plot-area"></div>
</figure>
<figure class="plot">
<figcaption>
<span class="plot-title">kNN@10 retention</span>
<span class="plot-sub">fraction of each point's 10 nearest input-space neighbors preserved in 2-D (higher = more faithful)</span>
</figcaption>
<div id="plot-knn" class="plot-area"></div>
</figure>
</div>
<div class="legend" id="legend"></div>
<div id="empty" class="empty" hidden>
No metrics to show. Dispatch a run above — sidecar JSONs appear in <code>figs/</code> after the flow completes.
</div>
</div>
</details>
<dialog id="run-modal" aria-label="run embedding">
<div class="run-modal-body">
<div class="run-modal-head">
<span>embedding</span>
<button type="button" class="run-modal-close" id="run-modal-close" aria-label="close">&times;</button>
</div>
<div class="compare-controls" id="modal-compare-controls">
<button type="button" class="cc-play" id="cc-play" aria-label="play / pause">&#9654;</button>
<input class="cc-scrub" id="cc-scrub" type="range"
min="0" max="1000" step="1" value="0" aria-label="time scrubber" />
<div class="cc-time" id="cc-times"></div>
<label class="cc-speed-wrap">
<span class="cc-lbl">speed</span>
<select class="cc-speed" id="cc-speed">
<option value="0.5">0.5&times;</option>
<option value="1" selected>1&times;</option>
<option value="2">2&times;</option>
<option value="4">4&times;</option>
</select>
</label>
<label class="cc-motion-wrap">
<span class="cc-lbl">motion</span>
<select class="cc-motion" id="cc-motion">
<option value="smooth" selected>smooth</option>
<option value="step">step</option>
</select>
</label>
<label class="cc-color-wrap">
<span class="cc-lbl">color</span>
<select class="cc-color" id="cc-color">
<option value="mono">mono</option>
<option value="original" selected>original</option>
</select>
</label>
<label class="cc-sync-wrap">
<span class="cc-lbl">axes</span>
<select class="cc-sync" id="cc-sync">
<option value="scaled-1x1">scaled &middot; 1:1</option>
<option value="scaled-3x2" selected>scaled &middot; 3:2</option>
<option value="locked-1x1">locked &middot; 1:1</option>
<option value="locked-3x2">locked &middot; 3:2</option>
</select>
</label>
</div>
<div class="compare-grid" id="modal-panel-host"></div>
</div>
</dialog>
<template id="compare-panel-tpl">
<article class="compare-panel">
<header class="compare-panel-head">
<span class="panel-tag" data-role="label">&#9679;</span>
<span class="panel-embedder" data-role="embedder">&hellip;</span>
<span class="panel-sep">&middot;</span>
<span class="panel-generator" data-role="generator">&hellip;</span>
<a class="panel-stem" data-role="stem-link" href="#" target="_blank">&#x2197;</a>
<span class="panel-params" data-role="params">loading&hellip;</span>
</header>
<div class="compare-canvas" data-role="canvas">
<div class="compare-status" data-role="status">loading&hellip;</div>
</div>
</article>
</template>
<footer class="colophon">
<span>&copy; 2026 Mind the Math LLC</span>
<span class="prefect-badge">
<span class="dot {% if not deployment_id %}bad{% endif %}"></span>
{% if deployment_id %}prefect · {{ deployment_id[:8] }}{% else %}prefect · unreachable{% endif %}
</span>
</footer>
<script src="/static/theme.js?v=11"></script>
<script type="module" src="/static/dataset-picker.js?v=11"></script>
<script type="module" src="/static/metrics.js?v=11"></script>
<script src="/static/compare-select.js?v=2"></script>
<script src="/static/runs-filter.js?v=1"></script>
<script type="module" src="/static/run-modal.js?v=2"></script>
<script>
// Anchor-links alone don't expand <details>; force it.
document.querySelector('a[href="#metrics"]')?.addEventListener('click', () => {
const d = document.getElementById('metrics');
if (d) d.open = true;
});
</script>
</body>
</html>