Published
Edited
Jun 22, 2021
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
viewof activeColor = Inputs.radio(["Degree", "Closeness", "Betweenness"], {label: "Color"})
Insert cell
md`## Change airport names shown based on number of unique connections`

Insert cell

viewof x = html`<input type=range min=10 max=100 step=any>`
Insert cell
x
Insert cell
linkWidth = d3.scaleLinear().domain([1, 10]).range([1, 30]);
Insert cell
viewof curves = Inputs.toggle({label: "Curves?", value: true})
Insert cell
curves
Insert cell
data
Insert cell
md `## Reprocess data`
Insert cell
md `## Cross link node objects `
Insert cell
// data.links.forEach(link => {
// const a = data.nodes.find(obj => obj.id == link.source);
// const b = data.nodes.find(obj => obj.id == link.target);

// if ('neighbors' in a == false) {
// a.neighbors = []
// }

// if (!b.neighbors) {
// b.neighbors = []
// }

// a.neighbors.push(b);
// b.neighbors.push(a);

// if (!a.links) {
// a.links = []
// }

// if (!b.links) {
// b.links = []
// }
// a.links.push(link);
// b.links.push(link);
// });
Insert cell
maxDistance = 6 // Limit the search distance, if desired.
Insert cell
md`## Force simulation drawing`
Insert cell
import {serialize} from "@palewire/saving-json"
Insert cell
// // function serialize (data) {
// // let s = JSON.stringify(data);
// // return new Blob([s], {type: "application/json"})
// // }
// DOM.download(serialize(data), null, "Download JSON")
Insert cell
mutable foo = 1
Insert cell
{ mutable foo = 2;
return mutable foo; }
Insert cell
mutable graph = {}
Insert cell
forceGraph = (data) => {
const w2 = width / 2,
h2 = height / 2,
nodeRadius = 5;

const ctx = DOM.context2d(width, height);
const canvas = ctx.canvas;
const simulationDurationInMs = 20000;
let startTime = Date.now();
let endTime = startTime + simulationDurationInMs;

const simulation = forceSimulation(width, height);
let transform = d3.zoomIdentity;
let radiusFxn = d3.scaleLinear().domain([1, 100]).range([10,50]);

// The simulation will alter the input data objects so make
// copies to protect the originals.
const nodes = data.nodes.map(d => Object.assign({}, d));
const edges = data.links.map(d => Object.assign({}, d));

d3.select(canvas)
.call(d3.drag()
// Must set this in order to drag nodes. New in v5?
.container(canvas)
.subject(dragSubject)
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded))
.call(d3.zoom()
.scaleExtent([1 / 100, 100])
.on('zoom', zoomed));

simulation.nodes(nodes)
.on("tick",simulationUpdate)
.on('end', function(){
mutable graph = { nodes, 'links': edges };
// let s = JSON.stringify(newData);
// // new Blob([s], {type: "application/json"})
});
simulation.force("link")
.links(edges);

function zoomed() {
transform = d3.event.transform;
simulationUpdate();
}
/** Find the node that was clicked, if any, and return it. */
function dragSubject() {
const x = transform.invertX(d3.event.x),
y = transform.invertY(d3.event.y);
const node = findNode(nodes, x, y, nodeRadius);
if (node) {
node.x = transform.applyX(node.x);
node.y = transform.applyY(node.y);
}
// else: No node selected, drag container
return node;
}

function dragStarted() {
if (!d3.event.active) {
simulation.alphaTarget(0.3).restart();
}
d3.event.subject.fx = transform.invertX(d3.event.x);
d3.event.subject.fy = transform.invertY(d3.event.y);
}

function dragged() {
d3.event.subject.fx = transform.invertX(d3.event.x);
d3.event.subject.fy = transform.invertY(d3.event.y);
}

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

