dc.js - dynamically change valueAccessor of a stacked layer in a lineChart and redraw it - d3.js

I am trying to realize a dashboard to display basic data.
I am actually completely stuck on an issue. Strangely enough, I couldn't find anything even similar to it online, so I don't have many leads on how to move forward.
I have mainly two charts:
a lineChart called "stackChart" that
displays consumption as a base layer with its valueAccessor function
dispalys production as a stacked layer with its value Accessor function
a barChart called "volumeChart" that is simply the rangeChart for the lineChart
I use radio buttons to select whether to aggregate the grouped data by sum or by average (using the same approach as this example) and then I just use:
stackChart.valueAccessor(/*function with new value (avg or sum)*/);
dc.redrawAll();
to refresh the base layer (consumption).
What I don't manage to do is to refresh the "stacked layer" by updating its valueAccessor! I can't find any way to access its valueAccessor (or, worst case, just completely remove the stacked layer and then add a new refreshed stacked layer using just ".stack(...)").
Here is the respective part of my code where the chart is built:
// Charts customization #js
stackChart
.renderArea(true)
.height(350)
.transitionDuration(1500)
.dimension(dateDim)
.group(powByTime, "Consumption")
// BASE LAYER valueAccessor HERE
.valueAccessor(function(d) { return d.value.conSum; })
.x(d3.time.scale().domain([minDate, maxDate]))
.xUnits(d3.time.days)
.elasticY(true)
.renderHorizontalGridLines(true)
.legend(dc.legend().x(80).y(0).itemHeight(13).gap(5))
.brushOn(false)
// STACKED LAYER HERE
.stack(powByTime, "Production", function(d) { return d.value.prodSum; })
.rangeChart(volumeChart)
.controlsUseVisibility(true)
;
And here is where I look for changes in the radio buttons and re-draw the layers:
// Listen for changes
d3.selectAll('#select-operation input')
.on('click', function() {
var aggrMode = this.value; // fetch "avg" or "sum" from buttons
// UPDATE BASE LAYER HERE:
stackChart.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; });
// ???HOW TO UPDATE STACKED LAYER valueAccessor function???
//stackChart.stack.valueAccessor(function(d) { var sel = accessors[aggrMode]['prodPow']; return d.value[sel]; });
dc.redrawAll();
});
If you need more details on what I am trying to do and full code you can check here.
As a reference, here is what it looks like:

I don't really know dc.js, but it may be possible that you can't change an accessor once it's been set. Try writing a single function for your accessor that will return either the sum or the average, depending on the state of some variable that you can set.

#Ryan's solution will probably work fine (and may be a better design), but here's the lowdown on the dc.js API with respect to stacking, in case you need it.
As described in this issue the group and stack API is pretty weird. It grew organically, in a backward-compatible way, so both the stacks and the value accessors on top of the stacks sort of branch out in a beautiful fractal of... well, no it's pretty messy.
But the issue also suggests the solution for your problem. Since chart.group() resets the set of stacks, just go ahead and build them all from scratch in your event handler:
stackChart.group(powByTime, "Consumption") // this resets the stacks
.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; })
.stack(powByTime, "Production", function(d) { var sel = accessors[aggrMode]['prodPow']; return d.value[sel]; });
Internally it's just emptying an array of layers/stacks and then populating it with some references.
This is quite efficient since dc.js doesn't store your data except where it is bound to the DOM elements. So it is the same amount of work to redraw using the old group and value accessor as it is to redraw using new ones.

Related

Unable to reset the focus ordinal bar chart

