Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active July 27, 2023 11:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/134eb65f5c1b00d8a73d to your computer and use it in GitHub Desktop.
Save nitaku/134eb65f5c1b00d8a73d to your computer and use it in GitHub Desktop.
Fuzzy graph

A visualization experiment for displaying fuzzy graphs (Rosenfeld 1975, in Fuzzy Sets and Their Applications to Cognitive and Decision Processes, page 77). Each node has a degree of membership to the set of graph nodes, encoded with its area (in red). The maximum degree of membership (i.e., 1) is represented as a gray circle of unit area. Links have a degree as well, linearly encoded with their thickness (dark gray). The maximum degree for links is limited by the minimum value between the degrees of the nodes it connnects, and is represented with a light gray line, while the unconstrained maximum degree (i.e., 1) is shown as a dotted, empty line.

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 and links ###
links = crisp.selectAll('.link')
.data(graph.links, (d) -> d.id)
enter_crisp_links = links
.enter().append('g')
.attr
class: 'crisp link'
enter_crisp_links.append('line')
.attr
class: 'external'
'stroke-width': thickness.range()[1]
enter_crisp_links.append('line')
.attr
class: 'internal'
'stroke-width': thickness.range()[1]-2
enter_crisp_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 = 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: #D66;
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
}
.crisp.link .external {
stroke: #DDD;
stroke-dasharray: 1 1;
}
.crisp.link .internal {
stroke: #FFF;
}
.fuzzy.link .max {
stroke: #DDD;
}
.fuzzy.link .value {
stroke: #888;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fuzzy graph</title>
<link rel="stylesheet" href="index.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="https://ialab.it.monash.edu/webcola/cola.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 crisp, d3cola, enter_crisp_links, 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 and links
*/
links = crisp.selectAll('.link').data(graph.links, function(d) {
return d.id;
});
enter_crisp_links = links.enter().append('g').attr({
"class": 'crisp link'
});
enter_crisp_links.append('line').attr({
"class": 'external',
'stroke-width': thickness.range()[1]
});
enter_crisp_links.append('line').attr({
"class": 'internal',
'stroke-width': thickness.range()[1] - 2
});
enter_crisp_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 = 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment