Note that there are some explanatory texts on larger screens.

plurals
  1. POd3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median
    primarykey
    data
    text
    <p>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): <a href="http://bl.ocks.org/129f64bfa2b0d48d27c9" rel="nofollow">http://bl.ocks.org/129f64bfa2b0d48d27c9</a></p> <p>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:</p> <ol> <li>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).</li> <li>I've also made a stab at creating Google Maps style +/- zoom buttons, without any success. Any ideas?</li> </ol> <p>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:</p> <ol> <li>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.</li> <li><p>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.</p> <pre><code>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; }; </code></pre></li> </ol> <p>A very simplified (i.e. old) version of the JS is below. You can find the full script at <a href="https://gist.github.com/richardwestenra/129f64bfa2b0d48d27c9#file-main-js" rel="nofollow">https://gist.github.com/richardwestenra/129f64bfa2b0d48d27c9#file-main-js</a></p> <pre><code>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 &gt; 30) { return colours[0]; } else if (val &gt; 10) { return colours[1]; } else if (val &gt; 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)+")"; }); }; } }); </code></pre> <p>Any help would be massively appreciated. Thanks!</p> <p>Edit: Here is a summary of the fixes I used, based on Superboggly's suggestions below:</p> <pre><code> // Zoom in/out buttons: d3.select('#zoomIn').on('click',function(){ d3.event.preventDefault(); if (zm.scale()&lt; 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()&gt; 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] &lt; 0 &amp;&amp; x.domain()[0] &lt; 0) { zm.translate([0, height * (1 - zm.scale())]); } else if(y.domain()[0] &lt; 0) { zm.translate([d3.event.translate[0], height * (1 - zm.scale())]); } else if(x.domain()[0] &lt; 0) { zm.translate([0, d3.event.translate[1]]); } ... }; </code></pre> <p>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.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload