block by nitaku 134eb65f5c1b00d8a73d

Fuzzy graph

Full Screen

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.

index.js

// 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);

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Fuzzy graph</title>
    <link rel="stylesheet" href="index.css">
    <script src="//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>

index.coffee

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)

index.css

.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;
}