Reputation: 5562
I'm trying to figure out how I can offset the x-axis ticks, as shown in this example, to be in the center of the bar when the x-axis uses scaleTime
.
Right now, I'm applying a transform to the axis when I append it to the svg as such:
// x-axis code
const x = d3.scaleTime()
.domain([d3.min(dates), d3.max(dates)])
.range([margin.left, width - margin.right]);
const x_axis = d3.axisBottom()
.scale(x);
...
// offsetting the axis horizontally when I append it with bandwidth / 2
svg.append('g')
.attr('transform', `translate(${bandwidth / 2},${height - margin.bottom})`)
.call(x_axis);
But this feels hacky and leaves space between the x-axis and the y-axis.
It seems like the example I mentioned has this right because it's not using scaleTime
but once scaleTime
comes into the picture then things get bad. How can offset my scaleTime ticks so they line up with the middle of my bars?
Full code below:
import * as d3 from 'd3';
import rawData from './data/readings.json';
import {
barSpacing,
margin,
getBandwidth,
} from './helpers';
const width = 1000;
const height = 500;
const animationDurationRatio = 5;
const barStyle = {
color: 'steelblue',
opacity: {
default: .9,
hover: 1
}
};
const getStepData = (data, stepNum) => {
return data.map((item, i) => {
const value = i < stepNum ? item.value : 0
return {
...item,
value
};
});
};
const data = rawData.map(item => {
return {
date: new Date(item.date),
value: item.breakfast
}
});
const dates = data.map(d => d.date);
const x = d3.scaleTime()
.domain([d3.min(dates), d3.max(dates)])
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.value))]).nice()
.range([height - margin.bottom, margin.top]);
const color = d3.scaleSequential(d3.interpolateRdYlGn)
.domain([140, 115]);
// Got these values using trial and error
// Still not 100% sure how this domain works
const x_axis = d3.axisBottom()
.scale(x);
const y_axis = d3.axisLeft()
.scale(y);
const chartWidth = x.range()[1];
const bandwidth = getBandwidth(chartWidth, data, barSpacing);
const svg = d3.create('svg')
.attr('width', chartWidth)
.attr('height', height)
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end');
const bar = svg.selectAll('g')
.data(getStepData(data, 0))
.join('g');
bar.append('rect')
.attr('fill', d => {
return color(d.value);
})
.attr('opacity', barStyle.opacity.default)
.attr('x', d => {
return x(d.date)
})
.attr('y', d => y(d.value))
.attr('width', bandwidth)
.attr('height', d => y(0) - y(d.value))
.on('mouseover', function() {
d3.select(this)
.transition(30)
.attr('opacity', barStyle.opacity.hover);
})
.on('mouseout', function() {
d3.select(this)
.transition()
.attr('opacity', barStyle.opacity.default);
});
bar.append('text')
.attr('fill', 'white')
.attr('x', (d, i) => x(d.date) + bandwidth / 2)
.attr('y', d => y(0) - 10)
.attr('dx', d => `0.${d.value.toString().length * 50}em`)
.text((d, i) => data[i].value);
svg.append('g')
.attr('transform', `translate(${bandwidth / 2},${height - margin.bottom})`)
.call(x_axis);
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(y_axis);
document.querySelector('body').appendChild(svg.node());
function animateBars (data) {
const bars = svg.selectAll('rect')
.data(data);
bars
.transition()
.ease(d3.easeLinear)
.duration(d => animationDurationRatio * d.value)
.attr('y', d => y(d.value))
.attr('fill', d => {
return color(d.value);
})
.attr('height', d => y(0) - y(d.value));
}
animateBars(data)
Upvotes: 1
Views: 857
Reputation: 5562
Because scaleTime
is a continuous scale, not a banded one, this isn't supported without some sort of hack or workaround (see workarounds below).
Note: Perhaps one reason for this is that some think bar charts aren't a good fit for time scales. Instead time is better visualized with a line chart or an area chart.
The best solution for this is to switch to scaleBand
or something else that supports discrete bands (here's an example).
One workaround is to make the first bar half the size and then offset all bars by half their width:
.attr('x', d => {
return x(d.date) - bandwidth / 2
})
.attr('width', (d, i) => i === 0 ? bandwidth / 2 : bandwidth)
Other hacks/workarounds might include adding an extra day, hiding the first tick, or messing with the axis offset.
Source: https://observablehq.com/@d3/stacked-bar-chart#comment-af5453e2ab24d987
Upvotes: 1
Reputation: 30
You need to translate both the axes with the same x units.
svg.append('g')
.attr('transform', `translate(${margin.left},${height-margin.bottom})`)
.call(x_axis);
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(y_axis);
const numberOfTicks = 5;
const x_axis = d3.axisBottom()
.scale(x)
.ticks(numberOfTicks - 1);
You can set the number of ticks in x-axis. The number of ticks generated on graph will be numberOfTicks + 1
Upvotes: 0