adding in more examples

This commit is contained in:
Michael Pilosov 2026-04-21 19:13:04 -06:00
parent 22ca411210
commit ac511c942f
2 changed files with 117 additions and 24 deletions

View File

@ -167,6 +167,9 @@
}
.card {
flex: 0 0 260px;
/* Override flex-item default min-width: auto so long content in
.card-path can't force the card wider than its flex-basis. */
min-width: 0;
scroll-snap-align: start;
border: 1px solid var(--hair);
background: var(--panel);
@ -261,6 +264,8 @@
font-size: 11px;
color: var(--muted);
margin-bottom: 8px;
/* Long sklearn paths would otherwise overflow and force the card wider. */
word-break: break-all;
}
.card-desc {
font-size: 13px;
@ -398,24 +403,30 @@ function normalize(points) {
let mx = 0, my = 0, mz = 0;
for (const p of points) { mx += p[0]; my += p[1]; mz += p[2]; }
mx /= n; my /= n; mz /= n;
let maxAbs = 1e-9;
for (const p of points) {
// Percentile-based scale: take the 95th percentile of per-point
// max-coord magnitude. Robust to outliers in the distribution tails
// (e.g. gaussian_quantiles / classification have long radial tails
// that, under pure max-abs normalization, shrink the visible bulk
// to a fraction of the viewport). p95 keeps datasets visually uniform.
const perPoint = new Float64Array(n);
for (let i = 0; i < n; i++) {
const p = points[i];
const a = Math.abs(p[0] - mx);
const b = Math.abs(p[1] - my);
const c = Math.abs(p[2] - mz);
if (a > maxAbs) maxAbs = a;
if (b > maxAbs) maxAbs = b;
if (c > maxAbs) maxAbs = c;
perPoint[i] = a > b ? (a > c ? a : c) : (b > c ? b : c);
}
const sorted = Array.from(perPoint).sort((a, b) => a - b);
const scale = Math.max(sorted[Math.floor(n * 0.95)], 1e-9);
const out = new Float32Array(n * 3);
for (let i = 0; i < n; i++) {
out[i*3] = (points[i][0] - mx) / maxAbs;
out[i*3+1] = (points[i][1] - my) / maxAbs;
out[i*3+2] = (points[i][2] - mz) / maxAbs;
out[i*3] = (points[i][0] - mx) / scale;
out[i*3+1] = (points[i][1] - my) / scale;
out[i*3+2] = (points[i][2] - mz) / scale;
}
// scaleFactor converts raw-unit jitter to normalized-unit jitter
// so a slider value in raw coords lands correctly after normalize().
return { positions: out, scaleFactor: 1 / maxAbs };
return { positions: out };
}
@ -440,7 +451,7 @@ function buildColors(labels, kind) {
}
function createScene(container, dataset) {
const { positions: basePositions, scaleFactor } = normalize(dataset.points);
const { positions: basePositions } = normalize(dataset.points);
const colors = buildColors(dataset.labels, dataset.kind);
// mutable copy — the render loop writes jittered positions here.
@ -482,7 +493,6 @@ function createScene(container, dataset) {
return {
scene, camera, renderer, controls, container, geometry,
basePositions,
scaleFactor,
// trajectory state is populated by buildTrajectories() once at init
trajectories: null,
numFrames: 0,
@ -543,6 +553,17 @@ async function main() {
const order = Object.entries(data);
let selectedId = null;
// A single ResizeObserver keeps every viz canvas sized to its real,
// post-layout container width. Handles initial layout, scroll-container
// late measurements, and window resize uniformly.
const vizToScene = new WeakMap();
const sizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const s = vizToScene.get(entry.target);
if (s) sizeScene(s);
}
});
order.forEach(([id, ds], i) => {
const card = document.createElement('div');
card.className = 'card';
@ -566,10 +587,13 @@ async function main() {
const viz = card.querySelector('.viz');
const s = createScene(viz, ds);
sizeScene(s);
// Precompute at the max frame count; toggling only changes cycle length.
buildTrajectories(s, MAX_FRAMES);
scenes.push(s);
// ResizeObserver fires once on observe with the real laid-out width,
// which is what we want — skips the forEach-time measurement race.
vizToScene.set(viz, s);
sizeObserver.observe(viz);
// Stop auto-rotate once the user interacts.
s.controls.addEventListener('start', () => { s.controls.autoRotate = false; });
@ -675,12 +699,8 @@ async function main() {
alert(`Would continue with generator:\n${ds.path}\n\n(demo — no flow dispatched yet)`);
});
// Resize handling.
let resizeTimer = null;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => scenes.forEach(sizeScene), 80);
});
// Resize is handled by the ResizeObserver above — it fires per-card on
// any width change (including the initial layout settle).
// Render loop. Each scene walks through its cumulative trajectory,
// interpolating between consecutive frames; wraps from frame N-1 back to 0

View File

@ -3,23 +3,48 @@ from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from sklearn.datasets import make_blobs, make_s_curve, make_swiss_roll
from sklearn.datasets import (
make_blobs,
make_classification,
make_gaussian_quantiles,
make_s_curve,
make_swiss_roll,
)
app = FastAPI()
HERE = Path(__file__).parent
N = 5000
SEED = 0
@lru_cache(maxsize=1)
def _datasets():
s, sl = make_s_curve(n_samples=5000, noise=0.03, random_state=0)
sr, srl = make_swiss_roll(n_samples=5000, noise=0.15, random_state=0)
s, sl = make_s_curve(n_samples=N, noise=0.03, random_state=SEED)
sr, srl = make_swiss_roll(n_samples=N, noise=0.15, random_state=SEED)
srh, srhl = make_swiss_roll(n_samples=N, noise=0.15, hole=True, random_state=SEED)
b, bl = make_blobs(
n_samples=5000, n_features=3, centers=5, cluster_std=1.0, random_state=0
n_samples=N, n_features=3, centers=5, cluster_std=1.0, random_state=SEED
)
gq, gql = make_gaussian_quantiles(
n_samples=N, n_features=3, n_classes=4, random_state=SEED
)
cls, clsl = make_classification(
n_samples=N,
n_features=3,
n_informative=3,
n_redundant=0,
n_repeated=0,
n_classes=4,
n_clusters_per_class=2,
class_sep=1.5,
random_state=SEED,
)
return {
"s_curve": {
"name": "S-Curve",
"path": "sklearn.datasets.make_s_curve",
"kwargs": {},
"description": (
"A 2-D manifold warped into R³. Continuous label encodes position "
"along the curve — a good test of whether a reducer unrolls the "
@ -32,6 +57,7 @@ def _datasets():
"swiss_roll": {
"name": "Swiss Roll",
"path": "sklearn.datasets.make_swiss_roll",
"kwargs": {},
"description": (
"A rolled-up plane. The canonical hard case for linear methods: "
"PCA collapses the spiral, non-linear methods should recover the "
@ -41,9 +67,23 @@ def _datasets():
"points": sr.tolist(),
"labels": srl.tolist(),
},
"swiss_roll_hole": {
"name": "Swiss Roll (hole)",
"path": "sklearn.datasets.make_swiss_roll",
"kwargs": {"hole": True},
"description": (
"Swiss roll with a rectangular hole punched through. Same manifold, "
"non-trivial topology — a faithful unroll should preserve the hole "
"rather than smearing it closed."
),
"kind": "continuous",
"points": srh.tolist(),
"labels": srhl.tolist(),
},
"blobs": {
"name": "Gaussian Blobs",
"path": "sklearn.datasets.make_blobs",
"kwargs": {"centers": 5, "cluster_std": 1.0},
"description": (
"Five isotropic Gaussian clusters in R³. Discrete class labels. "
"Tests whether a reducer preserves cluster separation when "
@ -53,6 +93,39 @@ def _datasets():
"points": b.tolist(),
"labels": bl.tolist(),
},
"gaussian_quantiles": {
"name": "Gaussian Quantiles",
"path": "sklearn.datasets.make_gaussian_quantiles",
"kwargs": {"n_classes": 4},
"description": (
"Concentric Gaussian shells in R³; class = which shell. Classes "
"are linearly inseparable by construction — PCA collapses them, "
"kernel and manifold methods have a chance."
),
"kind": "categorical",
"points": gq.tolist(),
"labels": gql.tolist(),
},
"classification": {
"name": "Hypercube Clusters",
"path": "sklearn.datasets.make_classification",
"kwargs": {
"n_informative": 3,
"n_redundant": 0,
"n_repeated": 0,
"n_classes": 4,
"n_clusters_per_class": 2,
"class_sep": 1.5,
},
"description": (
"Four classes, two sub-clusters each, placed at hypercube vertices "
"with informative noise. A denser discrete test than blobs — "
"within-class bimodality stresses cluster-preserving reducers."
),
"kind": "categorical",
"points": cls.tolist(),
"labels": clsl.tolist(),
},
}