Skip to content

Instantly share code, notes, and snippets.

@XavierGimenez
Last active May 17, 2017 12:52
Show Gist options
  • Save XavierGimenez/c8d24b2d2da3181455aa4607e266c347 to your computer and use it in GitHub Desktop.
Save XavierGimenez/c8d24b2d2da3181455aa4607e266c347 to your computer and use it in GitHub Desktop.
Network with multicategorical nodes

Force-directed graph where a set of qualifiers can be added each node. Useful for cases when the nodes of the network are multicategorical and this has to be represented somehow. Data is a subset of the character coappearence in Les Misérables.

Since the symbols used are external svgs the qualifier can be any visual. Regarding these external svg files some assumptions are made, such having a single 'g' acting as placeholder and having the content within the placeholder centereed around the 0,0 of the 'g' (As these svg are translated and scaled, it is important not to accumulate matrix transformations on the same element in order to achieve the desired effect).

Related bl.ocks

Mike Bostock's example for a force directed graph and Steve Haroz's example for a d3-force testing ground

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<meta charset='utf-8'>
<canvas width='1' height='1'></canvas>
<style>
body {
display: flex;
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
font-size: 14px;
width : 100%;
}
.controls {
flex-basis: 300px;
padding: 0 5px;
}
.controls .setting {
background-color: #eee;
border-radius: 3px;
margin: 5px 0;
padding: 5px;
}
svg {
flex-basis: 100%;
min-width: 200px;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
fill: 'tomato';
stroke: #fff;
stroke-width: 1.5px;
}
g.qualifier circle {
fill: cornsilk;
stroke: #333;
stroke-width: 6px;
}
</style>
<script src='https://d3js.org/d3.v4.min.js'></script>
<body>
<div class='controls'>
<div class='setting'>
<p>
<label for='nScale'>scale of qualifiers= <span id='scale-factor-value'></span></label>
<input type='range' min='0.1' max='0.5' step='0.01' id='scale-factor'>
</p>
</div>
<div class='setting'>
<p>
<label for='nAngle'>angle between qualifiers = <span id='angle-inc-value'></span></label>
<input type='range' min='10' max='45' id='angle-inc'>
</p>
</div>
<div class='setting'>
<p>
<label for='nLinkDistance'>Force link distance = <span id='link-distance-value'></span></label>
<input type='range' min='50' max='500' id='link-distance'>
</p>
</div>
</div>
<svg>
</svg>
</body>
<script>
var margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 760 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom,
initialPosition = {x: width / 2, y : height / 2 };
var svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var simulation = d3.forceSimulation()
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
var propertySymbolFiles = [
'item-bell.svg',
'item-a.svg',
'item-bolt.svg',
'item-b.svg',
'item-certificate.svg',
'item-clock.svg',
'item-exclamation.svg',
'item-c.svg',
'item-d.svg',
'item-e.svg'],
getRandomInt = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
propertySymbolSVGs = [],
degreeToRadians = Math.PI / 180,
nodes,
config = {
linkDistance:350,
propertyScaleFactor : .2, //scale factor relative to node size
radius : 3,
angleInitial : -45,
angleIncrement : 45
};
var draw = function(graph) {
var 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); });
nodes = svg.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(graph.nodes)
.enter().append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}));
nodes.append('circle')
.attr('r', function() { return getRandomInt(5, 50);})
.attr('fill', 'tomato');
nodes.append('title')
.text(function(d) { return d.id; });
var qualifiers = nodes
.selectAll('.qualifier')
.data(function(d) { return d.qualifiers; })
.enter().append('g')
.attr('class', 'qualifier');
qualifiers.each(function(qualifier) {
d3.select(this).node().appendChild(qualifier.cloneNode(true));
});
update(qualifiers);
simulation
.nodes(graph.nodes)
.on('tick', 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; });
nodes.attr('transform', function(d) {return 'translate(' + d.x + ',' + d.y + ')';});
})
.force('link')
.links(graph.links);
};
var update = function(qualifier) {
var item_radius,
qualifier_size;
qualifier.attr('transform', function(d, i) {
item_radius = +d3.select(this.parentElement).select('circle').attr('r');
return 'translate(' +
item_radius * Math.cos((config.angleInitial + (config.angleIncrement * i)) * degreeToRadians) + ',' +
item_radius * Math.sin((config.angleInitial + (config.angleIncrement * i)) * degreeToRadians) + ')';
});
qualifier.selectAll('g')
.attr('transform', function(d, i) {
item_radius = +d3.select(this.parentElement.parentElement).select('circle').attr('r');
qualifier_size = (item_radius*2) * config.propertyScaleFactor;
return 'scale(' + (qualifier_size / this.getBBox().width) + ')';
});
}
function updateScale(propertyScaleFactor) {
d3.select('#scale-factor-value').text(propertyScaleFactor);
d3.select('#scale-factor').property('value', propertyScaleFactor);
}
function updateAngleInc(angleIncrement) {
d3.select('#angle-inc-value').text(angleIncrement);
d3.select('#angle-inc').property('value', angleIncrement);
}
function updateLinkDistance(linkDistance) {
d3.select('#link-distance-value').text(linkDistance);
d3.select('#link-distance').property('value', linkDistance);
}
simulation.force('link')
.id(function(d) {return d.id;})
.distance(config.linkDistance);
d3.select('#scale-factor').on('input', function() {
config.propertyScaleFactor = +this.value;
updateScale(config.propertyScaleFactor);
update(nodes.selectAll('.qualifier'));
});
d3.select('#angle-inc').on('input', function() {
config.angleIncrement = +this.value;
updateAngleInc(config.angleIncrement);
update(nodes.selectAll('.qualifier'));
});
d3.select('#link-distance').on('input', function() {
config.linkDistance = +this.value;
updateLinkDistance(config.linkDistance);
simulation.force('link').distance(config.linkDistance);
simulation.alpha(1).restart();
});
updateScale(config.propertyScaleFactor);
updateAngleInc(config.angleIncrement);
updateLinkDistance(config.linkDistance);
var q = d3.queue();
propertySymbolFiles.forEach(function(propertySymbolFile) {
q.defer(d3.xml, propertySymbolFile);
});
q.awaitAll(function(error, files) {
if (error) throw error;
files.forEach(function(file) {
// assuming that every svg file contains a single child, a 'g'
// node containing the visuals, get this placeholder
propertySymbolSVGs.push(file.getElementsByTagName('svg')[0].getElementsByTagName('g')[0]);
});
//load network and add a set of qualifiers for each node.
// each qualifier is a visual symbol
d3.json('miserables.json', function(e, graph) {
graph.nodes.forEach(function(node) {
var length = getRandomInt(1,8);
node.qualifiers = []
for(var i=0; i<length; i++)
node.qualifiers.push(propertySymbolSVGs[getRandomInt(0,propertySymbolSVGs.length-1)]);
});
if(e) throw e;
draw(graph);
})
});
</script>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 56.8 56.8" style="enable-background:new 0 0 56.8 56.8;" xml:space="preserve">
<g>
<circle cx="0" cy="0" r="28.4" fill="tomato"/>
<path fill="black" stroke="black" d="M0.3,13.9c0-0.1-0.1-0.3-0.2-0.3c0,0,0,0-0.1,0c-0.7,0-1.3-0.3-1.8-0.7c-0.5-0.5-0.7-1.1-0.7-1.8
c0-0.1-0.1-0.3-0.2-0.3c0,0,0,0-0.1,0c-0.1,0-0.3,0.1-0.3,0.2c0,0,0,0,0,0.1c0,0.8,0.3,1.6,0.9,2.2c0.6,0.6,1.4,0.9,2.2,0.9
C0.1,14.2,0.3,14.1,0.3,13.9C0.3,14,0.3,14,0.3,13.9z M-11.3,8.9h22.7C8.2,5.4,6.7,0.6,6.7-5.6c0-0.6-0.2-1.3-0.4-1.8
C6-8.1,5.6-8.7,5.1-9.2C4.5-9.8,3.8-10.3,3-10.6c-1-0.4-2-0.6-3-0.6c-1,0-2,0.2-3,0.6c-0.8,0.3-1.5,0.8-2.1,1.4
C-5.6-8.7-6-8.1-6.3-7.4c-0.3,0.6-0.4,1.2-0.4,1.8C-6.7,0.6-8.2,5.4-11.3,8.9L-11.3,8.9z M14.5,8.9c0,1.2-1,2.2-2.2,2.2H4.5
c0,1.2-0.5,2.3-1.3,3.1c-0.8,0.8-2,1.3-3.2,1.3c-1.2,0-2.3-0.5-3.2-1.3c-0.9-0.8-1.3-2-1.3-3.1h-7.8c-0.6,0-1.2-0.2-1.6-0.7
c-0.4-0.4-0.7-1-0.7-1.6c0.6-0.5,1.1-1,1.6-1.5c0.6-0.6,1.1-1.3,1.5-2.1c0.5-0.9,1-1.8,1.3-2.8C-9.7,1.4-9.4,0.2-9.3-1
C-9-2.5-8.9-4-8.9-5.5c0-1.8,0.7-3.6,2-4.9c1.4-1.5,3.3-2.5,5.3-2.8c-0.1-0.2-0.1-0.4-0.1-0.7c0-0.4,0.2-0.9,0.5-1.2
c0.3-0.3,0.8-0.5,1.2-0.5c0.4,0,0.9,0.2,1.2,0.5c0.3,0.3,0.5,0.7,0.5,1.2c0,0.2,0,0.5-0.1,0.7c2.1,0.2,4,1.2,5.3,2.8
c1.3,1.3,2,3.1,2,4.9C8.9-4,9-2.5,9.3-1c0.2,1.2,0.5,2.4,0.9,3.6c0.3,1,0.8,1.9,1.3,2.8c0.4,0.7,0.9,1.4,1.5,2.1
C13.4,8,13.9,8.5,14.5,8.9L14.5,8.9z"/>
</g>
</svg>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
{
"nodes": [
{"id": "Myriel", "group": 1},
{"id": "Napoleon", "group": 1},
{"id": "Mlle.Baptistine", "group": 1},
{"id": "Mme.Magloire", "group": 1},
{"id": "CountessdeLo", "group": 1},
{"id": "Geborand", "group": 1},
{"id": "Champtercier", "group": 1},
{"id": "Cravatte", "group": 1},
{"id": "Count", "group": 1},
{"id": "OldMan", "group": 1},
{"id": "Labarre", "group": 2},
{"id": "Valjean", "group": 2},
{"id": "Marguerite", "group": 3},
{"id": "Mme.deR", "group": 2},
{"id": "Isabeau", "group": 2},
{"id": "Gervais", "group": 2},
{"id": "Tholomyes", "group": 3},
{"id": "Listolier", "group": 3},
{"id": "Fameuil", "group": 3},
{"id": "Blacheville", "group": 3},
{"id": "Favourite", "group": 3},
{"id": "Dahlia", "group": 3},
{"id": "Zephine", "group": 3},
{"id": "Fantine", "group": 3},
{"id": "Mme.Thenardier", "group": 4},
{"id": "Thenardier", "group": 4},
{"id": "Cosette", "group": 5},
{"id": "Javert", "group": 4}
],
"links": [
{"source": "Napoleon", "target": "Myriel", "value": 1},
{"source": "Mlle.Baptistine", "target": "Myriel", "value": 8},
{"source": "Mme.Magloire", "target": "Myriel", "value": 10},
{"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6},
{"source": "CountessdeLo", "target": "Myriel", "value": 1},
{"source": "Geborand", "target": "Myriel", "value": 1},
{"source": "Champtercier", "target": "Myriel", "value": 1},
{"source": "Cravatte", "target": "Myriel", "value": 1},
{"source": "Count", "target": "Myriel", "value": 2},
{"source": "OldMan", "target": "Myriel", "value": 1},
{"source": "Valjean", "target": "Labarre", "value": 1},
{"source": "Valjean", "target": "Mme.Magloire", "value": 3},
{"source": "Valjean", "target": "Mlle.Baptistine", "value": 3},
{"source": "Valjean", "target": "Myriel", "value": 5},
{"source": "Marguerite", "target": "Valjean", "value": 1},
{"source": "Mme.deR", "target": "Valjean", "value": 1},
{"source": "Isabeau", "target": "Valjean", "value": 1},
{"source": "Gervais", "target": "Valjean", "value": 1},
{"source": "Listolier", "target": "Tholomyes", "value": 4},
{"source": "Fameuil", "target": "Tholomyes", "value": 4},
{"source": "Fameuil", "target": "Listolier", "value": 4},
{"source": "Blacheville", "target": "Tholomyes", "value": 4},
{"source": "Blacheville", "target": "Listolier", "value": 4},
{"source": "Blacheville", "target": "Fameuil", "value": 4},
{"source": "Favourite", "target": "Tholomyes", "value": 3},
{"source": "Favourite", "target": "Listolier", "value": 3},
{"source": "Favourite", "target": "Fameuil", "value": 3},
{"source": "Favourite", "target": "Blacheville", "value": 4},
{"source": "Dahlia", "target": "Tholomyes", "value": 3},
{"source": "Dahlia", "target": "Listolier", "value": 3},
{"source": "Dahlia", "target": "Fameuil", "value": 3},
{"source": "Dahlia", "target": "Blacheville", "value": 3},
{"source": "Dahlia", "target": "Favourite", "value": 5},
{"source": "Zephine", "target": "Tholomyes", "value": 3},
{"source": "Zephine", "target": "Listolier", "value": 3},
{"source": "Zephine", "target": "Fameuil", "value": 3},
{"source": "Zephine", "target": "Blacheville", "value": 3},
{"source": "Zephine", "target": "Favourite", "value": 4},
{"source": "Zephine", "target": "Dahlia", "value": 4},
{"source": "Fantine", "target": "Tholomyes", "value": 3},
{"source": "Fantine", "target": "Listolier", "value": 3},
{"source": "Fantine", "target": "Fameuil", "value": 3},
{"source": "Fantine", "target": "Blacheville", "value": 3},
{"source": "Fantine", "target": "Favourite", "value": 4},
{"source": "Fantine", "target": "Dahlia", "value": 4},
{"source": "Fantine", "target": "Zephine", "value": 4},
{"source": "Fantine", "target": "Marguerite", "value": 2},
{"source": "Fantine", "target": "Valjean", "value": 9},
{"source": "Mme.Thenardier", "target": "Fantine", "value": 2},
{"source": "Mme.Thenardier", "target": "Valjean", "value": 7},
{"source": "Thenardier", "target": "Mme.Thenardier", "value": 13},
{"source": "Thenardier", "target": "Fantine", "value": 1},
{"source": "Thenardier", "target": "Valjean", "value": 12},
{"source": "Cosette", "target": "Mme.Thenardier", "value": 4},
{"source": "Cosette", "target": "Valjean", "value": 31},
{"source": "Cosette", "target": "Tholomyes", "value": 1},
{"source": "Cosette", "target": "Thenardier", "value": 1},
{"source": "Javert", "target": "Valjean", "value": 17},
{"source": "Javert", "target": "Fantine", "value": 5}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment