d3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median
Asked Answered
A

2

7

I've built a d3.js scatter plot with zoom/pan functionality. You can see the full thing here (click 'Open in a new window' to see the whole thing): http://bl.ocks.org/129f64bfa2b0d48d27c9

There are a couple of features that I've been unable to figure out, that I'd love a hand with it if someone can point me in the right direction:

  1. I want to apply X/Y zoom/pan boundaries to the area, so that you can't drag it below a certain point (e.g. zero).
  2. I've also made a stab at creating Google Maps style +/- zoom buttons, without any success. Any ideas?

Much less importantly, there are also a couple of areas where I've figured out a solution but it's very rough, so if you have a better solution then please do let me know:

  1. I've added a 'reset zoom' button but it merely deletes the graph and generates a new one in its place, rather than actually zooming the objects. Ideally it should actually reset the zoom.
  2. I've written my own function to calculate the median of the X and Y data. However I'm sure that there must be a better way to do this with d3.median but I can't figure out how to make it work.

    var xMed = median(_.map(data,function(d){ return d.TotalEmployed2011;}));
    var yMed = median(_.map(data,function(d){ return d.MedianSalary2011;}));
    
    function median(values) {
        values.sort( function(a,b) {return a - b;} );
        var half = Math.floor(values.length/2);
    
        if(values.length % 2)
            return values[half];
        else
            return (parseFloat(values[half-1]) + parseFloat(values[half])) / 2.0;
    };
    

A very simplified (i.e. old) version of the JS is below. You can find the full script at https://gist.github.com/richardwestenra/129f64bfa2b0d48d27c9#file-main-js

d3.csv("js/AllOccupations.csv", function(data) {

    var margin = {top: 30, right: 10, bottom: 50, left: 60},
        width = 960 - margin.left - margin.right,
        height = 500 - margin.top - margin.bottom;

    var xMax = d3.max(data, function(d) { return +d.TotalEmployed2011; }),
        xMin = 0,
        yMax = d3.max(data, function(d) { return +d.MedianSalary2011; }),
        yMin = 0;

    //Define scales
    var x = d3.scale.linear()
        .domain([xMin, xMax])
        .range([0, width]);

    var y = d3.scale.linear()
        .domain([yMin, yMax])
        .range([height, 0]);

    var colourScale = function(val){
        var colours = ['#9d3d38','#c5653a','#f9b743','#9bd6d7'];
        if (val > 30) {
            return colours[0];
        } else if (val > 10) {
            return colours[1];
        } else if (val > 0) {
            return colours[2];
        } else {
            return colours[3];
        }
    };


    //Define X axis
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom")
        .tickSize(-height)
        .tickFormat(d3.format("s"));

    //Define Y axis
    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left")
        .ticks(5)
        .tickSize(-width)
        .tickFormat(d3.format("s"));

    var svg = d3.select("#chart").append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
        .call(d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", zoom));

    svg.append("rect")
        .attr("width", width)
        .attr("height", height);

    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis);

    // Create points
    svg.selectAll("polygon")
        .data(data)
        .enter()
        .append("polygon")
        .attr("transform", function(d, i) {
            return "translate("+x(d.TotalEmployed2011)+","+y(d.MedianSalary2011)+")";
        })
        .attr('points','4.569,2.637 0,5.276 -4.569,2.637 -4.569,-2.637 0,-5.276 4.569,-2.637')
        .attr("opacity","0.8")
        .attr("fill",function(d) {
            return colourScale(d.ProjectedGrowth2020);
        });

    // Create X Axis label
    svg.append("text")
        .attr("class", "x label")
        .attr("text-anchor", "end")
        .attr("x", width)
        .attr("y", height + margin.bottom - 10)
        .text("Total Employment in 2011");

    // Create Y Axis label
    svg.append("text")
        .attr("class", "y label")
        .attr("text-anchor", "end")
        .attr("y", -margin.left)
        .attr("x", 0)
        .attr("dy", ".75em")
        .attr("transform", "rotate(-90)")
        .text("Median Annual Salary in 2011 ($)");


    function zoom() {
      svg.select(".x.axis").call(xAxis);
      svg.select(".y.axis").call(yAxis);
      svg.selectAll("polygon")
            .attr("transform", function(d) {
                return "translate("+x(d.TotalEmployed2011)+","+y(d.MedianSalary2011)+")";
            });
    };
    }
});

Any help would be massively appreciated. Thanks!

Edit: Here is a summary of the fixes I used, based on Superboggly's suggestions below:

    // Zoom in/out buttons:
    d3.select('#zoomIn').on('click',function(){
        d3.event.preventDefault();
        if (zm.scale()< maxScale) {
            zm.translate([trans(0,-10),trans(1,-350)]);
            zm.scale(zm.scale()*2);
            zoom();
        }
    });
    d3.select('#zoomOut').on('click',function(){
        d3.event.preventDefault();
        if (zm.scale()> minScale) {
            zm.scale(zm.scale()*0.5);
            zm.translate([trans(0,10),trans(1,350)]);
            zoom();
        }
    });
    // Reset zoom button:
    d3.select('#zoomReset').on('click',function(){
        d3.event.preventDefault();
        zm.scale(1);
        zm.translate([0,0]);
        zoom();
    });


    function zoom() {

        // To restrict translation to 0 value
        if(y.domain()[0] < 0 && x.domain()[0] < 0) {
            zm.translate([0, height * (1 - zm.scale())]);
        } else if(y.domain()[0] < 0) {
            zm.translate([d3.event.translate[0], height * (1 - zm.scale())]);
        } else if(x.domain()[0] < 0) {
            zm.translate([0, d3.event.translate[1]]);
        }
        ...
    };

The zoom translation that I used is very ad hoc and basically uses abitrary constants to keep the positioning more or less in the right place. It's not ideal, and I'd be willing to entertain suggestions for a more universally sound technique. However, it works well enough in this case.

Adda answered 25/2, 2013 at 15:3 Comment(0)
T
12

To start with the median function just takes an array and an optional accessor. So you can use it the same way you use max:

var med = d3.median(data, function(d) { return +d.TotalEmployed2011; });

As for the others if you pull out your zoom behaviour you can control it a bit better. So for example instead of

var svg = d3.select()...call(d3.behavior.zoom()...) 

try:

var zm = d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", zoom);
var svg = d3.select()...call(zm);

Then you can set the zoom level and translation directly:

function zoomIn() {
   zm.scale(zm.scale()*2);
   // probably need to compute a new translation also
}

function reset() {
   zm.scale(1);
   zm.translate([0,0]);
}

Restricting the panning range is a bit trickier. You can simply not update when the translate or scale is not to your liking inside you zoom function (or set the zoom's "translate" to what you need it to be). Something like (I think in your case):

function zoom() {
    if(y.domain()[0] < 0) {
        // To restrict translation to 0 value
        zm.translate([d3.event.translate[0], height * (1 - zm.scale())]);
    }
    ....
}        

Keep in mind that if you want zooming in to allow a negative on the axis, but panning not to you will find you get into some tricky scenarios.

This might be dated, but check out Limiting domain when zooming or panning in D3.js

Note also that the zoom behaviour did have functionality for limiting panning and zooming at one point. But the code was taken out in a later update.

Tollgate answered 25/2, 2013 at 20:24 Comment(5)
Thanks again Superboggly! Your zoomIn/reset code seems to work, but the scale only changes on the next zoom event (i.e. on drag or mousewheel). I'm having a hard time getting things to update on button click. It must be simple but I just can't figure it out. My button click code is: d3.select('#zoomIn').call(zoom).on('click',function(){ d3.event.preventDefault(); zm.scale(zm.scale()*2); }); I've tried various uses of .call(), zoom() and .on() to no avail.Adda
I added a quick little blue zoom square at the bottom of my last jsfiddle for you. All you are missing is a call to zoom() after you've set the scale. Think of the zoom function that you've supplied as applying the zoom state computed by the behaviour.Tollgate
okay! Sorted! It turns out that that's exactly what I was doing but it wasn't working for me... Long story short: You were using d3 version 3 while I was using version 2, and that's why it didn't work when I dropped your blue square in my code. Have upgraded to v3 and it works now. Cheers :)Adda
Ah good to know! I knew they rewrote the zoom behavior but I did not think to check versions!Tollgate
No worries :). By the way, I've pretty much shipped it now, but for future reference (in case anyone stumbles across this page and wants to know how I did it or can suggest improvements): I had a bit of trouble calculating the translate function so I ended up banging in some arbitrary constants. It seems to work well enough: function trans(xy,constant){ return zm.translate()[xy]+(constant*(zm.scale())); } zm.translate([trans(0,-10),trans(1,-350)]);Adda
L
-1

I don't like to reinvent the wheel. I was searching for scatter plots which allow zooming. Highcharts is one of them, but there's plotly, which is based on D3 and not only allows zooming, but you can also have line datasets too on the scatter plot, which I desire with some of my datasets, and that's hard to find with other plot libraries. I'd give it a try:

https://plot.ly/javascript/line-and-scatter/

https://github.com/plotly/plotly.js

Using such nice library can save you a lot of time and pain.

Lysias answered 17/2, 2016 at 9:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.