Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zerobias/29f4afced9b8c0f37ad3f065cc0fcc99 to your computer and use it in GitHub Desktop.
Save zerobias/29f4afced9b8c0f37ad3f065cc0fcc99 to your computer and use it in GitHub Desktop.
D3v4 Selectable, Draggable, Zoomable Force Directed Graph

A detailed description of this example can be found at emptypipes.org/2017/04/29/d3v4-selectable-zoomable-force-directed-graph/.

In summary:

  1. Clicking on a node selects it and de-selects everything else.
  2. Shift-clicking on a node toggles its selection status and leaves all other nodes as they are.
  3. Shift-dragging toggles the selection status of all nodes within the selection area.
  4. Dragging on a selected node drags all selected nodes.
  5. Dragging an unselected node selects and drags it while de-selecting everything else.

Upgrading selectable zoomable force directed graph implementation to D3 v4 required a few minor and not-so-minor changes.

  • The new brush in v4 captures the shift, alt and meta keys to perform some actions by default. To get around this, I forked d3-brush and modified it so that it doesn't capture the shift events. The new version (d3-brush-lite) can be found on github. There is an open github issue to disable this behavior in d3-brush.
  • Because the d3-drag behavior consumes all events in v4, it is no longer necessary to stop propagation.
  • The brush creates its own overlay which catches all events meaning that we don't need to turn the zoom behavior off when the shift key is pressed.
  • Whether a node is fixed is specified by the .fx and .fy parameters. This eliminates the need to set the .fixed parameter on each node.
  • The force layout in v4 lets us specify an accessor for the nodes that a link connects. This lets us use ids for a link's endpoint and makes the graph specification JSON easier to read:
let gr = 0
function classifyNode(node) {
if (isFinite(parseInt(node, 36))) return {type: 'int', node, id: node}
if (node.startsWith('point_')) return {type: 'point', node, id: node.replace('point_', '')}
if (node.startsWith('before_')) return {type: 'before', node, id: node.replace('before_', '')}
if (node.startsWith('after_')) return {type: 'after', node, id: node.replace('after_', '')}
throw new Error(`unknown node ${node}`)
}
window.processGraph = function processGraph(graph) {
const nodes = Object.keys(graph)
const classified = nodes.map(classifyNode)
const baseNodes = classified.map(
({node, type, id}) => ({
id: node,
group: parseInt(id, 36) * (type === 'int' ? 0 : 1),
// label: `${type} ${id}`,
// level: type === 'int'
// ? (id === '-1' ? 1 : 2)
// : (type === 'before' ? 1 : 2)
})
)
const links = []
for (const cl of classified) {
const source = cl.node
for (const target of graph[source]) {
const clt = classifyNode(target)
let value
if (clt.id === cl.id) {
value = 7
} else if (cl.type === 'int') {
value = 5
} else {
value = 1
}
links.push({source, target, value})
}
}
return {nodes: baseNodes, links}
}
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-dispatch'), require('d3-drag'), require('d3-interpolate'), require('d3-selection'), require('d3-transition')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-dispatch', 'd3-drag', 'd3-interpolate', 'd3-selection', 'd3-transition'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3,global.d3,global.d3));
}(this, (function (exports,d3Dispatch,d3Drag,d3Interpolate,d3Selection,d3Transition) { 'use strict';
var constant = function(x) {
return function() {
return x;
};
};
var BrushEvent = function(target, type, selection) {
this.target = target;
this.type = type;
this.selection = selection;
};
function nopropagation() {
d3Selection.event.stopImmediatePropagation();
}
var noevent = function() {
d3Selection.event.preventDefault();
d3Selection.event.stopImmediatePropagation();
};
var MODE_DRAG = {name: "drag"};
var MODE_SPACE = {name: "space"};
var MODE_HANDLE = {name: "handle"};
var MODE_CENTER = {name: "center"};
var X = {
name: "x",
handles: ["e", "w"].map(type),
input: function(x, e) { return x && [[x[0], e[0][1]], [x[1], e[1][1]]]; },
output: function(xy) { return xy && [xy[0][0], xy[1][0]]; }
};
var Y = {
name: "y",
handles: ["n", "s"].map(type),
input: function(y, e) { return y && [[e[0][0], y[0]], [e[1][0], y[1]]]; },
output: function(xy) { return xy && [xy[0][1], xy[1][1]]; }
};
var XY = {
name: "xy",
handles: ["n", "e", "s", "w", "nw", "ne", "se", "sw"].map(type),
input: function(xy) { return xy; },
output: function(xy) { return xy; }
};
var cursors = {
overlay: "crosshair",
selection: "move",
n: "ns-resize",
e: "ew-resize",
s: "ns-resize",
w: "ew-resize",
nw: "nwse-resize",
ne: "nesw-resize",
se: "nwse-resize",
sw: "nesw-resize"
};
var flipX = {
e: "w",
w: "e",
nw: "ne",
ne: "nw",
se: "sw",
sw: "se"
};
var flipY = {
n: "s",
s: "n",
nw: "sw",
ne: "se",
se: "ne",
sw: "nw"
};
var signsX = {
overlay: +1,
selection: +1,
n: null,
e: +1,
s: null,
w: -1,
nw: -1,
ne: +1,
se: +1,
sw: -1
};
var signsY = {
overlay: +1,
selection: +1,
n: -1,
e: null,
s: +1,
w: null,
nw: -1,
ne: -1,
se: +1,
sw: +1
};
function type(t) {
return {type: t};
}
// Ignore right-click, since that should open the context menu.
function defaultFilter() {
return !d3Selection.event.button;
}
function defaultExtent() {
var svg = this.ownerSVGElement || this;
return [[0, 0], [svg.width.baseVal.value, svg.height.baseVal.value]];
}
// Like d3.local, but with the name “__brush” rather than auto-generated.
function local(node) {
while (!node.__brush) if (!(node = node.parentNode)) return;
return node.__brush;
}
function empty(extent) {
return extent[0][0] === extent[1][0]
|| extent[0][1] === extent[1][1];
}
function brushSelection(node) {
var state = node.__brush;
return state ? state.dim.output(state.selection) : null;
}
function brushX() {
return brush$1(X);
}
function brushY() {
return brush$1(Y);
}
var brush = function() {
return brush$1(XY);
};
function brush$1(dim) {
var extent = defaultExtent,
filter = defaultFilter,
listeners = d3Dispatch.dispatch(brush, "start", "brush", "end"),
handleSize = 6,
touchending;
function brush(group) {
var overlay = group
.property("__brush", initialize)
.selectAll(".overlay")
.data([type("overlay")]);
overlay.enter().append("rect")
.attr("class", "overlay")
.attr("pointer-events", "all")
.attr("cursor", cursors.overlay)
.merge(overlay)
.each(function() {
var extent = local(this).extent;
d3Selection.select(this)
.attr("x", extent[0][0])
.attr("y", extent[0][1])
.attr("width", extent[1][0] - extent[0][0])
.attr("height", extent[1][1] - extent[0][1]);
});
group.selectAll(".selection")
.data([type("selection")])
.enter().append("rect")
.attr("class", "selection")
.attr("cursor", cursors.selection)
.attr("fill", "#777")
.attr("fill-opacity", 0.3)
.attr("stroke", "#fff")
.attr("shape-rendering", "crispEdges");
var handle = group.selectAll(".handle")
.data(dim.handles, function(d) { return d.type; });
handle.exit().remove();
handle.enter().append("rect")
.attr("class", function(d) { return "handle handle--" + d.type; })
.attr("cursor", function(d) { return cursors[d.type]; });
group
.each(redraw)
.attr("fill", "none")
.attr("pointer-events", "all")
.style("-webkit-tap-highlight-color", "rgba(0,0,0,0)")
.on("mousedown.brush touchstart.brush", started);
}
brush.move = function(group, selection) {
if (group.selection) {
group
.on("start.brush", function() { emitter(this, arguments).beforestart().start(); })
.on("interrupt.brush end.brush", function() { emitter(this, arguments).end(); })
.tween("brush", function() {
var that = this,
state = that.__brush,
emit = emitter(that, arguments),
selection0 = state.selection,
selection1 = dim.input(typeof selection === "function" ? selection.apply(this, arguments) : selection, state.extent),
i = d3Interpolate.interpolate(selection0, selection1);
function tween(t) {
state.selection = t === 1 && empty(selection1) ? null : i(t);
redraw.call(that);
emit.brush();
}
return selection0 && selection1 ? tween : tween(1);
});
} else {
group
.each(function() {
var that = this,
args = arguments,
state = that.__brush,
selection1 = dim.input(typeof selection === "function" ? selection.apply(that, args) : selection, state.extent),
emit = emitter(that, args).beforestart();
d3Transition.interrupt(that);
state.selection = selection1 == null || empty(selection1) ? null : selection1;
redraw.call(that);
emit.start().brush().end();
});
}
};
function redraw() {
var group = d3Selection.select(this),
selection = local(this).selection;
if (selection) {
group.selectAll(".selection")
.style("display", null)
.attr("x", selection[0][0])
.attr("y", selection[0][1])
.attr("width", selection[1][0] - selection[0][0])
.attr("height", selection[1][1] - selection[0][1]);
group.selectAll(".handle")
.style("display", null)
.attr("x", function(d) { return d.type[d.type.length - 1] === "e" ? selection[1][0] - handleSize / 2 : selection[0][0] - handleSize / 2; })
.attr("y", function(d) { return d.type[0] === "s" ? selection[1][1] - handleSize / 2 : selection[0][1] - handleSize / 2; })
.attr("width", function(d) { return d.type === "n" || d.type === "s" ? selection[1][0] - selection[0][0] + handleSize : handleSize; })
.attr("height", function(d) { return d.type === "e" || d.type === "w" ? selection[1][1] - selection[0][1] + handleSize : handleSize; });
}
else {
group.selectAll(".selection,.handle")
.style("display", "none")
.attr("x", null)
.attr("y", null)
.attr("width", null)
.attr("height", null);
}
}
function emitter(that, args) {
return that.__brush.emitter || new Emitter(that, args);
}
function Emitter(that, args) {
this.that = that;
this.args = args;
this.state = that.__brush;
this.active = 0;
}
Emitter.prototype = {
beforestart: function() {
if (++this.active === 1) this.state.emitter = this, this.starting = true;
return this;
},
start: function() {
if (this.starting) this.starting = false, this.emit("start");
return this;
},
brush: function() {
this.emit("brush");
return this;
},
end: function() {
if (--this.active === 0) delete this.state.emitter, this.emit("end");
return this;
},
emit: function(type) {
d3Selection.customEvent(new BrushEvent(brush, type, dim.output(this.state.selection)), listeners.apply, listeners, [type, this.that, this.args]);
}
};
function started() {
if (d3Selection.event.touches) { if (d3Selection.event.changedTouches.length < d3Selection.event.touches.length) return noevent(); }
else if (touchending) return;
if (!filter.apply(this, arguments)) return;
var that = this,
type = d3Selection.event.target.__data__.type,
mode = (d3Selection.event.metaKey ? type = "overlay" : type) === "selection" ? MODE_DRAG : (d3Selection.event.altKey ? MODE_CENTER : MODE_HANDLE),
signX = dim === Y ? null : signsX[type],
signY = dim === X ? null : signsY[type],
state = local(that),
extent = state.extent,
selection = state.selection,
W = extent[0][0], w0, w1,
N = extent[0][1], n0, n1,
E = extent[1][0], e0, e1,
S = extent[1][1], s0, s1,
dx,
dy,
moving,
lockX,
lockY,
point0 = d3Selection.mouse(that),
point = point0,
emit = emitter(that, arguments).beforestart();
if (type === "overlay") {
state.selection = selection = [
[w0 = dim === Y ? W : point0[0], n0 = dim === X ? N : point0[1]],
[e0 = dim === Y ? E : w0, s0 = dim === X ? S : n0]
];
} else {
w0 = selection[0][0];
n0 = selection[0][1];
e0 = selection[1][0];
s0 = selection[1][1];
}
w1 = w0;
n1 = n0;
e1 = e0;
s1 = s0;
var group = d3Selection.select(that)
.attr("pointer-events", "none");
var overlay = group.selectAll(".overlay")
.attr("cursor", cursors[type]);
if (d3Selection.event.touches) {
group
.on("touchmove.brush", moved, true)
.on("touchend.brush touchcancel.brush", ended, true);
} else {
var view = d3Selection.select(d3Selection.event.view)
.on("keydown.brush", keydowned, true)
.on("keyup.brush", keyupped, true)
.on("mousemove.brush", moved, true)
.on("mouseup.brush", ended, true);
d3Drag.dragDisable(d3Selection.event.view);
}
nopropagation();
d3Transition.interrupt(that);
redraw.call(that);
emit.start();
function moved() {
var point1 = d3Selection.mouse(that);
point = point1;
moving = true;
noevent();
move();
}
function move() {
var t;
dx = point[0] - point0[0];
dy = point[1] - point0[1];
switch (mode) {
case MODE_SPACE:
case MODE_DRAG: {
if (signX) dx = Math.max(W - w0, Math.min(E - e0, dx)), w1 = w0 + dx, e1 = e0 + dx;
if (signY) dy = Math.max(N - n0, Math.min(S - s0, dy)), n1 = n0 + dy, s1 = s0 + dy;
break;
}
case MODE_HANDLE: {
if (signX < 0) dx = Math.max(W - w0, Math.min(E - w0, dx)), w1 = w0 + dx, e1 = e0;
else if (signX > 0) dx = Math.max(W - e0, Math.min(E - e0, dx)), w1 = w0, e1 = e0 + dx;
if (signY < 0) dy = Math.max(N - n0, Math.min(S - n0, dy)), n1 = n0 + dy, s1 = s0;
else if (signY > 0) dy = Math.max(N - s0, Math.min(S - s0, dy)), n1 = n0, s1 = s0 + dy;
break;
}
case MODE_CENTER: {
if (signX) w1 = Math.max(W, Math.min(E, w0 - dx * signX)), e1 = Math.max(W, Math.min(E, e0 + dx * signX));
if (signY) n1 = Math.max(N, Math.min(S, n0 - dy * signY)), s1 = Math.max(N, Math.min(S, s0 + dy * signY));
break;
}
}
if (e1 < w1) {
signX *= -1;
t = w0, w0 = e0, e0 = t;
t = w1, w1 = e1, e1 = t;
if (type in flipX) overlay.attr("cursor", cursors[type = flipX[type]]);
}
if (s1 < n1) {
signY *= -1;
t = n0, n0 = s0, s0 = t;
t = n1, n1 = s1, s1 = t;
if (type in flipY) overlay.attr("cursor", cursors[type = flipY[type]]);
}
if (state.selection) selection = state.selection; // May be set by brush.move!
if (lockX) w1 = selection[0][0], e1 = selection[1][0];
if (lockY) n1 = selection[0][1], s1 = selection[1][1];
if (selection[0][0] !== w1
|| selection[0][1] !== n1
|| selection[1][0] !== e1
|| selection[1][1] !== s1) {
state.selection = [[w1, n1], [e1, s1]];
redraw.call(that);
emit.brush();
}
}
function ended() {
nopropagation();
if (d3Selection.event.touches) {
if (d3Selection.event.touches.length) return;
if (touchending) clearTimeout(touchending);
touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
group.on("touchmove.brush touchend.brush touchcancel.brush", null);
} else {
d3Drag.dragEnable(d3Selection.event.view, moving);
view.on("keydown.brush keyup.brush mousemove.brush mouseup.brush", null);
}
group.attr("pointer-events", "all");
overlay.attr("cursor", cursors.overlay);
if (state.selection) selection = state.selection; // May be set by brush.move (on start)!
if (empty(selection)) state.selection = null, redraw.call(that);
emit.end();
}
function keydowned() {
switch (d3Selection.event.keyCode) {
case 18: { // ALT
if (mode === MODE_HANDLE) {
if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
mode = MODE_CENTER;
move();
}
break;
}
case 32: { // SPACE; takes priority over ALT
if (mode === MODE_HANDLE || mode === MODE_CENTER) {
if (signX < 0) e0 = e1 - dx; else if (signX > 0) w0 = w1 - dx;
if (signY < 0) s0 = s1 - dy; else if (signY > 0) n0 = n1 - dy;
mode = MODE_SPACE;
overlay.attr("cursor", cursors.selection);
move();
}
break;
}
default: return;
}
noevent();
}
function keyupped() {
switch (d3Selection.event.keyCode) {
case 18: { // ALT
if (mode === MODE_CENTER) {
if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
mode = MODE_HANDLE;
move();
}
break;
}
case 32: { // SPACE
if (mode === MODE_SPACE) {
if (d3Selection.event.altKey) {
if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
mode = MODE_CENTER;
} else {
if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
mode = MODE_HANDLE;
}
overlay.attr("cursor", cursors[type]);
move();
}
break;
}
default: return;
}
noevent();
}
}
function initialize() {
var state = this.__brush || {selection: null};
state.extent = extent.apply(this, arguments);
state.dim = dim;
return state;
}
brush.extent = function(_) {
return arguments.length ? (extent = typeof _ === "function" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), brush) : extent;
};
brush.filter = function(_) {
return arguments.length ? (filter = typeof _ === "function" ? _ : constant(!!_), brush) : filter;
};
brush.handleSize = function(_) {
return arguments.length ? (handleSize = +_, brush) : handleSize;
};
brush.on = function() {
var value = listeners.on.apply(listeners, arguments);
return value === listeners ? brush : value;
};
return brush;
}
exports.brush = brush;
exports.brushX = brushX;
exports.brushY = brushY;
exports.brushSelection = brushSelection;
Object.defineProperty(exports, '__esModule', { value: true });
})));
function createV4SelectableForceDirectedGraph(svg, graph) {
// if both d3v3 and d3v4 are loaded, we'll assume
// that d3v4 is called d3v4, otherwise we'll assume
// that d3v4 is the default (d3)
if (typeof d3v4 == 'undefined') d3v4 = d3
const width = +svg.attr('width')
const height = +svg.attr('height')
let parentWidth = d3v4.select('svg').node().parentNode.clientWidth
let parentHeight = d3v4.select('svg').node().parentNode.clientHeight
var svg = d3v4
.select('svg')
.attr('width', parentWidth)
.attr('height', parentHeight)
// remove any previous graphs
svg.selectAll('.g-main').remove()
const gMain = svg.append('g').classed('g-main', true)
const rect = gMain
.append('rect')
.attr('width', parentWidth)
.attr('height', parentHeight)
.style('fill', 'white')
const gDraw = gMain.append('g')
const zoom = d3v4.zoom().on('zoom', zoomed)
gMain.call(zoom)
function zoomed() {
gDraw.attr('transform', d3v4.event.transform)
}
const color = d3v4.scaleOrdinal(d3v4.schemeCategory20)
if (!('links' in graph)) {
console.log('Graph is missing links')
return
}
const nodes = {}
let i
for (i = 0; i < graph.nodes.length; i++) {
nodes[graph.nodes[i].id] = graph.nodes[i]
graph.nodes[i].weight = 1.01
}
// the brush needs to go before the nodes so that it doesn't
// get called when the mouse is over a node
const gBrushHolder = gDraw.append('g')
let gBrush = null
const link = gDraw
.append('g')
.attr('class', 'link')
.selectAll('line')
.data(graph.links)
.enter()
.append('line')
.attr('stroke-width', ({value}) => Math.sqrt(value))
const node = gDraw
.append('g')
.attr('class', 'node')
.selectAll('circle')
.data(graph.nodes)
.enter()
.append('circle')
.attr('r', 5)
.attr('fill', d => {
if ('color' in d) return d.color
else return color(d.group)
})
.call(
d3v4
.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
)
// add titles for mouseover blurbs
node.append('title').text(d => {
if ('name' in d) return d.name
else return d.id
})
const simulation = d3v4
.forceSimulation()
.force(
'link',
d3v4
.forceLink()
.id(({id}) => id)
.distance(d => {
return 30
//var dist = 20 / d.value;
//console.log('dist:', dist);
return dist
})
)
.force('charge', d3v4.forceManyBody())
.force('center', d3v4.forceCenter(parentWidth / 2, parentHeight / 2))
.force('x', d3v4.forceX(parentWidth / 2))
.force('y', d3v4.forceY(parentHeight / 2))
simulation.nodes(graph.nodes).on('tick', ticked)
simulation.force('link').links(graph.links)
function ticked() {
// update node and line positions at every step of
// the force simulation
link
.attr('x1', ({source}) => source.x)
.attr('y1', ({source}) => source.y)
.attr('x2', ({target}) => target.x)
.attr('y2', ({target}) => target.y)
node.attr('cx', ({x}) => x).attr('cy', ({y}) => y)
}
let brushMode = false
let brushing = false
const brush = d3v4
.brush()
.on('start', brushstarted)
.on('brush', brushed)
.on('end', brushended)
function brushstarted() {
// keep track of whether we're actively brushing so that we
// don't remove the brush on keyup in the middle of a selection
brushing = true
node.each(d => {
d.previouslySelected = shiftKey && d.selected
})
}
rect.on('click', () => {
node.each(d => {
d.selected = false
d.previouslySelected = false
})
node.classed('selected', false)
})
function brushed() {
if (!d3v4.event.sourceEvent) return
if (!d3v4.event.selection) return
const extent = d3v4.event.selection
node.classed(
'selected',
d =>
(d.selected =
d.previouslySelected ^
(extent[0][0] <= d.x &&
d.x < extent[1][0] &&
extent[0][1] <= d.y &&
d.y < extent[1][1]))
)
}
function brushended() {
if (!d3v4.event.sourceEvent) return
if (!d3v4.event.selection) return
if (!gBrush) return
gBrush.call(brush.move, null)
if (!brushMode) {
// the shift key has been release before we ended our brushing
gBrush.remove()
gBrush = null
}
brushing = false
}
d3v4.select('body').on('keydown', keydown)
d3v4.select('body').on('keyup', keyup)
var shiftKey
function keydown() {
shiftKey = d3v4.event.shiftKey
if (shiftKey) {
// if we already have a brush, don't do anything
if (gBrush) return
brushMode = true
if (!gBrush) {
gBrush = gBrushHolder.append('g')
gBrush.call(brush)
}
}
}
function keyup() {
shiftKey = false
brushMode = false
if (!gBrush) return
if (!brushing) {
// only remove the brush if we're not actively brushing
// otherwise it'll be removed when the brushing ends
gBrush.remove()
gBrush = null
}
}
function dragstarted(d) {
if (!d3v4.event.active) simulation.alphaTarget(0.9).restart()
if (!d.selected && !shiftKey) {
// if this node isn't selected, then we have to unselect every other node
node.classed('selected', p => (p.selected = p.previouslySelected = false))
}
d3v4.select(this).classed('selected', p => {
d.previouslySelected = d.selected
return (d.selected = true)
})
node
.filter(({selected}) => selected)
.each(d => {
//d.fixed |= 2;
d.fx = d.x
d.fy = d.y
})
}
function dragged(d) {
//d.fx = d3v4.event.x;
//d.fy = d3v4.event.y;
node
.filter(({selected}) => selected)
.each(d => {
d.fx += d3v4.event.dx
d.fy += d3v4.event.dy
})
}
function dragended(d) {
if (!d3v4.event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
node
.filter(({selected}) => selected)
.each(d => {
//d.fixed &= ~6;
d.fx = null
d.fy = null
})
}
const texts = [
'Use the scroll wheel to zoom',
'Hold the shift key to select nodes'
]
svg
.selectAll('text')
.data(texts)
.enter()
.append('text')
.attr('x', 900)
.attr('y', (d, i) => 470 + i * 18)
.text(d => d)
return graph
}
#d3_selectable_force_directed_graph svg {
border: 1px solid;
font: 13px sans-serif;
text-anchor: end;
}
#d3_selectable_force_directed_graph .node {
stroke: #fff;
stroke-width: 1.5px;
}
.node .selected {
stroke: black;
}
.link {
stroke: #999;
}
window.graphHandlers = {}
window.graphData = window.processGraph({
// '-1': ['1', 'before_1'],
'1': [],
'4': [ '1', 'b' ],
'5': [],
'8': [ '5', 'v' ],
'11': [ 'w', '1z' ],
'12': [],
'15': [ '12', '11', '1a' ],
'20': [],
'25': [ '20', '2r', '2n', '2x' ],
'26': [],
'29': [ '26', '25', '2e' ],
'31': [ '2y', '2x', '36' ],
'36': [ '2x' ],
'54': [ '4z', '5x', '6h' ],
'55': [],
'58': [ '55', '54', '5d' ],
'63': [ '5y', '71' ],
'64': [],
'67': [ '64', '63', '6c' ],
'71': [],
'72': [],
'77': [ '72', '8d', '89', '8j' ],
'78': [],
'80': [ '7r' ],
'85': [],
'86': [],
'89': [ '86', '8x' ],
'93': [ '8y' ],
'94': [],
'97': [ '94', '93', '9c' ],
before_1: [ 'point_1' ],
point_1: [ 'after_1' ],
after_1: [ 'before_3' ],
before_2: [ 'point_2' ],
point_2: [ 'after_2' ],
after_2: [ 'before_8' ],
before_3: [ 'point_3' ],
point_3: [ 'after_3' ],
after_3: [ 'before_2' ],
b: [ '8' ],
before_4: [ 'point_4' ],
point_4: [ 'after_4' ],
after_4: [ 'before_d', 'before_n' ],
h: [ 'c', '1f', '2j' ],
c: [],
l: [ 'i', 'h', 'q' ],
i: [],
before_5: [ 'point_5' ],
point_5: [ 'after_5' ],
after_5: [ 'before_6', 'before_7' ],
before_6: [ 'point_6' ],
point_6: [ 'after_6' ],
after_6: [ 'before_4' ],
q: [ 'h' ],
before_7: [ 'point_7' ],
point_7: [ 'after_7' ],
after_7: [ 'before_4' ],
v: [ 'h' ],
before_8: [ 'point_8' ],
point_8: [ 'after_8' ],
after_8: [ 'before_4' ],
before_9: [ 'point_9' ],
point_9: [ 'after_9' ],
after_9: [ 'before_i' ],
w: [],
before_a: [ 'point_a' ],
point_a: [ 'after_a' ],
after_a: [ 'before_b', 'before_c' ],
before_b: [ 'point_b' ],
point_b: [ 'after_b' ],
after_b: [ 'before_9' ],
'1a': [ '11' ],
before_c: [ 'point_c' ],
point_c: [ 'after_c' ],
after_c: [ 'before_9' ],
'1f': [],
before_d: [ 'point_d' ],
point_d: [ 'after_d' ],
after_d: [ 'before_9' ],
before_e: [ 'point_e' ],
point_e: [ 'after_e' ],
after_e: [ 'before_u' ],
'1l': [ '1g', '2p', '2n', '2x' ],
'1g': [],
'1p': [ '1m', '1l', '1u' ],
'1m': [],
before_f: [ 'point_f' ],
point_f: [ 'after_f' ],
after_f: [ 'before_g', 'before_h' ],
before_g: [ 'point_g' ],
point_g: [ 'after_g' ],
after_g: [ 'before_e' ],
'1u': [ '1l' ],
before_h: [ 'point_h' ],
point_h: [ 'after_h' ],
after_h: [ 'before_e' ],
'1z': [],
before_i: [ 'point_i' ],
point_i: [ 'after_i' ],
after_i: [ 'before_e' ],
before_j: [ 'point_j' ],
point_j: [ 'after_j' ],
after_j: [ 'before_v' ],
before_k: [ 'point_k' ],
point_k: [ 'after_k' ],
after_k: [ 'before_l', 'before_m' ],
before_l: [ 'point_l' ],
point_l: [ 'after_l' ],
after_l: [ 'before_j' ],
'2e': [ '25' ],
before_m: [ 'point_m' ],
point_m: [ 'after_m' ],
after_m: [ 'before_j' ],
'2j': [],
before_n: [ 'point_n' ],
point_n: [ 'after_n' ],
after_n: [ 'before_j' ],
'2n': [ '2k', '3b' ],
'2k': [],
before_o: [ 'point_o' ],
point_o: [ 'after_o' ],
after_o: [ 'before_t' ],
'2p': [],
'2r': [],
before_p: [ 'point_p' ],
point_p: [ 'after_p' ],
after_p: [ 'before_10' ],
'2x': [ '2s', '3v' ],
'2s': [],
'2y': [],
before_q: [ 'point_q' ],
point_q: [ 'after_q' ],
after_q: [ 'before_r', 'before_s' ],
before_r: [ 'point_r' ],
point_r: [ 'after_r' ],
after_r: [ 'before_p' ],
before_s: [ 'point_s' ],
point_s: [ 'after_s' ],
after_s: [ 'before_p' ],
'3b': [ '2x' ],
before_t: [ 'point_t' ],
point_t: [ 'after_t' ],
after_t: [ 'before_p' ],
before_u: [ 'point_u' ],
point_u: [ 'after_u' ],
after_u: [ 'before_p' ],
before_v: [ 'point_v' ],
point_v: [ 'after_v' ],
after_v: [ 'before_p' ],
before_w: [ 'point_w' ],
point_w: [ 'after_w' ],
after_w: [ 'before_12' ],
'3h': [ '3c', '3x' ],
'3c': [],
'3l': [ '3i', '3h', '3q' ],
'3i': [],
before_x: [ 'point_x' ],
point_x: [ 'after_x' ],
after_x: [ 'before_y', 'before_z' ],
before_y: [ 'point_y' ],
point_y: [ 'after_y' ],
after_y: [ 'before_w' ],
'3q': [ '3h' ],
before_z: [ 'point_z' ],
point_z: [ 'after_z' ],
after_z: [ 'before_w' ],
'3v': [],
before_10: [ 'point_10' ],
point_10: [ 'after_10' ],
after_10: [ 'before_w' ],
'3x': [],
before_11: [ 'point_11' ],
point_11: [ 'after_11' ],
after_11: [],
before_12: [ 'point_12' ],
point_12: [ 'after_12' ],
after_12: [ 'before_11' ],
before_13: [ 'point_13' ],
point_13: [ 'after_13' ],
after_13: [ 'before_1b', 'before_1g' ],
'4z': [],
before_14: [ 'point_14' ],
point_14: [ 'after_14' ],
after_14: [ 'before_15', 'before_16' ],
before_15: [ 'point_15' ],
point_15: [ 'after_15' ],
after_15: [ 'before_13' ],
'5d': [ '54' ],
before_16: [ 'point_16' ],
point_16: [ 'after_16' ],
after_16: [ 'before_13' ],
before_17: [ 'point_17' ],
point_17: [ 'after_17' ],
after_17: [ 'before_1v' ],
'5j': [ '5e', '85' ],
'5e': [],
'5n': [ '5k', '5j', '5s' ],
'5k': [],
before_18: [ 'point_18' ],
point_18: [ 'after_18' ],
after_18: [ 'before_19', 'before_1a' ],
before_19: [ 'point_19' ],
point_19: [ 'after_19' ],
after_19: [ 'before_17' ],
'5s': [ '5j' ],
before_1a: [ 'point_1a' ],
point_1a: [ 'after_1a' ],
after_1a: [ 'before_17' ],
'5x': [],
before_1b: [ 'point_1b' ],
point_1b: [ 'after_1b' ],
after_1b: [ 'before_17' ],
before_1c: [ 'point_1c' ],
point_1c: [ 'after_1c' ],
after_1c: [ 'before_1l' ],
'5y': [],
before_1d: [ 'point_1d' ],
point_1d: [ 'after_1d' ],
after_1d: [ 'before_1e', 'before_1f' ],
before_1e: [ 'point_1e' ],
point_1e: [ 'after_1e' ],
after_1e: [ 'before_1c' ],
'6c': [ '63' ],
before_1f: [ 'point_1f' ],
point_1f: [ 'after_1f' ],
after_1f: [ 'before_1c' ],
'6h': [],
before_1g: [ 'point_1g' ],
point_1g: [ 'after_1g' ],
after_1g: [ 'before_1c' ],
before_1h: [ 'point_1h' ],
point_1h: [ 'after_1h' ],
after_1h: [ 'before_1q' ],
'6n': [ '6i', '7l' ],
'6i': [],
'6r': [ '6o', '6n', '6w' ],
'6o': [],
before_1i: [ 'point_1i' ],
point_1i: [ 'after_1i' ],
after_1i: [ 'before_1j', 'before_1k' ],
before_1j: [ 'point_1j' ],
point_1j: [ 'after_1j' ],
after_1j: [ 'before_1h' ],
'6w': [ '6n' ],
before_1k: [ 'point_1k' ],
point_1k: [ 'after_1k' ],
after_1k: [ 'before_1h' ],
before_1l: [ 'point_1l' ],
point_1l: [ 'after_1l' ],
after_1l: [ 'before_1h' ],
before_1m: [ 'point_1m' ],
point_1m: [ 'after_1m' ],
after_1m: [ 'before_23' ],
'7b': [ '78', '77', '7g' ],
before_1n: [ 'point_1n' ],
point_1n: [ 'after_1n' ],
after_1n: [ 'before_1o', 'before_1p' ],
before_1o: [ 'point_1o' ],
point_1o: [ 'after_1o' ],
after_1o: [ 'before_1m' ],
'7g': [ '77' ],
before_1p: [ 'point_1p' ],
point_1p: [ 'after_1p' ],
after_1p: [ 'before_1m' ],
'7l': [],
before_1q: [ 'point_1q' ],
point_1q: [ 'after_1q' ],
after_1q: [ 'before_1m' ],
before_1r: [ 'point_1r' ],
point_1r: [ 'after_1r' ],
after_1r: [ 'before_22' ],
'7r': [ '7m', '8b', '89', '8j' ],
'7m': [],
'7v': [ '7s', '7r', '80' ],
'7s': [],
before_1s: [ 'point_1s' ],
point_1s: [ 'after_1s' ],
after_1s: [ 'before_1t', 'before_1u' ],
before_1t: [ 'point_1t' ],
point_1t: [ 'after_1t' ],
after_1t: [ 'before_1r' ],
before_1u: [ 'point_1u' ],
point_1u: [ 'after_1u' ],
after_1u: [ 'before_1r' ],
before_1v: [ 'point_1v' ],
point_1v: [ 'after_1v' ],
after_1v: [ 'before_1r' ],
before_1w: [ 'point_1w' ],
point_1w: [ 'after_1w' ],
after_1w: [ 'before_21' ],
'8b': [],
'8d': [],
before_1x: [ 'point_1x' ],
point_1x: [ 'after_1x' ],
after_1x: [ 'before_28' ],
'8j': [ '8e', '9h' ],
'8e': [],
'8n': [ '8k', '8j', '8s' ],
'8k': [],
before_1y: [ 'point_1y' ],
point_1y: [ 'after_1y' ],
after_1y: [ 'before_1z', 'before_20' ],
before_1z: [ 'point_1z' ],
point_1z: [ 'after_1z' ],
after_1z: [ 'before_1x' ],
'8s': [ '8j' ],
before_20: [ 'point_20' ],
point_20: [ 'after_20' ],
after_20: [ 'before_1x' ],
'8x': [ '8j' ],
before_21: [ 'point_21' ],
point_21: [ 'after_21' ],
after_21: [ 'before_1x' ],
before_22: [ 'point_22' ],
point_22: [ 'after_22' ],
after_22: [ 'before_1x' ],
before_23: [ 'point_23' ],
point_23: [ 'after_23' ],
after_23: [ 'before_1x' ],
before_24: [ 'point_24' ],
point_24: [ 'after_24' ],
after_24: [],
'8y': [],
before_25: [ 'point_25' ],
point_25: [ 'after_25' ],
after_25: [ 'before_26', 'before_27' ],
before_26: [ 'point_26' ],
point_26: [ 'after_26' ],
after_26: [ 'before_24' ],
'9c': [ '93' ],
before_27: [ 'point_27' ],
point_27: [ 'after_27' ],
after_27: [ 'before_24' ],
'9h': [],
before_28: [ 'point_28' ],
point_28: [ 'after_28' ],
after_28: [ 'before_24' ],
'9l': [ '9i', '9s' ],
'9i': [],
before_29: [ 'point_29' ],
point_29: [ 'after_29' ],
after_29: [ 'before_2b' ],
'9p': [ '9m', '9x' ],
'9m': [],
before_2a: [ 'point_2a' ],
point_2a: [ 'after_2a' ],
after_2a: [ 'before_2c' ],
before_2b: [ 'point_2b' ],
point_2b: [ 'after_2b' ],
after_2b: [ 'before_2a' ],
'9s': [ '9p' ],
'9x': [ '54' ],
before_2c: [ 'point_2c' ],
point_2c: [ 'after_2c' ],
after_2c: [ 'before_13' ],
})
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
</head>
<div align='center' id="d3_selectable_force_directed_graph" style="width: 960px; height: 500px; margin: auto; margin-bottom: 12px">
<svg />
</div>
<link rel='stylesheet' href='d3v4-selectable-zoomable-force-directed-graph.css'>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="api.js"></script>
<script src="data.js"></script>
<script src="d3v4-brush-lite.js"></script>
<script src="d3v4-selectable-force-directed-graph.js"></script>
<script>
var svg = d3.select('#d3_selectable_force_directed_graph');
createV4SelectableForceDirectedGraph(svg, window.graphData)
// d3.json('miserables.with-ids.json', function(error, graph) {
// if (!error) {
// //console.log('graph', graph);
// createV4SelectableForceDirectedGraph(svg, graph);
// } else {
// console.error(error);
// }
// });
</script>
{
"nodes": [
{"id": "Myriel", "group": 1},
{"id": "Napoleon", "group": 1},
{"id": "Mlle.Baptistine", "group": 1},
{"id": "Mme.Magloire", "group": 1},
{"id": "CountessdeLo", "group": 1},
{"id": "Geborand", "group": 1},
{"id": "Champtercier", "group": 1},
{"id": "Cravatte", "group": 1},
{"id": "Count", "group": 1},
{"id": "OldMan", "group": 1},
{"id": "Labarre", "group": 2},
{"id": "Valjean", "group": 2},
{"id": "Marguerite", "group": 3},
{"id": "Mme.deR", "group": 2},
{"id": "Isabeau", "group": 2},
{"id": "Gervais", "group": 2},
{"id": "Tholomyes", "group": 3},
{"id": "Listolier", "group": 3},
{"id": "Fameuil", "group": 3},
{"id": "Blacheville", "group": 3},
{"id": "Favourite", "group": 3},
{"id": "Dahlia", "group": 3},
{"id": "Zephine", "group": 3},
{"id": "Fantine", "group": 3},
{"id": "Mme.Thenardier", "group": 4},
{"id": "Thenardier", "group": 4},
{"id": "Cosette", "group": 5},
{"id": "Javert", "group": 4},
{"id": "Fauchelevent", "group": 0},
{"id": "Bamatabois", "group": 2},
{"id": "Perpetue", "group": 3},
{"id": "Simplice", "group": 2},
{"id": "Scaufflaire", "group": 2},
{"id": "Woman1", "group": 2},
{"id": "Judge", "group": 2},
{"id": "Champmathieu", "group": 2},
{"id": "Brevet", "group": 2},
{"id": "Chenildieu", "group": 2},
{"id": "Cochepaille", "group": 2},
{"id": "Pontmercy", "group": 4},
{"id": "Boulatruelle", "group": 6},
{"id": "Eponine", "group": 4},
{"id": "Anzelma", "group": 4},
{"id": "Woman2", "group": 5},
{"id": "MotherInnocent", "group": 0},
{"id": "Gribier", "group": 0},
{"id": "Jondrette", "group": 7},
{"id": "Mme.Burgon", "group": 7},
{"id": "Gavroche", "group": 8},
{"id": "Gillenormand", "group": 5},
{"id": "Magnon", "group": 5},
{"id": "Mlle.Gillenormand", "group": 5},
{"id": "Mme.Pontmercy", "group": 5},
{"id": "Mlle.Vaubois", "group": 5},
{"id": "Lt.Gillenormand", "group": 5},
{"id": "Marius", "group": 8},
{"id": "BaronessT", "group": 5},
{"id": "Mabeuf", "group": 8},
{"id": "Enjolras", "group": 8},
{"id": "Combeferre", "group": 8},
{"id": "Prouvaire", "group": 8},
{"id": "Feuilly", "group": 8},
{"id": "Courfeyrac", "group": 8},
{"id": "Bahorel", "group": 8},
{"id": "Bossuet", "group": 8},
{"id": "Joly", "group": 8},
{"id": "Grantaire", "group": 8},
{"id": "MotherPlutarch", "group": 9},
{"id": "Gueulemer", "group": 4},
{"id": "Babet", "group": 4},
{"id": "Claquesous", "group": 4},
{"id": "Montparnasse", "group": 4},
{"id": "Toussaint", "group": 5},
{"id": "Child1", "group": 10},
{"id": "Child2", "group": 10},
{"id": "Brujon", "group": 4},
{"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}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment