Reputation: 26944
I have a graph which has data points for each day, but I'm dissatisfied with the default setup of the x axis.
The default x-axis picks out a selection of points to label, e.g. with xAxis.ticks(d3.time.month);
it will label the 1st of each month with the name of that month:
|-------|-------|-------|-------|-------|-------|
nov dec jan feb mar apr may
This works fine for a bar graph, where there is only one bar at each of the months above, but it is misleading for an area graph, as e.g. the 'd' in 'dec' appears under the data points for the 28th, 29th & 30th of november.
I want it to label the range that the month covers (the gaps between the ticks):
|-------|-------|-------|-------|-------|-------|
nov dec jan feb mar apr
Any easy way to specify this at a high level?
Upvotes: 1
Views: 842
Reputation: 1959
(2019) Using latest d3v5, other stackoverflow responses, 1, and mbostock trick to select and edit the labels text property, axis styling.
First giving a x_scale
and number of ticks
you can calculate the distance in rendered pixels of all ticks, including non linear scales. The function below returns an array with ticks distances / spaces.
// ticksDistance is constant for a specific x_scale
// apply scale to ticks and calcule distance
const getTicksDistance = (scale) => {
const ticks = scale.ticks(); // return array with ticks
const spaces = []
for(let i=0; i < ticks.length - 1; i++){
spaces.push(scale(ticks[i+1]) - scale(ticks[i]))
}
return spaces;
};
const ticksSpacingTime = getTicksDistance(x_scale_time);
Then use that distance to shift the labels from its current position, the index i
gives you the position d3 is iterating over the text labels.
svg.append("g")
.attr("class", "x-axis-shifted")
.attr("transform", "translate(0,100)")
.call(x_axis)
.selectAll("text")
.attr("x", (d,i) => ticksSpacingTime[i]/2)
const x_scale_time = d3.scaleTime()
.domain([new Date(2017,12,1),new Date()])
.range([0, 960]);
const x_axis_time = d3.axisBottom()
.scale(x_scale_time)
.ticks(d3.timeMonth.every(1))
const x_scale_pow = d3.scalePow().exponent(2)
.domain([0,20000])
.range([0, 960]);
const x_axis_pow = d3.axisBottom()
.scale(x_scale_pow)
.ticks(10)
// ticksDistance is constant for a specific x_scale
const getTicksDistance = (scale) => {
const ticks = scale.ticks();
const spaces = []
for(let i=0; i < ticks.length - 1; i++){
spaces.push(scale(ticks[i+1]) - scale(ticks[i]))
}
return spaces;
};
//you have to recalculate when x_scale or ticks change
const ticksSpacingTime = getTicksDistance(x_scale_time);
const ticksSpacingPow = getTicksDistance(x_scale_pow);
const svg = d3.select("body").append("svg")
.attr("width", "500px")
.attr("height","350px")
.style("width", "100%")
.style("height", "auto");
// normal
svg.append("g")
.attr("class", "x-axis-time")
.attr("transform", "translate(0,0)")
.call(x_axis_time)
// shift labels to half of the ticks distance
svg.append("g")
.attr("class", "x-axis-time-shifted")
.attr("transform", "translate(0,40)")
.call(x_axis_time)
.selectAll("text")
.attr("x", (d,i) => ticksSpacingTime[i]/2)
// normal
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0,110)")
.call(x_axis_pow)
// shift labels to half of the ticks distance
svg.append("g")
.attr("class", "x-axis-shifted")
.attr("transform", "translate(0,150)")
.call(x_axis_pow)
.selectAll("text")
.attr("x", (d,i) => ticksSpacingPow[i]/2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Upvotes: 1
Reputation: 16496
I had the same problem and solved it by using another scale for the X-axis that is shifted 15 days.
// scale is your usual x scale. change dates on the domain by 15 days
var d0 = scale.domain()[0];
d0.setDate(d0.getDate() - 15);
var d1 = scale.domain()[1];
d1.setDate(d1.getDate() - 15);
// use this new x scale for the axis only
new_scale = d3.time.scale().domain([d0, d1]).range(scale.range());
d3.svg.axis().scale(new_scale);
This way you have no awkwardly shifted texts and still leave all the heavy lifting to d3. For showing ticks you'd use a second x-axis with the normal scale.
Upvotes: 0
Reputation: 109242
To make them appear in the middle of the range, you would need to add an offset (dx) to the text
elements. This offset would be equal to half the size of an interval, which you can compute dynamically.
Here's an example that does something very similar.
Upvotes: 0