javascript function undefined error [duplicate] - javascript

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.

Related

D3js v3 to v5 upgrade: unable to convert strings to integers

I had a D3js code which produces bar graphs and works fine with version 3.x. I wanted to upgrade the code to version 5 in the interest of being updated. Upon doing so I was able to correct a number of syntax updates such as scaleLinear, scaleBand, etc. The data is imported via tsv. The code is able to show the graph on the page with the correct x axis widths for the bars. However, the yAxis bars go out of bounds and the scale on the y-axis is very short. For example, the data shows the maximum value of the data to be 30000, but the yaxis is only from 0-90. Upon further investigation the d.RFU values from which the y data is generated seems to be not converted from string to integers. In the v3 code, I had a function at the end which converted the type of d.RFU to integer using the unary operator
d.RFU = +d.RFU
However, it seems to be not working in v5. Could this be due to the promises implementation in replacement of the asynchronous code?
Any solutions on how to fix this in version 5?
Please let me know if you need any more information and forgive me if I have missed out anything as I am new to programming and this website. Any help is appreciated.
Here is parts of the code which I have right now:
//set dimensions of SVG and margins
var margin = { top: 30, right: 100, bottom: 50, left: 100, },
width = divWidth - margin.left - margin.right,
height = 250 - margin.top - margin.bottom,
x = d3.scaleBand()
.range([0, width - 20], 0.1),
y = d3.scaleLinear()
.range([height,0]);
//setup the axis
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var svg = d3.select("#bargraphID")
.append("svg")
.attr("width", width + margin.left + margin.right - 100)
.attr("height", height + margin.top + margin.bottom - 10)
.append("g")
.attr("transform", "translate (" + margin.left + "," + margin.top + ")");
d3.tsv(filename).then(function(data) {
// get x values from the document id
x.domain(data.map(function(d) {
return d.ID;
}));
yMax = d3.max(data, function(d) {
return d.RFU;
});
// get the y values from the document RFU tab
y.domain([0, yMax]);
//create the x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate (0, " + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("dx", "0em")
.attr("dy", "-0.55em")
.attr("y", 30)
.attr("class", "x-axisticks");
//create the y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
//add the data as bars
var bar = svg.selectAll("bar")
.data(data)
.enter()
.append("rect")
.style("fill", barColor)
.attr("fill-opacity", "0.3")
.attr("x", function(d) {
return x(d.ID);
})
.attr("width", x.bandwidth())
//set initial coords for bars for animation.
.attr("y", height)
.attr("height", 0)
//animate bars to final heights
.transition()
.duration(700)
.attr("y", function(d) {
return y(d.RFU);
})
.attr("height", function(d) {
return height - y(d.RFU);
})
.attr("fill-opacity", "1.0")
.attr("class", "y-data");
});
//convert RFU to integers
function type(d) {
d.RFU = +d.RFU;
return d;
}
Just like with the old v3 and v4 versions, you have to pass the row conversion function to d3.tsv in D3 v5:
d3.tsv(filename, type)
Then, use the promise with then. Have in mind that d3.tsv always return strings (be it D3 v3, v4 or v5), because:
If a row conversion function is not specified, field values are strings.
Here is the demo with fake data:
var tsv = URL.createObjectURL(new Blob([
`name RFU
foo 12
bar 42
baz 17`
]));
d3.tsv(tsv, type).then(function(data) {
console.log(data)
})
function type(d) {
d.RFU = +d.RFU;
return d;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: Since SO snippet may have a problem loading that blob in some browsers, here is the same code in JSFiddle, check the console: https://jsfiddle.net/kv0ea0x2/

"NaN" when zooming and selecting by id

I'm probably doing something wrong but the following fiddle is displaying some really strange behavior:
https://jsfiddle.net/pkerpedjiev/42w01t3e/8/
Before I explain it, here's the code:
function skiAreaElevationsPlot() {
var width = 550;
var height = 400;
var margin = {
'top': 30,
'left': 30,
'bottom': 30,
'right': 40
};
function chart(selection) {
selection.each(function(data) {
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg").append("g");
svg.attr('width', width)
.attr('height', height);
var zoom = d3.behavior.zoom()
.on("zoom", draw);
data = Object.keys(data).map(function(key) {
return data[key];
}).sort(function(a, b) {
return b.max_elev - a.max_elev;
});
svg.insert("rect", "g")
.attr("class", "pane")
.attr("width", width)
.attr("height", height)
.attr('pointer-events', 'all')
.call(zoom);
var yScale = d3.scale.linear()
.domain([0, d3.max(data.map(function(d) {
return d.max_elev;
}))])
.range([height - margin.top - margin.bottom, 0]);
var xScale = d3.scale.linear()
.domain([0, data.length])
.range([0, width - margin.left - margin.right]);
var widthScale = d3.scale.linear()
.domain(d3.extent(data.map(function(d) {
return d.area;
})))
.range([10, 30]);
zoom.x(xScale).scaleExtent([1, data.length / 30]);
var gMain = gEnter.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
gMain.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
function skiAreaMouseover(d) {
gMain.select('#n-' + d.uid)
.attr('visibility', 'visible');
}
function skiAreaMouseout(d) {
gMain.select('#n-' + d.uid)
.attr('visibility', 'visible');
}
// the rectangle showing each rect
gMain.selectAll('.resort-rect')
.data(data)
.enter()
.append('rect')
.classed('resort-rect', true)
.attr("clip-path", "url(#clip)")
.attr('id', function(d) {
return 'n-' + d.uid;
})
.on('mouseover', skiAreaMouseover)
.on('mouseout', skiAreaMouseout);
var gYAxis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (width - margin.right) + "," + margin.top + ")");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("right")
.tickSize(-(width - margin.left - margin.right))
.tickPadding(6);
gYAxis.call(yAxis);
draw();
function draw() {
function scaledX(d, i) {
console.log('xd', d);
return xScale(i);
}
function rectWidth(d, i) {
return widthScale(d.area);
}
gMain.selectAll('.resort-rect')
.attr('x', scaledX)
.attr('y', function(d) {
console.log('d', d);
return yScale(d.max_elev);
})
.attr('width', 20)
.attr('height', function(d) {
console.log('d:', d)
return yScale(d.min_elev) - yScale(d.max_elev);
})
.classed('resort-rect', true);
}
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
return chart;
}
var elevationsPlot = skiAreaElevationsPlot()
.width(550)
.height(300);
data = [{
"min_elev": 46,
"max_elev": 54,
"uid": "9809641c-ab03-4dec-8d51-d387c7e4f114",
"num_lifts": 1,
"area": "0.00"
}, {
"min_elev": 1354,
"max_elev": 1475,
"uid": "93eb6ade-8d78-4923-9806-c8522578843f",
"num_lifts": 1,
"area": "0.00"
}, {
"min_elev": 2067,
"max_elev": 2067,
"uid": "214fdca9-ae62-473b-b463-0ba3c5755476",
"num_lifts": 1,
"area": "0.00"
}];
d3.select('#ski-area-elevations')
.datum(data)
.call(elevationsPlot)
So, when the page is first loaded, a rectangle will be visible in the middle. If you try scrolling on the graph, the console.log statements in the draw function will produce output. Notice that the xd: and d: statements all consist of just one object from the data set.
Now, if you mouseover the rectangle and try zooming again (using the scroll wheel). A bunch of NaN errors will be displayed. Now some of the d: and xd: statements will now print lists of objects.
Why is this happening? The underlying bound data never changed.
What puzzles me is that if these statements:
gMain.select('#n-' + d.uid)
Are changed to:
gMain.selectAll('#n-' + d.uid)
The fiddle behaves properly. Why does this make a difference? Is this a bug, or am I missing something?
For googleability, here's the error I get:
Error: Invalid value for <rect> attribute y="NaN"
The simple solution is to replace gMain.select/gMain.selectAll in the mouse event routines with d3.select(this)
The complicated solution seems to be that a single select binds a parents data to whatever is selected if you're acting on an existing selection. gMain is an existing selection and has the 3 data values as an array bound to it - console.log (gMain.datum()) to see - so when you do a gMain.select("#oneoftherects") you replace the single object in #oneoftherects with that array, thus knackering the x,y,width,height etc routines that expect one object. (Using d3.select doesn't do the same as d3 isn't a selection)
http://bost.ocks.org/mike/selection/#non-grouping

Library for creating scatter plot charts with quadrants

I have to create scatter plot with quadrants. I have looked at libraries like d3.js , high charts, nvd3 but I found only normal scatter charts.
Can someone suggest which js library will help me achieve this?
Thanks
D3.js allows this feature if you simply add negative values for the coordinates in the scatterplot. Just off the top of my hat, you could give the points their regular coordinates, just set the domains of your d3.scale function as to allow negative values. Just an example would be
var x = d3.scale.linear().range([0, width]).domain([d3.min(data), d3.max(data)]);
This sets the range of your scatterplot to the width you have selected, but allows all values to be accepted into the plot, regardless of them being positive or negative. As is very well explained here, scales fit to the size of the range, spreading the contents of the domain over the range. It is, however, not a requirement that said domain is completely positive.
If you check for the biggest absolute number in your data, you can format the domain likewise, therefore having the axes in the center of your plot, instead of them being misaligned.
Next, just add your axes like normal, only shift them to the middle of your canvas using x and y attributes.
Here is the upated Plunker link for creating scatter plot charts with quadrants through d3.js:-
http://plnkr.co/edit/yEfkN0tn7DPAypAvyWjD?p=preview
Code:
<script>
var svg = d3.select("#scatter"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = +svg.attr("width"),
height = +svg.attr("height"),
domainwidth = width - margin.left - margin.right,
domainheight = height - margin.top - margin.bottom;
var x = d3.scaleLinear()
.domain(padExtent([1,5]))
.range(padExtent([0, domainwidth]));
var y = d3.scaleLinear()
.domain(padExtent([1,5]))
.range(padExtent([domainheight, 0]));
var g = svg.append("g")
.attr("transform", "translate(" + margin.top + "," + margin.top + ")");
g.append("rect")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.attr("fill", "#F6F6F6");
d3.json("data.json", function(error, data) {
if (error) throw error;
data.forEach(function(d) {
d.consequence = +d.consequence;
d.value = +d.value;
});
g.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 7)
.attr("cx", function(d) { return x(d.consequence); })
.attr("cy", function(d) { return y(d.value); })
.style("fill", function(d) {
if (d.value >= 3 && d.consequence <= 3) {return "#60B19C"} // Top Left
else if (d.value >= 3 && d.consequence >= 3) {return "#8EC9DC"} // Top Right
else if (d.value <= 3 && d.consequence >= 3) {return "#D06B47"} // Bottom Left
else { return "#A72D73" } //Bottom Right
});
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + y.range()[0] / 2 + ")")
.call(d3.axisBottom(x).ticks(5));
g.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x.range()[1] / 2 + ", 0)")
.call(d3.axisLeft(y).ticks(5));
});
function padExtent(e, p) {
if (p === undefined) p = 1;
return ([e[0] - p, e[1] + p]);
}
</script>

function that repeats indefinitely

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();

Drawing multiple real time lines

I want to draw multiple real time lines using JSON files. I am basically retrieving the JSON file from a website, getting the time data (duration in seconds), converting them into minutes and pushing them into the data array. This code checks the JSON file for every second.
I want to add as many line as possible. For example, I want to add the average of the elements in data array (average duration) and plot it on the same plane. I tried to add another "line" and "path" variable, however I wasn't able to plot it at the same time.
The data array is an empty array with 44 elements in the beginning, and everytime the code checks the JSON file it replaces those zeroes with the retrieved duration data.
Here is my code to draw only one line.
function graph() {
var n = 43,
duration = 1000,
now = new Date(Date.now() - duration),
count = 0,
data = d3.range(n).map(function() { return 0; });
var margin = {top: 10, right: 20, bottom: 30, left: 60},
width = 1200 - margin.left-margin.right,
height = 460 - 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 line2 = 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( "+margin.left+"," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
var yaxis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(y.axis = d3.svg.axis().scale(y).orient("left"));
d3.select(".y.axis")
.append("text")
.text("Travel Time (min)")
.attr("text-anchor", "middle")
.attr("transform","rotate( -90, 200, 0)")
.attr("y",-250);
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(" + margin.left + ",0)")
.append("path")
.data([data])
.attr("class", "line");
tick();
function tick() {
d3.json("route.json",function(barzo){
var tempdata = barzo.route;
var len = tempdata.realTime;
var lastdata = parseInt(len)/60; //this is the time variable I use.
// update the domains
now = new Date();
x.domain([now - (n - 2) * duration, now - duration]);
y.domain([0, d3.max(data)+5]);
// push the time into the data
data.push(count);
count = lastdata;
// 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);
yaxis.transition()
.duration(duration/10)
.ease("linear")
.call(y.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();
});
}
};
First, I included another another data array (data2) to push the new data points for the new path:
var n = 43,
duration = 1000,
now = new Date(Date.now() - duration),
count = 0,
data = d3.range(n).map(function() { return 0; });
data2 = d3.range(n).map(function() { return 0; });
Then, I defined another path for the line that uses the points of data2 array.
var path2 = svg.append("g")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(" + margin.left + ",0)")
.append("path")
.data([data2])
.attr("class", "line2")
In the tick function, I needed to select both of those lines to update them (You can write a function to do the same thing for these steps instead of repeating the same code twice).
// redraw the line
svg.select(".line")
.attr("d", line)
.attr("transform", null);
svg.select(".line2")
.attr("d", line2)
.attr("transform", null);
The same thing for transition and data shift as well
// slide the line left
path.transition()
.duration(duration)
.ease("linear")
.attr("transform", "translate(" + x(now - (n - 1) * duration) + ")");
path2.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();
data2.shift();

Categories