//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]; }