Last active
January 22, 2021 17:35
-
-
Save kratsg/c1859abe3e8f4fb136d11735a8fa4510 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// dimensions | |
var canvasWidth = 1200; | |
var canvasHeight = 800; | |
var margin = { | |
top: 50, | |
bottom: 50, | |
left: 200, | |
right: 50, | |
} | |
// create an svg to draw in | |
var svg = d3.select("body") | |
.append("svg") | |
.attr("width", canvasWidth) | |
.attr("height", canvasHeight) | |
.append('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
width = canvasWidth - margin.left - margin.right; | |
height = canvasHeight - margin.top - margin.bottom; | |
// load the graph | |
function build_graph(dataSource) { | |
var linkWidthScale = d3.scaleLinear() | |
.range([1, 15]); | |
var linkStrengthScale = d3.scaleLinear() | |
.range([0, 0.45]); | |
var simulation = d3.forceSimulation() | |
// pull nodes together based on the links between them | |
.force("link", d3.forceLink() | |
.id(function(d) { | |
return d.id; | |
}) | |
.strength(function(d) { | |
return linkStrengthScale(d.weight); | |
})) | |
// push nodes apart to space them out | |
.force("charge", d3.forceManyBody() | |
.strength(-200)) | |
// add some collision detection so they don't overlap | |
.force("collide", d3.forceCollide() | |
.radius(75)) | |
// and draw them around the centre of the space | |
.force("center", d3.forceCenter(width / 2, height / 2)); | |
var nodes; | |
var links; | |
var linkedByIndex = {}; | |
var filtered = []; | |
var analyses; | |
var legend; | |
d3.json(dataSource).then(function(graph) { | |
// set the nodes | |
nodes = graph.nodes; | |
// links between nodes | |
links = graph.links; | |
// build list of analyses for legend and filtering | |
analyses = nodes.map(node => ({name: node['analysis_name'], colour: node['colour']})); | |
analyses = analyses.filter((value, index, self) => self.findIndex((m) => m.name === value.name ) === index); | |
// get the domain set up | |
linkWidthScale.domain(d3.extent(links, function(d) { | |
return d.weight; | |
})); | |
linkStrengthScale.domain(d3.extent(links, function(d) { | |
return d.weight; | |
})); | |
// add the legend to the graphic | |
legend = d3.select("body > svg").selectAll(".legend") | |
.data(analyses) | |
.enter() | |
.append("g") | |
.attr("class", "legend") | |
.style("cursor", "pointer") | |
.attr("transform", function(d, i) { | |
return "translate(0," + (i * 20 + margin.top) + ")"; | |
}) | |
.on("click", d => legendFilter(d)); | |
// a circle to represent the node | |
legend.append("rect") | |
.attr("x", 5) | |
.attr("width", 15) | |
.attr("height", 15) | |
.attr("fill", d => d.colour) | |
.attr("stroke", d => d.colour) | |
.attr("stroke-width", 2); | |
// add a label to each node | |
legend.append("text") | |
.attr("x", 25) | |
.attr("y", 9.5) | |
.attr("dy", ".35em") | |
.text(d => d.name) | |
.style("stroke", "black") | |
.style("stroke-width", 0.5) | |
.style(d => d.colour); | |
update_graph(); | |
}); | |
var link = svg.append("g").selectAll(".link"), | |
node = svg.append("g").selectAll(".node"), | |
linkText = svg.append("g").selectAll(".link-labels"); | |
async function update_graph() { | |
link = link.data(links).join("path") | |
.attr("class", "link") | |
.attr('stroke', function(d) { | |
return "#ddd"; | |
}) | |
.attr('stroke-width', function(d) { | |
return linkWidthScale(d.weight); | |
}); | |
// add the labels to the edges | |
linkText = linkText.data(links).join("text") | |
.attr("class", "link-label") | |
.attr("font-family", "Arial, Helvetica, sans-serif") | |
.attr("fill", "Black") | |
.attr("dy", function(d){ return d.source == d.target ? "2.0em" : "0.35em"}) | |
.attr("text-anchor", "middle") | |
.style("font", "normal 12px Arial") | |
.text(function(d) { | |
return d.weight; | |
}); | |
// add the nodes to the graphic | |
node = node.data(nodes).join(function(enter){ | |
let group = enter.append("g") | |
.style("cursor", "pointer") | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)) | |
.on("mouseover", mouseOver(.1)) | |
.on("mouseout", mouseOut); | |
// a circle to represent the node | |
let node = group.append("circle") | |
.attr("class", "node") | |
.attr("r", 8) | |
.attr("fill", function(d) { | |
return d.colour; | |
}); | |
// add a label to each node | |
group.append("text") | |
.attr("dx", 12) | |
.attr("dy", ".35em") | |
.text(function(d) { | |
return d.id; | |
}) | |
.style("stroke", "black") | |
.style("stroke-width", 0.5) | |
.style("fill", function(d) { | |
return d.colour; | |
}); | |
return node; | |
}); | |
console.log(node); | |
// add the nodes to the simulation and | |
// tell it what to do on each tick | |
simulation | |
.nodes(nodes) | |
.on("tick", ticked); | |
// add the links to the simulation | |
simulation | |
.force("link") | |
.links(links); | |
simulation.alpha(1).alphaTarget(0).restart(); | |
// build a dictionary of nodes that are linked | |
links.forEach(function(d) { | |
linkedByIndex[d.source.index + "," + d.target.index] = 1; | |
}); | |
//await new Promise(r => setTimeout(r, 2000)); | |
//nodes.pop(); | |
//update_graph(); | |
} | |
// on each tick, update node and link positions | |
function ticked() { | |
link.attr("d", positionLink); | |
linkText | |
.attr("x", function(d) { | |
return ((d.source.x + d.target.x)/2); | |
}) | |
.attr("y", function(d) { | |
return ((d.source.y + d.target.y)/2); | |
}); | |
//node.attr("transform", positionNode); | |
console.log('it', node); | |
console.log(d3.selectAll(".node")); | |
d3.selectAll(".node").attr("transform", positionNode); | |
} | |
// keep within the boundaries | |
function ensureBoundaries(d) { | |
if (d.x < 0) { | |
d.x = 0 | |
}; | |
if (d.y < 0) { | |
d.y = 0 | |
}; | |
if (d.x > width) { | |
d.x = width | |
}; | |
if (d.y > height) { | |
d.y = height | |
}; | |
} | |
// links are drawn as curved paths between nodes, | |
// through the intermediate nodes | |
function positionLink(d) { | |
if(d.source == d.target) return null; | |
var offset = 30; | |
ensureBoundaries(d.source); | |
ensureBoundaries(d.target); | |
var midpoint_x = (d.source.x + d.target.x) / 2; | |
var midpoint_y = (d.source.y + d.target.y) / 2; | |
var dx = (d.target.x - d.source.x); | |
var dy = (d.target.y - d.source.y); | |
var normalise = Math.sqrt((dx * dx) + (dy * dy)); | |
var offSetX = midpoint_x + offset * (dy / normalise); | |
var offSetY = midpoint_y - offset * (dx / normalise); | |
var position = "M" + d.source.x + "," + d.source.y + | |
"S" + offSetX + "," + offSetY + | |
" " + d.target.x + "," + d.target.y; | |
return position; | |
} | |
// move the node based on forces calculations | |
function positionNode(d) { | |
ensureBoundaries(d); | |
return "translate(" + d.x + "," + d.y + ")"; | |
} | |
// check the dictionary to see if nodes are linked | |
function isConnected(a, b) { | |
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index; | |
} | |
// handle dragging nodes | |
function dragstarted(d) { | |
if (!d3.event.active) simulation.alphaTarget(0.3).restart(); | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(d) { | |
mouseOver(0.1)(d); | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragended(d) { | |
if (!d3.event.active) simulation.alphaTarget(0); | |
d.fx = null; | |
d.fy = null; | |
} | |
// fade nodes on hover | |
function mouseOver(opacity) { | |
return function(d) { | |
// check all other nodes to see if they're connected | |
// to this one. if so, keep the opacity at 1, otherwise | |
// fade | |
var opacity_by_index = {}; | |
node.style("stroke-opacity", function(o) { | |
thisOpacity = isConnected(d, o) ? 1 : opacity; | |
opacity_by_index[o.index] = thisOpacity; | |
return thisOpacity; | |
}); | |
node.style("fill-opacity", function(o) { | |
thisOpacity = isConnected(d, o) ? 1 : opacity; | |
return thisOpacity; | |
}); | |
// also style link accordingly | |
link.style("stroke-opacity", function(o) { | |
return o.source === d || o.target === d ? 1 : opacity; | |
}); | |
link.style("stroke", function(o) { | |
return o.source === d || o.target === d ? d.colour : "#ddd"; | |
}); | |
linkText.style("opacity", function(o) { | |
if(o.source === o.target){ | |
return opacity_by_index[o.source.index]; | |
} | |
return o.source === d || o.target === d ? 1 : opacity; | |
}); | |
}; | |
} | |
function mouseOut() { | |
node.style("stroke-opacity", 1); | |
node.style("fill-opacity", 1); | |
link.style("stroke-opacity", 1); | |
link.style("stroke", "#ddd"); | |
linkText.style("opacity", 1); | |
} | |
function legendFilter(d) { | |
// add the clicked key if not included: | |
if (filtered.indexOf(d.name) == -1) { | |
filtered.push(d.name); | |
// if all bars are un-checked, reset: | |
if (filtered.length == analyses.length) filtered = []; | |
} | |
// otherwise remove it: | |
else { | |
filtered.splice(filtered.indexOf(d.name), 1); | |
} | |
legend.selectAll("rect") | |
.transition() | |
.attr("fill", function(d, i) { | |
if (filtered.length) { | |
if (filtered.indexOf(d.name) == -1) { | |
return d.colour; | |
} else { | |
return "white"; | |
} | |
} else { | |
return d.colour; | |
} | |
}) | |
.duration(250); | |
console.log(filtered); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment