NVD3.js multiChart x-axis labels is aligned to multiple lines, but not multiple bars - d3.js

This question relates to NVD3.js multiChart x-axis labels is aligned to lines, but not bars
I am using NVD3.js multiChart to show multiple lines and multiple 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:
As you can see - x-axis (example, 2014-Feb) is not aligned to Bars.
1) How to align x-axis labels to bars and lines at the same time?
2) I need this solution for NVD3.js or how to properly integrate.
I made jsFiddle: http://jsfiddle.net/n2hfN/28/
Thanks!

The problem here is that nv.models.multiChart uses a linear scale for its x-axis, and then when it draws the bars it calls nv.models.multiBar, which uses an ordinal scale with .rangeBands().
You can follow this mess through the source code:
First lets look at multiChart.js
HERE is where it sets the x-scale to be a linear scale.
HERE it calls the nv.models.multiBar model to create the bars.
If we jump over to have a look at multiBar.js
HERE it creates an ordinal scale, and HERE it sets the range of the scale using .rangeBands()
The result is that the ordinal scale used for placing the bars, and the linear scale used for the chart's axis do not align. Here's what the two scales look like on their own if plotted on an axis:
The solution would be to force the chart to render the line graphs and the x-axis in terms of the ordinal scale used by the bars. This would work in your case because the bars and the lines all use the same data for the x-axis. This is very simple to do if you are making your own chart and not relying on nvd3, as I showed in my answer to your previous question HERE. This is extraordinarily complicated to do if you're trying to work within nvd3, and many others have tried and failed to switch out the default scales used by nvd3 charts. Have a look at this issue on the nvd3 github page that has been open since January, 2013 for example.
I've tried a number of approaches myself to reuse the bars' ordinal scale, but with little success. If you want to poke around and try to brute-force it yourself, I can tell you that from my experiments I came closest when using chart.bars1.xScale().copy() to make a copy of the bars' scale, and set its domain and rangeBands. Unfortunately, since the chart's width is computed at render time, and I can't seem to create a hook into the chart.update function, it is impossible to set the rangeBands' extent to the correct values.
In short, if you can't live with the labels being offset, you're probably going to need to code up your own chart without nvd3, or else find a different type of layout for your visualization.

After playing around with the NVD3 v1.7.1 source code with the immensely helpful guidance offered by jshanley's answer, I think I've managed to come up with an answer (perhaps more of a kludge than a good solution).
What I did was to have the x-axis labels align with the bars, and have the line data points align with the bars.
1.1. To align the x-axis label, I shifted the x-axis to the right so that the first label appears underneath the middle of the first bar. I then shifted the last label to the left, so that it appears underneath the middle of the last bar. See code here. The amount to shift by is computed at drawing time using .rangeBand() and saved in a rbcOffset variable (I had to modify multiBar.js for this to work).
1.2. To align the line data points with the bars, a similar shift is also required. Luckily, this part is easy because scatter.js (which is used by line chart) comes with a padData boolean variable that does what we want already. So basically, I just set padData to true and the lines shift to align with the bars, see here.
In order to properly integrate with NVD3 and make everything look good, some additional changes are required. I've forked NVD3 on GitHub so you can see the complete solution there. Of course, contributions are welcome.

I use last solution and it runs. So, you can specify
lines1.padData(true)
in order to align lines too.

Same here, I used the last solution,it worked for me as well. Find the following line in multiChart.js
if(dataLines1.length){
lines1.scatter.padData(true); // add this code to make the line in sync with the bar
d3.transition(lines1Wrap).call(lines1);
}

I encountered the same problem and fixed it with below code:
at lines 7832 and 7878 replace
.attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; })
with :
var w = (x.rangeBand() / (stacked && !data[j].nonStackable ? 1 : data.length));
var sectionWidth = availableWidth/(bars.enter()[0].length - 1);
if(bars.enter().length == 2)
return 'translate(' + ((i-1)*w + i*w + (i*(sectionWidth - 2*w))) + ',0)';
else
return 'translate(' + ((i-0.5)*w + i*(sectionWidth - w)) + ',0)';
The first case handles multiple bars case while the second one handles single bar case.

lawry's solution works. Also if using interactive guidelines, you need to shift the interactive line to match the new scale. Modify:
if(useInteractiveGuideline){
interactiveLayer
.width(availableWidth)
.height(availableHeight)
.margin({left:margin.left, top:margin.top})
.svgContainer(container)
.xScale(x);
wrap.select(".nv-interactive").call(interactiveLayer);
//ADD THIS LINE
wrap.select(".nv-interactiveGuideLine")
.attr('transform', 'translate(' + rbcOffset +', ' + 0 + ')' +
'scale(' + ((availableWidth - rbcOffset*2)/availableWidth) + ', 1)');
}
in multiChart.js.

Related

Adding specific ticks on D3 axis w/ brush

I need help adding specific ticks to an axis that is also being focused with a brush. I found a similar question/answer for a static axis (without a brush) here:
Adding a specific tick to a D3.js axis
It's along the lines of what I'm trying to do. My chart is more similar to this example though :
http://blockbuilder.org/pergamonster/419aef09c21ffe8dd1dac971016468d6
Say you wanted to have a 'July 4' tick on both the full axis and the focus axis at all times. I've tried the axis.ticks().push(...arrayOfNumbers) from the above question/answer in my code, but nothing happens. If I do axis.tickValues(arrayOfNumbers) it puts ONLY those numbers as ticks, but I would like to have the auto scaling number PLUS my specific numbers on the axis for the focus area.
Any help would be appreciated.
P.S. Though my chart is similar the about blockBuilder example, I'm using a linearScale not a timeScale if that matters. Thanks
draw a second x-axis where you only specify the extra ticks.
svg.append('g')
.attr('class', 'x-axis-extra')
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickValues([2.78,3.14]));
You need a bit of CSS to hide the extra path
.x-axis-extra path { opacity: 0; }

