Resources Numerical accuracy Calculation policy

Diagram layout into drawing objects

Places the blocks, connectors, arrowheads and labels of a diagram written as text, and turns them into lines, polylines, hatches and text in the drawing.

How the precision is kept

Every size is a multiple of the text height in 64-bit numbers. Block widths are estimated from the number of characters at 0.62 of the text height each, so the boxes are sized by that estimate and not by measured glyph widths. Positions are sums and averages of those sizes; no tolerance is used except a floor of 1e-6 on a symbol's bounding box.

Calculations under this policy

7 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

diagram.js CadCraft 0.8.2 64-bit 7 calculations Reviewed

Tests bin/test-cadcraft.mjs

Block size from its label

64-bit

The width and height of a diagram block.

w = longest line × th × 0.62 + 3 × th; h = (lines × 1.3 + 1.7) × th; diamond: w × 1.5, h × 1.6; minimum width 8 × th (diamond 10 × th)

The width counts characters and multiplies by 0.62 of the text height, an estimate of the average glyph width; it is not measured from the font. The constants 0.62, 1.3 and 1.7 are layout choices.

  • diagram.js
  • lines 395 to 406
  • CadCraft 0.8.2
/** Block sizes from the label: a line of text is ~0.62 of its height wide per character. */
export function nodeSize(n, th) {
  if (n.sym) {
    const sp = symbolPlace(n, th);
    if (sp) { const lab = String(n.label).split('\n'); return { w: Math.max(SYM_BOX * th, Math.max(...lab.map((l) => l.length)) * th * 0.62 + th), h: sp.h + 0.8 * th + (lab.length * 1.3 + 0.5) * th }; }
  }
  const lines = String(n.label).split('\n');
  const w = Math.max(...lines.map((l) => l.length)) * th * 0.62 + 3 * th;
  const h = (lines.length * 1.3 + 1.7) * th;
  const dia = n.shape === 'diamond';
  return { w: Math.max(dia ? 10 * th : 8 * th, dia ? w * 1.5 : w), h: dia ? h * 1.6 : h };
}

Symbol scale in a block

64-bit

The factor that fits a symbol drawing into a box of six text heights.

k = 6 × th ÷ max(w, 1.4 × h)

The symbol's bounding box width and height are floored at 1e-6 (absolute) before the division, so k cannot divide by zero.

  • diagram.js
  • lines 110 to 116
  • CadCraft 0.8.2
function symbolPlace(n, th) {
  const ents = symbolShape(n.sym);
  if (!ents || !ents.length) return null;
  const bb = boundsOfEnts(ents), w = Math.max(bb.x1 - bb.x0, 1e-6), h = Math.max(bb.y1 - bb.y0, 1e-6);
  const k = SYM_BOX * th / Math.max(w, h * 1.4);   // a wide symbol fills the box's width, a tall one 1.4 times the width in height
  return { ents, bb, k, w: w * k, h: h * k };
}

Symbol geometry scaled and moved

64-bit

Scales a symbol's lines, circles, arcs, polylines, hatches and text by k and moves them to the block.

x′ = x × k + dx; y′ = y × k + dy; r′ = r × k

One multiplication and one addition per coordinate. Arc angles and polyline bulges are copied unchanged, which is correct for a uniform scale.

  • diagram.js
  • lines 89 to 100
  • CadCraft 0.8.2
const xform = (ents, k, dx, dy, layer) => ents.map((e) => {
  const o = { ...e, layer, color: null };
  switch (e.type) {
    case 'line': return { ...o, x1: e.x1 * k + dx, y1: e.y1 * k + dy, x2: e.x2 * k + dx, y2: e.y2 * k + dy };
    case 'circle': return { ...o, cx: e.cx * k + dx, cy: e.cy * k + dy, r: e.r * k };
    case 'arc': return { ...o, cx: e.cx * k + dx, cy: e.cy * k + dy, r: e.r * k };
    case 'poly': return { ...o, pts: e.pts.map((q) => ({ x: q.x * k + dx, y: q.y * k + dy, b: q.b || 0 })) };
    case 'hatch': return { ...o, loops: e.loops.map((l) => l.map((v, i) => v * k + (i % 2 ? dy : dx))) };
    case 'text': return { ...o, x: e.x * k + dx, y: e.y * k + dy, h: (e.h || 1) * k };
    default: return o;
  }
});

