Spaces:
Running
Running
§6 UMAP: bias default view toward the dense bottom-right clusters
Browse filesLock min zoom to ~1.35 with a bottom-right offset (tx=-0.18, ty=0.15) so
the landing frame skips the wide empty band along the top and ~40% of the
viewport stops going to whitespace. Min-zoom matches the default view,
so visitors can pan/zoom around but never reach the fit-everything view
where the clusters drown in margin.
Co-authored-by: Cursor <cursoragent@cursor.com>
- assets/js/sections/umap.js +26 -13
assets/js/sections/umap.js
CHANGED
|
@@ -152,8 +152,17 @@
|
|
| 152 |
let bounds = [0,0,0,0];
|
| 153 |
let colorBy = "species";
|
| 154 |
// Viewport: translate (tx, ty) + scale around origin, in NDC space.
|
| 155 |
-
//
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
let dpr = Math.max(1, window.devicePixelRatio || 1);
|
| 158 |
let needsRedraw = false;
|
| 159 |
// Spatial grid for hover (built once after data load, in normalized world space).
|
|
@@ -687,11 +696,14 @@
|
|
| 687 |
// distracting always-active control on first paint.
|
| 688 |
function updateResetEnabled() {
|
| 689 |
if (!resetBtn) return;
|
| 690 |
-
const atDefault =
|
|
|
|
|
|
|
|
|
|
| 691 |
resetBtn.disabled = atDefault;
|
| 692 |
}
|
| 693 |
function resetView() {
|
| 694 |
-
view = {
|
| 695 |
updateResetEnabled();
|
| 696 |
requestRedraw();
|
| 697 |
}
|
|
@@ -699,10 +711,10 @@
|
|
| 699 |
// Keep the viewport always full of data. The data spans [-0.92, 0.92]·scale
|
| 700 |
// in world space; the viewport spans [-1, 1]. As long as 0.92·scale ≥ 1
|
| 701 |
// (zoom ≥ ~1.087), there's "slack" we can pan within: |tx| ≤ 0.92·scale-1.
|
| 702 |
-
//
|
| 703 |
-
//
|
| 704 |
-
//
|
| 705 |
-
//
|
| 706 |
function clampPan() {
|
| 707 |
const m = Math.max(0, 0.92 * view.scale - 1);
|
| 708 |
if (m === 0) {
|
|
@@ -766,11 +778,12 @@
|
|
| 766 |
const ndc = clientToNDC(e);
|
| 767 |
// Zoom factor — natural feeling on both trackpad and mouse wheel.
|
| 768 |
const factor = Math.exp(-e.deltaY * 0.0018);
|
| 769 |
-
// Min scale =
|
| 770 |
-
//
|
| 771 |
-
//
|
| 772 |
-
//
|
| 773 |
-
|
|
|
|
| 774 |
const k = newScale / view.scale;
|
| 775 |
// Zoom around the cursor: shift translate so the point under the cursor
|
| 776 |
// stays under the cursor.
|
|
|
|
| 152 |
let bounds = [0,0,0,0];
|
| 153 |
let colorBy = "species";
|
| 154 |
// Viewport: translate (tx, ty) + scale around origin, in NDC space.
|
| 155 |
+
// We *don't* show the data fully fit-to-canvas at zoom 1: the UMAP layout
|
| 156 |
+
// has a wide empty band along the top edge and a smaller one on the left,
|
| 157 |
+
// and fit-all wastes ~40% of the viewport on whitespace. Instead the
|
| 158 |
+
// landing view is biased toward the bottom-right (where Vertebrates, Plants,
|
| 159 |
+
// Fungi/Bacteria/Viruses sit) with a moderate zoom so the dense clusters
|
| 160 |
+
// fill the frame on first paint. The same view doubles as the minimum
|
| 161 |
+
// zoom-out (scale ≥ MIN_SCALE in the wheel handler) so visitors can pan
|
| 162 |
+
// around but can never zoom *back* into the empty-margins view.
|
| 163 |
+
const MIN_SCALE = 1.35;
|
| 164 |
+
const DEFAULT_VIEW = { tx: -0.18, ty: 0.15, scale: MIN_SCALE };
|
| 165 |
+
let view = { ...DEFAULT_VIEW };
|
| 166 |
let dpr = Math.max(1, window.devicePixelRatio || 1);
|
| 167 |
let needsRedraw = false;
|
| 168 |
// Spatial grid for hover (built once after data load, in normalized world space).
|
|
|
|
| 696 |
// distracting always-active control on first paint.
|
| 697 |
function updateResetEnabled() {
|
| 698 |
if (!resetBtn) return;
|
| 699 |
+
const atDefault =
|
| 700 |
+
view.tx === DEFAULT_VIEW.tx &&
|
| 701 |
+
view.ty === DEFAULT_VIEW.ty &&
|
| 702 |
+
view.scale === DEFAULT_VIEW.scale;
|
| 703 |
resetBtn.disabled = atDefault;
|
| 704 |
}
|
| 705 |
function resetView() {
|
| 706 |
+
view = { ...DEFAULT_VIEW };
|
| 707 |
updateResetEnabled();
|
| 708 |
requestRedraw();
|
| 709 |
}
|
|
|
|
| 711 |
// Keep the viewport always full of data. The data spans [-0.92, 0.92]·scale
|
| 712 |
// in world space; the viewport spans [-1, 1]. As long as 0.92·scale ≥ 1
|
| 713 |
// (zoom ≥ ~1.087), there's "slack" we can pan within: |tx| ≤ 0.92·scale-1.
|
| 714 |
+
// Since MIN_SCALE > 1.087 the slack branch is always taken in practice —
|
| 715 |
+
// the m === 0 fallback only kicks in if somebody later relaxes the wheel-
|
| 716 |
+
// handler clamp below ~1.087, in which case data snaps to (0, 0) so no
|
| 717 |
+
// white edge creeps in.
|
| 718 |
function clampPan() {
|
| 719 |
const m = Math.max(0, 0.92 * view.scale - 1);
|
| 720 |
if (m === 0) {
|
|
|
|
| 778 |
const ndc = clientToNDC(e);
|
| 779 |
// Zoom factor — natural feeling on both trackpad and mouse wheel.
|
| 780 |
const factor = Math.exp(-e.deltaY * 0.0018);
|
| 781 |
+
// Min scale = MIN_SCALE (matches DEFAULT_VIEW.scale): we intentionally
|
| 782 |
+
// disallow zooming further out than the bottom-right framing so the user
|
| 783 |
+
// can't reach the fit-everything view where ~40% of the canvas is empty
|
| 784 |
+
// margin (long whitespace band above the clusters). Max 50× keeps
|
| 785 |
+
// individual points pickable.
|
| 786 |
+
const newScale = Math.min(50, Math.max(MIN_SCALE, view.scale * factor));
|
| 787 |
const k = newScale / view.scale;
|
| 788 |
// Zoom around the cursor: shift translate so the point under the cursor
|
| 789 |
// stays under the cursor.
|