Reputation: 2643
I have a d3 time scale chart. At the moment, the axis ticks render a date for every data object. The data could have a range of anything from 1 day of data, or 2 weeks in 1 month, or 5 months worth of data or even more, for example.
Ideally, we want to display ticks with a week number, based on the data - not the week number of the year or month like this: xAxis.tickFormat(d3.time.format('%W'))
Eg, if the data starts from 15th of July, the first tick would say 'week 1', then 'week 2', etc.
How could you achieve such axis ticks with a time based d3 line chart? I'm new to d3 and almost lost with how to achieve this.
I'm using moment.js as well, so inside xAxis.tickFormat
, i've tried using a function with some logic that returns different values depending on the date, but this seems fragile and not 'the d3 way'. Also tried using a custom time formatter as seen here.
Alternatively, we could have simpler ticks - just displaying the month and/or day with d3.time.format(%d-%b)
, but then there are duplicate tick values like 'Feb...Feb..Feb..Feb...Feb..Mar..Mar..Mar..`. Is there a method to prevent duplicate values from appearing?
I've tried limiting the amount of ticks, but this isn't working as expected. Eg, If I have xAxis.ticks(5)
, 3 ticks appear; same story if I have xAxis.ticks(2)
. If ticks is defined as 1, only 1 tick appears. What's going on here?
Any help would be most appreciated! Code below with 2 dataset examples
<!DOCTYPE html>
<body>
<style>
path { fill: #CCC; }
.line { fill: none; stroke: #000; stroke-width: 5px;}
</style>
<div id="chart-container">
<svg id="chart"></svg>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var now = moment();
var chartData = [
{ timestamp: moment(now).subtract(27, 'days').format('DD-MMM-YY'), value: 40 },
{ timestamp: moment(now).subtract(25, 'days').format('DD-MMM-YY'), value: 36 },
{ timestamp: moment(now).subtract(24, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(21, 'days').format('DD-MMM-YY'), value: 35 },
{ timestamp: moment(now).subtract(20, 'days').format('DD-MMM-YY'), value: 35 },
{ timestamp: moment(now).subtract(18, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(17, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(16, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(15, 'days').format('DD-MMM-YY'), value: 32 },
{ timestamp: moment(now).subtract(13, 'days').format('DD-MMM-YY'), value: 35 },
{ timestamp: moment(now).subtract(11, 'days').format('DD-MMM-YY'), value: 31 },
{ timestamp: moment(now).subtract(10, 'days').format('DD-MMM-YY'), value: 28 },
{ timestamp: moment(now).subtract(9, 'days').format('DD-MMM-YY'), value: 32 },
{ timestamp: moment(now).subtract(8, 'days').format('DD-MMM-YY'), value: 30 },
{ timestamp: moment(now).subtract(7, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(6, 'days').format('DD-MMM-YY'), value: 36 }
];
//data could have a shorter date range of eg, 1 or 2 weeks
//ideally we want to still display 'week 1, 2, 3, 4' etc in the axis.
//alternatively display dates instead
// var chartData = [
// { timestamp: moment(now).subtract(27, 'days').format('DD-MMM-YY'), value: 40 },
// { timestamp: moment(now).subtract(25, 'days').format('DD-MMM-YY'), value: 36 },
// { timestamp: moment(now).subtract(24, 'days').format('DD-MMM-YY'), value: 33 },
// { timestamp: moment(now).subtract(21, 'days').format('DD-MMM-YY'), value: 35 },
// { timestamp: moment(now).subtract(20, 'days').format('DD-MMM-YY'), value: 35 }
// ];
let lastObj = chartData[chartData.length - 1];
let lastObjTimestamp = lastObj.timestamp;
let lastAndNow = moment(lastObjTimestamp).diff(now, 'days');
console.log('difference between last entry ' + lastObjTimestamp + ' and today: ' + lastAndNow);
var chartWrapperDomId = 'chart-container';
var chartDomId = 'chart';
var chartWrapperWidth = document.getElementById(chartWrapperDomId).clientWidth;
var margin = 40;
var width = chartWrapperWidth - margin;
var height = 500 - margin * 2;
var xMin = d3.time.format('%d-%b-%y').parse(chartData[0].timestamp);
var xMax = d3.time.format('%d-%b-%y').parse(chartData[chartData.length-1].timestamp);
//set the scale for the x axis
var xScale = d3.time.scale();
xScale.domain([xMin, xMax]);
xScale.range([0, width]);
var yScale = d3.scale.linear()
.range([height, 0])
.nice();
console.log('no5 ', chartData[5].timestamp)
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.tickFormat(d3.time.format('%d-%b'));
//.tickFormat(d3.time.format('%b'))
//tickFormat(d3.time.format('%W'));
//.ticks(5);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left');
var line = d3.svg.line()
.x(function(d) {
return xScale(d.timestamp);
})
.y(function(d) {
return yScale(d.value);
});
var svg = d3.select('#' + chartDomId)
.attr('width', width + margin * 2)
.attr('height', height + margin * 2)
.append('g')
.attr('transform', 'translate(' + margin + ',' + margin + ')');
chartData.forEach(function(d) {
d.timestamp = d3.time.format('%d-%b-%y').parse(d.timestamp);
d.value = +d.value;
});
yScale.domain(d3.extent(chartData, function(d) {
return d.value;
}));
svg.append('g')
.attr('class', 'axis x-axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
svg.append('g')
.attr('class', 'axis y-axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
svg.append('path')
.datum(chartData)
.attr('class', 'line')
.attr('d', line);
</script>
</body>
Upvotes: 1
Views: 3886
Reputation: 17
Since in many cases d3 forces judgments on how many ticks to place along your axes regardless of how many you specify, this is how I went about defining the specific ticks that I wanted:
var tickLabels = ['Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5'];
d3.svg.axis()
.scale(x)
.orient("bottom")
.tickValues([0, 7, 14, 21, 28])
.tickFormat(function(d, i) {
return tickLabels[i]
});
.tickValues specifies the data points (aka dates) where I want my ticks to be placed, while tickFormat changes each of those respective numbers to "Week 1", "Week 2", etc.
Upvotes: 1
Reputation: 6671
You need to find the first weekday (eg. Wednesday) from your data and set ticks according to that.
It can be achived using the following code:
var weekday = new Array(7);
weekday[0]= 'sunday';
weekday[1] = 'monday';
weekday[2] = 'tuesday';
weekday[3] = 'wednesday';
weekday[4] = 'thursday';
weekday[5] = 'friday';
weekday[6] = 'saturday';
var startDay = weekday[new Date(chartData[0].timestamp).getDay()];
var week = 1;
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.tickFormat(function() { return 'week ' + week++ })
.ticks(d3.time[startDay]);
<!DOCTYPE html>
<body>
<style>
path { fill: #CCC; }
.line { fill: none; stroke: #000; stroke-width: 5px;}
</style>
<div id="chart-container">
<svg id="chart"></svg>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var now = moment();
var chartData = [
{ timestamp: moment(now).subtract(27, 'days').format('DD-MMM-YY'), value: 40 },
{ timestamp: moment(now).subtract(25, 'days').format('DD-MMM-YY'), value: 36 },
{ timestamp: moment(now).subtract(24, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(21, 'days').format('DD-MMM-YY'), value: 35 },
{ timestamp: moment(now).subtract(20, 'days').format('DD-MMM-YY'), value: 35 },
{ timestamp: moment(now).subtract(18, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(17, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(16, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(15, 'days').format('DD-MMM-YY'), value: 32 },
{ timestamp: moment(now).subtract(13, 'days').format('DD-MMM-YY'), value: 35 },
{ timestamp: moment(now).subtract(11, 'days').format('DD-MMM-YY'), value: 31 },
{ timestamp: moment(now).subtract(10, 'days').format('DD-MMM-YY'), value: 28 },
{ timestamp: moment(now).subtract(9, 'days').format('DD-MMM-YY'), value: 32 },
{ timestamp: moment(now).subtract(8, 'days').format('DD-MMM-YY'), value: 30 },
{ timestamp: moment(now).subtract(7, 'days').format('DD-MMM-YY'), value: 33 },
{ timestamp: moment(now).subtract(6, 'days').format('DD-MMM-YY'), value: 36 }
];
//data could have a shorter date range of eg, 1 or 2 weeks
//ideally we want to still display 'week 1, 2, 3, 4' etc in the axis.
//alternatively display dates instead
// var chartData = [
// { timestamp: moment(now).subtract(27, 'days').format('DD-MMM-YY'), value: 40 },
// { timestamp: moment(now).subtract(25, 'days').format('DD-MMM-YY'), value: 36 },
// { timestamp: moment(now).subtract(24, 'days').format('DD-MMM-YY'), value: 33 },
// { timestamp: moment(now).subtract(21, 'days').format('DD-MMM-YY'), value: 35 },
// { timestamp: moment(now).subtract(20, 'days').format('DD-MMM-YY'), value: 35 }
// ];
let lastObj = chartData[chartData.length - 1];
let lastObjTimestamp = lastObj.timestamp;
let lastAndNow = moment(lastObjTimestamp).diff(now, 'days');
console.log('difference between last entry ' + lastObjTimestamp + ' and today: ' + lastAndNow);
var chartWrapperDomId = 'chart-container';
var chartDomId = 'chart';
var chartWrapperWidth = document.getElementById(chartWrapperDomId).clientWidth;
var margin = 40;
var width = chartWrapperWidth - margin;
var height = 500 - margin * 2;
var xMin = d3.time.format('%d-%b-%y').parse(chartData[0].timestamp);
var xMax = d3.time.format('%d-%b-%y').parse(chartData[chartData.length-1].timestamp);
//set the scale for the x axis
var xScale = d3.time.scale();
xScale.domain([xMin, xMax]);
xScale.range([0, width]);
var yScale = d3.scale.linear()
.range([height, 0])
.nice();
console.log('no5 ', chartData[5].timestamp)
var weekday = new Array(7);
weekday[0]= 'sunday';
weekday[1] = 'monday';
weekday[2] = 'tuesday';
weekday[3] = 'wednesday';
weekday[4] = 'thursday';
weekday[5] = 'friday';
weekday[6] = 'saturday';
var startDay = weekday[new Date(chartData[0].timestamp).getDay()];
var week = 1;
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
//.tickFormat(d3.time.format('%d-%b'));
//.tickFormat(d3.time.format('%b'))
//tickFormat(d3.time.format('%W'));
.tickFormat(function() { return 'week ' + week++ })
.ticks(d3.time[startDay]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left');
var line = d3.svg.line()
.x(function(d) {
return xScale(d.timestamp);
})
.y(function(d) {
return yScale(d.value);
});
var svg = d3.select('#' + chartDomId)
.attr('width', width + margin * 2)
.attr('height', height + margin * 2)
.append('g')
.attr('transform', 'translate(' + margin + ',' + margin + ')');
chartData.forEach(function(d) {
d.timestamp = d3.time.format('%d-%b-%y').parse(d.timestamp);
d.value = +d.value;
});
yScale.domain(d3.extent(chartData, function(d) {
return d.value;
}));
svg.append('g')
.attr('class', 'axis x-axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
svg.append('g')
.attr('class', 'axis y-axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
svg.append('path')
.datum(chartData)
.attr('class', 'line')
.attr('d', line);
</script>
</body>
Upvotes: 3