Topic 6.3
Time Scales
In one line
A time scale maps dates to positions and picks calendar-aware ticks (months, weeks, days, hours). The hard part is time itself: scaleTime follows the viewer's clock and scaleUtc follows UTC, and a day is not always 24 hours.
Think of it like this
A wall calendar. Months have different lengths, but each month gets one page. A calendar does not put a mark every 30 days, because people think in months, weeks and days. A time axis works the same way: its ticks sit on calendar boundaries, not on equal numbers of milliseconds.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- UTC
- The world's reference time: no summer time, the same everywhere. Written with a Z at the end: 2025-03-30T00:00:00Z.
- time zone
- A region's rule for local clock time, such as Atlantic/Azores (UTC−1 in winter, UTC+0 in summer).
- summer time (DST)
- Moving clocks forward in spring and back in autumn. Days then last 23 or 25 hours.
- instant
- One exact moment, the same for everyone, stored as milliseconds since 1970.
- interval
- A calendar unit D3 can step by: d3.utcDay, d3.utcWeek, d3.utcMonth.
Step by step
01Dates in, pixels out
The first example builds scaleUtc over the year's first and last dates. 18 July lands at 272 pixels of 500, and invert(250) gives back the middle of the year as a Date: 2 July.
02The span picks the step
Asked for 10 ticks, a year gives 12 monthly ticks (the first gap, 744 hours, is January's 31 days). Asked for 5, it gives four 3-month ticks. A fortnight gives daily ticks, one day gives 3-hour ticks. The wanted count is only a hint, as in Topic 6.1.
03The clock that skips an hour
The second example asks the Azores clock directly. At 00:59 UTC on 30 March it reads 23:59 (UTC−1). One minute later it reads 01:00 (UTC+0). It then measures each local day: 23 hours for 30 March, 25 hours for 26 October, 24 for the rest.
04The fix for daily data
scaleUtc ticks are always 24 hours apart: the example prints 24 24 24 24 24 for the same five days. Because daily rows are UTC midnights, UTC ticks and rows line up in every time zone.
Try it yourself
- 1
Ask for fewer ticks
In the first example change
show("year, count 10", ...)to count 3. The target gap is about 4 months, so the nearest step is 3 months and you get the same four ticks as with count 5. The list has only 18 steps, so many counts give the same axis. - 2
Pick the interval yourself
Replace
x.ticks(d3.utcMonth.every(3))withx.ticks(d3.utcMonth.every(2)). You get January, March, May, July, September and November: 2025-01 2025-03 2025-05 2025-07 2025-09 2025-11. Passing an interval skips the automatic choice.
Code & diagrams
Everything here uses UTC, so the printed values are the same on every computer.
Console
(nothing was printed)Expected console output
domain: [ 2025-01-01T00:00:00.000Z, 2025-12-31T00:00:00.000Z ]
18 July -> 272.0 px invert(250) -> 2025-07-02T00:00:00.000Z
year, count 10 12 ticks, first gap 744 h, labels: 2025 | February | March
year, count 5 4 ticks, first gap 2160 h, labels: 2025 | April | July
two weeks 15 ticks, first gap 24 h, labels: June | Mon 02 | Tue 03
one day 9 ticks, first gap 3 h, labels: June | 03 AM | 06 AM
every 3 months: 2025-01 2025-04 2025-07 2025-10
nice(): 2025-03-01 to 2025-11-01
nice(week): 2025-03-09 to 2025-10-12
padded 3 days: 2024-12-29 to 2026-01-03
domain from text: [ Invalid Date, Invalid Date ] -> NaN
text given to a Date domain: undefinedEvery line names its time zone, so this prints the same everywhere. scaleTime would print what your own clock does, so the course never logs it.
Preview
Console
Nothing logged yet.Expected console output
the instant: 2025-03-30T00:00:00.000Z
UTC writes it as 30 Mar
Asia/Kolkata writes it as 30 Mar
Pacific/Auckland writes it as 30 Mar
America/New_York writes it as 29 Mar
Atlantic/Azores writes it as 29 Mar
clock at 00:59Z on 30 March: 23:59 GMT-1 | at 01:00Z: 01:00 GMT+0
clock at 00:59Z on 26 October: 00:59 GMT+0 | at 01:00Z: 00:00 GMT-1
28 Mar: 24 h | 29 Mar: 24 h | 30 Mar: 23 h | 31 Mar: 24 h | 24 Oct: 24 h | 25 Oct: 24 h | 26 Oct: 25 h
scaleUtc day ticks, hours apart: 24 24 24 24 24Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Build the domain from date text
In the first example replace .domain(d3.extent(rows, (d) => d.date)) with .domain(["2025-01-01", "2025-12-31"]) and run it.
Myth vs fact
Myth
A day always has 24 hours.
Fact
Not in a zone with summer time: 30 March 2025 lasted 23 hours in Ardenvale and 26 October lasted 25.
Myth
scaleTime and scaleUtc differ only in speed.
Fact
They place the ticks on different midnights. For data whose rows are UTC days, scaleTime puts ticks hours away from the rows in most zones.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Local-time functions (
d3.timeDay,scaleTime) use the browser's zone, so the same code draws different ticks in Lisbon and New York, and tests that log them fail on another machine. This is why the course logs only UTC values or values with an explicittimeZone. - ▸
D3 ships only UTC and viewer-local intervals, no named-zone ones such as "Atlantic/Azores midnight". You can build one with
d3.timeInterval(floor, offset)(the same helper D3 uses for its own) and pass it toscale.ticks(interval); thefloorfunction must find the zone's midnight, which is the hard part (the example'sstartOfDayis a slow way to do it).
Remember this
- 1
The problem this solves: a date is a number of milliseconds, but a good axis must say "1 March", not "1740787200000", and must place its marks on the first of each month even though months are 28 to 31 days long. A time scale does both. The trouble is that "first of the month" and "midnight" depend on which time zone you mean.
- 2
**Two scales:
scaleTimeandscaleUtc.** (Recall from Topic 6.1: a scale has a domain, a range,ticks(),nice()andinvert(); the time scales keep all of them, withDateobjects in the domain.) They have the same methods.scaleTimerounds to the viewer's local midnights.scaleUtcrounds to UTC midnights.d3.autoTypeturns2025-06-01into midnight UTC, soscaleUtcputs every daily mark exactly on a tick, in every viewer's time zone. WithscaleTime, a viewer in New York gets ticks at 04:00 UTC in summer and 05:00 UTC in winter, so a label made with a local formatter can show the day before the row's own date (the example in this topic shows29 Marfor a row dated 30 March). - 3
How time ticks are chosen. D3 has a fixed list of 18 steps: 1, 5, 15 and 30 seconds, 1, 5, 15 and 30 minutes, 1, 3, 6 and 12 hours, 1 and 2 days, 1 week, 1 and 3 months, 1 year. It divides the span by the wanted count and picks the step nearest to the result by ratio. A year with count 10 gives monthly ticks (12), and with count 5 gives three-month ticks (4). Beyond a year it falls back to the 1-2-5 rule on years. Weeks start on Sunday by default. Pass an interval to choose yourself:
d3.utcMondaygives every Monday, andd3.utcMonth.every(2)gives every second month. - 4
Tick labels depend on the tick's level. The default
tickFormatlooks at each tick on its own: a tick at midnight on 1 January prints2025, a first of a month prints the month name (February), a Sunday printsJun 08, other daysMon 02, and an hour prints03 AM. This is why a year axis starts with2025and then readsFebruary,March. Pass a specifier (tickFormat(10, "%b")) to use one format for all ticks. - 5
A day is not always 24 hours. In Ardenvale's zone, Atlantic/Azores, clocks went forward on Sunday 30 March 2025: at 01:00 UTC the clock jumped from 23:59 (UTC−1) to 01:00 (UTC+0), so local midnight of 30 March never happened and the day lasted 23 hours. On 26 October clocks went back at 01:00 UTC, and the day lasted 25 hours. Local-time ticks across these days are 23 or 25 hours apart, and a chart that assumes 24 hours is off by an hour.
Explain it without notes
What is the difference between scaleTime and scaleUtc, and which fits daily.csv?
How does D3 choose between monthly and 3-monthly ticks?
Why was 30 March 2025 only 23 hours long in Ardenvale?
Practice
Make a scale for June 2025 (UTC) over 480 pixels and print the pixel for 15 June at noon. Then print the ticks for every Monday as ISO dates.
Write dayLabel(date) that prints a daily row's date as 30 Mar in every viewer's time zone.
Trade-offs
- ↔
UTC scales line daily data up exactly, but their ticks fall on UTC midnights, which are not the viewer's midnights. For a chart of instants (trip starts) the viewer's local time is what readers expect.
Done when you can
I can create a scaleUtc from Dates and say what it returns for text.
I can say how D3 picks a time tick step.
I can use nice(), ticks(interval) and utcDay.offset.
I can explain why a local day can last 23 or 25 hours.