The code below is from the last/bottom example of Mike Bostock's D3.js path tutorial http://bost.ocks.org/mike/path/. It creates a live graph of a user's page scrolling activity. If you watch the code run, you'll notice that the graph is running continuously, with the line graph sliding from right to left whether or not there's been any scrolling activity. Question: What is it about the tick function below that makes it run continuously, and how could it be altered to stop and start upon a click event?
(function() {
var n = 243,
duration = 750,
now = new Date(Date.now() - duration),
count = 0,
data = d3.range(n).map(function() { return 0; });
var margin = {top: 6, right: 0, bottom: 20, left: 40},
width = 960 - margin.right,
height = 120 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([now - (n - 2) * duration, now - duration])
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var line = d3.svg.line()
.interpolate("basis")
.x(function(d, i) { return x(now - (n - 1 - i) * duration); })
.y(function(d, i) { return y(d); });
var svg = d3.select("body").append("p").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("margin-left", -margin.left + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var axis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line");
var transition = d3.select({}).transition()
.duration(750)
.ease("linear");
d3.select(window)
.on("scroll", function() { ++count; });
(function tick() {
transition = transition.each(function() {
// update the domains
now = new Date();
x.domain([now - (n - 2) * duration, now - duration]);
y.domain([0, d3.max(data)]);
// push the accumulated count onto the back, and reset the count
data.push(Math.min(30, count));
count = 0;
// redraw the line
svg.select(".line")
.attr("d", line)
.attr("transform", null);
// slide the x-axis left
axis.call(x.axis);
// slide the line left
path.transition()
.attr("transform", "translate(" + x(now - (n - 1) * duration) + ")");
// pop the old data point off the front
data.shift();
}).transition().each("start", tick);
})();
})()
first off, you are missing the part where transition is defined.
Its a var of some sort but is not defined in your snippet.
That is kinda important but its not needed to know why the function continues to run.
You need to first understand both forms of the jquery each function.
http://api.jquery.com/each/
http://api.jquery.com/jquery.each/
transition = transition.each(function() { ... }).transition().each("start", tick);
::note that ... is hiding a lot of code::
this is essentially one line of code.
it is saying for each child of the transition var, if any, run the "..." code for it. That is the first jquery each statement. Inside this function $(this) would equal the child obj of transition. The child object is never used however.
After all iterations are complete, then the transition() function is ran. (again not sure what that is)
Lastly, each is called again but with a different form. This time it is saying run the tick function with the string "start" as the obj. It means that inside that function $(this) = "start". Again the obj is never used.
Because the invoking object is never used, essentially .each is just calling the tick function.
Really, this is a very odd way of doing this. not sure why there is such heavy use of .each. My understanding is that .each is actually kind of slow compared to other means of iterating and invoking callbacks.
UPDATE--
to start/stop with a click I would introduce a var at the top of the script.
var running = true;
(set to false to start as not running)
then surround contents of the tick function with an if statement.
if(running)
{
transition = transition.each(function() { ... }).transition().each("start", tick);
}
then create one or two click handlers. (for one toggle or start and stop buttons)
There are many ways to accomplish this.
$(#[button ID]).click( function(){
if(running)
{
running = false;
}
else
{
running = true;
tick();
}
});
This is a basic plan of attack. There maybe an issue when clicking the button in rapid succession. That is for you to fix as needed.
I've had this same problem before...the transition is defined as:
var transition = d3.select({}).transition()
.duration(1000)
.ease("linear");
You need to run a function that changed the transition to zero:
transition = transition.transition(0).duration(0);
This essentially stops the transition from running completely.
Then to restart the transition:
transition = d3.select({}).transition()
.duration(shiftDuration)
.ease("linear");
tick();
Related
I am trying to build a bar graph that I can switch between the amount of data displayed based on a particular length of time. So far the code that I have is this,
var margin = {
top : 20,
right : 20,
bottom : 30,
left : 50
}, width = 960 - margin.left - margin.right, height = 500
- margin.top - margin.bottom;
var barGraph = function(json_data, type) {
if (type === 'month')
var barData = monthToArray(json_data);
else
var barData = dateToArray(json_data);
var y = d3.scale.linear().domain([ 0, Math.round(Math.max.apply(null,
Object.keys(barData).map(function(e) {
return barData[e]['Count']}))/100)*100 + 100]).range(
[ height, 0 ]);
var x = d3.scale.ordinal().rangeRoundBands([ 0, width ], .1)
.domain(d3.entries(barData).map(function(d) {
return barData[d.key].Date;
}));
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
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 + ")");
svg.append("g").attr("class", "x axis").attr("transform",
"translate(0," + height + ")").call(xAxis);
svg.append("g").attr("class", "y axis").call(yAxis).append(
"text").attr("transform", "rotate(-90)").attr("y", 6)
.attr("dy", ".71em").style("text-anchor", "end").text(
"Total Hits");
svg.selectAll(".barComplete").data(d3.entries(barData)).enter()
.append("rect").attr("class", "barComplete").attr("x",
function(d) {
return x(barData[d.key].Date)
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "orange");
var bar = svg.selectAll(".barHits").data(d3.entries(barData))
.enter().append("rect").attr("class", "barHits").attr(
"x", function(d) {
return x(barData[d.key].Date) + x.rangeBand() / 2
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "red");
};
This does a great job displaying my original data set, I have a button set up to switch between the data sets which are all drawn at the beginning of the page. all of the arrays exist but no matter how hard I try I keep appending a new graph to the div so after the button click I have two graphs, I have tried replacing all of the code in the div, using the enter() exit() remove() functions but when using these I get an error stating that there is no exit() function I got this by following a post that someone else had posted here and another one here but to no avail. Any ideas guys?
What you are most likely doing is drawing a new graph probably with a new div each time the button is clicked, one thing you can try doing is to hold on to the charts container element somewhere, and when the button is clicked, you simply clear it's children and re-draw the graphs.
In practice, I almost never chain .data() and .enter().
// This is the syntax I prefer:
var join = svg.selectAll('rect').data(data)
var enter = join.enter()
/* For reference: */
// Selects all rects currently on the page
// #returns: selection
svg.selectAll('rect')
// Same as above but overwrites data of existing elements
// #returns: update selection
svg.selectAll('rect').data(data)
// Same as above, but now you can add rectangles not on the page
// #returns: enter selection
svg.selectAll('rect').data(data).enter()
Also important:
# selection.enter()
The enter selection merges into the update selection when you append or insert.
Okay, I figured it out So I will first direct all of your attention to here Where I found my answer. The key was in the way I was drawing the svg element, instead of replacing it with the new graph I was just continually drawing a new one and appending it to the #chart div. I found the answer that I directed you guys to and added this line in the beginning of my existing barGraph function.
d3.select("#barChart").select("svg").remove();
and it works like a charm, switches the graphs back and forth just as I imagined it would, now I have a few more tasks for the bargraph itself and I can move on to another project.
Thank you all for all of your help and maybe I can return the favor one day!
I am download multiple json data files and then visualize that files using D3 data charts. When I load the page it takes lot of time 2-3 minutes to download and visualize, even more on mobile devices. and browser shows a dialog for unresponsiveness. Is there a way to improve load time and handle the load time gracefully?
Every file is of few hundred (100 - 500) KBs and there are 20 - 200 files
Here is a sample code for line chart only
drawLineChart it downloads the json file and extract the data from it, formatLineChartData formats that data to input d3 and finally lineChartConfig draws the chart. similarly there are functions for bar charts, pie charts, word clouds and maps.
var drawLineChart = function(lineChartData, suffix, el){
var n = lineChartData.length;
var dataArray = [];
var labels= '';
var allNull =true; // flag to check if every ajax does not return any data
for (var i=0; i<n; i++){
spark.loadCounterJSON(lineChartData[i].file + suffix,i,
function(res){
var data =res.values;
if(data){
if(lineChartData[res.counter].slug !== 'global'){
allNull = false;
}
var title = Object.keys(data.entities)[0];
graphValues = data[title];
if(graphValues!=''){
labels = data['properties'];
dataArray.push(
formatLineChartData(
graphValues,
lineChartData[res.counter].name,
true
)
);
}
}
if(res.counter === n && !allNull){ // all outer ajax done;
lineChartConfig(el,
dataArray,
false,
true,
''
,labels);
}
});
}
};
var formatLineChartData = function(graphValues, key, xDataType){
formatedData = [];
$.each(graphValues, function(i, v){
value = {};
if(xDataType !== undefined){
value['x'] = new Date(v.x);
}
else {value['x']=v.x;}
value['y']=parseInt(v.y)
formatedData.push(value);
});
return {values:formatedData,key:key};
}
var lineChartConfig = function (div, data, guideline, useDates, auxOptions,labels, width, height) {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
//var parseDate = d3.time.format("%d-%b-%y").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("right");
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
var svg = d3.select(div).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 + ")");
var dataResult;
for(var i=0;i<data.length;i++){
dataResult = data[i].values;
}
//console.log(dataResult);
dataResult.forEach(function(d) {
d.x = new Date(d.x);
d.y = d.y;
});
x.domain(d3.extent(dataResult, function(d) { return d.x; }));
y.domain(d3.extent(dataResult, function(d) { return d.y; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
svg.append("path")
.datum(dataResult)
.attr("class", "line")
.attr("d", line);
}
The first thing you want to do is figure out where the problem is. If you have profiling or performance tools you could try those, but the quick dirty way would probably just have your script echo when it finishes downloading a file, when it finishes creating a chart, etc with the current time. This should give you a rough idea of where your time is being spent.
To improve the download speed, you would need to either make the file smaller, not downloading files you don't need, or if the hold up is in the upload speed on your server rather than download speed at the client, improve your infrastructure.
To improve processing speed of the charts... you would need to optimize the code which if you are using a built API you might not have options for. But you definitely want to make sure you aren't making any redundant calls, and check the documentation for any optimization options. Your server side operations could also be improved by multithreading/multiprocessing if possible and you have the hardware to support it.
As for handling it gracefully, the general principle should be to use asynchronous operations as much as possible. For example, if you are loading multiple charts, start each as a progress bar that updates as the data downloads (etc), and then display a chart as soon as it's available. It won't make the process go any faster, but it will keep the page responsive and keep the user informed.
Here is some example code and a fiddle of it:
var w = 400;
var h = 400;
var r = 20;
var factor = 5;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w/2 + "," + h/2 + ")");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "black");
svg.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", r)
.style("fill", "red");
svg.selectAll("circle")
.transition()
.duration(2000)
.attr("transform", "scale(" + 1/factor +")")
.attr("r", r*factor);
http://jsfiddle.net/o1wzfas7/2/
In the example, I am scaling two circles down by a factor of 5 (which also scales their positions and thus moves them "closer" to each other) and simultaneously scaling up the circles' radii by a factor of 5. The idea is that they'll appear to move closer to each other without changing size (as if I was changing their "cx" and "cy" attributes instead), but for some reason the scale transition and radius transition seem to go at different rates, so you see the circles get larger and then settle back to the initial size.
Does anybody know how I would do this using scale and radius transitions, but having the two cancel each other out so that the circles don't appear to change in size?
First, to explain what's going on:
The problem is that the changes you are making cancel out multiplicatively, but transitions proceed in an additive way.
So for your simple example, where radius (r) starts at 20, the scale (s) starts out (implicitly) as 1 and you are transitioning by a factor of 5, the effective radius of the circle is r*s:
At the start of transition:
r =20
s =1
r*s =20
At the end of transition:
r =4
s =5
r*s =20
Now, the way you're thinking of it in your head is that the factor should transition from 1 to 5, but that's not what is going to happen. The default transition functions don't see your factor, they just see that radius is transitioning from 20 to 4, and scale is transitioning from 1 to 5.
Therefore, at the midpoint of the transition, each attribute will be at the midpoint (average) of its start and end values:
r = (20+4)/2 = 12
s = (1+5)/2 = 3
r*s = 36
In order to do what you want, you're going to have to create a custom tween, which directly transitions the factor, and then calculates the radius and scale from there:
svg.selectAll("circle")
.transition()
.duration(2000)
.tween("factor", function(d,i){
/* create an interpolator for your factor */
var f = d3.interpolateNumber(1,factor);
/* store the selected element in a variable for easy modification */
var c = d3.select(this);
/* return the function which will do the updates at each tick */
return function(t) {
var f_t = f(t);
c.attr("transform", "scale(" + 1/f_t + ")" );
c.attr("r", r*f_t );
};
});
Note that in your real application, you'll need to store the "start" value for your factor transition in a global variable or each data object, since it won't automatically be 1 when you transition to a different scaling factor.
var w = 400;
var h = 400;
var r = 20;
var factor = 5;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w/2 + "," + h/2 + ")");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "black");
svg.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", r)
.style("fill", "red");
svg.selectAll("circle")
.transition()
.duration(2000)
.tween("factor", function(d,i){
/* create an interpolator for your factor */
var f = d3.interpolateNumber(1,factor);
/* store the selected element in a variable for easy modification */
var c = d3.select(this);
/* return the function which will do the updates at each tick */
return function(t) {
var f_t = f(t);
c.attr("transform", "scale(" + 1/f_t + ")" );
c.attr("r", r*f_t );
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
This question already has answers here:
What is the (function() { } )() construct in JavaScript?
(28 answers)
Closed 9 years ago.
I got stuck in something and I know it might be silly!
I try to figure out what the parenthesis ")()" at the end of this code do?
jsFiddle Since if I remove them it does not show any thing. I need to add more function in this part of the code but because of the parenthesis I got the errors.
(function () {
var n = 143,
duration = 750,
now = new Date(Date.now() - duration),
count = 0,
data = d3.range(n).map(function () {
return 0;
});
var margin = {
top: 6,
right: 0,
bottom: 20,
left: 40
},
width = 560 - margin.right,
height = 120 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([now - (n - 2) * duration, now - duration])
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var line = d3.svg.line()
.interpolate("basis")
.x(function (d, i) {
return x(now - (n - 1 - i) * duration);
})
.y(function (d, i) {
return y(d);
});
var svg = d3.select("body").append("p").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("margin-left", -margin.left + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var axis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.data([data])
.attr("class", "line");
tick();
d3.select(window)
.on("scroll", function () {
++count;
});
function tick() {
// update the domains
now = new Date();
x.domain([now - (n - 2) * duration, now - duration]);
y.domain([0, d3.max(data)]);
// push the accumulated count onto the back, and reset the count
data.push(Math.random()*10);
count = 0;
// redraw the line
svg.select(".line")
.attr("d", line)
.attr("transform", null);
// slide the x-axis left
axis.transition()
.duration(duration)
.ease("linear")
.call(x.axis);
// slide the line left
path.transition()
.duration(duration)
.ease("linear")
.attr("transform", "translate(" + x(now - (n - 1) * duration) + ")")
.each("end", tick);
// pop the old data point off the front
data.shift();
}
})()
Thank you!!
Immediately-Invoked Function Expression (IIFE)
Good reading here: http://benalman.com/news/2010/11/immediately-invoked-function-expression/
You can of course add into it any functions but because of scoping, you can call these functions only in same or deeper scope.
e.g test() function: http://jsfiddle.net/ZaJZu/
You defined an anonymous functions. Usually a named function like:
function myfunc(){
//code
}
can be called:
myfunc();
Exactly this () parenthesis are doing.It called the anonymous function on completion. If you don't want these, then named your function and call it from where you need as give example above.
Updated fiddle without Parenthesis
The outer parentheses around the whole thing turn the function into a function expression (as opposed to a function declaration). There are other ways to do that, but the parentheses are the common convention. The () at the end of the function expression is what triggers the immediate function invocation.
This is not a self-invoked anonymous function as that would be a recursive function. The pattern is called an Immediately-Invoked Function Expression (IIFE). It’s commonly used in the module pattern, but it’s also used relatively often to assign the result of a small, inline function to a variable. Regular old invoked-later function expressions (without the () at the end) are also commonly passed as callbacks or assigned to variables, or used inline in object literals to define methods.
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:
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).
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:
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.
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.
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.
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.