I'm trying to draw a bar chart with padding between the bars, but I can't get rangeRoundBands to insert padding - though the docs tell me this is possible, so I'm doing something wrong.
This is my code:
var x0 = d3.scale.ordinal()
.rangeRoundBands([margin.left, width], 0.1, 0.1);
...
ap_bars.transition().duration(1000)
.attr("x", function(d, i) {
return i * x0.rangeBand();
})
.attr("width", x0.rangeBand());
But the bars are all stacked together.
JSFiddle here: http://jsfiddle.net/6pnem/5/
You're changing the size of the band with padding correctly, but the way you're plotting the bars will also put them side by side right next to each other. As you increase the padding, you'll see the that the bars just get skinnier and skinnier but stay pinched together.
Instead of using a constant based on the data index (i * x0.rangeBand()) you need to position the bar based on scaled data: (x0(d)). You'll probably want to adjust the position of the bar so that center of the bar is the center of each band: (x0(d) - x0.rangeBand() * 0.5).
Related
I want to create a grouped bar chart where each bar is unique and not part of a series.
For example, imagine a bar chart showing the population of each major city, grouped by state.
Is this possible with d3js and any pointers on how to to get started?
Thanks
Yes it is possible!
As with most visualisation the process is one of working out the structure of the chart and then working out how to get the data in to that structure. In this case I think the data in it's simplest form is going to look something like this ...
let groups = [
{ name:'one', values:[1,3,6,2] },
{ name:'two', values:[3,5,7,3,2,5] },
{ name:'three', values:[9,2,5] },
{ name:'four', values:[6] }
];
An array of groups, where each group has some properties including an array of values to be represented as individual bars.
The tricky bit is working out the position of each group and each bar along the horizontal axis. The way I think about this is as follows: If we say each bar has a width of 1 unit, how much space do we want between the groups? Maybe a bar and a half, so 1.5 units. Now each group is going to take up space equal to the number of bars in it (the length of the values array) and the total width required by the chart will be the sum of those values plus the spaces between them.
i.e.
let dataWidth = d3.sum(groups, d=>d.values.length) + (groups.length-1) * groupPadding;
We also want to go through the groups and work out their "start position", where the group is placed horizontally in terms of bar units, like this:
let groupPadding = 1.5;
let currentWidth = 0;
groups = groups.map(group=>{
group.width = group.values.length;
group.startPosition = currentWidth;
currentWidth += group.width+groupPadding;
return group;
});
Next we need to make an x and a y-scale using d3-scale.
For the y-scale let's keep it simple and hard code the maximum value
const yScale = d3.scaleLinear()
.domain([0,10])
.range([chartHeight, 0]); // chart height is just your charts height in pixels
and for the x scale we use the dataWidth we calculated above as the maximum for the domain, we'll use this scale to convert bar units into screen pixels
const xScale = d3.scaleLinear()
.domain([0, barWidth)
.range([0, chartWidth]); //chartWidth, the width of your chart in pixels
OK. Now we have everything we need to know to draw the chart. The structure of the chart as I see envisage it...
Each group in groups is an svg g element positioned according to its startPosition. Each of those g elements contains a set of rect elements, one for each value. Inside the g element you may also want to put stuff like a group label.
Broadly this would look something like this...
Create the groups
const barGroups = chart.selectAll('g.bar-group')
.data(groups)
.enter()
.append('g')
.classed('bar-group', true)
.attr('transform', group=>`translate(${xScale(group.startPosition)}, 0)`);
Add the rectangles
barGroups.each(function(group){
const barGroup = d3.select(this);
barGroup.selectAll('rect')
.data(group.values)
.enter()
.append('rect')
.attr('width', xScale(1))
.attr('height', value => yScale(value))
.attr('x', (value, i) => xScale(i))
.attr('y', value => chartHeight - yScale(value));
Here's a bl.ock that puts it all together and fills in the gaps to give a working example: Grouped Bars
I am using D3 to draw a line chart. The value at x=0 does not show up.
The code for the axis is shown below.
const xScale = d3
.scaleTime()
.domain(d3.extent(data[0].series, d => d.time))
.range([xPadding, width - xPadding]);
const xAxis = d3
.axisBottom(xScale)
.ticks(4)
.tickSizeOuter(0)
.tickSizeInner(0)
.tickFormat(d3.timeFormat('%Y'));
I am not sure why it is not showing up the label at x=0, which is 2014. On checking the SVG, only three tick marks are displayed, but the one at x=0 is not in the SVG element.
CodePen for this: https://codepen.io/vijayst/pen/bLJYoK?editors=1111
I see different solutions which have their pros and cons. The third solution should be the cleanest and most generic.
Add the left tick manually:
Since d3 handles itself the location of x-axis ticks, one way of doing so would (if the data set is fixed) would be to manually add the missing tick:
svg
.append("g")
.append("text")
.text("2014-02-01") // can be retrieved from data instead of being harcoded
.style("font-size", 10)
.style("font-family", "sans-serif")
.attr("transform", "translate(0," + (height - yPadding + 10) + ")")
which looks great, but in this case you might have problems if for a given dataset, d3 chooses to display a tick close to the left edge of the axis. Both d3's tick and the label we've included could overlap.
Modify the x-scale to start before the first day of the year:
An other solution would be to increase the x-axis range on the left to make it start one month before the first point's date. To try this out, we can replace:
.domain(d3.extent(data[0].series, d => d.time))
with
.domain(d3.extent([new Date(2013, 12), new Date(2019, 1)]))
which allow d3 to legitimately include a "year-tick" for 2014 at the begin of the x-axis.
but in this case, the first point will have an offset with the begin of the x-axis range.
Push a specific tick to ticks auto-generated by d3:
An other solution: we can push a specific tick to the ticks auto-generated by d3. But this requires to modify the format of ticks to "%Y-%m".
In order to do this, replace:
.tickFormat(d3.timeFormat("%Y"));
with
.tickFormat(d3.timeFormat("%Y-%m"));
we can then push a new specific tick to the set of ticks generated by d3:
var ticks = xScale.ticks();
ticks.push(new Date(2014, 1, 1));
xAxis.tickValues(ticks);
and include some padding in the left and the right of the chart since now tick labels have a part displayed outside the graph:
const svg = d3
.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("padding-left", 15)
.style("padding-right", 15);
I have a plunker here https://plnkr.co/edit/hBWoIIyzcHELGyewOyZE?p=preview
I'm using this as starting point to create a stacked bar chart
https://bl.ocks.org/d3indepth/30a7091e97b03eeba2a6a3ca1067ca92
I need the chart to be vertical and to have axis
In my example I have the the chart vertical but I'm stuck getting it to start from the base and go upwards.
I know it's because the y attr but I'm stuck getting it to work.
.attr('y', function(d) {
return d[0];
})
try basing the y attribute as the difference between height and d[1]:
.attr('y', function(d) {
return height - d[1];
})
your plunker works for me (i think as desired?) with this correction.
explanation: these coordinates are relative to point 0, 0 (upper left hand corner) so for the y-axis of a graph like this you always have to flip it around... you captured it correctly in your axis scales (e.g. for the x: [0, width] and y: [height, 0])
I am using the same chart as below. I want to push the x-axis headers i.e. Regular, Premium, Budget little bit below i.e. top padding or margin. Give some styling to it like give background color and change text color. I tried using fill and it does not work as desired. I would like to hide Price Tier/Channel also
http://dimplejs.org/examples_viewer.html?id=bars_vertical_grouped
These are SVG text elements so there is no top-padding or margin. You can move them down a bit by increasing the y property though, running the following after you call the chart.draw method will move the labels down 5 pixels:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) {
// Get the y property of the current shape and add 5 pixels
return parseFloat(d3.select(this).attr("y")) + 5;
});
To change the text colour you need to use the fill property (again that's an svg text thing):
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.style("fill", "red");
To colour the background of the text is a little less trivial, there actually isn't a thing for that in SVG, however you can insert a rectangle behind the text and do what you like with it:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
// Iterate each shape matching the selector above (all the x axis labels)
.each(function () {
// Select the shape in the current iteration
var shape = d3.select(this);
// Get the bounds of the text (accounting for font-size, alignment etc)
var bounds = shape.node().getBBox();
// Get the parent group (this the target for the rectangle to make sure all its transformations etc are applied)
var parent = d3.select(this.parentNode);
// This is just the number of extra pixels to add around each edge as the bounding box is tight fitting.
var padding = 2;
// Insert a rectangle before the text element in the DOM (SVG z-position is entirely determined by DOM position)
parent.insert("rect", ".dimple-custom-axis-label")
// Set the bounds using the bounding box +- padding
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
// Do whatever styling you want - or set a class and use CSS.
.style("fill", "pink");
});
These three statements can all be chained together so the final code will look a bit like this:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) { return parseFloat(d3.select(this).attr("y")) + 5; })
.style("fill", "red")
.each(function () {
var shape = d3.select(this);
var bounds = shape.node().getBBox();
var parent = d3.select(this.parentNode);
var padding = 2;
parent.insert("rect", ".dimple-custom-axis-label")
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
.style("fill", "pink");
});
FYI the dimple-custom-axis-label class was added in a recent release of dimple so please make sure you are using the latest version. Otherwise you'll have to find an alternative selector
I'm trying to wrap my head around the log scales provided by D3.js. It should be noted that as of yesterday, I had no idea what a logarithmic scale was.
For practice, I made a column chart displaying a dataset with four values: [100, 200, 300, 500]. I used a log scale to determine their height.
var y = d3.scale.log()
.domain([1, 500])
.range([height, 1]);
This scale doesn't work (at least not when applied to the y-axis as well). The bar representing the value 500 does not reach the top of the svg container as it should. If I change the domain to [100, 500] that bar does reach the top but the axis ticks does not correspond to the proper values of the bars. Because 4e+2 is 4*10^2, right?
What am I not getting here? Here is a fiddle.
Your scale already reverses the range to account for the SVG y-coordinates starting at the top of the screen -- ie, you have domain([min, max]) and range([max, min]). This means your calcs for the y position and height should be reversed because your scale already calculated y directly:
bars.append("rect")
.attr("x", function (d, i) { return i * 20 + 20; })
.attr("y", function (d) { return y(d); })
.attr("width", 15)
.attr("height", function (d) { return height - y(d); });
Here's an updated Fiddle: http://jsfiddle.net/findango/VeNYj/2/