Skip to content

Instantly share code, notes, and snippets.

@make-github-pseudonymous-again
Forked from GerHobbelt/.gitignore
Last active August 29, 2015 14:01
Show Gist options
  • Save make-github-pseudonymous-again/7daf089f0936b9e16aa9 to your computer and use it in GitHub Desktop.
Save make-github-pseudonymous-again/7daf089f0936b9e16aa9 to your computer and use it in GitHub Desktop.
D3.js undirected multigraph where you can add nodes, links and delete them

Derived from the D3.js example gist 3616279.

Features

  • undirected multigraph where you can add nodes, links and delete them

Usage

  • Click on node to select it. From there you can:

    • Click on second node to add a link;
    • Press escape to unselect;
    • Press delete to delete the node;
  • Click on link to select it. From there you can:

    • Press escape to unselect;
    • Press delete to delete the node;
  • Drag node to move entire graph around.

  • ctrl-click on background to add a new node.

  • zoom in/out

Notes

CODE IS MESSY... BIG RED-WHITE DOT IS THE CENTER OF MASS...

The ability to draw circular references is following the overall force design used here, where helper nodes (and ditto links) are used to produce the bezier-based links; a self-reference is nothing but yet another nodeA -> helper -> nodeB link chain where 'nodeB' just happens to be nodeA again.

This code uses two(2) d3.layout.force() forces: one to layout the original nodes (or their group node equivalent), the second is used to layout all the helper nodes (which are only visible in debug modes 1 and 2): the latter are used to construct the bezier paths representing the links.

