Skip to content

Commit 0e42a9a

Browse files
committed
Fix d3.time.day for Firefox in Europe/London.
The UK was on permanent BST from February 1968 until October 1971. This is handled differently by Chrome and Firefox. Chrome appears to apply "current" DST rules, hence new Date(0) is 00:00. Firefox thinks it is in BST, and thus new Date(0) is 01:00, and in fact there appears to be no such thing as a local time of 00:00 on Jan 1st, 1970. This difference in interpretation caused d3.time.day.floor to return 01:00 instead of 00:00 due to internally using new Date(1970, 0), which is equivalent to new Date(0). This led to the discovery of other timezone-related bugs, where a DST change caused a day to start at 01:00 instead of 00:00. Up until now, we had been assuming that every day had a midnight, but this is not the case, e.g., Fri Mar 28 2014 in Asia/Amman. This assumption was present in d3.time.scale, where a date with a non-zero hour was being formatted as "%I %p", even if it was a date returned by d3.time.day.floor. To fix the Firefox issue with the time being 01:00 at the epoch, we use date.setHours(0, 0, 0, 0) to ensure a midnight is returned if it exists. We then check to see if the resulting date is on a day boundary, by subtracting a millisecond and comparing the year, month and day fields. If not, we perform a binary search to find the true boundary, which fixes the aforementioned example Asia/Amman problem. Lastly, d3.time.scale was adjusted to check for boundaries using the millisecond decrement technique, rather than checking for non-zero fields.
1 parent 41bfffe commit 0e42a9a

5 files changed

Lines changed: 95 additions & 63 deletions

File tree