function simulationUpdate() {
ctx.save();
ctx.clearRect(0, 0, width, height);
ctx.translate(transform.x, transform.y);
ctx.scale(transform.k, transform.k);
// Draw edges
edges.forEach(function(d) {
if (curves) {
const l = Math.sqrt(Math.pow(d.target.x - d.source.x, 2) + Math.pow(d.target.y - d.source.y, 2)); // line length

const a = Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x); // line angle
const e = l * 1; // control point distance

const cp = { // control point
x: (d.source.x + d.target.x) / 2 + e * Math.cos(a - Math.PI / 2),
y: (d.source.y + d.target.y) / 2 + e * Math.sin(a - Math.PI / 2)
};
ctx.beginPath();
ctx.moveTo(d.source.x, d.source.y);
ctx.quadraticCurveTo(cp.x, cp.y, d.target.x, d.target.y);
// const xMid = (d.target.x + d.source.y) / 2
// const t0 = 0.1
// const t1 = 0.9
// const cp0y = (1-t0) * d.source.y + t0 * d.target.y
// const cp1y = (1-t1) * d.source.y + t1 * d.target.y
// ctx.beginPath();
// ctx.moveTo(d.source.x, d.source.y);
// ctx.bezierCurveTo(xMid, cp0y, xMid, cp1y, d.target.x, d.target.y);
} else {
ctx.beginPath();
ctx.moveTo(d.source.x, d.source.y);
ctx.lineTo(d.target.x, d.target.y);
}
ctx.lineWidth = linkWidth(d.value);
ctx.strokeStyle = 'rgb(216, 216, 216, 1)';
ctx.stroke();
});
// Draw nodes
nodes.forEach(function(d, i) {
ctx.beginPath();
// Node fill
ctx.moveTo(d.x + radiusFxn(d.occurence), d.y);
ctx.arc(d.x, d.y, radiusFxn(d.occurence), 0, 2 * Math.PI);
ctx.fillStyle = color(d);
ctx.fill();
// Node outline
ctx.strokeStyle = 'white'
ctx.lineWidth = '3'
ctx.stroke();
if (d.occurence > x) {
ctx.fillStyle = 'black';
ctx.font = '36px sans-serif';
ctx.fillText(d.id, d.x, d.y);
ctx.strokeStyle = 'white'
}
});
ctx.restore();
}
const highlightNodes = new Set();
const highlightLinks = new Set();
let hoverNode = null;
// canvas.onmouseover = (e) => {
// const transform = d3.zoomTransform(canvas);
// let subject = null;
// // Distance is expressed in unzoomed coordinates, ie. keeps proportion with circles radius
// // at any zoom level.
// let distance = maxDistance;
// console.log(e)
// const x = transform.invertX(e.layerX);
// const y = transform.invertY(e.layerY);
// for (const c of data.nodes) {
// let d = Math.hypot(x - c.x, y - c.y);
// if (d < distance) {
// distance = d;
// subject = c;
// }
// }
// console.log(e, subject);
// return subject
// ? {
// circle: subject,
// x: transform.applyX(subject.x),
// y: transform.applyY(subject.y)
// }
// : null;
// }
return canvas;
}
Insert cell
Insert cell

viewof linkMax = html`<input type=range min=100 max=500 step=any>`
Insert cell
linkMax
Insert cell
linkDistanceFxn = d3.scaleLinear().domain([1000,1]).range([2, linkMax])
Insert cell
function forceSimulation(width, height) {
return d3.forceSimulation().force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force('collision', d3.forceCollide().radius(function(d) {
return d3.scaleLog().domain([1, 10]).range([5, 100])(d.occurence) + 2
}))
.force("link", d3.forceLink().id(d => d.id).distance(d => {return linkDistanceFxn(d.value)}))
.velocityDecay(0.3);
}
Insert cell
data = FileAttachment("lcc_routes_1@2.json").json();
Insert cell
data.links[0]
Insert cell
Insert cell
changeColor = centralities[activeColor];
Insert cell
centralities = ({'Degree': 'd_centrality', 'Closeness': 'c_centrality', 'Betweenness': 'b_centrality'});
Insert cell
extents = {let extents = []
for (let i=0; i < data.nodes.length; i+=1) {
extents.push(data.nodes[i][changeColor])
}
return d3.extent(extents);
}
Insert cell
Insert cell
Insert cell
graph.nodes[0], graph.links[0]
Insert cell
DOM.download(serialize(graph), 'network.json', "Download JSON")
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