I need to create a d3 bar chart that can have negative values. Ideally the axis zero position should be calculated based on the extent of the data, but I'd settle for a solution that assumes symmetric positive and negative extent, i.e. that it would be always in the middle of the chart.
Here's an example of what I'd like to achieve.
OK, let's say you have an array of numbers as your dataset, and this includes some positive and negative values:
var data = [-15, -20, -22, -18, 2, 6, -26, -18];
You'll want two scales to construct a bar chart. You need one quantitative scale (typically a linear scale) to compute the bar positions along the x-axis, and a second ordinal scale to compute the bar positions along the y-axis.
For the quantitative scale, you typically need to compute the domain of your data, which is based on the minimum and maximum value. An easy way to do that is via d3.extent:
var x = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width]);
You might also want to nice the scale to round the extent slightly. As another example, sometimes you want the zero-value to be centered in the middle of the canvas, in which case you'll want to take the greater of the minimum and maximum value:
var x0 = Math.max(-d3.min(data), d3.max(data));
var x = d3.scale.linear()
.domain([-x0, x0])
.range([0, width])
.nice();
Alternatively, you can hard-code whatever domain you want.
var x = d3.scale.linear()
.domain([-30, 30])
.range([0, width]);
For the y-axis, you'll want to use rangeRoundBands to divide the vertical space into bands for each bar. This also lets you specify the amount of padding between bars. Often an ordinal scale is used with some identifying data—such as a name or a unique id. However, you can also use ordinal scales in conjunction with the data's index:
var y = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, height], .2);
Now that you've got your two scales, you can create the rect elements to display the bars. The one tricky part is that in SVG, rects are positioned (the x and y attributes) based on their top-left corner. So we need to use the x- and y-scales to compute the position of the top-left corner, and that depends on whether the associated value is positive or negative: if the value is positive, then the data value determines the right edge of the bar, while if it's negative, it determines the left edge of the bar. Hence the conditionals here:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return x(Math.min(0, d)); })
.attr("y", function(d, i) { return y(i); })
.attr("width", function(d, i) { return Math.abs(x(d) - x(0)); })
.attr("height", y.rangeBand());
Lastly, you can add an axis to display tick marks on top. You might also compute a fill style (or even a gradient) to alter the differentiate the appearance of positive and negative values. Putting it all together:
Bar Chart with Negative Values
Related
I have a stackblitz here - https://stackblitz.com/edit/d3-start-above-zero?embed=1&file=index.js&hideNavigation=1
It's a super simple bar chart with one bar
The y axis shows values from 0-200
The values for the bar are start at 50 and finish at 150 so I wanted to draw the bars at these values so the bar would be somewhere in the center of the graph.
I sort of have the height but can't fix the y position.
You can make the range going from 0 to height...
scale.range([0, height])
...or from height to 0...
scale.range([height, 0])
...but this doesn't change the fact that, in an SVG, the coordinates system of the y axis goes from the top to the bottom of the page. Therefore, the y attribute has to be always smaller than the height for a rectangle.
That being said, you have to use finish for the y attribute:
.attr("y", function (d, i) {
return y(d.finish);
})
.attr("height", function(d,i){
return y(d.start) - y(d.finish);
});
Here is the updated code: https://stackblitz.com/edit/d3-start-above-zero-qnk7bl?file=index.js
I'm following Mike Bostock's example to produce a horizontal bar chart with positive and negative values.
I'm trying to centralise the y-axis, so that there is always the same length of x-axis each side. But can't find any examples out there. I want the scale of the x-axis to remain sensitive to the data, so I don't want to specify the range.
I can't get the example he gives to work. :
For the quantitative scale, compute the data domain (the minimum and maximum value) using d3.extent:
var x = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d.value; }))
.range([0, width]);
Nicing the scale will extend the extent slightly to the nearest round numbers. If you want the zero-value to
be centered in the middle of the canvas, take the greater of the
minimum and maximum value by magnitude, or simply hard-code the
desired domain.
Any examples of how to correctly calculate the domain or help most welcome!
First, let's discover which one is greater, the minimum or the maximum value:
var greater;
if (Math.abs(d3.min(data, function(d){ return d.value})) > Math.abs(d3.max(data, function(d){ return d.value}))) {
greater = Math.abs(d3.min(data, function(d){ return d.value}));
} else {
greater = Math.abs(d3.max(data, function(d){ return d.value}));
};
And then set the scale:
var x = d3.scale.linear()
.domain([greater * -1, greater])
.range([0, width]);
I made a histogram / bar graph. I read in my frequency data as integers and set up my y-axis like this:
var yScale = d3.scale.linear().range([300, 0]).domain([0, 2]);
var yAxis = d3.svg.axis().scale(yScale).orient(‘left’)
.tickFormat(d3.format(,.0f));
Unfortunately, the y axis repeats each frequency several times as shown here:
How do I tell d3 to stop repeating y-values on the y-axis? I don’t want to use .ticks(someNumber) since I want to keep the number of ticks itself flexible.
I needed mine to be dynamic, this worked for me: [Version 4]
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(d3.format("%d"));
// Reset the axes domains with new data
y.domain([0, d3.max(data, function (d) { return d.value; })]);
if (y.domain() [1] < 10) {
yAxis.ticks(y.domain()[1])
// 2 ticks
//yAxis.tickValues(y.domain());
}
// Add the y-axis with a transition
yAxisG
.transition()
.duration(500)
.call(yAxis);
Use .ticks(n) instead of tickFormat() on your axis. The ticks() function defines how many ticks d3 should target - it's not always exactly that number. It chooses the most sane division unit on its own. n is 10 by default but you could change it depending on the domain, so for the example data you could set it to 3 (0,1,2). You could theoretically also use it on data enter.
Is your graph's range/height dynamic depending on data? In most cases you don't want that as it's unpredictable. And if you set your graph's height explicitly anyway you DO want to limit the number of ticks and labels to a number best suiting that size.
You might also want to look into https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_nice . That allows you to define rules for your ticks.
I'm building my first line graph in d3:
http://jsfiddle.net/j94RZ/
I want to know how to utilize either the scale or axis allow me to draw a grid (of, presumably rectangles) where I can set a different background colour for each of the section of the grid...so I can alternate colours for each cell of the grid. I want the grid to be drawn and be constrained by the axes of my graph and then also adapt if the spacing of the axes ticks change (i.e. the axes changes like this: http://bl.ocks.org/mbostock/1667367). So if my graph has an x axis with 4 ticks and a y axis of 7 ticks then my graph will have a background grid that's 7 blocks high and 4 blocks wide.
I've been playing with the idea of using a range which starts at zero and ends at the full width of the graph but I don't know what value I can use for the step. Is there any way to sort of query the axis and return how many ticks there are?
var gridRange = d3.range(0, width, step?);
A better approach than your current solution would be to use scale.ticks() explicitly to get the tick values. The advantage of that is that it will still work if you change the number of ticks for some reason.
To get an alternating grid pattern instead of a single fill, you can use something like this code.
.attr("fill", function(d, i) {
return (i % 2) == 1 ? "green" : "blue";
})
Finally, to get the full grid pattern, you can either use an explicit loop as you've suggested, or nested selections. The idea here is to first pass in the y ticks, create a g element for each and then pass the x ticks to each one of these groups. In code, this looks something like this.
svg.selectAll("g.grid")
.data(y.ticks()).enter().append("g").attr("class", "grid")
.selectAll("rect")
.data(x.ticks()).enter().append("rect");
To set the position, you can access the indices within the top and bottom level data arrays like this.
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d, i, j) {
return yScale(j);
})
To set the x position, you need the index of the inner array (passed to the set of g elements), which can be accessed through the second argument of your callback. For the outer array, simply add another argument (j here).
And that's really all there is to it. Complete jsfiddle here. To update this grid dynamically, you would simply pass in the new tick values (gotten from scale.ticks()), match with the existing data, and handle the enter/update/exit selections in the usual manner.
If you want to do without the auxiliary scales (i.e. without .rangeBand()), you can calculate the width/height of the rectangles by taking the extent of the range of a scale and dividing it by the number of ticks minus 1. Altogether, this makes the code a bit uglier (mostly because you need one fewer rectangle than ticks and therefore need to subtract/remove), but a bit more general. A jsfiddle that takes this approach is here.
So after a few helpful comments above I've got close to a solution. Using Ordinal rangebands get me close to where I want to go.
I've created the range bands by using the number of ticks on my axis as a basis for the range of the input domain:
var xScale = d3.scale.ordinal()
.domain(d3.range(10))
.rangeRoundBands([0, width],0);
var yScale = d3.scale.ordinal()
.domain(d3.range(4))
.rangeRoundBands([0, height],0);
I've then tried drawing the rectangles out like so:
svg.selectAll("rect")
.data(p)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d,i) {
0
})
.attr("width", xScale.rangeBand())
.attr("height", yScale.rangeBand())
.attr("fill", "green").
attr('stroke','red');
This gets me the desired effect but for only one row deep:
http://jsfiddle.net/Ny2FJ/2/
I want,somehow to draw the green blocks for the whole table (and also without having to hard code the amount of ticks in the ordinal scales domain). I tried to then apply the range bands to the y axis like so (knowing that this wouldn't really work though) http://jsfiddle.net/Ny2FJ/3/
svg.selectAll("rect")
.data(p)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d,i) {
return yScale(i);
})
.attr("width", xScale.rangeBand())
.attr("height", yScale.rangeBand())
.attr("fill", "green").
attr('stroke','red');
The only way I can think to do this is to introduce a for loop to run the block of code in this fiddle http://jsfiddle.net/Ny2FJ/2/ for each tick of the y axis.
I have a zoomable area plot done in D3, which works well. Now I am trying to add a rectangle to the specified location along x-axis in the middle of the plot. However, I can't seem to figure out how to do that. "rect" element is specified using absolute (x,y) of the plot and so when using zooms it stays in the same position.
So I was wondering if there is a way to tie "rect" to the axis when plotting, so that it benefits from all the zoom and translate behaviour or do I need to manually edit the x,y,width and length of the rectangle according to translation as well as figuring out where the corresponding x and y coordinates are on the graph? I am trying to use "rect" because it seems the most flexible element to use.
Thanks
Alex
I'm not sure how you are doing the zooming, but I am guessing you are changing the parameters of the scales you use with your axis? You should be able to use the same scales to place your rectangle.
If you are starting with plot coordinates then maybe using the invert function on the scale will help (available at least for quantitive scales), e.g. https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_invert
You should be able to take initial plot coordinates and invert them to determine data coordinates that can then move with changes in the scale.
If the scale is linear you can probably invert the length and width too, but you will have to compute offsets if your domain does not include 0. Easiest is to compute the rectangle's end points, something like:
var dataX0 = xScale.invert(rect.x);
var dataX1 = xScale.invert(rect.x + rect.width);
var dataWidth = dataX1 - dataX0;
If you have the data in axes coordinates already you should be able to do something like:
var rectData = [{x: 'April 1, 1999', y: 10000, width: 100, height:100}];
svg.selectAll('rect.boxy')
.data(rectData)
.enter().append('rect').classed('boxy', true)
.style('fill','black');
svg.selectAll('rect.boxy')
.attr('x', function(d) { return x(new Date(d.x));} )
.attr('y', function(d) { return y(d.y);})
.attr('width', function(d) { return d.width;} )
.attr('height', function(d) { return d.height;} );
Based on the example you shared where x and y (as functions) are the scales the axes are based on.