Pretext: exacte tekstbreedte voor D3 force-graph nodes

Definitie

@chenglou/pretext is een JavaScript/TypeScript-library die tekst opmeet via canvas zonder DOM-reflows te veroorzaken. In D3 force-directed graphs vervangt het de gangbare heuristiek word.length * N + padding voor node-afmetingen.

Context

In SVG force-graphs op basis van D3 worden nodegroottes doorgaans geschat via tekstlengte of vaste hit areas. Dit leidt tot incorrecte collision-radii (woorden overlappen of staan te ver uit elkaar) en hit areas die niet overeenkomen met de zichtbare tekst. Pretext meet de werkelijke rendered breedte op basis van het canvas-font-systeem, inclusief CSS-eigenschappen zoals letter-spacing en text-transform.

Kernpunten

Basispatroon voor node-meting

import { prepareWithSegments, measureNaturalWidth } from '@chenglou/pretext';
 
function measureNodes(nodes) {
  for (const n of nodes) {
    // Uppercase-tekst (CSS text-transform) moet als uppercase gemeten worden
    const word = n.type === 'noun' ? n.word.toUpperCase() : n.word;
    const px = fontPxForType(n.type);
    const letterSpacing = n.type === 'noun' ? 0.08 * px : 0;
    const opts = letterSpacing ? { letterSpacing } : {};
    const prepared = prepareWithSegments(word, fontStringForType(n.type), opts);
    n.textWidth = measureNaturalWidth(prepared);
    n.textPx = px;
  }
}

Font-string moet overeenkomen met CSS

De font-string voor prepareWithSegments volgt de CSS font-shorthand notatie: "italic 700 21px Wavehaus, Futura, sans-serif". Als de font-string afwijkt van wat de browser gebruikt, wijkt de meting af.

Na font-loading meten

Meting moet plaatsvinden nadat webfonts geladen zijn, anders meet Pretext de fallback-font. Gebruik document.fonts.ready:

fetch(DATA_URL)
  .then(r => r.json())
  .then(data => document.fonts.ready.then(() => data))
  .then(data => {
    measureNodes(data.nodes);
    buildGraph(data);
  });

Integratie in D3

Gebruik de gemeten breedte voor hit areas en collision-radius:

// Hit area als rechthoek op basis van gemeten breedte
node.append('rect')
  .attr('x', d => -(d.textWidth / 2 + PAD_H))
  .attr('y', d => -(d.textPx / 2 + PAD_V))
  .attr('width', d => d.textWidth + PAD_H * 2)
  .attr('height', d => d.textPx + PAD_V * 2);
 
// Collision force op basis van werkelijke breedte
.force('collision', d3.forceCollide().radius(d => d.textWidth / 2 + PAD_H + 4))

Wanneer Pretext niet nodig is

Voor enkelvoudige woorden zonder bijzondere CSS is canvas.measureText(word) functioneel equivalent. Pretext voegt waarde toe bij: complexe scripts (bidirectioneel, CJK), letter-spacing, multi-line tekst, of wanneer meerdere font-varianten in één pass gemeten worden.

Wat Pretext niet is

Pretext is geen CSS-vervanger. Het elimineert DOM-reflows bij tekstmeting (getBoundingClientRect, offsetHeight), niet CSS zelf.

Verbanden

Bronnen

Lou, C. (2026). Pretext [Software]. GitHub. https://github.com/chenglou/pretext

Sessie-herkomst