NVD3 line chart with realtime data - d3.js

I have a really simple line chart written using NVD3.js. I've written a simple redraw based on timer, pulled from examples I've seen, but I get the error
Uncaught TypeError: Cannot read property 'y' of undefined
The JS is
var data = [{
"key": "Long",
"values": getData()
}];
var chart;
nv.addGraph(function () {
chart = nv.models.cumulativeLineChart()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] / 100 })
.color(d3.scale.category10().range());
chart.xAxis
.tickFormat(function (d) {
return d3.time.format('%x')(new Date(d))
});
chart.yAxis
.tickFormat(d3.format(',.1%'));
d3.select('#chart svg')
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function redraw() {
d3.select('#chart svg')
.datum(data)
.transition().duration(500)
.call(chart);
}
function getData() {
var arr = [];
var theDate = new Date(2012, 01, 01, 0, 0, 0, 0);
for (var x = 0; x < 30; x++) {
arr.push([theDate.getTime(), Math.random() * 10]);
theDate.setDate(theDate.getDate() + 1);
}
return arr;
}
setInterval(function () {
var long = data[0].values;
var next = new Date(long[long.length - 1][0]);
next.setMonth(next.getMonth() + 1)
long.shift();
long.push([next.getTime(), Math.random() * 100]);
redraw();
}, 1500);

Second Answer (after comment)
I looked at source for cumulativeLineChart. You can see the display.y property get created during chart creation. It relies on a private method: "indexify". If some derivative of that method was made public, then perhaps you could do something like chart.reindexify() before redrawing.
As a temporary workaround, you could recreate the chart from scratch on every update. If you remove the transition, that seems to work okay. Example jsfiddle: http://jsfiddle.net/kaliatech/PGyKF/.
First Answer
I believe there is bug in cumulativeLineChart. It appears that the cumulativeLineChart adds a "display.y" property dynamically to data values in the series. However, it does not regenerate this property when new values are added to the series for a redraw. I don't know of anyway to make it do this, although I'm new to NVD3.
Do you really need a CumulativeLineChart, or would a normal line chart be sufficient? If so, I had to make the following changes to your code:
Change from cumulativeLineChart to lineChart
Change from using 2 dimension arrays of data, to using objects of data (with x,y properties)
(I'm not familiar enough with NVD3 to say what data formats is expects. The 2D array obviously works for initial loads, but I think it fails to work for subsequent redraws. This is likely related to the same issue you are having with cumulativeLineChart. I thought changing to objects would fix cumulativeLineChart as well, but it didn't seem to.)
I also changed the following, although not as important:
Modified your getData function to create a new instance of Date to avoid unexpected consequences of sharing a reference as the date gets incremented.
Modified the update interval function to generate new data in increments of days (not months) with y values in the same range as the getData function.
Here's a working jsfiddle with those changes:
http://jsfiddle.net/kaliatech/4TMMD/

I found what I think is a better solution. The problem occurs because the cumulative chart sets the y function during processing. Whenever your want to refresh the chart, first set it back to a default which returns the correct original y. In your redraw function do this before updating:
chart.y(function (d) { return d.y; });
Even better would be if the cumulative chart could do this for itself (store the original access function before setting the new one, and put it back before re-indexing). If I get a chance, I'll try to push a fix.

I ran into the same issue. I changed the y() function on the lines from
.y(function(d) { return d.display.y })
to
.y(function(d) { return d.display ? d.display.y : d.y })
This gets rid of the error. Obviously it won't be displaying the (non-existent) indexed value in the error case, but in my experience, the chart gets updated again with display defined, and it looks correct.

Related

d3 v5 plotting simple line chart and mapping/casting to numbers using promise

