Scatter plot d3.js - zoom / drag borders, zoom buttons, reset scaling, median calculation

I built a scatter plot of d3.js with the zoom / pan function. You can see everything here (click "Open in a new window" to see it all): http://bl.ocks.org/129f64bfa2b0d48d27c9

There are several functions that I could not understand that I would like a hand if someone could point me in the right direction:

  • I want to apply an X / Y scale / pan frame to an area so that you cannot drag it below a certain point (e.g., zero).
  • I also hit on creating +/- zoom buttons in the style of Google Maps without any success. Any ideas?

Much less important, there are also several areas where I figured out a solution, but it is very rude, so if you have a better solution, please tell me:

  • I added a 'reset zoom' button, but it just deletes the graph and generates a new one instead, rather than just scaling the objects. Ideally, this should be a reset zoom.
  • I wrote my own function to calculate the median of the X and Y data. However, I am sure there should be a better way to do this with d3.median, but I cannot 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; }; 

Below is a very simplified (i.e. old) version of JS. The full script can be found 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 greatly 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 I used is very special and basically uses non-standard constants to support positioning more or less in the right place. This is not ideal, and I am ready to accept offers for a more versatile sound technology. However, in this case, it works quite well.

+6
source share
2 answers

To start with the median function, an array and an additional accessor are simply taken. That way you can use it the same way you use max:

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

As with the rest, if you pull out your scale, you can improve it a bit. So, for example, instead of

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

to 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 directly set the zoom level and translation:

 function zoomIn() { zm.scale(zm.scale()*2); // probably need to compute a new translation also } function reset() { zm.scale(1); zm.translate([0,0]); } 

Limiting the pan range is a bit trickier. You can simply not update when the translation or scale is not at your discretion inside the zoom function (or set the scale to β€œtranslate” to what you need). 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 to scale to allow negative on the axis, but panning on you will not make you face some complex scenarios.

This may be dated, but check the Domain Restriction when zooming or panning in D3.js

Also note that the zoom behavior had functionality to limit panning and zooming by one point . But the code was deduced in a later update .

+11
source

I don't like reinventing the wheel. I was looking for scatter plots that allow me to scale. Highcharts is one of them, but built on the basis of D3, not only scaling, but you can also have linear datasets on the scatter chart, which I desire with some of my datasets, and it is difficult to find with other graphics libraries. I would try:

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

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

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

-1
source

All Articles