|
//debug panel///////////////////////////////////////////////////////////////////////////// |
|
var alpha = d3.select("#alpha").text("waiting..."), |
|
cog = d3.select("#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true).datum({instID: null}), |
|
fdgInst = d3.select("#fdg"); |
|
elapsedTime = ElapsedTime("#panel", {margin: 0, padding: 0}) |
|
.message(function (id) { |
|
return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap()) |
|
}); |
|
elapsedTime.consoleOn = false; |
|
|
|
alpha.log = function(e, instID) { |
|
elapsedTime.mark().timestamp(); |
|
alpha.text(d3.format(" >8.4f")(e.alpha)); |
|
fdgInst.text("fdg instance: " + instID); |
|
}; |
|
|
|
d3.select("#update").on("click", (function() { |
|
var dataSet = false; |
|
return function() { |
|
fdg.data(dataSets[(dataSet = !dataSet, +dataSet)]) |
|
} |
|
})()); |
|
d3.select("#jump").on("click", function() { |
|
var jumpXY = 50, |
|
p = d3.transform(fdg.attr("transform")) |
|
.translate.map(function(d){return d+jumpXY}); |
|
fdg.zoomTo({x: p[0], y: p[1]}); |
|
}); |
|
////////////////////////////////////////////////////////////////////////////////////////// |
|
var dataSets = [{ |
|
"nodes" : [ |
|
{"name": "node1", "r": 10}, |
|
{"name": "node2", "r": 10}, |
|
{"name": "node3", "r": 30}, |
|
{"name": "node4", "r": 15} |
|
], |
|
"edges": [ |
|
{"source": 2, "target": 0}, |
|
{"source": 2, "target": 1}, |
|
{"source": 2, "target": 3} |
|
] |
|
}, |
|
{ |
|
"nodes":[ |
|
{"name": "node1", "r": 20}, |
|
{"name": "node2", "r": 10}, |
|
{"name": "node3", "r": 30}, |
|
{"name": "node4", "r": 15}, |
|
{"name": "node5", "r": 10}, |
|
{"name": "node6", "r": 10} |
|
], |
|
"edges":[ |
|
{"source": 2, "target": 0}, |
|
{"source": 2, "target": 1}, |
|
{"source": 2, "target": 3}, |
|
{"source": 2, "target": 4}, |
|
{"source": 2, "target": 5} |
|
] |
|
} |
|
], |
|
svg = SVG({width: 600, height: 200-34, margin: {top: 25, right: 5, bottom: 15, left: 15}}, "#viz", {dblclik: null}), |
|
fdg = FDG(svg, alpha.log).zoomTime(1000) |
|
.on("dblclick", function(d){ |
|
d3.event.stopPropagation(); |
|
fdg.zoomTo(d); |
|
}); |
|
|
|
fdg.data(dataSets[0]); |
|
|
|
function SVG (size, selector, z){ |
|
//delivers an svg background with zoom/drag context in the selector element |
|
//if height or width is NaN, assume it is a valid length but ignore margin |
|
var margin = size.margin || {top: 0, right: 0, bottom: 0, left: 0}, |
|
unitW = isNaN(size.width), unitH = isNaN(size.height), |
|
w = unitW ? size.width : size.width - margin.left - margin.right, |
|
h = unitH ? size.height : size.height - margin.top - margin.bottom, |
|
x, y, xAxis, yAxis, |
|
px = d3.scale.linear() |
|
.domain([-w/2, w/2]) |
|
.range([0, w]), |
|
py = d3.scale.linear() |
|
.domain([-h/2, h/2]) |
|
.range([0, h]), |
|
pxAxis = d3.svg.axis() |
|
.scale(px) |
|
.orient("bottom") |
|
.tickSize(3), |
|
pyAxis = d3.svg.axis() |
|
.scale(py) |
|
.orient("right") |
|
.tickSize(3), |
|
zoomStart = function() {return this}, |
|
zoomed = function(){return this}, |
|
container, |
|
|
|
zoom = d3.behavior.zoom().scaleExtent(z && z.extent || [0.4, 4]) |
|
.on("zoom", function(d, i, j){ |
|
onZoom.call(this, d, i, j); |
|
zoomed.call(this, d, i, j); |
|
}) |
|
.on("zoomstart", function(d, i, j){ |
|
onZoomStart.call(this, d, i, j); |
|
zoomStart.call(this, d, i, j); |
|
}), |
|
svg = d3.select(selector).selectAll("svg").data([["transform root"]]); |
|
svg.enter().append("svg"); |
|
svg.attr({width: size.width, height: size.height}); |
|
|
|
var g = svg.selectAll("#zoom").data(id), |
|
gEnter = g.enter().append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") |
|
.style("cursor", "move") |
|
.call(zoom) |
|
.attr({class: "outline", id: "zoom"}), |
|
zoomText = gEnter.append("text") |
|
.text("g#zoom: transform = translate ( margin.left , margin.top ); .call(zoom)") |
|
.style("fill", "#5c5c5c") |
|
.attr("dy", "-.35em"), |
|
surface = gEnter.append("rect") |
|
.attr({width: w, height: h}) |
|
.style({"pointer-events": "all", fill: "#ccc", "stroke-width": 3, "stroke": "#fff"}), |
|
surfaceText = gEnter.append("text") |
|
.text("event capture surface: style='pointer-events: none'") |
|
.style("fill", "#5c5c5c") |
|
.attr({"dy": "1em", "dx": ".2em"}), |
|
gx = gEnter.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + h + ")") |
|
.call(xAxis || noop), |
|
gy = gEnter.append("g") |
|
.attr("class", "y axis") |
|
.call(yAxis || noop), |
|
gpx = gEnter.append("g") |
|
.attr("class", "x paxis") |
|
.attr("transform", "translate(0," + h + ")") |
|
.call(pxAxis), |
|
gpy = gEnter.append("g") |
|
.attr("class", "y paxis") |
|
.attr("transform", "translate(" + w + ",0)") |
|
.call(pyAxis); |
|
|
|
|
|
if(z && (typeof z.dblclik != "undefined")) gEnter.on("dblclick.zoom", z.dblclik); |
|
|
|
function onZoomStart(){ |
|
// zoom translate and scale are initially [0,0] and 1 |
|
// this needs to be aligned with the container to stop |
|
// jump back to zero before first jump transition |
|
var t = d3.transform(container.attr("transform")); |
|
zoom.translate(t.translate); zoom.scale(t.scale[0]); |
|
} |
|
|
|
function onZoom(){ |
|
var e = d3.event.sourceEvent, |
|
isWheel = e && ((e.type == "mousewheel") || (e.type == "wheel")), |
|
t = d3.transform(container.attr("transform")); |
|
t.translate = d3.event.translate; t.scale = [d3.event.scale, d3.event.scale]; |
|
return isWheel ? zoomWheel.call(this, t) : zoomInst.call(this, t) |
|
} |
|
function zoomInst(t){ |
|
container.attr("transform", t.toString()); |
|
gx.call(xAxis || noop); |
|
gy.call(yAxis || noop); |
|
} |
|
function zoomWheel(t){ |
|
container.transition().duration(450).attr("transform", t.toString()); |
|
gx.transition().duration(450).call(xAxis || noop); |
|
gy.transition().duration(450).call(yAxis || noop); |
|
} |
|
|
|
g.h = h; |
|
g.w = w; |
|
|
|
g.container = function(selector){ |
|
var d3_data, d3_datum; |
|
if(selector) { |
|
container = g.selectAll(selector); |
|
// temporarily subclass container |
|
d3_data = container.data; |
|
d3_datum = container.datum; |
|
// need a reference to the update selection |
|
// so force data methods back to here |
|
container.data = function() { |
|
delete container.data; // remove the sub-classing |
|
return container = d3_data.apply(container, arguments) |
|
} |
|
} |
|
return container; |
|
}; |
|
|
|
g.xScale = function(_){ |
|
var y; |
|
if(!_) return (y = subclass(x, function() { |
|
zoom.x(x); // set the base value for the zoom's understanding of x.domain |
|
gx.call(xAxis || noop); |
|
}), y); |
|
zoom.x(x = _); |
|
gx.call(xAxis || noop); |
|
return this; |
|
}; |
|
g.yScale = function(_){ |
|
if(!_) return subclass(y, function() { |
|
zoom.y(y); // set the base value for the zoom's understanding of y.domain |
|
gy.call(xAxis || noop); |
|
}); |
|
zoom.y(y = _); |
|
gy.call(yAxis || noop); |
|
return this; |
|
}; |
|
g.xAxis = function(_){ |
|
if(!_) return subclass(xAxis, function() { |
|
gx.call(xAxis); |
|
}); |
|
gx.call(xAxis = _); |
|
return this; |
|
}; |
|
g.yAxis = function(_){ |
|
if(!_) return subclass(yAxis, function() { |
|
gy.call(yAxis); |
|
}); |
|
gy.call(yAxis = _) |
|
return this; |
|
}; |
|
|
|
g.zoomTo = function(t, p){ |
|
// map p to the center of the plot surface |
|
var s = zoom.scale(), |
|
p1 = [w/2 - p.x * s, h/2 - p.y * s]; |
|
container.transition().duration(t).call(zoom.translate(p1).event); |
|
}; |
|
|
|
g.onZoom = function(cb){zoomed = cb;}; |
|
g.onZoomStart = function(cb) {zoomStart = cb;}; |
|
|
|
d3.rebind(g, zoom, "translate"); |
|
d3.rebind(g, zoom, "scale"); |
|
|
|
return g; |
|
|
|
function noop(){}; |
|
|
|
function subclass(x, post){ |
|
// hook a post-processor to all methods of x |
|
return Object.keys(x).reduce(function(s, k) { |
|
return (s[k] = function() { |
|
var ret = x[k].apply(x, arguments); |
|
post(); |
|
return ret; |
|
}, s) |
|
}, {}) |
|
} |
|
} |
|
function FDG (svg, tickLog) { |
|
var instID = Date.now(), |
|
size = [svg.w, svg.h], |
|
events = ["mouseover", "mouseout", "click", "dblclick", "contextmenu"], |
|
dispatch = d3.dispatch.apply(null, events), |
|
force = d3.layout.force() |
|
.size(size) |
|
.charge(-1000) |
|
.linkDistance(50) |
|
.on("end", function() { |
|
// manage dead instances of force |
|
// only stop if this instance is the current owner |
|
if(cog.datum().instID != instID) return true; |
|
cog.classed("fa-spin", false); |
|
elapsedTime.stop(); |
|
}) |
|
.on("start", function() { |
|
// mark as active and brand the insID to establish ownership |
|
cog.classed("fa-spin", true).datum().instID = instID; |
|
elapsedTime.start(); |
|
}), |
|
fdg = {}; |
|
|
|
function data(data) { |
|
force |
|
.nodes(data.nodes) |
|
.links(data.edges) |
|
.on("tick", (function(instID) { |
|
return function(e) { |
|
if(tickLog) tickLog.call(this, e, instID); |
|
|
|
contentText.text(function(d){return d()}) |
|
|
|
lines.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("transform", function(d) { |
|
return "translate(" + [d.x, d.y] + ")" |
|
}); |
|
var nodesBB = nodes.node().getBBox(), |
|
textBB = nodeText.node().getBBox(); |
|
nodeText |
|
.attr({ |
|
x: nodesBB.x + nodesBB.width/2 - textBB.width/2, |
|
y: nodesBB.y + nodesBB.height, |
|
dy: "-0.35em" |
|
}); |
|
} |
|
})(instID)); |
|
|
|
hookDrag(force.drag(), "dragstart.force", function(d) { |
|
// prevent dragging on the nodes from dragging the canvas |
|
var e = d3.event.sourceEvent; |
|
e.stopPropagation(); |
|
d.fixed = e.shiftKey || e.touches && (e.touches.length > 1); |
|
}); |
|
|
|
hookDrag(force.drag(), "dragend.force", function(d) { |
|
// prevent dragging on the nodes from dragging the canvas |
|
var e = d3.event.sourceEvent; |
|
d.fixed = e.shiftKey || d.fixed; |
|
}); |
|
var x = d3.scale.linear() |
|
.domain([-svg.w/2, svg.w/2]) |
|
.range([0, svg.w]), |
|
y = d3.scale.linear() |
|
.domain([-svg.h/2, svg.h/2]) |
|
.range([0, svg.h]), |
|
xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient("top") |
|
.tickSize(svg.h), |
|
yAxis = d3.svg.axis() |
|
.scale(y) |
|
.orient("left") |
|
.tickSize(-svg.w); |
|
|
|
svg |
|
.xScale(x) |
|
.yScale(y) |
|
.xAxis(xAxis) |
|
.yAxis(yAxis) |
|
.onZoom(zoomed); |
|
|
|
var content = svg.container("g#fdg").data([data]); |
|
content.enter().append("g").attr({"id": "fdg", class: "outline"}); |
|
|
|
var contentText = content.selectAll(".contentText") |
|
.data([ |
|
function() { |
|
var t = d3.transform(content.attr("transform")); |
|
return "content: transform = translate (" + f(0)(t.translate) + ") scale (" + f(1)(t.scale) + ")"; |
|
function f(p) { |
|
return function _f(x) { |
|
return Array.isArray(x) ? x.map(_f) : d3.format("." + p + "f")(x); |
|
} |
|
} |
|
}]) |
|
.enter().append("text").classed("contentText", true) |
|
.text(function(d){return d()}) |
|
.style("fill", "#5c5c5c") |
|
.attr({"dy": 20, "dx": 20}); |
|
|
|
var lines = content.selectAll(".links") |
|
.data(linksData), |
|
linesEnter = lines.enter() |
|
.insert("line", d3.select("#nodes") ? "#nodes" : null) |
|
.attr("class", "links") |
|
.attr({stroke: "steelblue", "stroke-width": 3}); |
|
var nodes = content.selectAll("#nodes") |
|
.data(nodesData), |
|
nodesEnter = nodes.enter().append("g") |
|
.attr("id", "nodes") |
|
.style("outline", "1px solid black"), |
|
nodeText = content.selectAll(".Nodetext").data(["nodes"]), |
|
nodeTextEnter = nodeText.enter().append("text").classed("Nodetext", true) |
|
.text(id) |
|
.style("fill", "#5c5c5c"), |
|
node = nodes.selectAll(".node") |
|
.data(id), |
|
newNode = node.enter().append("g") |
|
.attr("class", "node") |
|
.style("cursor", "pointer") |
|
.call(force.drag) |
|
.on("mouseover", dispatch.mouseover) |
|
.on("mouseout", dispatch.mouseout) |
|
.on("dblclick", dispatch.dblclick) |
|
.on('contextmenu', function(d, i) { |
|
d3.event.preventDefault(); |
|
dispatch.contextmenu(d, i); |
|
}), |
|
circles = newNode.append("circle") |
|
.attr({class: "content"}) |
|
.attr("r", function(d) { |
|
return d.r |
|
}) |
|
.style({"fill": "red", opacity: 0.8}), |
|
labels = newNode.append("text") |
|
.text(function(d, i) {return i}); |
|
|
|
lines.exit().remove(); |
|
node.exit().remove(); |
|
|
|
//svg.xScale().domain([-svg.w/2, svg.w/2]); |
|
|
|
function nodesData(d) { |
|
return [d.nodes]; |
|
} |
|
|
|
function linksData(d) { |
|
return d.edges; |
|
} |
|
|
|
function hookDrag(target, event, hook) { |
|
//hook force.drag behaviour |
|
var stdDragStart = target.on(event); |
|
target.on(event, function(d) { |
|
hook.call(this, d); |
|
stdDragStart.call(this, d); |
|
}); |
|
} |
|
|
|
function zoomed() { |
|
force.alpha(0.01); |
|
}; |
|
|
|
// zoom context services |
|
// content is the target for zoom movements in zoomed |
|
d3.rebind(fdg, content, "attr"); |
|
// access the current transform state in zoom listener coordinates |
|
d3.rebind(fdg, svg, "translate"); |
|
|
|
fdg.nodes = data.nodes; |
|
|
|
force.start(); |
|
return fdg; |
|
}; |
|
|
|
// events |
|
d3.rebind.bind(null, fdg, dispatch, "on").apply(null, events); |
|
|
|
fdg.zoomTime = (function() { |
|
var _t; |
|
return function(t) { |
|
if(t == undefined) return _t; |
|
if(t == null) return (fdg.zoomTo = svg.zoomTo, this); |
|
fdg.zoomTo = fdg.zoomTo.bind(null, _t = t); |
|
return this; |
|
} |
|
})(); |
|
d3.rebind(fdg, svg, "zoomTo") |
|
fdg.zoomTo = function(t, p){ |
|
// tell svg which point in the domain space maps to the center of the range |
|
svg.zoomTo(t, p); |
|
return this; |
|
}; |
|
|
|
fdg.data = data; |
|
return fdg; |
|
|
|
} |
|
function id(d){return d;} |
|
function myName(args) { |
|
return /function\s+(\w*)\(/.exec(args.callee)[1]; |
|
} |