Minor tweaks for histogram generated using d3 - d3.js

Following is the stripped down version is what I'm using to generate histograms using d3 and a bit of jQuery.http://bl.ocks.org/4611158
While most of it might seem right, I'm still confused regarding
Why there is no '14' in the x-axis as should have been for the given input in the above example? Instead 13 gets the ordinate of what should have been 14's
In my trials d3.layout.histogram() assigned negative(and hence non-plot table) widths when I try altering the output range of scale to some non-zero value. why is it so? what is the possible workaround?
My main motive to use ordinal scale was to make ticks centrally aligned below the bars, unlike what Mike used in his demo for histograms. I've also made the number of bins equal to the number of ticks in d3.layout.histogram() for the very same purpose. I'm sure there might be a better way around to code what I'm looking for
Also any ideas how to add a 'graph' of indicator lines like its been done in nvd3 visualization (light gray in background )that will make it more pleasing?

There is no 14 and there are two 8s on the x-axis. This is because the bins function will diligently divide the range = 14 - 1 = 13 into 14 intervals as per the API reference:
The bins may be specified as a number, in which case the range of values will be split
uniformly into the given number of bins. Or, bins may be an array of threshold values,
defining the bins; the specified array must contain the rightmost (upper) value, thus
specifying n + 1 values for n bins. ...
Before solving this issue, I am guessing that the second problem you are facing is that if the rangeDefault has negative values, then some values are not plotted. To fix that problem, being unaware of the exact need of it, I will start by removing the following:
rangeDefault[0] = 0; //All histograms start from 0 <-- REMOVED
Then to fix the first problem, use the second form of arguments for binsas shown here:
var bins = [];
for(var ii = settings.range[0], jj = 0; ii <= settings.range[1] + 1; ii++, jj++)
bins[jj] = ii;
var data = d3.layout.histogram()
.bins(bins)(settings.data);
I hope this addresses the primary queries.
Adding the light grey indicator lines is fairly easy, as shown here. The changes were:
vis.css
.y.axis line.tick { opacity: .3; }
vis.js
Moving the axis before the chart in the DOM because of how SVG is laid out affects its z-index:
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("g").attr("class", "x axis");
gEnter.append("g").attr("class","y axis");
gEnter.append("g").attr("class", "bars");
And finally making the major tickSize on the y-axis -(width - margin.right - margin.left):
yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSubdivide(true)
.tickPadding(5)
.ticks(10)
.tickSize(-(width - margin.right - margin.left), 2, 8);

Related

dc.js heatmap - make the top row rects to begin at y="0"

