adding in more examples
This commit is contained in:
parent
22ca411210
commit
ac511c942f
@ -167,6 +167,9 @@
|
|||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
flex: 0 0 260px;
|
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;
|
scroll-snap-align: start;
|
||||||
border: 1px solid var(--hair);
|
border: 1px solid var(--hair);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@ -261,6 +264,8 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
/* Long sklearn paths would otherwise overflow and force the card wider. */
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.card-desc {
|
.card-desc {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -398,24 +403,30 @@ function normalize(points) {
|
|||||||
let mx = 0, my = 0, mz = 0;
|
let mx = 0, my = 0, mz = 0;
|
||||||
for (const p of points) { mx += p[0]; my += p[1]; mz += p[2]; }
|
for (const p of points) { mx += p[0]; my += p[1]; mz += p[2]; }
|
||||||
mx /= n; my /= n; mz /= n;
|
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 a = Math.abs(p[0] - mx);
|
||||||
const b = Math.abs(p[1] - my);
|
const b = Math.abs(p[1] - my);
|
||||||
const c = Math.abs(p[2] - mz);
|
const c = Math.abs(p[2] - mz);
|
||||||
if (a > maxAbs) maxAbs = a;
|
perPoint[i] = a > b ? (a > c ? a : c) : (b > c ? b : c);
|
||||||
if (b > maxAbs) maxAbs = b;
|
|
||||||
if (c > maxAbs) maxAbs = 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);
|
const out = new Float32Array(n * 3);
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
out[i*3] = (points[i][0] - mx) / maxAbs;
|
out[i*3] = (points[i][0] - mx) / scale;
|
||||||
out[i*3+1] = (points[i][1] - my) / maxAbs;
|
out[i*3+1] = (points[i][1] - my) / scale;
|
||||||
out[i*3+2] = (points[i][2] - mz) / maxAbs;
|
out[i*3+2] = (points[i][2] - mz) / scale;
|
||||||
}
|
}
|
||||||
// scaleFactor converts raw-unit jitter to normalized-unit jitter
|
return { positions: out };
|
||||||
// so a slider value in raw coords lands correctly after normalize().
|
|
||||||
return { positions: out, scaleFactor: 1 / maxAbs };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -440,7 +451,7 @@ function buildColors(labels, kind) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createScene(container, dataset) {
|
function createScene(container, dataset) {
|
||||||
const { positions: basePositions, scaleFactor } = normalize(dataset.points);
|
const { positions: basePositions } = normalize(dataset.points);
|
||||||
const colors = buildColors(dataset.labels, dataset.kind);
|
const colors = buildColors(dataset.labels, dataset.kind);
|
||||||
|
|
||||||
// mutable copy — the render loop writes jittered positions here.
|
// mutable copy — the render loop writes jittered positions here.
|
||||||
@ -482,7 +493,6 @@ function createScene(container, dataset) {
|
|||||||
return {
|
return {
|
||||||
scene, camera, renderer, controls, container, geometry,
|
scene, camera, renderer, controls, container, geometry,
|
||||||
basePositions,
|
basePositions,
|
||||||
scaleFactor,
|
|
||||||
// trajectory state is populated by buildTrajectories() once at init
|
// trajectory state is populated by buildTrajectories() once at init
|
||||||
trajectories: null,
|
trajectories: null,
|
||||||
numFrames: 0,
|
numFrames: 0,
|
||||||
@ -543,6 +553,17 @@ async function main() {
|
|||||||
const order = Object.entries(data);
|
const order = Object.entries(data);
|
||||||
let selectedId = null;
|
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) => {
|
order.forEach(([id, ds], i) => {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card';
|
card.className = 'card';
|
||||||
@ -566,10 +587,13 @@ async function main() {
|
|||||||
|
|
||||||
const viz = card.querySelector('.viz');
|
const viz = card.querySelector('.viz');
|
||||||
const s = createScene(viz, ds);
|
const s = createScene(viz, ds);
|
||||||
sizeScene(s);
|
|
||||||
// Precompute at the max frame count; toggling only changes cycle length.
|
// Precompute at the max frame count; toggling only changes cycle length.
|
||||||
buildTrajectories(s, MAX_FRAMES);
|
buildTrajectories(s, MAX_FRAMES);
|
||||||
scenes.push(s);
|
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.
|
// Stop auto-rotate once the user interacts.
|
||||||
s.controls.addEventListener('start', () => { s.controls.autoRotate = false; });
|
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)`);
|
alert(`Would continue with generator:\n${ds.path}\n\n(demo — no flow dispatched yet)`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize handling.
|
// Resize is handled by the ResizeObserver above — it fires per-card on
|
||||||
let resizeTimer = null;
|
// any width change (including the initial layout settle).
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
clearTimeout(resizeTimer);
|
|
||||||
resizeTimer = setTimeout(() => scenes.forEach(sizeScene), 80);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render loop. Each scene walks through its cumulative trajectory,
|
// Render loop. Each scene walks through its cumulative trajectory,
|
||||||
// interpolating between consecutive frames; wraps from frame N-1 back to 0
|
// interpolating between consecutive frames; wraps from frame N-1 back to 0
|
||||||
|
|||||||
@ -3,23 +3,48 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
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()
|
app = FastAPI()
|
||||||
HERE = Path(__file__).parent
|
HERE = Path(__file__).parent
|
||||||
|
|
||||||
|
N = 5000
|
||||||
|
SEED = 0
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _datasets():
|
def _datasets():
|
||||||
s, sl = make_s_curve(n_samples=5000, noise=0.03, random_state=0)
|
s, sl = make_s_curve(n_samples=N, noise=0.03, random_state=SEED)
|
||||||
sr, srl = make_swiss_roll(n_samples=5000, noise=0.15, random_state=0)
|
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(
|
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 {
|
return {
|
||||||
"s_curve": {
|
"s_curve": {
|
||||||
"name": "S-Curve",
|
"name": "S-Curve",
|
||||||
"path": "sklearn.datasets.make_s_curve",
|
"path": "sklearn.datasets.make_s_curve",
|
||||||
|
"kwargs": {},
|
||||||
"description": (
|
"description": (
|
||||||
"A 2-D manifold warped into R³. Continuous label encodes position "
|
"A 2-D manifold warped into R³. Continuous label encodes position "
|
||||||
"along the curve — a good test of whether a reducer unrolls the "
|
"along the curve — a good test of whether a reducer unrolls the "
|
||||||
@ -32,6 +57,7 @@ def _datasets():
|
|||||||
"swiss_roll": {
|
"swiss_roll": {
|
||||||
"name": "Swiss Roll",
|
"name": "Swiss Roll",
|
||||||
"path": "sklearn.datasets.make_swiss_roll",
|
"path": "sklearn.datasets.make_swiss_roll",
|
||||||
|
"kwargs": {},
|
||||||
"description": (
|
"description": (
|
||||||
"A rolled-up plane. The canonical hard case for linear methods: "
|
"A rolled-up plane. The canonical hard case for linear methods: "
|
||||||
"PCA collapses the spiral, non-linear methods should recover the "
|
"PCA collapses the spiral, non-linear methods should recover the "
|
||||||
@ -41,9 +67,23 @@ def _datasets():
|
|||||||
"points": sr.tolist(),
|
"points": sr.tolist(),
|
||||||
"labels": srl.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": {
|
"blobs": {
|
||||||
"name": "Gaussian Blobs",
|
"name": "Gaussian Blobs",
|
||||||
"path": "sklearn.datasets.make_blobs",
|
"path": "sklearn.datasets.make_blobs",
|
||||||
|
"kwargs": {"centers": 5, "cluster_std": 1.0},
|
||||||
"description": (
|
"description": (
|
||||||
"Five isotropic Gaussian clusters in R³. Discrete class labels. "
|
"Five isotropic Gaussian clusters in R³. Discrete class labels. "
|
||||||
"Tests whether a reducer preserves cluster separation when "
|
"Tests whether a reducer preserves cluster separation when "
|
||||||
@ -53,6 +93,39 @@ def _datasets():
|
|||||||
"points": b.tolist(),
|
"points": b.tolist(),
|
||||||
"labels": bl.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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user