Skip to content

Commit

Permalink
Sub-second and multi-year ticks for time scales.
Browse files Browse the repository at this point in the history
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
mbostock committed Jan 29, 2012
1 parent f67e895 commit 117942e
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 26 deletions.
1 change: 0 additions & 1 deletion d3.js
Expand Up @@ -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],
Expand Down
47 changes: 43 additions & 4 deletions d3.time.js
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
};
Expand Down
2 changes: 1 addition & 1 deletion d3.time.min.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/scale/linear.js
Expand Up @@ -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],
Expand Down
20 changes: 19 additions & 1 deletion src/time/scale-utc.js
Expand Up @@ -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);
};
27 changes: 24 additions & 3 deletions src/time/scale.js
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
79 changes: 64 additions & 15 deletions test/time/scale-test.js
Expand Up @@ -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), [
Expand Down Expand Up @@ -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)
]);
}
},

Expand Down Expand Up @@ -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");
}
},

Expand Down Expand Up @@ -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), [
Expand Down Expand Up @@ -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)
]);
}
},

Expand Down Expand Up @@ -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.