I have a dc.js heatmap working:
But I want to add grid lines to it, like so:
You can see that the lines to not match up with the bottom edges of the rects. Inserting the lines themselves is easy, you just start at zero and add 11 lines based on the height of the rects, which in this case will always be 11 / chart.effectiveHeight().
The reason they do not match up, seems to be that the top rect row does not always start at 0, instead, there seems to be a random(?) y position that the chart starts at, this will change with the height of the chart container, eg this y position starts at 5:
If it was consistent, then I could just start appending lines from that number instead of 0, but it is not. I have tried a couple of hacky work arounds, however I am unsure as to how to get the y position of all the rects after they are available in the DOM.
Interestingly the demo heatmap does not have this issue:
Here is the code for the heatmap:
const heat_map = dc.heatMap('#heatmap');
heat_map
.width(0)
.height(0)
.margins(margins)
.dimension(hm_dim)
.group(hm_group)
.keyAccessor(function(d) { return +d.key[0]; })
.valueAccessor(function(d) { return +d.key[1]; })
.colorAccessor(function(d) { return +d.value; })
.colors(color_scale)
.calculateColorDomain()
.yBorderRadius(0)
.xBorderRadius(0)
heat_map.render();
Is there a way to force the rects to begin at 0? Or get the random y position for the top rows? I did have a look at the source code but got a bit lost. Also I thought about creating a false group that would include each rect in the grid, and the grid lines could then be rect borders, but I thought that was a bit heavy handed.
Outlining the cells using CSS
It's easy to outline the cells using CSS:
rect.heat-box {
stroke-width: 1;
stroke: black;
}
Example fiddle.
However, as you point out, this only works if all the cells have values; crossfilter will not create the empty ones and I agree it would be absurd fill them in using a fake group just for some lines.
So, to answer your original question...
Why is there a gap at the top of the chart?
The heatmap calculates an integer size for the cells, and there may be space left over (since the space doesn't divide perfectly).
It's kind of nasty but the heatmap example avoids having extra space by calculating the width and height for the chart using the count of cells in each dimension:
chart
.width(45 * 20 + 80)
.height(45 * 5 + 40)
The default margins are {top: 10, right: 50, bottom: 30, left: 30} so this allocates 45x45 pixels for each cell and adds on the margins to get the right chart size.
Since the heatmap in this example draws 20 columns by 5 rows, it will calculate the cell width and height as 45.
Alternative Answer for Responsive/Resizable Charts
I am revisiting this question after rewriting my heatmap chart to be responsive - using the "ResizeObserver" method outlined in the dc.js resizing examples and Gordon's answer to this question
While specifying the chart width and height for the heatmap in Gordon's answer still works, it does not combine well with the resizing method because resized charts will have their .width and .height set to 'null'. Which means that this rounding issue will reoccur and the heat boxes will be again be offset by a random integer x or y value of anywhere between 0 and 5 (unless you want to write a custom resizing function for heatmaps).
The alternative answer is relatively simple and can be determined by selecting just one heat-box element in the heatmap.
The vertical offset value for the heat boxes is the remainder value when the heat-box y attribute is divided by the heat-box height attribute.
const heatbox_y = heat_map.select('.heat-box').attr('y);
const heatbox_height = heat_map.select('.heat-box').attr('height')
const vertical_offset = heatbox_y % heatbox_height
The modulus % will return the remainder.
The horizontal offset can be determined in the same way.
Thus you can append lines to the chart at regular intervals determined by the heatbox_height + the vertical_offset values.
This will work if you pick any heat-box in the chart, and so it is suitable for instances like this where you cannot guarantee that there will be a heat-box at each x or y level. And it means that you are free to set your chart height and width to 'null' if needed.

dcjs dynamic zooming to fit range of values

I have a rowchart in DCjs that plots the top N values of a given parameter. However, for the unfiltered data these differ from each other by a very small number.
I've had to label each row with it's unique identifier, as my random generator produced two identical names, meaning that if I use name as the dimension, I end up with one of ten thousand data points having a value greater than 100%.
However, the main problem here is that the difference between each row is tiny, and can be around 0.0001.
However if I zoom in on that part of the x-axis using
var max = dim.top[1][0].value;
var min = dim.top(10)[9].value;
chart
.dimension(dim)
.group(group)
.x(d3.scaleLinear()
.range([-100, chart.width()])
.domain([min-(max-min)*0.1,max])
)
.cap(10)
.othersGrouper(null)
.colors(['#ff0000']);
Firstly I loose the ID label on the left. Secondly as I also have to use .elasticX(false) for the zooming to work, it means that when I add filters, the range of the x-axis doesn't change with the values e.g.
Is there a way to get dynamic zooming such that the range of the x-axis depends on the range of values presented?
elasticX is a really simple feature which does pretty much what your code does, although it locks the min or max to zero depending if the data is positive or negative:
var extent = d3.extent(_rowData, _chart.cappedValueAccessor);
if (extent[0] > 0) {
extent[0] = 0;
}
if (extent[1] < 0) {
extent[1] = 0;
}
(calculateAxisScale source)
This code gets called (indirectly) before each render and redraw.
Here's some general purpose advice for when elasticX or elasticY doesn't do exactly what you want. I've never seen it fail! (Which is saying something in such a quirky codebase as dc.js.)
First, disable elasticX. Then create a function which calculates the X domain and sets it:
function custom_elastic(chart) {
var max = chart.dimension().top[1][0].value;
var min = chart.dimension().top(10)[9].value;
chart.x().domain([min,max]);
}
I've parameterized it on the chart for generality.
Now we can have this function called on the preRender and preRedraw events. These events will pass the chart when they fire:
chart.on('preRender', custom_elastic)
.on('preRedraw', custom_elastic);
And that should do it!
BTW, you probably don't want to set the range of the scale - this is set automatically by the chart, and it's a little more complicated than you have it since it takes margins into account.
Debugging the min and max
Looking at your fiddle I realized that I hadn't given a second look to how you are calculating the min and max.
I also hadn't noticed that you had the range start at -100.
Good first step logging it; it reports
min: 0.81, max: 0.82
which is incorrect. The top ten are from 0.96 to 1.
The issue is that the dimension's key is the id, so the rows returned by .top() are in reverse alphabetical order (the "largest" strings).
Again you're on the right track with
console.log(Group.top(Infinity))
Yes! The group will give you the top 10 by value.
var top10 = thischart.group().top(10);
var max = top10[0].value;
var min = top10[9].value;
Nice!
fiddle
But wait, doesn't it look like the bars are stretching off the left side of the chart?
Hacking the row chart with a renderlet to draw bars starting at the left edge
Okay now it's clear that the row chart doesn't support this. Here is a hack to resize the bars to the left edge:
chart.on('renderlet', function(chart) {
var transform = chart.select('g.row rect').attr('transform');
var tx = +transform.split('(')[1].split(',')[0];
chart.selectAll('g.row rect')
.attr('transform', null)
.attr('width', function(d) {
return +d3.select(this).attr('width') + tx;
})
chart.selectAll('g.row text.row')
.attr('transform', null);
})
All the row rects are going to be offset by a large negative number, which we grab first in tx. Then we remove the transform from both the rects and the text, and add tx to the width of the row rects.
fiddle
Great! But where's the last bar? Well, we took the top ten values for the min and max, so the tenth bar is the minimum value.
You'll have to figure out what works for you, but I found that looking at the top 20 values left the top 10 at good sizes:
var N = 20;
var topN = thischart.group().top(N);
var max = topN[0].value;
var min = topN[N-1].value;
final fiddle
This hack does not play well with the built-in transitions, so I turned them off for this chart:
chart.transitionDuration(0)
It would be a lot more work to hack that part, and better to fix it in the chart itself.

Add mean value and n-number to nvd3 boxplot

I am using nvd3 boxplot for my charts. Is there any option to have mean as an asterisk (*) on the boxplot? Can we also have the n value above the top whisker similar to the image below.
This issue has been posted here.
Thanks in advance.
Edit
I would like to add a mean value which I calculate from the data points and not just the center of the box plot. The computed mean may not be in the center of the box plot due to outliers.
You can achieve this by doing the following algorithm:
Get all the rectangles
Find the middle point
Create a text and put it in the above calculated center
Code snippet:
function makeMarkOnMean(){
d3.selectAll(".mean").remove();//remove all * mean markers
//get all the rectangles
d3.selectAll(".nv-boxplot-box")[0].forEach(function(r){
window.setTimeout(function(){
var x = parseFloat(d3.select(r).attr("x")) + d3.select(r).attr("width")/2 - 3; //x position of the star
var y = parseFloat(d3.select(r).attr("y")) + parseFloat(d3.select(r).attr("height"))/2+12;//y position of the star
//now make the star on the above x and y
d3.select(r.parentNode).append("text").attr("class", "mean").style("font-size", "x-large").text("*").style("fill", "red").attr("x",x).attr("y", y);
},500)
});
Working code here.

d3 transitioning from bar to pie and back

LIVE DEMO
So I have this notion that all single axis data should be allowed to be displayed in all the basic ways; and at the very least from a pie to a bar. Ideally this would be an animated transition, but thats were the difficulty comes in.
Getting a pie chart to work is easy enough, as is getting a bar chart. Here is what I have so far:
# fields
width = 750
height = width/2
margin = 20
radius = (height-(margin*2))/2
# helpers
pie = d3.layout.pie().value (d) -> d
arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius/4)
x = d3.scale.linear().domain([0, 100]).range [0, width]
$http.get('/Classification_Top_10_by_Count.json').success (data) ->
percents = (parseFloat item.Percent for item in data).sort d3.ascending
svg = d3.select('#svgStage').append('svg')
.attr('width', width+(margin*2))
.attr('height', height+(margin*2))
svg.data([percents])
g = svg.append('g')
.attr('transform', "translate(#{radius},#{radius})")
paths = g.selectAll 'path'
paths.data(pie).enter().append('path')
.attr('d', arc)
toBars = ->
g.selectAll('path').transition().duration(2000)
.attr 'd', (d, index) ->
# this is over complex because I was playing with it.
cord =
tl : [0, index*20]
tr : [d.value*20, index*20]
br : [d.value*20, index*20-20]
bl : [0, index*20-20]
oCord = [
cord.tl
cord.tr
cord.br
cord.bl
]
"M #{oCord[0][0]}, #{oCord[0][2]}
A 0, 0 0 0, 0 #{oCord[1][0]}, #{oCord[1][3]}
L #{oCord[2][0]}, #{oCord[2][4]}
A 0, 0 0 0, 0 #{oCord[3][0]}, #{oCord[3][5]}
Z"
Obviously for this to work its got to be path element to path element, and the transition is working now. Problem is it looks like crap. The moment it starts it looks garbled, until it over and becomes decent bar chart.
I've been looking at this : http://d3-example.herokuapp.com/examples/showreel/showreel.html
Which demonstrates a bar transitioning to a donut in much the way I would like. Looking at the source code, this is accomplished through a custom tween. (view source line 518)
Now I'm in over my head. What is going on here? How can I get this transition to work? Has anyone else out there dealt with this problem?
UPDATE
Just to be clear, below illustrations the intention of my transition abit more clearly.
Bounty clarity. I added a bounty to this question because I need an explanation of what was going wrong. Superboggly did that, so he got the bounty. However Amit Aviv's approach is superior, so I accept his answer as the most correct. I have also +1ed both.
Here is my take: http://jsfiddle.net/amitaviv99/x6RWs/42/
My approach was to approximate both the arcs & bars using cubic bezier curves, with the exact same number of control points. The code is somewhat complicated, and need some work. But the result is quite smooth.
Here is an excerpt (SO requires..)
var bezierArc = function(radiusIn, radiusOut, startAngle, endAngle){
var arcIn = makeCompArc(radiusIn, startAngle, endAngle);
var arcIOut = makeCompArc(radiusOut, startAngle, endAngle);
var lines = makeBezierDoubleLine(radiusIn, radiusOut, startAngle, endAngle);
var path = [arcIn, lines[0], arcOut, lines[1]].join(' ');
return path;
}
D3 does a pretty good job of interpolating between paths, but it was having trouble with your original before and after path so instead of taking over the whole tweening process myself I thought maybe we could come up with better paths to make the job easier for D3. My result.
The first thing is to look at the svg arc path element. It basically goes like this:
A rx,ry a f1,f2 x,y
you can read the details here. This will draw an arc from wherever you are (previous final coordinate) to the coordinates x,y. But the things to focus on are that the first two numbers are the implied ellipse's radii and the last part before the end coordinates, that I've marked f1,f2, are flags and so not interpolate-able.
So the main weirdness in the transition from your code is because you are trying to interpolate between
A rx,ry, 0 0,1
A 0,0 0 0,0
You will immediately see a smoother transition if you set your end-path to A0,0 0 0,1 in the one case.
To make the pieces fit together a bit better I animated the pie's inner radius so that the segments looked more like the bars but curved, then I let D3 figure out the curve-to-bar transition but without switching the arc flag. Then you want the bars to have flat ends. The path will have a flatter arc if you increase your implied ellipses radii! So I simply used 100,100. My final transition-to path for the bars looks like:
"M " + oCord[0][0] + "," + oCord[0][1] +
"A100,100 0 0,1 " + oCord[1][0] + "," + oCord[1][1] +
"L " + oCord[2][0] + "," + oCord[2][1] +
"A100,100 0 0,0 " + oCord[3][0] + "," + oCord[3][1] +
"Z";
Then To actually, properly, flatten the endpoints I have a second transition (they run serially) to zero the Arc segments of the path. I suspect there is a better way to do this kind of cleanup with D3 transitions, but a transition with duration 0 also works.
To get the reverse to work nicely I set the paths to the flattened-arc-curves from above. Having the large radius and correct flags means the D3-computed transition back to the doughnut chart works well. Then I simply animate the inner radius back out.

D3's scale not working properly

Well, I'm starting with D3 and I'm trying to create a vertical bar chart.
I really don't know what's happening but some things are not working as expected for me (maybe because I'm just a noob on the matter).
I'm using line scales, works pretty well with axes, but it's miscalculating the height of the bars, for instance the higher values are not displayed (because of the low value of the result).
I've used the d3.max to determine the range. I really don't get what's happening.
var yScaleLeft = d3.scale.linear()
.domain([0, d3.max(stats)])
.range([realHeight, 0]);
.attr("height", function(d) {
return yScaleLeft(d);
});
Here is the code: http://jsfiddle.net/aKhhb/ Look at * Scales * and // Stats bars
(Just forget about the x-alignement of the bars, I will see that later, I want to set its height properly)
Thanks a lot! Y saludos desde Chile :)
The issue is that your input and output ranges are mirrored -- that is, the largest input value maps to the smallest output value. That is fine, but you need to take it into account when calculating the y and height attributes. Essentially, you had the calculations for both reversed.
Fixed fiddle here. I've also fixed the x axis by adding your margin and half of the bar width to the computed x positions. Oh and you don't need parseInt() when doing calculations, only when you actually want to parse an integer from a string.

Resources