Public
Edited
Apr 6
Paused
Importers
9 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)) || { ...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) => ({
...n,
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

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more