Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active June 6, 2019 10:58
Show Gist options
  • Save emeeks/625641430adead4bd7dbc9c1ab3f5102 to your computer and use it in GitHub Desktop.
Save emeeks/625641430adead4bd7dbc9c1ab3f5102 to your computer and use it in GitHub Desktop.

Revisions

  1. emeeks revised this gist Mar 3, 2017. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -17,7 +17,6 @@
    background-color: white;
    font-family: 'Lato';
    overflow: visible;
    margin-top: 100px;
    }

    line {
    @@ -84,7 +83,7 @@
    </style>
    </head>
    <body>
    <svg width=960 height=960></svg>
    <svg width=1000 height=650></svg>
    <script src="https://d3js.org/d3.v4.js"></script>
    <script src="bboxCollide.js"></script>
    <script src="https://cdn.rawgit.com/susielu/d3-annotation/master/d3-annotation.js"></script>
  2. emeeks revised this gist Mar 3, 2017. No changes.
  3. Elijah Meeks created this gist Mar 3, 2017.
    5 changes: 5 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    ## Using collision detection with network visualization labels

    This demonstrates how to use [d3-annotation()](https://github.com/susielu/d3-annotation/) with `bboxCollide` to procedurally place node labels. After using the nodes data to create a network visualization of the Les Miserables play, we filter the nodes to leave out the side characters and pass that array to `d3-annotation`. We then create a second `forceSimulation`, this time using the size of the notes as the property in our bounding box collision detection, to move the labels out of each others' way.

    `d3-annotation` by [Susie Lu](https://twitter.com/datatoviz).
    178 changes: 178 additions & 0 deletions bboxCollide.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,178 @@
    (function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree')) :
    typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree'], factory) :
    (factory((global.d3 = global.d3 || {}),global.d3));
    }(this, function (exports,d3Quadtree) { 'use strict';

    function bboxCollide (bbox) {

    function x (d) {
    return d.x + d.vx;
    }

    function y (d) {
    return d.y + d.vy;
    }

    function constant (x) {
    return function () {
    return x;
    };
    }

    var nodes,
    boundingBoxes,
    strength = 1,
    iterations = 1;

    if (typeof bbox !== "function") {
    bbox = constant(bbox === null ? [[0,0][1,1]] : bbox)
    }

    function force () {
    var i,
    tree,
    node,
    xi,
    yi,
    bbi,
    nx1,
    ny1,
    nx2,
    ny2

    var cornerNodes = []
    nodes.forEach(function (d, i) {
    cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + (boundingBoxes[i][1][0] + boundingBoxes[i][0][0]) / 2, y: d.y + (boundingBoxes[i][0][1] + boundingBoxes[i][1][1]) / 2})
    cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][0][0], y: d.y + boundingBoxes[i][0][1]})
    cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][0][0], y: d.y + boundingBoxes[i][1][1]})
    cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][1][0], y: d.y + boundingBoxes[i][0][1]})
    cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][1][0], y: d.y + boundingBoxes[i][1][1]})
    })
    var cn = cornerNodes.length

    for (var k = 0; k < iterations; ++k) {
    tree = d3Quadtree.quadtree(cornerNodes, x, y).visitAfter(prepareCorners);

    for (i = 0; i < cn; ++i) {
    var nodeI = ~~(i / 5);
    node = nodes[nodeI]
    bbi = boundingBoxes[nodeI]
    xi = node.x + node.vx
    yi = node.y + node.vy
    nx1 = xi + bbi[0][0]
    ny1 = yi + bbi[0][1]
    nx2 = xi + bbi[1][0]
    ny2 = yi + bbi[1][1]
    tree.visit(apply);
    }
    }

    function apply (quad, x0, y0, x1, y1) {
    var data = quad.data
    if (data) {
    var bWidth = bbLength(bbi, 0),
    bHeight = bbLength(bbi, 1);

    if (data.node.index !== nodeI) {
    var dataNode = data.node
    var bbj = boundingBoxes[dataNode.index],
    dnx1 = dataNode.x + dataNode.vx + bbj[0][0],
    dny1 = dataNode.y + dataNode.vy + bbj[0][1],
    dnx2 = dataNode.x + dataNode.vx + bbj[1][0],
    dny2 = dataNode.y + dataNode.vy + bbj[1][1],
    dWidth = bbLength(bbj, 0),
    dHeight = bbLength(bbj, 1)

    if (nx1 <= dnx2 && dnx1 <= nx2 && ny1 <= dny2 && dny1 <= ny2) {

    var xSize = [Math.min.apply(null, [dnx1, dnx2, nx1, nx2]), Math.max.apply(null, [dnx1, dnx2, nx1, nx2])]
    var ySize = [Math.min.apply(null, [dny1, dny2, ny1, ny2]), Math.max.apply(null, [dny1, dny2, ny1, ny2])]

    var xOverlap = bWidth + dWidth - (xSize[1] - xSize[0])
    var yOverlap = bHeight + dHeight - (ySize[1] - ySize[0])

    var xBPush = xOverlap * strength * (yOverlap / bHeight)
    var yBPush = yOverlap * strength * (xOverlap / bWidth)

    var xDPush = xOverlap * strength * (yOverlap / dHeight)
    var yDPush = yOverlap * strength * (xOverlap / dWidth)

    if ((nx1 + nx2) / 2 < (dnx1 + dnx2) / 2) {
    node.vx -= xBPush
    dataNode.vx += xDPush
    }
    else {
    node.vx += xBPush
    dataNode.vx -= xDPush
    }
    if ((ny1 + ny2) / 2 < (dny1 + dny2) / 2) {
    node.vy -= yBPush
    dataNode.vy += yDPush
    }
    else {
    node.vy += yBPush
    dataNode.vy -= yDPush
    }
    }

    }
    return;
    }

    return x0 > nx2 || x1 < nx1 || y0 > ny2 || y1 < ny1;
    }

    }

    function prepareCorners (quad) {

    if (quad.data) {
    return quad.bb = boundingBoxes[quad.data.node.index]
    }
    quad.bb = [[0,0],[0,0]]
    for (var i = 0; i < 4; ++i) {
    if (quad[i] && quad[i].bb[0][0] < quad.bb[0][0]) {
    quad.bb[0][0] = quad[i].bb[0][0]
    }
    if (quad[i] && quad[i].bb[0][1] < quad.bb[0][1]) {
    quad.bb[0][1] = quad[i].bb[0][1]
    }
    if (quad[i] && quad[i].bb[1][0] > quad.bb[1][0]) {
    quad.bb[1][0] = quad[i].bb[1][0]
    }
    if (quad[i] && quad[i].bb[1][1] > quad.bb[1][1]) {
    quad.bb[1][1] = quad[i].bb[1][1]
    }
    }
    }

    function bbLength (bbox, heightWidth) {
    return bbox[1][heightWidth] - bbox[0][heightWidth]
    }

    force.initialize = function (_) {
    var i, n = (nodes = _).length; boundingBoxes = new Array(n);
    for (i = 0; i < n; ++i) boundingBoxes[i] = bbox(nodes[i], i, nodes);
    };

    force.iterations = function (_) {
    return arguments.length ? (iterations = +_, force) : iterations;
    };

    force.strength = function (_) {
    return arguments.length ? (strength = +_, force) : strength;
    };

    force.bbox = function (_) {
    return arguments.length ? (bbox = typeof _ === "function" ? _ : constant(+_), force) : bbox;
    };

    return force;
    }

    exports.bboxCollide = bboxCollide;

    Object.defineProperty(exports, '__esModule', { value: true });

    }));
    207 changes: 207 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,207 @@
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <link href='https://fonts.googleapis.com/css?family=Lato:300,900' rel='stylesheet' type='text/css'>

    <style>

    :root {
    --annotation-color: #e91e56;
    }
    body{
    background-color: whitesmoke;
    }

    svg {
    background-color: white;
    font-family: 'Lato';
    overflow: visible;
    margin-top: 100px;
    }

    line {
    stroke:#e3e3e3;
    }

    .editable .annotation-subject, .editable .annotation-textbox {
    cursor: move;
    }

    .line {
    fill: none;
    stroke: black;
    stroke-width: 1px;
    }

    .annotation path {
    stroke: var(--annotation-color);
    fill: rgba(0,0,0,0);
    }

    .annotation path.connector-arrow{
    fill: var(--annotation-color);
    }

    .annotation text {
    fill: var(--annotation-color);
    }

    .annotation-title {
    font-weight: bold;
    }

    .annotation .annotation-subject circle.handle {
    display: none;
    }

    .annotation-note-bg {
    fill: rgba(255, 255, 255, 0);
    }

    circle.handle {
    stroke-dasharray: 5;
    stroke: grey;
    fill: rgba(255, 255, 255, 0);
    cursor: move;

    stroke-opacity: .4;
    }

    circle.handle.highlight {
    stroke-opacity: 1;
    }

    .annotation.major {
    font-weight: 900;
    font-size: 1em;
    }

    .annotation-note-label tspan {
    text-anchor: middle;
    }

    </style>
    </head>
    <body>
    <svg width=960 height=960></svg>
    <script src="https://d3js.org/d3.v4.js"></script>
    <script src="bboxCollide.js"></script>
    <script src="https://cdn.rawgit.com/susielu/d3-annotation/master/d3-annotation.js"></script>

    <script>

    var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

    var color = d3.scaleOrdinal(d3.schemeCategory20)
    .range(["#e91e56", "#00965f", "#00bcd4", "#3f51b5", "#9c27b0", "#ff5722", "#cddc39", "#607d8b", "#8bc34a"]);
    var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id( d => d.id ))
    .force("charge", d3.forceManyBody().strength(-80))
    .force("center", d3.forceCenter(width / 2, height / 2));
    d3.json("miserables.json", function(error, graph) {

    if (error) throw error;
    var link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
    .attr("stroke-width", d => Math.sqrt(d.value));

    var node = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("r", d => d.type === "major" ? 9 : 3)
    .style("fill", d => d3.hsl(color(d.group)).darker())
    .style("fill-opacity", d => d.type === "other" ? 0.5 : 1)

    node.append("title")
    .text(d => d.id);

    window.collide = d3.bboxCollide((a) => {
    return [[a.offsetCornerX - 5, a.offsetCornerY - 10],[a.offsetCornerX + a.width + 5, a.offsetCornerY + a.height+ 5]]
    })
    .strength(0.5)
    .iterations(1)

    window.yScale = d3.scaleLinear()

    simulation
    .nodes(graph.nodes)
    .on("tick", ticked)
    .on("end", function() {

    const noteBoxes = makeAnnotations.collection().noteNodes

    window.labelForce = d3.forceSimulation(noteBoxes)
    .force("x", d3.forceX(a => a.positionX).strength(a => Math.max(0.25, Math.min(3, Math.abs(a.x - a.positionX) / 20))))
    .force("y", d3.forceY(a => a.positionY).strength(a => Math.max(0.25, Math.min(3, Math.abs(a.x - a.positionX) / 20))))
    .force("collision", window.collide)
    .alpha(0.5)
    .on('tick', d => {
    makeAnnotations.annotations()
    .forEach((d, i) => {
    const match = noteBoxes[i]
    d.dx = match.x - match.positionX
    d.dy = match.y - match.positionY
    })

    makeAnnotations.update()
    })

    })
    const nonOtherNodes = graph.nodes
    .filter(d => d.type !== "other")

    simulation.force("link")
    .links(graph.links);
    function ticked() {
    link
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y);
    node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);

    makeAnnotations.annotations()
    .forEach((d, i) => {
    d.position = nonOtherNodes[i]
    })
    }

    window.makeAnnotations = d3.annotation()
    .type(d3.annotationLabel)
    .annotations(nonOtherNodes
    .map((d,i) => {
    return {
    data: {x: d.x, y: d.y, group: d.group},
    note: { label: d.id,
    align: "middle",
    orientation: "fixed" },
    connector: { type: "elbow"},
    className: d.type
    }
    }))
    .accessors({ x: d => d.x , y: d => d.y})

    svg.append("g")
    .attr("class", "annotation-test")
    .call(makeAnnotations)

    svg.selectAll(".annotation-note text")
    .style("fill", d => color(d.data.group))

    svg.selectAll(".annotation-connector > path")
    .style("stroke", (d,i) => color(d.data.group))

    });
    </script>
    </body>
    </html>
    337 changes: 337 additions & 0 deletions miserables.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,337 @@
    {
    "nodes": [
    { "type": "minor", "id": "Myriel", "group": 1},
    { "type": "other", "id": "Napoleon", "group": 1},
    { "type": "other", "id": "Mlle.Baptistine", "group": 1},
    { "type": "other", "id": "Mme.Magloire", "group": 1},
    { "type": "other", "id": "CountessdeLo", "group": 1},
    { "type": "other", "id": "Geborand", "group": 1},
    { "type": "other", "id": "Champtercier", "group": 1},
    { "type": "other", "id": "Cravatte", "group": 1},
    { "type": "other", "id": "Count", "group": 1},
    { "type": "other", "id": "OldMan", "group": 1},
    { "type": "other", "id": "Labarre", "group": 2},
    { "type": "major", "id": "Valjean", "group": 2},
    { "type": "other", "id": "Marguerite", "group": 3},
    { "type": "other", "id": "Mme.deR", "group": 2},
    { "type": "other", "id": "Isabeau", "group": 2},
    { "type": "other", "id": "Gervais", "group": 2},
    { "type": "minor", "id": "Tholomyes", "group": 3},
    { "type": "other", "id": "Listolier", "group": 3},
    { "type": "other", "id": "Fameuil", "group": 3},
    { "type": "other", "id": "Blacheville", "group": 3},
    { "type": "other", "id": "Favourite", "group": 3},
    { "type": "other", "id": "Dahlia", "group": 3},
    { "type": "other", "id": "Zephine", "group": 3},
    { "type": "major", "id": "Fantine", "group": 3},
    { "type": "major", "id": "Mme.Thenardier", "group": 4},
    { "type": "major", "id": "Thenardier", "group": 4},
    { "type": "major", "id": "Cosette", "group": 5},
    { "type": "major", "id": "Javert", "group": 4},
    { "type": "minor", "id": "Fauchelevent", "group": 0},
    { "type": "minor", "id": "Bamatabois", "group": 2},
    { "type": "other", "id": "Perpetue", "group": 3},
    { "type": "minor", "id": "Simplice", "group": 2},
    { "type": "other", "id": "Scaufflaire", "group": 2},
    { "type": "other", "id": "Woman1", "group": 2},
    { "type": "other", "id": "Judge", "group": 2},
    { "type": "minor", "id": "Champmathieu", "group": 2},
    { "type": "minor", "id": "Brevet", "group": 2},
    { "type": "minor", "id": "Chenildieu", "group": 2},
    { "type": "minor", "id": "Cochepaille", "group": 2},
    { "type": "minor", "id": "Pontmercy", "group": 4},
    { "type": "other", "id": "Boulatruelle", "group": 6},
    { "type": "major", "id": "Eponine", "group": 4},
    { "type": "other", "id": "Anzelma", "group": 4},
    { "type": "other", "id": "Woman2", "group": 5},
    { "type": "minor", "id": "MotherInnocent", "group": 0},
    { "type": "other", "id": "Gribier", "group": 0},
    { "type": "other", "id": "Jondrette", "group": 7},
    { "type": "other", "id": "Mme.Burgon", "group": 7},
    { "type": "major", "id": "Gavroche", "group": 8},
    { "type": "minor", "id": "Gillenormand", "group": 5},
    { "type": "minor", "id": "Magnon", "group": 5},
    { "type": "minor", "id": "Mlle.Gillenormand", "group": 5},
    { "type": "other", "id": "Mme.Pontmercy", "group": 5},
    { "type": "other", "id": "Mlle.Vaubois", "group": 5},
    { "type": "other", "id": "Lt.Gillenormand", "group": 5},
    { "type": "major", "id": "Marius", "group": 8},
    { "type": "other", "id": "BaronessT", "group": 5},
    { "type": "minor", "id": "Mabeuf", "group": 8},
    { "type": "abc", "id": "Enjolras", "group": 8},
    { "type": "abc", "id": "Combeferre", "group": 8},
    { "type": "abc", "id": "Prouvaire", "group": 8},
    { "type": "abc", "id": "Feuilly", "group": 8},
    { "type": "abc", "id": "Courfeyrac", "group": 8},
    { "type": "other", "id": "Bahorel", "group": 8},
    { "type": "other", "id": "Bossuet", "group": 8},
    { "type": "abc", "id": "Joly", "group": 8},
    { "type": "abc", "id": "Grantaire", "group": 8},
    { "type": "other", "id": "MotherPlutarch", "group": 9},
    { "type": "other", "id": "Gueulemer", "group": 4},
    { "type": "other", "id": "Babet", "group": 4},
    { "type": "other", "id": "Claquesous", "group": 4},
    { "type": "other", "id": "Montparnasse", "group": 4},
    { "type": "minor", "id": "Toussaint", "group": 5},
    { "type": "other", "id": "Child1", "group": 10},
    { "type": "other", "id": "Child2", "group": 10},
    { "type": "minor", "id": "Brujon", "group": 4},
    { "type": "other", "id": "Mme.Hucheloup", "group": 8}
    ],
    "links": [
    {"source": "Napoleon", "target": "Myriel", "value": 1},
    {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8},
    {"source": "Mme.Magloire", "target": "Myriel", "value": 10},
    {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6},
    {"source": "CountessdeLo", "target": "Myriel", "value": 1},
    {"source": "Geborand", "target": "Myriel", "value": 1},
    {"source": "Champtercier", "target": "Myriel", "value": 1},
    {"source": "Cravatte", "target": "Myriel", "value": 1},
    {"source": "Count", "target": "Myriel", "value": 2},
    {"source": "OldMan", "target": "Myriel", "value": 1},
    {"source": "Valjean", "target": "Labarre", "value": 1},
    {"source": "Valjean", "target": "Mme.Magloire", "value": 3},
    {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3},
    {"source": "Valjean", "target": "Myriel", "value": 5},
    {"source": "Marguerite", "target": "Valjean", "value": 1},
    {"source": "Mme.deR", "target": "Valjean", "value": 1},
    {"source": "Isabeau", "target": "Valjean", "value": 1},
    {"source": "Gervais", "target": "Valjean", "value": 1},
    {"source": "Listolier", "target": "Tholomyes", "value": 4},
    {"source": "Fameuil", "target": "Tholomyes", "value": 4},
    {"source": "Fameuil", "target": "Listolier", "value": 4},
    {"source": "Blacheville", "target": "Tholomyes", "value": 4},
    {"source": "Blacheville", "target": "Listolier", "value": 4},
    {"source": "Blacheville", "target": "Fameuil", "value": 4},
    {"source": "Favourite", "target": "Tholomyes", "value": 3},
    {"source": "Favourite", "target": "Listolier", "value": 3},
    {"source": "Favourite", "target": "Fameuil", "value": 3},
    {"source": "Favourite", "target": "Blacheville", "value": 4},
    {"source": "Dahlia", "target": "Tholomyes", "value": 3},
    {"source": "Dahlia", "target": "Listolier", "value": 3},
    {"source": "Dahlia", "target": "Fameuil", "value": 3},
    {"source": "Dahlia", "target": "Blacheville", "value": 3},
    {"source": "Dahlia", "target": "Favourite", "value": 5},
    {"source": "Zephine", "target": "Tholomyes", "value": 3},
    {"source": "Zephine", "target": "Listolier", "value": 3},
    {"source": "Zephine", "target": "Fameuil", "value": 3},
    {"source": "Zephine", "target": "Blacheville", "value": 3},
    {"source": "Zephine", "target": "Favourite", "value": 4},
    {"source": "Zephine", "target": "Dahlia", "value": 4},
    {"source": "Fantine", "target": "Tholomyes", "value": 3},
    {"source": "Fantine", "target": "Listolier", "value": 3},
    {"source": "Fantine", "target": "Fameuil", "value": 3},
    {"source": "Fantine", "target": "Blacheville", "value": 3},
    {"source": "Fantine", "target": "Favourite", "value": 4},
    {"source": "Fantine", "target": "Dahlia", "value": 4},
    {"source": "Fantine", "target": "Zephine", "value": 4},
    {"source": "Fantine", "target": "Marguerite", "value": 2},
    {"source": "Fantine", "target": "Valjean", "value": 9},
    {"source": "Mme.Thenardier", "target": "Fantine", "value": 2},
    {"source": "Mme.Thenardier", "target": "Valjean", "value": 7},
    {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13},
    {"source": "Thenardier", "target": "Fantine", "value": 1},
    {"source": "Thenardier", "target": "Valjean", "value": 12},
    {"source": "Cosette", "target": "Mme.Thenardier", "value": 4},
    {"source": "Cosette", "target": "Valjean", "value": 31},
    {"source": "Cosette", "target": "Tholomyes", "value": 1},
    {"source": "Cosette", "target": "Thenardier", "value": 1},
    {"source": "Javert", "target": "Valjean", "value": 17},
    {"source": "Javert", "target": "Fantine", "value": 5},
    {"source": "Javert", "target": "Thenardier", "value": 5},
    {"source": "Javert", "target": "Mme.Thenardier", "value": 1},
    {"source": "Javert", "target": "Cosette", "value": 1},
    {"source": "Fauchelevent", "target": "Valjean", "value": 8},
    {"source": "Fauchelevent", "target": "Javert", "value": 1},
    {"source": "Bamatabois", "target": "Fantine", "value": 1},
    {"source": "Bamatabois", "target": "Javert", "value": 1},
    {"source": "Bamatabois", "target": "Valjean", "value": 2},
    {"source": "Perpetue", "target": "Fantine", "value": 1},
    {"source": "Simplice", "target": "Perpetue", "value": 2},
    {"source": "Simplice", "target": "Valjean", "value": 3},
    {"source": "Simplice", "target": "Fantine", "value": 2},
    {"source": "Simplice", "target": "Javert", "value": 1},
    {"source": "Scaufflaire", "target": "Valjean", "value": 1},
    {"source": "Woman1", "target": "Valjean", "value": 2},
    {"source": "Woman1", "target": "Javert", "value": 1},
    {"source": "Judge", "target": "Valjean", "value": 3},
    {"source": "Judge", "target": "Bamatabois", "value": 2},
    {"source": "Champmathieu", "target": "Valjean", "value": 3},
    {"source": "Champmathieu", "target": "Judge", "value": 3},
    {"source": "Champmathieu", "target": "Bamatabois", "value": 2},
    {"source": "Brevet", "target": "Judge", "value": 2},
    {"source": "Brevet", "target": "Champmathieu", "value": 2},
    {"source": "Brevet", "target": "Valjean", "value": 2},
    {"source": "Brevet", "target": "Bamatabois", "value": 1},
    {"source": "Chenildieu", "target": "Judge", "value": 2},
    {"source": "Chenildieu", "target": "Champmathieu", "value": 2},
    {"source": "Chenildieu", "target": "Brevet", "value": 2},
    {"source": "Chenildieu", "target": "Valjean", "value": 2},
    {"source": "Chenildieu", "target": "Bamatabois", "value": 1},
    {"source": "Cochepaille", "target": "Judge", "value": 2},
    {"source": "Cochepaille", "target": "Champmathieu", "value": 2},
    {"source": "Cochepaille", "target": "Brevet", "value": 2},
    {"source": "Cochepaille", "target": "Chenildieu", "value": 2},
    {"source": "Cochepaille", "target": "Valjean", "value": 2},
    {"source": "Cochepaille", "target": "Bamatabois", "value": 1},
    {"source": "Pontmercy", "target": "Thenardier", "value": 1},
    {"source": "Boulatruelle", "target": "Thenardier", "value": 1},
    {"source": "Eponine", "target": "Mme.Thenardier", "value": 2},
    {"source": "Eponine", "target": "Thenardier", "value": 3},
    {"source": "Anzelma", "target": "Eponine", "value": 2},
    {"source": "Anzelma", "target": "Thenardier", "value": 2},
    {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1},
    {"source": "Woman2", "target": "Valjean", "value": 3},
    {"source": "Woman2", "target": "Cosette", "value": 1},
    {"source": "Woman2", "target": "Javert", "value": 1},
    {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3},
    {"source": "MotherInnocent", "target": "Valjean", "value": 1},
    {"source": "Gribier", "target": "Fauchelevent", "value": 2},
    {"source": "Mme.Burgon", "target": "Jondrette", "value": 1},
    {"source": "Gavroche", "target": "Mme.Burgon", "value": 2},
    {"source": "Gavroche", "target": "Thenardier", "value": 1},
    {"source": "Gavroche", "target": "Javert", "value": 1},
    {"source": "Gavroche", "target": "Valjean", "value": 1},
    {"source": "Gillenormand", "target": "Cosette", "value": 3},
    {"source": "Gillenormand", "target": "Valjean", "value": 2},
    {"source": "Magnon", "target": "Gillenormand", "value": 1},
    {"source": "Magnon", "target": "Mme.Thenardier", "value": 1},
    {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9},
    {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2},
    {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2},
    {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1},
    {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1},
    {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1},
    {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2},
    {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1},
    {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1},
    {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6},
    {"source": "Marius", "target": "Gillenormand", "value": 12},
    {"source": "Marius", "target": "Pontmercy", "value": 1},
    {"source": "Marius", "target": "Lt.Gillenormand", "value": 1},
    {"source": "Marius", "target": "Cosette", "value": 21},
    {"source": "Marius", "target": "Valjean", "value": 19},
    {"source": "Marius", "target": "Tholomyes", "value": 1},
    {"source": "Marius", "target": "Thenardier", "value": 2},
    {"source": "Marius", "target": "Eponine", "value": 5},
    {"source": "Marius", "target": "Gavroche", "value": 4},
    {"source": "BaronessT", "target": "Gillenormand", "value": 1},
    {"source": "BaronessT", "target": "Marius", "value": 1},
    {"source": "Mabeuf", "target": "Marius", "value": 1},
    {"source": "Mabeuf", "target": "Eponine", "value": 1},
    {"source": "Mabeuf", "target": "Gavroche", "value": 1},
    {"source": "Enjolras", "target": "Marius", "value": 7},
    {"source": "Enjolras", "target": "Gavroche", "value": 7},
    {"source": "Enjolras", "target": "Javert", "value": 6},
    {"source": "Enjolras", "target": "Mabeuf", "value": 1},
    {"source": "Enjolras", "target": "Valjean", "value": 4},
    {"source": "Combeferre", "target": "Enjolras", "value": 15},
    {"source": "Combeferre", "target": "Marius", "value": 5},
    {"source": "Combeferre", "target": "Gavroche", "value": 6},
    {"source": "Combeferre", "target": "Mabeuf", "value": 2},
    {"source": "Prouvaire", "target": "Gavroche", "value": 1},
    {"source": "Prouvaire", "target": "Enjolras", "value": 4},
    {"source": "Prouvaire", "target": "Combeferre", "value": 2},
    {"source": "Feuilly", "target": "Gavroche", "value": 2},
    {"source": "Feuilly", "target": "Enjolras", "value": 6},
    {"source": "Feuilly", "target": "Prouvaire", "value": 2},
    {"source": "Feuilly", "target": "Combeferre", "value": 5},
    {"source": "Feuilly", "target": "Mabeuf", "value": 1},
    {"source": "Feuilly", "target": "Marius", "value": 1},
    {"source": "Courfeyrac", "target": "Marius", "value": 9},
    {"source": "Courfeyrac", "target": "Enjolras", "value": 17},
    {"source": "Courfeyrac", "target": "Combeferre", "value": 13},
    {"source": "Courfeyrac", "target": "Gavroche", "value": 7},
    {"source": "Courfeyrac", "target": "Mabeuf", "value": 2},
    {"source": "Courfeyrac", "target": "Eponine", "value": 1},
    {"source": "Courfeyrac", "target": "Feuilly", "value": 6},
    {"source": "Courfeyrac", "target": "Prouvaire", "value": 3},
    {"source": "Bahorel", "target": "Combeferre", "value": 5},
    {"source": "Bahorel", "target": "Gavroche", "value": 5},
    {"source": "Bahorel", "target": "Courfeyrac", "value": 6},
    {"source": "Bahorel", "target": "Mabeuf", "value": 2},
    {"source": "Bahorel", "target": "Enjolras", "value": 4},
    {"source": "Bahorel", "target": "Feuilly", "value": 3},
    {"source": "Bahorel", "target": "Prouvaire", "value": 2},
    {"source": "Bahorel", "target": "Marius", "value": 1},
    {"source": "Bossuet", "target": "Marius", "value": 5},
    {"source": "Bossuet", "target": "Courfeyrac", "value": 12},
    {"source": "Bossuet", "target": "Gavroche", "value": 5},
    {"source": "Bossuet", "target": "Bahorel", "value": 4},
    {"source": "Bossuet", "target": "Enjolras", "value": 10},
    {"source": "Bossuet", "target": "Feuilly", "value": 6},
    {"source": "Bossuet", "target": "Prouvaire", "value": 2},
    {"source": "Bossuet", "target": "Combeferre", "value": 9},
    {"source": "Bossuet", "target": "Mabeuf", "value": 1},
    {"source": "Bossuet", "target": "Valjean", "value": 1},
    {"source": "Joly", "target": "Bahorel", "value": 5},
    {"source": "Joly", "target": "Bossuet", "value": 7},
    {"source": "Joly", "target": "Gavroche", "value": 3},
    {"source": "Joly", "target": "Courfeyrac", "value": 5},
    {"source": "Joly", "target": "Enjolras", "value": 5},
    {"source": "Joly", "target": "Feuilly", "value": 5},
    {"source": "Joly", "target": "Prouvaire", "value": 2},
    {"source": "Joly", "target": "Combeferre", "value": 5},
    {"source": "Joly", "target": "Mabeuf", "value": 1},
    {"source": "Joly", "target": "Marius", "value": 2},
    {"source": "Grantaire", "target": "Bossuet", "value": 3},
    {"source": "Grantaire", "target": "Enjolras", "value": 3},
    {"source": "Grantaire", "target": "Combeferre", "value": 1},
    {"source": "Grantaire", "target": "Courfeyrac", "value": 2},
    {"source": "Grantaire", "target": "Joly", "value": 2},
    {"source": "Grantaire", "target": "Gavroche", "value": 1},
    {"source": "Grantaire", "target": "Bahorel", "value": 1},
    {"source": "Grantaire", "target": "Feuilly", "value": 1},
    {"source": "Grantaire", "target": "Prouvaire", "value": 1},
    {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3},
    {"source": "Gueulemer", "target": "Thenardier", "value": 5},
    {"source": "Gueulemer", "target": "Valjean", "value": 1},
    {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1},
    {"source": "Gueulemer", "target": "Javert", "value": 1},
    {"source": "Gueulemer", "target": "Gavroche", "value": 1},
    {"source": "Gueulemer", "target": "Eponine", "value": 1},
    {"source": "Babet", "target": "Thenardier", "value": 6},
    {"source": "Babet", "target": "Gueulemer", "value": 6},
    {"source": "Babet", "target": "Valjean", "value": 1},
    {"source": "Babet", "target": "Mme.Thenardier", "value": 1},
    {"source": "Babet", "target": "Javert", "value": 2},
    {"source": "Babet", "target": "Gavroche", "value": 1},
    {"source": "Babet", "target": "Eponine", "value": 1},
    {"source": "Claquesous", "target": "Thenardier", "value": 4},
    {"source": "Claquesous", "target": "Babet", "value": 4},
    {"source": "Claquesous", "target": "Gueulemer", "value": 4},
    {"source": "Claquesous", "target": "Valjean", "value": 1},
    {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1},
    {"source": "Claquesous", "target": "Javert", "value": 1},
    {"source": "Claquesous", "target": "Eponine", "value": 1},
    {"source": "Claquesous", "target": "Enjolras", "value": 1},
    {"source": "Montparnasse", "target": "Javert", "value": 1},
    {"source": "Montparnasse", "target": "Babet", "value": 2},
    {"source": "Montparnasse", "target": "Gueulemer", "value": 2},
    {"source": "Montparnasse", "target": "Claquesous", "value": 2},
    {"source": "Montparnasse", "target": "Valjean", "value": 1},
    {"source": "Montparnasse", "target": "Gavroche", "value": 1},
    {"source": "Montparnasse", "target": "Eponine", "value": 1},
    {"source": "Montparnasse", "target": "Thenardier", "value": 1},
    {"source": "Toussaint", "target": "Cosette", "value": 2},
    {"source": "Toussaint", "target": "Javert", "value": 1},
    {"source": "Toussaint", "target": "Valjean", "value": 1},
    {"source": "Child1", "target": "Gavroche", "value": 2},
    {"source": "Child2", "target": "Gavroche", "value": 2},
    {"source": "Child2", "target": "Child1", "value": 3},
    {"source": "Brujon", "target": "Babet", "value": 3},
    {"source": "Brujon", "target": "Gueulemer", "value": 3},
    {"source": "Brujon", "target": "Thenardier", "value": 3},
    {"source": "Brujon", "target": "Gavroche", "value": 1},
    {"source": "Brujon", "target": "Eponine", "value": 1},
    {"source": "Brujon", "target": "Claquesous", "value": 1},
    {"source": "Brujon", "target": "Montparnasse", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Joly", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1},
    {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1}
    ]
    }
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.