I have finally decided to saddle up and adopt d3 v5 syntax after years of using v3. After looking at some tutorials and examples, v5 syntax really struck me as sublime. The readability is far improved and it seems easier to integrate multiple data sources.
To my dismay, and despite my reverence of it, I couldn't quite build a visual from scratch with the new Promise syntax. Here is my simple graph: (note I'm using hard coded data for the sake of this post, and I have commented out the .csv() call that I'd actually use. It should still be functionally the same)
var margins = {top:50, right:50, bottom:50, left:50};
var width = window.innerWidth - margins.left - margins.right;
var height = window.innerHeight - margins.top - margins.bottom;
var sampleData = [
{'y':32, 'x':1},
{'y':20, 'x':2},
{'y':19, 'x':3},
{'y':12, 'x':4},
{'y':15, 'x':5},
{'y':19, 'x':6},
{'y':22, 'x':7},
{'y':26, 'x':8},
{'y':31, 'x':9},
{'y':36, 'x':10}
];
//var dataset = d3.csv("my-data.csv").then(function(data)
// {return data;
// });
var dataset = sampleData.then(function(data)
{return data;
});
var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');
var myLine = dataset.then(function(data) {
Promise.all(data.map(function(d) {return {X:+d.x, Y:+d.y}}))//ensure numeric parsing
var xScale = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.X; }))
.range([0,width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.Y; }))
.range([height,0]);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
var line = d3.line()
.x(function(d) {return xScale(d.x); })
.y(function(d) {return yScale(d.y); });
var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');
var graphGroup = svg.append('g')
.attr('transform',"translate("+margins.left+","+margins.top+")");
graphGroup.append('path')
.attr('d', function(d) {return line(data); });
graphGroup.append('g')
.attr('class', 'axis x')
.attr('transform', "translate(0,"+height+")")
.call(xAxis);
graphgroup.append('g')
.attr('class', 'axis y')
.call(yAxis);
});
I get this error in the console:
Uncaught TypeError: sampleData.then is not a function
Question
I take the point that Promise.all() and .then() are not always favorable for really simple data visuals, but I'd still like to know why I can't make the above script output a minimal line graph. From then, hopefully, I can slowly take the training wheels off and find my stride with v5.
I'm particularly confused with how to cast to numbers using the unary + with Promise.
Although there are many twists and turns when it comes to using Promises, it turns out that the actual changes required to port code to make use of the d3-fetch module in favor of the deprecated d3-request module are strikingly minimal. Loosely speaking, to adapt your—or any pre-v5—code to use the new d3-fetch module you just move the callback from one method to another. Thus, the former
d3.dsv(url, callback);
now becomes
d3.dsv(url).then(callback);
The only thing to be aware of is to check if the callback's signature matches the one expected for .then. This only becomes relevant, though, if your callback used two parameters to handle errors:
function callback(error, data) {
// Handle error
if (error) throw error;
// Manipulate data
}
With Promises this is split into two separated methods:
function onFullfilled(data) {
// Manipulate data
}
function onRejected(error) {
// Handle error
}
These callback can be used in two ways:
// 1.
d3.dsv(url).then(onFullfilled, onRejected);
// 2.
d3.dsv(url).then(onFullfilled).catch(onRejected);
Another important point is that you cannot return data from your callback (beware of the infamous "How do I return the response from an asynchronous call?"!). d3.dsv now returns a Promise, not your data; you have to handle the data inside your callback. If you become more skilled using Promises you might have a look into the await operator, though, which allows you to wait for a Promise and its fulfilled value. Although this is ECMAScript 2017 (ES8) syntax it has already seen wide-spread browser support.
That being the general case, now for your code: sampleData is an Array object which, of course, does not have a .then() method and, hence, the error. To make the code work there is not much to do apart from uncommenting the lines featuring d3.dsv and putting the relevant code handling data inside the callback.
If you really want to do an offline simulation with hardcoded data you can use Promise.resolve() which will return a Promise already resolved with the given value. In your case instead of
d3.csv("my-data.csv")
.then(function(data) { });
you can use
Promise.resolve(sampleDate)
.then(function(data) { }); // Same handler as above

D3 Transitionning data with sequences sunburst

