score:3

I've modified mbostock's drag+zoom example to have a 4x zoom speed and put it in a jsfiddle. I've explained my thinking below. This is my first attempt at a stack overflow answer, please be nice.

As explained in Raúl Martín's answer you can use a formula within the redraw() function to change the rate of zoom. You need to make some extra steps to make sure that the d3 behaviour still works nicely with the modified zoom rate.

Zoom centered on a chosen point (e.g. cursor)
By default d3 behaviour focuses the zoom on the mouse pointer, e.g. if you have the mouse pointer at the top left of the image it zooms in on the top left rather than the center of the image. To get this effect it scales the image and then also changes the translation of the image so that the point under the mouse cursor stays at the same location on the screen. This is why the value of zoom.translate() changes when you scroll the mousewheel even though the image doesn't look like it is moving across the screen.

If you change the zoom speed the d3 zoom.translate() values are no longer correct. To work out the correct translation you need to know the following information (ignore the numeric values):

var prev_translate = [100,100] // x, y translation of the image in last redraw
var prev_scale = 0.1           // Scale applied to the image last redraw
var new_scale = 0.4            // The new scale being applied
var zoom_cp = [150, 150]       // The zoom "center point" e.g. mouse pointer

The formula to work out the new_translate to apply to the image is then:

new_translate[0] = zoom_cp[0] - (zoom_cp[0] - prev_translate[0]) 
    * new_scale / prev_scale;
new_translate[1] = zoom_cp[1] - (zoom_cp[1] - prev_translate[1]) 
    * new_scale / prev_scale;

You can apply this to the image along with your new scale with:

svg.attr("transform", "translate(" + new_translate + ")scale(" + new_scale + ")");

You'll then have to update prev_scale = new_scale and prev_translate = new_translate ready for the next iteration of redraw()

Pan without scaling

The d3 zoom behaviour allows you to pan without scaling by clicking and dragging. If you click and drag then zoom.scale() stays the same but zoom.translate() changes. The new zoom.translate() value is still correct even after you have modified the zoom speed. However, you need to know when to use this zoom.translate() value and when to use the translate value that you calculate for zooming in on a center point.

You can work out whether a pan or a zoom is happening by looking at whether prev_scale is the same as new scale. If the two values are identical you know a pan is taking place and you can use new_translate = zoom.translate() to move the image. Otherwise, you know that a zoom is taking place and you can calculate the new_translate value as described above. I do this by adding a function to the zoomstart event.

var zoom_type = "?";
var scale_grad = 4; // Zoom speed multiple
var intercept = 1 * (1 - scale_grad)

var svg = d3.select("body").append("svg:svg")
            .attr("width", 1000)
            .attr("height", 2000)      
            .append("svg:g")
                .call(d3.behavior.zoom()
                      .on("zoom", redraw)
                      .on("zoomstart", zoomstarted))
            .append("svg:g");

function zoomstarted() {
    zoom_type = "?";
}

function redraw() {
    var scale = d3.event.scale;

    // Use a linear scale, don't let it go below the minimum scale
    // extent
    var new_scale = Math.max(scale_grad * scale + intercept, 
                        scale_extent[0]);

    // If hit the minimum scale extent then stop d3 zoom from 
    // going any further
    if (new_scale == scale_extent[0]) {
         zoom.scale((scale_extent[0] - intercept) / scale_grad);   
    }

    // Set up zoom_type if have just started
    // If the scale hasn't changed then a pure translation is
    // taking place, otherwise it is a scale
    if (zoom_type == "?") {
        if (new_scale == prev_scale) {
            zoom_type = "translate"
        } else {
            zoom_type = "scale"
        }
    }

    // zoom_cp is the static point during the zoom, set as 
    // mouse pointer position (you need to define a listener to track)
    var new_translate = [0, 0];
    zoom_cp = [mouse_x, mouse_y];

    // If the event is a translate just apply d3 translate
    // Otherwise calculate what translation is required to 
    // keep the zoom center point static
    if (zoom_type == "translate") {
        new_translate = d3.event.translate
    } else if (zoom_type == "scale") {
        new_translate[0] = zoom_cp[0]
             - (zoom_cp[0] - prev_translate[0]) * new_scale / prev_scale;
        new_translate[1] = zoom_cp[1] 
             - (zoom_cp[1] - prev_translate[1]) * new_scale / prev_scale;
}

        // Update the variables that track the last iteration of the 
        // zoom
        prev_translate = new_translate;
        prev_scale = new_scale;
        zoom.translate(new_translate);

        // Apply scale and translate behaviour
        svg.attr("transform", "translate(" + new_translate + 
             ")scale(" + new_scale + ")");
}

score:8

You need adjust the scale inside the function with a mathematical function when you select the function the important is that for x=0 the y=0 you can use pow is easier in this case Math.pow(d3.event.scale,.1) the second parameter does the zoom more slowly when is smaller.

It´s not a good idea use a very complicated function because the browser will turn slow.

When you have the new scale, you need recalculate the translation. You don´t complicate the problem, in SVG you have the actual height with this.getBBox().height this ok, but it is not exactly because you are one iteration behind. You could calculate the new height with (originalHeight * scale) and the translate with (originalHeight - (originalHeight * scale))/2

  • Well origialHeight*scale is the newHeight

  • The originalHeight - newHeight is the difference, and you want the center, you need divide for 2, the half part of the square and the half part below.

  • Now we need do the action with the width. It is the same

The code:

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

    svg.append("svg:rect")
        .attr("width", 200)
        .attr("height", 300)
        .attr("fill", 'green');

    function redraw() {
        var velocity = 1/10;
        var scale =  Math.pow(d3.event.scale,velocity);
        var translateY = (300 - (300 * scale))/2;
        var translateX = (200 - (200 * scale))/2;

        svg.attr("transform", "translate(" + [translateX,translateY] + ")" + " scale(" +scale+ ")");            
    };

Note that I put the 200 and 300 hardcoded, you can use a property, use constant...

I created a fiddler: http://jsfiddle.net/t0j5b3e2/


Related Query

More Query from same tag