block by nitaku d8c76dfc03589e6dbf1f

Half matrix for symmetrical relations

Full Screen

An experiment on showing a weighted symmetric relation. Since the depicted relation is symmetric (and reflexive), only half of the matrix (even excluding the diagonal) can be shown without losing information. This could be useful to save screen estate.

Unfortunately, reading the half-matrix does not feel very natural, making especially difficult to see line-based patterns, since lines are often split in halves. A set of interactive techniques is put into place to ease these issues, at least in part.

In this specific example, the number of co-occurrences of characters from Les Miserables is encoded with the area of the blue dots (a different take on this classic example by Mike Bostock).

index.js

// Generated by CoffeeScript 1.4.0
(function() {
  var DIST, SIDE, height, svg, vis, width;

  SIDE = 7;

  DIST = 8;

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  vis = svg.append('g').attr({
    transform: "translate(" + (width / 2) + ", " + (height - 10) + ")"
  });

  d3.json('miserables.json', function(data) {
    var PAD, cells, cellsize, cursor_x, cursor_x2, cursor_xcurve, cursor_y, cursor_y2, cursor_ycurve, icells, interactive_cells, labels_a, labels_b, length;
    length = data.nodes.length;
    cellsize = d3.scale.sqrt().domain([
      0, d3.max(data.links, function(d) {
        return d.value;
      })
    ]).range([0, SIDE / 2]);
    data.links.forEach(function(l) {
      l.source = data.nodes[l.source];
      return l.target = data.nodes[l.target];
    });
    data.nodes.forEach(function(d, i) {
      return d.i = i;
    });
    cursor_x = vis.append('line').style({
      display: 'none'
    }).attr({
      "class": 'cursor x',
      transform: 'rotate(-45)'
    });
    cursor_x2 = vis.append('line').style({
      display: 'none'
    }).attr({
      "class": 'cursor x',
      transform: 'rotate(-45)'
    });
    cursor_xcurve = vis.append('path').style({
      display: 'none'
    }).attr({
      "class": 'cursor x',
      d: "M0 " + (-SIDE / 2) + " C0 " + (-SIDE / 6) + " " + (SIDE / 6) + " 0 " + (SIDE / 2) + " 0"
    });
    cursor_y = vis.append('line').style({
      display: 'none'
    }).attr({
      "class": 'cursor y',
      transform: 'rotate(-45)'
    });
    cursor_y2 = vis.append('line').style({
      display: 'none'
    }).attr({
      "class": 'cursor y',
      transform: 'rotate(-45)'
    });
    cursor_ycurve = vis.append('path').style({
      display: 'none'
    }).attr({
      "class": 'cursor y',
      d: "M0 " + (-SIDE / 2) + " C0 " + (-SIDE / 6) + " " + (SIDE / 6) + " 0 " + (SIDE / 2) + " 0"
    });
    cells = vis.selectAll('.cell').data(data.links);
    cells.enter().append('circle').attr({
      "class": 'cell',
      r: function(d) {
        return cellsize(d.value);
      },
      transform: function(d) {
        return "rotate(-45) translate(" + ((d.source.i - length / 2) * DIST) + "," + ((d.target.i - length / 2) * DIST) + ")";
      }
    });
    PAD = 4;
    labels_a = vis.selectAll('.label_a').data(data.nodes.slice(1));
    labels_a.enter().append('text').each(function(d) {
      return d.label_a = d3.select(this);
    }).text(function(d) {
      return d.name;
    }).attr({
      "class": 'label label_a',
      dy: '0.35em',
      transform: function(d) {
        return "rotate(45) translate(" + (-length / 2 * DIST - PAD - DIST / 2) + "," + ((-d.i + length / 2) * DIST) + ")";
      }
    }).on('mouseenter', function(d) {
      if (d.label_a != null) {
        d.label_a.classed('highlighted_x', true);
      }
      if (d.label_b != null) {
        d.label_b.classed('highlighted_x', true);
      }
      cursor_x.style({
        display: 'inline'
      }).attr({
        x1: (d.i - length / 2) * DIST,
        x2: (d.i - length / 2) * DIST,
        y1: (-length / 2 - 0.5) * DIST,
        y2: (d.i - length / 2 - 0.5) * DIST
      });
      cursor_x2.style({
        display: 'inline'
      }).attr({
        x1: (d.i - length / 2 + 0.5) * DIST,
        x2: (+length / 2 - 0.5) * DIST,
        y1: (d.i - length / 2) * DIST,
        y2: (d.i - length / 2) * DIST
      });
      if (d.label_b != null) {
        return cursor_xcurve.style({
          display: 'inline'
        }).attr({
          transform: "rotate(-45) translate(" + ((d.i - length / 2) * DIST) + ", " + ((d.i - length / 2) * DIST) + ")"
        });
      }
    }).on('mouseleave', function(d) {
      if (d.label_a != null) {
        d.label_a.classed('highlighted_x', false);
      }
      if (d.label_b != null) {
        d.label_b.classed('highlighted_x', false);
      }
      cursor_x.style({
        display: 'none'
      });
      cursor_x2.style({
        display: 'none'
      });
      return cursor_xcurve.style({
        display: 'none'
      });
    });
    labels_b = vis.selectAll('.label_b').data(data.nodes.slice(0, -1));
    labels_b.enter().append('text').each(function(d) {
      return d.label_b = d3.select(this);
    }).text(function(d) {
      return d.name;
    }).attr({
      "class": 'label label_b',
      dy: '0.35em',
      transform: function(d) {
        return "rotate(-45) translate(" + (length / 2 * DIST + PAD - DIST / 2) + "," + ((d.i - length / 2) * DIST) + ")";
      }
    }).on('mouseenter', function(d) {
      if (d.label_a != null) {
        d.label_a.classed('highlighted_y', true);
      }
      if (d.label_b != null) {
        d.label_b.classed('highlighted_y', true);
      }
      cursor_y.style({
        display: 'inline'
      }).attr({
        x1: (d.i - length / 2 + 0.5) * DIST,
        x2: (+length / 2 - 0.5) * DIST,
        y1: (d.i - length / 2) * DIST,
        y2: (d.i - length / 2) * DIST
      });
      cursor_y2.style({
        display: 'inline'
      }).attr({
        x1: (d.i - length / 2) * DIST,
        x2: (d.i - length / 2) * DIST,
        y1: (-length / 2 - 0.5) * DIST,
        y2: (d.i - length / 2 - 0.5) * DIST
      });
      if (d.label_a != null) {
        return cursor_ycurve.style({
          display: 'inline'
        }).attr({
          transform: "rotate(-45) translate(" + ((d.i - length / 2) * DIST) + ", " + ((d.i - length / 2) * DIST) + ")"
        });
      }
    }).on('mouseleave', function(d) {
      if (d.label_a != null) {
        d.label_a.classed('highlighted_y', false);
      }
      if (d.label_b != null) {
        d.label_b.classed('highlighted_y', false);
      }
      cursor_y.style({
        display: 'none'
      });
      cursor_y2.style({
        display: 'none'
      });
      return cursor_ycurve.style({
        display: 'none'
      });
    });
    icells = [];
    d3.range(0, length).forEach(function(y) {
      return d3.range(y + 1, length).forEach(function(x) {
        return icells.push({
          x: x,
          y: y,
          source: data.nodes[x],
          target: data.nodes[y]
        });
      });
    });
    interactive_cells = vis.selectAll('.interactive_cell').data(icells);
    return interactive_cells.enter().append('rect').attr({
      "class": 'interactive_cell',
      x: -DIST / 2,
      y: -DIST / 2,
      width: DIST,
      height: DIST,
      transform: function(d) {
        return "rotate(-45) translate(" + ((d.x - length / 2) * DIST) + "," + ((d.y - length / 2) * DIST) + ")";
      }
    }).on('mouseenter', function(d) {
      if (d.source.label_a != null) {
        d.source.label_a.classed('highlighted_x', true);
      }
      if (d.target.label_a != null) {
        d.target.label_a.classed('highlighted_y', true);
      }
      if (d.source.label_b != null) {
        d.source.label_b.classed('highlighted_x', true);
      }
      if (d.target.label_b != null) {
        d.target.label_b.classed('highlighted_y', true);
      }
      cursor_x.style({
        display: 'inline'
      }).attr({
        x1: (d.x - length / 2) * DIST,
        x2: (d.x - length / 2) * DIST,
        y1: (-length / 2 - 0.5) * DIST,
        y2: (d.x - length / 2 - 0.5) * DIST
      });
      cursor_x2.style({
        display: 'inline'
      }).attr({
        x1: (d.x - length / 2 + 0.5) * DIST,
        x2: (+length / 2 - 0.5) * DIST,
        y1: (d.x - length / 2) * DIST,
        y2: (d.x - length / 2) * DIST
      });
      cursor_y.style({
        display: 'inline'
      }).attr({
        x1: (d.y - length / 2 + 0.5) * DIST,
        x2: (+length / 2 - 0.5) * DIST,
        y1: (d.y - length / 2) * DIST,
        y2: (d.y - length / 2) * DIST
      });
      cursor_y2.style({
        display: 'inline'
      }).attr({
        x1: (d.y - length / 2) * DIST,
        x2: (d.y - length / 2) * DIST,
        y1: (-length / 2 - 0.5) * DIST,
        y2: (d.y - length / 2 - 0.5) * DIST
      });
      if (d.source.label_b != null) {
        cursor_xcurve.style({
          display: 'inline'
        }).attr({
          transform: "rotate(-45) translate(" + ((d.x - length / 2) * DIST) + ", " + ((d.x - length / 2) * DIST) + ")"
        });
      }
      if (d.target.label_a != null) {
        return cursor_ycurve.style({
          display: 'inline'
        }).attr({
          transform: "rotate(-45) translate(" + ((d.y - length / 2) * DIST) + ", " + ((d.y - length / 2) * DIST) + ")"
        });
      }
    }).on('mouseleave', function(d) {
      if (d.source.label_a != null) {
        d.source.label_a.classed('highlighted_x', false);
      }
      if (d.target.label_a != null) {
        d.target.label_a.classed('highlighted_y', false);
      }
      if (d.source.label_b != null) {
        d.source.label_b.classed('highlighted_x', false);
      }
      if (d.target.label_b != null) {
        d.target.label_b.classed('highlighted_y', false);
      }
      cursor_x.style({
        display: 'none'
      });
      cursor_x2.style({
        display: 'none'
      });
      cursor_xcurve.style({
        display: 'none'
      });
      cursor_y.style({
        display: 'none'
      });
      cursor_y2.style({
        display: 'none'
      });
      return cursor_ycurve.style({
        display: 'none'
      });
    });
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Half matrix for symmetrical relations</title>
    <link type="text/css" href="index.css" rel="stylesheet"/>
    <script src="//d3js.org/d3.v3.min.js"></script>
  </head>
  <body>
        <svg height="500" width="960"></svg>
        <script src="index.js"></script>
  </body>
</html>

index.coffee

SIDE = 7
DIST = 8

svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

vis = svg.append('g')
  .attr
    transform: "translate(#{width/2}, #{height-10})"

d3.json 'miserables.json', (data) ->
  length = data.nodes.length

  cellsize = d3.scale.sqrt()
    .domain([0, d3.max data.links, (d) -> d.value])
    .range([0,SIDE/2])

  # objectify the graph
  data.links.forEach (l) ->
    l.source = data.nodes[l.source]
    l.target = data.nodes[l.target]

  # store the index of the node within its object, in order to use it when placing link cells
  data.nodes.forEach (d, i) ->
    d.i = i

  # create the cursors
  cursor_x = vis.append('line')
    .style
      display: 'none'
    .attr
      class: 'cursor x'
      transform: 'rotate(-45)'

  cursor_x2 = vis.append('line')
    .style
      display: 'none'
    .attr
      class: 'cursor x'
      transform: 'rotate(-45)'

  cursor_xcurve = vis.append('path')
    .style
      display: 'none'
    .attr
      class: 'cursor x'
      d: "M0 #{-SIDE/2} C0 #{-SIDE/6} #{SIDE/6} 0 #{SIDE/2} 0"

  cursor_y = vis.append('line')
    .style
      display: 'none'
    .attr
      class: 'cursor y'
      transform: 'rotate(-45)'

  cursor_y2 = vis.append('line')
    .style
      display: 'none'
    .attr
      class: 'cursor y'
      transform: 'rotate(-45)'

  cursor_ycurve = vis.append('path')
    .style
      display: 'none'
    .attr
      class: 'cursor y'
      d: "M0 #{-SIDE/2} C0 #{-SIDE/6} #{SIDE/6} 0 #{SIDE/2} 0"

  # draw the cells containing data
  cells = vis.selectAll('.cell')
    .data(data.links)

  cells.enter().append('circle')
    .attr
      class: 'cell'
      r: (d) -> cellsize(d.value)
      transform: (d) -> "rotate(-45) translate(#{(d.source.i-length/2)*DIST},#{(d.target.i-length/2)*DIST})"

  # draw the labels
  PAD = 4
  labels_a = vis.selectAll('.label_a')
    .data(data.nodes.slice(1)) # first label is useless, the diagonal is not shown

  labels_a.enter().append('text')
    .each((d) -> d.label_a = d3.select(this))
    .text((d) -> d.name)
    .attr
      class: 'label label_a'
      dy: '0.35em'
      transform: (d) -> "rotate(45) translate(#{-length/2*DIST-PAD-DIST/2},#{(-d.i+length/2)*DIST})"
    .on 'mouseenter', (d) ->
      d.label_a.classed('highlighted_x', true) if d.label_a?
      d.label_b.classed('highlighted_x', true) if d.label_b?

      cursor_x
        .style
          display: 'inline'
        .attr
          x1: (d.i-length/2)*DIST
          x2: (d.i-length/2)*DIST
          y1: (-length/2-0.5)*DIST
          y2: (d.i-length/2-0.5)*DIST

      cursor_x2
        .style
          display: 'inline'
        .attr
          x1: (d.i-length/2+0.5)*DIST
          x2: (+length/2-0.5)*DIST
          y1: (d.i-length/2)*DIST
          y2: (d.i-length/2)*DIST

      if d.label_b?
        cursor_xcurve
          .style
            display: 'inline'
          .attr
            transform: "rotate(-45) translate(#{(d.i-length/2)*DIST}, #{(d.i-length/2)*DIST})"


    .on 'mouseleave', (d) ->
      d.label_a.classed('highlighted_x', false) if d.label_a?
      d.label_b.classed('highlighted_x', false) if d.label_b?

      cursor_x
        .style
          display: 'none'

      cursor_x2
        .style
          display: 'none'

      cursor_xcurve
        .style
          display: 'none'

  labels_b = vis.selectAll('.label_b')
    .data(data.nodes.slice(0,-1)) # last label is useless, the diagonal is not shown

  labels_b.enter().append('text')
    .each((d) -> d.label_b = d3.select(this))
    .text((d) -> d.name)
    .attr
      class: 'label label_b'
      dy: '0.35em'
      transform: (d) -> "rotate(-45) translate(#{length/2*DIST+PAD-DIST/2},#{(d.i-length/2)*DIST})"
    .on 'mouseenter', (d) ->
      d.label_a.classed('highlighted_y', true) if d.label_a?
      d.label_b.classed('highlighted_y', true) if d.label_b?

      cursor_y
        .style
          display: 'inline'
        .attr
          x1: (d.i-length/2+0.5)*DIST
          x2: (+length/2-0.5)*DIST
          y1: (d.i-length/2)*DIST
          y2: (d.i-length/2)*DIST

      cursor_y2
        .style
          display: 'inline'
        .attr
          x1: (d.i-length/2)*DIST
          x2: (d.i-length/2)*DIST
          y1: (-length/2-0.5)*DIST
          y2: (d.i-length/2-0.5)*DIST

      if d.label_a?
        cursor_ycurve
          .style
            display: 'inline'
          .attr
            transform: "rotate(-45) translate(#{(d.i-length/2)*DIST}, #{(d.i-length/2)*DIST})"

    .on 'mouseleave', (d) ->
      d.label_a.classed('highlighted_y', false) if d.label_a?
      d.label_b.classed('highlighted_y', false) if d.label_b?

      cursor_y
        .style
          display: 'none'

      cursor_y2
        .style
          display: 'none'

      cursor_ycurve
        .style
          display: 'none'

  # create "interactive" cells to show the cursors
  icells = []
  d3.range(0, length).forEach (y) ->
    d3.range(y+1, length).forEach (x) ->
      icells.push {x: x, y: y, source: data.nodes[x], target: data.nodes[y]}

  interactive_cells = vis.selectAll('.interactive_cell')
    .data(icells)

  interactive_cells.enter().append('rect')
    .attr
      class: 'interactive_cell'
      x: -DIST/2
      y: -DIST/2
      width: DIST
      height: DIST
      transform: (d) -> "rotate(-45) translate(#{(d.x-length/2)*DIST},#{(d.y-length/2)*DIST})"
    .on 'mouseenter', (d) ->
      d.source.label_a.classed('highlighted_x', true) if d.source.label_a?
      d.target.label_a.classed('highlighted_y', true) if d.target.label_a?
      d.source.label_b.classed('highlighted_x', true) if d.source.label_b?
      d.target.label_b.classed('highlighted_y', true) if d.target.label_b?
      cursor_x
        .style
          display: 'inline'
        .attr
          x1: (d.x-length/2)*DIST
          x2: (d.x-length/2)*DIST
          y1: (-length/2-0.5)*DIST
          y2: (d.x-length/2-0.5)*DIST

      cursor_x2
        .style
          display: 'inline'
        .attr
          x1: (d.x-length/2+0.5)*DIST
          x2: (+length/2-0.5)*DIST
          y1: (d.x-length/2)*DIST
          y2: (d.x-length/2)*DIST

      cursor_y
        .style
          display: 'inline'
        .attr
          x1: (d.y-length/2+0.5)*DIST
          x2: (+length/2-0.5)*DIST
          y1: (d.y-length/2)*DIST
          y2: (d.y-length/2)*DIST

      cursor_y2
        .style
          display: 'inline'
        .attr
          x1: (d.y-length/2)*DIST
          x2: (d.y-length/2)*DIST
          y1: (-length/2-0.5)*DIST
          y2: (d.y-length/2-0.5)*DIST

      if d.source.label_b?
        cursor_xcurve
          .style
            display: 'inline'
          .attr
            transform: "rotate(-45) translate(#{(d.x-length/2)*DIST}, #{(d.x-length/2)*DIST})"

      if d.target.label_a?
        cursor_ycurve
          .style
            display: 'inline'
          .attr
            transform: "rotate(-45) translate(#{(d.y-length/2)*DIST}, #{(d.y-length/2)*DIST})"

    .on 'mouseleave', (d) ->
      d.source.label_a.classed('highlighted_x', false) if d.source.label_a?
      d.target.label_a.classed('highlighted_y', false) if d.target.label_a?
      d.source.label_b.classed('highlighted_x', false) if d.source.label_b?
      d.target.label_b.classed('highlighted_y', false) if d.target.label_b?

      cursor_x
        .style
          display: 'none'

      cursor_x2
        .style
          display: 'none'

      cursor_xcurve
        .style
          display: 'none'

      cursor_y
        .style
          display: 'none'

      cursor_y2
        .style
          display: 'none'

      cursor_ycurve
        .style
          display: 'none'

index.css

@font-face {
  font-family: "Lacuna";
  src: url("lacuna.ttf");
}

.label {
  font-family: "Lacuna";
  font-size: 8px;
}
.highlighted_x.label {
  font-weight: bold;
  fill: #E21975;
}
.highlighted_y.label {
  font-weight: bold;
  fill: #FF7100;
}
.label_a {
  text-anchor: end;
}

.cell {
  fill: #2272C0;
}
.interactive_cell {
  fill: transparent;
}
.interactive_cell:hover {
  stroke: #FF3B1C;
}

.cursor {
  fill: none;
  stroke-width: 0.5;
  stroke-linecap: square;
}
.cursor.x {
    stroke: #E21975;
}
.cursor.y {
    stroke: #FF7100;
}

miserables.json

{
  "nodes":[
    {"name":"Myriel","group":1},
    {"name":"Napoleon","group":1},
    {"name":"Mlle.Baptistine","group":1},
    {"name":"Mme.Magloire","group":1},
    {"name":"CountessdeLo","group":1},
    {"name":"Geborand","group":1},
    {"name":"Champtercier","group":1},
    {"name":"Cravatte","group":1},
    {"name":"Count","group":1},
    {"name":"OldMan","group":1},
    {"name":"Labarre","group":2},
    {"name":"Valjean","group":2},
    {"name":"Marguerite","group":3},
    {"name":"Mme.deR","group":2},
    {"name":"Isabeau","group":2},
    {"name":"Gervais","group":2},
    {"name":"Tholomyes","group":3},
    {"name":"Listolier","group":3},
    {"name":"Fameuil","group":3},
    {"name":"Blacheville","group":3},
    {"name":"Favourite","group":3},
    {"name":"Dahlia","group":3},
    {"name":"Zephine","group":3},
    {"name":"Fantine","group":3},
    {"name":"Mme.Thenardier","group":4},
    {"name":"Thenardier","group":4},
    {"name":"Cosette","group":5},
    {"name":"Javert","group":4},
    {"name":"Fauchelevent","group":0},
    {"name":"Bamatabois","group":2},
    {"name":"Perpetue","group":3},
    {"name":"Simplice","group":2},
    {"name":"Scaufflaire","group":2},
    {"name":"Woman1","group":2},
    {"name":"Judge","group":2},
    {"name":"Champmathieu","group":2},
    {"name":"Brevet","group":2},
    {"name":"Chenildieu","group":2},
    {"name":"Cochepaille","group":2},
    {"name":"Pontmercy","group":4},
    {"name":"Boulatruelle","group":6},
    {"name":"Eponine","group":4},
    {"name":"Anzelma","group":4},
    {"name":"Woman2","group":5},
    {"name":"MotherInnocent","group":0},
    {"name":"Gribier","group":0},
    {"name":"Jondrette","group":7},
    {"name":"Mme.Burgon","group":7},
    {"name":"Gavroche","group":8},
    {"name":"Gillenormand","group":5},
    {"name":"Magnon","group":5},
    {"name":"Mlle.Gillenormand","group":5},
    {"name":"Mme.Pontmercy","group":5},
    {"name":"Mlle.Vaubois","group":5},
    {"name":"Lt.Gillenormand","group":5},
    {"name":"Marius","group":8},
    {"name":"BaronessT","group":5},
    {"name":"Mabeuf","group":8},
    {"name":"Enjolras","group":8},
    {"name":"Combeferre","group":8},
    {"name":"Prouvaire","group":8},
    {"name":"Feuilly","group":8},
    {"name":"Courfeyrac","group":8},
    {"name":"Bahorel","group":8},
    {"name":"Bossuet","group":8},
    {"name":"Joly","group":8},
    {"name":"Grantaire","group":8},
    {"name":"MotherPlutarch","group":9},
    {"name":"Gueulemer","group":4},
    {"name":"Babet","group":4},
    {"name":"Claquesous","group":4},
    {"name":"Montparnasse","group":4},
    {"name":"Toussaint","group":5},
    {"name":"Child1","group":10},
    {"name":"Child2","group":10},
    {"name":"Brujon","group":4},
    {"name":"Mme.Hucheloup","group":8}
  ],
  "links":[
    {"source":1,"target":0,"value":1},
    {"source":2,"target":0,"value":8},
    {"source":3,"target":0,"value":10},
    {"source":3,"target":2,"value":6},
    {"source":4,"target":0,"value":1},
    {"source":5,"target":0,"value":1},
    {"source":6,"target":0,"value":1},
    {"source":7,"target":0,"value":1},
    {"source":8,"target":0,"value":2},
    {"source":9,"target":0,"value":1},
    {"source":11,"target":10,"value":1},
    {"source":11,"target":3,"value":3},
    {"source":11,"target":2,"value":3},
    {"source":11,"target":0,"value":5},
    {"source":12,"target":11,"value":1},
    {"source":13,"target":11,"value":1},
    {"source":14,"target":11,"value":1},
    {"source":15,"target":11,"value":1},
    {"source":17,"target":16,"value":4},
    {"source":18,"target":16,"value":4},
    {"source":18,"target":17,"value":4},
    {"source":19,"target":16,"value":4},
    {"source":19,"target":17,"value":4},
    {"source":19,"target":18,"value":4},
    {"source":20,"target":16,"value":3},
    {"source":20,"target":17,"value":3},
    {"source":20,"target":18,"value":3},
    {"source":20,"target":19,"value":4},
    {"source":21,"target":16,"value":3},
    {"source":21,"target":17,"value":3},
    {"source":21,"target":18,"value":3},
    {"source":21,"target":19,"value":3},
    {"source":21,"target":20,"value":5},
    {"source":22,"target":16,"value":3},
    {"source":22,"target":17,"value":3},
    {"source":22,"target":18,"value":3},
    {"source":22,"target":19,"value":3},
    {"source":22,"target":20,"value":4},
    {"source":22,"target":21,"value":4},
    {"source":23,"target":16,"value":3},
    {"source":23,"target":17,"value":3},
    {"source":23,"target":18,"value":3},
    {"source":23,"target":19,"value":3},
    {"source":23,"target":20,"value":4},
    {"source":23,"target":21,"value":4},
    {"source":23,"target":22,"value":4},
    {"source":23,"target":12,"value":2},
    {"source":23,"target":11,"value":9},
    {"source":24,"target":23,"value":2},
    {"source":24,"target":11,"value":7},
    {"source":25,"target":24,"value":13},
    {"source":25,"target":23,"value":1},
    {"source":25,"target":11,"value":12},
    {"source":26,"target":24,"value":4},
    {"source":26,"target":11,"value":31},
    {"source":26,"target":16,"value":1},
    {"source":26,"target":25,"value":1},
    {"source":27,"target":11,"value":17},
    {"source":27,"target":23,"value":5},
    {"source":27,"target":25,"value":5},
    {"source":27,"target":24,"value":1},
    {"source":27,"target":26,"value":1},
    {"source":28,"target":11,"value":8},
    {"source":28,"target":27,"value":1},
    {"source":29,"target":23,"value":1},
    {"source":29,"target":27,"value":1},
    {"source":29,"target":11,"value":2},
    {"source":30,"target":23,"value":1},
    {"source":31,"target":30,"value":2},
    {"source":31,"target":11,"value":3},
    {"source":31,"target":23,"value":2},
    {"source":31,"target":27,"value":1},
    {"source":32,"target":11,"value":1},
    {"source":33,"target":11,"value":2},
    {"source":33,"target":27,"value":1},
    {"source":34,"target":11,"value":3},
    {"source":34,"target":29,"value":2},
    {"source":35,"target":11,"value":3},
    {"source":35,"target":34,"value":3},
    {"source":35,"target":29,"value":2},
    {"source":36,"target":34,"value":2},
    {"source":36,"target":35,"value":2},
    {"source":36,"target":11,"value":2},
    {"source":36,"target":29,"value":1},
    {"source":37,"target":34,"value":2},
    {"source":37,"target":35,"value":2},
    {"source":37,"target":36,"value":2},
    {"source":37,"target":11,"value":2},
    {"source":37,"target":29,"value":1},
    {"source":38,"target":34,"value":2},
    {"source":38,"target":35,"value":2},
    {"source":38,"target":36,"value":2},
    {"source":38,"target":37,"value":2},
    {"source":38,"target":11,"value":2},
    {"source":38,"target":29,"value":1},
    {"source":39,"target":25,"value":1},
    {"source":40,"target":25,"value":1},
    {"source":41,"target":24,"value":2},
    {"source":41,"target":25,"value":3},
    {"source":42,"target":41,"value":2},
    {"source":42,"target":25,"value":2},
    {"source":42,"target":24,"value":1},
    {"source":43,"target":11,"value":3},
    {"source":43,"target":26,"value":1},
    {"source":43,"target":27,"value":1},
    {"source":44,"target":28,"value":3},
    {"source":44,"target":11,"value":1},
    {"source":45,"target":28,"value":2},
    {"source":47,"target":46,"value":1},
    {"source":48,"target":47,"value":2},
    {"source":48,"target":25,"value":1},
    {"source":48,"target":27,"value":1},
    {"source":48,"target":11,"value":1},
    {"source":49,"target":26,"value":3},
    {"source":49,"target":11,"value":2},
    {"source":50,"target":49,"value":1},
    {"source":50,"target":24,"value":1},
    {"source":51,"target":49,"value":9},
    {"source":51,"target":26,"value":2},
    {"source":51,"target":11,"value":2},
    {"source":52,"target":51,"value":1},
    {"source":52,"target":39,"value":1},
    {"source":53,"target":51,"value":1},
    {"source":54,"target":51,"value":2},
    {"source":54,"target":49,"value":1},
    {"source":54,"target":26,"value":1},
    {"source":55,"target":51,"value":6},
    {"source":55,"target":49,"value":12},
    {"source":55,"target":39,"value":1},
    {"source":55,"target":54,"value":1},
    {"source":55,"target":26,"value":21},
    {"source":55,"target":11,"value":19},
    {"source":55,"target":16,"value":1},
    {"source":55,"target":25,"value":2},
    {"source":55,"target":41,"value":5},
    {"source":55,"target":48,"value":4},
    {"source":56,"target":49,"value":1},
    {"source":56,"target":55,"value":1},
    {"source":57,"target":55,"value":1},
    {"source":57,"target":41,"value":1},
    {"source":57,"target":48,"value":1},
    {"source":58,"target":55,"value":7},
    {"source":58,"target":48,"value":7},
    {"source":58,"target":27,"value":6},
    {"source":58,"target":57,"value":1},
    {"source":58,"target":11,"value":4},
    {"source":59,"target":58,"value":15},
    {"source":59,"target":55,"value":5},
    {"source":59,"target":48,"value":6},
    {"source":59,"target":57,"value":2},
    {"source":60,"target":48,"value":1},
    {"source":60,"target":58,"value":4},
    {"source":60,"target":59,"value":2},
    {"source":61,"target":48,"value":2},
    {"source":61,"target":58,"value":6},
    {"source":61,"target":60,"value":2},
    {"source":61,"target":59,"value":5},
    {"source":61,"target":57,"value":1},
    {"source":61,"target":55,"value":1},
    {"source":62,"target":55,"value":9},
    {"source":62,"target":58,"value":17},
    {"source":62,"target":59,"value":13},
    {"source":62,"target":48,"value":7},
    {"source":62,"target":57,"value":2},
    {"source":62,"target":41,"value":1},
    {"source":62,"target":61,"value":6},
    {"source":62,"target":60,"value":3},
    {"source":63,"target":59,"value":5},
    {"source":63,"target":48,"value":5},
    {"source":63,"target":62,"value":6},
    {"source":63,"target":57,"value":2},
    {"source":63,"target":58,"value":4},
    {"source":63,"target":61,"value":3},
    {"source":63,"target":60,"value":2},
    {"source":63,"target":55,"value":1},
    {"source":64,"target":55,"value":5},
    {"source":64,"target":62,"value":12},
    {"source":64,"target":48,"value":5},
    {"source":64,"target":63,"value":4},
    {"source":64,"target":58,"value":10},
    {"source":64,"target":61,"value":6},
    {"source":64,"target":60,"value":2},
    {"source":64,"target":59,"value":9},
    {"source":64,"target":57,"value":1},
    {"source":64,"target":11,"value":1},
    {"source":65,"target":63,"value":5},
    {"source":65,"target":64,"value":7},
    {"source":65,"target":48,"value":3},
    {"source":65,"target":62,"value":5},
    {"source":65,"target":58,"value":5},
    {"source":65,"target":61,"value":5},
    {"source":65,"target":60,"value":2},
    {"source":65,"target":59,"value":5},
    {"source":65,"target":57,"value":1},
    {"source":65,"target":55,"value":2},
    {"source":66,"target":64,"value":3},
    {"source":66,"target":58,"value":3},
    {"source":66,"target":59,"value":1},
    {"source":66,"target":62,"value":2},
    {"source":66,"target":65,"value":2},
    {"source":66,"target":48,"value":1},
    {"source":66,"target":63,"value":1},
    {"source":66,"target":61,"value":1},
    {"source":66,"target":60,"value":1},
    {"source":67,"target":57,"value":3},
    {"source":68,"target":25,"value":5},
    {"source":68,"target":11,"value":1},
    {"source":68,"target":24,"value":1},
    {"source":68,"target":27,"value":1},
    {"source":68,"target":48,"value":1},
    {"source":68,"target":41,"value":1},
    {"source":69,"target":25,"value":6},
    {"source":69,"target":68,"value":6},
    {"source":69,"target":11,"value":1},
    {"source":69,"target":24,"value":1},
    {"source":69,"target":27,"value":2},
    {"source":69,"target":48,"value":1},
    {"source":69,"target":41,"value":1},
    {"source":70,"target":25,"value":4},
    {"source":70,"target":69,"value":4},
    {"source":70,"target":68,"value":4},
    {"source":70,"target":11,"value":1},
    {"source":70,"target":24,"value":1},
    {"source":70,"target":27,"value":1},
    {"source":70,"target":41,"value":1},
    {"source":70,"target":58,"value":1},
    {"source":71,"target":27,"value":1},
    {"source":71,"target":69,"value":2},
    {"source":71,"target":68,"value":2},
    {"source":71,"target":70,"value":2},
    {"source":71,"target":11,"value":1},
    {"source":71,"target":48,"value":1},
    {"source":71,"target":41,"value":1},
    {"source":71,"target":25,"value":1},
    {"source":72,"target":26,"value":2},
    {"source":72,"target":27,"value":1},
    {"source":72,"target":11,"value":1},
    {"source":73,"target":48,"value":2},
    {"source":74,"target":48,"value":2},
    {"source":74,"target":73,"value":3},
    {"source":75,"target":69,"value":3},
    {"source":75,"target":68,"value":3},
    {"source":75,"target":25,"value":3},
    {"source":75,"target":48,"value":1},
    {"source":75,"target":41,"value":1},
    {"source":75,"target":70,"value":1},
    {"source":75,"target":71,"value":1},
    {"source":76,"target":64,"value":1},
    {"source":76,"target":65,"value":1},
    {"source":76,"target":66,"value":1},
    {"source":76,"target":63,"value":1},
    {"source":76,"target":62,"value":1},
    {"source":76,"target":48,"value":1},
    {"source":76,"target":58,"value":1}
  ]
}