Skip to content

Instantly share code, notes, and snippets.

@shreyasbharath
Created March 29, 2018 06:41
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 shreyasbharath/11a05f099a37d7eece554f83d2209d58 to your computer and use it in GitHub Desktop.
Save shreyasbharath/11a05f099a37d7eece554f83d2209d58 to your computer and use it in GitHub Desktop.
Graph comparison
license: mit

A simple method for comparing small, similar graphs. Two linked views are presented, showing differences as "phantom" nodes and links. The views share the same layout, ensuring comparability.

forked from nitaku's block: Graph comparison

graph = {
nodes: [
{id: 'A', graphs:['I','II']},
{id: 'B', graphs:['II']},
{id: 'C', graphs:['I','II']},
{id: 'D', graphs:['I']},
{id: 'E', graphs:['II']},
{id: 'F', graphs:['I','II']},
{id: 'G', graphs:['I','II']},
{id: 'H', graphs:['I','II']},
{id: 'I', graphs:['I','II']},
{id: 'J', graphs:['I']}
],
links: [
{id: 1, source: 'A', target: 'B', graphs:['II']},
{id: 2, source: 'A', target: 'C', graphs:['I','II']},
{id: 3, source: 'A', target: 'D', graphs:['I']},
{id: 4, source: 'B', target: 'E', graphs:['II']},
{id: 5, source: 'B', target: 'F', graphs:['II']},
{id: 6, source: 'C', target: 'G', graphs:['I','II']},
{id: 7, source: 'C', target: 'F', graphs:['I','II']},
{id: 8, source: 'F', target: 'G', graphs:['I','II']},
{id: 9, source: 'G', target: 'H', graphs:['I','II']},
{id: 10, source: 'G', target: 'I', graphs:['I','II']},
{id: 11, source: 'H', target: 'I', graphs:['I','II']},
{id: 12, source: 'I', target: 'J', graphs:['I']}
]}
### objectify the graph ###
### resolve node IDs (not optimized at all!) ###
for l in graph.links
for n in graph.nodes
if l.source is n.id
l.source = n
if l.target is n.id
l.target = n
R = 18
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
defs = svg.append('defs')
### define arrow markers for graph links ###
defs.append('marker')
.attr
id: 'end-arrow'
viewBox: '0 0 10 10'
refX: 4+R
refY: 5
orient: 'auto'
.append('path')
.attr
d: 'M0,0 L0,10 L10,5 z'
defs.append('marker')
.attr
id: 'phantom-end-arrow'
viewBox: '0 0 10 10'
refX: 4+R
refY: 5
orient: 'auto'
.append('path')
.attr
d: 'M0,0 L0,10 L10,5 z'
### create views ###
defs.append('clipPath')
.attr
id: 'square_window'
.append('rect')
.attr
x: 0
y: 0
width: width/2
height: height
views_data = ['I','II']
views = svg.selectAll('.view')
.data(views_data)
enter_views = views.enter().append('g')
.attr
class: 'view'
'clip-path': 'url(#square_window)'
transform: (d) -> if d is 'II' then "translate(#{width/2},0)" else 'translate(0,0)'
svg.append('line')
.attr
class: 'separator'
x1: width/2
y1: 0
x2: width/2
y2: height
### create phantom nodes and links ###
phantom_links_layer = enter_views.append('g')
phantom_links = phantom_links_layer.selectAll('.link')
.data(((v) -> graph.links.filter((l) -> v not in l.graphs)), (d) -> d.id)
phantom_links
.enter().append('line')
.attr('class', 'phantom link')
phantom_nodes_layer = enter_views.append('g')
phantom_nodes = phantom_nodes_layer.selectAll('.node')
.data(((v) -> graph.nodes.filter((n) -> v not in n.graphs)), (d) -> d.id)
enter_phantom_nodes = phantom_nodes.enter().append('g')
.attr('class', 'phantom node')
enter_phantom_nodes.append('circle')
.attr('r', R)
### create nodes and links ###
links_layer = enter_views.append('g')
links = links_layer.selectAll('.link')
.data(((v) -> graph.links.filter((l) -> v in l.graphs)), (d) -> d.id)
links
.enter().append('line')
.attr('class', 'link')
nodes_layer = enter_views.append('g')
nodes = nodes_layer.selectAll('.node')
.data(((v) -> graph.nodes.filter((n) -> v in n.graphs)), (d) -> d.id)
enter_nodes = nodes.enter().append('g')
.attr('class', 'node')
enter_nodes.append('circle')
.attr('r', R)
### draw the label ###
enter_nodes.append('text')
.text((d) -> d.id)
.attr('dy', '0.35em')
### cola layout ###
graph.nodes.forEach (v) ->
v.width = 2.5*R
v.height = 2.5*R
d3cola = cola.d3adaptor()
.size([width/2, height])
.linkDistance(70)
.avoidOverlaps(true)
.nodes(graph.nodes)
.links(graph.links)
.on 'tick', () ->
### update nodes and links ###
views.selectAll('.node')
.attr('transform', (d) -> "translate(#{d.x},#{d.y})")
views.selectAll('.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)
enter_nodes
.call(d3cola.drag)
enter_phantom_nodes
.call(d3cola.drag)
d3cola.start(30,30,30)
.node > circle {
fill: #DDD;
stroke: #777;
stroke-width: 2px;
}
.node > text {
font-family: sans-serif;
text-anchor: middle;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
}
.link {
stroke: #88A;
stroke-width: 4px;
marker-end: url(#end-arrow);
}
#end-arrow {
fill: #88A;
}
.separator {
stroke: #dfd7c4;
shape-rendering: crispEdges;
}
.phantom.node > circle {
fill: #EEE;
stroke: #EEE;
}
.phantom.link {
stroke: #EEE;
marker-end: url(#phantom-end-arrow);
}
#phantom-end-arrow {
fill: #EEE;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Graph comparison</title>
<link rel="stylesheet" href="index.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://marvl.infotech.monash.edu/webcola/cola.v3.min.js"></script>
</head>
<body>
<svg width="960px" height="500px"></svg>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.4.0
(function() {
var R, d3cola, defs, enter_nodes, enter_phantom_nodes, enter_views, graph, height, l, links, links_layer, n, nodes, nodes_layer, phantom_links, phantom_links_layer, phantom_nodes, phantom_nodes_layer, svg, views, views_data, width, _i, _j, _len, _len1, _ref, _ref1,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
graph = {
nodes: [
{
id: 'A',
graphs: ['I', 'II']
}, {
id: 'B',
graphs: ['II']
}, {
id: 'C',
graphs: ['I', 'II']
}, {
id: 'D',
graphs: ['I']
}, {
id: 'E',
graphs: ['II']
}, {
id: 'F',
graphs: ['I', 'II']
}, {
id: 'G',
graphs: ['I', 'II']
}, {
id: 'H',
graphs: ['I', 'II']
}, {
id: 'I',
graphs: ['I', 'II']
}, {
id: 'J',
graphs: ['I']
}
],
links: [
{
id: 1,
source: 'A',
target: 'B',
graphs: ['II']
}, {
id: 2,
source: 'A',
target: 'C',
graphs: ['I', 'II']
}, {
id: 3,
source: 'A',
target: 'D',
graphs: ['I']
}, {
id: 4,
source: 'B',
target: 'E',
graphs: ['II']
}, {
id: 5,
source: 'B',
target: 'F',
graphs: ['II']
}, {
id: 6,
source: 'C',
target: 'G',
graphs: ['I', 'II']
}, {
id: 7,
source: 'C',
target: 'F',
graphs: ['I', 'II']
}, {
id: 8,
source: 'F',
target: 'G',
graphs: ['I', 'II']
}, {
id: 9,
source: 'G',
target: 'H',
graphs: ['I', 'II']
}, {
id: 10,
source: 'G',
target: 'I',
graphs: ['I', 'II']
}, {
id: 11,
source: 'H',
target: 'I',
graphs: ['I', 'II']
}, {
id: 12,
source: 'I',
target: 'J',
graphs: ['I']
}
]
};
/* objectify the graph
*/
/* resolve node IDs (not optimized at all!)
*/
_ref = graph.links;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
l = _ref[_i];
_ref1 = graph.nodes;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
n = _ref1[_j];
if (l.source === n.id) {
l.source = n;
}
if (l.target === n.id) {
l.target = n;
}
}
}
R = 18;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
defs = svg.append('defs');
/* define arrow markers for graph links
*/
defs.append('marker').attr({
id: 'end-arrow',
viewBox: '0 0 10 10',
refX: 4 + R,
refY: 5,
orient: 'auto'
}).append('path').attr({
d: 'M0,0 L0,10 L10,5 z'
});
defs.append('marker').attr({
id: 'phantom-end-arrow',
viewBox: '0 0 10 10',
refX: 4 + R,
refY: 5,
orient: 'auto'
}).append('path').attr({
d: 'M0,0 L0,10 L10,5 z'
});
/* create views
*/
defs.append('clipPath').attr({
id: 'square_window'
}).append('rect').attr({
x: 0,
y: 0,
width: width / 2,
height: height
});
views_data = ['I', 'II'];
views = svg.selectAll('.view').data(views_data);
enter_views = views.enter().append('g').attr({
"class": 'view',
'clip-path': 'url(#square_window)',
transform: function(d) {
if (d === 'II') {
return "translate(" + (width / 2) + ",0)";
} else {
return 'translate(0,0)';
}
}
});
svg.append('line').attr({
"class": 'separator',
x1: width / 2,
y1: 0,
x2: width / 2,
y2: height
});
/* create phantom nodes and links
*/
phantom_links_layer = enter_views.append('g');
phantom_links = phantom_links_layer.selectAll('.link').data((function(v) {
return graph.links.filter(function(l) {
return __indexOf.call(l.graphs, v) < 0;
});
}), function(d) {
return d.id;
});
phantom_links.enter().append('line').attr('class', 'phantom link');
phantom_nodes_layer = enter_views.append('g');
phantom_nodes = phantom_nodes_layer.selectAll('.node').data((function(v) {
return graph.nodes.filter(function(n) {
return __indexOf.call(n.graphs, v) < 0;
});
}), function(d) {
return d.id;
});
enter_phantom_nodes = phantom_nodes.enter().append('g').attr('class', 'phantom node');
enter_phantom_nodes.append('circle').attr('r', R);
/* create nodes and links
*/
links_layer = enter_views.append('g');
links = links_layer.selectAll('.link').data((function(v) {
return graph.links.filter(function(l) {
return __indexOf.call(l.graphs, v) >= 0;
});
}), function(d) {
return d.id;
});
links.enter().append('line').attr('class', 'link');
nodes_layer = enter_views.append('g');
nodes = nodes_layer.selectAll('.node').data((function(v) {
return graph.nodes.filter(function(n) {
return __indexOf.call(n.graphs, v) >= 0;
});
}), function(d) {
return d.id;
});
enter_nodes = nodes.enter().append('g').attr('class', 'node');
enter_nodes.append('circle').attr('r', R);
/* draw the label
*/
enter_nodes.append('text').text(function(d) {
return d.id;
}).attr('dy', '0.35em');
/* cola layout
*/
graph.nodes.forEach(function(v) {
v.width = 2.5 * R;
return v.height = 2.5 * R;
});
d3cola = cola.d3adaptor().size([width / 2, height]).linkDistance(70).avoidOverlaps(true).nodes(graph.nodes).links(graph.links).on('tick', function() {
/* update nodes and links
*/
views.selectAll('.node').attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
return views.selectAll('.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;
});
});
enter_nodes.call(d3cola.drag);
enter_phantom_nodes.call(d3cola.drag);
d3cola.start(30, 30, 30);
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment