Tim Sodergren
Tim Sodergren

Reputation: 11

D3 data do not rescale properly after pan or zoom

I am working with D3 v4 and JS. I have a scatter plot with a predefined set of data loaded along with axes with the ability to pan and zoom. I need to be able to then dynamically add points and eventually output them in data space not pixel space. I am using the "rescaleX" and "rescaleY" methods of the zoom object. They work fine for rescaling the axes but, when I try to add new points, the location of the plotted point does correspond to the mouse location. Here is a simplified version of the code:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>

    var data = [{x:17,y:3},
        {x:20,y:16},
        {x:2,y:13},
        {x:19,y:10},
        {x:13,y:15},
        {x:2,y:2},
        {x:5,y:8},
        {x:11,y:19},
        {x:20,y:12},
        {x:10,y:20}];

    var width = 600;
    var height = 600;
    var padding = 50;
    var newXscale, newYscale;

    var dataScale = d3.scaleLinear()
        .domain([0,21])
        .range([0, width]);

    var svg = d3.select('body').append('svg')
        .attr('width', width+2*padding)
        .attr('height', height+2*padding)
        .on('click', clicked);

    var xAxis = d3.axisTop()
        .scale(dataScale);

    var gX = svg.append('g')
        .attr('transform','translate(50,50)')
        .call(xAxis);

    var yAxis = d3.axisLeft()
        .scale(dataScale);

    var gY = svg.append('g')
        .attr('transform','translate(50,50)')
        .call(yAxis);

    var canvas = svg.append('g')

    var points = canvas.append('g');
    points.selectAll('circle').data(data)
        .enter().append('circle')
        .attr('cx', function(d) {return dataScale(d.x)+padding})
        .attr('cy', function(d) {return dataScale(d.y)+padding})
        .attr('r', 5);

    var zoom
    var zoomOn = false;
    window.addEventListener('keydown', function (event) {
        if (event.key=='z') {
            if (zoomOn) {
                d3.select('#zoomBox').remove();
                zoomOn = false;
            } else {
                zoom = d3.zoom()
                    .scaleExtent([0.1, 10])
                    .on('zoom', zoomed);

                svg.append("rect")
                    .attr('cursor','move')
                    .attr("width", width+padding*2)
                    .attr("height", height+padding*2)
                    .attr('id','zoomBox')
                    .style("fill", "none")
                    .style("pointer-events", "all")
                    .call(zoom);
                zoomOn = true;
            }

        }
    });

    function zoomed() {
        canvas.attr("transform", d3.event.transform)
        newXscale = d3.event.transform.rescaleX(dataScale);
        newYscale = d3.event.transform.rescaleY(dataScale);
        gX.call(xAxis.scale(newXscale));
        gY.call(yAxis.scale(newYscale));
    }

    function clicked() {
        var coords = d3.mouse(this);
        points.append('circle')
            .attr('cx',coords[0])
            .attr('cy',coords[1])
            .attr('r',5);
        var x = newXscale.invert(coords[0]-padding);
        var y = newYscale.invert(coords[1]-padding);
        console.log(x+' '+y);
    }

</script>

</body>
</html>

Upvotes: 1

Views: 892

Answers (2)

Tim Sodergren
Tim Sodergren

Reputation: 11

I figured it out. The problem lied in the fact that I was removing the zoom box when toggling the zoom. I switched the event listener to just hide the box and unbind the pointer-events. Here is the final code:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>

    var data = [{x:17,y:3},
        {x:20,y:16},
        {x:2,y:13},
        {x:19,y:10},
        {x:13,y:15},
        {x:2,y:2},
        {x:5,y:8},
        {x:11,y:19},
        {x:20,y:12},
        {x:10,y:20}];

    var width = 600;
    var height = 600;
    var padding = 50;
    var newXscale, newYscale;
    var zoomOn = false;

    var xScale = d3.scaleLinear()
        .domain([0,21])
        .range([0, width]);

    var yScale = d3.scaleLinear()
        .domain([0,21])
        .range([0, width]);

    var svg = d3.select('body').append('svg')
        .attr('width', width+2*padding)
        .attr('height', height+2*padding)
        .on('click', clicked)
        .attr('cursor','crosshair');

    var xAxis = d3.axisTop()
        .scale(xScale);

    var gX = svg.append('g')
        .attr('transform','translate(50,50)')
        .call(xAxis);

    var yAxis = d3.axisLeft()
        .scale(yScale);

    var gY = svg.append('g')
        .attr('transform','translate(50,50)')
        .call(yAxis);

    var canvas = svg.append('g')

    var points = canvas.append('g');
    points.selectAll('circle').data(data)
        .enter().append('circle')
        .attr('cx', function(d) {return xScale(d.x)+padding})
        .attr('cy', function(d) {return yScale(d.y)+padding})
        .attr('r', 5);

    var zoom = d3.zoom()
        .scaleExtent([0.1, 10])
        .on('zoom', zoomed);

    var zoombox = svg.append("rect")
        .attr("width", width+padding*2)
        .attr("height", height+padding*2)
        .attr('id','zoomBox')
        .style("fill", "none")
        .style("pointer-events", "none")
        .style('visibility','off')
        .call(zoom);

    window.addEventListener('keydown', function (event) {
        if (event.key=='z') {
            if (zoomOn) {
                d3.select('#zoomBox')
                    .attr('cursor','auto')
                    .style('pointer-events','none')
                    .style('visibility','off');
                zoomOn = false;
            } else {
                d3.select('#zoomBox')
                    .attr('cursor','move')
                    .style('pointer-events','all')
                    .style('visibilty','on')
                zoomOn = true;
            }

        }
    });

    function zoomed() {
        canvas.attr("transform", d3.event.transform)
        newXscale = d3.event.transform.rescaleX(xScale);
        newYscale = d3.event.transform.rescaleY(yScale);
        gX.call(xAxis.scale(newXscale));
        gY.call(yAxis.scale(newYscale));
        newZscale = d3.event.transform.k;

    }

    function clicked() {
        var coords = d3.mouse(this);
        if (newXscale && newYscale) {
            var x = newXscale.invert(coords[0] - padding);
            var y = newYscale.invert(coords[1] - padding);
        };
        console.log(newZscale);
        points.append('circle')
            .attr('cx', (!x) ? coords[0] : xScale(x) + (padding / newZscale))
            .attr('cy', (!y) ? coords[1] : yScale(y) + (padding / newZscale))
            .attr('r', 5);
        console.log(x + ' ' + y);
    }

</script>

</body>
</html>

Upvotes: 0

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

Create a variable to store the zoom level:

newZscale = d3.event.transform.k;

And, in your clicked function, use the dateScale to plot the new circles, dividing the padding by the zoom level:

function clicked() {
    var coords = d3.mouse(this);
    if (newXscale && newYscale) {
        var x = newXscale.invert(coords[0] - padding);
        var y = newYscale.invert(coords[1] - padding);
    };
    console.log(newZscale);
    points.append('circle')
        .attr('cx', (!x) ? coords[0] : dataScale(x) + (padding / newZscale))
        .attr('cy', (!y) ? coords[1] : dataScale(y) + (padding / newZscale))
        .attr('r', 5);
    console.log(x + ' ' + y);
}

Here is the demo:

var data = [{
    x: 17,
    y: 3
}, {
    x: 20,
    y: 16
}, {
    x: 2,
    y: 13
}, {
    x: 19,
    y: 10
}, {
    x: 13,
    y: 15
}, {
    x: 2,
    y: 2
}, {
    x: 5,
    y: 8
}, {
    x: 11,
    y: 19
}, {
    x: 20,
    y: 12
}, {
    x: 10,
    y: 20
}];

var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale, newZscale;

var dataScale = d3.scaleLinear()
    .domain([0, 21])
    .range([0, width]);

var svg = d3.select('body').append('svg')
    .attr('width', width + 2 * padding)
    .attr('height', height + 2 * padding)
    .on('click', clicked);

var xAxis = d3.axisTop()
    .scale(dataScale);

var gX = svg.append('g')
    .attr('transform', 'translate(50,50)')
    .call(xAxis);

var yAxis = d3.axisLeft()
    .scale(dataScale);

var gY = svg.append('g')
    .attr('transform', 'translate(50,50)')
    .call(yAxis);

var canvas = svg.append('g')

var points = canvas.append('g');
points.selectAll('circle').data(data)
    .enter().append('circle')
    .attr('cx', function(d) {
        return dataScale(d.x) + padding
    })
    .attr('cy', function(d) {
        return dataScale(d.y) + padding
    })
    .attr('r', 5);

var zoom
var zoomOn = false;
window.addEventListener('keydown', function(event) {
    if (event.key == 'z') {
        if (zoomOn) {
            d3.select('#zoomBox').remove();
            zoomOn = false;
        } else {
            zoom = d3.zoom()
                .scaleExtent([0.1, 10])
                .on('zoom', zoomed);



            svg.append("rect")
                .attr('cursor', 'move')
                .attr("width", width + padding * 2)
                .attr("height", height + padding * 2)
                .attr('id', 'zoomBox')
                .style("fill", "none")
                .style("pointer-events", "all")
                .call(zoom);
            zoomOn = true;
        }

    }
});

function zoomed() {
    canvas.attr("transform", d3.event.transform)
    newXscale = d3.event.transform.rescaleX(dataScale);
    newYscale = d3.event.transform.rescaleY(dataScale);
    newZscale = d3.event.transform.k;
    gX.call(xAxis.scale(newXscale));
    gY.call(yAxis.scale(newYscale));
}

function clicked() {
    var coords = d3.mouse(this);
    if (newXscale && newYscale) {
        var x = newXscale.invert(coords[0] - padding);
        var y = newYscale.invert(coords[1] - padding);
    };
    points.append('circle')
        .attr('cx', (!x) ? coords[0] : dataScale(x) + (padding / newZscale))
        .attr('cy', (!y) ? coords[1] : dataScale(y) + (padding / newZscale))
        .attr('r', 5);
}
<script src="https://d3js.org/d3.v4.min.js"></script>

Upvotes: 1

Related Questions