Skip to content

Instantly share code, notes, and snippets.

@sungeunanlee
Last active August 9, 2019 02:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sungeunanlee/8c7fb6f47730d73c7bd8126db97f1b00 to your computer and use it in GitHub Desktop.
Save sungeunanlee/8c7fb6f47730d73c7bd8126db97f1b00 to your computer and use it in GitHub Desktop.
D3 Food web network visualization

This example demonstrates how to visualize food web building on D3's force layout example (https://bl.ocks.org/mbostock/3750558).

The network generates from the focal species and retrieves its predators, prey, and competitors using data from Globi (https://www.globalbioticinteractions.org). When hovering over a node, its name, the relationship with the focal species, and the picture are shown in the right panel. Cliking the name will direct you to the detail species page in EOL. Cliking a new species node will trigeer a focal species transition, and such change will be shown in a slow animation.

The visualization leverages the json data retrieved from Globi: https://beta.eol.org/api/pages/328447/pred_prey.json, where 328447 is an eol id.

Also note that clicking the new node will take some time since it fetches new data from the server.

Main Interactions

  • Hover your mouse over the node to see its name and the picture.
  • Click the node to change the focal species.
  • Drag and drog the node for re-positioning.

This visualization has the following functions and has used the corresponding resources.

Functions and resources:

If you have any questions, email to sungeun dot an at gatech.edu.

<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<style>
/* style definitions */
* {
font: 12px verdana;
}
.node circle{
stroke: #777;
stroke-width: 0px;
}
.split_left {
height: 90%;
width: 80%;
position: fixed;
z-index: 1;
top: 0;
overflow-x: hidden;
left: 0;
}
.split_right {
height: 90%;
width: 20%;
position: fixed;
z-index: 1;
top: 0;
overflow-x: hidden;
right: 0;
}
.split_bottom {
height: 10%;
width: 100%;
position: fixed;
z-index: 1;
bottom:0;
overflow-x: hidden;
left:0
right:0;
background:white;
}
.link {
stroke: #c1c1c1;
stroke-width: 1px;
}
.new_link {
stroke: #c1c1c1;
stroke-width: 1px;
}
.link.arrow {
stroke: #51768C;
stroke: #c1c1c1;
}
.link.prey {
stroke: #EE7560;
stroke: #c1c1c1;
}
.controls{
display: inline-block;
}
button {
border-radius: 8px;
font-size: 16px;
background-color: #e7e7e7;
color: black;
border: 2px solid #e7e7e7;
}
button:hover {
background-color: #e7e7e7;
background-color: white;
}
.tooltip {
bottom: 0%;
left:0%;
transform: translate(0%, 0%);
position: absolute;
background: #DDDDDD;
text-align: center;
width: 150px;
height: 250px;
padding: 5px;
font: 15px verdana;
border-radius: 10px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .1);
}
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
</style>
<body>
<!--The screen-->
<div>
<!--Left-->
<div class = "split_left">
<div class="centered">
<svg id="networkSvg"></svg>
</div>
</div>
<!--Right-->
<div class = "split_right">
<div><div id = "tooltipDiv" class="tooltip"></div>
<svg id="tooltipSvg"></svg>
</div>
</div>
<!--Bottom-->
<div class ="split_bottom">
<div class="controls">
<button id="zoom_in">+</button>
<button id="zoom_out">-</button>
<button id="reset">Reset</button>
</div>
</div>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="trophic_web.js"></script>
</body>
</html>
//new data
var dataStored = [];
var nodeIDList = [];
var linkIDList = [];
var sitePrefix = 'https://beta.eol.org';//"https://beta.eol.org";
var pageId;
//graph
var graph,
node,
link,
new_node,
existing_node,
existing_link,
new_link;
//for animation purpose
var source_nodes=[],
existing_nodes=[],
new_nodes=[],
hiding_nodes=[],
existing_links = [],
new_links = [],
transition = false;
//node positions
var curSource,
predPos = [],
preyPos = [],
compPos = [],
sourcePos= [];
var compList = [];
//Node number limit
var nLimit = 7;
//network graph window #networkSvg
var width= 1000,
height= 800,
radius = 6,
source_radius = 30;
//node colors
var color = d3.scaleOrdinal(d3.schemeSet3);
color(1);
color(2);
color(3);
color(4);
color(5);
//svg selection and sizing
var s = d3.select("#networkSvg")
.attr("width", width)
.attr("height", height);
var svg = s.append("g")
.attr("width", width)
.attr("height", height);
var tooltip = d3.select("#tooltipDiv");
var tooltipSvg = d3.select("#tooltipSvg");
var zoom = d3.zoom().scaleExtent([0, 3])
.on("zoom", function() {
svg.attr("transform", d3.event.transform);});
s.call(zoom);
d3.select("#reset").on("click", function() {
s.transition().duration(100).call(zoom.transform, d3.zoomIdentity);
toggleVisibilityOfNodesAndLinks(graph, graph.nodes[0]);
updateGraph();
});
d3.select("#zoom_in").on("click", function() {
zoom.scaleBy(s.transition().duration(100), 1.1);
});
d3.select("#zoom_out").on("click", function() {
zoom.scaleBy(s.transition().duration(100), 0.9);
});
//legend label HTML
var sequentialScale = tooltipSvg.append("g")
.attr("class", "legendarray")
.attr("transform", "translate(0,80)")
.append("g")
.attr("class", "legendCells")
.attr("transform", "translate(0, 12.015625)");
var predLegend = sequentialScale.append("g")
.attr("class", "cell")
.attr("transform", "translate(0,0)");
predLegend
.append("rect").attr("class", "watch")
.attr("height", 15).attr("width", 30)
.attr("style", "fill: rgb(141, 211, 199);");
predLegend
.append("text")
.attr("class", "label")
.attr("transform", "translate(40, 12.5)")
.text("Predator");
var preyLegend = sequentialScale.append("g")
.attr("class", "cell")
.attr("transform", "translate(0,20)");
preyLegend
.append("rect")
.attr("class", "watch")
.attr("height", 15)
.attr("width", 30)
.attr("style", "fill: rgb(255, 255, 179);");
preyLegend
.append("text")
.attr("class", "label")
.attr("transform", "translate(40, 12.5)")
.text("Prey");
var compLegend = sequentialScale.append("g")
.attr("class", "cell").attr("transform", "translate(0,40)");
compLegend
.append("rect")
.attr("class", "watch")
.attr("height", 15).attr("width", 30)
.attr("style", "fill: rgb(128, 177, 211);");
compLegend
.append("text")
.attr("class", "label")
.attr("transform", "translate(40, 12.5)")
.text("Competitor");
var pattern = svg.selectAll('.pattern');
var marker = svg.selectAll('.marker')
.data(["arrow", "longer"])
.enter().append('marker')
.attr("id", function(d) {return d;})
.attr("viewBox", "0 -5 10 10")
.attr("refX", function(d) {
if(d == "arrow") {
return 20;
} else {
return 60;
}
})
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.attr("fill", "#9b9b9b")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.style("stroke", "#9b9b9b");
node = svg.selectAll('.node');
new_node = svg.selectAll('.new_node');
existing_node =svg.selectAll('.existing_node');
new_link = svg.selectAll('.new_link');
existing_link =svg.selectAll('.existing_link');
link = svg.selectAll('.line');
marker = svg.selectAll('marker');
// force simulation initialization
var simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody()
.strength(function(d) { return -500;}))
.force("center", d3.forceCenter(width / 2, height / 2));
//eol page id
var eol_id = "328447";
//initialize first graph
initializeGraph(eol_id);
zoom.scaleBy(s.transition().duration(100), 0.81);
function initializeGraph(eol_id){
//calculate prey and predator positions (according to the source node coordinates)
calculatePositions((width-100)/2,(height-100)/2);
//query prey_predator json
d3.json(dataUrl(eol_id), function(err, g) {
if (err) throw err;
graph = g;
dataStored.push(eol_id);
graph.nodes[0].x = (width-100)/2;
graph.nodes[0].y = (height-100)/2;
//initialize the first source node
source_nodes.push(graph.nodes[0]);
//display tooltip
tooltip.style("display", "inline-block")
.style("opacity", .9)
tooltip.html("<p style=\"font-size: 15px; color:"+ color(0)+"; font-style: italic;\"><a href=\"https://eol.org/pages/"+graph.nodes[0].id+"\" style=\"color: black; font-weight: bold; font-size: 15px\" target=\"_blank\">"+graph.nodes[0].label+ "</a><br /><p><strong>source</strong> of "+graph.nodes[0].label+"</p><img src=\""+ graph.nodes[0].icon+ "\" width=\"140\"><p>");
graph.nodes.forEach(n=>{
n.px = n.x;
n.py = n.y;
existing_nodes.push(n);
if(!(nodeIDList.includes(n.id.toString()))){
nodeIDList.push(n.id.toString());}
});
graph.links.forEach(l=>{
existing_links.push(l);
if(!(linkIDList.includes([l.source.toString()+l.target.toString()]))) {
linkIDList.push([l.source.toString()+l.target.toString()]);
}
});
simulation
.nodes(graph.nodes)
simulation.force("link")
.links(graph.links);
toggleVisibilityOfNodesAndLinks(graph, graph.nodes[0]);
updateCoordinates();
updateGraph();
transition=false;
});
}
function calculatePositions (sourceX, sourceY) {
sourcePos.length = 0;
preyPos.length = 0;
predPos.length = 0;
compPos.length = 0;
var add, preyAngle, predAngle;
//set another dimension for padding
var r_width = width-100;
var r_height = height- 100;
//alternative heights (display purpose)
var radius= [height/4, height/4+20];
sourcePos.push(sourceX);
sourcePos.push(sourceY);
for (var i= 0; i< nLimit ; i++) {
if(nLimit == 1){
add = 1/8;
predAngle = (7/6 + add) * Math.PI;
} else {
//add = 1/((nLimit-1)*2);
add = 2/(3*(nLimit-1));
//predAngle = (7/6) * Math.PI;
predAngle = (7/6 + (i)*add) * Math.PI;
}
preyAngle = (1/6 + ((i)*add)) * Math.PI;
preyPos.push([((radius[i%2] * Math.cos(preyAngle)) + sourceX),
((radius[i%2] * Math.sin(preyAngle)) + sourceY)]);
predPos.push ([((radius[i%2] * Math.cos(predAngle)) + sourceX),
((radius[i%2] * Math.sin(predAngle)) + sourceY)]);
}
}
function updateGraph() {
transition = true;
var gColor = ["source", "predator", "prey", "", "", "competitor"];
//copy nodes
var tmp_eNodes = existing_nodes.slice();
var tmp_nNodes = new_nodes.slice();
var tmp_hNodes = hiding_nodes.slice();
var currentNodes = tmp_eNodes.concat(tmp_nNodes);
var tmp_eLinks = existing_links.slice();
var tmp_nLinks = new_links.slice();
//clear previous items
existing_nodes=[];
new_nodes = [];
hiding_nodes = [];
existing_links = [];
new_links = [];
currentNodes.forEach(node => {
if(node.show) {
existing_nodes.push(node);
} else {
hiding_nodes.push(node);
}
});
tmp_hNodes.forEach(node=> {
if(node.show) {
new_nodes.push(node);
} else {
hiding_nodes.push(node);
}
});
graph.links.filter(n => n.show).forEach(l =>{
if(existing_nodes.includes(l.source) && existing_nodes.includes(l.target)){
existing_links.push(l);
} else {
new_links.push(l);
}
});
console.log("existing_nodes", existing_nodes);
console.log("new_nodes", new_nodes);
console.log("existing_links", existing_links);
console.log("(new_links)", new_links);
//EXIT-Remove previous nodes/links
svg.selectAll('line').data(graph.links.filter(n=>{n.show})).exit().remove();
svg.selectAll('.node').data(new_node).exit().remove();
svg.selectAll('.new_node').data(new_node).exit().remove();
svg.selectAll('.existing_node').data(existing_node).exit().remove();
existing_link = svg.selectAll('.line')
.data(existing_links, function(d) { return d.id;})
.enter().append('line')
.attr('class', 'link')
.attr("marker-end", function(d) {
if(source_nodes.includes(d.target)){
return "url(#longer)";
} else {
return "url(#arrow)";
}
})
.attr("x1", function(d) {return d.source.px;})
.attr("y1", function(d) {return d.source.py;})
.attr("x2", function(d) {return d.target.px;})
.attr("y2", function(d) {return d.target.py;});
new_link = svg.selectAll('.new_link')
.data(new_links, function(d) { return d.id;})
.enter().append('line')
.attr('class', 'new_link')
.attr('opacity', 0)
.attr("marker-end", function(d) {
if(source_nodes.includes(d.target)){
return "url(#longer)";
} else {
return "url(#arrow)";
}
})
.attr("x1", function(d) {return d.source.nx;})
.attr("y1", function(d) {return d.source.ny;})
.attr("x2", function(d) {return d.target.nx;})
.attr("y2", function(d) {return d.target.ny;});
console.log("new_nodes", new_nodes);
new_node = svg.selectAll('.new_node')
//UPDATE
.data(new_nodes)
.enter().append('g')
.attr('class', 'new_node')
.attr("id", function(d) {return d.label.replace(/\s/g,'');})
.attr("x", function(d) {return d.fx;})
.attr("y", function(d) {return d.fy;})
.attr("transform", d => `translate(${d.nx},${d.ny})`)
.attr('opacity', 0)
.call(d3.drag()
.subject(function() {
var t = d3.select(this);
var tr = getTranslation(t.attr("transform"));
return {x: t.attr("x") + tr[0],
y: t.attr("y") + tr[1]};
})
.on("drag", function(d,i) {
d3.select(this).attr("transform", function(d,i) {
d.x = d3.event.x;
d.y = d3.event.y;
return "translate(" + [ d3.event.x, d3.event.y ] + ")";});
svg.selectAll('.new_link').data(new_links).filter(l => (l.source === d))
.transition().duration(1).attr("x1", d3.event.x).attr("y1", d3.event.y);
svg.selectAll('.link').data(existing_links).filter(l => (l.source === d))
.transition().duration(1).attr("x1", d3.event.x).attr("y1", d3.event.y);
svg.selectAll('.new_link').data(new_links).filter(l => (l.target === d))
.transition().duration(1).attr("x2", d3.event.x).attr("y2", d3.event.y);
svg.selectAll('.link').data(existing_links).filter(l => (l.target === d))
.transition().duration(1).attr("x2", d3.event.x).attr("y2", d3.event.y);
}));
//APPEND IMAGE
new_node.append("svg:pattern")
.attr("id", function(d) {return d.id.toString();})
.attr("width", "100%")
.attr("height", "100%")
.attr("patternContentUnits", "objectBoundingBox")
.attr("preserveAspectRatio", "xMidYMid slice")
.attr("viewBox", "0 0 1 1")
.append("svg:image")
.attr("xlink:href", function(d) {return d.icon;})
.attr("width", "1")
.attr("height", "1")
.attr("preserveAspectRatio", "xMidYMid slice");
//APPEND CIRCLE
new_node.append('circle')
.attr("r", function(d) {
if(source_nodes.includes (d)){
return source_radius;
} else {
return radius;
}
})
.attr("fill", function(d) {
if (source_nodes.includes (d)) {
return 'url(#'+d.id.toString()+')';
}
else if (d.type == "predator" | d.type =="prey" | d.type =="competitor") {
return color(gColor.indexOf(d.type));
}
else if (d.group%2==0) { return color(1);}
else {return color(2);}
}) ;
new_node.on("click", d => {
appendJSON(d);
})
.on('mouseover.fade', fade(0.1))
.on('mouseout.fade', fade(1))
.on('mouseover.tooltip', function(d) {
tooltip.style("display", "inline-block")
.style("opacity", .9)
tooltip.html("<p style=\"font-size: 15px; color:"+ color(gColor.indexOf(d.type))+"; font-style: italic;\"><a href=\"https://eol.org/pages/"+d.id+"\" style=\"color: black; font-weight: bold; font-size: 15px\" target=\"_blank\">"+d.label+ "</a><br /><p><strong>"+d.type+"</strong> of "+curSource.label+"</p><img src=\""+ d.icon+ "\" width=\"140\"><p>");
});
new_node.append('text')
.attr('x', function(d) {
if (source_nodes.includes(d)){
return 32;
} else {
return 0;
}
})
.attr('y', function(d) {
if(source_nodes.includes(d)){
return 0;
}else {
return 15;
}
})
.attr('dy', '.35em')
.attr("fill", 'black')
.attr("font-family", "verdana")
.attr("font-size", "10px")
.attr("text-anchor",function(d) {
if(source_nodes.includes(d)) {
return "left";
} else {
return "middle";
}
})
.text(function(d) {return d.label;});
existing_node = svg.selectAll('.existing_node')
//UPDATE
.data(existing_nodes)
.enter().append('g')
.attr('class', 'existing_node')
.attr("transform", d => `translate(${d.px},${d.py})`)
.call(d3.drag()
.subject(function() {
var t = d3.select(this);
var tr = getTranslation(t.attr("transform"));
return {x: t.attr("x") + tr[0],
y: t.attr("y") + tr[1]};
})
.on("drag", function(d,i) {
d3.select(this).attr("transform", function(d,i) {
d.x = d3.event.x;
d.y = d3.event.y;
return "translate(" + [ d3.event.x, d3.event.y ] + ")";});
svg.selectAll('.new_link').data(new_links).filter(l => (l.source === d))
.transition().duration(1).attr("x1", d3.event.x).attr("y1", d3.event.y);
svg.selectAll('.link').data(existing_links).filter(l => (l.source === d))
.transition().duration(1).attr("x1", d3.event.x).attr("y1", d3.event.y);
svg.selectAll('.new_link').data(new_links).filter(l => (l.target === d))
.transition().duration(1).attr("x2", d3.event.x).attr("y2", d3.event.y);
svg.selectAll('.link').data(existing_links).filter(l => (l.target === d))
.transition().duration(1).attr("x2", d3.event.x).attr("y2", d3.event.y);
}));
//.on("drag", dragged));
//APPEND IMAGE
existing_node.append("svg:pattern")
.attr("id", function(d) {return d.id.toString();})
.attr("width", "100%")
.attr("height", "100%")
.attr("patternContentUnits", "objectBoundingBox")
.attr("preserveAspectRatio", "xMidYMid slice")
.attr("viewBox", "0 0 1 1")
.append("svg:image")
.attr("xlink:href", function(d) {return d.icon;})
.attr("width", "1")
.attr("height", "1")
.attr("preserveAspectRatio", "xMidYMid slice");
existing_node.append('circle')
.attr("r", function(d) {
if(source_nodes.includes (d)){
return source_radius;
} else {
return radius;
}
})
.attr("fill", function(d) {
if (source_nodes.includes (d)) {
return 'url(#'+d.id.toString()+')';
}
else if (d.type == "predator" | d.type =="prey" | d.type =="competitor") {
return color(gColor.indexOf(d.type));
}
else if (d.group%2==0) { return color(1);}
else {return color(2);}
})
.on('mouseover.fade', fade(0.1))
.on('mouseout.fade', fade(1))
.on('mouseover.tooltip', function(d) {
tooltip.style("display", "inline-block")
.style("opacity", .9)
tooltip.html("<p style=\"font-size: 15px; color:"+ color(gColor.indexOf(d.type))+"; font-style: italic;\"><a href=\"https://eol.org/pages/"+d.id+"\" style=\"color: black; font-weight: bold; font-size: 15px\" target=\"_blank\">"+d.label+ "</a><br /><p><strong>"+d.type+"</strong> of "+curSource.label+"</p><img src=\""+ d.icon+ "\" width=\"140\"><p>");
});
existing_node.append('text')
.attr('x', function(d) {
if (source_nodes.includes(d)){
return 32;
} else {
return 0;
}
})
.attr('y', function(d) {
if(source_nodes.includes(d)){
return 0;
}else {
return 15;
}
})
.attr('dy', '.35em')
.attr("fill", 'black')
.attr("font-family", "verdana")
.attr("font-size", "10px")
.attr("text-anchor",function(d) {
if(source_nodes.includes(d)) {
return "left";
} else {
return "middle";
}
})
.text(function(d) {return d.label;});
existing_node.on("click", d => {appendJSON(d);});
new_node.on("click", d => {appendJSON(d);})
//ANIMATION
//existing nodes stay same & link follows the nodes
svg.selectAll('.existing_node').data(existing_nodes)
.transition().duration(5000).attr("transform", d => `translate(${d.nx},${d.ny})`);
svg.selectAll('.link').data(existing_links)
.transition().duration(5000).attr("x1", function(d) { return d.source.nx; }).attr("y1", function(d) { return d.source.ny; }).attr("x2", function(d) { return d.target.nx; }).attr("y2", function(d) { return d.target.ny; })
//new nodes and links appear after transition
svg.selectAll('.new_node')
.transition().duration(5000).delay(1000).attr("opacity", 1);
svg.selectAll('.new_link').transition().duration(3000).delay(3000).attr("opacity", 1).on('end', function () {transition = false});
simulation
.nodes(graph.nodes)
simulation.force("link")
.links(graph.links);
simulation.alpha(1).alphaTarget(0).restart();
//new coordinate (n.x, n.y) -> past coordinate (p.x, p.y)
updateCoordinates();
}
function getTranslation(transform) {
// Create a dummy g for calculation purposes only. This will never
// be appended to the DOM and will be discarded once this function
// returns.
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
// Set the transform attribute to the provided string value.
g.setAttributeNS(null, "transform", transform);
// consolidate the SVGTransformList containing all transformations
// to a single SVGTransform of type SVG_TRANSFORM_MATRIX and get
// its SVGMatrix.
var matrix = g.transform.baseVal.consolidate().matrix;
// As per definition values e and f are the ones for the translation.
return [matrix.e, matrix.f];
}
function dragged(d, i) {
d3.select(this).attr("transform", function(d,i) {
return "translate(" + [ d3.event.x,d3.event.y ] + ")"});
}
/*
function dragged(d) {
if (!d3.event.active) simulation.alphaTarget(0);
console.log("event", d3.event.x, d3.event.y);
d.x = d3.event.x, d.y = d3.event.y;
console.log("filtered links", existing_link.filter(function(l) { return l.source === d; }));
console.log("filtered links", existing_link.filter(function(l) { return l.target === d; }))
//existing_node.attr("transform", d => `translate(${d3.event.x},${d3.event.y})`)
d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
//d3.select(this).attr("transform", d => `translate(${d3.event.x},${d3.event.y})`);
existing_link.filter(function(l) { return l.source === d; }).attr("x1", d.x).attr("y1", d.y);
existing_link.filter(function(l) { return l.target === d; }).attr("x2", d.x).attr("y2", d.y);
}
*/
function updateCoordinates() {
graph.nodes.forEach(n=> {
n.px = n.nx;
n.py = n.ny;
});
}
//new data
function appendJSON(d) {
var eol_id = d.id.toString();
//http request to JSON data
if(!(dataStored.includes(eol_id))) {
d3.json(dataUrl(eol_id), function(err, g) {
if (err) {alert("No data found!"); throw err;}
g.nodes.forEach(n => {
if(!(nodeIDList.includes(n.id.toString()))) {
//adding new nodes
graph.nodes.push(n);
n.x = 0;
n.y = 0;
n.px = 0;
n.py = 0;
n.nx = 0;
n.ny = 0;
n.show=false;
nodeIDList.push(n.id.toString());
hiding_nodes.push(n);
}
});
g.links.forEach(l=> {
if(!(linkIDList.includes(l.source.toString()+l.target.toString()))) {
graph.links.push(l);
l.show=false;
linkIDList.push(l.source.toString()+l.target.toString());
}
});
simulation
.nodes(graph.nodes)
simulation.force("link")
.links(graph.links);
toggleVisibilityOfNodesAndLinks(graph, d);
updateGraph();
dataStored.push(eol_id);
});
} else {
//already stored data
toggleVisibilityOfNodesAndLinks(graph, d);
updateGraph();
}
}
function toggleVisibilityOfNodesAndLinks (graph,d) {
var preyList = [];
var predList = [];
compList = [];
curSource = addSourceNode(d);
graph.nodes.forEach(node=> {
if (node.id==d.id){
node.type ="source";
node.show = true;
} else if (isConnectedOneWay(d, node) && d.id != node.id){
if (preyList.length < nLimit) {
node.show = true;
node.type = "prey";
preyList.push(node);
} else{
node.show = false;
node.type="none";
}
} else if (isConnectedOneWay(node, d) && d.id != node.id) {
if (predList.length < nLimit) {
node.show=true;
node.type = "predator";
predList.push(node);
} else {
node.show = false;
node.type ="none";
}
}
else {
node.show=false;
node.type="none";
}
});
//competitors
graph.nodes.forEach(node=> {
preyList.forEach(n=>{
if (isConnectedOneWay(node, n) && node.type == "none"){
if (compList.length < 10) {
node.show = true;
node.type = "competitor";
compList.push([node, n] );
} else{
node.show = false;
node.type="none";
}
}
});
});
graph.links.forEach(link => {
if(link.source.show && link.target.show){
link.show = true;
} else {
link.show = false;
}
});
updatePositions((width-100)/2, (height-100)/2);
}
function loadData(eolId, animate) {
//query prey_predator json
d3.json(dataUrl(eolId), function(err, g) {
if (err) throw err;
var prevGraph = graph;
graph = g;
pruneGraph(graph, prevGraph);
updatePositions();
updateGraph(animate);
$dimmer.removeClass('active');
});
}
function dataUrl(pageId) {
return sitePrefix + "/api/pages/" + pageId + "/pred_prey.json"
}
function updatePositions(sourceX, sourceY) {
console.log("update positions")
//make a copy of an array
var tmpPreyPos, tmpPredPos, tmpCompPos;
tmpPreyPos = preyPos.slice();
tmpPredPos = predPos.slice();
graph.nodes.filter(n => n.show).forEach(node => {
if (node.type == "source") {
console.log("source position", sourcePos[0],sourcePos[1])
node.nx = sourcePos[0];
node.ny = sourcePos[1];
}
else if (node.type == "predator") {
var middle = tmpPredPos[Math.floor(tmpPredPos.length/2)];
var index = tmpPredPos.indexOf(middle);
node.nx = middle[0];
node.ny = middle[1];
if (index > -1) {
tmpPredPos.splice(index, 1);
}
} else if (node.type == "prey") {
if(tmpPreyPos.length != 0){
var middle = tmpPreyPos[Math.floor(tmpPreyPos.length/2)];
var index = tmpPreyPos.indexOf(middle);
node.nx = middle[0];
node.ny = middle[1];
if (index > -1) {
tmpPreyPos.splice(index, 1);
}
}}
});
if(compList.length != 0){
var extra = 5;
var gap = (width-100)/(compList.length+extra);
compPos.length = 0;
for(var i = 0; i<compList.length+extra; i++) {
var value = 100 + (i*gap);
compPos.push(value);
}
tmpCompPos = compPos.slice();
for (var i =0; i < extra; i++ ) {
tmpCompPos.splice(Math.floor(tmpCompPos.length/2), 1);
}
var varHeight = -1
compList.forEach(c => {
if(c[1].nx < width/2) {
c[0].nx = tmpCompPos[0];
tmpCompPos.splice(0, 1);
} else {
var endIndex = tmpCompPos.length-1;
c[0].nx = tmpCompPos[endIndex];
tmpCompPos.splice(endIndex, 1);
}
c[0].ny = sourceY+(15*varHeight);
varHeight = varHeight*-1;
});
}
}
function addSourceNode (d) {
//most recent source
var index = source_nodes.length-1;
//the first source node
if (d.id == source_nodes[0].id) {
//remove everything
source_nodes.splice(d);
//put the first source node (reset effect)
source_nodes.push(d);
d.type = "source";
}
//already the source node
else if (source_nodes.includes(d)) {
d.type = "source";
}
else {
source_nodes.push(d);
d.type = "source";
}
return d;
}
function fade(opacity) {
return d => {
if(!(transition)) {
new_node.transition().duration(500).style('stroke-opacity', function (o) {
const thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;});
existing_node.transition().duration(500).style('stroke-opacity', function (o) {
const thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;});
new_link.style('opacity', o => (o.source === d || o.target === d ? 1 : opacity));
existing_link.style('opacity', o => (o.source === d || o.target === d ? 1 : opacity));
}};
}
function isConnected(a, b) {
const linkedByIndex = {};
graph.links.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = 1;
});
return linkedByIndex[`${a.index},${b.index}`] || linkedByIndex[`${b.index},${a.index}`] || a.index === b.index;
}
function isConnectedOneWay(a, b) {
const linkedByIndex = {};
graph.links.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = 1;
});
return linkedByIndex[`${a.index},${b.index}`];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment