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

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.

Related

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.

Centering force offsets force diagram d3js v4

I have a simple force diagram that looks like this:
In the picture we can see nodes nicely aligned along side the x axis with a small 100px buffer zone to the left and the right of the middle (black) node. This is handled by 2 scales:
Left scale with range (10, width/2 - 100). 10 gives a small padding from the edge.
Right scale with range (width/2 + 100, width -10).
The position of the node in the middle is (width/2, height/2). The x position of red and green nodes is handled by the right/left scale as well as the axis at the bottom.
The above works fine until I add
.force("center", d3.forceCenter(width / 2, height / 2));
to my force definition and the result is:
It offsets the picture to the left by x amount. Why is that?
The svg element that the nodes are appended to doesnt have any css styling (padding or margins), maybe it should? What am I missing here?
UPDATE:
Here is Fiddle with the same problem. You can see that the right and left nodes are slightly off the number it should be on top of. If you would comment out line 40 the offset is no longer.
Whilst putting this together I realised that the problem might be how my diagram's svg is constructed. At the moment its
svg
|
- g.nodes
- g.links
- g.axisleft
- g.axisright
Nodes are appended into g.nodes element.
It is the center of gravity of the nodes
The chart center X is 400.
The node positions are
x1=168.68
x2=388.68
x3=642.68
Difference to 400
x1= -231.32
x2= -11.32
x3= 242.68
If you add them up it is very close to 0.
Force layout does not necessarily guarantee that the node will end up in a given starting coordinate. You are applying multiple forces to nodes and the nodes' final position is basically the result of superimposing all these forces.
In your case, the final positions are the result of interaction between forceCenter and force.X. You can remove one of them and see how the other one alone works.
One way to fix the problem in your case is to use the symmetrical nature of forceCenter:
.force('x', d3.forceX().x(function(d) {
if (d.cl === 0)
return width / 2;
if (d.cl < 0)
return xScale_lo(-5);
return xScale_hi(5); //instead of 6
I am not sure how this will work when the number of nodes increases. If the goal is to have separated clusters of nodes, you can experiment with cluster forces.

How to correctly transition stacked bars in d3.js

I'm trying to get a stacked bar chart to animate correctly as bars come and go. There's probably a good example of this somewhere (maybe I'll ask as a separate question), but the examples I'm finding don't show transitions with individual stack elements exiting and entering I want to make sure that as bars are exiting, they drag down the bars above them, and as they're entering, they push up the bars above them. And I don't want any gaps or overlaps midway through the transition.
Can anyone point me to an example that does this?
Correcting my wrong-headed question:
Ashitaka answered the question with a helpful jsfiddle. His answer prompted me to look at the d3 stack layout more closely, where I read:
In the simplest case, layers is a two-dimensional array of values. All of the 2nd-dimensional arrays must be the same length.
So, I concluded I was going about this all wrong. I shouldn't have been trying to remove stack bars at all. If bars in my data were going to disappear, I should leave them in the data and change their height to zero. That way the transitions work great. I haven't yet had to deal with new bars appearing.
One confusing aspect of transitioning stacked charts (and working with SVG in general) is that the coordinate system origin is at the top-left corner, which means that y increases downwards.
First, our data should have 2 y related attributes:
y, the height of the bar
And y0, the baseline or the y position of the bar when it's on top of other bars. This should be calculated by d3.layout.stack().
Then, we should create 2 scales:
One for height, which works exactly as expected:
var heightScale = d3.scale.linear()
.domain([0, maxStackY])
.range([0, height]);
And one for the y position, which works in the reverse way:
var yScale = d3.scale.linear()
.domain([0, maxStackY])
.range([height, 0]);
With these two scales, we can create some functions to calculate the appropriate y positions and heights of our bars:
var barBaseY = function (d) { return yScale(d.y0); };
var barTopY = function (d) { return yScale(d.y0 + d.y); };
var barHeight = function (d) { return heightScale(d.y); };
Next, it's critical that we create a key function so that elements are bound to the correct data:
var joinKey = function (d) { return d.name; };
Without this function D3 would join the data using its index, which would break everything.
Now, to remove or add a set of bars from the stack, we take these steps:
Recalculate the stack:
var newStack = stack(enabledSeries());
Join the new stack with the current selection of layers with the data function:
layers = layers.data(newStack, joinKey);
With our key function, D3 determines the bars that are to be added, removed or updated.
Access the appropriate bars:
layers.enter() contains the "enter selection", that is, the new set of bars to be added.
layers.exit() contains the "exit selection", that is, the set of bars to be removed.
And simply layers contains the "update selection", that is, the bars that are to be updated. However, after enter.append the "update selection" is modified to contain both entering and updating elements. This has changed in D3 v4 though.
Animate the bars:
For added bars, we create them with height 0 and y position barBaseY.
Then we animate all the bars' height and y attributes.
For removed bars, we animate them to height 0 and y position barBaseY, the exact opposite of adding bars. Then we animate all the remaining bars' height and y attributes. D3 is smart enough to render all these animations at the same time.
Here's a pared down version of the stacked chart I linked to in my first comment.
And here's a visual explanation of why you have to animate both y and height attributes to simulate a bar diminishing in size "going down".

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.

Minor tweaks for histogram generated using d3

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);

Resources