Public
Edited
Feb 12
Paused
Importers
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = ForceGraph(
{
nodes: miserables.nodes,
links: miserables.links.slice(0, maxNumLinks)
},
{
disjoint: true,
nodeId: (d) => d.id,
nodeGroup: (d) => d.group,
nodeStroke: (d) => (d.id == "Valjean" ? "black" : "white"),
nodeLabel: (d) => `${d.id}`,
nodeLabelFill: "#ccc",
nodeRadius: 5,
linkStrokeWidth: (l) => Math.sqrt(l.value),
linkStrokeOpacity: (l) => lo(l.value),
width,
height: 600,
renderer,
autoFit,
keepAspectRatio: aspectRatio,
// drawLinksWhenAlphaIs: 0.9, // useful for larger graphs
// extent: [
// [0, 0],
// [width, 600]
// ],
useEdgeBundling: useEdgeBundling, // will be slow on large graphs
edgeBundling: {
min_alpha_to_bundle: null // bundle inmediately
},
extentForce: "forceTransport",
useForceInABox,
forceInABox: {
template: "treemap",
strength: 0.1
},
collide: false,
useZoom: true,
useSmartLabels: true,
smartLabels: {
labelsInCentroids: false,
font: (d) => (d.id == "Valjean" ? "14pt sans-serif" : "10pt sans-serif"),
hoverFont: (d) =>
d.id == "Valjean" ? "bolder 14pt sans-serif" : "bolder 10pt sans-serif",
},
colors: d3.schemeAccent,
invalidation, // a promise to stop the simulation when the cell is re-run,
_this: this
}
)
Insert cell
lo = d3.scaleLinear().domain([0, d3.max(miserables.links, l => l.value)]).range([0.1, 0.7])
Insert cell
// howto("ForceGraph")
Insert cell
// chart2 = ForceGraph(
// {
// nodes: miserables.nodes,
// links: miserables.links.slice(0, maxNumLinks)
// },
// {
// nodeId: (d) => d.id,
// nodeGroup: (d) => d.group,
// nodeLabel: (d) => `${d.id}\n${d.group}`,
// linkStrokeWidth: (l) => Math.sqrt(l.value),
// width,
// height: 600,
// invalidation, // a promise to stop the simulation when the cell is re-run,
// renderer: "svg",
// // drawLinksWhenAlphaIs: 0.4, // useful for larger graphs
// extent: [
// [0, 0],
// [width, 600]
// ],
// nodeRadius: 30,
// // extentForce: "forceBoundary",
// forces: {
// collide: () => d3.forceCollide(30),
// center: () => d3.forceCenter(width / 2, 300)
// // link: ({ nodes, links }) => d3.forceLink(links)
// },
// nodeLabelDy: 0, // label dy distance to node center
// _this: this,
// }
// )
Insert cell
miserables = FileAttachment("miserables.json").json()
Insert cell
defaultOpts = ({
nodeId: (d) => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup: undefined, // given d in nodes, returns an (ordinal) value for color
nodeGroups: undefined, // an array of ordinal values representing the node groups
nodeFill: "currentColor", // node stroke fill (if not using a group color encoding)
nodeStroke: () => "#fff", // node stroke color
nodeStrokeWidth: 1.5, // node stroke width, in pixels
nodeStrokeOpacity: 1, // node stroke opacity
nodeRadius: 3, // node radius, in pixels or a function
nodeLabel: undefined, // given d in nodes, a title string,
nodeLabelFill: "#333", // node legend color
nodeLabelStroke: null, // node legend color
nodeLabelTextAlign: "center", // node legend align used used in canvas
nodeLabelTextAnchor: "middle", // used in svg
nodeLabelFont: "8pt sans-serif",
nodeLabelDy: -8, // label dy distance to node center
nodeLabelDx: 0, // label dy distance to node center
nodeStrength: undefined, // nodeCharge strength
linkSource: undefined, // given d in links, returns a node identifier string. If undefined defaults to ({ source }) => nodeId(source) || source
linkTarget: undefined, // given d in links, returns a node identifier string
linkStroke: "#999", // link stroke color
linkStrokeOpacity: 0.6, // link stroke opacity
linkStrokeWidth: 1.5, // given d in links, returns a stroke width in pixels
linkStrokeLinecap: "round", // link stroke linecap
linkStrength: undefined,
linkDistance: 10,
linkCurve: d3.curveBasis,
colors: d3.schemeTableau10, // an array of color strings, for the node groups
color: null, // you can provide your own custom d3.scaleOrdinal color scale here
width: width, // outer width, in pixels
height: 400, // outer height, in pixels
invalidation, // when this promise resolves, stop the simulation,
renderer: "canvas", // either "svg" or "canvas",
drawLinksWhenAlphaIs: null, // if set to a number [0,1] will only draw links if alpha is below this number
_this: undefined,
extent: null, // set to [[0, 0], [width, height]] for a border to avoid nodes to leave the canvas
extentForce: "forceBoundary", // one of ["forceBoundary", "forceTransport", "forceExtent"]
extentBorder: 20, // Size of the border
extentStrength: 0.1, // Strength of the border
simulationRestartAlpha: 1, // What alpha to set when redrawing
forceXStrength: undefined, // if not provided with try to fit in the width height
forceYStrength: undefined, // if not provided with try to fit in the width height
centeringStrength: 0.05,
collide: true,
collidePadding: 1,
collideIterations: 4,
forces: null, // You can pass your own forces using an object {"forceName", d3.forceManyBody()},
disjoint: true, // Set to false if your network is heavily connected,
useEdgeBundling: false, // true to use edge bundling
edgeBundling: {
bundling_stiffness: 0.1, // global bundling constant controlling edge stiffness
step_size: 0.1, // init. distance to move points
subdivision_rate: 2, // subdivision rate increase
cycles: 2, // number of cycles to perform
iterations: 90, // init. number of iterations for cycle
iterations_rate: 0.6666667, // rate at which iteration number decreases i.e. 2/3
compatibility_threshold: 0.2, // "which pairs of edges should be considered compatible (default is set to 0.6, 60% compatiblity)"
max_links: 1000, // If your network is large, edgebundling will only apply to these many links
min_alpha_to_bundle: 0.6
},
useForceInABox: false, // will group by nodeGroup
forceInABox: {
template: "force", // Either treemap or force
strength: 0.3, // Strength to foci
linkStrengthInterCluster: 0.001, // linkStrength between nodes of different clusters
linkStrengthIntraCluster: 0.1, // linkStrength between nodes of the same cluster
forceLinkDistance: 100, // linkDistance between meta-nodes on the template (Force template only)
forceLinkStrength: 0.1, // linkStrength between meta-nodes of the template (Force template only)
forceCharge: -1, // Charge between the meta-nodes (Force template only)
forceNodeSize: 10 // Used to compute the template force nodes size (Force template only)
},
useZoom: true,
zoomScaleExtent: [0.1, 20],
minDistanceForDrag: 10,
autoFit: true, // keep nodes in view using scales
keepAspectRatio: false,
useSmartLabels: true,
smartLabels: {
stroke: "white", // label stroke color
threshold: 2000, // Areas over this size would get labels
font: (d) => "8pt sans-serif",
hover: true, // Show label of the hovered point
onHover: (i) => i, // callback when hovered, will pass the index of the selected element
hoverFont: (d) => "bolder 8pt sans-serif",
labelsInCentroids: false,
backgroundFill: "#fefefe01", // What to paint the bg rect of the labels. Needed for the onHover
strokeWidth: 5,
showVoronoi: false,
voronoiStroke: "#ccc",
showAnchors: false,
anchorsStroke: "orange",
anchorsFill: "none",
useOcclusion: true,
occludedStyle: "opacity: 0.2" // css style rules to be used on occluded labels
},
alphaTarget: 0,
debug: false
})
Insert cell
ForceGraph({nodes: [], links: []})
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/force-directed-graph
function ForceGraph(
{
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{source, target}, …])
},
_opts = {}
) {
let opts = {
...defaultOpts,
..._opts,
edgeBundling: {
...defaultOpts.edgeBundling,
..._opts.edgeBundling
},
forceInABox: {
...defaultOpts.forceInABox,
..._opts.forceInABox
},
smartLabels: {
...defaultOpts.smartLabels,
..._opts.smartLabels
}
};

if (opts.nodeRadius && typeof opts.nodeRadius !== "function") {
const numberNodeRadius = opts.nodeRadius;
opts.nodeRadius = () => numberNodeRadius;
}
if (opts.nodeStroke && typeof opts.nodeStroke !== "function") {
const nodeStroke = opts.nodeStroke;
opts.nodeStroke = () => nodeStroke;
}

opts.linkSource =
opts.linkSource || (({ source }) => opts.nodeId(source) || source);
opts.linkTarget =
opts.linkTarget || (({ target }) => opts.nodeId(target) || target);

// Compute values.
const N = d3.map(nodes, opts.nodeId).map(intern);
const LS = d3.map(links, opts.linkSource).map(intern);
const LT = d3.map(links, opts.linkTarget).map(intern);
if (opts.nodeLabel === undefined) opts.nodeLabel = (_, i) => N[i];
const T = opts.nodeLabel == null ? null : d3.map(nodes, opts.nodeLabel);
const R = opts.nodeRadius == null ? null : d3.map(nodes, opts.nodeRadius);
const G =
opts.nodeGroup == null ? null : d3.map(nodes, opts.nodeGroup).map(intern);
const NS =
opts.nodeStroke == null ? null : d3.map(nodes, opts.nodeStroke).map(intern);
const W =
typeof opts.linkStrokeWidth !== "function"
? null
: d3.map(links, opts.linkStrokeWidth);
const L =
typeof opts.linkStroke !== "function"
? null
: d3.map(links, opts.linkStroke);
const LO =
typeof opts.linkStrokeOpacity !== "function"
? null
: d3.map(links, opts.linkStrokeOpacity);

opts.x = d3.scaleLinear().domain([0, opts.width]).range([0, opts.width]);
opts.y = d3.scaleLinear().domain([0, opts.height]).range([0, opts.height]);

// Copy nodes
if (opts._this?.nodes) {
// Make a shallow copy to protect against mutation, while
// recycling old nodes to preserve position and velocity.
const oldNodes = new Map(opts._this?.nodes.map((d) => [d.id, d]));
nodes = nodes.map((n, i) =>
Object.assign(
oldNodes.get(opts.nodeId(n)) || { id: N[i], groupBy: G && G[i] }
)
);
} else {
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (n, i) => ({
id: N[i],
groupBy: G && G[i] // we need the groupBy for forceInABox
}));
}
links = d3.map(links, (_, i) => ({ source: LS[i], target: LT[i] }));

