Skip to content

Instantly share code, notes, and snippets.

@ppham27
Last active November 14, 2023 15:03
Show Gist options
  • Save ppham27/d4dd06966137fe02d9c1 to your computer and use it in GitHub Desktop.
Save ppham27/d4dd06966137fe02d9c1 to your computer and use it in GitHub Desktop.
Infection Visualization

Infection Visualization

This is the visualization that accompanies Infection. You can find the code at GitHub. This dataset consists of 6 components. 5 of the components have 5 nodes, and the big component has 23 nodes. The number of infections has been limited to 26. Repeatedly, press the infect button to see how to optimally infect the nodes. We want all nodes in a component to share the same state. As you can you see, it is possible to have infect 25 nodes with this restriction. This solution was found through dynamic programming. Note that a greedy algorithm would have failed here since it would have selected the largest component, which has 23 nodes. After infecting this component, we would not be able to infect any other components because that would exceed 26 infections.

Some interesting features of this visualization that can be reused:

  • the use of SVG markers for arrowheads
  • nodes are draggable with one node's position being fixed ondragstart
  • the infection count is interpolated, so it counts up smoothly
// basic set up
var width = 960;
var height = 500;
var nodeSize = 15;
var uninfectedFill = "#b3cde3";
var infectedFill = "#fbb4ae";
var force = d3.layout.force()
.size([width, height])
.linkDistance(80)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("#graph")
.append("svg")
.attr("width", width)
.attr("height", height);
// arrowhead marker
svg.append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 0 10 10")
.attr("refX", "0")
.attr("refY", "5")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", "8")
.attr("markerHeight", "6")
.attr("orient", "auto")
.append("path")
.attr("d","M 0 0 L 10 5 L 0 10 z");
// legend
var legendRowHeight = 40;
var legendLabels = [{"row": 1, "label": "uninfected", "fill": uninfectedFill},
{"row": 2, "label": "infected", "fill": infectedFill},
{"row": 3, "label": "student-coach"}];
var legend = svg
.append("g").attr("id", "legend")
.attr("transform","translate(30 20)");
legend.selectAll(".legend-label")
.data(legendLabels).enter()
.append("text")
.attr("x", 30)
.attr("y", function(d) { return d.row*legendRowHeight; })
.attr("text-anchor", "right")
.attr("dy", "5px")
.text(function(d) { return d.label; });
legend.selectAll(".legend-circle")
.data(legendLabels.slice(0, 2)).enter()
.append("circle")
.attr("class", "node")
.attr("cy", function(d) { return d.row*legendRowHeight; })
.attr("r", nodeSize)
.attr("fill", function(d) { return d.fill; });
legend
.append("line")
.attr("class", "link")
.attr("x1", -nodeSize).attr("y1", legendRowHeight*3)
.attr("x2", nodeSize-5).attr("y2", legendRowHeight*3)
.attr("marker-end", "url(#arrowhead)");
// state variables
var infectionState = 0;
var numInfected = 0;
// interactive panel
var controls = d3.select("#graph").append("div").attr("id", "controls");
controls
.append("span")
.attr("class", "button")
.append("button")
.attr("type", "button")
.text("Infect")
.on("click", infect);
controls
.append("span")
.attr("class", "button")
.append("button")
.attr("type", "button")
.text("Reset")
.on("click", reset);
controls.append("span")
.attr("id", "status")
.text("Users Infected: ")
.append("span")
.attr("id", "infected-number")
.text(numInfected);
// build graph
var link = svg.selectAll(".link.data");
var node = svg.selectAll(".node.data");
var nodeLabel = svg.selectAll(".node-label.data");
var totalInfected = -1;
d3.json("graph.json", function(error, graph) {
force.charge(-125);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link data")
.attr("marker-end","url(#arrowhead)");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node data")
.attr("fill", uninfectedFill)
.attr("r", nodeSize)
.on("mouseover", mouseoverNode)
.on("mouseout", mouseoutNode)
.call(drag);
nodeLabel = nodeLabel.data(graph.nodes)
.enter().append("text")
.attr("class", "node-label data")
.attr("text-anchor", "middle")
.attr("dy", "5px")
.text(function(d) { return d.name; })
.on("mouseover", mouseoverNodeLabel)
.on("mouseout", mouseoutNodeLabel)
.call(drag);
totalInfected = node.filter(function(d) { return d.infect > 0; }).size();
d3.select("span#infected-number").text('0 of ' + totalInfected);
});
// callbacks
function tick() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.source.x + 0.75*(d.target.x - d.source.x); })
.attr("y2", function(d) { return d.source.y + 0.75*(d.target.y - d.source.y); })
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
nodeLabel.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
function dragstart(d) {
node.each(function(dd) { dd.fixed = false; });
d.fixed = true;
}
function mouseoverNode() {
d3.select(this).classed("hover", true);
}
function mouseoutNode() {
d3.select(this).classed("hover", false);
}
function mouseoverNodeLabel(d) {
node.filter(function(dd) { return dd.name === d.name; }).classed("hover", true);
}
function mouseoutNodeLabel(d) {
node.filter(function(dd) { return d.name === d.name; }).classed("hover", false);
}
function infect() {
// if there are still users left to infect
if (numInfected < node.filter(function(d) { return d.infect > 0; }).size()) {
infectionState += 1;
updateInfections();
}
}
function reset() {
infectionState = 0;
updateInfections();
}
function updateInfections() {
var transitionDuration = 1000;
var oldNumInfected = numInfected;
if (infectionState === 0) {
node.transition().duration(transitionDuration).attr("fill", uninfectedFill);
numInfected = 0;
} else {
numInfected = node.filter(function(d) {
return d.infect !== 0 && d.infect <= infectionState;
}).transition().duration(transitionDuration).attr("fill", infectedFill).size();
}
// number text transition
d3.select("span#infected-number")
.transition()
.duration(transitionDuration)
.tween("textContent", function() {
var f = d3.interpolate(oldNumInfected, numInfected);
return function(t) {
var intNumInfected = f(t);
this.textContent = intNumInfected.toFixed(0) + " of " + totalInfected;
if (intNumInfected === totalInfected) {
d3.select("span#infected-number").text(numInfected + " of " + totalInfected + ". All infected!");
}
};
});
}
{"links": [{"target": 2, "source": 0}, {"target": 4, "source": 0}, {"target": 2, "source": 1}, {"target": 3, "source": 1}, {"target": 4, "source": 1}, {"target": 4, "source": 3}, {"target": 7, "source": 5}, {"target": 8, "source": 5}, {"target": 9, "source": 5}, {"target": 7, "source": 6}, {"target": 8, "source": 6}, {"target": 9, "source": 6}, {"target": 8, "source": 7}, {"target": 9, "source": 7}, {"target": 9, "source": 8}, {"target": 12, "source": 10}, {"target": 13, "source": 10}, {"target": 14, "source": 10}, {"target": 12, "source": 11}, {"target": 13, "source": 11}, {"target": 14, "source": 11}, {"target": 14, "source": 12}, {"target": 16, "source": 15}, {"target": 17, "source": 15}, {"target": 18, "source": 15}, {"target": 17, "source": 16}, {"target": 18, "source": 16}, {"target": 19, "source": 16}, {"target": 19, "source": 17}, {"target": 19, "source": 18}, {"target": 21, "source": 20}, {"target": 23, "source": 20}, {"target": 22, "source": 21}, {"target": 23, "source": 21}, {"target": 24, "source": 21}, {"target": 23, "source": 22}, {"target": 24, "source": 22}, {"target": 24, "source": 23}, {"target": 26, "source": 25}, {"target": 28, "source": 25}, {"target": 29, "source": 25}, {"target": 32, "source": 25}, {"target": 34, "source": 25}, {"target": 35, "source": 25}, {"target": 36, "source": 25}, {"target": 39, "source": 25}, {"target": 40, "source": 25}, {"target": 43, "source": 25}, {"target": 47, "source": 25}, {"target": 28, "source": 26}, {"target": 29, "source": 26}, {"target": 31, "source": 26}, {"target": 32, "source": 26}, {"target": 33, "source": 26}, {"target": 34, "source": 26}, {"target": 35, "source": 26}, {"target": 37, "source": 26}, {"target": 38, "source": 26}, {"target": 39, "source": 26}, {"target": 40, "source": 26}, {"target": 41, "source": 26}, {"target": 42, "source": 26}, {"target": 43, "source": 26}, {"target": 44, "source": 26}, {"target": 45, "source": 26}, {"target": 47, "source": 26}, {"target": 28, "source": 27}, {"target": 29, "source": 27}, {"target": 30, "source": 27}, {"target": 31, "source": 27}, {"target": 32, "source": 27}, {"target": 33, "source": 27}, {"target": 35, "source": 27}, {"target": 36, "source": 27}, {"target": 37, "source": 27}, {"target": 38, "source": 27}, {"target": 39, "source": 27}, {"target": 40, "source": 27}, {"target": 41, "source": 27}, {"target": 42, "source": 27}, {"target": 43, "source": 27}, {"target": 45, "source": 27}, {"target": 46, "source": 27}, {"target": 29, "source": 28}, {"target": 30, "source": 28}, {"target": 33, "source": 28}, {"target": 34, "source": 28}, {"target": 35, "source": 28}, {"target": 36, "source": 28}, {"target": 37, "source": 28}, {"target": 38, "source": 28}, {"target": 40, "source": 28}, {"target": 42, "source": 28}, {"target": 43, "source": 28}, {"target": 44, "source": 28}, {"target": 45, "source": 28}, {"target": 46, "source": 28}, {"target": 47, "source": 28}, {"target": 30, "source": 29}, {"target": 31, "source": 29}, {"target": 32, "source": 29}, {"target": 33, "source": 29}, {"target": 34, "source": 29}, {"target": 36, "source": 29}, {"target": 37, "source": 29}, {"target": 38, "source": 29}, {"target": 39, "source": 29}, {"target": 40, "source": 29}, {"target": 41, "source": 29}, {"target": 42, "source": 29}, {"target": 43, "source": 29}, {"target": 44, "source": 29}, {"target": 46, "source": 29}, {"target": 47, "source": 29}, {"target": 32, "source": 30}, {"target": 33, "source": 30}, {"target": 34, "source": 30}, {"target": 35, "source": 30}, {"target": 36, "source": 30}, {"target": 37, "source": 30}, {"target": 38, "source": 30}, {"target": 39, "source": 30}, {"target": 41, "source": 30}, {"target": 42, "source": 30}, {"target": 43, "source": 30}, {"target": 44, "source": 30}, {"target": 47, "source": 30}, {"target": 32, "source": 31}, {"target": 33, "source": 31}, {"target": 34, "source": 31}, {"target": 35, "source": 31}, {"target": 36, "source": 31}, {"target": 37, "source": 31}, {"target": 38, "source": 31}, {"target": 39, "source": 31}, {"target": 40, "source": 31}, {"target": 41, "source": 31}, {"target": 42, "source": 31}, {"target": 43, "source": 31}, {"target": 45, "source": 31}, {"target": 46, "source": 31}, {"target": 47, "source": 31}, {"target": 34, "source": 32}, {"target": 35, "source": 32}, {"target": 36, "source": 32}, {"target": 37, "source": 32}, {"target": 39, "source": 32}, {"target": 40, "source": 32}, {"target": 41, "source": 32}, {"target": 42, "source": 32}, {"target": 43, "source": 32}, {"target": 44, "source": 32}, {"target": 46, "source": 32}, {"target": 47, "source": 32}, {"target": 35, "source": 33}, {"target": 36, "source": 33}, {"target": 37, "source": 33}, {"target": 38, "source": 33}, {"target": 40, "source": 33}, {"target": 41, "source": 33}, {"target": 42, "source": 33}, {"target": 44, "source": 33}, {"target": 45, "source": 33}, {"target": 46, "source": 33}, {"target": 35, "source": 34}, {"target": 37, "source": 34}, {"target": 39, "source": 34}, {"target": 40, "source": 34}, {"target": 41, "source": 34}, {"target": 42, "source": 34}, {"target": 43, "source": 34}, {"target": 44, "source": 34}, {"target": 46, "source": 34}, {"target": 47, "source": 34}, {"target": 36, "source": 35}, {"target": 37, "source": 35}, {"target": 39, "source": 35}, {"target": 40, "source": 35}, {"target": 45, "source": 35}, {"target": 46, "source": 35}, {"target": 37, "source": 36}, {"target": 38, "source": 36}, {"target": 39, "source": 36}, {"target": 40, "source": 36}, {"target": 41, "source": 36}, {"target": 42, "source": 36}, {"target": 43, "source": 36}, {"target": 44, "source": 36}, {"target": 45, "source": 36}, {"target": 46, "source": 36}, {"target": 39, "source": 37}, {"target": 40, "source": 37}, {"target": 41, "source": 37}, {"target": 42, "source": 37}, {"target": 44, "source": 37}, {"target": 45, "source": 37}, {"target": 46, "source": 37}, {"target": 47, "source": 37}, {"target": 39, "source": 38}, {"target": 40, "source": 38}, {"target": 42, "source": 38}, {"target": 43, "source": 38}, {"target": 44, "source": 38}, {"target": 45, "source": 38}, {"target": 47, "source": 38}, {"target": 40, "source": 39}, {"target": 41, "source": 39}, {"target": 42, "source": 39}, {"target": 43, "source": 39}, {"target": 44, "source": 39}, {"target": 47, "source": 39}, {"target": 41, "source": 40}, {"target": 42, "source": 40}, {"target": 43, "source": 40}, {"target": 44, "source": 40}, {"target": 45, "source": 40}, {"target": 46, "source": 40}, {"target": 43, "source": 41}, {"target": 44, "source": 41}, {"target": 45, "source": 41}, {"target": 43, "source": 42}, {"target": 44, "source": 42}, {"target": 45, "source": 42}, {"target": 46, "source": 42}, {"target": 44, "source": 43}, {"target": 47, "source": 43}, {"target": 45, "source": 44}, {"target": 46, "source": 44}, {"target": 47, "source": 44}, {"target": 47, "source": 45}, {"target": 47, "source": 46}], "nodes": [{"infect": 2, "name": "hma", "id": 0}, {"infect": 2, "name": "knx", "id": 1}, {"infect": 2, "name": "sbr", "id": 2}, {"infect": 2, "name": "mjt", "id": 3}, {"infect": 2, "name": "ujf", "id": 4}, {"infect": 1, "name": "ejw", "id": 5}, {"infect": 1, "name": "fsu", "id": 6}, {"infect": 1, "name": "fmy", "id": 7}, {"infect": 1, "name": "lgx", "id": 8}, {"infect": 1, "name": "ypc", "id": 9}, {"infect": 3, "name": "hww", "id": 10}, {"infect": 3, "name": "ghd", "id": 11}, {"infect": 3, "name": "vvq", "id": 12}, {"infect": 3, "name": "aud", "id": 13}, {"infect": 3, "name": "frj", "id": 14}, {"infect": 5, "name": "vby", "id": 15}, {"infect": 5, "name": "tgo", "id": 16}, {"infect": 5, "name": "vrd", "id": 17}, {"infect": 5, "name": "flw", "id": 18}, {"infect": 5, "name": "rre", "id": 19}, {"infect": 4, "name": "lph", "id": 20}, {"infect": 4, "name": "blj", "id": 21}, {"infect": 4, "name": "ywx", "id": 22}, {"infect": 4, "name": "zty", "id": 23}, {"infect": 4, "name": "jic", "id": 24}, {"infect": 0, "name": "dsn", "id": 25}, {"infect": 0, "name": "xgx", "id": 26}, {"infect": 0, "name": "dpd", "id": 27}, {"infect": 0, "name": "hly", "id": 28}, {"infect": 0, "name": "vaa", "id": 29}, {"infect": 0, "name": "fzl", "id": 30}, {"infect": 0, "name": "njx", "id": 31}, {"infect": 0, "name": "xiv", "id": 32}, {"infect": 0, "name": "ttk", "id": 33}, {"infect": 0, "name": "ivl", "id": 34}, {"infect": 0, "name": "eko", "id": 35}, {"infect": 0, "name": "ben", "id": 36}, {"infect": 0, "name": "qwp", "id": 37}, {"infect": 0, "name": "njp", "id": 38}, {"infect": 0, "name": "urb", "id": 39}, {"infect": 0, "name": "avm", "id": 40}, {"infect": 0, "name": "bgt", "id": 41}, {"infect": 0, "name": "idg", "id": 42}, {"infect": 0, "name": "mgn", "id": 43}, {"infect": 0, "name": "jlq", "id": 44}, {"infect": 0, "name": "wgw", "id": 45}, {"infect": 0, "name": "cpa", "id": 46}, {"infect": 0, "name": "cxu", "id": 47}]}
<!DOCTYPE html>
<html>
<head>
<title>Infection Visualization</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div id="graph">
</div>
<script src="graph.js"></script>
</body>
</html>
body {
font-family: HelveticaNeue, Helvetica;
color: #333;
}
.link {
stroke: #777;
stroke-width: 1.5px;
}
#arrowhead {
stroke: #777;
fill: #777;
}
.node {
stroke: #777;
stroke-width: 1.5px;
}
.node.data {
cursor: move;
}
.node.data.hover {
stroke-width: 3px;
}
.node-label.data {
cursor: move
}
.button {
padding: 10px;
}
#status {
padding: 10px;
}
#controls {
position: absolute;
top: 480px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment