Skip to content

Instantly share code, notes, and snippets.

@kratsg
Last active January 22, 2021 17:35
Show Gist options
  • Save kratsg/c1859abe3e8f4fb136d11735a8fa4510 to your computer and use it in GitHub Desktop.
Save kratsg/c1859abe3e8f4fb136d11735a8fa4510 to your computer and use it in GitHub Desktop.
// 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