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.