Skip to content

Instantly share code, notes, and snippets.

@bcrisp
Last active April 19, 2017 19:19
Show Gist options
  • Save bcrisp/85b2242ad22c0d29f38430d2f986aab6 to your computer and use it in GitHub Desktop.
Save bcrisp/85b2242ad22c0d29f38430d2f986aab6 to your computer and use it in GitHub Desktop.
D3 Canvas Force Network
{"nodes":[{"id":"a","group":1},{"id":"b","group":1},{"id":"c","group":1}],"links":[{"value":1,"source":"a","target":"b"}]}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<input id="newNode" title="Enter new project"/>
<button onclick="addNode(document.getElementById('newNode').value);">Add Node</button>
<canvas width="960" height="600"></canvas>
<div id="content" width="300" height="30"><p id="mycontent"></p></div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="render.js"></script>
</html>
var width = 960,
height = 600,
devicePixelRatio = window.devicePixelRatio || 1;
var canvas = d3.select("canvas").attr("width", (width ) * devicePixelRatio)
.attr("height", (height) * devicePixelRatio)
.style("width", width + "px")
.style("height", height + "px");
var context = canvas.node().getContext("2d"),
localCanvas = document.querySelector("canvas"),
nodeCache = [],
radius = 15,
initialDragNode = {},
contextFont="20px Georgia",
contextStrokeStyle = "#aaa",
connecting = false,
coords = [];
context.scale(devicePixelRatio, devicePixelRatio);
function regenerateJSON(){
function replacer(key,value)
{
if (key=="x"
|| key =="y"
|| key=="vy"
|| key=="vx"
|| key =="fx"
|| key=="fy"
|| key=="index")
return undefined;
else return value;
}
var jsonGraph = {nodes:simulation.nodes(), links: simulation.force("link")
.links()}
d3.select("#mycontent").html(JSON.stringify(jsonGraph, replacer));
}
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
function addNode(name){
nodeCache.push({id: name});
simulation.nodes(nodeCache);
regenerateJSON();
}
d3.json("graph.json", function(error, graph) {
if (error) throw error;
nodeCache = graph.nodes;
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links).distance(200);
canvas
.call(d3.drag()
.container(localCanvas)
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function ticked(event) {
context.clearRect(0, 0, width, height);
context.fillStyle = "steelblue";
context.beginPath();
var n = graph.nodes.length;
for (i = 0; i < n; ++i) {
d = graph.nodes[i];
context.moveTo(d.x, d.y);
context.arc(d.x, d.y, d.radius, 10, 10 * Math.PI);
drawNode(d)
}
context.fill();
context.beginPath();
graph.links.forEach(drawLink);
context.strokeStyle = contextStrokeStyle;
context.stroke();
context.fill();
if (connecting){
var d = {};
d.source = initialDragNode;
d.target = {};
d.target.x = coords[0]; // mouse[0];
d.target.y = coords[1]; //mouse[1];
context.beginPath();
drawLink(d);
context.strokeStyle = contextStrokeStyle;
context.stroke();
}
}
function dragged() {
if (event.altKey){
var mouse = d3.mouse(this);
coords[0] = mouse[0];
coords[1] = mouse[1];
connecting = true;
var closest = simulation.find(d3.event.x, d3.event.y, 30);
if(closest){
if (closest != initialDragNode){
if (!graph.links.some(function(element, index, array){return element.source == initialDragNode && element.target == closest}))
{
connecting = false;
graph.links.push({source: initialDragNode, target: closest});
simulation.force("link").links(graph.links);
regenerateJSON();
dragended();
}}}
}
else{
d3.event.subject.fx = d3.event.x;
d3.event.subject.fy = d3.event.y;
}
}
function dragsubject() {
return simulation.find(d3.event.x, d3.event.y);
}
function dragstarted(){
if(!event.altKey){
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.event.subject.fx = d3.event.subject.x;
d3.event.subject.fy = d3.event.subject.y;
}
else {
connecting = true;
dragended();
initialDragNode = d3.event.subject;
}
}
function dragended() {
connecting = false;
if (!d3.event.active) simulation.alphaTarget(0.3);
d3.event.subject.fx = null;
d3.event.subject.fy = null;
}
function drawLink(d) {
var θ = Math.atan2((d.source.y - d.target.y), (d.source.x - d.target.x));
var offset = 1.3;
var srcx = d.source.x - offset * (radius * Math.cos(θ));
var srcy = d.source.y - offset * (radius * Math.sin(θ));
var tarx = d.target.x - radius * Math.cos(θ + Math.PI);
var tary = d.target.y - radius * Math.sin(θ + Math.PI);
context.moveTo(srcx, srcy);
context.lineTo(tarx, tary);
}
function drawNode(d) {
context.moveTo(d.x, d.y);
context.arc(d.x, d.y, radius, 0, 2 * Math.PI);
context.font=contextFont;
context.fillText(d.id,d.x + 30,d.y);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment