search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: label collision avoidance and increased zoom for mobile

- Labels now check bounding box overlaps before rendering — dense
clusters show only the most important labels instead of stacking
- Coarse/fine labels sorted by cluster size so biggest clusters win
- Max zoom increased from 15x to 30x for exploring dense areas
- Legend and stats no longer overlap on mobile (stacked vertically)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+41 -20
+3 -3
site/atlas.css
··· 245 245 .header .nav { gap: 8px; font-size: 10px; } 246 246 #search-input { width: 90px; font-size: 10px; padding: 4px 6px; } 247 247 #search-input:focus { width: 130px; } 248 - .legend { bottom: 8px; left: 10px; gap: 6px; font-size: 9px; } 249 - .stats { bottom: 8px; right: 10px; font-size: 9px; } 250 - .tooltip { max-width: 240px; font-size: 11px; } 248 + .legend { bottom: 24px; left: 10px; right: 10px; gap: 6px; font-size: 9px; } 249 + .stats { bottom: 8px; left: 10px; right: 10px; text-align: center; font-size: 9px; } 250 + .tooltip { max-width: 260px; font-size: 11px; } 251 251 .tooltip .meta { font-size: 10px; } 252 252 .tooltip .platform-badge { font-size: 9px; } 253 253 }
+38 -17
site/atlas.js
··· 31 31 } 32 32 33 33 // --- view state --- 34 - var view = { zoom: 1, panX: 0, panY: 0, minZoom: 0.5, maxZoom: 15, dirty: true }; 34 + var view = { zoom: 1, panX: 0, panY: 0, minZoom: 0.5, maxZoom: 30, dirty: true }; 35 35 36 36 // --- data --- 37 37 var data = null; ··· 377 377 } 378 378 } 379 379 380 - // --- labels (no shadowBlur — uses strokeText outline instead) --- 380 + // --- labels with collision avoidance --- 381 381 ctx.globalAlpha = 1; 382 382 ctx.fillStyle = dark ? 'rgba(255,255,255,0.75)' : 'rgba(0,0,0,0.65)'; 383 383 ctx.textAlign = 'center'; 384 384 ctx.textBaseline = 'middle'; 385 385 386 386 var small = W < 600; 387 + // placed label bounding boxes for collision detection 388 + var placed = []; // [{x, y, hw, hh}] — center + half-width/half-height 389 + var PAD = small ? 2 : 4; // padding between labels 390 + 391 + function canPlace(lx, ly, tw, th) { 392 + var hw = tw / 2 + PAD, hh = th / 2 + PAD; 393 + for (var k = 0; k < placed.length; k++) { 394 + var p = placed[k]; 395 + if (Math.abs(lx - p.x) < hw + p.hw && Math.abs(ly - p.y) < hh + p.hh) return false; 396 + } 397 + placed.push({ x: lx, y: ly, hw: hw, hh: hh }); 398 + return true; 399 + } 400 + 387 401 if (zoom < 2) { 388 - // coarse labels 402 + // coarse labels — sort by cluster size so biggest labels win 389 403 ctx.font = (small ? '9px' : '12px') + ' monospace'; 390 404 ctx.globalAlpha = 0.85; 391 - for (var c = 0; c < data.clusters.coarse.length; c++) { 392 - var cl = data.clusters.coarse[c]; 393 - var sx = cx + cl.cx * scale, sy = cy + cl.cy * scale; 405 + var fontSize = small ? 9 : 12; 406 + var sorted = data.clusters.coarse.slice().sort(function(a, b) { return b.count - a.count; }); 407 + for (var c = 0; c < sorted.length; c++) { 408 + var cl = sorted[c]; 409 + var sx = cx + cl.cx * scale, sy = cy + cl.cy * scale - Math.sqrt(cl.count) * 1.5; 394 410 if (sx < -50 || sx > W + 50 || sy < -20 || sy > H + 20) continue; 395 - drawLabel(cl.label, sx, sy - Math.sqrt(cl.count) * 1.5, dark); 411 + var tw = ctx.measureText(cl.label).width; 412 + if (canPlace(sx, sy, tw, fontSize)) drawLabel(cl.label, sx, sy, dark); 396 413 } 397 414 } else if (zoom < 5) { 398 - // fine labels 415 + // fine labels — sort by size 399 416 ctx.font = (small ? '8px' : '11px') + ' monospace'; 400 417 ctx.globalAlpha = 0.75; 401 - for (var c = 0; c < data.clusters.fine.length; c++) { 402 - var cl = data.clusters.fine[c]; 418 + var fontSize = small ? 8 : 11; 419 + var sorted = data.clusters.fine.slice().sort(function(a, b) { return b.count - a.count; }); 420 + for (var c = 0; c < sorted.length; c++) { 421 + var cl = sorted[c]; 403 422 if (cl.cx < xMin || cl.cx > xMax || cl.cy < yMin || cl.cy > yMax) continue; 404 - var sx = cx + cl.cx * scale, sy = cy + cl.cy * scale; 423 + var sx = cx + cl.cx * scale, sy = cy + cl.cy * scale - 14; 405 424 if (sx < -50 || sx > W + 50 || sy < -20 || sy > H + 20) continue; 406 - drawLabel(cl.label, sx, sy - 14, dark); 425 + var tw = ctx.measureText(cl.label).width; 426 + if (canPlace(sx, sy, tw, fontSize)) drawLabel(cl.label, sx, sy, dark); 407 427 } 408 428 } else { 409 429 // document titles 410 430 ctx.font = (small ? '9px' : '11px') + ' monospace'; 411 431 ctx.globalAlpha = 0.7; 412 - var shown = 0, maxLabels = small ? 25 : 50; 413 - var truncLen = small ? 30 : 45; 432 + var fontSize = small ? 9 : 11; 433 + var shown = 0, maxLabels = small ? 20 : 50; 434 + var truncLen = small ? 25 : 45; 414 435 for (var i = 0; i < n && shown < maxLabels; i++) { 415 436 var px = pointsX[i], py = pointsY[i]; 416 437 if (px < xMin || px > xMax || py < yMin || py > yMax) continue; 417 438 var title = data.points[i].title; 418 439 if (!title) continue; 419 - var sx = cx + px * scale, sy = cy + py * scale; 440 + var sx = cx + px * scale, sy = cy + py * scale - 10; 420 441 if (sx < 0 || sx > W || sy < 0 || sy > H) continue; 421 442 if (title.length > truncLen) title = title.substring(0, truncLen - 2) + '\u2026'; 422 - drawLabel(title, sx, sy - 10, dark); 423 - shown++; 443 + var tw = ctx.measureText(title).width; 444 + if (canPlace(sx, sy, tw, fontSize)) { drawLabel(title, sx, sy, dark); shown++; } 424 445 } 425 446 } 426 447