All examples By author By category About

jebeck

all about D3 time scales

all about D3 time scales

The Problem

At Tidepool, we have need to plot data that comes out of diabetes devices that are completely naïve with respect to timezone information. All we have in our data is the timestamp reflecting the time displayed on the user's device at the time of the event in question. We call this deviceTime, and we store it in ISO 8601 format without a timezone offset - i.e., YYYY-MM-DDTHH:MM:SS. Because they are completely timezone-naïve, diabetes devices do not automatically make the switch from standard to daylight time and vice versa (where such transformations are the norm). Furthermore, because (at least in the United States) the change from daylight to standard time (or, again, vice versa) occurs in the middle of the night - specifically, at 2 a.m. - we can almost assume that very few or no users of diabetes devices change the time on their devices at the precise moment when this transformation is called for. Thus, we can assume that most - if not all - users will generate some datapoints that are technically undefined on a timezone-aware scale. For example, 2:30 a.m. on March 9th, 2014 is undefined (for American locales) because the switch from standard to daylight savings time happens at the turnover from 1:59 a.m. directly to 3:00 a.m. in the next minute.

Until we have timezone-aware data coming out of diabetes devices, we want to present a user's data to them as transparently as possible. Plotting diabetes device data on a scale that automatically accounts for the switches to and from daylight savings time is not transparent: two datapoints - at 1:30 a.m. and 2:30 a.m. on March 9th, 2014, for example - will be plotted in the exact same place even though the user saw (or would have seen, had they not been asleep) two different timestamps on their device with an hour of time passing between glances at the device. On the other hand, if we plot the data we retrieve from diabetes devices on a scale that knows nothing of daylight savings time, then when the user eventually changes the time on their device to reflect the switch to or from daylight time, they will see an overlap or a gap in their data that directly corresponds with their manipulation of the device's settings and thus remains perfectly transparent. (The same applies when a user travels across timezones and adjusts their device accordingly.)

The Solution

D3 provides two types of time scales: UTC (d3.time.scale.utc()) and non-UTC (d3.time.scale()). For the purposes of this document, the most important feature of UTC time is that it does not undergo any transformation to or from daylight savings time. This feature makes D3's UTC scale the only appropriate scale for us to use when plotting timezone-naïve diabetes device data, at least if we are to maintain the transparency described above.

In the vast majority of cases, we are not, in fact, plotting UTC data; we are merely exploiting - in a somewhat kludgey fashion - the fact that UTC does not undergo any scheduled transformations but rather retains the same timezone offset (namely, 0) all year round. (Because pretending we are plotting UTC data when we really are not is a somewhat ugly solution, we choose to keep the code that applies the transformation to "UTC" - really just appending Z, the ISO 8601 shorthand for the UTC timezone offset - segregated from the rest of our code.)

As will be discussed further below, there is a second reason for employing D3's UTC scales to plot our data, and that is to ensure consistent results across different browsers and when viewing data generated in one timezone (e.g., Pacific) in another (e.g., Eastern).

Examples

This gist contains three examples of D3 time scales that - at least in theory - share a common domain and range.

The domain for the scale is March 8th, 2014 at noon to March 10th, 2014 at midnight. We chose these dates to highlight the change from standard to daylight time that occurred on March 9th, 2014 at 2 a.m.

The range for each scale is 0 to the width of the SVG.

tideline's current implementation

Tideline's current implementation of time scales first applies the transformation of appending Z to the timezone-naïve ISO 8601 (without a timezone offset) deviceTime that is present in the data. We use d3.time.scale.utc() and set the domain like so:

.domain([new Date('2014-03-08T12:00:00.000Z'), new Date('2014-03-10T00:00:00.000Z')])

using JavaScript's built-in Date constructor.

without UTC

So, what happens if we don't apply the append Z transformation and don't use a UTC scale? The second example displays the result of using d3.time.scale() and setting the domain like so:

.domain([new Date('2014-03-08T12:00:00'), new Date('2014-03-10T00:00:00')])

still using JavaScript's Date constructor.

Several things to note here:

D3's suggested solution for the JavaScript Date constructor problem

D3 is aware of the JavaScript Date constructor implementation difference between browsers and provides an alternative to using this constructor in order to ensure consistent results across browsers. (Unfortunately, this advice is given neither in D3's time scales documentation nor in the documentation for time formatting, so I only discovered it recently, via this discussion on GitHub.)

This example was produced using a D3 time-formatting function:

var timeFormat = d3.time.format('%Y-%m-%dT%H:%M:%S');

which will return a cross-browser-consistent Date object when used to set the domain of the scale:

.domain([timeFormat.parse('2014-03-08T12:00:00'), timeFormat.parse('2014-03-10T00:00:00')])

The append Z trick to produce "UTC" date strings is no longer necessary here, but the same undesirable (if correct) foreshortening of the time span between midnight and 3 a.m. on March 9th occurs and results in overlapping data at the switch to DST: Overlap

In addition, although the axis itself is identical across all three browsers under consideration here, we don't actually achieve full cross-browser consistency:

achieving #1 with D3's alternative to the Date constructor

In fact, if we want to ensure cross-browser consistency and remove the foreshortening of the timespan between midnight and 3 a.m. on March 9th, the only way to do it seems to be a D3 notational variant of the current tideline implementation using d3.time.scale.utc() again. Namely, using a time format specification for parsing date strings that assumes the appending of a UTC timezone offset (here +0000, since D3 doesn't appear to include the shorthand Z as a proper offset when parsing timezone offsets with the %Z format specification option):

var timeFormat2 = d3.time.format('%Y-%m-%dT%H:%M:%S%Z');

And setting the domain like so:

.domain([timeFormat2.parse('2014-03-08T12:00:00+0000'), timeFormat2.parse('2014-03-10T00:00:00+0000')])