I am trying to reset after choosing some of the individual's bar.
index.html: (line no. 62)
<span>
reset
</span>
This seems not to work. I was able to reset all the graphs pie chart, line chart, etc but not this one.
Those two ordinal graphs are created in index.js like this:
var focus = new dc.barChart('#focus');
var range = new dc.barChart('#range');
https://blockbuilder.org/ninjakx/483fd69328694c6b6125bb43b9f7f8a7
Update:
It looks weird now Coz it's showing a single bar and all the bar have got invisible but I want them to be visible (in gray colour) but not clickable.
This example replaces the built-in filtering functionality of the bar chart with its own implementation of ordinal selection, because the chart has a linear scale.
The example uses a global variable focusFilter to store the current selection. We need to empty this out and we also need to update the dimension filter as the original filterAll would do, pulling that code out of the click handler:
focus.applyFilter = function() { // non-standard method
if(focusFilter.length)
this.dimension().filterFunction(function(k) {
return focusFilter.includes(k);
});
else this.dimension().filter(null);
};
focus.filterAll = function() {
focusFilter = [];
this.applyFilter();
};
This will also allow dc.filterAll() to work, for a "reset all" link.
Fork of your block.
For some reason, I could not get the original
reset
links to work at all in this block, so I replaced them with the equivalent D3 click handlers:
d3.select('#reset-focus').on('click', () => {
focus.filterAll();
dc.redrawAll();
})
d3.select('#reset-all').on('click', () => {
dc.filterAll();
dc.redrawAll();
})
I also updated the focus ordinal bar example. Note that automatic hiding/showing of the reset link doesn't work because the chart still has an irrelevant range filter inside of it.

Unfiltered data on/off

I am using composite charts to see the unfiltered data, but I want to hide sometimes the unfiltered data and make the 'y' axis elastic. Hiding the unfiltered data wasn't hard, just an event listener on chart, but I can't make possible the elasticity on 'y' axis, when the unfiltered data is hidden. Perhaps it's not even possible in a case like this. Any ideas?
chart.select('.unfiltered_data').on('change', function() {
if(!this.checked) {
console.log("Stop showing unfiltered data!")
chart.select('.sub._0')
.attr('visibility', 'hidden')
// chart.elasticY(true)
chart.redraw()
}
else {
console.log("Show unfiltered data!")
chart.select('.sub._0')
.attr('visibility', 'visible')
// chart.elasticY(false)
chart.redraw()
}
})
There is (almost) always a way to do it in dc.js, because dc.js is a leaky abstraction by design!
First I tried to change which child charts are included in each composite chart, but that wasn't the right approach because a composite chart's children can't be changed on a redraw, only on a render. And we want to animate when switching between showing the unfiltered and not showing.
So instead I thought we could
use your visibility idea
turn off elasticY when the unfiltered is hidden, and
use the filtered child chart's domain instead
So I added a checkbox
<label><input type="checkbox" id="unfiltered" name="unfiltered" checked> Show Unfiltered</label>
and a global variable
var show_unfiltered = true;
The handler looks like this:
function ydomain_from_child1(chart) {
chart.y().domain([0, chart.children()[1].yAxisMax()]);
chart.resizing(true);
}
d3.select('#unfiltered').on('change', function() {
show_unfiltered = this.checked;
charts.forEach(chart => {
chart.select('.sub._0').attr('visibility', show_unfiltered ? 'visible' : 'hidden');
chart.elasticY(show_unfiltered);
if(!show_unfiltered) {
ydomain_from_child1(chart);
chart.children()[1].colors(d3.schemeCategory10);
chart.on('preRedraw.hide-unfiltered', ydomain_from_child1);
}
else {
chart.children()[1].colors('red');
chart.on('preRedraw.hide-unfiltered', null);
}
})
dc.redrawAll();
});
Whenever the checkbox is toggled, we turn on or off elasticY based on the setting. When the unfiltered are not shown, we'll simulate elasticY with a preRedraw handler which determines the domain from the second (filtered) child chart.
Additionally, we turn on/off the red color scheme for the filtered chart based on the checkbox.
I have added this to the compare unfiltered example.
I found I had to make one more change: the filtered chart was hidden when there were no filters. So I had to disable this hiding if the unfiltered was unchecked:
var any_filters = !show_unfiltered || charts.some(chart => chart.filters().length);
chart.select('.sub._1')
.attr('visibility', any_filters ? 'visible' : 'hidden')

dc.js Line Chart doesn't show Data

Data doesn't show in my line chart. It just shows the y-axis and x-axis.
My data arrays are like this:
time_list = ['00:00:00', '01:00:00', '02:00:00']
val_list = [0.7274859768018719, 0.6894762867153069, 0.6994151884676558]
I set up the chart like this:
<div id="#chart-cpu-performance"></div>
<script type="text/javascript">
var cpuperformance = dc.lineChart("#chart-cpu-performance");
var connection = new WebSocket(`ws://localhost:8001/websocket`);
function render_plots(dim, val) {
cpuperformance
.width(990)
.height(540)
.dimension(dim)
.group(val)
.x(d3.scaleBand().domain(['00:00:00', '04:00:00', '08:00:00','12:00:00',
'16:00:00', '20:00:00','23:00:00']).rangeRound([0,100]))
.y(d3.scaleLinear().domain([0,1]))
.curve(d3.curveLinear)
.renderArea(false)
.renderDataPoints(true);
cpuperformance.render();
}
connection.onmessage = function(event){
//get data and parse
var newData = JSON.parse(event.data);
var updateObject = []
newData.time_list.forEach(function additem(item, index) {
updateObject.push({time: item, avg_value: newData.val_list[index]})
});
var xfilter = crossfilter(updateObject)
var timeDim = xfilter.dimension(function(d) {return d.time;});
var avg_vPertime = timeDim.group().reduceSum(function(d) {return +d.avg_value;});
render_plots(timeDim, avg_vPertime)
}
</script>
Did I miss some parameters for cpuperformance in the render_plots function?
Yes, you missed xUnits. Also I suggest using .elasticX(true) rather than specifying the domain yourself.
Unfortunately most coordinate grid charts are not able to determine automatically what kind of data keys they are dealing with, the primary kinds being numeric, ordinal, and time.
So if your data is ordinal, you need to have
.xUnits(dc.units.ordinal)
A lot of the logic is different for charts with an ordinal X scale, and this parameter directs dc.js to use the ordinal logic. Other values tell bar charts how wide to make the bars for the numeric (dc.units.integers, dc.units.fp.precision) and time scales (d3.timeHours etc).
Also in this example, only one of the data points matches the domain you passed to scaleBand. So you'll only see one point.
It's easier to use
.elasticX(true)
.x(d3.scaleBand().rangeRound([0,100]))
and let the chart figure out what to put in the domain.
Fiddle with a working version of your code.

dc.js color pie chart slice color with color domain

I've got a pie chart with the following for coloring the slices.
.colorAccessor(function (d) {
return d.value.count;
})
.colors(colorbrewer.YlGn[9])
.colorDomain([0, grpXtents[1]])
I am calculating the grpXtents using d3.extent
This works fine. How do I recalculate the extents for my color domain when I click on filter on other charts in the group?
Thanks!
You may not need to calculate it yourself. Please try this:
chart.on('preRedraw', function() {
chart.calculateColorDomain();
});
https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#dc.colorMixin+calculateColorDomain
It should probably be an option on the colorMixin rather than requiring this hook.

How to remove NVD3 chart resize/update delay

I've created an NVD3 multiBarChart and placed it in a jQuery resizable container. When resizing the chart, each render incurs the same delay as when the chart is first drawn: staggered left-to-right delayed drawing of the bars. This looks cool when the chart is first drawn, but it's a nuisance when resizing the chart. I've experimented with nv.d3.css, reducing every delay to 0ms to no avail. Haven't yet inspected the NVD3 JS and am hoping not to need to.
Fiddle:
http://jsfiddle.net/a5Fnj/10/
var container = $("#mycontainer");
$(container[0]).resizable();
var svg = d3.select(container[0]).append("svg");
nv.addGraph(function () {
var chart = nv.models.multiBarChart();
chart.xAxis.tickFormat(d3.format(',f'));
chart.yAxis.tickFormat(d3.format(',.1f'));
d3.select(container[0]).select("svg")
.datum(exampleData())
.transition().duration(0).call(chart);
nv.utils.windowResize(chart.update);
this.stackedbar = chart;
});
function exampleData() {
return stream_layers(3, 10 + Math.random() * 100, .1).map(function (data, i) {
return {
key: 'Stream' + i,
values: data
};
});
}
As of NVD3 1.7.1 you can use the duration option:
chart.duration(0);
I used transitionDuration: -1 that worked for a stackedAreaChart.
Edit
This helped remove the transition when appending chart data, not the re-size issue, please check the comments below.
In the latest version (from github), you can set .transitionDuration():
chart.transitionDuration(0);
Edit: Even with this, some of the transitions/durations are hardcoded in the NVD3 source. The only way to get rid of those is to modify the source.

Resources