D3 is a low-level data visualisation library by Observable.

D3 stands for “Data-Driven Documents”.

Getting started

Documentation

Create svg element and add that to the appropriate div-element

const width = 640;
const height = 400;
    
const svg = d3.create("svg")
	.attr("width", width)
	.attr("height", height);
 
element.append(svg.node()!!);

The double-bang operator is important when using ⌨️ TypeScript, otherwise, it will result in

Argument of type 'SVGSVGElement | null' is not assignable to parameter of type 'string | Node'. Type 'null' is not assignable to type 'string | Node'.

Interactivity

Graphs

Force Simulation

When implementing the example of a disjointed force-directed graph in ⌨️ TypeScript, d3.forceSimulation needs nodes of type SimulationNodeDatum. This requires implementing a specific type:

interface CustomNode extends d3.SimulationNodeDatum {
    id: string;
}
 
export function setupPlot(element: HTMLDivElement) {
	(...)
	 
    const nodes: CustomNode[] = [{
        id: "test-1"
    }, {
        id: "test-2"
    }]
 
    const simulation = d3.forceSimulation(nodes)
	(...)

for links any object with type { source: string; target: string; } is sufficient.

By default, nodes will be centred around (0, 0), corresponding to the view box’s top left corner. To show the nodes, the view box must be set to have (0, 0) at the centre using .attr("viewBox", [-width/2, -height/2, width, height]);

const svg = d3.create("svg")
	.attr("width", width)
	.attr("height", height)
	.attr("viewBox", [-width/2, -height/2, width, height]);

The rendered links and nodes must be updated on each simulation tick.

The final result

import * as d3 from "d3"
 
interface CustomNode extends d3.SimulationNodeDatum {
    id: string;
}
 
export function setupPlot(element: HTMLDivElement) {
    const width = 640;
    const height = 400;
 
    const nodes: CustomNode[] = [{
        id: "test-1"
    }, {
        id: "test-2"
    }]
    const links: d3.SimulationLinkDatum<CustomNode>[] = [{
        source: "test-1",
        target: "test-2"
    }]
 
    const simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).id(d=> (d as CustomNode).id))
        .force("charge", d3.forceManyBody())
        .force("x", d3.forceX())
        .force("y", d3.forceY());
 
    const svg = d3.create("svg")
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [-width/2, -height/2, width, height]);
 
    // Add a line for each link, and a circle for each node.
    const link = svg.append("g")
        .attr("stroke", "#999")
        .attr("stroke-opacity", 0.6)
        .selectAll("line")
        .data(links)
        .join("line")
        .attr("stroke-width", 2);
 
    const node = svg.append("g")
        .attr("stroke", "#fff")
        .attr("stroke-width", 1.5)
        .selectAll("circle")
        .data(nodes)
        .join("circle")
        .attr("r", 5)
 
    simulation.on("tick", () => {
        link
            .attr("x1", d => (d.source as CustomNode).x!!)
            .attr("y1", d => (d.source as CustomNode).y!!)
            .attr("x2", d => (d.target as CustomNode).x!!)
            .attr("y2", d => (d.target as CustomNode).y!!);
 
        node
            .attr("cx", d => d.x!!)
            .attr("cy", d => d.y!!);
    })
 
    element.append(svg.node()!!);
}