Pattern for ChartJS line chart race, achieving good performance - animation

I would like to use ChartJS to create a "line chart race", i.e. a simple line chart that updates when new data is added in to the data array. Specifically, I want the both sets of axes to update. Note this is different to the "progressive line" chart sample which holds the axes fixed and draws the line from left to right.
At the moment I created something with the code below. However, when the chart reaches > 200-300 data points it starts to slow down and by the time it reaches > 1000 data points it's very slow. Can anybody advise on the pattern / structure I should follow to achieve the desired effect?
myChart = new Chart(ctx, config)
setInterval(updateChart, 100)
function updateChart() {
if (i < labelsData.length) {
myChart.data.labels.push(labelsData[i])
myChart.data.datasets[0].data.push(conData[i])
i = i + 1
myChart.update()
} else {
}
}
This achieves the desired result but the performance is very poor

Related

Stackbar chart in dcjs split by dimension

I am trying to create a stack bar chart in dcjs. The dcjs stack bar examples are quite clear the huge difference from the barchart with that of stack is that the stack function. The stack function takes the same group as input and it can take third parameter as function which decides by which value it has to split. I rather want a dimension to be split the entire bar chart.
Lets say the following data point is something like this
data = [
{activity:"A1",time_taken:10,activity_group:"Master A"},
{activity:"A2",time_taken:20,activity_group:"Master B"},
{activity:"A1",time_taken:30,activity_group:"Master C"},
{activity:"A2",time_taken:15,activity_group:"Master D"}
]
I want to have activity group in x-axis split by its activity representing time taken on y-axis, like this:
How do I achieve this ?
Your fiddle is on dc.js version 1.7, which is more than five years old and not something I can wrap my head around. :-/
I converted it to dc.js version 2, which also uses D3 v3.
dc.js is not great at showing the raw data, it's more about showing aggregated data. But in this case it could make sense to create a stack for every activity_group; that way it will automatically be assigned its own color.
Using ES6 we can get a list of all activity_group like this:
const stacks = [...new Set(data.map(row => row.activity_group))];
Now let's aggregate the data by stack:
var groupActivity = dimByActivity.group().reduce(
function reduceAdd(p, v) {
p[v.activity_group] += v.time_taken;
return p;
},
function reduceRemove(p, v) {
p[v.activity_group] -= v.time_taken;
return p;
},
function reduceInitial() {
return Object.fromEntries(stacks.map(stack => [stack,0]));
});
This is substantially the same as the stacked bar example except that we have a stack per activity_group.
Note that we are creating all the stacks in every bin, just leaving them zero where they don't exist. This is because dc.js expects the same stacks for every X value - it won't work otherwise.
As in the stacked bar example, we'll programmatically add the stacks to the chart, keeping in mind that we need to use .group() for the first stack:
function sel_stack(valueKey) {
return function(d) {
return d.value[valueKey];
};
}
// ...
stacks.forEach(function(stack, i) {
if(i===0)
chanUtil.group(groupActivity, stack, sel_stack(stack));
else
chanUtil.stack(groupActivity, stack, sel_stack(stack));
})
Here's the output. I messed a little with the margins and height in order to get the legend not to overlap and there are probably smarter ways to deal with this:
Fork of your fiddle.
As I said, this is making dc.js do something it doesn't want to do, so YMMV!

dc.js - avoid data points animation when adding data to scatter plot

I'm trying to implement a live data visualization (i.e. with new data arriving periodically) using dc.js. The problem I'm having is the following - when new data is added to the plot, already existing points often start to "dance around", even though they were not changed. Can this be avoided?
The following fiddle illustrates this.
My guess is that crossfilter sorts data internally, which results in points moving on the chart for data items that changed their position (index) in the internal storage. Data is added in the following way:
var data = [];
var ndx = crossfilter(data)
setInterval(function() {
var value = ndx.size() + 1;
if (value > 50) {
return;
}
var newElement = {
x: myRandom(),
y: myRandom()
};
ndx.add([newElement]);
dc.redrawAll();
}, 1000);
Any ideas?
I stand by my comments above. dc.js should be fixed by binding the data using a key function, and probably the best way to deal with the problem is just to disable transitions on the scatterplot using .transitionDuration(0)
However, I was curious if it was possible to work around the current problems by keeping the group in a set order using a fake group. And it is indeed, at least for this example where there is no aggregation and we just want to display the original data points.
First, we add a third field, index, to the data. This has to order the data in the same order in which it comes in. As noted in the discussion above, the scatter plot is currently binding data by its index, so we need to keep the points in a set order; nothing should be inserted.
var newElement = {
index: value,
x: myRandom(),
y: myRandom()
};
Next, we have to preserve this index through the binning and aggregation. We could keep it either in the key or in the value, but keeping it in the key seems more fitting:
xyiDimension = ndx.dimension(function(d) {
return [+d.x, +d.y, d.index];
}),
xyiGroup = xyiDimension.group();
The original reduction didn't make sense to me, so I dropped it. We'll just use the default behavior, which counts the number of rows which fall into each bin. The counts should be 1 if included, or 0 if filtered out. Including the index in the key also ensures uniqueness, which the original keys were not guaranteed to have.
Now we can create a fake group that keeps everything sorted by index:
var xyiGroupSorted = {
all: function() {
var ret = xyiGroup.all().slice().sort((a,b) => a.key[2] - b.key[2]);
return ret;
}
}
This will fetch the original data whenever it's requested by the chart, create a copy of the array (because the original is owned by crossfilter), and sort it to return it to the correct order.
And voila, we have a scatter plot that behaves the way it should, even though the data has gone through crossfilter.
Fork of your fiddle: https://jsfiddle.net/gordonwoodhull/mj81m42v/13/
[After all this, maybe we shouldn't have given the data to crossfilter in the first place! We could have just created a fake group which exposes the original data. But maybe there's some use to this technique. At least it proves that there's almost always a way to work around any problems in dc.js & crossfilter.]

dc.js Composite Graph - Plot New Line for Each Person

Good Evening Everyone,
I'm trying to take the data from a database full of hour reports (name, timestamp, hours worked, etc.) and create a plot using dc.js to visualize the data. I would like the timestamp to be on the x-axis, the sum of hours for the particular timestamp on the y-axis, and a new bar graph for each unique name all on the same chart.
It appears based on my objectives that using crossfilter.js the timestamp should be my 'dimension' and then the sum of hours should be my 'group'.
Question 1, how would I then use the dimension and group to further split the data based on the person's name and then create a bar graph to add to my composite graph? I would like for the crossfilter.js functionality to remain intact so that if I add a date range tool or some other user controllable filter, everything updates accordingly.
Question 2, my timestamps are in MySQL datetime format: YYYY-mm-dd HH:MM:SS so how would I go about dropping precision? For instance, if I want to combine all entries from the same day into one entry (day precision) or combine all entries in one month into a single entry (month precision).
Thanks in advance!
---- Added on 2017/01/28 16:06
To further clarify, I'm referencing the Crossfilter & DC APIs alongside the DC NASDAQ and Composite examples. The Composite example has shown me how to place multiple line/bar charts on a single graph. On the composite chart I've created, each of the bar charts I've added a dimension based off of the timestamps in the data-set. Now I'm trying to figure out how to define the groups for each. I want each bar chart to represent the total time worked per timestamp.
For example, I have five people in my database, so I want there to be five bar charts within the single composite chart. Today all five submitted reports saying they worked 8 hours, so now all five bar charts should show a mark at 01/28/2017 on the x-axis and 8 hours on the y-axis.
var parseDate = d3.time.format('%Y-%m-%d %H:%M:%S').parse;
data.forEach(function(d) {
d.timestamp = parseDate(d.timestamp);
});
var ndx = crossfilter(data);
var writtenDimension = ndx.dimension(function(d) {
return d.timestamp;
});
var hoursSumGroup = writtenDimension.group().reduceSum(function(d) {
return d.time_total;
});
var minDate = parseDate('2017-01-01 00:00:00');
var maxDate = parseDate('2017-01-31 23:59:59');
var mybarChart = dc.compositeChart("#my_chart");
mybarChart
.width(window.innerWidth)
.height(480)
.x(d3.time.scale().domain([minDate,maxDate]))
.brushOn(false)
.clipPadding(10)
.yAxisLabel("This is the Y Axis!")
.compose([
dc.barChart(mybarChart)
.dimension(writtenDimension)
.colors('red')
.group(hoursSumGroup, "Top Line")
]);
So based on what I have right now and the example I've provided, in the compose section I should have 5 charts because there are 5 people (obviously this needs to be dynamic in the end) and each of those charts should only show the timestamp: total_time data for that person.
At this point I don't know how to further breakup the group hoursSumGroup based on each person and this is where my Question #1 comes in and I need help figuring out.
Question #2 above is that I want to make sure that the code is both dynamic (more people can be handled without code change), when minDate and maxDate are later tied to user input fields, the charts update automatically (I assume through adjusting the dimension variable in some way), and if I add a names filter that if I unselect names that the chart will update by removing the data for that person.
A Question #3 that I'm now realizing I'll want to figure out is how to get the person's name to show up in the pointer tooltip (the title) along with timestamp and total_time values.
There are a number of ways to go about this, but I think the easiest thing to do is to create a custom reduction which reduces each person into a sub-bin.
First off, addressing question #2, you'll want to set up your dimension based on the time interval you're interested in. For instance, if you're looking at days:
var writtenDimension = ndx.dimension(function(d) {
return d3.time.hour(d.timestamp);
});
chart.xUnits(d3.time.hours);
This will cause each timestamp to be rounded down to the nearest hour, and tell the chart to calculate the bar width accordingly.
Next, here's a custom reduction (from the FAQ) which will create an object for each reduced value, with values for each person's name:
var hoursSumGroup = writtenDimension.group().reduce(
function(p, v) { // add
p[v.name] = (p[v.name] || 0) + d.time_total;
return p;
},
function(p, v) { // remove
p[v.name] -= d.time_total;
return p;
},
function() { // init
return {};
});
I did not go with the series example I mentioned in the comments, because I think composite keys can be difficult to deal with. That's another option, and I'll expand my answer if that's necessary.
Next, we can feed the composite line charts with value accessors that can fetch the value by name.
Assume we have an array names.
compositeChart.shareTitle(false);
compositeChart.compose(
names.map(function(name) {
return dc.lineChart(compositeChart)
.dimension(writtenDimension)
.colors('red')
.group(hoursSumGroup)
.valueAccessor(function(kv) {
return kv.value[name];
})
.title(function(kv) {
return name + ' ' + kv.key + ': ' + kv.value;
});
}));
Again, it wouldn't make sense to use bar charts here, because they would obscure each other.
If you filter a name elsewhere, it will cause the line for the name to drop to zero. Having the line disappear entirely would probably not be so simple.
The above shareTitle(false) ensures that the child charts will draw their own titles; the title functions just add the current name to those titles (which would usually just be key:value).

Get only non-filtered data from dc.js chart (dimension / group)

So this is a question regarding a rather specific problem. As I know from Gordon, main contributor of dc.js, there is no support for elasticY(true) function for logarithmic scales.
So, after knowing this, I tried to implement my own solution, by building a workaround, inside dc.js's renderlet event. This event is always triggered by a click of the user onto the barchart. What I wanted to do is this:
let groupSize = this.getGroupSize(fakeGroup, this.yValue);
let maximum = group.top(1)[0].value;
let minimum = group.top(groupSize)[groupSize-1].value;
console.log(minimum, maximum);
chart.y(d3.scale.log().domain([minimum, maximum])
.range(this.height, 0)
.nice()
.clamp(true));
I thought, that at this point the "fakeGroup" (which is just group.top(50)) contains only the data points that are NOT filtered out after the user clicked somewhere. However, this group always contains all data points that are in the top 50 and doesn't change on filter events.
What I really wanted is get all data points that are NOT filtered out, to get a new maximum and minimum for the yScale and rescale the yAxis accordingly by calling chart.y(...) again.
Is there any way to get only data rows that are still in the chart and not filtered out. I also tried using remove_empty_bins(group) but didn't have any luck with that. Somewhere is always all() or top() missing, even after giving remove_empty_bins both functions.
This is how i solved it:
I made a function called rescale(), which looks like this:
rescale(chart, group, fakeGroup) {
let groupSize = this.getGroupSize(fakeGroup, this.yValue);
let minTop = group.top(groupSize)[groupSize-1].value;
let minimum = minTop > 0 ? minTop : 0.0001;
let maximum = group.top(1)[0].value;
chart.y(d3.scale.log().domain([minimum, maximum])
.range(this.height, 0)
.nice()
.clamp(true));}
I think the parameters are pretty self-explanatory, I just get my chart, the whole group as set by dimension.group.reduceSum and a fake group I created, which contains the top 50 elements, to reduce bar count of my chart.
The rescale() method is called in the event listener
chart.on('preRedraw', (chart) => {
this.rescale(chart, group, fakeGroup);
}
So what I do is re-defining (re-setting min and max values regarding filtered data) the charts yAxis everytime the chart gets redrawn, which happens to also be every time one of my charts is filtered. So now, the scale always fits the filtered data the chart contains after filtering another chart.

Simple way to add raw data to dc.js composite chart via Ajax

I have a composite chart of 2 line charts however I need to add a third chart to it.
This third chart will have these unique properties:
The data will come in via an ajax call and be available as a two dimensional array [[timestamp,value],[timestamp,value]...]
Every new ajax call needs to replace the values of the previous one
It does not need to respect any of the filters and will not be used on any other charts
It will however need to use a differently scaled Y axis.. (and labeled so on the right)
This is how the chart currently looks with only two of the charts
This is my code with the start of a third line graph... Assuming I have the array of new data available i'm at a little loss of the best/simplest way to handle this.
timeChart
.width(width).height(width*.333)
.dimension(dim)
.renderHorizontalGridLines(true)
.x(d3.time.scale().domain([minDate,maxDate]))
.xUnits(d3.time.months)
.elasticY(true)
.brushOn(true)
.legend(dc.legend().x(60).y(10).itemHeight(13).gap(5))
.yAxisLabel(displayName)
.compose([
dc.lineChart(timeChart)
.colors(['blue'])
.group(metric, "actual" + displayName)
.valueAccessor (d) -> d.value.avg
.interpolate('basis-open')
.dimension(dim),
dc.lineChart(timeChart)
.colors(['red'])
.group(metric, "Normal " + displayName)
.valueAccessor (d) -> d.value.avg_avg
.interpolate('basis-open'),
dc.lineChart(timeChart)
.colors(['#666'])
.y()#This needs to be scaled and labeled on the right side of the chart
.group() #I just want to feed a simple array of values into here
])
Also side note: I've noticed what I might be a small bug with the legend rendering. As you can see in the legend both have the same label but i've used different strings in the second .group() argument.
I believe you are asking a few questions here. I will try to answer the main question: how do you add data to a dc chart.
I created an example here: http://jsfiddle.net/djmartin_umich/qBr7y/
In this example I simply add random data to the crossfilter, though this could easily be adapted to pull data from the server:
function AddData(){
var q = Math.floor(Math.random() * 6) + 1;
currDate = currDate.add('month', 1);
cf.add( [{date: currDate.clone().toDate(), quantity: q}]);
$("#log").append(q + ", ");
}
I call this method once a second. Once it completes, I reset the x domain and redraw the chart.
window.setInterval(function(){
AddData();
lineChart.x(d3.time.scale().domain([startDate, currDate]));
dc.redrawAll();
}, 1000);
I recommend trying to get this working in isolation before trying to add the complexity of multiple y-axis scales.
Currently your best bet is to create a fake group. Really the .data method on the charts is supposed to do this, but it doesn't work for charts that derive from the stack mixin.
https://github.com/dc-js/dc.js/wiki/FAQ#filter-the-data-before-its-charted

Resources