Block coordinates

64-bit

The x and y of every block in a layered diagram.

x = previous edge + w ÷ 2, gap 2.5 × th; y = Σ (layer height + 4 × th); then x → mean of neighbors' x, kept in order with the gap

Running sums of block sizes and gaps, then 4 passes that move each block to the average x of its linked neighbors and clamp it so neighbors keep the minimum gap. The number of passes is fixed, not run until nothing moves.

  • diagram.js
  • lines 450 to 466
  • CadCraft 0.8.2
const GX = 2.5 * th, GY = 4 * th;
const x = new Array(V.length).fill(0), y = new Array(V.length).fill(0);
let cursorY = 0;
const layerH = L.map((ly) => Math.max(0, ...ly.map((k) => V[k].h)));
L.forEach((ly, li) => { let cx = 0; for (const k of ly) { x[k] = cx + V[k].w / 2; cx += V[k].w + GX; } y[li] = cursorY; cursorY += layerH[li] + GY; });
for (let it = 0; it < 4; it++) {
  for (const ly of (it % 2 ? [...L].reverse() : L)) {
    const want = ly.map((k) => { const ns = [...links[k].up, ...links[k].down]; return ns.length ? ns.reduce((s, n) => s + x[n], 0) / ns.length : x[k]; });
    for (let p = 0; p < ly.length; p++) { const k = ly[p], lo = p ? x[ly[p - 1]] + (V[ly[p - 1]].w + V[k].w) / 2 + GX : -Infinity; x[k] = Math.max(lo, want[p]); }
    for (let p = ly.length - 2; p >= 0; p--) { const k = ly[p], hi = x[ly[p + 1]] - (V[ly[p + 1]].w + V[k].w) / 2 - GX; if (x[k] > hi) x[k] = hi; }
    // the push right above may have crowded the left again: one more pass left to right keeps the minimum gaps
    for (let p = 1; p < ly.length; p++) { const k = ly[p], lo = x[ly[p - 1]] + (V[ly[p - 1]].w + V[k].w) / 2 + GX; if (x[k] < lo) x[k] = lo; }
  }
}
const yc = y.map((t, li) => t + (layerH[li] || 0) / 2);   // a layer's middle: where its blocks are centred
const minX = Math.min(...V.map((v, k) => x[k] - v.w / 2));
for (let k = 0; k < V.length; k++) x[k] -= minX;

Sequence diagram positions

64-bit

The x of each lifeline and the y of each message row in a sequence diagram.

gap ≥ 2.5 × th and ≥ (label length × th × 0.8 × 0.62 + 2 × th) ÷ lifelines crossed; row = 3.4 × th; top = box height + 2.6 × th

Lifeline spacing is widened by an estimate of each message label's width (0.62 × 0.8 × th per character) spread over the lifelines the message crosses. Rows are at top + i × row, a product and not a running sum.

  • diagram.js
  • lines 544 to 568
  • CadCraft 0.8.2