Introducing
I'm using Sequences Sunburst of d3.js for visualization of data.
I want to add a transition between two datasets (triggered by a button). I would like each arc to animate to display the new data.
Something like this: (1)Sunburst_Exemple, but without changing the accessor.
Research
In the (1)Sunburst_Example, the value accessor is modified. But I want to change the data, not the function that defines how to reach the data.
So, I searched a way for redefining data into path.
I was inspired by (2)Sunburst_Exemple, using each() method to store old values and attrTween() for transitions. But nothing is changing and I have the following error message:
Maximum call stack size exceeded . Maybe caused by the fact I have a method and I'm not in a global scope.
(2) link : _http://ninjapixel.io/StackOverflow/doughnutTransition.html
Then I have tried (3)Zoomable_Sunburst example, but nothing it happens in my case... .
(3) link : _http://bl.ocks.org/mbostock/4348373
My Example
Here is my example : JSFIDDLE
Problem is :
colors are lost
transition is not happening
I think I don't understand how transitioning is really working, I could have missed something that could help me in this case.
Any help ?
-
Listener of button call click method that redefined nodes and path.
/*CHANGING DATA*/
function click(d){
d3.select("#container").selectAll("path").remove();
var nodes = partition.nodes(d)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
}) ;
var path = vis.data([d]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.name]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.each(stash)
.transition()
.duration(750)
.attrTween("d", arcTween);
;
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
_
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
Data Characteristics :
the root node is the same for the two datasets.
the structure is globally the same, only the values are changing.
Some fields disappear but we can add them with value equal to 0.

nvd3 prevent repeated values on y axis

My chart repeats the values on the y axis. Is there a way I can only have 1,2,3 but nothing between (e.g the problem I have is shown on the chart below on the y axis):
The code im using is something like this:
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.xAxis
.tickFormat(function(d) { return d3.time.format('%x')(new Date(d)) });
chart.yAxis
.tickFormat(d3.format(',.1f'));
d3.select('#errorsFiredByDate')
.datum([{values: output, key: "Count by date"}])
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
You have a few options to control the ticks in a D3 axis. You can specify the number of ticks explicitly using .ticks(), but this is more of a hint to the layout that may be disregarded. If you want to be absolutely sure about the ticks, use .tickValues() to set the tick values explicitly.
In your case, this would look something like this.
chart.yAxis.tickValues(d3.range(chart.yAxis.scale().domain()[0], chart.yAxis.scale().domain()[1]);
Other methods for figuring out the range may be more suitable, depending on your concrete application.
I was facing the same problem and I did this in chart options to fix
yAxis: {
tickFormat: function(d){
if ( (d * 10) % 10 === 0 ) {
return d3.format('.0f')(d);
}
else {
return '';
}
}
}
A late reply , but might be useful to others. I use a workaround to eliminate duplicate ticks, by maintaining an array of already applied ticks & d3's multi format specifier.
uniqueTicks = [];
xTickFormat = d3.time.format.multi([
["%Y", function (d) {
if (uniqueTicks.indexOf(d.getFullYear()) === -1) {
uniqueTicks.push(d.getFullYear());
return true;
} else {
return false;
}
}],
["", function () {
return true;
}]
]);
d3.svg.axis().tickFormat(xTickFormat );
This trick can be tweaked for any other type of tick format.

how to transition a multiseries line chart to a new dataset

I could really use some guidance setting up a transition on my multiseries line chart. As an example of what I need, I've started with this great multiseries line chart: http://bl.ocks.org/mbostock/3884955. To that code, I've added an update() function that's called once using setInterval(). I've also created a new data set called data2.csv which is similar to data.tsv but has different values.
The update function should change the data that the line chart is displaying. Forget about making a nice smooth transition, I can't even get the data to update in the chart view. When I try using the update function, it looks like the new data is loaded properly into the javascript variables, but the lines on the chart don't change at all.
I've seen variations on this question asked a few times but haven't found an answer yet. Can anyone help me figure out how to transition this multi-series line chart to a new dataset (also multiseries)?
function update() {
d3.csv("data2.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));
// format the date
data.forEach(function(d) {
d.date = parseDate(d.date);
});
// rearrange the data, same as in the original example code
var cities2 = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {date: d.date, temperature: +d[name]};
})
};
});
// update the .city g's to the new dataset
var city2 = svg.selectAll(".city")
.data(cities2);
// redraw the lines with the new data???
city2.selectAll("path")
.attr("d", function(d) { return line(d.values); });
clearInterval(transitionInterval);
});
}
UPDATE: NikhilS's answer contains the key to the solution in the comment trail.
You should make sure you are following the enter + update process as outlined by Mike Bostock in his stuff on the General Update Pattern. It looks like you haven't invoked any kind of d3 transition. You also haven't specified an enter or exit for the update function, which will cause problems if you have new data coming in and/or old data going out. Try changing this:
var city2 = svg.selectAll(".city")
.data(cities2);
city2.selectAll("path")
.attr("d", function(d) { return line(d.values); });
to the following:
var city2 = svg.selectAll('.city')
.data(cities2);
var cityGroups = city2.enter().append('g')
.attr('class', 'city');
cityGroups.append('path')
.attr('class', 'line');
d3.transition().selectAll('.line')
.attr('d', function(d) { return line(d.values); });
city2.exit().remove();
I made a basic data re-join and update demo a while back, which you can view here.
use d3 Transition, you can make some sort of animation.
If you want to select a sub-interval of the data to plot the graph, no need manipulation on the data, just use a d3 brush and clip the graph
For a multi-series line graph with most of the line graph elements, you could refer to this example: http://mpf.vis.ywng.cloudbees.net/

nvd3.js chart ajax data redraw - missing hovereffect + former yAxis scale

I am using nvd3 to draw a simple line chart with data receiving via an ajax request. It is working perfectly with the first drawing request but not on redrawing. The chart redraws by calling the same drawing function but with different data + differen max/min values.
When redrawing the chart with new data the "hover circle" does not appear, whereas the tooltip does. Furthermore when clicking on the legend of the chart and force a redraw by that the hover appears again, but the values of the yAxis are changed to these of the first drawn chart.
So far I assume that when redrawing the chart still holds the old max/min values - but only concerning the "hover" effect. The general chart looks fine so far also on redraw - the problem just faces the hover and that's it.
Sounds pretty confusing, but hopefully you will get the point.
Some code:
d3.json(queryurl, function(data2){
nv.addGraph(function(jsonData) {
if(chart){
chart.remove();
}
chart = nv.models.lineChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.color(d3.scale.category10().range());
chart.xAxis
.tickFormat(function(d) {
return d3.time.format('%x')(new Date(d))
});
chart.yAxis
.scale()
.tickFormat(d3.format(''));
chart.lines.yDomain([maxmin.max,maxmin.min]);
d3.select('#chart1 #chartsvg')
.datum(data2)
.transition().duration(600)
.call(chart);
nv.utils.windowResize(chart.update);
});
});
return chart;}
Try using .empty() on the svg element before redrawing.
I've only just started with NVD3 and D3 myself, however am doing a similar thing. What worked for me is to separate the data update function with the chart creation function. Do note the caveat below though...
I have the following to create the graph:
initGraph = function(url) {
d3.json(url, function(data) {
nv.addGraph(function() {
chart = nv.models.multiBarChart();
d3.select('#chart svg').datum(data).transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
});
};
And the following function to update it:
redrawGraph = function(url) {
d3.json(url, function(data) {
d3.select('#chart svg').datum(data).transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
});
};
I don't know if this is the recommended solution as I'm still at the "hack until it works" stage. With this, all the functions of the chart work after invocation of redrawGraph() (including axes redraw and tooltips).
Caveat: this seems to occasionally result in miscalculated ticks on recalculation:

Resources