83

D3 has a force directed layout here. Is there a way to add zooming to this graph? Currently, I was able to capture the mouse wheel event but am not really sure how to write the redraw function itself. Any suggestions?

var vis = d3.select("#graph")
  .append("svg:svg")
  .call(d3.behavior.zoom().on("zoom", redraw)) // <-- redraw function
  .attr("width", w)
  .attr("height", h);
xxx
  • 1,153
  • 1
  • 11
  • 23
Legend
  • 113,822
  • 119
  • 272
  • 400
  • See also this example http://thisismattmiller.com/blog/add-zoom-slider-to-d3-js/ by Matt Miller. It only adds a "g" element at the end of the process. – arivero Jun 24 '12 at 03:37
  • 4
    somebody showed how to combine zui53 (a library for zoomable interfaces) and d3js: http://bl.ocks.org/timelyportfolio/5149102 – widged Mar 13 '13 at 09:16

6 Answers6

97

Update 6/4/14

See also Mike Bostock's answer here for changes in D3 v.3 and the related example. I think this probably supersedes the answer below.

Update 2/18/2014

I think @ahaarnos's answer is preferable if you want the entire SVG to pan and zoom. The nested g elements in my answer below are really only necessary if you have non-zooming elements in the same SVG (not the case in the original question). If you do apply the behavior to a g element, then a background rect or similar element is required to ensure that the g receives pointer events.

Original Answer

I got this working based on the zoom-pan-transform example - you can see my jsFiddle here: http://jsfiddle.net/nrabinowitz/QMKm3/

It was a bit more complex than I had hoped - you have to nest several g elements to get it to work, set the SVG's pointer-events attribute to all, and then append a background rectangle to receive the pointer events (otherwise it only works when the pointer is over a node or link). The redraw function is comparatively simple, just setting a transform on the innermost g:

var vis = d3.select("#chart")
  .append("svg:svg")
    .attr("width", w)
    .attr("height", h)
    .attr("pointer-events", "all")
  .append('svg:g')
    .call(d3.behavior.zoom().on("zoom", redraw))
  .append('svg:g');

vis.append('svg:rect')
    .attr('width', w)
    .attr('height', h)
    .attr('fill', 'white');

function redraw() {
  console.log("here", d3.event.translate, d3.event.scale);
  vis.attr("transform",
      "translate(" + d3.event.translate + ")"
      + " scale(" + d3.event.scale + ")");
}

This effectively scales the entire SVG, so it scales stroke width as well, like zooming in on an image.

There is another example that illustrates a similar technique.

Community
  • 1
  • 1
