D3 v4 forcelayout example with dropdown filters.
forked from mbostock's block: Force Dragging I
forked from almsuarez's block: Force Layout v4 with Filters
forked from almsuarez's block: Force Layout v4 with Filters v2
forked from almsuarez's block: (d3v4) Force Layout w highlights
forked from almsuarez's block: (d3v4) Force Layout w complex nodes
forked from almsuarez's block: (d3v4) Force Layout w curved connections
forked from almsuarez's block: (d3v4) Force Layout w update buttons
xxxxxxxxxx
<meta charset="utf-8">
<style>
.link {
/* stroke: #c1c1c1; */
stroke-width: 2px;
pointer-events: all;
}
.node circle {
pointer-events: all;
stroke: #777;
stroke-width: 1px;
}
path {
fill: none;
stroke-width: 3;
}
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;
}
</style>
<body>
<div id="option">
<input name="updateButton"
type="button"
value="Update"
onclick="updateData()" />
</div>
<br>
<div id="option">
<input name="resetButton"
type="button"
value="Reset"
onclick="resetData()" />
</div>
</body>
<svg width="940" height="700"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<script>
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 currNodes = graph.nodes
var currLinks = graph.links
const svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height');
// const width = 960;
// const height = 700;
var simulation = d3.forceSimulation(currNodes)
//.force('link', d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(currLinks).distance(200))
.force('center', d3.forceCenter(width / 2, height / 2))
//.force('collide', d3.forceCollide(25))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaTarget(1)
.on('tick', ticked);
var R = 8;
var g = svg.append("g")
//.attr("transform", "translate(" + width / 2 + "," + height/ 2 + ")")
//simulation.force('link')
//.links(currLinks);
restart();
//Draw links colored by T
console.log(currLinks.length)
// d3.interval(function (){
// updateData();
// restart();
// console.log(currLinks.length)
// }, 4000, d3.now());
// d3.interval(function() {
// resetData();
// restart();
// }, 4000, d3.now()+2000);
var temp;
function updateData(){
temp = currLinks.pop();
restart()
}
function resetData(){
currNodes = []
currLinks = []
currNodes = graph.nodes
currLinks = graph.links
//currLinks = graph.links
//currLinks.push(temp)
restart()
}
function restart(){
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");
// 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));
});
});
maxSame = _.chain(currLinks)
.sortBy(function(x) {
return x.sameTotal;
})
.last()
.value().sameTotal;
_.each(currLinks, function(link) {
link.maxSameHalf = Math.round(maxSame / 2);
});
///
///NODE UPDATE SECTION
///
//Bind Data
node = node.data(currNodes, function(d) { return d.id;});
//Enter
node.enter().append('circle')
.call(function(node) { node.transition().attr("r", R);})
.attr("fill", function(d) { return "#CCC";}).merge(node)
//Update
node.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");
})
.on('dblclick',releasenode)
.attr('class', 'node')
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));;
//Exit
node.exit().transition()
.attr("r",0)
.remove();
///
///LINK UPDATE SECTION
///
link = link.data(currLinks, function(d) {return d.source.id + "-" + d.target.id;});
link.exit().transition()
.attr("stroke-opacity", 0)
//.attrTween("d", linkArc)
.remove();
// link = g.selectAll('.link')
link.enter().append('path')
.attr('stroke', function(d){return color(d.T);}).merge(link);
//Add mouseover events to links
link.enter().attr('class', 'link')
.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");
});
// .attrTween("x1", function(d) { return function() { return d.source.x; }; })
// .attrTween("x2", function(d) { return function() { return d.target.x; }; })
// .attrTween("y1", function(d) { return function() { return d.source.y; }; })
// .attrTween("y2", function(d) { return function() { return d.target.y; }; })
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);})})
.merge(link);
// copied to here from "Modifying a Force Layout with buttons"
simulation.nodes(currNodes);
simulation.force("link").links(currLinks);
simulation.alpha(1).restart();
}
function ticked() {
// link
// .attr('x1', d => d.source.x)
// .attr('y1', d => d.source.y)
// .attr('x2', d => d.target.x)
// .attr('y2', d => d.target.y);
link.attr("d", linkArc)
node.attr('transform', d => `translate(${d.x},${d.y})`);
//node.attr("cx", function(d) { return d.x; })
//.attr("cy", function(d) { return 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;
}
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;
}
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;
}
//Fade rules for hovering over nodes
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));
};
}
//Specific Fade Rules for Link Hover Instances
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));
}
}
//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>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js
https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js