D3.js: Mouse over between grid lines

I am creating a line chart with grid lines. Here's the similar example: http://bl.ocks.org/hunzy/11110940
I need to change the background of vertical space between X(2) to X(4), X(8) to X(10) and X(12) to X(14) on mouse hover. But I am not able to understand how to use D3 to reference those spaces between the grid lines.
There is nothing there to click on/hover over. The normal axis/grid creates lines, not rectangles. You would need to change the default behave of the axis objects to create "invisible, but clickable" rectangles in order to be able to attach a mouse event to those spaces.
I don't know if this is the recommended approach but it seems like it could work. After the axis has been created:
something
.attr('class','xaxis')
.call(xAxis)
You could select the ticks with something like this:
d3.select(svg).select('.xaxis g.tick').each(function(){
// this.transform will be "translate(X,Y)"
})
In the function you can query the existing properties of the g elements and extract the transform attribute which will contain the X and Y offset for the "tick". This can be used to determine one dimension of your rectangle objects. The other dimension is determined by the size of the other axis or something like that.

d3 center bar chart's x-axis on arbitrary value

I have a bar chart that is wider than the svg element but, with panning, you're able to drag left and right. The x-axis is time based (I use d3.time.scale()).
After building the chart, I'd like to be able to pan to a specific point on the x-axis. For example, the user may have already panned to a certain point and shut down their session - I'd like to put them back where they were when they return.
I've been looking at doing something like:
d3.selectAll('rect')
.attr('transform', 'translate(' + savedXaxisLocation + ',0)';
Is that the right idea? I'm assuming I also need to do that to the x axis itself?
As you can tell I'm feeling my way around this - I'd be happy to include any other code or screenshots if y'all feel it relevant.
In general, you would have a top-level g element that contains everything else and would translate that. This saves you from having to translate all the elements individually. When you do that, you may want to apply a clipPath to hide the elements that have been panned out of view.
This example should help you to get a better idea of what you can do and how.

transitioning multiple elements with d3

I'm having a heck of a time with transitions in D3js. I posted a fiddle here: dynamic area graph. The problem that I run into is that when trying to follow this tutorial path transitions, I run into sync problems with the xAxis. Bostock indicates that the domain should be skewed slightly so that the data appears to "shift" in from the side. However, when I do that, the data reflected will be listed under a tick mark that is "2 minutes" behind the actual time it should be listed. If I just update the data as-is, without doing the tricky stuff with the clip-path, it works fine. All of the data is in sync. Just for reference, the xAxis is an integer, linear scale. Dealing with date strings was madding, even though d3 has great time manipulation, I just find dealing with epoch easier. If someone could check out the fiddle and let me know how to transition the entire drawing...I want it to be smooth like in the examples that bostock has.
Since SO requires some code, here's the data structure that I'm generating. The rest is in the fiddle:
setInterval(function(){
lastTime = lastTime.add('m',1);
var data = {"apikey":"FOO",
"description":"a dumb description",
"beg_effective_dt_tm":lastTime,
"data":{
"sum":getRandomInt(385,4000),
}
};
tick(data);
},1000)
I think this is close to what you are after: http://jsfiddle.net/JJ7Rj/1/
It is close because the graph is delayed by one value.
Most of the architecture was already there, and your concern that you might lose the sync between the xAxis and the data was correct. Mike gets around it by changing his range of the scales. The other (and better, IMO) way is to do it by changing the domains. I made the following two primary changes.
The domain of the axis
I have modified the minMax function such that it does not include the latest and the last point in the domain of the xAxis. Note that this means that the most recent value (as well as the oldest value) is actually displayed outside the visible region. This is an unfortunate limitation of using the monotone interpolation, which Mike talks about at the bottom of his post.
function minMax(pd) {
return [
d3.min(pd.slice(1).slice(0, -1),function(d){ return d.beg_effective_dt_tm; }),
d3.max(pd.slice(1).slice(0, -1),function(d){ return d.beg_effective_dt_tm; })
];
}
If you want to have all the values visible, then you'll get the wiggling effect of the discontinuous tangent suddenly forming when the new value comes in. You can obtain that by removing the .slice(0, -1).
The initial transform
For each element, I have initially placed the DOM element one step to the right.
var step = x(newVal.beg_effective_dt_tm) - x(pd[pd.length - 1].beg_effective_dt_tm);
// ...
.attr("transform", 'translate(' + step + ')');
// ...
Then finally, I have transitioned everything back to their rightful place:
clipPath.selectAll("path,circle,.dp").transition()
.ease("linear")
.attr("transform", "translate(" + 0 + ",0)");
Also, I have enabled the transition for the xAxis.

d3 bar chart does not extend to full width of svg

I have a bar chart (http://tributary.io/inlet/4720197) that plots a dataset with a large number of points that I can't get to fill the entire width of the svg. I had the same issue with a line chart and was able to resolve by using rangePoints.
I've been attempting something similar with this chart but can't quite get the right combination (it's using two scales for the grouping) and am unclear how to get the side-by-side bars that I previously accomplished with rangeBand() given that function does not exist with rangePoints. What am I missing?

Resources