Skip to content

Instantly share code, notes, and snippets.

@almsuarez
Last active February 27, 2018 02:19
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 almsuarez/fc04cba1594938e5a10017af8a88d3af to your computer and use it in GitHub Desktop.
Save almsuarez/fc04cba1594938e5a10017af8a88d3af to your computer and use it in GitHub Desktop.
Checkbox Interactive forcelayout
license: gpl-3.0

This example demonstrates how to add and remove nodes and links from a force-directed layout. The graph initially appears with three disconnected nodes A, B and C. After one second, the three are connected in a loop. At two seconds, node C is removed, along with the links A-C and B-C. At three seconds, node C is reintroduced, restoring the original links A-C and B-C. Every subsequent second alternates between these two steps.

This example uses the general update pattern for data joins. See also modifying a force layout with transitions.

forked from mbostock's block: Modifying a Force Layout

forked from almsuarez's block: Checkbox Interactive forcelayout

<!DOCTYPE html>
<style>
div.tooltip {
position: absolute;
background-color: white;
max-width; 200px;
height: auto;
padding: 1px;
border-style: solid;
border-radius: 4px;
border-width: 1px;
box-shadow: 3px 3px 10px rgba(0, 0, 0, .5);
pointer-events: none;
}
/* .link {
stroke-width: 2px;
pointer-events: all;
} */
.node circle {
pointer-events: all;
stroke: #777;
stroke-width: 1px;
}
path {
fill: none;
stroke-width: 4px;
}
</style>
<body>
<h1>T Filter</h1>
<!-- <div id="option">
<input name="updateButton"
type="button"
value="Update"
onclick="updateData()" />
</div> -->
<input type="checkbox" class="myCheckbox" value="Am" checked="checked"> Am
<input type="checkbox" class="myCheckbox" value="An" checked="checked"> An
<input type="checkbox" class="myCheckbox" value="Ar" checked="checked"> Ar
<input type="checkbox" class="myCheckbox" value="As" checked="checked"> As
<input type="checkbox" class="myCheckbox" value="Ba" checked="checked"> Ba
<input type="checkbox" class="myCheckbox" value="Br" checked="checked"> Br
<input type="checkbox" class="myCheckbox" value="Ch" checked="checked"> Ch
<input type="checkbox" class="myCheckbox" value="Cn" checked="checked"> Cn
<br>
<input type="checkbox" class="myCheckbox" value="Cy" checked="checked"> Cy&nbsp
<input type="checkbox" class="myCheckbox" value="Gl" checked="checked"> Gl&nbsp
<input type="checkbox" class="myCheckbox" value="Gy" checked="checked"> Gy&nbsp
<input type="checkbox" class="myCheckbox" value="Li" checked="checked"> Li
<input type="checkbox" class="myCheckbox" value="Os" checked="checked"> Os
<input type="checkbox" class="myCheckbox" value="Pr" checked="checked"> Pr,
<input type="checkbox" class="myCheckbox" value="Vi" checked="checked"> Vi
<input type="checkbox" class="myCheckbox" value="Zy" checked="checked"> Zy
<br>
<div id="option">
<input name="resetButton"
type="button"
value="Reset"
onclick="resetData()" />
</div>
</body>
<svg width="960" height="900"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//color = d3.scaleOrdinal(d3.schemeCategory10);
var colorScale = d3.schemeCategory20
var color = d3.scaleOrdinal(colorScale).domain(['Am',
'An',
'Ar',
'As',
'Ba',
'Br',
'Ch',
'Cn',
'Cy',
'Gl',
'Gy',
'Li',
'Os',
'Pr',
'Vi',
'Zy']);
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var graph = {
nodes: [
{"T":["Ba","Ar"],"Q":["sp","sp"],"id":0},
{"T":["As","Zy","Gy"],"Q":["sp","fu","la"],"id":1},
{"T":["Ch"],"Q":["nu","ni","tr"],"id":2},
{"T":["An","Gy"],"Q":["fu","va","ec","cl","la","pl"],"id":3},
{"T":["As","Gl","An"],"Q":["sp","fu","va","cl","la"],"id":4},
{"T":["Ba","Ar"],"Q":["me","fu","ec","sp","sp"],"id":5},
{"T":["As","Li","Br","An","Gy"],"Q":["sp","fu","va","sp","la"],"id":6},
{"T":["Pr","Cy","Ch"],"Q":["nu","sp","fu","va","pl"],"id":7},
{"T":["Ba","Ar"],"Q":["sp","fu","va","di","ge"],"id":8},
{"T":["Ar","Os","Am"],"Q":["sp","va","ec","ec","cl","tr","la"],"id":9},
{"T":["Vi","Cn"],"Q":["sp","fu","di","sp","ge"],"id":10},
{"T":["An"],"Q":["sp","ni","fu","va","cl","di","tr"],"id":11},
{"T":["Ar","Ch"],"Q":["me","nu","sp","va","sp"],"id":12},
{"T":["Vi","Ar","Pr"],"Q":["sp","fu","sp"],"id":13},
{"T":["An","Gy"],"Q":["sp","ni","va","cl","tr","la","sp","na"],"id":14},
{"T":["Ba","Am"],"Q":["sp","fu","sp","di","ge"],"id":15},
{"T":["Vi","Ar","Ba","Cy","Ch","Ch"],"Q":["nu","sp","fu","va","di"],"id":16},
{"T":["An","Gy"],"Q":["sp","di"],"id":17},{"T":["An","Ar"],"Q":["sp","sp","tr","ad"],"id":18},
{"T":["Ch","Ar"],"Q":["ni","fu","va","cl","sp","la"],"id":19}
],
links : [
{"source":0.0,"target":5.0,"T":"Ba"},{"source":8.0,"target":15.0,"T":"Ba"},
{"source":8.0,"target":16.0,"T":"Ba"},{"source":15.0,"target":5.0,"T":"Ba"},
{"source":1.0,"target":3.0,"T":"Gy"},{"source":1.0,"target":6.0,"T":"Gy"},
{"source":1.0,"target":14.0,"T":"Gy"},{"source":1.0,"target":17.0,"T":"Gy"},
{"source":3.0,"target":6.0,"T":"Gy"},{"source":3.0,"target":14.0,"T":"Gy"},
{"source":3.0,"target":17.0,"T":"Gy"},{"source":6.0,"target":14.0,"T":"Gy"},
{"source":6.0,"target":17.0,"T":"Gy"},{"source":14.0,"target":17.0,"T":"Gy"},
{"source":7.0,"target":13.0,"T":"Pr"},{"source":0,"target":5.0,"T":"Ch"},
{"source":2.0,"target":12.0,"T":"Ch"},{"source":2.0,"target":12.0,"T":"Br"},
{"source":0.0,"target":5.0,"T":"Ba"},{"source":0.0,"target":5.0,"T":"Gy"},
{"source":0.0,"target":5.0,"T":"Br"}
]
}
// var a = {id: "a"},
// b = {id: "b"},
// c = {id: "c"},
// nodes = [a, b, c],
// links = [];
var origNodes = graph.nodes;
var origLinks = graph.links;
var simulation = d3.forceSimulation(origNodes)
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(origLinks).distance(200))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaTarget(1)
.on("tick", ticked);
var g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
link = g.append("g")
//.attr("stroke", "#000")
//.attr("stroke-width", 1.5)
.selectAll(".link"),
node = g.append("g")
//.attr("stroke", "#fff")
//.attr("stroke-width", 1.5)
.selectAll(".node");
currNodes = origNodes
currLinks = origLinks
restart();
d3.selectAll(".myCheckbox").on("change", updateData);
//Test for multiple criteria from jherax/f11d669ba286f21b7a2dcff69621eb72
function multiFilter(array, filters) {
const filterKeys = Object.keys(filters);
// filters all elements passing the criteria
return array.filter((item) => {
// dynamically validate all filter criteria
return filterKeys.every(key => !!~filters[key].indexOf(item[key]));
});
}
function updateData(){
//only return Links where the link Taxa is "Ba"
//Look into multiple filter github:
//https://stackoverflow.com/questions/43204972/filter-javascript-array-on-multiple-conditions
//success!
//filters = ["Ar", "Gy"]
currLinks = origLinks
currNodes = origNodes
filters = [];
d3.selectAll(".myCheckbox").each(function(d){
cb = d3.select(this);
if(cb.property("checked")){
filters.push(cb.property("value"));
}
})
if(filters.length > 0){
currLinks = currLinks.filter(el => //for each element in currLinks
filters.find(ele=> //for each element in filters
el.T.indexOf(ele) > -1 )
!== undefined );
currNodes = currNodes.filter(el=> //for each el in currNodes
el.T.find(ele => //for each ele in T
filters.find(elem=> //each elem in filters
ele.indexOf(elem) > -1 )
!== undefined )
!== undefined )
}
// currLinks = currLinks.filter(function (el) {
// return (el.T === "Ba" || el.T === "Gy");
// })
// currNodes = currNodes.filter(function (el) {
// return (el.T.indexOf("Ba") > -1 || el.T.indexOf("Gy") > -1)
// })
// Try using flex filter from https://github.com/halshing/Playground/blob/master/ArrayOfObjectsFilter/Filter.js
//currNodes.push({"id":31});
//currNodes.push({"id":32})
restart()
}
function resetData(){
currLinks = origLinks
currNodes = origNodes
d3.selectAll('.myCheckbox').property('checked', "checked")
//alert(currLinks === graph.links);
//currLinks = graph.links
//currLinks.push(temp)
restart()
}
function restart() {
// Generate Statitics on links
_.each(currLinks, function(link) {
// find other links with same target+source or source+target
var same = _.where(currLinks, {
'source': link.source,
'target': link.target
});
var sameAlt = _.where(currLinks, {
'source': link.target,
'target': link.source
});
var sameAll = same.concat(sameAlt);
_.each(sameAll, function(s, i) {
s.sameIndex = (i + 1);
s.sameTotal = sameAll.length;
s.sameTotalHalf = (s.sameTotal / 2);
s.sameUneven = ((s.sameTotal % 2) !== 0);
s.sameMiddleLink = ((s.sameUneven === true) &&
(Math.ceil(s.sameTotalHalf) === s.sameIndex));
s.sameLowerHalf = (s.sameIndex <= s.sameTotalHalf);
s.sameArcDirection = s.sameLowerHalf ? 0 : 1;
s.sameIndexCorrected = s.sameLowerHalf ? s.sameIndex : (s.sameIndex - Math.ceil(s.sameTotalHalf));
});
});
if (currLinks.length > 0){
var maxSame = _.chain(currLinks)
.sortBy(function(x) {
return x.sameTotal;
})
.last()
.value().sameTotal;
}
_.each(currLinks, function(link) {
link.maxSameHalf = Math.round(maxSame / 2);
});
// Apply the general update pattern to the nodes.
// Data Join
node = node.data(currNodes, function(d) { return d.id;});
// Exit
node.exit().remove();
node = node.enter().append("circle").attr("fill", function(d) {
return color(d.T); }).attr("r", 8)
.on('mouseover.tooltip', function(d) {
tooltip.transition()
.duration(300)
.style("opacity", .8);
tooltip.html("Project:" + d.id + "<p/>T:" + d.T + "<p/>Q:" + d.Q)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY + 10) + "px");
})
.on('mouseover.fade', fade(0.1))
.on("mouseout.tooltip", function() {
tooltip.transition()
.duration(100)
.style("opacity", 0);
})
.on('mouseout.fade', fade(1))
.on("mousemove", function() {
tooltip.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY + 10) + "px");
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('dblclick',releasenode)
.merge(node);
// Apply the general update pattern to the links.
//Data Join
link = link.data(currLinks, function(d) { return d.source.id + "-" + d.target.id; });
//Exit
link.exit().remove();
//Update+Merge
link = link.enter().append('path')
.call(function(link) {link.transition().attr("stroke-opacity", 1);})
.call(function(link) {link.transition().attr("d", linkArc)})
.call(function(link) {link.transition().attr('stroke', function(d){return color(d.T);})})
.on('mouseover.fade', linkFade(0.1))
.on('mouseover.tooltip', function(d) {
tooltip.transition()
.duration(300)
.style("opacity", .8);
tooltip.html("Source:"+ d.source.id +
"<p/>Target:" + d.target.id +
"<p/>T:" + d.T)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY + 10) + "px");
})
.on("mouseout.tooltip", function() {
tooltip.transition()
.duration(100)
.style("opacity", 0);
})
.on('mouseout.fade', linkFade(1))
.on("mousemove", function() {
tooltip.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY + 10) + "px");
})
.merge(link);
// Update and restart the simulation.
simulation.nodes(currNodes);
simulation.force("link").links(currLinks);
simulation.alpha(1).restart();
}
function ticked() {
// node.attr("cx", function(d) { return d.x; })
// .attr("cy", function(d) { return d.y; })
// link.attr("x1", function(d) { return d.source.x; })
// .attr("y1", function(d) { return d.source.y; })
// .attr("x2", function(d) { return d.target.x; })
// .attr("y2", function(d) { return d.target.y; });
link.attr("d", linkArc)
node.attr('transform', d => `translate(${d.x},${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(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;
}
function releasenode(d) {
d.fx = null;
d.fy = null;
}
const linkedByIndex = {};
currLinks.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = 1;
});
function isConnected(a, b) {
return linkedByIndex[`${a.index},${b.index}`] || linkedByIndex[`${b.index},${a.index}`] || a.index === b.index;
}
function fade(opacity) {
return d => {
node.style('stroke-opacity', function (o) {
const thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style('stroke-opacity', o => (o.source === d || o.target === d ? 1 : opacity));
};
}
function linkFade(opacity) {
return d => {
node.style('stroke-opacity', function(o){
const thisOpacity = isConnected(d.source, o) && isConnected(d.target, o)? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style('stroke-opacity', o => (o.source === d.source && o.target === d.target ? 1 : opacity));
}
}
function linkArc(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy),
unevenCorrection = (d.sameUneven ? 0 : 0.5);
// curvature term defines how tight the arcs are (lower number = tigher curve)
var curvature = 2,
arc = (1.0/curvature)*((dr * d.maxSameHalf) / (d.sameIndexCorrected - unevenCorrection));
//console.log(d.maxSameHalf)
//d.maxSameHalf always showing zero...
if (d.sameMiddleLink) {
arc = 0;
}
return "M" + d.source.x + "," + d.source.y + "A" + arc + "," + arc + " 0 0," + d.sameArcDirection + " " + d.target.x + "," + d.target.y;
}
///
///LEGEND DETAILS
///
//Legend Details
var sequentialScale = d3.scaleOrdinal(colorScale)
.domain(['Am',
'An',
'Ar',
'As',
'Ba',
'Br',
'Ch',
'Cn',
'Cy',
'Gl',
'Gy',
'Li',
'Os',
'Pr',
'Vi',
'Zy']);
svg.append("g")
.attr("class", "legendSequential")
.attr("transform", "translate("+(width-140)+","+(height-400)+")");
var legendSequential = d3.legendColor()
.shapeWidth(30)
.cells(11)
.orient("vertical")
.title("Link legend:")
.titleWidth(100)
.scale(sequentialScale)
svg.select(".legendSequential")
.call(legendSequential);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment