Permalink
Browse files

Sub-second and multi-year ticks for time scales.

Fixes #428. This is built on top of existing tick support for linear scales: for
small intervals, a linear scale computes ticks based on milliseconds; for large
intervals, a linear scale computes ticks based on fractional years. This commit
also extends the time scale's formatter to display milliseconds.
  • Loading branch information...
1 parent f67e895 commit 117942e28462a46d7f24a8c805c250ddb9fe89d7 @mbostock mbostock committed Jan 29, 2012
Showing with 151 additions and 26 deletions.
  1. +0 −1 d3.js
  2. +43 −4 d3.time.js
  3. +1 −1 d3.time.min.js
  4. +0 −1 src/scale/linear.js
  5. +19 −1 src/time/scale-utc.js
  6. +24 −3 src/time/scale.js
  7. +64 −15 test/time/scale-test.js
View
1 d3.js
@@ -2458,7 +2458,6 @@ function d3_scale_linearNice(dx) {
};
}
-// TODO Dates? Ugh.
function d3_scale_linearTickRange(domain, m) {
var extent = d3_scaleExtent(domain),
span = extent[1] - extent[0],
View
@@ -552,7 +552,9 @@ function d3_time_scale(linear, methods, format) {
if (typeof m !== "function") {
var span = extent[1] - extent[0],
target = span / m,
- i = d3.bisect(d3_time_scaleSteps, target, 1, d3_time_scaleSteps.length - 1);
+ i = d3.bisect(d3_time_scaleSteps, target);
+ if (i == d3_time_scaleSteps.length) return methods.year(extent, m);
+ if (!i) return linear.ticks(m).map(d3_time_scaleDate);
if (Math.log(target / d3_time_scaleSteps[i - 1]) < Math.log(d3_time_scaleSteps[i] / target)) --i;
m = methods[i];
k = m[1];
@@ -591,6 +593,19 @@ function d3_time_scaleFormat(formats) {
};
}
+function d3_time_scaleSetYear(y) {
+ var d = new Date(y, 0, 1);
+ d.setFullYear(y); // Y2K fail
+ return d;
+}
+
+function d3_time_scaleGetYear(d) {
+ var y = d.getFullYear(),
+ d0 = d3_time_scaleSetYear(y),
+ d1 = d3_time_scaleSetYear(y + 1);
+ return y + (d - d0) / (d1 - d0);
+}
+
var d3_time_scaleSteps = [
1e3, // 1-second
5e3, // 5-second
@@ -640,10 +655,16 @@ var d3_time_scaleLocalFormats = [
[d3.time.format("%a %d"), function(d) { return d.getDay() && d.getDate() != 1; }],
[d3.time.format("%I %p"), function(d) { return d.getHours(); }],
[d3.time.format("%I:%M"), function(d) { return d.getMinutes(); }],
- [d3.time.format(":%S"), function(d) { return d.getSeconds() || d.getMilliseconds(); }]
+ [d3.time.format(":%S"), function(d) { return d.getSeconds(); }],
+ [d3.time.format(".%L"), function(d) { return d.getMilliseconds(); }]
];
-var d3_time_scaleLocalFormat = d3_time_scaleFormat(d3_time_scaleLocalFormats);
+var d3_time_scaleLinear = d3.scale.linear(),
+ d3_time_scaleLocalFormat = d3_time_scaleFormat(d3_time_scaleLocalFormats);
+
+d3_time_scaleLocalMethods.year = function(extent, m) {
+ return d3_time_scaleLinear.domain(extent.map(d3_time_scaleGetYear)).ticks(m).map(d3_time_scaleSetYear);
+};
d3.time.scale = function() {
return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat);
@@ -676,11 +697,29 @@ var d3_time_scaleUTCFormats = [
[d3.time.format.utc("%a %d"), function(d) { return d.getUTCDay() && d.getUTCDate() != 1; }],
[d3.time.format.utc("%I %p"), function(d) { return d.getUTCHours(); }],
[d3.time.format.utc("%I:%M"), function(d) { return d.getUTCMinutes(); }],
- [d3.time.format.utc(":%S"), function(d) { return d.getUTCSeconds() || d.getUTCMilliseconds(); }]
+ [d3.time.format.utc(":%S"), function(d) { return d.getUTCSeconds(); }],
+ [d3.time.format.utc(".%L"), function(d) { return d.getUTCMilliseconds(); }]
];
var d3_time_scaleUTCFormat = d3_time_scaleFormat(d3_time_scaleUTCFormats);
+function d3_time_scaleUTCSetYear(y) {
+ var d = new Date(Date.UTC(y, 0, 1));
+ d.setUTCFullYear(y); // Y2K fail
+ return d;
+}
+
+function d3_time_scaleUTCGetYear(d) {
+ var y = d.getUTCFullYear(),
+ d0 = d3_time_scaleUTCSetYear(y),
+ d1 = d3_time_scaleUTCSetYear(y + 1);
+ return y + (d - d0) / (d1 - d0);
+}
+
+d3_time_scaleUTCMethods.year = function(extent, m) {
+ return d3_time_scaleLinear.domain(extent.map(d3_time_scaleUTCGetYear)).ticks(m).map(d3_time_scaleUTCSetYear);
+};
+
d3.time.scale.utc = function() {
return d3_time_scale(d3.scale.linear(), d3_time_scaleUTCMethods, d3_time_scaleUTCFormat);
};
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -83,7 +83,6 @@ function d3_scale_linearNice(dx) {
};
}
-// TODO Dates? Ugh.
function d3_scale_linearTickRange(domain, m) {
var extent = d3_scaleExtent(domain),
span = extent[1] - extent[0],
View
@@ -26,11 +26,29 @@ var d3_time_scaleUTCFormats = [
[d3.time.format.utc("%a %d"), function(d) { return d.getUTCDay() && d.getUTCDate() != 1; }],
[d3.time.format.utc("%I %p"), function(d) { return d.getUTCHours(); }],
[d3.time.format.utc("%I:%M"), function(d) { return d.getUTCMinutes(); }],
- [d3.time.format.utc(":%S"), function(d) { return d.getUTCSeconds() || d.getUTCMilliseconds(); }]
+ [d3.time.format.utc(":%S"), function(d) { return d.getUTCSeconds(); }],
+ [d3.time.format.utc(".%L"), function(d) { return d.getUTCMilliseconds(); }]
];
var d3_time_scaleUTCFormat = d3_time_scaleFormat(d3_time_scaleUTCFormats);
+function d3_time_scaleUTCSetYear(y) {
+ var d = new Date(Date.UTC(y, 0, 1));
+ d.setUTCFullYear(y); // Y2K fail
+ return d;
+}
+
+function d3_time_scaleUTCGetYear(d) {
+ var y = d.getUTCFullYear(),
+ d0 = d3_time_scaleUTCSetYear(y),
+ d1 = d3_time_scaleUTCSetYear(y + 1);
+ return y + (d - d0) / (d1 - d0);
+}
+
+d3_time_scaleUTCMethods.year = function(extent, m) {
+ return d3_time_scaleLinear.domain(extent.map(d3_time_scaleUTCGetYear)).ticks(m).map(d3_time_scaleUTCSetYear);
+};
+
d3.time.scale.utc = function() {
return d3_time_scale(d3.scale.linear(), d3_time_scaleUTCMethods, d3_time_scaleUTCFormat);
};
View
@@ -20,7 +20,9 @@ function d3_time_scale(linear, methods, format) {
if (typeof m !== "function") {
var span = extent[1] - extent[0],
target = span / m,
- i = d3.bisect(d3_time_scaleSteps, target, 1, d3_time_scaleSteps.length - 1);
+ i = d3.bisect(d3_time_scaleSteps, target);
+ if (i == d3_time_scaleSteps.length) return methods.year(extent, m);
+ if (!i) return linear.ticks(m).map(d3_time_scaleDate);
if (Math.log(target / d3_time_scaleSteps[i - 1]) < Math.log(d3_time_scaleSteps[i] / target)) --i;
m = methods[i];
k = m[1];
@@ -59,6 +61,19 @@ function d3_time_scaleFormat(formats) {
};
}
+function d3_time_scaleSetYear(y) {
+ var d = new Date(y, 0, 1);
+ d.setFullYear(y); // Y2K fail
+ return d;
+}
+
+function d3_time_scaleGetYear(d) {
+ var y = d.getFullYear(),
+ d0 = d3_time_scaleSetYear(y),
+ d1 = d3_time_scaleSetYear(y + 1);
+ return y + (d - d0) / (d1 - d0);
+}
+
var d3_time_scaleSteps = [
1e3, // 1-second
5e3, // 5-second
@@ -108,10 +123,16 @@ var d3_time_scaleLocalFormats = [
[d3.time.format("%a %d"), function(d) { return d.getDay() && d.getDate() != 1; }],
[d3.time.format("%I %p"), function(d) { return d.getHours(); }],
[d3.time.format("%I:%M"), function(d) { return d.getMinutes(); }],
- [d3.time.format(":%S"), function(d) { return d.getSeconds() || d.getMilliseconds(); }]
+ [d3.time.format(":%S"), function(d) { return d.getSeconds(); }],
+ [d3.time.format(".%L"), function(d) { return d.getMilliseconds(); }]
];
-var d3_time_scaleLocalFormat = d3_time_scaleFormat(d3_time_scaleLocalFormats);
+var d3_time_scaleLinear = d3.scale.linear(),
+ d3_time_scaleLocalFormat = d3_time_scaleFormat(d3_time_scaleLocalFormats);
+
+d3_time_scaleLocalMethods.year = function(extent, m) {
+ return d3_time_scaleLinear.domain(extent.map(d3_time_scaleGetYear)).ticks(m).map(d3_time_scaleSetYear);
+};
d3.time.scale = function() {
return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat);
View
@@ -77,6 +77,17 @@ suite.addBatch({
local(2011, 0, 1, 12, 30)
]);
},
+ "generates sub-second ticks": function(scale) {
+ var x = scale().domain([local(2011, 0, 1, 12, 0, 0), local(2011, 0, 1, 12, 0, 1)]);
+ assert.deepEqual(x.ticks(4), [
+ local(2011, 0, 1, 12, 0, 0, 0),
+ local(2011, 0, 1, 12, 0, 0, 200),
+ local(2011, 0, 1, 12, 0, 0, 400),
+ local(2011, 0, 1, 12, 0, 0, 600),
+ local(2011, 0, 1, 12, 0, 0, 800),
+ local(2011, 0, 1, 12, 0, 1, 0)
+ ]);
+ },
"generates 1-second ticks": function(scale) {
var x = scale().domain([local(2011, 0, 1, 12, 0, 0), local(2011, 0, 1, 12, 0, 4)]);
assert.deepEqual(x.ticks(4), [
@@ -238,6 +249,15 @@ suite.addBatch({
local(2013, 0, 1, 0, 0),
local(2014, 0, 1, 0, 0)
]);
+ },
+ "generates multi-year ticks": function(scale) {
+ var x = scale().domain([local(0, 11, 18), local(2014, 2, 2)]);
+ assert.deepEqual(x.ticks(6), [
+ local( 500, 0, 1, 0, 0),
+ local(1000, 0, 1, 0, 0),
+ local(1500, 0, 1, 0, 0),
+ local(2000, 0, 1, 0, 0)
+ ]);
}
},
@@ -272,13 +292,13 @@ suite.addBatch({
},
"formats minute on second zero": function(format) {
assert.equal(format(local(2011, 1, 2, 11, 59)), "11:59");
- assert.equal(format(local(2011, 1, 2, 12, 01)), "12:01");
- assert.equal(format(local(2011, 1, 2, 12, 02)), "12:02");
+ assert.equal(format(local(2011, 1, 2, 12, 1)), "12:01");
+ assert.equal(format(local(2011, 1, 2, 12, 2)), "12:02");
},
"otherwise, formats second": function(format) {
- assert.equal(format(local(2011, 1, 2, 12, 01, 09)), ":09");
- assert.equal(format(local(2011, 1, 2, 12, 01, 10)), ":10");
- assert.equal(format(local(2011, 1, 2, 12, 01, 11)), ":11");
+ assert.equal(format(local(2011, 1, 2, 12, 1, 9)), ":09");
+ assert.equal(format(local(2011, 1, 2, 12, 1, 10)), ":10");
+ assert.equal(format(local(2011, 1, 2, 12, 1, 11)), ":11");
}
},
@@ -306,6 +326,17 @@ suite.addBatch({
utc(2011, 0, 1, 12, 30)
]);
},
+ "generates sub-second ticks": function(scale) {
+ var x = scale().domain([utc(2011, 0, 1, 12, 0, 0), utc(2011, 0, 1, 12, 0, 1)]);
+ assert.deepEqual(x.ticks(4), [
+ utc(2011, 0, 1, 12, 0, 0, 0),
+ utc(2011, 0, 1, 12, 0, 0, 200),
+ utc(2011, 0, 1, 12, 0, 0, 400),
+ utc(2011, 0, 1, 12, 0, 0, 600),
+ utc(2011, 0, 1, 12, 0, 0, 800),
+ utc(2011, 0, 1, 12, 0, 1, 0)
+ ]);
+ },
"generates 1-second ticks": function(scale) {
var x = scale().domain([utc(2011, 0, 1, 12, 0, 0), utc(2011, 0, 1, 12, 0, 4)]);
assert.deepEqual(x.ticks(4), [
@@ -467,6 +498,15 @@ suite.addBatch({
utc(2013, 0, 1, 0, 0),
utc(2014, 0, 1, 0, 0)
]);
+ },
+ "generates multi-year ticks": function(scale) {
+ var x = scale().domain([utc(0, 11, 18), utc(2014, 2, 2)]);
+ assert.deepEqual(x.ticks(6), [
+ utc( 500, 0, 1, 0, 0),
+ utc(1000, 0, 1, 0, 0),
+ utc(1500, 0, 1, 0, 0),
+ utc(2000, 0, 1, 0, 0)
+ ]);
}
},
@@ -501,25 +541,34 @@ suite.addBatch({
},
"formats minute on second zero": function(format) {
assert.equal(format(utc(2011, 1, 2, 11, 59)), "11:59");
- assert.equal(format(utc(2011, 1, 2, 12, 01)), "12:01");
- assert.equal(format(utc(2011, 1, 2, 12, 02)), "12:02");
+ assert.equal(format(utc(2011, 1, 2, 12, 1)), "12:01");
+ assert.equal(format(utc(2011, 1, 2, 12, 2)), "12:02");
+ },
+ "formats second on millisecond zero": function(format) {
+ assert.equal(format(utc(2011, 1, 2, 12, 1, 9)), ":09");
+ assert.equal(format(utc(2011, 1, 2, 12, 1, 10)), ":10");
+ assert.equal(format(utc(2011, 1, 2, 12, 1, 11)), ":11");
},
- "otherwise, formats second": function(format) {
- assert.equal(format(utc(2011, 1, 2, 12, 01, 09)), ":09");
- assert.equal(format(utc(2011, 1, 2, 12, 01, 10)), ":10");
- assert.equal(format(utc(2011, 1, 2, 12, 01, 11)), ":11");
+ "otherwise, formats milliseconds": function(format) {
+ assert.equal(format(utc(2011, 1, 2, 12, 1, 0, 9)), ".009");
+ assert.equal(format(utc(2011, 1, 2, 12, 1, 0, 10)), ".010");
+ assert.equal(format(utc(2011, 1, 2, 12, 1, 0, 11)), ".011");
}
}
}
}
});
-function local(year, month, day, hours, minutes, seconds) {
- return new Date(year, month, day, hours || 0, minutes || 0, seconds || 0);
+function local(year, month, day, hours, minutes, seconds, milliseconds) {
+ var date = new Date(year, month, day, hours || 0, minutes || 0, seconds || 0, milliseconds || 0);
+ date.setFullYear(year); // Y2K fail
+ return date;
}
-function utc(year, month, day, hours, minutes, seconds) {
- return new Date(Date.UTC(year, month, day, hours || 0, minutes || 0, seconds || 0));
+function utc(year, month, day, hours, minutes, seconds, milliseconds) {
+ var date = new Date(Date.UTC(year, month, day, hours || 0, minutes || 0, seconds || 0, milliseconds || 0));
+ date.setUTCFullYear(year); // Y2K fail
+ return date;
}
suite.export(module);

0 comments on commit 117942e

Please sign in to comment.