5

I'm building a timeline visualisation with d3 where the domain can vary from a couple of days to several decades. I'm using a d3 time scale and axis like this:

var timeScale = d3.time.scale()
    .domain([firstEvent, lastEvent])
    .range([leftPadding, w - rightPadding]);
var timeAxis = d3.svg.axis()
    .scale(timeScale)
    .orient("bottom");
timeAxis.ticks(5);

Since the domain is so variable, it is convenient to use ticks(x) which will automatically choose the tick format. My problem is that in some cases, the year is not shown, which is crucial. My idea was to check the tick format after the axis is created, and if it doesn't contain a year, show it manually next to the axis. However, I haven't been able to get the scale's tick format; using timeScale.tickFormat() just returns a function. How can I solve this problem?

Alexis Tyler
  • 1,394
  • 6
  • 30
  • 48
smcs
  • 1,772
  • 3
  • 18
  • 48
  • You can call `.ticks()` without any arguments to get the tick locations. – Lars Kotthoff Dec 16 '15 at 01:20
  • @LarsKotthoff Yes, but `.ticks()` returns the unformatted locations, so one can't tell if the year will be displayed in any of their labels as far as I can see. – smcs Dec 16 '15 at 01:49

2 Answers2

4

How about a simple regex test?

var hasYear = false;
var re = /^\d{4}$/;
// get the ticks text    
d3.selectAll('.x>.tick>text').each(function(d){
  // does the text look like a year?
  var txt = this.textContent;
  if (re.test(txt)){
    hasYear = true;
  }
});
console.log(hasYear);

Used this sample code and played around with various ranges:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.axis text {
  font: 10px sans-serif;
}

.axis line,
.axis path {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>

var margin = {top: 250, right: 40, bottom: 250, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.time.scale()
    .domain([new Date(2013, 12, 2), new Date(2013, 12, 6)])
    .range([0, width]);

var xAxis = d3.svg.axis()
    .scale(x)
    .ticks(5);

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);
    
xAxis.ticks().some(function(d){
  return /^\d{4}$/.exec("3434")
})

var hasYear = false;
var re = /^\d{4}$/;
d3.selectAll('.x>.tick>text').each(function(d){
  var txt = this.textContent;
  if (re.test(txt)){
    hasYear = true;
  }
});
console.log(hasYear);


</script>
Mark
  • 106,305
  • 20
  • 172
  • 230
1

D3's time axis formatting checks whether any of the dates displayed on the ticks is on midnight, first of January to display only the year. You can check whether a full year will be displayed by getting the ticks and then checking this condition:

function isYear(t) {
  return +t == +(new Date(t.getFullYear(), 0, 1, 0, 0, 0, 0));
}
var ticks = timeScale.ticks();
var willDisplayYear = ticks.some(isYear);

Complete demo for both cases here.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • Thanks for your answer, Lars. Do you have a source for the way the time axis decides whether or not a year is displayed? It sounds very unlikely to me, I think it just depends on the range of the axis. The code gives me an error--`t.getFullYear` is not a function. Apparently it needs to be called from a Date object. I created one first: var date = new Date(t); new Date(+t) == new Date(date.getFullYear(), 0, 0, 0, 0, 0, 0); The code returns `false` even though a year is present in the ticks. I've marked Mark's answer as correct as it seems like a more robust solution either way. – smcs Dec 17 '15 at 00:18
  • Ah you're right -- you need to use the scale for this. I'll fix the answer. – Lars Kotthoff Dec 17 '15 at 00:25
  • It still returns `false` though even when there are full years in the tick labels. Could have to do with the time zones. – smcs Dec 17 '15 at 03:04
  • Hmm, interesting. Yes, it's probably related to that. – Lars Kotthoff Dec 17 '15 at 03:20
  • Ok, I had a logic bug in there -- the first day of the month is 1 and not 0. Nothing to do with time zones. I've fixed the answer and added a link to a complete example. – Lars Kotthoff Dec 17 '15 at 04:54
  • Ah that makes sense! So this is another solution. But could it return false positives? Consider this scenario: the timeline domain spans from 31.12. 23:50 to 1.1. 00:10. Could in this or a similar scenario a tick be generated that sits exactly at midnight but does not display the year but e.g. the time? In other words, is `Date(t.getFullYear(), 0, 1, 0, 0, 0, 0)` with certainty exclusive to year labels? – smcs Dec 17 '15 at 05:19
  • If you use the default time format yes, because it's exactly the same check that the time format is using to figure out whether to display a year. So in the case you've outlined the year would actually be shown. – Lars Kotthoff Dec 17 '15 at 05:21
  • Lars, do you know the conditions for the other tick formats as well? I am thinking about conditional font sizes. – smcs Jan 07 '16 at 01:54
  • See https://stackoverflow.com/questions/11801250/custom-multi-scale-time-formats-in-d3 – Lars Kotthoff Jan 07 '16 at 03:09