/** Sequence diagrams: blocks across the top, a dashed lifeline under each, one message per row. */
function layoutSequence(graph, th) {
  const P = graph.nodes, idx = new Map(P.map((n, i) => [n.id, i]));
  const size = P.map((n) => nodeSize(n, th));
  const boxH = Math.max(...size.map((s) => s.h));
  const gap = new Array(Math.max(0, P.length - 1)).fill(2.5 * th);   // the distance between neighbouring lifelines, widened by the labels that cross it
  const need = (a, b, text) => { const lo = Math.min(a, b), hi = Math.max(a, b); if (lo === hi) return; const w = (String(text).length * th * 0.8 * 0.62 + 2 * th) / (hi - lo); for (let k = lo; k < hi; k++) gap[k] = Math.max(gap[k], w); };
  graph.edges.forEach((e) => need(idx.get(e.from), idx.get(e.to), e.label));
  const cx = []; let acc = 0;
  P.forEach((n, i) => { if (i) acc = Math.max(acc + gap[i - 1], cx[i - 1] + size[i - 1].w / 2 + size[i].w / 2 + 1.5 * th) - 0; cx.push(i ? acc : size[0].w / 2); acc = cx[i]; });
  const row = 3.4 * th, top = boxH + 2.6 * th;
  const edges = graph.edges.map((e, i) => {
    const a = idx.get(e.from), b = idx.get(e.to), y = top + i * row;
    if (a === b) {
      const d = th * 1.4;
      return { i, pts: [{ x: cx[a], y: y - th * 0.4 }, { x: cx[a] + d, y: y - th * 0.4 }, { x: cx[a] + d, y: y + th * 0.9 }, { x: cx[a], y: y + th * 0.9 }], label: e.label ? { x: cx[a] + d, y: y + th * 0.25, vertical: true } : null };
    }
    return { i, pts: [{ x: cx[a], y }, { x: cx[b], y }], label: e.label ? { x: (cx[a] + cx[b]) / 2, y, vertical: false } : null };
  });
  const bottom = top + graph.edges.length * row;
  const nodes = P.map((n, i) => ({ id: n.id, x: cx[i], y: boxH / 2, w: size[i].w, h: boxH }));
  const lifelines = P.map((_, i) => ({ x: cx[i], y0: boxH, y1: bottom }));
  const xs = [...nodes.flatMap((n) => [n.x - n.w / 2, n.x + n.w / 2]), ...edges.flatMap((e) => e.pts.map((p) => p.x))];
  return { nodes, edges, lifelines, bounds: { minX: Math.min(...xs), maxX: Math.max(...xs), minY: 0, maxY: bottom } };
}

Layout to drawing coordinates

64-bit

Places the laid-out diagram at a point in the drawing, turning y from downward to upward.

X = x + px − minX; Y = y − (py − minY)

Two additions per coordinate. When the insertion point is far from the origin the diagram's small offsets are added to a large coordinate, which is ordinary 64-bit rounding at that magnitude.

  • diagram.js
  • line 580
  • CadCraft 0.8.2
const X = (px) => x + px - b.minX, Y = (py) => y - (py - b.minY);   // the layout's y grows down; the drawing's grows up

Arrowhead triangle

64-bit

The three corners of a connector's filled arrowhead.

u = (to − from) ÷ |to − from|; corners: to, to − u × s ± n × 0.35 × s

The direction is normalized by Math.hypot; a zero-length last segment divides by 1 instead, which gives a degenerate triangle at the point and no error.

  • diagram.js
  • lines 572 to 575
  • CadCraft 0.8.2
const arrowHead = (from, to, s) => {
  const dx = to.x - from.x, dy = to.y - from.y, d = Math.hypot(dx, dy) || 1, ux = dx / d, uy = dy / d;
  return [to.x, to.y, to.x - ux * s - uy * s * 0.35, to.y - uy * s + ux * s * 0.35, to.x - ux * s + uy * s * 0.35, to.y - uy * s - ux * s * 0.35, to.x, to.y];
};

In detail

Method
Layered layout: longest-path layers, barycenter ordering in 6 sweeps, packing with fixed gaps (2.5 and 4 text heights) and 4 passes pulling each block toward the mean of its neighbors; orthogonal connectors.
Number types
64-bit numbers throughout.
Tolerances
A symbol's width and height are floored at 1e-6 before the scale factor is divided by them.
Limits
Text width is an estimate of 0.62 × height per character. The layout runs a fixed number of passes and is not iterated to convergence.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach