Published
Edited
Sep 19, 2021
1 star
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
function ngle(cx, cy, ex, ey) {
var dy = ey - cy;
var dx = ex - cx;
var theta = Math.atan2(dy, dx); // range (-PI, PI]
theta *= 180 / Math.PI; // rads to degs, range (-180, 180]
//if (theta < 0) theta = 360 + theta; // range [0, 360)
return theta;
}
Insert cell
ngle(0, 0, 20, 20)
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
({
node: [
{
id: "1",
type: "File"
},
{
id: "2",
type: "Package"
}
],

links: [
{
from: "1",
to: "2"
}
]
})
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 angle(cx, cy, ex, ey) {
var dy = ey - cy;
var dx = ex - cx;
var theta = Math.atan2(dy, dx); // range (-PI, PI]
theta *= 180 / Math.PI; // rads to degs, range (-180, 180]
//if (theta < 0) theta = 360 + theta; // range [0, 360)
return theta;
}

function canvas_arrow(context, fromx, fromy, tox, toy) {
var headlen = 50; // length of head in pixels
var dx = tox - fromx;
var dy = toy - fromy;
var angle = Math.atan2(dy, dx);
context.moveTo(fromx, fromy);
context.lineTo(tox, toy);
context.lineTo(
tox - headlen * Math.cos(angle - Math.PI / 6),
toy - headlen * Math.sin(angle - Math.PI / 6)
);
context.moveTo(tox, toy);
context.lineTo(
tox - headlen * Math.cos(angle + Math.PI / 6),
toy - headlen * Math.sin(angle + Math.PI / 6)
);
}

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 (false) {
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();

const cx = d.source.x + (d.target.x - d.source.x) / 2;
const cy = d.source.y + (d.target.y - d.source.y) / 2;
/*
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.fillStyle = "red";
ctx.arc(cx, cy, 10, 0, 2 * Math.PI);
ctx.fill();
*/

//ctx.save();
//ctx.translate(cx, cy);
//ctx.rotate(
// angle(d.target.x, d.target.y, d.source.x, d.source.y) / Math.PI
//);
//ctx.textAlign = "center";
//ctx.fillText(">>>", 0, 0);
//ctx.restore();

ctx.save();
ctx.beginPath();
canvas_arrow(ctx, d.source.x, d.source.y, cx, cy);
ctx.strokeStyle = "red";
ctx.lineWidth = 5;
ctx.stroke();

ctx.restore();
});

// 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
d3
.nest()
.key((d) => [d.source, d.target].sort())
.entries(data.links)
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

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