Skip to content

Instantly share code, notes, and snippets.

@slattery
Forked from XavierGimenez/.block
Last active March 22, 2019 14:51
Show Gist options
  • Save slattery/b3b4cf4ec6ef85983535464f2e3d88db to your computer and use it in GitHub Desktop.
Save slattery/b3b4cf4ec6ef85983535464f2e3d88db to your computer and use it in GitHub Desktop.
Grouping nodes in a Force-Directed Graph (many flavored)
license: gpl-3.0
height: 600

Fork!! Learning FC with grouping.

Example of a force-directed graph with an extra layer for grouping nodes.

Nodes and groups can be dragged indepedently. The type of curve and the scale of the shape can be tuned in order to define the contour of the group shapes

<!DOCTYPE html>
<meta charset='utf-8'>
<style>
body {
font-family: sans-serif, Arial;
font-size: 12px;
font-weight: bold;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
path {
fill-opacity: .1;
stroke-opacity: 1;
}
</style>
<div id="scaleFactorSettings">
<p>Scale of the groups: <span id='scaleFactorLabel'>1.2</span></p>
<input type="range" min="1" max="3" value="1.2" step=".1"
oninput="scaleFactor = value; d3.select('#scaleFactorLabel').text(scaleFactor); updateGroups()">
</div>
<div id="curveSettings">
<p>Type of curve: <span id='curveLabel'>curveCatmullRomClosed</span></p>
</div>
<svg width='400' height='400'></svg>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script>
var svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height'),
color = d3.scaleOrdinal(d3.schemeCategory20),
valueline = d3.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
.curve(d3.curveCatmullRomClosed),
paths,
groups,
groupIds,
scaleFactor = 1.2,
polygon,
centroid,
node,
link,
curveTypes = ['curveBasisClosed', 'curveCardinalClosed', 'curveCatmullRomClosed', 'curveLinearClosed'],
simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d) { return d.id; }))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
d3.json('miserables.json', function(error, graph) {
if (error) throw error;
// create selector for curve types
var select = d3.select('#curveSettings')
.append('select')
.attr('class','select')
.on('change', function() {
var val = d3.select('select').property('value');
d3.select('#curveLabel').text(val);
valueline.curve(d3[val]);
updateGroups();
});
var options = select
.selectAll('option')
.data(curveTypes).enter()
.append('option')
.text(function (d) { return d; });
// create groups, links and nodes
groups = svg.append('g').attr('class', 'groups');
link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(graph.links)
.enter().append('line')
.attr('stroke-width', function(d) { return Math.sqrt(d.value); });
node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(graph.nodes)
.enter().append('circle')
.attr('r', 5)
.attr('fill', function(d) { return color(d.group); })
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
// count members of each group. Groups with less
// than 3 member will not be considered (creating
// a convex hull need 3 points at least)
groupIds = d3.set(graph.nodes.map(function(n) { return +n.group; }))
.values()
.map( function(groupId) {
return {
groupId : groupId,
count : graph.nodes.filter(function(n) { return +n.group == groupId; }).length
};
})
.filter( function(group) { return group.count > 2;})
.map( function(group) { return group.groupId; });
paths = groups.selectAll('.path_placeholder')
.data(groupIds, function(d) { return +d; })
.enter()
.append('g')
.attr('class', 'path_placeholder')
.append('path')
.attr('stroke', function(d) { return color(d); })
.attr('fill', function(d) { return color(d); })
.attr('opacity', 0);
paths
.transition()
.duration(2000)
.attr('opacity', 1);
// add interaction to the groups
groups.selectAll('.path_placeholder')
.call(d3.drag()
.on('start', group_dragstarted)
.on('drag', group_dragged)
.on('end', group_dragended)
);
node.append('title')
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on('tick', ticked)
.force('link')
.links(graph.links);
function ticked() {
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; });
node
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
updateGroups();
}
});
// select nodes of the group, retrieve its positions
// and return the convex hull of the specified points
// (3 points as minimum, otherwise returns null)
var polygonGenerator = function(groupId) {
var node_coords = node
.filter(function(d) { return d.group == groupId; })
.data()
.map(function(d) { return [d.x, d.y]; });
return d3.polygonHull(node_coords);
};
function updateGroups() {
groupIds.forEach(function(groupId) {
var path = paths.filter(function(d) { return d == groupId;})
.attr('transform', 'scale(1) translate(0,0)')
.attr('d', function(d) {
polygon = polygonGenerator(d);
centroid = d3.polygonCentroid(polygon);
// to scale the shape properly around its points:
// move the 'g' element to the centroid point, translate
// all the path around the center of the 'g' and then
// we can scale the 'g' element properly
return valueline(
polygon.map(function(point) {
return [ point[0] - centroid[0], point[1] - centroid[1] ];
})
);
});
d3.select(path.node().parentNode).attr('transform', 'translate(' + centroid[0] + ',' + (centroid[1]) + ') scale(' + scaleFactor + ')');
});
}
// drag nodes
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;
}
// drag groups
function group_dragstarted(groupId) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.select(this).select('path').style('stroke-width', 3);
}
function group_dragged(groupId) {
node
.filter(function(d) { return d.group == groupId; })
.each(function(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
})
}
function group_dragended(groupId) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.select(this).select('path').style('stroke-width', 1);
}
</script>
{
"nodes": [
{"group": 0, "id": "Climate Change and Sustainability", "radius": 15},
{"group": 0, "id": "Conservation", "radius": 3},
{"group": 0, "id": "Environmental Communications", "radius": 6},
{"group": 0, "id": "Environmental Policy, Policy Analysis, and Economics"},
{"group": 0, "id": "Forest Ecology and Management", "radius": 10},
{"group": 0, "id": "Urban Science and Practice", "radius": 6},
{"group":1, "id":"Center for Business and the Environment at Yale"},
{"group":1, "id":"Global Institute of Sustainable Forestry"},
{"group":1, "id":"Hixon Center for Urban Ecology"},
{"group":1, "id":"SEARCH Center"},
{"group":1, "id":"The School Forests"},
{"group":1, "id":"Tropical Resources Institute"},
{"group":1, "id":"Yale Center for Environmental Communication"},
{"group":1, "id":"Yale Center for Environmental Law and Policy"},
{"group":1, "id":"Yale Green Chemistry & Green Engineering"},
{"group":1, "id":"Yale Program on Climate Change Communication"},
{"group":1, "id":"F&ES Climate Initiative"},
{"group":1, "id":"F&ES Sustainability Initiative"},
{"group":1, "id":"Quiet Corner Initiative"},
{"group":1, "id":"Ucross High Plains Stewardship Initiative"},
{"group":1, "id":"Urban Resources Initiative"},
{"group":1, "id":"Urban@Yale Initiative"},
{"group":3, "id":"Business and the Environment"},
{"group":3, "id":"Climate Change Science and Solutions"},
{"group":3, "id":"Ecosystems, Conservation, and Land Management"},
{"group":3, "id":"Energy and the Environment"},
{"group":3, "id":"Environmental Policy Analysis"},
{"group":3, "id":"Industrial Ecology and Green Engineering"},
{"group":3, "id":"Nature and Society"},
{"group":3, "id":"Water Resource Science and Management"},
{"group":1, "id":"The Forests Dialogue"},
{"group":1, "id":"Yale Forest Forum"},
{"group":1, "id":"Environmental Leadership and Training Institute"},
{"group":1, "id":"Sustaining Family Forests Initiative"},
{"group":1, "id":"Yale Climate Connections"}
],
"links": [
{"source": "Climate Change and Sustainability", "target": "Business and the Environment", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Center for Business and the Environment at Yale", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Climate Change Science and Solutions", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Climate Change Science and Solutions", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Energy and the Environment", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Energy and the Environment", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Environmental Policy Analysis", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "F&ES Climate Initiative", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "F&ES Sustainability Initiative", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Industrial Ecology and Green Engineering", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "SEARCH Center", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Yale Center for Environmental Law and Policy", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Yale Climate Connections", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Yale Green Chemistry & Green Engineering", "value": 1 },
{"source": "Climate Change and Sustainability", "target": "Yale Program on Climate Change Communication", "value": 1 },
{"source": "Conservation", "target": "Ecosystems, Conservation, and Land Management", "value": 1 },
{"source": "Conservation", "target": "Ucross High Plains Stewardship Initiative", "value": 1 },
{"source": "Conservation", "target": "Water Resource Science and Management", "value": 1 },
{"source": "Environmental Communications", "target": "Climate Change Science and Solutions", "value": 1 },
{"source": "Environmental Communications", "target": "Environmental Policy Analysis", "value": 1 },
{"source": "Environmental Communications", "target": "Nature and Society", "value": 1 },
{"source": "Environmental Communications", "target": "Yale Center for Environmental Communication", "value": 1 },
{"source": "Environmental Communications", "target": "Yale Climate Connections", "value": 1 },
{"source": "Environmental Communications", "target": "Yale Program on Climate Change Communication", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Ecosystems, Conservation, and Land Management", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Environmental Leadership and Training Institute", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Global Institute of Sustainable Forestry", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Nature and Society", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Quiet Corner Initiative", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Sustaining Family Forests Initiative", "value": 1 },
{"source": "Forest Ecology and Management", "target": "The Forests Dialogue", "value": 1 },
{"source": "Forest Ecology and Management", "target": "The School Forests", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Tropical Resources Institute", "value": 1 },
{"source": "Forest Ecology and Management", "target": "Yale Forest Forum", "value": 1 },
{"source": "Urban Science and Practice", "target": "Climate Change Science and Solutions", "value": 1 },
{"source": "Urban Science and Practice", "target": "Hixon Center for Urban Ecology", "value": 1 },
{"source": "Urban Science and Practice", "target": "Nature and Society", "value": 1 },
{"source": "Urban Science and Practice", "target": "Urban Resources Initiative", "value": 1 },
{"source": "Urban Science and Practice", "target": "Urban@Yale Initiative", "value": 1 },
{"source": "Urban Science and Practice", "target": "Water Resource Science and Management", "value": 1}
]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment