Update viewer (default language, LOD tuning, filter foreground, embeddings text)
Browse files- index.html +39 -18
- manifest.json +0 -0
index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
-
<title>Web-graph
|
| 7 |
<!-- Streaming (fetch-based) LOD viewer. Reads ./manifest.json + ./t/<id>.bin tiles produced by
|
| 8 |
embed_2d_tile.py, lazy-loading tiles by viewport + zoom so the full ~53 M-node map fits without
|
| 9 |
inlining anything. Serve its directory over HTTP (python -m http.server) and open this file. -->
|
|
@@ -129,8 +129,8 @@
|
|
| 129 |
<div id="stage"><canvas id="cv" aria-label="2D map of host embeddings"></canvas></div>
|
| 130 |
|
| 131 |
<section id="rail" class="panel" aria-label="Controls">
|
| 132 |
-
<div class="brand"><h1>Web-graph
|
| 133 |
-
<p class="lede" id="lede">The link-prediction
|
| 134 |
Structure the model learned is the shape you see. Recolor to ask which properties it separates.</p>
|
| 135 |
<div class="grp">
|
| 136 |
<span class="lbl">Color by</span>
|
|
@@ -201,13 +201,15 @@
|
|
| 201 |
const prog = gl.createProgram();
|
| 202 |
gl.attachShader(prog, sh(gl.VERTEX_SHADER, `
|
| 203 |
attribute vec2 a_xy; attribute vec3 a_col; attribute float a_deg; attribute float a_alpha;
|
| 204 |
-
uniform vec2 u_scale; uniform vec2 u_off; uniform float u_pt;
|
| 205 |
varying vec3 v_col; varying float v_alpha;
|
| 206 |
void main() {
|
| 207 |
vec2 p = a_xy * u_scale + u_off;
|
| 208 |
gl_Position = vec4(p, 0.0, 1.0);
|
| 209 |
gl_PointSize = u_pt * (0.65 + 1.9 * a_deg);
|
| 210 |
v_col = a_col; v_alpha = a_alpha;
|
|
|
|
|
|
|
| 211 |
}`));
|
| 212 |
gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, `
|
| 213 |
precision mediump float; varying vec3 v_col; varying float v_alpha; uniform float u_alpha;
|
|
@@ -227,12 +229,13 @@
|
|
| 227 |
const U = {
|
| 228 |
scale: gl.getUniformLocation(prog, "u_scale"), off: gl.getUniformLocation(prog, "u_off"),
|
| 229 |
pt: gl.getUniformLocation(prog, "u_pt"), alpha: gl.getUniformLocation(prog, "u_alpha"),
|
|
|
|
| 230 |
};
|
| 231 |
gl.enableVertexAttribArray(A.xy); gl.enableVertexAttribArray(A.col);
|
| 232 |
gl.enableVertexAttribArray(A.deg); gl.enableVertexAttribArray(A.alpha);
|
| 233 |
|
| 234 |
// ββ state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 235 |
-
let META = null, N = 0, nCat = 0, active = 0, filterVal = -1;
|
| 236 |
let view = { s: 0.9, cx: 0, cy: 0 };
|
| 237 |
let asp = 1, dpr = Math.min(window.devicePixelRatio || 1, 2), dirty = true;
|
| 238 |
const tileMeta = new Map(); // id -> manifest tile {id,z,n,bbox}
|
|
@@ -266,8 +269,9 @@
|
|
| 266 |
$("nnodes").textContent = N >= 1e6 ? (N / 1e6).toFixed(N % 1e6 ? 1 : 0) + "M"
|
| 267 |
: (N / 1e3).toFixed(0) + "k";
|
| 268 |
if (META.lede) $("lede").textContent = META.lede;
|
|
|
|
| 269 |
buildColorBy(); buildSearch();
|
| 270 |
-
resize(); recolor(
|
| 271 |
}
|
| 272 |
|
| 273 |
// ββ tile loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -335,9 +339,13 @@
|
|
| 335 |
}
|
| 336 |
|
| 337 |
// ββ LOD + viewport β which tiles are active βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 338 |
-
// LOD = degree band shown
|
| 339 |
-
//
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
function viewport() {
|
| 342 |
const hw = 1 / (view.s * asp), hh = 1 / view.s, m = 0.25; // 25% prefetch margin
|
| 343 |
return [view.cx - hw * (1 + m), view.cy - hh * (1 + m),
|
|
@@ -364,6 +372,18 @@
|
|
| 364 |
}
|
| 365 |
|
| 366 |
// ββ draw ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
function draw() {
|
| 368 |
const bg = getComputedStyle(document.body).backgroundColor.match(/\d+/g) || [11, 14, 21];
|
| 369 |
gl.clearColor(bg[0] / 255, bg[1] / 255, bg[2] / 255, 1); gl.clear(gl.COLOR_BUFFER_BIT);
|
|
@@ -372,15 +392,16 @@
|
|
| 372 |
gl.uniform2f(U.off, -view.cx * view.s * asp, -view.cy * view.s);
|
| 373 |
// sub-linear (sqrt) point growth: gaps outpace dots on zoom-in so clusters declutter
|
| 374 |
gl.uniform1f(U.pt, Math.max(1.2, dpr * (1.5 + 3.4 * Math.sqrt(view.s))));
|
|
|
|
|
|
|
| 375 |
gl.uniform1f(U.alpha, N > 4e6 ? 0.5 : 0.75);
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
gl.
|
| 380 |
-
gl.
|
| 381 |
-
|
| 382 |
-
gl.
|
| 383 |
-
gl.drawArrays(gl.POINTS, 0, t.n); drawn++;
|
| 384 |
}
|
| 385 |
$("tiles").textContent = drawn;
|
| 386 |
if (pin) drawPin();
|
|
@@ -413,7 +434,7 @@
|
|
| 413 |
const seg = $("colorby");
|
| 414 |
META.labelSets.forEach((ls, i) => {
|
| 415 |
const b = document.createElement("button"); b.textContent = ls.name;
|
| 416 |
-
b.setAttribute("aria-pressed", i ===
|
| 417 |
b.onclick = () => { [...seg.children].forEach((c) => c.setAttribute("aria-pressed", "false"));
|
| 418 |
b.setAttribute("aria-pressed", "true"); recolor(i); };
|
| 419 |
seg.appendChild(b);
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>Web-graph embeddings β 2D map (streaming)</title>
|
| 7 |
<!-- Streaming (fetch-based) LOD viewer. Reads ./manifest.json + ./t/<id>.bin tiles produced by
|
| 8 |
embed_2d_tile.py, lazy-loading tiles by viewport + zoom so the full ~53 M-node map fits without
|
| 9 |
inlining anything. Serve its directory over HTTP (python -m http.server) and open this file. -->
|
|
|
|
| 129 |
<div id="stage"><canvas id="cv" aria-label="2D map of host embeddings"></canvas></div>
|
| 130 |
|
| 131 |
<section id="rail" class="panel" aria-label="Controls">
|
| 132 |
+
<div class="brand"><h1>Web-graph embeddings</h1><span class="n" id="nnodes">β</span></div>
|
| 133 |
+
<p class="lede" id="lede">The link-prediction embeddings, projected to 2D and streamed by tile.
|
| 134 |
Structure the model learned is the shape you see. Recolor to ask which properties it separates.</p>
|
| 135 |
<div class="grp">
|
| 136 |
<span class="lbl">Color by</span>
|
|
|
|
| 201 |
const prog = gl.createProgram();
|
| 202 |
gl.attachShader(prog, sh(gl.VERTEX_SHADER, `
|
| 203 |
attribute vec2 a_xy; attribute vec3 a_col; attribute float a_deg; attribute float a_alpha;
|
| 204 |
+
uniform vec2 u_scale; uniform vec2 u_off; uniform float u_pt; uniform float u_matchOnly;
|
| 205 |
varying vec3 v_col; varying float v_alpha;
|
| 206 |
void main() {
|
| 207 |
vec2 p = a_xy * u_scale + u_off;
|
| 208 |
gl_Position = vec4(p, 0.0, 1.0);
|
| 209 |
gl_PointSize = u_pt * (0.65 + 1.9 * a_deg);
|
| 210 |
v_col = a_col; v_alpha = a_alpha;
|
| 211 |
+
// foreground pass: draw only the filter-matched points (alpha==1), cull the dimmed rest
|
| 212 |
+
if (u_matchOnly > 0.5 && a_alpha < 0.99) { gl_PointSize = 0.0; gl_Position = vec4(2.0, 2.0, 2.0, 1.0); }
|
| 213 |
}`));
|
| 214 |
gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, `
|
| 215 |
precision mediump float; varying vec3 v_col; varying float v_alpha; uniform float u_alpha;
|
|
|
|
| 229 |
const U = {
|
| 230 |
scale: gl.getUniformLocation(prog, "u_scale"), off: gl.getUniformLocation(prog, "u_off"),
|
| 231 |
pt: gl.getUniformLocation(prog, "u_pt"), alpha: gl.getUniformLocation(prog, "u_alpha"),
|
| 232 |
+
matchOnly: gl.getUniformLocation(prog, "u_matchOnly"),
|
| 233 |
};
|
| 234 |
gl.enableVertexAttribArray(A.xy); gl.enableVertexAttribArray(A.col);
|
| 235 |
gl.enableVertexAttribArray(A.deg); gl.enableVertexAttribArray(A.alpha);
|
| 236 |
|
| 237 |
// ββ state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 238 |
+
let META = null, N = 0, nCat = 0, active = 0, filterVal = -1, defaultSet = 0;
|
| 239 |
let view = { s: 0.9, cx: 0, cy: 0 };
|
| 240 |
let asp = 1, dpr = Math.min(window.devicePixelRatio || 1, 2), dirty = true;
|
| 241 |
const tileMeta = new Map(); // id -> manifest tile {id,z,n,bbox}
|
|
|
|
| 269 |
$("nnodes").textContent = N >= 1e6 ? (N / 1e6).toFixed(N % 1e6 ? 1 : 0) + "M"
|
| 270 |
: (N / 1e3).toFixed(0) + "k";
|
| 271 |
if (META.lede) $("lede").textContent = META.lede;
|
| 272 |
+
defaultSet = Math.max(0, META.labelSets.findIndex((l) => l.name === "language")); // color by language on load
|
| 273 |
buildColorBy(); buildSearch();
|
| 274 |
+
resize(); recolor(defaultSet); requestAnimationFrame(loop);
|
| 275 |
}
|
| 276 |
|
| 277 |
// ββ tile loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
// ββ LOD + viewport β which tiles are active βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 342 |
+
// LOD = highest degree band shown at the current zoom. The bands themselves are baked into the
|
| 343 |
+
// tiles; this only controls WHEN each appears. LOD_RATE = bands revealed per zoom-doubling (lower
|
| 344 |
+
// β low-degree hosts hold off until you zoom deeper); LOD_BIAS = bands the overview already shows.
|
| 345 |
+
const LOD_RATE = 0.7, LOD_BIAS = 0.4;
|
| 346 |
+
function lodLevel() {
|
| 347 |
+
return Math.max(0, Math.min(META.zMax, Math.round(Math.log2(view.s) * LOD_RATE + LOD_BIAS)));
|
| 348 |
+
}
|
| 349 |
function viewport() {
|
| 350 |
const hw = 1 / (view.s * asp), hh = 1 / view.s, m = 0.25; // 25% prefetch margin
|
| 351 |
return [view.cx - hw * (1 + m), view.cy - hh * (1 + m),
|
|
|
|
| 372 |
}
|
| 373 |
|
| 374 |
// ββ draw ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 375 |
+
function drawTiles() { // bind + draw each ready active tile; returns count
|
| 376 |
+
let drawn = 0;
|
| 377 |
+
for (const id of activeIds) {
|
| 378 |
+
const t = ready.get(id); if (!t || t.status !== "ready") continue;
|
| 379 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, t.bXY); gl.vertexAttribPointer(A.xy, 2, gl.SHORT, true, 0, 0);
|
| 380 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, t.bCol); gl.vertexAttribPointer(A.col, 3, gl.UNSIGNED_BYTE, true, 0, 0);
|
| 381 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, t.bDeg); gl.vertexAttribPointer(A.deg, 1, gl.UNSIGNED_BYTE, true, 0, 0);
|
| 382 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, t.bAlpha); gl.vertexAttribPointer(A.alpha, 1, gl.UNSIGNED_BYTE, true, 0, 0);
|
| 383 |
+
gl.drawArrays(gl.POINTS, 0, t.n); drawn++;
|
| 384 |
+
}
|
| 385 |
+
return drawn;
|
| 386 |
+
}
|
| 387 |
function draw() {
|
| 388 |
const bg = getComputedStyle(document.body).backgroundColor.match(/\d+/g) || [11, 14, 21];
|
| 389 |
gl.clearColor(bg[0] / 255, bg[1] / 255, bg[2] / 255, 1); gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
| 392 |
gl.uniform2f(U.off, -view.cx * view.s * asp, -view.cy * view.s);
|
| 393 |
// sub-linear (sqrt) point growth: gaps outpace dots on zoom-in so clusters declutter
|
| 394 |
gl.uniform1f(U.pt, Math.max(1.2, dpr * (1.5 + 3.4 * Math.sqrt(view.s))));
|
| 395 |
+
// pass 1: whole scene (filtered-out points already dimmed to ~10% via per-vertex alpha)
|
| 396 |
+
gl.uniform1f(U.matchOnly, 0);
|
| 397 |
gl.uniform1f(U.alpha, N > 4e6 ? 0.5 : 0.75);
|
| 398 |
+
const drawn = drawTiles();
|
| 399 |
+
// pass 2 (filter active): redraw only the matched points, opaque and on top (foreground)
|
| 400 |
+
if (filterVal >= 0) {
|
| 401 |
+
gl.uniform1f(U.matchOnly, 1);
|
| 402 |
+
gl.uniform1f(U.alpha, N > 4e6 ? 0.9 : 1.0);
|
| 403 |
+
drawTiles();
|
| 404 |
+
gl.uniform1f(U.matchOnly, 0);
|
|
|
|
| 405 |
}
|
| 406 |
$("tiles").textContent = drawn;
|
| 407 |
if (pin) drawPin();
|
|
|
|
| 434 |
const seg = $("colorby");
|
| 435 |
META.labelSets.forEach((ls, i) => {
|
| 436 |
const b = document.createElement("button"); b.textContent = ls.name;
|
| 437 |
+
b.setAttribute("aria-pressed", i === defaultSet ? "true" : "false");
|
| 438 |
b.onclick = () => { [...seg.children].forEach((c) => c.setAttribute("aria-pressed", "false"));
|
| 439 |
b.setAttribute("aria-pressed", "true"); recolor(i); };
|
| 440 |
seg.appendChild(b);
|
manifest.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|