Search code examples
javascriptd3.jszooming

d3.js v4 minimap or how to have shared zoom on two elements?


I found this nice example of zooming with minimap, but it is written in the old version v3. I almost convert it to v4 but there is a problem with d3.event. In v3 d3.event seems to share the zooming params between the two elements where the zoom is called. So if I scale on the main canvas and then on the minimap canvas - the d3.event will have the last scale value of the main canvas zoom d3.event and it will continue with the zoom as it should be. But in v4 the both d3 zoom events have somehow separate values for scale or translate. As in the documentation:

The zoom behavior stores the zoom state on the element to which the zoom behavior was applied, not on the zoom behavior itself. This is because the zoom behavior can be applied to many elements simultaneously, and each element can be zoomed independently.

But that leaves the question how I can have one shared zoom event on two elements?


Solution

  • EDIT :

    Great thanks also to Bill White, who posted in the same thread his updated article - D3 MINIMAP V4 UPDATE. The article is actually an update of the example I posted in my question.


    Answers to the question can be found here.

    @mbostock kindly answered my question in an issue thread. Also I found another solution. I succeeded with implementing some of the logic and formulas from his code directly into my zoom handler. It works like a charm.

    So this is the updated zoom handler from v3 to v4:

    Note: Translation boundery checks not included.

    // ....
    
    var scale = 1;
    var minScale = .5;
    var maxScale = 7.5;
    var translation = [0, 0];
    
    // ....
    
    // Used for both main canvas and minimap zoom
    var zoom = d3.zoom()
        .on('zoom', zoomHandler);
    
    function zoomHandler(newScale) {
        var prevScale = scale;
        var previousTranslation = getXYFromTranslate(panCanvas.attr('transform'));
        var isZoomEvent = d3.event && d3.event.sourceEvent.deltaY;
        var isDragEvent = d3.event && (d3.event.sourceEvent.movementX || d3.event.sourceEvent.movementY);
    
        if (isZoomEvent) {
            scale = calculateNewScale(prevScale, d3.event.sourceEvent);
            scale = checkScaleBounderies(scale);
    
            var mousePosition = d3.mouse(this);
    
            // Based on d3.js zoom algorythm
            translation[0] = mousePosition[0] - ((mousePosition[0] - previousTranslation[0]) / prevScale) * scale;
            translation[1] = mousePosition[1] - ((mousePosition[1] - previousTranslation[1]) / prevScale) * scale;
        } else if (isDragEvent) {
            translation[0] = previousTranslation[0] + d3.event.sourceEvent.movementX;
            translation[1] = previousTranslation[1] + d3.event.sourceEvent.movementY;
        } else if (newScale) {
            scale = newScale;
        }
    
        // Apply the new dimensions to the main canvas
        panCanvas.attr('transform', 'translate(' + translation + ') scale(' + scale + ')');
    
        // Apply the new dimensions to the minimap
        minimap.scale(scale).render();
    }
    
    // Calculate the new scale value based on d3.js zoom formula
    function calculateNewScale(prevScale, event) {
        return prevScale * Math.pow(2, -event.deltaY * (event.deltaMode ? 120 : 1) / 500);
    }
    
    // Check if scale has reached max or min
    function checkScaleBounderies(newScale) {
        return Math.max(minScale, Math.min(maxScale, newScale));
    }
    
    //....
    
    function getXYFromTranslate(transform) {
        // Create a dummy g for calculation purposes only. This will never
        // be appended to the DOM and will be discarded once this function
        // returns.
        var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    
        // Set the transform attribute to the provided string value.
        g.setAttributeNS(null, 'transform', transform);
    
        // consolidate the SVGTransformList containing all transformations
        // to a single SVGTransform of type SVG_TRANSFORM_MATRIX and get
        // its SVGMatrix.
        var matrix = g.transform.baseVal.consolidate().matrix;
    
        // As per definition values e and f are the ones for the translation.
        return [matrix.e, matrix.f];
    }