I have a composite chart of three line charts. One of the charts is using a "fake" group as suggested here. As you can see in the snapshot below though the scale of the 3rd chart is very different from the other two. The solution I would like is to have a dual axis chart as shown here in pure d3. I think it can be done using a .renderlet() on the main composite chart but I was wondering if there was a "sexier" solution with pure dc.js?
Here is a snapshot:
and here is my code. (in coffeescript) I tried using .y and .yAxis on the internal charts but that had no effect.
actualValuesChart = dc.lineChart(mainChart)
.group(metric, "actual " + #displayName)
.valueAccessor (d) -> d.value.avg
.colors(['green'])
.interpolate('basis-open')
normValuesChart = dc.lineChart(mainChart)
.group(metric, "normal " + #displayName)
.valueAccessor (d) -> d.value.avg_avg
.colors(['rgba(0,0,255,1)'])
.interpolate('basis-open')
clipsCountChart = dc.lineChart(mainChart)
.group(buildFakeGroup(defaultClipsArray))
.colors(['red'])
.interpolate('basis-open')
# .y(d3.scale.linear().range([100, 0]))
# .yAxis(d3.svg.axis().scale(d3.scale.linear().range([100, 0])))
mainChart
.dimension(#dimension.monthStamp)
.width(thisChart.width + 30)
.height(thisChart.width*.333)
.yAxisLabel(#displayName)
.elasticY(true)
.x(d3.time.scale().domain([minDate,maxDate]))
.xUnits(d3.time.months)
.brushOn(true)
.legend(dc.legend().x(60).y(10).itemHeight(13).gap(5))
.renderHorizontalGridLines(true)
.compose([actualValuesChart,normValuesChart,clipsCountChart])
Yes! It's a pretty new feature and I haven't tried it out myself, but if you create some of your subcharts with useRightYAxis, and then set rightY for the second scale on the composite chart, you should get what you're looking for.
But this is totally bleeding edge and I see that the example is currently broken (web/examples/right-axis.html). So please follow up if you run into trouble by creating an issue or starting a discussion on the user group.
Related
I use dc.js for showing the results of multiple classification algorithms. More specifically, I want to show a precision recall chart (each point corresponds to a result of a classification system).
I already used a dc.js scatter chart for this which works fine.
Additionally I would like to have a d3 contour in the background of the chart which shows the F-measure.
This is already implemented. The only issue is that the contour part is in the foreground and not in the background of the chart.
Please have a look at the jsfiddle for a full example.
Two questions are still open for me because I'm not a dc.js or d3 expert:
Is there a way to put the contour in the background or the symbols(cycles) of the scatter chart in the foreground (I already tried it with the help of this stackoverflow question but with no success)
I used the 'g.brush' selector to get the area of the inner chart. This works fine as long as the brushing is turned on. Is the selector a good way to go or are there better alternatives (which may also work if brushing is switched off).
In my example I put the contour part in the upper left corner to see if it works but I also provide the code (currently uncommented) to increase the width and height of the contour to the correct size.
chart
.on('renderlet', function (chart) {
var innerChart = chart.select('g.brush');
var width = 300, height=300;
//getting the correct width, height
//var innerChartBoundingRect = innerChart.node().getBoundingClientRect();
//var width = innerChartBoundingRect.width, height=innerChartBoundingRect.height;
[contours, color] = generateFmeasureContours(width,height, 1);
innerChart
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
var symbols = chart.chartBodyG().selectAll('path.symbol');
symbols.moveToFront();
});
jsfiddle
Putting something in the background is a general purpose SVG skill.
SVG renders everything in the order it is declared, from back to front, so the key is to put your content syntactically before everything else in the chart.
I recommend encapsulating it in an svg <g> element, and to get the order right you can use d3-selection's insert method and the :first-child CSS selector instead of append:
.on('pretransition', function (chart) {
// add contour layer to back (beginning of svg) only if it doesn't exist
var contourLayer = chart.g().selectAll('g.contour-layer').data([0]);
contourLayer = contourLayer
.enter().insert('g', ':first-child')
.attr('class', 'contour-layer')
.attr('transform', 'translate(' + [chart.margins().left,chart.margins().top].join(',') + ')')
.merge(contourLayer);
A few more points on this implementation:
use dc's pretransition event because it happens immediately after rendering and redrawing (whereas renderlet waits for transitions to complete)
the pattern .data([0]).enter() adds the element only if it doesn't exist. (It binds a 1-element array; it doesn't matter what that element is.) This matters because the event handler will get called on every redraw and we don't want to keep adding layers.
we give our layer the distinct class name contour-layer so that we can identify it, and so the add-once pattern works
contourLayer = contourLayer.enter().insert(...)...merge(contourLayer) is another common D3 pattern to insert stuff and merge it back into the selection so that we treat insertion and modification the same later on. This would probably be simpler with the newer selection.join method but tbh I haven't tried that yet.
(I think there may also have been some improvements in ordering that might be easier than insert, but again, I'm going with what I know works.)
finally, we fetch the upper-left offset from the margin mixin
Next, we can retrieve the width and height of the actual chart body using
(sigh, undocumented) methods from dc.marginMixin:
var width = chart.effectiveWidth(), height = chart.effectiveHeight();
And we don't need to move dots to front or any of that; the rest of your code is as before except we use this new layer instead of drawing to the brushing layer:
contourLayer
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
Fork of your fiddle.
Again, if you'd like to collaborate on getting a contour example into dc.js, that would be awesome!
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
I am using NVD3.js multiChart to show multiple lines and bars in the chart. All is working fine, but the x-axis labels is aligned only to the line points, not bars. I want to correctly align labels directly below the bars as it should. But I get this:
With red lines I marked where the labels should be.
I made jsFiddle: http://jsfiddle.net/n2hfN/
Thanks!
As #Miichi mentioned, this is a bug in nvd3...
I'm surprised that they have a TODO to "figure out why the value appears to be shifted" because it's pretty obvious... The bars use an ordinal scale with .rangeBands() and the line uses a linear scale, and the two scales are never made to relate to one another, except in that they share the same endpoints.
One solution would be to take the ordinal scale from the bars, and simply adjust it by half of the bar width to make the line's x-scale. That would put the line points in the center of the bars. I imagine that something similar is done in the nv.models.linePlusBarChart that #LarsKotthoff mentioned.
Basically, your line's x-scale would look something like this:
var xScaleLine = function(d) {
var offset = xScaleBars.rangeBand() / 2;
return xScaleBars(d) + offset;
};
...where xScaleBars is the x-scale used for the bar portion of the chart.
By combing through the source code for nvd3, it seems that this scale is accessible as chart.bars1.scale().
Maybe someday the authors of nvd3 will decide that their kludge of a library deserves some documentation. For now, I can show you the kind of thing that would solve the problem, by making a custom chart, and showing how the two scales would relate.
First, I'll use your data, but separate the line and bar data into two arrays:
var barData = [
{"x":0,"y":6500},
{"x":1,"y":8600},
{"x":2,"y":17200},
{"x":3,"y":15597},
{"x":4,"y":8600},
{"x":5,"y":814}
];
var lineData = [
{"x":0,"y":2},
{"x":1,"y":2},
{"x":2,"y":4},
{"x":3,"y":6},
{"x":4,"y":2},
{"x":5,"y":5}
];
Then set up the scales for the bars. For the x-scale, I'll use an ordinal scale and rangeRoundBands with the default group spacing for nvd3's multiBar which is 0.1. For the y-scale I'll use a regular linear scale, using .nice() so that the scale doesn't end on an awkward value as it does by default in nvd3. Having some space above the largest value gives you some context, which is "nice" to have when trying to interpret a chart.
var xScaleBars = d3.scale.ordinal()
.domain(d3.range(barData.length))
.rangeRoundBands([0, w], 0.1);
var yScaleBars = d3.scale.linear()
.domain([0, d3.max(barData, function(d) {return d.y;})])
.range([h, 0])
.nice(10);
Now here's the important part. For the line's x-scale, don't make a separate scale, but just make it a function of the bars' x-scale:
var xScaleLine = function(d) {
var offset = xScaleBars.rangeBand() / 2;
return xScaleBars(d) + offset;
};
Here's the complete example as a JSBin. I've tried to document the major sections with comments so it's easy to follow the overall logic of it. If you can figure out from the nvd3 source code exactly what each of the elements of the multiChart are called and how to set the individual scales of the constituent parts, then you might be able to just plug in the new scale.
My feeling on it is that you need to have a pretty good handle on how d3 works to do anything useful with nvd3, and if you want to customize it, you're probably better off just rolling your own chart. That way you have complete knowledge and control of what the element classes and variable names of the parts of your chart are, and can do whatever you want with them. If nvd3 ever gets proper documentation, maybe this will become a simple fix. Good luck, and I hope this at least helps you get started.
When creating a barchart using dc.js and a smaller dataset, I can get the bars and gaps to look pretty consistent.
When using a larger dataset and d3.scale.linear(), I haven't been able to get the bars and gaps to look anywhere as nice as when using a Date chart and d3.time.scale().
The bars are either too thin or thick without a gap - http://neil-s.com/unison/crossfilter/test/Crossfilter.jpg
Here is some sample code for one of the top bar charts from my image above:
var tempDim = xFilter.dimension(function(d) {return d.temp;});
var tempCount = tempDim.group().reduceCount(function(d) {return d.temp;});
var minTemp = tempDim.bottom(1)[0].temp;
var maxTemp = tempDim.top(1)[0].temp;
tempBarChart
.width(375).height(157)
.dimension(tempDim)
.group(tempCount)
.x(d3.scale.linear().domain([minTemp, maxTemp]))
.centerBar(true)
.elasticX(true)
.gap(15)
.xUnits(function(){return 15;})
.xAxis().ticks(6)
I've experimented with the gap, xUnits, and ticks values, but no luck. Any suggestions?
Not pretty!
This is a known bug with dc.js.
https://github.com/dc-js/dc.js/issues/952
I think it works slightly better in 1.7 than in the 2.0 development branch, but it is still not perfect.
The only thing I can think of as a workaround for now is to create a renderlet which adjusts the widths after the fact. :-(
In this live code link,
http://nvd3.org/livecode/index.html#codemirrorNav
if you add chart.yRange([0, 300]) for inverting the y-axis, the x-axis moves up
and sticks to the top(near the legend).
Any possible fix?
PS: The problem is with most of the charts on that page but 'Cumulative line chart' is closest to my use case.
The position of the x axis is hardcoded to y.range()[0] in the NVD3 source, but you can adjust this after the chart has been drawn. In your particular case, add the following code after .call(chart):
d3.select('.nv-x.nv-axis')
.attr('transform', 'translate(0,' + chart.yAxis.range()[1] + ')');