<!DOCTYPE html>
<html>
<head>
<title>Undirected Multi Graph</title>
<script src="http://d3js.org/d3.v2.js" charset="utf-8"></script>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<link href='http://fonts.googleapis.com/css?family=Gudea:700,400italic,400' rel='stylesheet' type='text/css'>
<style type="text/css">
svg {
border: 10px solid #eee;
}
body {
font: 10px 'Gudea', sans-serif;
}
text {
font-family: 'Gudea', sans-serif;
font-size: 8pt;
}
circle.node {
fill: lightsteelblue;
stroke: #555;
stroke-width: 3px;
}
circle.leaf {
stroke: #fff;
stroke-width: 7px;
cursor: pointer;
}
circle.link-expanded {
stroke: #555;
stroke-width: 3px;
stroke-dasharray: 2px 4px;
}
circle.helper {
stroke-width: 0;
fill: #333;
opacity: 0.6;
}
circle.center-of-mass {
fill: white;
stroke: red;
stroke-width: 5px;
}
path.hlink {
stroke: #333;
stroke-opacity: 0.5;
fill: none;
cursor: pointer;
}
path.hlink.selected {
stroke : hsl(180, 100%, 50%);
}
path.trace-alpha {
stroke: #0ee;
stroke-opacity: 0.8;
stroke-width: 1.5px;
fill: none;
pointer-events: none;
}
path.trace-c2 {
stroke: #3bb;
stroke-opacity: 0.8;
stroke-width: 1.5px;
fill: none;
pointer-events: none;
}
line.link {
stroke: red;
stroke-opacity: 0.7;
pointer-events: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="http://gerhobbelt.github.com/bl.ocks.org-hack/fixit.js" ></script>
<script type="text/javascript" src="./index.js" ></script>
</body>
</html>
var dynamic_multigraph = function(option){
"use strict";
// INPUT
option = $.extend(true, {
data : {"nodes":[],"links":[]},
radius : 20,
stroke : 5,
force1 : {
distance : function (l, i) { return (l.distance + 1) * 35; },
charge : function (v, i) { return v.weight ? -30 * (v.weight + 5) : -500 ; },
gravity : 0.005,
friction : 0.7
},
force2 : {
distance : function (l) { return l.cyclic ? l.distance * 25 : (l.distance + 1) * 10; },
gravity : 0.0, // JUST A TAD OF GRAVITY TO HELP KEEP THOSE CURVY BUTTOCKS DECENT
charge : function (d, i) {
// HELPER NODES HAVE A MEDIUM-TO-HIGH CHARGE, DEPENDING ON THE NUMBER OF LINKS THE RELATED FORCE LINK REPRESENTS.
// HENCE BUNDLES OF LINKS FRO A->B WILL HAVE HELPER NODES WITH HUGE CHARGES: BETTER SPREADING OF THE LINK PATHS.
//
if (d.fixed) return -10;
var l = d.link_ref, c = l.link_count || 1;
if (l.source.link_count > 0 || l.target.link_count > 0) return -30;
return -1;
},
friction : 0.95
},
zoom_range : [0.1, 10],
loop_curve : 0.5,
pathgen : d3.svg.line().interpolate("basis"),
cycle_pathgen : d3.svg.line().interpolate("basis"),
bcolor : 'rgba(0,0,0,0)',
nodeid : function (n) { return n.name; },
fast_stop_threshold : 0.0
}, option);
var width = option.width,
height = option.height,
debug = option.debug, // 0: DISABLE, 1: ALL, 2: ONLY FORCE2 + CURVES, 3: CURVES ONLY
pathgen = option.pathgen,
cycle_pathgen = option.cycle_pathgen,
data = option.data,
root = option.root,
radius = option.radius, // DEFAULT POINT RADIUS
// CYCLIC ~ SELF-REFERENTIAL LINKS: DETERMINES THE 'RADIUS' OF
// THE BEZIER PATH CONSTRUCTED FOR THE LINK;
cycle_curvep = option.loop_curve,
bcolor = option.bcolor;
// ==
// PREPARE DATA STRUCT TO ALSO CARRY OUR 'PATH HELPER NODES':
data.helpers = {
left: {},
right: {},
cyclic: {}
};
// ==
// INIT SVG ROOT ELEMENT
var vis = d3.select(root).append('svg').attr("width", width).attr("height", height);
var debug_g = vis.append("g").attr("class", "debug_g");
var path_trace_alpha = debug_g.append("path").attr("class", "trace-alpha");
var path_trace_c2 = debug_g.append("path").attr("class", "trace-c2");
var superwrap = vis.append("g").attr("width", width).attr("height", height);
var background = superwrap.append('svg:rect')
.attr("width", width)
.attr("height", height)
.attr('fill', bcolor);
var wrap = superwrap.append("g").attr("width", width).attr("height", height);
var linkg = wrap.append("g").attr("class", "linkg");
var helper_nodeg = wrap.append("g").attr("class", "helper_nodeg");
var center_of_mass = wrap.append("g")
.attr("class", "center-of-mass")
.append("circle")
.attr("class", "center-of-mass")
.attr("visibility", (debug == 1) ? "visible" : "hidden")
.attr("r", 10);
var helper_linkg = wrap.append("g").attr("class", "helper_linkg");
var nodeg = wrap.append("g").attr("class", "nodeg");
var edge_text = wrap.append("g").attr("class", "edge-text");
// ==
// LOCAL VARIABLES
var link, hlink, etext,
hnode,
alpha_trace = [],
alpha_line, alpha_scale,
c2_trace = [],
c2_line, c2_scale;
// ==
// DRAWING TOOLS
alpha_line = d3.svg.line()
.x(function (d, i) { return i; })
.y(function (d, i) { return d; });
alpha_scale = d3.scale.pow()
.exponent(-1.0)
.domain([5, 0.005])
.range([10, height - 10])
.clamp(true);
c2_scale = d3.scale.log()
.domain([0.01, width * height])
.range([10, height - 10])
.clamp(true);
c2_line = d3.svg.line()
.x(function (d, i) { return i; })
.y(function (d, i) { return d; });
var fill = d3.scale.category20();
// ==
// ID GENERATORS
var nodeid = option.nodeid;
var linkid = function (l) {
var u = nodeid(l.source), v = nodeid(l.target);
return u < v ? u + "|" + v : v + "|" + u;
};
// ==
// INIT DATA STRUCTURES
var nm = {}, // NODE MAP
nml = {}, // NODE MAP FOR LEFT-SIDE 'LINK PATH HELPER NODES'
nmr = {}, // NODE MAP FOR RIGHT-SIDE 'LINK PATH HELPER NODES'
nmc = {}, // NODE MAP FOR CYCLIC ~ SELF-REFERENCING 'LINK PATH HELPER NODES'
nmimg = {}, // NODE MAP FOR CLONED NODES FOR FORCE2
lm = {}, // LINK MAPS - LM ~ LML-LMM-LMR
lml = {},
lmm = {},
lmr = {},
lmc = {},
nodes = [], // OUTPUT NODES
helper_nodes = []; // HELPER FORCE GRAPH NODES
var PREV_UUID = -1;
var uuid = function(){ return ++PREV_UUID; };
var add_edge = function (net, e) {
var u, v,
rui, rvi, ui, vi, lu, rv, uimg, vimg,
i, ix,
l, ll, l_, lr;
// WHILE D3.LAYOUT.FORCE DOES CONVERT LINK.SOURCE AND LINK.TARGET NUMERIC VALUES TO DIRECT NODE REFERENCES,
// IT DOESN'T FOR OTHER ATTRIBUTES, SUCH AS .REAL_SOURCE, SO WE DO NOT USE INDEXES IN NM[] BUT DIRECT NODE
// REFERENCES TO SKIP THE D3.LAYOUT.FORCE IMPLICIT LINKS CONVERSION LATER ON AND ENSURE THAT BOTH .SOURCE/.TARGET
// AND .REAL_SOURCE/.REAL_TARGET ARE OF THE SAME TYPE AND POINTING AT VALID NODES.
rui = nodeid(e.source);
rvi = nodeid(e.target);
u = nm[rui];
v = nm[rvi];
if (u === v) {
// SKIP NON-ORIGINAL LINKS FROM NODE TO SAME (A-A); THEY ARE RENDERED AS 0-LENGTH LINES ANYHOW. LESS LINKS IN ARRAY = FASTER ANIMATION.
// SELF-REFERENTIAL 'LINKS' ARE PRODUCED AS 2 LINKS+1 HELPER NODE; THIS IS A GENERALIZED APPROACH SO WE
// CAN SUPPORT MULTIPLE SELF-REFERENTIAL LINKS AS THANKS TO THE FORCE LAYOUT
// THOSE HELPERS WILL ALL BE IN DIFFERENT PLACES, HENCE THE LINK 'PATH' FOR EACH
// PARALLEL LINK WILL BE DIFFERENT.
ui = nodeid(u);
ix = ui + "|" + ui + "|" + uuid();
l = lm[ix] || (lm[ix] = {
source: u,
target: u,
size: 1,
distance: e.weight || 1,
cyclic : true,
ix : ix
});
l.pos = net.links.push(l) - 1;
// LINK(U,V) ==> U -> LU -> U
lu = nmc[ix] ||
(nmc[ix] = data.helpers.cyclic[ix] ||
(data.helpers.cyclic[ix] = {
ref: u,
id: "_ch_" + ix,
size: -1,
link_ref: l,
cyclic_helper: true
})
);
lu.pos = net.helper_nodes.push(lu) - 1;
uimg = nmimg[ui];
l_ = lmc[ix] || (lmc[ix] = {
g_ref: l,
ref: e,
id: "c" + ix,
source: uimg,
target: lu,
real_source: u,
size: 1,
distance: e.weight || 1,
cyclic: true,
ix : ix
});
l_.pos = net.helper_links.push(l_) - 1;
l_.pos2 = net.helper_render_links.push(l_) - 1;
return;
}
// 'LINKS' ARE PRODUCED AS 3 LINKS+2 HELPER NODES; THIS IS A GENERALIZED APPROACH SO WE
// CAN SUPPORT MULTIPLE LINKS BETWEEN ELEMENT NODES, ALWAYS, AS EACH
// 'ORIGINAL LINK' GETS ITS OWN SET OF 2 HELPER NODES AND THANKS TO THE FORCE LAYOUT
// THOSE HELPERS WILL ALL BE IN DIFFERENT PLACES, HENCE THE LINK 'PATH' FOR EACH
// PARALLEL LINK WILL BE DIFFERENT.
ui = nodeid(u);
vi = nodeid(v);
ix = (ui < vi ? ui + "|" + vi : vi + "|" + ui) + "|" + uuid();
l = lm[ix] || (lm[ix] = {
source: u,
target: v,
size: 0,
distance: e.weight || 1,
ix : ix
});
if (l.pos === undefined) l.pos = net.links.push(l) - 1;
// LINK(U,V) ==> U -> LU -> RV -> V
lu = nml[ix] || (nml[ix] = data.helpers
.left[ix] || (data.helpers.left[ix] = {
ref: u,
id: "_lh_" + ix,
size: -1,
link_ref: l,
ix : ix
}));
if (lu.pos === undefined) lu.pos = net.helper_nodes.push(lu) - 1;
rv = nmr[ix] || (nmr[ix] = data.helpers
.right[ix] || (data.helpers.right[ix] = {
ref: v,
id: "_rh_" + ix,
size: -1,
link_ref: l,
ix : ix
}));
if (rv.pos === undefined) rv.pos = net.helper_nodes.push(rv) - 1;
uimg = nmimg[ui];
vimg = nmimg[vi];
ll = lml[ix] || (lml[ix] = {
g_ref: l,
ref: e,
id: "l" + ix,
source: uimg,
target: lu,
real_source: u,
real_target: v,
size: 0,
distance: e.weight || 1,
left_seg: true,
ix : ix
});
if (ll.pos === undefined) ll.pos = net.helper_links.push(ll) - 1;
l_ = lmm[ix] || (lmm[ix] = {
g_ref: l,
ref: e,
id: "m" + ix,
source: lu,
target: rv,
real_source: u,
real_target: v,
size: 0,
distance: e.weight || 1,
middle_seg: true,
ix : ix
});
if (l_.pos === undefined) {
l_.pos = net.helper_links.push(l_) - 1;
l_.pos2 = net.helper_render_links.push(l_) - 1;
}
lr = lmr[ix] || (lmr[ix] = {
g_ref: l,
ref: e,
id: "r" + ix,
source: rv,
target: vimg,
real_source: u,
real_target: v,
size: 0,
distance: e.weight || 1,
right_seg: true,
ix : ix
});
if (lr.pos === undefined) lr.pos = net.helper_links.push(lr) - 1;
++l.size;
++ll.size;
++l_.size;
++lr.size;
// these are only useful for single-linked nodes, but we don't care; here we have everything we need at minimum cost.
if (l.size == 1) {
++u.link_count;
++v.link_count;
u.first_link = l;
v.first_link = l;
u.first_link_target = v;
v.first_link_target = u;
}
};
var del_edge = function (net, e) {
var u, v,
rui, rvi, ui, vi, lu, rv, uimg, vimg,
i, ix,
l, ll, l_, lr, j;
// WHILE D3.LAYOUT.FORCE DOES CONVERT LINK.SOURCE AND LINK.TARGET NUMERIC VALUES TO DIRECT NODE REFERENCES,
// IT DOESN'T FOR OTHER ATTRIBUTES, SUCH AS .REAL_SOURCE, SO WE DO NOT USE INDEXES IN NM[] BUT DIRECT NODE
// REFERENCES TO SKIP THE D3.LAYOUT.FORCE IMPLICIT LINKS CONVERSION LATER ON AND ENSURE THAT BOTH .SOURCE/.TARGET
// AND .REAL_SOURCE/.REAL_TARGET ARE OF THE SAME TYPE AND POINTING AT VALID NODES.
rui = nodeid(e.real_source || e.source);
rvi = nodeid(e.real_target || e.real_source || e.target);
u = nm[rui];
v = nm[rvi];
if (u === v) {
// SKIP NON-ORIGINAL LINKS FROM NODE TO SAME (A-A); THEY ARE RENDERED AS 0-LENGTH LINES ANYHOW. LESS LINKS IN ARRAY = FASTER ANIMATION.
// SELF-REFERENTIAL 'LINKS' ARE PRODUCED AS 2 LINKS+1 HELPER NODE; THIS IS A GENERALIZED APPROACH SO WE
// CAN SUPPORT MULTIPLE SELF-REFERENTIAL LINKS AS THANKS TO THE FORCE LAYOUT
// THOSE HELPERS WILL ALL BE IN DIFFERENT PLACES, HENCE THE LINK 'PATH' FOR EACH
// PARALLEL LINK WILL BE DIFFERENT.
ix = e.ix;
l = lm[ix];
delete lm[ix];
j = net.links.length;
while(--j > l.pos) --net.links[j].pos;
net.links.splice(l.pos, 1);
// LINK(U,V) ==> U -> LU -> U
lu = nmc[ix];
delete nmc[ix];
delete data.helpers.cyclic[ix];
j = net.helper_nodes.length;
while(--j > lu.pos){
--net.helper_nodes[j].pos;
--net.helper_nodes[j].index;
}
net.helper_nodes.splice(lu.pos, 1);
l_ = lmc[ix];
delete lmc[ix];
j = net.helper_links.length;
while(--j > l_.pos) --net.helper_links[j].pos;
net.helper_links.splice(l_.pos, 1);
j = net.helper_render_links.length;
while(--j > l_.pos2) --net.helper_render_links[j].pos2;
net.helper_render_links.splice(l_.pos2, 1);
return;
}
// 'LINKS' ARE PRODUCED AS 3 LINKS+2 HELPER NODES; THIS IS A GENERALIZED APPROACH SO WE
// CAN SUPPORT MULTIPLE LINKS BETWEEN ELEMENT NODES, ALWAYS, AS EACH
// 'ORIGINAL LINK' GETS ITS OWN SET OF 2 HELPER NODES AND THANKS TO THE FORCE LAYOUT
// THOSE HELPERS WILL ALL BE IN DIFFERENT PLACES, HENCE THE LINK 'PATH' FOR EACH
// PARALLEL LINK WILL BE DIFFERENT.
ui = nodeid(u);
vi = nodeid(v);
ix = e.ix;
l = lm[ix];
if(--l.size === 0) delete lm[ix];
j = net.links.length;
while(--j > l.pos) --net.links[j].pos;
net.links.splice(l.pos, 1);
// LINK(U,V) ==> U -> LU -> RV -> V
lu = nml[ix];
if(l.size === 0){
delete nml[ix];
delete data.helpers.left[ix];
}
j = net.helper_nodes.length;
while(--j > lu.pos){
--net.helper_nodes[j].pos;
--net.helper_nodes[j].index;
}
net.helper_nodes.splice(lu.pos, 1);
rv = nmr[ix];
if(l.size === 0){
delete nmr[ix];
delete data.helpers.right[ix];
}
j = net.helper_nodes.length;
while(--j > rv.pos){
--net.helper_nodes[j].pos;
--net.helper_nodes[j].index;
}
net.helper_nodes.splice(rv.pos, 1);
uimg = nmimg[ui];
vimg = nmimg[vi];
ll = lml[ix];
if(--ll.size === 0) delete lml[ix];
j = net.helper_links.length;
while(--j > ll.pos) --net.helper_links[j].pos;
net.helper_links.splice(ll.pos, 1);
l_ = lmm[ix];
if(--l_.size === 0) delete lmm[ix];
j = net.helper_links.length;
while(--j > l_.pos) --net.helper_links[j].pos;
net.helper_links.splice(l_.pos, 1);
j = net.helper_render_links.length;
while(--j > l_.pos2) --net.helper_render_links[j].pos2;
net.helper_render_links.splice(l_.pos2, 1);
lr = lmr[ix];
if(--lr.size === 0) delete lmr[ix];
j = net.helper_links.length;
while(--j > lr.pos) --net.helper_links[j].pos;
net.helper_links.splice(lr.pos, 1);
// these are only useful for single-linked nodes, but we don't care; here we have everything we need at minimum cost.
if (l.size === 0) {
--u.link_count;
--v.link_count;
u.first_link = null;
v.first_link = null;
u.first_link_target = null;
v.first_link_target = null;
}
};
var add_vertex = function (net, n) {
n.x = width / 2;
n.y = height / 2;
var img;
// THE NODE SHOULD BE DIRECTLY VISIBLE
nm[nodeid(n)] = n;
img = {
ref: n,
x: n.x,
y: n.y,
size: 0,
fixed: 1,
id: nodeid(n)
};
nmimg[nodeid(n)] = img;
n.pos = nodes.push(n) - 1;
img.pos = net.helper_nodes.push(img) - 1;
n.link_count = 0;
n.first_link = null;
n.first_link_target = null;
};
var del_vertex = function (net, n) {
var img = nmimg[nodeid(n)], j;
j = net.links.length;
var list = [];
while(j--){
if(n === net.links[j].source || n === net.links[j].target)
list.push(net.links[j]);
}
j = list.length;
while(j--) del_edge(net, list[j]);
delete nm[nodeid(n)];
delete nmimg[nodeid(n)];
j = nodes.length;
while(--j > n.pos){
--nodes[j].pos;
--nodes[j].index;
}
nodes.splice(n.pos, 1);
j = net.helper_nodes.length;
while(--j > img.pos){
--net.helper_nodes[j].pos;
--net.helper_nodes[j].index;
}
net.helper_nodes.splice(img.pos, 1);
n.first_link = null;
n.first_link_target = null;
};
var network = function (data) {
var links = [], // OUTPUT LINKS
helper_links = [], // HELPER FORCE GRAPH LINKS
helper_render_links = [], // HELPER FORCE GRAPH LINKS
k;
var net = {
nodes: nodes,
links: links,
helper_nodes: helper_nodes,
helper_links: helper_links,
helper_render_links: helper_render_links,
eadd : function(e){
add_edge(net, e);
net.force1.start();
net.force2.start();
net.update();
},
vadd : function(v){
add_vertex(net, v);
net.force1.start();
net.force2.start();
net.update();
},
edel : function(e){
del_edge(net, e);
net.force1.start();
net.force2.start();
net.update();
},
vdel : function(v){
del_vertex(net, v);
net.force1.start();
net.force2.start();
net.update();
},
uuid : uuid
};
// DETERMINE NODES
for (k = 0; k < data.nodes.length; ++k) {
add_vertex(net, data.nodes[k]);
}
// DETERMINE LINKS
for (k = 0; k < data.links.length; ++k) {
add_edge(net, data.links[k]);
}
return net;
};
// ==
// SVG ELEMENTS TOOLS
var vsvg = function(net){
if (debug && debug < 3) {
hnode = helper_nodeg
.selectAll("circle.node")
.data(
net.helper_nodes,
function (d) { return d.id; }
);
hnode.exit().remove();
hnode_vsvg(hnode.enter());
}
var node = nodeg.selectAll("circle.node").data(net.nodes, function(d){
return nodeid(d);
});
node.exit().remove();
nodeg_vsvg(node.enter());
return node;
};
var hnode_vsvg = function(enter){
enter.append("circle")
.attr("class", "node helper")
.attr("r", function (d) { return 2; })
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.style("fill", function (_, i) { return fill(i); });
};
var nodeg_vsvg = function(enter){
enter.append("circle")
.attr("class", "node leaf")
.attr("r", function (d) { return radius + 1; })
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.style("fill", function (_, i) { return fill(i); } );
};
var esvg = function(net){
if (debug == 1) {
link = linkg.selectAll("line.link").data(net.links, function(d){
return linkid(d);
});
link.exit().remove();
link.enter().append("line")
.attr("class", "link")
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
link.style("stroke-width", option.stroke);
}
hlink = helper_linkg.selectAll("path.hlink").data(net.helper_render_links,
function (d) { return d.id; }
);
hlink.exit().remove();
hlink.enter().append("path").attr("class", "hlink");
hlink.style("stroke-width", option.stroke);
etext = edge_text.selectAll('text').data(net.helper_render_links,
function (d) { return d.id; }
);
etext.exit().remove();
etext.enter().append('text')
.attr("text-anchor", "middle")
.text(function(d) {return d.distance || '';});
};
// UPDATE DRAWING
var update = function(net, force1, force2){
// UPDATE SVG ELEMENTS
esvg(net);
var node = vsvg(net), power;
node.call(force1.drag);
var drag_in_progress = false;
var change_squared = width * height;
// CPU LOAD REDUX FOR THE FIX, PART 3: JUMPSTART THE ANNEALING PROCESS AGAIN WHEN THE USER MOVES THE MOUSE OUTSIDE THE NODE,
// WHEN WE BELIEVE THE DRAG IS STILL GOING ON; EVEN WHEN IT ISN'T ANYMORE, BUT D3 DOESN'T INFORM US ABOUT THAT!
node.on("mouseout.ger_fix", function (d) {
if (debug == 1) console.log(
"mouseout.ger_fix", this,
arguments, d.fixed,
drag_in_progress
);
if (drag_in_progress) force1.resume();
});
var resume_threshold = 0.05;
force1.on("tick", function (e) {
if (debug) {
alpha_trace.push(alpha_scale(e.alpha));
if (alpha_trace.length > width) alpha_trace.shift();
path_trace_alpha.attr("d", alpha_line(alpha_trace));
c2_trace.push(c2_scale(change_squared));
if (c2_trace.length > width) c2_trace.shift();
path_trace_c2.attr("d", c2_line(c2_trace));
}
/*
Force all nodes with only one link to point outwards.
To do this, we first calculate the center mass (okay, we wing it, we fake node 'weight'),
then see whether the target node for links from single-link nodes is closer to the
center-of-mass than us, and if it isn't, we push the node outwards.
*/
var center = {
x: width / 2,
y: height / 2,
weight: 0
},
centroids = { x: 0, y: 0, weight: 0 },
gc = centroids,
size, c, k, mx, my, dx, dy, alpha;
drag_in_progress = false;
net.nodes.forEach(function (n) {
var w = Math.max(1, n.size || 0, n.weight || 0);
center.x += w * n.x;
center.y += w * n.y;
center.weight += w;
gc.x += w * n.x;
gc.y += w * n.y;
gc.weight += w;
if (n.fixed & 2) drag_in_progress = true;
});
size = force1.size();
mx = size[0] / 2;
my = size[1] / 2;
gc.x /= gc.weight;
gc.y /= gc.weight;
// MOVE THE ENTIRE GRAPH SO THAT ITS CENTER OF MASS SITS AT THE CENTER, PERIOD.
center.x /= center.weight || 1;
center.y /= center.weight || 1;
if (debug == 1) {
center_of_mass
.attr("cx", center.x)
.attr("cy", center.y);
}
dx = mx - center.x;
dy = my - center.y;
alpha = e.alpha * 5;
dx *= alpha;
dy *= alpha;
net.nodes.forEach(function (n) {
n.x += dx;
n.y += dy;
});
change_squared = 0;
// FIXUP .PX/.PY SO DRAG BEHAVIOUR AND ANNEALING GET THE CORRECT VALUES, AS
// FORCE.TICK() WOULD EXPECT .PX AND .PY TO BE THE .X AND .Y OF YESTERDAY.
net.nodes.forEach(function (n) {
// RESTRAIN ALL NODES TO WINDOW AREA
var k, dx, dy,
/* styled border outer thickness and a bit */
r = (n.size > 0 ? n.size + radius : radius + 1) + 2;
dx = 0;
if (n.x < r) dx = r - n.x;
else if (n.x > size[0] - r) dx = size[0] - r - n.x;
dy = 0;
if (n.y < r) dy = r - n.y;
else if (n.y > size[1] - r) dy = size[1] - r - n.y;
k = 1.2;
n.x += dx * k;
n.y += dy * k;
// RESTRAINING COMPLETED.......................
// FIXES 'ELUSIVE' NODE BEHAVIOUR WHEN HOVERING WITH THE MOUSE (RELATED TO FORCE.DRAG)
if (n.fixed) {
// 'ELUSIVE BEHAVIOUR' ~ MOVE MOUSE NEAR NODE AND NODE WOULD TAKE OFF, I.E. ACT AS AN ELUSIVE CREATURE.
n.x = n.px;
n.y = n.py;
}
n.px = n.x;
n.py = n.y;
// PLUS COPY FOR FASTER STOP CHECK
change_squared += (n.qx - n.x) * (n.qx - n.x);
change_squared += (n.qy - n.y) * (n.qy - n.y);
n.qx = n.x;
n.qy = n.y;
});
// ALSO RESTRAIN HELPER NODES TO WITHIN THE VISIBLE AREA --> LINK PATHS ARE ALMOST ALWAYS KET IN-VIEW:
net.helper_nodes.forEach(function (n) {
// RESTRAIN ALL NODES TO WINDOW AREA
var k, dx, dy,
r = (n.size > 0 ? n.size : 1) + 5 /* heuristic */ ;
dx = 0;
if (n.x < r)
dx = r - n.x;
else if (n.x > size[0] - r)
dx = size[0] - r - n.x;
dy = 0;
if (n.y < r)
dy = r - n.y;
else if (n.y > size[1] - r)
dy = size[1] - r - n.y;
k = 1.2;
n.x += dx * k;
n.y += dy * k;
// RESTRAINING COMPLETED.......................
n.px = n.x;
n.py = n.y;
// PLUS COPY FOR FASTER STOP CHECK
change_squared += (n.qx - n.x) * (n.qx - n.x);
change_squared += (n.qy - n.y) * (n.qy - n.y);
n.qx = n.x;
n.qy = n.y;
});
if (!isFinite(change_squared))
change_squared = width * height;
// KICK THE FORCE2 TO ALSO DO A BIT OF ANNEALING ALONGSIDE:
// TO MAKE IT DO SOMETHING, WE NEED TO SURROUND IT ALPHA-TWEAKING STUFF, THOUGH.
force2.resume();
force2.tick();
force2.stop();
// FAST STOP + THE DRAG FIX, PART 2:
if (change_squared < option.fast_stop_threshold) {
if (debug == 1) console.log("fast stop: CPU load redux");
force1.stop();
// FIX PART 4: MONITOR D3 RESETTING THE DRAG MARKER:
if (drag_in_progress) {
if (debug == 1) console.log("START monitor drag in progress", drag_in_progress);
d3.timer(function () {
drag_in_progress = false;
net.nodes.forEach(function (n) {
if (n.fixed & 2) drag_in_progress = true;
});
force1.resume();
if (debug == 1) console.log("monitor drag in progress: drag ENDED", drag_in_progress);
// QUIT MONITORING AS SOON AS WE NOTICED THE DRAG ENDED.
// NOTE: WE CONTINUE TO MONITOR AT +500MS INTERVALS BEYOND THE LAST TICK
// AS THIS TIMER FUNCTION ALWAYS KICKSTARTS THE FORCE LAYOUT AGAIN
// THROUGH FORCE.RESUME().
// D3.TIMER() API ONLY ACCEPTS AN INITIAL DELAY; WE CAN'T SET THIS
// THING TO SCAN, SAY, EVERY 500MSECS UNTIL THE DRAG IS DONE,
// SO WE DO IT THAT WAY, VIA THE REVIVED FORCE.TICK PROCESS.
return true;
}, 500);
}
} else if (change_squared > net.nodes
.length * 0.1 * resume_threshold &&
e.alpha < resume_threshold) {
// JOLT THE ALPHA (AND THE VISUAL) WHEN THERE'S STILL A LOT OF CHANGE WHEN WE HIT THE ALPHA THRESHOLD.
force1.alpha(e.alpha *= 2); //FORCE.RESUME(), BUT NOW WITH DECREASING ALPHA STARTING VALUE SO THE JOLTS DON'T GET SO BIG.
// AND 'DAMPEN OUT' THE TRIGGER POINT, SO IT BECOMES HARDER AND HARDER TO TRIGGER THE THRESHOLD.
// THIS IS DONE TO COPE WITH THOSE INSTABLE (FOREVER ROTATING, ETC.) LAYOUTS...
resume_threshold *= 0.75;
}
//--------------------------------------------------------------------
if (debug == 1) {
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
}
node.attr("cx", function (d) {
return d.x;
})
.attr("__name__", function (d) {
return d.name;
})
.attr("cy", function (d) {
return d.y;
});
});
force2.on("tick", function (e) {
/*
Update all 'real'=fixed nodes.
*/
net.helper_nodes.forEach(function (n) {
var o;
if (n.fixed) {
o = n.ref;
n.px = n.x = o.x;
n.py = n.y = o.y;
}
});
net.helper_links.forEach(function (l) {
var o = l.g_ref;
l.distance = o.distance;
});
// NOTE: FORCE2 IS FULLY DRIVEN BY FORCE(1), BUT STILL THERE'S NEED FOR 'FAST STOP' HANDLING IN HERE
// AS OUR FORCE2 MAY BE MORE 'JOYOUS' IN ANIMATING THE LINKS THAT FORCE IS ANIMATING THE NODES
// THEMSELVES. HENCE WE ALSO TAKE THE DELTA MOVEMENT OF THE HELPER NODES INTO ACCOUNT!
net.helper_nodes.forEach(function (n) {
// SKIP THE 'FIXED' BUGGERS: THOSE ARE ALREADY ACCOUNTED FOR IN FORCE.TICK!
if (n.fixed) return;
// PLUS COPY FOR FASTER STOP CHECK
change_squared += (n.qx - n.x) * (n.qx - n.x);
change_squared += (n.qy - n.y) * (n.qy - n.y);
n.qx = n.x;
n.qy = n.y;
});
if (!isFinite(change_squared)) change_squared = width * height;
//--------------------------------------------------------------------
var logged = false;
hlink.attr("d", function (d) {
if (isFinite(d.real_source.x)) {
var linedata, dx, dy, f;
if (d.cyclic) {
// CONSTRUCT ROUND-ISH BEZIER FROM NODE TO HELPER AND BACK AGAIN:
dx = d.target.x - d.real_source.x;
dy = d.target.y - d.real_source.y;
linedata = [
[d.real_source.x, d.real_source.y],
[d.target.x - cycle_curvep * dy,
d.target.y + cycle_curvep * dx
],
[d.target.x + cycle_curvep * dx,
d.target.y + cycle_curvep * dy
],
[d.target.x + cycle_curvep * dy,
d.target.y - cycle_curvep * dx
],
[d.real_source.x, d.real_source.y]
];
return cycle_pathgen(linedata);
} else {
linedata = [
[d.real_source.x, d.real_source.y],
[d.source.x, d.source.y],
[d.target.x, d.target.y],
[d.real_target.x, d.real_target.y]
];
return pathgen(linedata);
}
} else {
if (!logged) {
console.log("boom");
logged = true;
}
return null;
}
});
etext
.attr("y", function(d) { return (d.source.y + d.target.y) / 2; })
.attr("x", function(d) { return (d.source.x + d.target.x) / 2; });
if (debug && debug < 3) {
hnode.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
}
});
return net;
};
// INIT DRAWING EVENTS
var init = function(){
var net = network(data);
var force1 = d3.layout.force()
.nodes(net.nodes)
.links(net.links)
.size([width, height])
.linkDistance(option.force1.distance)
.gravity(option.force1.gravity)
.charge(option.force1.charge)
.friction(option.force1.friction) // FRICTION ADJUSTED TO GET DAMPENED DISPLAY: LESS BOUNCY BOUNCY BALL [SWEDISH CHEF, ANYONE?]
.start();
/**
* AND HERE'S THE CRAZY IDEA FOR ALLOWING AND RENDERING MULTIPLE LINKS BETWEEN 2 NODES, ETC., AS THE INITIAL ATTEMPT
* TO INCLUDE THE 'HELPER' NODES IN THE BASIC 'FORCE' FAILED DRAMATICALLY FROM A VISUAL POV: WE 'OVERLAY' THE BASIC
* NODES+LINKS FORCE WITH A SECOND FORCE LAYOUT WHICH 'AUGMENTS' THE ORIGINAL FORCE LAYOUT BY HAVING IT 'LAYOUT' ALL
* THE HELPER NODES (WITH THEIR LINKS) BETWEEN THE 'FIXED' REAL NODES, WHICH ARE LAID OUT BY THE ORIGINAL FORCE.
*
* THIS WAY, WE ALSO HAVE THE FREEDOM TO APPLY A COMPLETELY DIFFERENT FORCE FIELD SETUP TO THE HELPERS (NO GRAVITY
* AS IT DOESN'T MAKE SENSE FOR HELPERS, DIFFERENT CHARGE VALUES, ETC.).
*/
var force2 = d3.layout.force()
.nodes(net.helper_nodes)
.links(net.helper_links)
.size([width, height])
.linkDistance(option.force2.distance)
.gravity(option.force2.gravity)
.charge(option.force2.charge)
.friction(option.force2.friction)
.start()
.stop(); // AND IMMEDIATELY STOP! FORCE.TICK WILL DRIVE THIS ONE EVERY TICK!
net.force1 = force1;
net.force2 = force2;
net.update = function(){ update(net, force1, force2); };
net.update();
return net;
};
// ==
// OK, DO IT
var net = init();
// ==
// EVENT HANDLERS
var setzoom = function(translate, scale){
wrap.attr("transform",
"translate(" + translate[0] + ',' + translate[1] +
")scale(" + scale + ")"
);
};
var zoomed = function () {
wrap.attr("transform",
"translate(" + d3.event.translate +
")scale(" + d3.event.scale + ")"
);
};
var zoom = d3.behavior.zoom()
.scaleExtent(option.zoom_range)
.on("zoom", zoomed);
superwrap.call(zoom);
superwrap.on('dblclick.zoom', null);
vis.attr("opacity", 1e-6)
.transition()
.duration(1000)
.attr("opacity", 1);
net.option = option;
net.zoom = setzoom;
net.size = function(s){
vis.attr("width", s[0]).attr("height", s[1]);
if(s[0] > 0 || s[1] > 0) {
superwrap.attr("width", s[0]).attr("height", s[1]);
background.attr("width", s[0]).attr("height", s[1]);
wrap.attr("width", s[0]).attr("height", s[1]);
}
net.force1.size(s);
net.force2.size(s);
};
return net;
// ==
};
// --------------------------------------------------------
var pulse = function(circle, option) {
var action;
var one = function(){
return circle.transition()
.duration(600)
[option.type](option[option.type], option.range[1])
.transition()
.duration(600)
[option.type](option[option.type], option.range[0])
.ease('sine');
};
var reset = function(){
return circle.transition()
.duration(600)
[option.type](option[option.type], option.range[0])
.ease('sine');
};
var repeat = function() {
return one().each("end", action);
};
var stop = function(){
action = reset;
return {start : start, stop : stop};
};
var start = function(){
action = repeat;
repeat();
return {start : start, stop : stop};
};
return {start : start, stop : stop};
};
var rwedge = function(i, j){
return {source:i, target:j, weight : 1 + Math.floor(Math.random() * 5)};
};
(function(){
"use strict";
var w = 940, h = 480;
// var k = function(V){return Math.sqrt(V.length / (w * h));};
var option = {
debug : 1,
root : 'div#root',
width : w,
height : h
};
var net = dynamic_multigraph(option);
// var z = 6;
// net.zoom([(1 - z) * w / 2, (1 - z) * h / 2], 7.36);
var select = {vertex : null, edge : null};
var pulsing;
$('div#root').on('click', '> svg > g > g > g:nth-child(4) > path', function (e) {
if(select.vertex) {
pulsing.stop();
select.vertex = null;
}
else if(select.edge) {
select.edge[1].classed('selected', false);
pulsing.stop();
}
var svg = d3.select(this);
var edge = svg[0][0].__data__;
console.log('edge click', edge);
select.edge = [edge, svg];
select.edge[1].classed('selected', true);
pulsing = pulse(select.edge[1], {
'style' : 'stroke-width',
'range' : [option.stroke + 'px', 1.3 * option.stroke + 'px'],
'type' : 'style'
});
pulsing.start();
net.update();
e.stopImmediatePropagation();
});
$('div#root').on('click', '> svg > g > g > g:nth-child(5) > circle', function (e) {
if (select.edge){
select.edge[1].classed('selected', false);
pulsing.stop();
select.edge = null;
}
var svg = d3.select(this);
var vertex = svg[0][0].__data__;
console.log('vertex click', vertex);
if(select.vertex === null){
select.vertex = [vertex, svg];
pulsing = pulse(select.vertex[1], {
'attr' : 'r',
'range' : [net.option.radius + 1, net.option.radius + 4],
'type' : 'attr'
});
pulsing.start();
}
else{
console.log('2nd vertex click');
net.eadd(rwedge(select.vertex[0], vertex));
// pulsing.stop();
// select.vertex = null;
}
e.stopImmediatePropagation();
});
$('html').keyup(function(e){
if(e.keyCode === 46){ // DELETE
if(select.vertex !== null){
console.log('delete vertex', select.vertex);
pulsing.stop();
net.vdel(select.vertex[0]);
select.vertex = null;
}
else if(select.edge !== null){
console.log('delete edge', select.edge);
select.edge[1].classed('selected', false);
pulsing.stop();
net.edel(select.edge[0]);
select.edge = null;
}
}
else if (e.keyCode === 27){ // ESCAPE
if(select.vertex) {
pulsing.stop();
select.vertex = null;
}
else if (select.edge){
select.edge[1].classed('selected', false);
pulsing.stop();
select.edge = null;
}
}
});
$('div#root').on('click', function (e) {
console.log('frame click');
if(e.ctrlKey){
if(select.vertex) {
pulsing.stop();
select.vertex = null;
}
else if (select.edge){
select.edge[1].classed('selected', false);
pulsing.stop();
select.edge = null;
}
else{
net.vadd({ name : net.uuid() });
}
e.stopImmediatePropagation();
}
});
var k = 1 + Math.floor(Math.random() * 4);
var i = 0, j = 0;
while(k--){
var n = 1 + Math.floor(Math.random() * 7);
var t = i;
var len = i + n;
for(; i < len; ++i) net.vadd({name : net.uuid()});
for(i -= n; i < len; ++i){
for(j = t; j < len; ++j){
if(Math.random() < 0.3)
net.eadd(rwedge(net.nodes[j], net.nodes[i]));
}
}
}
window.net = net;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment