Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active January 3, 2022 06:02
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mbostock/3cfa2d1dbae2162a60203b287431382c to your computer and use it in GitHub Desktop.
Save mbostock/3cfa2d1dbae2162a60203b287431382c to your computer and use it in GitHub Desktop.
Sleep Cycles
license: gpl-3.0

This example shows how to plot daily time intervals in the style of Eric Boam’s Seven Months of Sleep. (The data here is fake.)

This graph uses a time scale to plot time-of-day along the y-axis. Time scales are normally used to plot absolute time: a specific moment on a specific day in a specific year. Here, though, we’re interested in studying the daily pattern, so the start and end times for each sleep interval are converted to offsets (elapsed times) relative to the midnight immediately preceeding the start time. Given an interval d:

var midnight = d3.utcDay.floor(d[0]),
    startOffset = d[0] - midnight,
    endOffset = d[1] - midnight;

The offsets are measured in milliseconds since that’s the JavaScript convention. To avoid inconsistencies across browser local time zones, the dates are represented as local times in the data but parsed as UTC. The resulting Date objects are inconsistent with the original dates, but the difference is irrelevant because we just want to plot the local time-of-day.

Also, this representation makes it easy to convert an offset back to an absolute time on an arbitrary day for using with a d3.scaleUtc and d3.utcFormat. The arbitrary day is the UNIX epoch:

function date(offset) {
  return new Date(offset);
}

A similar technique is used in my visualization of Eric Fischer’s Twitter feed.

#!/usr/bin/env node
var d3 = require("d3");
var hour = 36e5,
date0 = new Date(2016, 5, 1),
date1 = d3.timeMonth.offset(date0, 7),
timeFormat = d3.timeFormat("%Y-%m-%dT%H:%M:%S.%L"),
data = d3.timeDays(date0, date1).map(fakeDatum(22 * hour, 8 * hour, 0.5 * hour));
process.stdout.write(d3.csvFormat(data) + "\n");
function fakeDatum(offset, duration, deviation) {
var random = d3.randomNormal(0, deviation);
return function(date) {
return {
asleep: timeFormat(new Date(+date + random() + offset)),
awake: timeFormat(new Date(+date + random() + offset + duration))
};
};
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.data {
fill: steelblue;
}
.axis--y .tick line {
stroke: #fff;
stroke-opacity: 0.8;
}
.axis--x .tick line {
stroke: #000;
stroke-opacity: 0.25;
}
.axis .domain {
display: none;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var parseTime = d3.utcParse("%Y-%m-%dT%H:%M:%S.%L"),
formatHour = d3.utcFormat("%-I:%M %p"),
formatMonth = d3.utcFormat("%B");
var svg = d3.select("svg"),
margin = {top: 0, right: 0, bottom: 0, left: 70},
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 x = d3.scaleUtc()
.rangeRound([0, width]);
var y = d3.scaleUtc()
.domain([date(19.65 * 36e5), date(32.35 * 36e5)]) // about 7:40 PM to 8:20 AM
.rangeRound([0, height]);
var area = d3.area()
.curve(d3.curveStepAfter)
.x(function(d) { return x(d.day); })
.y0(function(d) { return y(date(d[0] - d.day)); })
.y1(function(d) { return y(date(d[1] - d.day)); });
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y)
.tickFormat(formatHour)
.tickSize(-width)
.tickPadding(10));
d3.csv("sleep.csv", type, function(error, data) {
if (error) throw error;
var date0 = data[0].day,
date1 = d3.utcDay.offset(data[data.length - 1].day, 1);
x.domain([date0, date1]);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(formatMonth)
.tickSize(-height)
.tickPadding(-10))
.selectAll("text")
.attr("text-anchor", "start")
.attr("x", 10)
.attr("dy", null);
g.insert("path", ".axis")
.datum(data.concat({day: date1, 0: 0, 1: 0})) // for step-after
.attr("class", "data")
.attr("d", area);
});
function type(d) {
d = [parseTime(d.asleep), parseTime(d.awake)];
d.day = d3.utcDay.floor(d[0]);
return d;
}
function date(offset) {
return new Date(offset);
}
</script>
asleep awake
2016-06-01T22:00:11.942 2016-06-02T05:24:56.877
2016-06-02T22:06:38.621 2016-06-03T05:01:12.771
2016-06-03T21:22:08.347 2016-06-04T06:38:30.906
2016-06-04T21:59:35.031 2016-06-05T04:46:39.798
2016-06-05T21:39:50.396 2016-06-06T04:40:52.970
2016-06-06T23:01:36.793 2016-06-07T06:46:53.771
2016-06-07T21:19:37.152 2016-06-08T05:54:52.999
2016-06-08T22:03:37.596 2016-06-09T05:55:48.665
2016-06-09T22:48:49.202 2016-06-10T05:44:16.569
2016-06-10T21:38:46.273 2016-06-11T04:49:54.359
2016-06-11T22:27:32.649 2016-06-12T06:03:21.422
2016-06-12T22:44:15.736 2016-06-13T07:09:17.746
2016-06-13T21:47:55.150 2016-06-14T06:28:54.528
2016-06-14T22:03:48.329 2016-06-15T05:21:41.680
2016-06-15T21:49:35.901 2016-06-16T05:55:36.426
2016-06-16T21:26:03.427 2016-06-17T06:35:43.980
2016-06-17T23:01:04.235 2016-06-18T06:10:02.337
2016-06-18T21:53:16.604 2016-06-19T05:56:33.018
2016-06-19T21:46:22.054 2016-06-20T06:16:53.612
2016-06-20T21:40:59.829 2016-06-21T06:13:21.603
2016-06-21T21:20:50.964 2016-06-22T05:57:31.615
2016-06-22T21:56:15.373 2016-06-23T05:47:55.715
2016-06-23T22:29:42.049 2016-06-24T05:57:21.136
2016-06-24T22:06:26.393 2016-06-25T06:48:54.859
2016-06-25T21:59:10.693 2016-06-26T06:02:39.104
2016-06-26T22:19:02.422 2016-06-27T06:09:21.681
2016-06-27T21:24:13.693 2016-06-28T05:46:32.159
2016-06-28T22:16:47.551 2016-06-29T05:30:29.142
2016-06-29T22:08:11.772 2016-06-30T05:44:53.721
2016-06-30T21:39:59.945 2016-07-01T06:35:02.050
2016-07-01T21:11:29.536 2016-07-02T06:40:16.195
2016-07-02T22:36:34.439 2016-07-03T06:04:45.694
2016-07-03T21:27:03.093 2016-07-04T06:08:32.228
2016-07-04T21:57:03.456 2016-07-05T06:03:17.918
2016-07-05T21:23:36.667 2016-07-06T05:55:27.259
2016-07-06T22:02:48.697 2016-07-07T05:16:21.445
2016-07-07T22:00:23.864 2016-07-08T05:52:17.379
2016-07-08T22:35:51.676 2016-07-09T05:57:14.687
2016-07-09T21:42:28.964 2016-07-10T06:39:58.602
2016-07-10T21:58:23.032 2016-07-11T06:24:59.331
2016-07-11T22:55:40.516 2016-07-12T06:16:50.101
2016-07-12T21:54:25.107 2016-07-13T06:20:14.051
2016-07-13T21:58:34.478 2016-07-14T05:04:28.938
2016-07-14T22:03:25.047 2016-07-15T06:24:51.000
2016-07-15T22:07:02.239 2016-07-16T05:12:10.988
2016-07-16T22:17:20.832 2016-07-17T06:46:13.246
2016-07-17T22:26:16.742 2016-07-18T05:11:16.263
2016-07-18T22:48:47.053 2016-07-19T05:58:40.052
2016-07-19T21:38:24.027 2016-07-20T05:35:41.065
2016-07-20T22:11:33.176 2016-07-21T05:31:28.638
2016-07-21T22:08:52.986 2016-07-22T06:29:55.395
2016-07-22T21:28:45.297 2016-07-23T06:36:12.098
2016-07-23T21:57:37.824 2016-07-24T05:56:38.309
2016-07-24T22:13:16.835 2016-07-25T06:22:41.693
2016-07-25T22:04:07.014 2016-07-26T06:00:58.073
2016-07-26T21:59:51.743 2016-07-27T06:10:38.707
2016-07-27T22:21:43.421 2016-07-28T05:38:59.399
2016-07-28T21:40:40.072 2016-07-29T05:33:41.253
2016-07-29T21:53:50.727 2016-07-30T05:06:52.643
2016-07-30T21:01:40.841 2016-07-31T06:25:19.526
2016-07-31T21:22:34.049 2016-08-01T06:02:11.300
2016-08-01T22:01:15.460 2016-08-02T05:28:35.733
2016-08-02T22:23:19.970 2016-08-03T06:16:38.863
2016-08-03T21:50:52.815 2016-08-04T05:41:15.186
2016-08-04T22:08:45.913 2016-08-05T05:34:47.699
2016-08-05T22:27:36.226 2016-08-06T06:10:01.000
2016-08-06T22:21:28.037 2016-08-07T06:12:16.517
2016-08-07T22:25:18.310 2016-08-08T06:41:58.419
2016-08-08T22:29:22.481 2016-08-09T06:06:01.048
2016-08-09T21:24:11.188 2016-08-10T05:13:49.806
2016-08-10T22:08:06.070 2016-08-11T06:13:29.810
2016-08-11T22:56:54.783 2016-08-12T07:03:33.333
2016-08-12T22:08:42.154 2016-08-13T06:48:09.494
2016-08-13T22:34:35.437 2016-08-14T06:02:18.112
2016-08-14T21:48:41.412 2016-08-15T06:26:20.858
2016-08-15T21:58:24.751 2016-08-16T05:34:26.954
2016-08-16T22:09:15.284 2016-08-17T06:01:32.671
2016-08-17T22:14:04.142 2016-08-18T06:27:57.498
2016-08-18T21:50:48.905 2016-08-19T06:40:35.026
2016-08-19T22:26:12.694 2016-08-20T06:03:47.044
2016-08-20T21:23:27.508 2016-08-21T04:53:22.649
2016-08-21T21:14:10.386 2016-08-22T05:45:17.094
2016-08-22T22:32:19.139 2016-08-23T05:27:47.498
2016-08-23T22:18:17.801 2016-08-24T05:43:31.429
2016-08-24T21:49:59.507 2016-08-25T06:21:35.597
2016-08-25T22:34:53.626 2016-08-26T06:23:46.190
2016-08-26T21:59:54.919 2016-08-27T05:41:28.656
2016-08-27T22:17:33.665 2016-08-28T06:22:43.512
2016-08-28T22:26:17.904 2016-08-29T05:56:46.079
2016-08-29T22:13:12.812 2016-08-30T06:32:15.552
2016-08-30T22:09:25.867 2016-08-31T06:08:44.253
2016-08-31T21:48:07.009 2016-09-01T06:28:38.149
2016-09-01T21:56:09.094 2016-09-02T06:01:00.956
2016-09-02T22:23:36.485 2016-09-03T06:16:00.035
2016-09-03T22:15:11.278 2016-09-04T06:45:39.631
2016-09-04T22:03:17.405 2016-09-05T05:37:23.027
2016-09-05T20:55:46.430 2016-09-06T06:02:23.536
2016-09-06T23:20:43.155 2016-09-07T06:21:55.625
2016-09-07T21:29:23.213 2016-09-08T05:58:51.733
2016-09-08T21:20:43.018 2016-09-09T05:59:53.674
2016-09-09T21:37:51.883 2016-09-10T05:56:27.781
2016-09-10T22:52:59.937 2016-09-11T06:39:25.814
2016-09-11T20:58:48.284 2016-09-12T05:52:10.124
2016-09-12T22:42:36.211 2016-09-13T06:20:24.174
2016-09-13T21:26:47.624 2016-09-14T06:22:45.357
2016-09-14T22:15:04.113 2016-09-15T05:58:24.014
2016-09-15T22:30:48.743 2016-09-16T06:14:29.037
2016-09-16T21:48:48.847 2016-09-17T05:44:32.082
2016-09-17T21:36:18.019 2016-09-18T05:59:07.454
2016-09-18T21:18:48.193 2016-09-19T05:33:37.613
2016-09-19T23:00:47.530 2016-09-20T05:50:25.413
2016-09-20T22:02:34.052 2016-09-21T06:15:21.956
2016-09-21T22:27:21.058 2016-09-22T06:51:16.539
2016-09-22T21:18:54.324 2016-09-23T06:08:52.213
2016-09-23T22:06:52.794 2016-09-24T06:02:55.204
2016-09-24T20:55:28.967 2016-09-25T06:01:37.374
2016-09-25T22:49:56.021 2016-09-26T05:25:23.372
2016-09-26T21:20:36.642 2016-09-27T06:27:11.296
2016-09-27T22:06:59.354 2016-09-28T05:43:29.519
2016-09-28T21:48:26.105 2016-09-29T06:36:00.514
2016-09-29T21:33:58.756 2016-09-30T06:59:53.779
2016-09-30T22:23:59.710 2016-10-01T05:41:11.553
2016-10-01T22:18:19.023 2016-10-02T06:07:46.209
2016-10-02T21:13:58.146 2016-10-03T05:40:54.638
2016-10-03T22:13:53.748 2016-10-04T06:16:40.628
2016-10-04T21:36:13.572 2016-10-05T05:46:26.505
2016-10-05T22:47:04.082 2016-10-06T06:16:54.190
2016-10-06T21:51:39.591 2016-10-07T05:43:58.235
2016-10-07T23:11:17.888 2016-10-08T05:43:40.614
2016-10-08T21:55:47.770 2016-10-09T06:05:27.475
2016-10-09T22:55:37.263 2016-10-10T07:02:18.046
2016-10-10T21:37:53.631 2016-10-11T05:30:50.230
2016-10-11T22:26:38.920 2016-10-12T06:00:50.667
2016-10-12T21:45:58.205 2016-10-13T05:58:11.630
2016-10-13T22:39:35.008 2016-10-14T06:28:17.236
2016-10-14T21:56:13.389 2016-10-15T06:13:19.767
2016-10-15T22:10:25.083 2016-10-16T07:00:16.188
2016-10-16T21:03:34.987 2016-10-17T06:13:49.564
2016-10-17T22:06:33.189 2016-10-18T05:50:43.757
2016-10-18T22:23:42.858 2016-10-19T05:16:27.125
2016-10-19T22:20:48.260 2016-10-20T05:36:20.362
2016-10-20T22:15:23.358 2016-10-21T06:13:50.057
2016-10-21T21:39:52.355 2016-10-22T06:06:02.845
2016-10-22T21:45:00.025 2016-10-23T06:03:11.832
2016-10-23T21:31:10.730 2016-10-24T06:20:06.396
2016-10-24T22:07:53.957 2016-10-25T06:33:50.358
2016-10-25T22:18:36.247 2016-10-26T05:45:12.212
2016-10-26T22:18:52.389 2016-10-27T05:59:50.223
2016-10-27T22:42:14.831 2016-10-28T06:12:57.580
2016-10-28T21:25:43.261 2016-10-29T06:40:36.375
2016-10-29T22:09:35.621 2016-10-30T04:22:28.969
2016-10-30T20:26:03.801 2016-10-31T04:49:25.885
2016-10-31T21:47:08.478 2016-11-01T05:34:39.103
2016-11-01T21:24:03.325 2016-11-02T06:02:53.806
2016-11-02T20:51:43.826 2016-11-03T06:05:49.063
2016-11-03T21:51:57.335 2016-11-04T05:48:59.983
2016-11-04T22:12:10.034 2016-11-05T05:57:18.667
2016-11-05T21:39:28.002 2016-11-06T06:00:20.210
2016-11-06T21:51:41.501 2016-11-07T06:47:06.210
2016-11-07T22:16:11.272 2016-11-08T05:48:00.335
2016-11-08T20:57:05.165 2016-11-09T05:49:30.976
2016-11-09T21:59:39.464 2016-11-10T06:11:19.588
2016-11-10T22:13:37.989 2016-11-11T05:57:01.287
2016-11-11T21:38:38.657 2016-11-12T06:31:12.856
2016-11-12T21:58:43.070 2016-11-13T07:21:21.669
2016-11-13T22:08:45.370 2016-11-14T05:29:38.375
2016-11-14T22:19:56.174 2016-11-15T06:29:30.474
2016-11-15T22:07:45.324 2016-11-16T05:33:20.261
2016-11-16T21:58:59.960 2016-11-17T06:32:16.830
2016-11-17T21:52:51.907 2016-11-18T06:10:07.543
2016-11-18T22:51:02.533 2016-11-19T05:43:44.644
2016-11-19T21:03:28.845 2016-11-20T06:30:22.160
2016-11-20T22:00:11.251 2016-11-21T06:11:42.886
2016-11-21T21:07:42.006 2016-11-22T05:15:17.569
2016-11-22T22:16:33.066 2016-11-23T05:35:48.042
2016-11-23T21:21:42.433 2016-11-24T06:43:50.439
2016-11-24T22:10:16.479 2016-11-25T05:24:14.501
2016-11-25T21:21:35.528 2016-11-26T05:46:37.247
2016-11-26T21:54:18.382 2016-11-27T05:22:14.885
2016-11-27T20:56:51.970 2016-11-28T06:24:23.997
2016-11-28T22:31:35.756 2016-11-29T05:38:28.878
2016-11-29T22:52:15.888 2016-11-30T05:33:58.603
2016-11-30T21:48:29.412 2016-12-01T05:46:17.146
2016-12-01T22:07:18.586 2016-12-02T06:35:23.553
2016-12-02T21:38:31.536 2016-12-03T06:33:03.987
2016-12-03T21:26:33.781 2016-12-04T05:27:06.236
2016-12-04T21:54:56.851 2016-12-05T06:01:23.206
2016-12-05T22:09:48.931 2016-12-06T06:23:59.403
2016-12-06T21:09:32.060 2016-12-07T06:26:13.400
2016-12-07T21:52:09.791 2016-12-08T05:30:50.051
2016-12-08T22:07:48.418 2016-12-09T05:44:54.568
2016-12-09T22:19:14.730 2016-12-10T06:03:07.832
2016-12-10T21:47:32.866 2016-12-11T05:38:37.218
2016-12-11T21:29:06.277 2016-12-12T06:46:29.007
2016-12-12T21:45:25.863 2016-12-13T05:54:21.639
2016-12-13T21:42:07.623 2016-12-14T05:59:25.558
2016-12-14T22:13:11.789 2016-12-15T06:06:13.621
2016-12-15T22:54:39.228 2016-12-16T06:18:43.216
2016-12-16T22:17:29.293 2016-12-17T05:25:36.101
2016-12-17T22:05:18.674 2016-12-18T06:06:02.676
2016-12-18T22:19:10.397 2016-12-19T06:18:34.049
2016-12-19T21:33:16.023 2016-12-20T06:25:11.988
2016-12-20T22:17:57.257 2016-12-21T05:47:03.158
2016-12-21T22:05:47.787 2016-12-22T06:17:23.136
2016-12-22T21:15:18.084 2016-12-23T06:30:54.819
2016-12-23T22:20:37.216 2016-12-24T06:04:28.468
2016-12-24T23:24:00.663 2016-12-25T05:56:18.096
2016-12-25T22:27:47.179 2016-12-26T05:48:14.225
2016-12-26T22:40:16.737 2016-12-27T05:51:42.063
2016-12-27T22:01:11.393 2016-12-28T06:32:49.421
2016-12-28T22:17:14.517 2016-12-29T05:37:40.144
2016-12-29T21:21:08.582 2016-12-30T05:26:38.677
2016-12-30T21:45:32.471 2016-12-31T06:15:52.291
2016-12-31T21:32:12.126 2017-01-01T06:43:38.269
@Fil
Copy link

Fil commented Jun 26, 2016

Unfortunately the example doesn't work well (or indeed at all) outside the California timezone. With my OS set in Europe TZ, the blue bars are displayed much higher (so high in fact that they don't appear on-screen if I don't tweak the y range). If I switch to New York TZ, then some bars appear a little, and others not.

Here is a block that apparently fixes the graph: https://bl.ocks.org/Fil/f2348baf08f341f8ee4ee24c1c2e0651

However the fix is not pretty. The problem is that the code uses some functions which are meant for the viewer's local time (eg. d3.timeDay.floor) in order to compute the sleeper's local day (say: the midnight that precedes sleeping time).

BTW this also means that the current code fails (even in California TZ) when the person works on D3 past midnight and goes to sleep at e.g. 1am. But that second problem is easily fixed by having
d.day = d3.timeDay.floor(d3.timeHour.offset(d[0],-12))
instead of
d.day = d3.timeDay.floor(d[0])
(with that, going to bed before 12am is associated to the previous day's night)

For the TZ problem, I don't think my solution is right. I'm fiddling with a var fixtz that add the timezones difference to all dates. There must be something better — but at that point I think we need to expand the API so that .floor() and the like can be set to work relative to a specific TZ, and I'm giving up :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment