Skip to content

Instantly share code, notes, and snippets.

@robert-moore
Created May 24, 2026 21:35
Show Gist options
  • Select an option

  • Save robert-moore/8487fd71407c45c9700f0268ecd77364 to your computer and use it in GitHub Desktop.

Select an option

Save robert-moore/8487fd71407c45c9700f0268ecd77364 to your computer and use it in GitHub Desktop.
Label layout helper using D3 force simulation — featured in The Nine Thirty-Six, Issue 06
import * as d3 from 'd3';
export interface IDecoratedForceNode extends d3.SimulationNodeDatum {
width: number;
targetX: number;
}
function boundedTick(simulation: d3.Simulation<any, any>, boundMin = 0, boundMax = 1) {
simulation.tick();
simulation.nodes().forEach((d: any) => {
const x = d.x;
if (x < boundMin + d.width / 2) {
d.x = d.width;
} else if (x > boundMax - d.width / 2) {
d.x = boundMax;
}
d.y = 0;
});
}
export function layoutUniformWidth(anchors: number[], itemWidth: number, boundMin = 0, boundMax = 1) {
const nodes = anchors.map((d) => ({
width: itemWidth,
targetX: d,
x: d + (Math.random() - 0.5) * 0.01 * itemWidth,
y: 0,
vx: 0,
vy: 0,
}));
const simulation = d3
.forceSimulation(nodes)
.force('x', d3.forceX((d: IDecoratedForceNode) => d.targetX).strength(0.01))
// .force('charge', d3.forceManyBody().strength(-itemWidth))
.force('y', d3.forceY(0).strength(1))
.force(
'collide',
d3
.forceCollide()
.radius((d: any) => d.width / 2)
.strength(1)
.iterations(5)
)
.stop();
for (let i = 0; i < 100; ++i) {
boundedTick(simulation, boundMin, boundMax);
}
return nodes.map((d) => d.x);
}
export function layoutVariableWidth(items: { x: number; width: number }[], boundMin = 0, boundMax = 1) {
const nodes = items.map((d) => {
let x = d.x;
if (x < boundMin + d.width / 2) {
x = d.width / 2;
} else if (x > boundMax - d.width / 2) {
x = boundMax - d.width / 2;
}
return {
width: d.width,
targetX: x,
x,
y: 0,
vx: 0,
vy: 0,
};
});
const simulation = d3
.forceSimulation(nodes)
.force('x', d3.forceX((d: IDecoratedForceNode) => d.targetX).strength(0.1))
.force('y', d3.forceY(0).strength(1))
.force(
'collide',
d3
.forceCollide()
.radius((d: any) => d.width / 2)
.strength(1)
.iterations(3)
)
.stop();
for (let i = 0; i < 100; ++i) {
boundedTick(simulation, boundMin, boundMax);
}
return nodes.map((d) => d.x);
}
// todo - left align, right align
// todo - automatic shrink
// including giving each node several different widths,
// and shrinking automatically if squeeze pressure exceeds threshold
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment