Reputation: 3787
I have a line chart in d3.js where I have labels on the X-axis every year (showing 20 years of data). The labels are created with:
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%b/%d/%Y")).ticks(d3.timeYear))
.selectAll("text")
.style("text-anchor", "end")
.attr("dy", ".25em")
.attr("transform", "rotate(-45)");
The outcome looks like this:
Now, the thing is, I need the labels to not be placed on January 1st of each year - I need them on June 30th. How can I accomplish that?
See the Fiddle here to try for yourself.
Upvotes: 2
Views: 175
Reputation: 38151
You can specify an interval with axis.ticks()
, D3 provides a number of built in intervals which we can use and then filter for the appropriate day/month/time.
If we wanted June 1 every year we could use:
var axis = d3.axisBottom(x)
.tickFormat(d3.timeFormat("%b/%d/%Y"))
.ticks(d3.timeMonth.filter(function(d) { return d.getMonth() == 5; })))
If we want June 30 we can specify with a bit more specificity:
var axis = d3.axisBottom(x)
.tickFormat(d3.timeFormat("%b/%d/%Y"))
.ticks(d3.timeDay.filter(function(d) { return d.getMonth() == 5 && d.getDate() == 30 }))
The d3-time docs have some more description of d3 intervals and the d3-scale documentation time scale details have some example implementations of this method here.
Here's an updated fiddle
Upvotes: 1
Reputation: 7403
One way of doing this is to specify explicitly each desired axis tick's value. The function axis.tickValues
is designed for this.
The following function generates an array of dates, starting from the min
date (june 28th, 2000 in your case), and adding a year until the max
date (june 28th, 2020) is reached. It is necessary to go through this generation step because the dataset does not contain data for all years.
function generateTickvalues(min, max) {
let res = []
, currentExtent = new Date(min.valueOf())
while(currentExtent <= max) {
res.push(new Date(currentExtent.valueOf()))
currentExtent.setFullYear(currentExtent.getFullYear() + 1);
}
return res
}
Remark: new Date(date.valueOf())
is necessary in this function so that the date values from the original dataset are not overwritten.
The min and max dates from the dataset can conveniently be found using d3.extent
. This array can also be used when calling x.domain
.
let dateExtent = d3.extent(data, function(d) { return d.date})
let tickValues = generateTickvalues(dateExtent[0], dateExtent[1])
x.domain(dateExtent);
Then, when generating the axis, call the function axis.tickValues
, passing the array of years starting from June just generated:
d3.axisBottom(x)
.tickFormat(d3.timeFormat("%b/%d/%Y"))
.ticks(d3.timeYear)
.tickValues(tickValues)
Demo in the snippet below:
const data = [
{ value: 46, date: '2000-06-28', formatted_date: '06/28/2000' },
{ value: 48, date: '2003-06-28', formatted_date: '06/28/2003' },
{ value: 26, date: '2004-06-28', formatted_date: '06/28/2004' },
{ value: 36, date: '2006-06-28', formatted_date: '06/28/2006' },
{ value: 40, date: '2010-06-28', formatted_date: '06/28/2010' },
{ value: 48, date: '2012-06-28', formatted_date: '06/28/2012' },
{ value: 34, date: '2018-06-28', formatted_date: '06/28/2018' },
{ value: 33, date: '2020-06-28', formatted_date: '06/28/2020' }
];
create_area_chart(data, 'history-chart-main');
function generateTickvalues(min, max) {
let res = []
, currentExtent = new Date(min.valueOf())
while(currentExtent <= max) {
res.push(new Date(currentExtent.valueOf()))
currentExtent.setFullYear(currentExtent.getFullYear() + 1);
}
return res
}
function create_area_chart(data, target){
document.getElementById(target).innerHTML = '';
var parentw = document.getElementById(target).offsetWidth;
var parenth = 0.6*parentw;
var svg = d3.select('#'+target).append("svg").attr("width", parentw).attr("height", parenth),
margin = {top: 20, right: 20, bottom: 40, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var parseTime = d3.timeParse("%Y-%m-%d");
bisectDate = d3.bisector(function(d) { return d.date; }).left;
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var area = d3.area()
.x(function (d) { return x(d.date); })
.y1(function (d) { return y(d.value); });
data.forEach(function (d) {
//only parse time if not already parsed (i.e. when using time period filters)
if(parseTime(d.date))
d.date = parseTime(d.date);
d.value = +d.value;
});
let dateExtent = d3.extent(data, function(d) { return d.date})
let tickValues = generateTickvalues(dateExtent[0], dateExtent[1])
x.domain(dateExtent);
y.domain([0, 1.05 * d3.max(data, function (d) { return d.value; })]);
area.y0(y(0));
g.append("rect")
.attr("transform", "translate(" + -margin.left + "," + -margin.top + ")")
.attr("width", svg.attr("width"))
.attr('class', 'overlay')
.attr("height", svg.attr("height"))
.on("mouseover", function () {
d3.selectAll(".eps-tooltip").remove();
d3.selectAll(".eps-remove-trigger").remove();
focus.style("display", "none");
});
g.append("path")
.datum(data)
.attr("fill", "#f6f6f6")
.attr("d", area);
//create line
var valueline = d3.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return y(d.value); });
g.append("path")
.data([data])
.attr('fill', 'none')
.attr('stroke', '#068d46')
.attr("class", "line")
.attr("d", valueline);
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%b/%d/%Y")).ticks(d3.timeYear).tickValues(tickValues))
.selectAll("text")
.style("text-anchor", "end")
.attr("dy", ".25em")
.attr("transform", "rotate(-45)");
g.append("g")
.call(d3.axisLeft(y)
.tickFormat(function (d) { return "$" + d }))
.append("text")
.attr("fill", "#068d46")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end");
var focus = g.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("line")
.attr("class", "x-hover-line hover-line")
.attr("y1", 0)
.attr("y2", height);
focus.append("line")
.attr("class", "y-hover-line hover-line")
.attr("x1", width)
.attr("x2", width);
focus.append("circle")
.attr("fill", "#068d46")
.attr("r", 4);
focus.append("text")
.attr("class", "text-date focus-text")
.attr("x", 0)
.attr("y", -20)
.attr("dy", ".31em")
.style("text-anchor", "middle");
focus.append("text")
.attr("class", "text-val focus-text")
.attr("x", 0)
.attr("y", -30)
.attr("dy", ".31em")
.style("text-anchor", "middle");
g.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function () { focus.style("display", null); })
.on("mousemove", function () {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.year > d1.year - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.date) + "," + y(d.value) + ")");
focus.select(".text-date").text(function () { return d.formatted_date; });
focus.select(".text-val").text(function () { return '$' + d.value; });
focus.select(".x-hover-line").attr("y2", height - y(d.value));
focus.select(".y-hover-line").attr("x2", width + width);
});
}
.chart {
text-align: center;
padding: 10px 10px 25px 10px;
background: #f6f6f6;
}
.chart svg {
overflow: visible;
}
.chart .overlay {
fill: none;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
<div class="chart" id="history-chart-main"></div>
Upvotes: 1