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]);
const nodes = data.nodes.map((d) => Object.assign({}, d));
const edges = data.links.map((d) => Object.assign({}, d));
d3.select(canvas)
.call(
d3
.drag()
.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;
}