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.
Last active
July 27, 2023 11:54
-
-
Save nitaku/134eb65f5c1b00d8a73d to your computer and use it in GitHub Desktop.
Fuzzy graph
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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