// Initialize towards the middle
for (let n of nodes) {
const randomFactor = 3; // how far from the center should nodes be initialized
n.x =
n.x !== undefined
? n.x
: (Math.random() - 0.5) * (opts.width / randomFactor) + opts.width / 2;
n.y =
n.y !== undefined
? n.y
: (Math.random() - 0.5) * (opts.height / randomFactor) +
opts.height / 2;
}

// Compute default domains.
if (G && opts.nodeGroups === undefined) opts.nodeGroups = d3.sort(G);

if (opts.debug) console.log("color", opts.color);
// Construct the scales.
const color = opts.color
? opts.color
: opts.nodeGroup == null
? null
: d3.scaleOrdinal(opts.nodeGroups, opts.colors);

let simulation =
opts._this?.simulation?.alpha(opts.simulationRestartAlpha)?.restart() ||
d3.forceSimulation();

simulation.nodes(nodes);

if (opts.forces) {
for (let k in opts.forces) {
simulation.force(k, opts.forces[k]({ nodes, links }));
}
} else {
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({ index: i }) => N[i]);
if (opts.nodeStrength !== undefined) forceNode.strength(opts.nodeStrength);
if (opts.linkStrength !== undefined) forceLink.strength(opts.linkStrength);

if (opts.forceXStrength === undefined) {
opts.forceXStrength = (opts.height / opts.width) * opts.centeringStrength;
}
if (opts.forceYStrength === undefined) {
opts.forceYStrength = (opts.width / opts.height) * opts.centeringStrength;
}

const forceCollide = opts.collide
? d3
.forceCollide((_, i) => R[i] + opts.collidePadding)
.iterations(opts.collideIterations)
: null;

const forceX = d3.forceX(opts.width / 2).strength(opts.forceXStrength);
const forceY = d3.forceY(opts.height / 2).strength(opts.forceYStrength);

let groupingForce;
if (opts.useForceInABox) {
groupingForce = forceInABox()
.size([opts.width, opts.height - 100]) // Size of the chart
.template(opts.forceInABox.template) // Either treemap or force
.groupBy("groupBy") // We use a fixed groupBy attribute
.strength(opts.forceInABox.strength) // Strength to foci
.links(links) // The graph links. Must be called after setting the grouping attribute (Force template only)
// .enableGrouping(useGroupInABox)
.linkStrengthInterCluster(opts.forceInABox.linkStrengthInterCluster) // linkStrength between nodes of different clusters
.linkStrengthIntraCluster(opts.forceInABox.linkStrengthIntraCluster) // linkStrength between nodes of the same cluster
.forceLinkDistance(opts.forceInABox.forceLinkDistance) // linkDistance between meta-nodes on the template (Force template only)
.forceLinkStrength(opts.forceInABox.forceLinkStrength) // linkStrength between meta-nodes of the template (Force template only)
.forceCharge(opts.forceInABox.forceCharge) // Charge between the meta-nodes (Force template only)
.forceNodeSize(opts.forceInABox.forceNodeSize); // Used to compute the template force nodes size (Force template only)
}

simulation
.force(
"link",
forceLink
.distance(opts.linkDistance)
.strength(
opts.useForceInABox
? groupingForce.getLinkStrength
: opts.linkStrength || 0.1
)
)
.force("charge", forceNode)
.force("x", opts.useForceInABox ? null : opts.disjoint ? forceX : null)
.force("y", opts.useForceInABox ? null : opts.disjoint ? forceY : null)
.force(
"center",
opts.useForceInABox
? null
: !opts.disjoint
? d3.forceCenter(opts.width / 2, opts.height / 2)
: null
)
.force("collide", forceCollide)
.force("forceInABox", opts.useForceInABox ? groupingForce : null);
}

// ***** Edge bundling *****
opts.bundling =
opts.useEdgeBundling &&
edgeBundling(
{ nodes, links: sample(links, opts.edgeBundling.max_links) },
{
...opts.edgeBundling
}
);
// ***** Edge bundling *****

simulation.alphaTarget(opts.alphaTarget).restart();

if (opts.extent) {
simulation.force(
"boundary",
opts.extentForce === "forceBoundary"
? forceBoundary(...opts.extent.flat(2))
.border(opts.extentBorder)
.hardBoundary(true)
.strength(opts.extentStrength)
: opts.extentForce === "forceExtent"
? forceExtent(opts.extent)
: forceTransport(opts.extent, 5, opts.extentStrength * 10)
);
}

opts = {
...opts,
nodes,
links,
N,
LS,
LT,
T,
G,
W,
L,
LO,
R,
NS,
drag,
simulation,
color
};

const { target, ticked } =
opts.renderer === "canvas" ? renderCanvas(opts) : renderSVG(opts);

simulation.on("tick", ticked);

(opts.invalidation || invalidation).then(() => {
console.log("🚫 Invalidation -> Stopping simulation");
simulation.stop();
});

function intern(value) {
return value !== null && typeof value === "object"
? value.valueOf()
: value;
}

ticked();

return Object.assign(target, {
scales: { color },
simulation,
nodes,
links
});
}
Insert cell
function renderCanvas(opts) {
// Code from @fil https://observablehq.com/@d3/force-directed-graph-canvas
let context;
try {
if (opts._this?.tagName !== "CANVAS") {
console.log("not a canvas", opts._this);
throw new Error("recreating canvas");
}
context = opts._this?.getContext("2d");
} catch (e) {
context = DOM.context2d(width, opts.height);
}

const line = d3
.line()
.curve(opts.linkCurve)
.x((d) => d.x)
.y((d) => d.y) // links generator
.context(context);

const zoom = d3
.zoom()
.extent([
[0, 0],
[opts.width, opts.height]
])
.scaleExtent(opts.zoomScaleExtent)
.on("zoom", ({ transform }) => ticked(transform));

function drawNodesAndLinks(transform = d3.zoomTransform(context.canvas)) {
if (
opts.drawLinksWhenAlphaIs === null ||
opts.simulation.alpha() < opts.drawLinksWhenAlphaIs
) {
context.save();
// constant opacity
if (!opts.LO) {
context.globalAlpha = opts.linkStrokeOpacity;
}

for (const [i, link] of opts.links.entries()) {
context.beginPath();
drawLink(link, transform);

// Dynamic opacity
if (opts.LO) context.globalAlpha = opts.LO[i];
context.strokeStyle = opts.L ? opts.L[i] : opts.linkStroke;
context.lineWidth = opts.W ? opts.W[i] : opts.linkStrokeWidth;
context.stroke();
}
context.restore();
}

context.save();
context.globalAlpha = opts.nodeStrokeOpacity;
for (const [i, node] of opts.nodes.entries()) {
context.beginPath();
drawNode(node, i, transform);
context.fillStyle = opts.G ? opts.color(opts.G[i]) : opts.nodeFill;
context.strokeStyle = opts.NS ? opts.NS[i] : opts.nodeStroke;
context.fill();
context.stroke();
}
context.restore();
} // drawNodesAndLinks

function ticked(transform = d3.zoomTransform(context.canvas)) {
context.save();

computeAutoFit(opts);
// console.log("Ticked canvas");
if (opts.useEdgeBundling && opts.bundling) {
for (let l of opts.links) {
delete l.path;
}
if (
(!opts.edgeBundling.min_alpha_to_bundle &&
opts.edgeBundling.min_alpha_to_bundle !== 0) ||
opts.simulation.alpha() < opts.edgeBundling.min_alpha_to_bundle
) {
opts.bundling.update();
}
}

context.clearRect(0, 0, width, opts.height);
drawNodesAndLinks(transform);

// Draw Labels
context.save();
context.fillStyle = opts.nodeLabelFill;
context.textAlign = opts.nodeLabelTextAlign;
context.textAnchor = opts.nodeLabelTextAnchor;
if (opts.T) {
context.font = opts.nodeLabelFont;
context.beginPath();

if (opts.useSmartLabels) {
smartLabels(opts.nodes, {
...opts.smartLabels,
target: context.canvas,
label: (_, i) => opts.T[i],
x: (d) => transform.applyX(opts.x(d.x)),
y: (d) => transform.applyY(opts.y(d.y)),
width: opts.width,
height: opts.height,
renderer: opts.renderer,
onHover: () => drawNodesAndLinks(transform),
});
} else {
for (const [i, node] of opts.nodes.entries()) {
drawLabel(node, i, transform);
}
}
context.stroke();
}
context.restore();
}

function drawLink(l, transform) {
line(
opts.useEdgeBundling && l.path
? l.path.map((d) => applyTransform(d, transform, opts))
: [
applyTransform(l.source, transform, opts),
applyTransform(l.target, transform, opts)
]
);
}

function drawNode(d, i, transform) {
d = applyTransform(d, transform, opts);
context.moveTo(d.x + opts.R[i], d.y);
context.arc(d.x, d.y, opts.R[i], 0, 2 * Math.PI);
}

function drawLabel(d, i, transform) {
d = applyTransform(d, transform, opts);
context.fillText(opts.T[i], d.x, d.y - opts.R[i] - 2);
}

return {
target: d3
.select(context.canvas)
.call(
opts
.drag(opts.simulation, context.canvas, opts)
.on("start.render drag.render end.render", () => {
return ticked(d3.zoomTransform(context.canvas));
})
)
.call(
opts.useZoom ? zoom : () => {} // do not use zoom
)
.node(),
ticked
};
} // renderCanvas
Insert cell
function renderSVG(opts) {
let svg;

try {
if (opts._this?.tagName !== "svg") {
console.log("not an svg", opts._this);
throw new Error("recreating svg");
}
svg = d3.select(opts._this);
} catch {
svg = d3.create("svg");
}

const line = d3
.line()
.curve(opts.linkCurve)
.x((d) => d.x)
.y((d) => d.y); // links generator

svg
.attr("width", opts.width)
.attr("height", opts.height)
.attr("viewBox", [0, 0, width, opts.height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

const linkG = svg
.selectAll("g#gLinks")
.data([0])
.join("g")
.attr("id", "gLinks");

const link = linkG
.attr(
"stroke",
typeof opts.linkStroke !== "function" ? opts.linkStroke : null
)
.attr("fill", "none")
.attr("stroke-opacity", (_, i) =>
opts.LO ? opts.LO[i] : opts.linkStrokeOpacity
)
.attr(
"stroke-width",
typeof opts.linkStrokeWidth !== "function" ? opts.linkStrokeWidth : null
)
.attr("stroke-linecap", opts.linkStrokeLinecap)
.selectAll("path")
.data(opts.links)
.join("path");

const nodeG = svg
.selectAll("g#gNodes")
.data([0])
.join("g")
.attr("id", "gNodes");

const node = nodeG
.attr("fill", opts.nodeFill)
.attr("stroke-opacity", opts.nodeStrokeOpacity)
.attr("stroke-width", opts.nodeStrokeWidth)
.selectAll("circle.node")
.data(opts.nodes)
.join("circle")
.attr("class", "node")
.attr("stroke", (_, i) => (opts.NS ? opts.NS[i] : opts.nodeStroke))
.attr("r", (_, i) => opts.R[i]);

let label = null;
let sl = null;

if (!opts.useSmartLabels) {
label = nodeG
.selectAll("text.label")
.data(opts.nodes)
.join("text")
.attr("fill", opts.nodeLabelFill)
.attr("stroke", opts.nodeLabelStroke || "none")
.style("text-anchor", opts.nodeLabelAlign)
.style("text-anchor", opts.nodeLabelTextAnchor)
.style("font", opts.nodeLabelFont)
.attr("class", "label")
.text((_, i) => opts.T[i]);
}

node.call(opts.drag(opts.simulation, svg.node(), opts));

const zoom = d3
.zoom()
.extent([
[0, 0],
[opts.width, opts.height]
])
.scaleExtent(opts.zoomScaleExtent)
.on("zoom", ({ transform }) => ticked(transform));

svg.call(
opts.useZoom ? zoom : () => {} // do not use zoom
);

if (opts.W) link.attr("stroke-width", ({ index: i }) => opts.W[i]);
if (opts.L) link.attr("stroke", ({ index: i }) => opts.L[i]);
if (opts.G) node.attr("fill", ({ index: i }) => opts.color(opts.G[i]));
if (opts.T) node.append("title").text(({ index: i }) => opts.T[i]);

function ticked(transform = d3.zoomTransform(svg.node())) {
computeAutoFit(opts);
if (opts.useEdgeBundling && opts.bundling) {
for (let l of opts.links) {
delete l.path;
}
if (
(!opts.edgeBundling.min_alpha_to_bundle &&
opts.edgeBundling.min_alpha_to_bundle !== 0) ||
opts.simulation.alpha() < opts.edgeBundling.min_alpha_to_bundle
) {
opts.bundling.update();
}
}

link
.attr(
"display",
opts.drawLinksWhenAlphaIs === null ||
opts.simulation.alpha() <= opts.drawLinksWhenAlphaIs
? "block"
: "none"
)
.attr("d", (l) => {
return line(
opts.useEdgeBundling && l.path
? l.path.map((d) => applyTransform(d, transform, opts))
: [
applyTransform(l.source, transform, opts),
applyTransform(l.target, transform, opts)
]
);
});

node
.attr("cx", (d) => applyTransform(d, transform, opts).x)
.attr("cy", (d) => applyTransform(d, transform, opts).y);

if (opts.useSmartLabels) {
smartLabels(opts.nodes, {
...opts.smartLabels,
target: nodeG,
label: (_, i) => opts.T[i],
x: (d) => transform.applyX(opts.x(d.x)),
y: (d) => transform.applyY(opts.y(d.y)),
width: opts.width,
height: opts.height
});
} else {
label
.attr(
"x",
(d) => applyTransform(d, transform, opts).x + opts.nodeLabelDx
)
.attr(
"y",
(d) => applyTransform(d, transform, opts).y + opts.nodeLabelDy
);
}
}

return { target: svg.node(), ticked };
} // renderSVG
Insert cell
function applyTransform(d, transform, opts) {
const [x, y] = transform.apply([opts.x(d.x), opts.y(d.y)]);
return { ...d, x, y };
}
Insert cell
function drag(simulation, node, opts) {
function dragsubject(event) {
const transform = d3.zoomTransform(node);
let [x, y] = transform.invert([event.x, event.y]);
x = opts.x.invert(x);
y = opts.y.invert(y);
let subject = simulation.find(x, y);

let d = Math.hypot(x - subject.x, y - subject.y);

return d < opts.minDistanceForDrag
? {
circle: subject,
x: transform.applyX(opts.x(subject.x)),
y: transform.applyY(opts.y(subject.y))
}
: null;
}

function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.circle.fx = event.subject.circle.x;
event.subject.circle.fy = event.subject.circle.y;
}

function dragged(event) {
const transform = d3.zoomTransform(node);
event.subject.circle.fx = opts.x.invert(transform.invertX(event.x));
event.subject.circle.fy = opts.y.invert(transform.invertY(event.y));
}

function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.circle.fx = null;
event.subject.circle.fy = null;
}

return d3
.drag()
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
// function autoFit(node, zoom) {
// console.log("autofit", d3.zoomTransform(node).translate(100, 0).scale(8));
// // zoom.translateBy(node, 100, 100);
// d3.select(node).call(zoom.transform, d3.zoomIdentity.translate(100, 0).scale(8));
// }
Insert cell
function intern(value) {
return value !== null && typeof value === "object"
? value.valueOf()
: value;
}
Insert cell
function sample(array, n) {
if (n >= array.length) return array;

return array.filter((d, i) => (i % Math.floor(array.length/n)) === 0);
}
Insert cell
function filterNetwork(
network,
{
nodeId = (d) => d.id,
byNodes = null,
byLinks = null,
edgelessNodes = false, // Should we allow nodes without edges?
nodesMap,
egoDistance = 1,
filterSource = true,
filterTarget = true
} = {}
) {
nodesMap = nodesMap
? nodesMap
: new Map(network.nodes.map((d, i, all) => [nodeId(d, i, all), d]));
let res = {
nodes: [...network.nodes],
links: [...network.links]
};
//
function filterNodesFromLinks(res) {
const ids = [...new Set(res.links.map((l) => [l.source, l.target]).flat())];
res.nodes = ids.map((id) => nodesMap.get(id)).filter((d) => d); // undefined when the node is not visible anymore;

return res;
}

function filterLinksFromNodes(
{ nodes, links },
{ filterSource = true, filterTarget = true } = {}
) {
const ids = new Set(nodes.map(nodeId));

let filteredLinks = links;

links = links
.filter(
({ source, target }) =>
(!filterSource || ids.has(source)) &&
(!filterTarget || ids.has(target))
)
.map((l) => ({ ...l }));

return { nodes, links };
}

// Filter nodes
if (byNodes) {
res.nodes = res.nodes.filter(byNodes).map((n) => ({ ...n }));
}

res = filterLinksFromNodes(res, { filterSource, filterTarget });

// Then links
if (byLinks) {
res.links = res.links.filter(byLinks).map((l) => ({
...l
}));
// console.log("Filter Links", res);
// Remove edgeless nodes
if (!edgelessNodes) {
res = filterNodesFromLinks(res, { filterSource, filterTarget });
}
}

for (
let currentDistance = 1;
currentDistance < egoDistance;
currentDistance += 1
) {
res = filterLinksFromNodes(
{ nodes: res.nodes, links: network.links },
{ filterSource, filterTarget: false }
);

// // 0.5
// res = filterNodesFromLinks(res, { filterSource, filterTarget: false });
res = filterNodesFromLinks(res, { filterSource, filterTarget });
}

return res;
}
Insert cell
function computeAutoFit(opts) {
if (opts.autoFit) {
const yExtent = d3.extent(opts.nodes, (d) => d.y);
const xExtent = d3.extent(opts.nodes, (d) => d.x);

opts.x.domain(d3.extent(opts.nodes, (d) => d.x));
// .nice();
opts.y.domain(d3.extent(opts.nodes, (d) => d.y));
// .nice();

if (opts.keepAspectRatio) {
const ratio = opts.width / opts.height;
const newRatio = (xExtent[1] - xExtent[0]) / (yExtent[1] - yExtent[0]);

// console.log(ratio, newRatio);

if (newRatio < ratio) {
// Adjust x axis to fit
const d =
(opts.width / opts.height) * (yExtent[1] - yExtent[0]) -
(xExtent[1] - xExtent[0]);

// console.log("👉🏼 Adjust x", d, xExtent);
opts.x.domain([xExtent[0] - d / 2, xExtent[1] + d / 2]);
// .nice();
opts.y.domain(yExtent);
// .nice();
} else {
// Adjust y axis to fit
const d =
(opts.height / opts.width) * (xExtent[1] - xExtent[0]) -
(yExtent[1] - yExtent[0]);

// console.log("👆 Adjust y", d, yExtent);
opts.y.domain([yExtent[0] - d / 2, yExtent[1] + d / 2]);
// .nice();
opts.x.domain(xExtent);
// .nice();
}
}
}
}
Insert cell
Insert cell
viewof ty = Inputs.range([0, 1000])
Insert cell
{
let width = 600,
height = 300,
x = d3.scaleLinear().domain([0, width]).range([0, width]),
y = d3.scaleLinear().domain([0, height]).range([0, height]),
newW,
newH;
const opts = {
width,
height,
x: d3.scaleLinear().domain([0, width]).range([0, width]),
y: d3.scaleLinear().domain([0, height]).range([0, height]),
autoFit: true,
keepAspectRatio: true,
nodes: [
{ x: 10, y: 10 },
{ x: width, y: ty }
]
};

x.domain(d3.extent(opts.nodes, (d) => d.x)).nice();
y.domain(d3.extent(opts.nodes, (d) => d.y)).nice();
let nodesX = x.domain()[0],
nodesY = y.domain()[0],
nodesWidth = x.domain()[1] - x.domain()[0],
nodesHeight = y.domain()[1] - y.domain()[0];
newW = opts.x.domain()[1] - opts.x.domain()[0];
newH = opts.y.domain()[1] - opts.y.domain()[0];

computeAutoFit(opts);
const target = html`<svg width="800" height="650">
<rect x="0" y="0" width="${width}" height="${height}" fill="none" stroke="black"/>
<rect x="${nodesX}" y="${nodesY}" width="${nodesWidth}" height="${nodesHeight}" fill="none" stroke="red"/>
<rect x="${opts.x.domain()[0]}" y="${
opts.y.domain()[0]
}" width="${newW}" height="${newH}" fill="none" stroke="blue" stroke-width="3"/>
</svg>`;

return target;
}
Insert cell
import {howto} from "@d3/example-components"
Insert cell
import {Swatches} from "@d3/color-legend"
Insert cell
import {forceTransport, forceExtent} from "21d2053b3bc85bce"
Insert cell
forceBoundary = require("d3-force-boundary@0.0.3")
Insert cell
d3 = require("d3@7")
Insert cell
import {edgeBundling} from "@john-guerra/force-edge-bundling"
Insert cell
forceInABox = require("force-in-a-box")
Insert cell
import { smartLabels } from "@john-guerra/smart-labels"
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more