d3.js

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8289,8 +8289,14 @@ d3 = function() {
82898289
d3.time.years = d3.time.year.range;
82908290
d3.time.years.utc = d3.time.year.utc.range;
82918291
d3.time.day = d3_time_interval(function(date) {
8292-
var day = new d3_time(2e3, 0);
8293-
day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
8292+
var hi = +date, day = new d3_time(hi);
8293+
day.setHours(0, 0, 0, 0);
8294+
var lo = +day, day0 = new d3_time(lo - 1);
8295+
while (d3_time_dayEqual(day0, day) && lo < hi) {
8296+
var mid = Math.floor(.5 * (lo + hi));
8297+
day.setTime(mid), day0.setTime(mid - 1);
8298+
if (d3_time_dayEqual(day, date)) hi = mid; else lo = mid + 1;
8299+
}
82948300
return day;
82958301
}, function(date, offset) {
82968302
date.setDate(date.getDate() + offset);
@@ -8303,6 +8309,9 @@ d3 = function() {
83038309
var year = d3.time.year(date);
83048310
return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5);
83058311
};
8312+
function d3_time_dayEqual(a, b) {
8313+
return a.getDate() === b.getDate() && a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
8314+
}
83068315
d3_time_daySymbols.forEach(function(day, i) {
83078316
day = day.toLowerCase();
83088317
i = 7 - i;
@@ -8689,17 +8698,19 @@ d3 = function() {
86898698
return scale.domain(d3_scale_nice(scale.domain(), m));
86908699
};
86918700
scale.ticks = function(m, k) {
8692-
var extent = d3_scaleExtent(scale.domain());
8701+
var extent = d3_scaleExtent(scale.domain()), floor;
86938702
if (typeof m !== "function") {
86948703
var span = extent[1] - extent[0], target = span / m, i = d3.bisect(d3_time_scaleSteps, target);
86958704
if (i == d3_time_scaleSteps.length) return methods.year(extent, m);
86968705
if (!i) return linear.ticks(m).map(d3_time_scaleDate);
86978706
if (target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target) --i;
86988707
m = methods[i];
86998708
k = m[1];
8709+
floor = m[0].floor;
87008710
m = m[0].range;
87018711
}
8702-
return m(extent[0], new Date(+extent[1] + 1), k);
8712+
var ticks = m(extent[0], new Date(+extent[1] + 1), k);
8713+
return floor ? ticks.map(floor) : ticks;
87038714
};
87048715
scale.tickFormat = function() {
87058716
return format;
@@ -8714,8 +8725,9 @@ d3 = function() {
87148725
}
87158726
function d3_time_scaleFormat(formats) {
87168727
return function(date) {
8717-
var i = formats.length - 1, f = formats[i];
8718-
while (!f[1](date)) f = formats[--i];
8728+
d3_time_scaleBoundaryDate.setTime(date - 1);
8729+
var i = 0, f = formats[i];
8730+
while (!f[1](date, d3_time_scaleBoundaryDate)) f = formats[++i];
87198731
return f[0](date);
87208732
};
87218733
}
@@ -8730,21 +8742,22 @@ d3 = function() {
87308742
}
87318743
var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ];
87328744
var d3_time_scaleLocalMethods = [ [ d3.time.second, 1 ], [ d3.time.second, 5 ], [ d3.time.second, 15 ], [ d3.time.second, 30 ], [ d3.time.minute, 1 ], [ d3.time.minute, 5 ], [ d3.time.minute, 15 ], [ d3.time.minute, 30 ], [ d3.time.hour, 1 ], [ d3.time.hour, 3 ], [ d3.time.hour, 6 ], [ d3.time.hour, 12 ], [ d3.time.day, 1 ], [ d3.time.day, 2 ], [ d3.time.week, 1 ], [ d3.time.month, 1 ], [ d3.time.month, 3 ], [ d3.time.year, 1 ] ];
8733-
var d3_time_scaleLocalFormats = [ [ d3.time.format("%Y"), d3_true ], [ d3.time.format("%B"), function(d) {
8734-
return d.getMonth();
8735-
} ], [ d3.time.format("%b %d"), function(d) {
8736-
return d.getDate() != 1;
8737-
} ], [ d3.time.format("%a %d"), function(d) {
8738-
return d.getDay() && d.getDate() != 1;
8739-
} ], [ d3.time.format("%I %p"), function(d) {
8740-
return d.getHours();
8741-
} ], [ d3.time.format("%I:%M"), function(d) {
8742-
return d.getMinutes();
8743-
} ], [ d3.time.format(":%S"), function(d) {
8744-
return d.getSeconds();
8745-
} ], [ d3.time.format(".%L"), function(d) {
8746-
return d.getMilliseconds();
8747-
} ] ];
8745+
var d3_time_scaleBoundaryDate = new Date(0);
8746+
var d3_time_scaleLocalFormats = [ [ d3.time.format("%Y"), function(d, a) {
8747+
return a.getFullYear() !== d.getFullYear();
8748+
} ], [ d3.time.format("%B"), function(d, a) {
8749+
return a.getMonth() !== d.getMonth() && d.getDay();
8750+
} ], [ d3.time.format("%b %d"), function(d, a) {
8751+
return a.getDate() !== d.getDate() && !d.getDay();
8752+
} ], [ d3.time.format("%a %d"), function(d, a) {
8753+
return a.getDate() !== d.getDate();
8754+
} ], [ d3.time.format("%I %p"), function(d, a) {
8755+
return a.getHours() !== d.getHours();
8756+
} ], [ d3.time.format("%I:%M"), function(d, a) {
8757+
return a.getMinutes() !== d.getMinutes();
8758+
} ], [ d3.time.format(":%S"), function(d, a) {
8759+
return a.getSeconds() !== d.getSeconds();
8760+
} ], [ d3.time.format(".%L"), d3_true ] ];
87488761
var d3_time_scaleLinear = d3.scale.linear(), d3_time_scaleLocalFormat = d3_time_scaleFormat(d3_time_scaleLocalFormats);
87498762
d3_time_scaleLocalMethods.year = function(extent, m) {
87508763
return d3_time_scaleLinear.domain(extent.map(d3_time_scaleGetYear)).ticks(m).map(d3_time_scaleSetYear);
@@ -8755,21 +8768,21 @@ d3 = function() {
87558768
var d3_time_scaleUTCMethods = d3_time_scaleLocalMethods.map(function(m) {
87568769
return [ m[0].utc, m[1] ];
87578770
});
8758-
var d3_time_scaleUTCFormats = [ [ d3.time.format.utc("%Y"), d3_true ], [ d3.time.format.utc("%B"), function(d) {
8759-
return d.getUTCMonth();
8760-
} ], [ d3.time.format.utc("%b %d"), function(d) {
8761-
return d.getUTCDate() != 1;
8762-
} ], [ d3.time.format.utc("%a %d"), function(d) {
8763-
return d.getUTCDay() && d.getUTCDate() != 1;
8764-
} ], [ d3.time.format.utc("%I %p"), function(d) {
8765-
return d.getUTCHours();
8766-
} ], [ d3.time.format.utc("%I:%M"), function(d) {
8767-
return d.getUTCMinutes();
8768-
} ], [ d3.time.format.utc(":%S"), function(d) {
8769-
return d.getUTCSeconds();
8770-
} ], [ d3.time.format.utc(".%L"), function(d) {
8771-
return d.getUTCMilliseconds();
8772-
} ] ];
8771+
var d3_time_scaleUTCFormats = [ [ d3.time.format.utc("%Y"), function(d, a) {
8772+
return a.getUTCFullYear() !== d.getUTCFullYear();
8773+
} ], [ d3.time.format.utc("%B"), function(d, a) {
8774+
return a.getUTCMonth() !== d.getUTCMonth() && d.getUTCDay();
8775+
} ], [ d3.time.format.utc("%b %d"), function(d, a) {
8776+
return a.getUTCDate() !== d.getUTCDate() && !d.getUTCDay();
8777+
} ], [ d3.time.format.utc("%a %d"), function(d, a) {
8778+
return a.getUTCDate() !== d.getUTCDate();
8779+
} ], [ d3.time.format.utc("%I %p"), function(d, a) {
8780+
return a.getUTCHours() !== d.getUTCHours();
8781+
} ], [ d3.time.format.utc("%I:%M"), function(d, a) {
8782+
return a.getUTCMinutes() !== d.getUTCMinutes();
8783+
} ], [ d3.time.format.utc(":%S"), function(d, a) {
8784+
return a.getUTCSeconds() !== d.getUTCSeconds();
8785+
} ], [ d3.time.format.utc(".%L"), d3_true ] ];
87738786
var d3_time_scaleUTCFormat = d3_time_scaleFormat(d3_time_scaleUTCFormats);
87748787
function d3_time_scaleUTCSetYear(y) {
87758788
var d = new Date(Date.UTC(y, 0, 1));

d3.min.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/time/day.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@ import "time";
33
import "year";
44

55
d3.time.day = d3_time_interval(function(date) {
6-
var day = new d3_time(2000, 0);
7-
day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
6+
var hi = +date,
7+
day = new d3_time(hi);
8+
day.setHours(0, 0, 0, 0);
9+
var lo = +day,
10+
day0 = new d3_time(lo - 1);
11+
while (d3_time_dayEqual(day0, day) && lo < hi) {
12+
var mid = Math.floor(.5 * (lo + hi));
13+
day.setTime(mid), day0.setTime(mid - 1);
14+
if (d3_time_dayEqual(day, date)) hi = mid;
15+
else lo = mid + 1;
16+
}
817
return day;
918
}, function(date, offset) {
1019
date.setDate(date.getDate() + offset);
@@ -19,3 +28,7 @@ d3.time.dayOfYear = function(date) {
1928
var year = d3.time.year(date);
2029
return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5);
2130
};
31+
32+
function d3_time_dayEqual(a, b) {
33+
return a.getDate() === b.getDate() && a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
34+
}

src/time/scale-utc.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ var d3_time_scaleUTCMethods = d3_time_scaleLocalMethods.map(function(m) {
99
});
1010

1111
var d3_time_scaleUTCFormats = [
12-
[d3.time.format.utc("%Y"), d3_true],
13-
[d3.time.format.utc("%B"), function(d) { return d.getUTCMonth(); }],
14-
[d3.time.format.utc("%b %d"), function(d) { return d.getUTCDate() != 1; }],
15-
[d3.time.format.utc("%a %d"), function(d) { return d.getUTCDay() && d.getUTCDate() != 1; }],
16-
[d3.time.format.utc("%I %p"), function(d) { return d.getUTCHours(); }],
17-
[d3.time.format.utc("%I:%M"), function(d) { return d.getUTCMinutes(); }],
18-
[d3.time.format.utc(":%S"), function(d) { return d.getUTCSeconds(); }],
19-
[d3.time.format.utc(".%L"), function(d) { return d.getUTCMilliseconds(); }]
12+
[d3.time.format.utc("%Y"), function(d, a) { return a.getUTCFullYear() !== d.getUTCFullYear(); }],
13+
[d3.time.format.utc("%B"), function(d, a) { return a.getUTCMonth() !== d.getUTCMonth() && d.getUTCDay(); }],
14+
[d3.time.format.utc("%b %d"), function(d, a) { return a.getUTCDate() !== d.getUTCDate() && !d.getUTCDay(); }],
15+
[d3.time.format.utc("%a %d"), function(d, a) { return a.getUTCDate() !== d.getUTCDate(); }],
16+
[d3.time.format.utc("%I %p"), function(d, a) { return a.getUTCHours() !== d.getUTCHours(); }],
17+
[d3.time.format.utc("%I:%M"), function(d, a) { return a.getUTCMinutes() !== d.getUTCMinutes(); }],
18+
[d3.time.format.utc(":%S"), function(d, a) { return a.getUTCSeconds() !== d.getUTCSeconds(); }],
19+
[d3.time.format.utc(".%L"), d3_true]
2020
];
2121

2222
var d3_time_scaleUTCFormat = d3_time_scaleFormat(d3_time_scaleUTCFormats);

src/time/scale.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ function d3_time_scale(linear, methods, format) {
3434
};
3535

3636
scale.ticks = function(m, k) {
37-
var extent = d3_scaleExtent(scale.domain());
37+
var extent = d3_scaleExtent(scale.domain()),
38+
floor;
3839
if (typeof m !== "function") {
3940
var span = extent[1] - extent[0],
4041
target = span / m,
@@ -44,9 +45,11 @@ function d3_time_scale(linear, methods, format) {
4445
if (target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target) --i;
4546
m = methods[i];
4647
k = m[1];
48+
floor = m[0].floor;
4749
m = m[0].range;
4850
}
49-
return m(extent[0], new Date(+extent[1] + 1), k); // inclusive upper bound
51+
var ticks = m(extent[0], new Date(+extent[1] + 1), k); // inclusive upper bound
52+
return floor ? ticks.map(floor) : ticks;
5053
};
5154

5255
scale.tickFormat = function() {
@@ -66,8 +69,9 @@ function d3_time_scaleDate(t) {
6669

6770
function d3_time_scaleFormat(formats) {
6871
return function(date) {
69-
var i = formats.length - 1, f = formats[i];
70-
while (!f[1](date)) f = formats[--i];
72+
d3_time_scaleBoundaryDate.setTime(date - 1);
73+
var i = 0, f = formats[i];
74+
while (!f[1](date, d3_time_scaleBoundaryDate)) f = formats[++i];
7175
return f[0](date);
7276
};
7377
}
@@ -127,15 +131,17 @@ var d3_time_scaleLocalMethods = [
127131
[d3.time.year, 1]
128132
];
129133

134+
var d3_time_scaleBoundaryDate = new Date(0);
135+
130136
var d3_time_scaleLocalFormats = [
131-
[d3.time.format("%Y"), d3_true],
132-
[d3.time.format("%B"), function(d) { return d.getMonth(); }],
133-
[d3.time.format("%b %d"), function(d) { return d.getDate() != 1; }],
134-
[d3.time.format("%a %d"), function(d) { return d.getDay() && d.getDate() != 1; }],
135-
[d3.time.format("%I %p"), function(d) { return d.getHours(); }],
136-
[d3.time.format("%I:%M"), function(d) { return d.getMinutes(); }],
137-
[d3.time.format(":%S"), function(d) { return d.getSeconds(); }],
138-
[d3.time.format(".%L"), function(d) { return d.getMilliseconds(); }]
137+
[d3.time.format("%Y"), function(d, a) { return a.getFullYear() !== d.getFullYear(); }],
138+
[d3.time.format("%B"), function(d, a) { return a.getMonth() !== d.getMonth() && d.getDay(); }],
139+
[d3.time.format("%b %d"), function(d, a) { return a.getDate() !== d.getDate() && !d.getDay(); }],
140+
[d3.time.format("%a %d"), function(d, a) { return a.getDate() !== d.getDate(); }],
141+
[d3.time.format("%I %p"), function(d, a) { return a.getHours() !== d.getHours(); }],
142+
[d3.time.format("%I:%M"), function(d, a) { return a.getMinutes() !== d.getMinutes(); }],
143+
[d3.time.format(":%S"), function(d, a) { return a.getSeconds() !== d.getSeconds(); }],
144+
[d3.time.format(".%L"), d3_true]
139145
];
140146

141147
var d3_time_scaleLinear = d3.scale.linear(),

0 commit comments

Comments
 (0)