Search code examples
javascriptd3.jssvgzoomingbounds

How to zoom a d3 chart correctly?


I made a chart with d3 word cloud layout. I want to zoom it using d3.zoom(). The problem is that when I implement the zoom function, the SVG is out of bounds. Like this example: https://codepen.io/bitbyte/pen/oVKGjx

var words = ["two", "two", "seven", "seven", "seven", "seven", "seven", "seven", "seven", "three", "three", "three", "eight", "eight", "eight", "eight", "eight", "eight", "eight", "eight", "five", "five", "five", "five", "five", "four", "four", "four", "four", "nine", "nine", "nine", "nine", "nine", "nine", "nine", "nine", "nine", "one", "ten", "ten", "ten", "ten", "ten", "ten", "ten", "ten", "ten", "ten", "six", "six", "six", "six", "six", "six"]
        .map(function(d,i) {
            //console.log(d);
            return {text: d, size: -i};
        });

var fontName = "Impact",
    cWidth = 720,
    cHeight = 400,
    svg,
    wCloud,
    bbox,
    ctm,
    bScale,
    bWidth,
    bHeight,
    bMidX,
    bMidY,
    bDeltaX,
    bDeltaY;

var cTemp = document.createElement('canvas'),
    ctx = cTemp.getContext('2d');
    ctx.font = "100px " + fontName;

var fRatio = Math.min(cWidth, cHeight) / ctx.measureText(words[0].text).width,
    fontScale = d3.scale.linear()
        .domain([
            d3.min(words, function(d) { return d.size; }), 
            d3.max(words, function(d) { return d.size; })
        ])
        //.range([20,120]),
        .range([20,100*fRatio/2]), // tbc
    fill = d3.scale.category20();

d3.layout.cloud()
    .size([cWidth, cHeight])
    .words(words)
    //.padding(2) // controls
    .rotate(function() { return ~~(Math.random() * 2) * 90; })
    .font(fontName)
    .fontSize(function(d) { return fontScale(d.size) })
    .on("end", draw)
    .start();

function draw(words, bounds) {
    // move and scale cloud bounds to canvas
    // bounds = [{x0, y0}, {x1, y1}]
    bWidth = bounds[1].x - bounds[0].x;
    bHeight = bounds[1].y - bounds[0].y;
    bMidX = bounds[0].x + bWidth/2;
    bMidY = bounds[0].y + bHeight/2;
    bDeltaX = cWidth/2 - bounds[0].x + bWidth/2;
    bDeltaY = cHeight/2 - bounds[0].y + bHeight/2;
    bScale = bounds ? Math.min( cWidth / bWidth, cHeight / bHeight) : 1;



    svg = d3.select(".cloud").append("svg")
        .attr("width", cWidth)
        .attr("height", cHeight)
     .call(d3.behavior.zoom().on("zoom", function () {
    svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")")
  }))


    wCloud = svg.append("g")

        .attr("transform", "translate(360,200)")
        .selectAll("text")
        .data(words)
        .enter().append("text")
        .style("font-size", function(d) { return d.size + "px"; })
        .style("font-family", fontName)
        .style("fill", function(d, i) { return fill(i); })
        .attr("text-anchor", "middle")
        .transition()
        .duration(500)
        .attr("transform", function(d) {
            return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
        })
        .text(function(d) { return d.text; });

    bbox = wCloud.node(0).getBBox();


};

What is the correct way to make a zoom chart like this https://bl.ocks.org/sgruhier/50990c01fe5b6993e82b8994951e23d0

With the square fixed container with the SVG inside of them and not moving around for all the page when you zoom in it.


Solution

  • Here is an updated codepen with your solution: https://codepen.io/cstefanache/pen/ZPgmwy

    you have to apply the transformation on an group element not directly on the SVG DOM in order to allow the user have further mouse interaction with the SVG viewport.

    .call(d3.behavior.zoom().on("zoom", function () {
        groupElement.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")")
    }))
    

    the changes to your original codepen was to store the reference to the word cloud group in order to be used and apply all changes to that reference

    wCloud = svg.append("g");   
    wCloud.selectAll("text")
        .data(words)
        ...
    

    Also the initial transformation was removed because if the group element has a transformation value initially it will be replaced with a new transformation from the d3 event that will have large differences between x,y position and it will generate a flicker on the first transformation.

    In order to avoid this the initial position of the group has no transformation but the words were placed relative to the center of the viewport:

    .attr("transform", function(d) {
        return "translate(" + [bMidX + d.x, bMidY + d.y] + ")rotate(" + d.rotate + ")";
    })