nrabinowitz
  • 55,314
  • 10
  • 149
  • 165
  • Thanks! This saved me some time tonight! http://nolasatellitegovernment.tulane.edu/vis/d3/force/linked.html – jerrygarciuh Oct 27 '11 at 03:06
  • As of 12/1/2011 actual browser behavior isn't the same as in your jsFiddle. The fiddle preserves the force layout click+drag behavior. In a real browser (IE, Chrome, Safari, Firefox) click+drag results in a pan of the whole layout. Any thought on how to have the zoom/pan layer ignore clicks on nodes? –  Dec 01 '11 at 20:02
  • 1
    @Ogg - I'm not sure what you mean here - jsFiddle just presents your results in an iFrame, not some sort of custom browser, so what you see *is* real browser behavior. jsFiddle does add some things, e.g. a `body` tag, so I recommend looking at the frame source and seeing what you're missing. – nrabinowitz Dec 01 '11 at 23:49
  • Is there a way to exempt text elements from the scaling so they remain at the same font size? – Eric Hartford Oct 10 '12 at 22:17
  • 2
    @EricStob - that might be a new question. But see http://jsfiddle.net/56RDx/2/ - this simply rescales the font size by the inverse of the zoom scale. – nrabinowitz Oct 10 '12 at 23:51
  • This also works when the 2nd `append('svg:g')` call is removed. I see it's also in the zoom-pan-transform example, but don't see why. – ramiro Oct 24 '12 at 19:22
  • When you zoom it, it seems that the background rectangle shrinks and you lose your zoom target. What's a good way to scale the background rectangle too. – Evan Nov 02 '12 at 01:54
  • @Evan - Good point. Append the background rectangle to the outer group, not the inner one assigned to `vis`. See http://jsfiddle.net/nrabinowitz/psaC6/ – nrabinowitz Nov 02 '12 at 17:45
  • Is there a way to restrict the zoom-out percentage? – ajmartin Nov 27 '12 at 00:46
  • 1
    @ajmartin - see [`zoom.scaleExtent()`](https://github.com/mbostock/d3/wiki/Zoom-Behavior#wiki-scaleExtent) – nrabinowitz Nov 27 '12 at 17:20
  • What is the colon in `svg:g` (why is not `g`)? – Ziyuan Dec 26 '12 at 22:16
  • The colon may no longer be needed - in earlier versions of d3, you were required to namespace SVG elements when appending. – nrabinowitz Dec 27 '12 at 05:12
  • @CQM - using the standard D3 zoom behavior, scrolling up will zoom out, and scrolling down or double-click will zoom in. – nrabinowitz Jan 11 '13 at 19:04
  • This does not seem to work on mobile browsers. You can pan/zoom but you can no longer click on the individual nodes. You can verify by going to his answer in a mobile browser. Don't have one? You can emulate by going to iOS simulator or http://www.browserstack.com/. – makeshifthoop Feb 11 '13 at 00:34
  • @nrabinowitz, Hi, is there a way to dynamicaly update your solution with AJAX in some specific ways, like change radius of speficic nodes, but don't change its positions in visualization, or to fade color of some nodes over time, but not change anything else. Respectively all together: I want to make some visualization as you provided in your solution, but I'd like to know how to dynamicaly change over time every aspect as I want (radius of circle, its color, strength of force between circles etc.) Thank you! – Kamil Feb 25 '13 at 02:56
  • Using this code, I can't zoom or pan at the edges of my graph, only in the center. I tried increasing the size of the rect but to no avail. It happens in Firefox, IE10 and Chrome. Is anyone else encountering this? – Matthias Braun May 14 '13 at 12:01
  • zoom/pan doesn't seem to be working in IE9 for me. Anyone know why? – aug Jul 08 '13 at 17:28
  • @ChristianStewart - what version? This appears to work for me: http://jsfiddle.net/2J8M9/ (v.3.3.11) – nrabinowitz Dec 09 '13 at 22:46
  • @nrabinowitz It's working now, but instead of translating the SVG I translate a container 'g' - I accidentally was translating the entire SVG before. – Christian Stewart Dec 10 '13 at 01:02
  • 1
    When using [version 3](http://d3js.org/d3.v3.js) of d3, the ability to drag an individual node does not work in this example. Instead it pans the whole graph as though you have not clicked on a node. This works on [version 2](http://d3js.org/d3.v2.js) but I need a feature on v3. Any ideas? – Daryl Van Sittert Feb 03 '14 at 16:44
  • @ahaarnos's solution is much better than this approach. – Uri Feb 18 '14 at 17:47
  • @Uri - I agree, with the caveat that it only works if you want the entire SVG to zoom. See my updated answer. – nrabinowitz Feb 18 '14 at 19:19
  • I'm experiencing the same issues as Daryl. The problem is not limited to this method, but affects @ahaarnos's solution as well. Attempting to drag a single node pans the entire graphic. Here's an updated fiddle with D3 v3 that exhibits the issue: http://jsfiddle.net/QMKm3/715/ – David Marx Jun 03 '14 at 18:35
  • 1
    Here's the solution for D3 v3: http://stackoverflow.com/questions/17953106/why-does-d3-js-v3-break-my-force-graph-when-implementing-zooming-when-v2-doesnt – David Marx Jun 03 '14 at 19:53
  • Why is vis.append("svg:rect") necessary? (I tried, it doesn't work without it). Many thanks – Lzh Jan 17 '15 at 07:38
  • The rect is used to have mouse events bubble to it, and it can't capture the events itself (because it is just a grouping) element. My question is still there if someone can add more insight on how d3 zoom works. Thanks – Lzh Jan 17 '15 at 08:12
18

Why the nested <g>'s?

This code below worked well for me (only one <g>, with no random large white <rect>:

var svg = d3.select("body")
    .append("svg")
      .attr({
        "width": "100%",
        "height": "100%"
      })
      .attr("viewBox", "0 0 " + width + " " + height )
      .attr("preserveAspectRatio", "xMidYMid meet")
      .attr("pointer-events", "all")
    .call(d3.behavior.zoom().on("zoom", redraw));

var vis = svg
    .append('svg:g');

function redraw() {
  vis.attr("transform",
      "translate(" + d3.event.translate + ")"
      + " scale(" + d3.event.scale + ")");
}

Where all the elements in your svg are then appended to the vis element.

aaaronic
  • 435
  • 5
  • 7
  • 1
    May it be that you could lose the attributes "viewBox", "preserveAspectRatio" and "pointer-events" and it would still work? – notan3xit Jul 11 '13 at 16:37
  • @notan3xit is right, viewBox, preserveAspectRatio and pointer-events are not necessary. The key is to apply the `transformation` attribute on the `g` element, *not* on the `svg` element. – Lekensteyn May 20 '14 at 16:33
  • Doesn't seem to work with D3 v3, or rather the zoom feature still works but the ability to move individual nodes is lost. @nrabinowitz solution exhibits the same problem. Here's nrabinowitz' fiddle updated to use ahaarnos' solution: http://jsfiddle.net/QMKm3/716/ and here's the same fiddle updated to use D3v3 to illustrate the problem: http://jsfiddle.net/QMKm3/717/ – David Marx Jun 03 '14 at 18:41
  • Perfect idea to add the zoom behavior to the SVG element, I did not know you could do that and therefor always resorted to using annoying background rectangles. Adding the behavior on the SVG works at least in modern versions of Chrome, FF, and Opera. – rcijvat Nov 12 '15 at 11:54
14

The provided answers work in D3 v2 but not in v3. I've synthesized the responses into a clean solution and resolved the v3 issue using the answer provided here: Why does d3.js v3 break my force graph when implementing zooming when v2 doesn't?

First the main code. This is a cleaned up version of @ahaarnos' answer:

    var svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
            .call(d3.behavior.zoom().on("zoom", redraw))
        .append('g');

    function redraw() {
      svg.attr("transform",
          "translate(" + d3.event.translate + ")"
          + " scale(" + d3.event.scale + ")");
    }   

Now you have pan and zoom, but you won't be able to drag nodes because the pan functionality will override the drag functionality. So we need to do this:

var drag = force.stop().drag()
.on("dragstart", function(d) {
    d3.event.sourceEvent.stopPropagation(); // to prevent pan functionality from 
                                            //overriding node drag functionality.
    // put any other 'dragstart' actions here
});

Here's @nrabinowitz' fiddle modified to use this cleaner zoom implementation, but illustrating how D3v3 breaks node drag: http://jsfiddle.net/QMKm3/718/

And here's the same fiddle modified to work with D3v3: http://jsfiddle.net/QMKm3/719/

Community
  • 1
  • 1
David Marx
  • 8,172
  • 3
  • 45
  • 66
2

I got my graph to work without the second "svg:g" append.

[...].attr("pointer-events", "all")
     .attr("width", width2)
     .attr("height", height2)
     .append('svg:g')
     .call(d3.behavior.zoom().on("zoom", redraw));

The rest is the same.

cem
  • 199
  • 2
  • 4
0

I got a solution for D3 force directed graph with zooming option.

    var m = [40, 240, 40, 240],
    width = 960,
    height = 700,
    root;
var svg = d3.select("body").append("svg")
    .attr("class", "svg_container")
    .attr("width", width)
    .attr("height", height)
    .style("overflow", "scroll")
    .style("background-color", "#EEEEEE")
    .append("svg:g")
    .attr("class", "drawarea")
    .append("svg:g")
    .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

//applying zoom in&out for svg
d3.select("svg") 
.call(d3.behavior.zoom()
    .scaleExtent([0.5, 5])
    .on("zoom", zoom));

//zooming 
function zoom() { //zoom in&out function 
    var scale = d3.event.scale,
        translation = d3.event.translate,
        tbound = -height * scale,
        bbound = height * scale,
        lbound = (-width + m[1]) * scale,
        rbound = (width - m[3]) * scale;
    // limit translation to thresholds
    translation = [
        Math.max(Math.min(translation[0], rbound), lbound),
        Math.max(Math.min(translation[1], bbound), tbound)
    ];
    d3.select(".drawarea")
        .attr("transform", "translate(" + translation + ")" +
            " scale(" + scale + ")");
}
Aravind Cheekkallur
  • 3,157
  • 6
  • 27
  • 41
0

If you want to zoom and pan force layout without changing node-size, try below. You can also drag nodes without trembling. This code is based on original force layout example. As for nodes and links data, please refer to original sample data. http://bl.ocks.org/mbostock/4062045

Plz note the variables xScale and yScale, the functions dragstarted(), dragged(), and dragended(). Function tick() was changed as well.

You can see the result at http://steelblue.tistory.com/9 The language on the site is Korean. However you can easily find the result at the third example on the page.

var graph = {
    "nodes": [
      { "name": "Myriel", "group": 1 },
      { "name": "Napoleon", "group": 1 },
      // ......
      { "name": "Mme.Hucheloup", "group": 8 }
    ],
    "links": [
      { "source": 1, "target": 0, "value": 1 },
      { "source": 2, "target": 0, "value": 8 },
    // .......
      { "source": 76, "target": 58, "value": 1 }
    ]
};
var width = 640,
    height = 400;
 var color = d3.scale.category20();



var xScale = d3.scale.linear()
        .domain([0, width])
         .range([0, width]);

var yScale = d3.scale.linear()
    .domain([0, height])
   .range([0, height]);
var zoomer = d3.behavior.zoom().x(xScale).y(yScale).scaleExtent([0.1, 8]).on("zoom", zoom);
function zoom() {

    tick(); 
};

var drag = d3.behavior.drag()
        .origin(function (d) { return d; })
         .on("dragstart", dragstarted)
        .on("drag", dragged)
        .on("dragend", dragended);

function dragstarted(d) {
    d3.event.sourceEvent.stopPropagation();

    d.fixed |= 2;         
}
function dragged(d) {

    var mouse = d3.mouse(svg.node());
    d.x = xScale.invert(mouse[0]);
    d.y = yScale.invert(mouse[1]);
    d.px = d.x;         
    d.py = d.y;
    force.resume();
}

function dragended(d) {

    d.fixed &= ~6;           }

var force = d3.layout.force()
    .charge(-120)
    .linkDistance(30)
    .size([width, height]);

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

svg.call(zoomer);

    force
        .nodes(graph.nodes)
        .links(graph.links)
        .start();

    var link = svg.selectAll(".link")
        .data(graph.links)
      .enter().append("line")
        .attr("class", "link")
        .style("stroke-width", function (d) { return Math.sqrt(d.value); });

    var node = svg.selectAll(".node")
        .data(graph.nodes)
      .enter().append("circle")
        .attr("class", "node")
        .attr("r", 5)
        .style("fill", function (d) { return color(d.group); })
        .call(drag);

    node.append("title")
        .text(function (d) { return d.name; });

    force.on("tick",tick);

function tick(){            
        link.attr("x1", function (d) { return  xScale(d.source.x); })
            .attr("y1", function (d) { return yScale(d.source.y);  })
            .attr("x2", function (d) { return xScale(d.target.x); })
            .attr("y2", function (d) { return yScale(d.target.y); });

        node.attr("transform", function (d) {
            return "translate(" + xScale(d.x) + "," + yScale(d.y) + ")";
        });


    };