A simpler approach to the visualization of fuzzy graphs (see the previous example). This time, links only show the constrained maximum degree (in light gray) and the actual degree (in dark gray). Avoiding to display the unconstrained maximum degree (i.e., 1) reduce the complexity of the visualization, at the expense of losing a reference for evaluating a link’s thickness.
// Generated by CoffeeScript 1.4.0
(function() {
var crisp, d3cola, enter_crisp_nodes, enter_fuzzy_links, enter_fuzzy_nodes, fuzzy, graph, height, l, links, n, nodes, radius, svg, thickness, width, _i, _j, _len, _len1, _ref, _ref1;
graph = {
nodes: [
{
id: 'A',
u: Math.random()
}, {
id: 'B',
u: Math.random()
}, {
id: 'C',
u: Math.random()
}, {
id: 'D',
u: Math.random()
}, {
id: 'E',
u: Math.random()
}, {
id: 'F',
u: Math.random()
}, {
id: 'G',
u: Math.random()
}, {
id: 'H',
u: Math.random()
}, {
id: 'I',
u: Math.random()
}, {
id: 'J',
u: Math.random()
}, {
id: 'K',
u: Math.random()
}, {
id: 'L',
u: Math.random()
}, {
id: 'M',
u: Math.random()
}, {
id: 'N',
u: Math.random()
}, {
id: 'O',
u: Math.random()
}
],
links: [
{
id: 1,
source: 'A',
target: 'B'
}, {
id: 2,
source: 'B',
target: 'C'
}, {
id: 3,
source: 'C',
target: 'A'
}, {
id: 4,
source: 'B',
target: 'D'
}, {
id: 5,
source: 'D',
target: 'C'
}, {
id: 6,
source: 'D',
target: 'E'
}, {
id: 7,
source: 'E',
target: 'F'
}, {
id: 8,
source: 'F',
target: 'G'
}, {
id: 9,
source: 'F',
target: 'H'
}, {
id: 10,
source: 'G',
target: 'H'
}, {
id: 11,
source: 'G',
target: 'I'
}, {
id: 12,
source: 'H',
target: 'I'
}, {
id: 13,
source: 'J',
target: 'E'
}, {
id: 14,
source: 'J',
target: 'L'
}, {
id: 15,
source: 'J',
target: 'K'
}, {
id: 16,
source: 'K',
target: 'L'
}, {
id: 17,
source: 'L',
target: 'M'
}, {
id: 18,
source: 'M',
target: 'K'
}, {
id: 19,
source: 'N',
target: 'O'
}
]
};
/* 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;
}
}
l.u = Math.min(Math.random(), l.source.u, l.target.u);
}
radius = d3.scale.sqrt().domain([0, 1]).range([0, 18]);
thickness = d3.scale.linear().domain([0, 1]).range([0, 10]);
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
/* create a crisp and a fuzzy layer
*/
crisp = svg.append('g');
fuzzy = svg.append('g');
/* create crisp nodes
*/
nodes = crisp.selectAll('.node').data(graph.nodes, function(d) {
return d.id;
});
enter_crisp_nodes = nodes.enter().append('g').attr({
"class": 'crisp node'
});
enter_crisp_nodes.append('circle').attr({
r: radius.range()[1]
});
enter_crisp_nodes.append('title').text(function(d) {
return "(" + d.id + " " + (d3.format('%')(d.u)) + ")";
});
/* create fuzzy nodes and links
*/
links = fuzzy.selectAll('.link').data(graph.links, function(d) {
return d.id;
});
enter_fuzzy_links = links.enter().append('g').attr({
"class": 'fuzzy link'
});
enter_fuzzy_links.append('line').attr({
"class": 'max',
'stroke-width': function(d) {
return thickness(Math.min(d.source.u, d.target.u));
}
});
enter_fuzzy_links.append('line').attr({
"class": 'value',
'stroke-width': function(d) {
return thickness(d.u);
}
});
enter_fuzzy_links.append('title').text(function(d) {
return "(" + d.source.id + ")-[" + (d3.format('%')(d.u)) + "]-(" + d.target.id + ")\nMax: " + (d3.format('%')(Math.min(d.source.u, d.target.u)));
});
nodes = fuzzy.selectAll('.node').data(graph.nodes, function(d) {
return d.id;
});
enter_fuzzy_nodes = nodes.enter().append('g').attr({
"class": 'fuzzy node'
});
enter_fuzzy_nodes.append('circle').attr({
r: function(d) {
return radius(d.u);
}
});
/* draw the label
*/
enter_fuzzy_nodes.append('text').text(function(d) {
return d.id;
}).attr({
dy: '0.8em',
x: function(d) {
return radius(d.u);
},
y: function(d) {
return radius(d.u) / 2;
}
});
/* cola layout
*/
graph.nodes.forEach(function(v) {
v.width = 2.5 * radius(v.u);
return v.height = 2.5 * radius(v.u);
});
d3cola = cola.d3adaptor().size([width, height]).linkDistance(60).avoidOverlaps(true).nodes(graph.nodes).links(graph.links).on('tick', function() {
/* update nodes and links
*/
svg.selectAll('.node').attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
svg.selectAll('.crisp.link > line').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;
});
return svg.selectAll('.fuzzy.link > line').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_crisp_nodes.call(d3cola.drag);
d3cola.start(30, 30, 30);
}).call(this);
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fuzzy graph II</title>
<link rel="stylesheet" href="index.css">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//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>
graph = {
nodes: [
{id: 'A', u: Math.random()},
{id: 'B', u: Math.random()},
{id: 'C', u: Math.random()},
{id: 'D', u: Math.random()},
{id: 'E', u: Math.random()},
{id: 'F', u: Math.random()},
{id: 'G', u: Math.random()},
{id: 'H', u: Math.random()},
{id: 'I', u: Math.random()},
{id: 'J', u: Math.random()},
{id: 'K', u: Math.random()},
{id: 'L', u: Math.random()},
{id: 'M', u: Math.random()},
{id: 'N', u: Math.random()},
{id: 'O', u: Math.random()}
],
links: [
{id: 1, source: 'A', target: 'B'},
{id: 2, source: 'B', target: 'C'},
{id: 3, source: 'C', target: 'A'},
{id: 4, source: 'B', target: 'D'},
{id: 5, source: 'D', target: 'C'},
{id: 6, source: 'D', target: 'E'},
{id: 7, source: 'E', target: 'F'},
{id: 8, source: 'F', target: 'G'},
{id: 9, source: 'F', target: 'H'},
{id: 10, source: 'G', target: 'H'},
{id: 11, source: 'G', target: 'I'},
{id: 12, source: 'H', target: 'I'},
{id: 13, source: 'J', target: 'E'},
{id: 14, source: 'J', target: 'L'},
{id: 15, source: 'J', target: 'K'},
{id: 16, source: 'K', target: 'L'},
{id: 17, source: 'L', target: 'M'},
{id: 18, source: 'M', target: 'K'},
{id: 19, source: 'N', target: 'O'}
]}
### 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
# link's u cannot exceed the ones of connected nodes
l.u = Math.min(Math.random(), l.source.u, l.target.u)
radius = d3.scale.sqrt()
.domain([0,1])
.range([0,18])
thickness = d3.scale.linear()
.domain([0,1])
.range([0,10])
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
### create a crisp and a fuzzy layer ###
crisp = svg.append('g')
fuzzy = svg.append('g')
### create crisp nodes ###
nodes = crisp.selectAll('.node')
.data(graph.nodes, (d) -> d.id)
enter_crisp_nodes = nodes.enter().append('g')
.attr
class: 'crisp node'
enter_crisp_nodes.append('circle')
.attr
r: radius.range()[1]
enter_crisp_nodes.append('title')
.text((d) -> "(#{d.id} #{d3.format('%')(d.u)})")
### create fuzzy nodes and links ###
links = fuzzy.selectAll('.link')
.data(graph.links, (d) -> d.id)
enter_fuzzy_links = links
.enter().append('g')
.attr
class: 'fuzzy link'
enter_fuzzy_links.append('line')
.attr
class: 'max'
'stroke-width': (d) -> thickness(Math.min(d.source.u, d.target.u))
enter_fuzzy_links.append('line')
.attr
class: 'value'
'stroke-width': (d) -> thickness(d.u)
enter_fuzzy_links.append('title')
.text((d) -> "(#{d.source.id})-[#{d3.format('%')(d.u)}]-(#{d.target.id})\nMax: #{d3.format('%')(Math.min(d.source.u,d.target.u))}")
nodes = fuzzy.selectAll('.node')
.data(graph.nodes, (d) -> d.id)
enter_fuzzy_nodes = nodes.enter().append('g')
.attr
class: 'fuzzy node'
enter_fuzzy_nodes.append('circle')
.attr
r: (d) -> radius(d.u)
### draw the label ###
enter_fuzzy_nodes.append('text')
.text((d) -> d.id)
.attr
dy: '0.8em'
x: (d) -> radius(d.u)
y: (d) -> radius(d.u)/2
### cola layout ###
graph.nodes.forEach (v) ->
v.width = 2.5*radius(v.u)
v.height = 2.5*radius(v.u)
d3cola = cola.d3adaptor()
.size([width, height])
.linkDistance(60)
.avoidOverlaps(true)
.nodes(graph.nodes)
.links(graph.links)
.on 'tick', () ->
### update nodes and links ###
svg.selectAll('.node')
.attr('transform', (d) -> "translate(#{d.x},#{d.y})")
svg.selectAll('.crisp.link > line')
.attr('x1', (d) -> d.source.x)
.attr('y1', (d) -> d.source.y)
.attr('x2', (d) -> d.target.x)
.attr('y2', (d) -> d.target.y)
svg.selectAll('.fuzzy.link > line')
.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_crisp_nodes
.call(d3cola.drag)
d3cola.start(30,30,30)
.crisp.node > circle {
fill: #DDD;
}
.fuzzy.node > circle {
fill: #595;
pointer-events: none;
}
.node > text {
font-family: sans-serif;
text-anchor: start;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
text-shadow: -2px 0 white, 0 2px white, 2px 0 white, 0 -2px white, -1px -1px white, 1px -1px white, 1px 1px white, -1px 1px white
}
.fuzzy.link .max {
stroke: #DDD;
}
.fuzzy.link .value {
stroke: #888;
}