Expanding on mbostock's weekday.js and my Weekdays gists by adding adaptive tick mark format and wrapping it all up in a dayselect scale (d3.scale.dayselect
).
Last active
August 29, 2015 14:01
-
-
Save mayo/e27554b34bff1f177c05 to your computer and use it in GitHub Desktop.
Dayselect Scale
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
date | value | |
---|---|---|
01/5/2014 | 1 | |
02/5/2014 | 2 | |
05/5/2014 | 1 | |
06/5/2014 | 2 | |
08/5/2014 | 2 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Expecting a function that can map between non-uniform day scale (weekdays, | |
// for eg.) and a linear scale. The map function needs to return linear values | |
// as output, given a input Date(), respond to .invert() given a value from | |
// linear scale and return a corresponding Date() object, and a .factor | |
// property containing a multplier to convert the linear values to | |
// miliseconds. | |
// | |
// This scale is currently tailored to weekday scale, as the values in | |
// dayselect_time_scaleSteps reflect 5 day weeks. It should be possible to | |
// calculate these values based on the map function, rather than hardcoding | |
// them into the function. | |
// | |
// In theory, it should be possible to use any function that does similar | |
// mapping, for eg. business hours. Let's call it weekhours and each hour of a | |
// business week would be mapped onto a uniform scale just like weekdays. | |
// Performance of this kind of map may be an issue though. | |
d3.scale.dayselect = d3.scale.dayselect = function(mapFunction) { | |
function dayselect_scale(linear, methods, format, mapFunction) { | |
function scale(x) { | |
return linear(x); | |
} | |
function tickMethod(extent, count) { | |
var span = extent[1] - extent[0]; | |
//var target = span / count; | |
var target = span * mapFunction.factor / count; | |
var i = d3.bisect(dayselect_time_scaleSteps, target); | |
/* changing 31536e6 to 22550.4e6, to factor for shorter years */ | |
return i == dayselect_time_scaleSteps.length ? [dayselect_time_scaleLocalMethods.year, dayselect_scale_linearTickRange(extent.map(function(d) { return d / 22550.4e6; }), count)[2]] | |
: !i ? [dayselect_time_scaleMilliseconds, dayselect_scale_linearTickRange(extent, count)[2]] | |
: dayselect_time_scaleLocalMethods[target / dayselect_time_scaleSteps[i - 1] < dayselect_time_scaleSteps[i] / target ? i - 1 : i]; | |
} | |
scale.ticks = function(interval, skip) { | |
var extent = dayselect_scaleExtent(x.domain()); | |
var method = interval == null ? tickMethod(extent, 10) | |
: typeof interval === "number" ? tickMethod(extent, interval) | |
: !interval.range && [{range: interval}, skip]; // assume deprecated range function | |
if (method) interval = method[0], skip = method[1]; | |
//return | |
out = interval.range(mapFunction.invert(extent[0]), mapFunction.invert(+extent[1] + 1), skip < 1 ? 1 : skip); // inclusive upper bound | |
//convert to weekdays | |
return out.map(function(e) { return mapFunction(e); }); | |
} | |
scale.tickFormat = function() { | |
return format; | |
}; | |
scale.copy = function() { | |
return dayselect_scale(linear.copy(), methods, format, mapFunction); | |
}; | |
return d3.rebind(scale, linear, "nice", "domain", "invert", "range", "rangeRound", "interpolate", "clamp"); | |
} | |
/* clean copy from d3, becase we're crossing namespaces */ | |
function dayselect_scale_linearTickRange(domain, m) { | |
if (m == null) m = 10; | |
var extent = dayselect_scaleExtent(domain), | |
span = extent[1] - extent[0], | |
step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), | |
err = m / span * step; | |
// Filter ticks to get closer to the desired count. | |
if (err <= .15) step *= 10; | |
else if (err <= .35) step *= 5; | |
else if (err <= .75) step *= 2; | |
// Round start and stop values to step interval. | |
extent[0] = Math.ceil(extent[0] / step) * step; | |
extent[1] = Math.floor(extent[1] / step) * step + step * .5; // inclusive | |
extent[2] = step; | |
return extent; | |
} | |
/* clean copy from d3, becase we're crossing namespaces */ | |
function dayselect_scaleExtent(domain) { | |
var start = domain[0], stop = domain[domain.length - 1]; | |
return start < stop ? [start, stop] : [stop, start]; | |
} | |
/* clean copy from d3, becase we're crossing namespaces */ | |
function dayselect_time_scaleDate(t) { | |
return new Date(mapFunction.invert(t)); | |
} | |
var dayselect_time_scaleSteps = [ | |
1e3, // 1-second | |
5e3, // 5-second | |
15e3, // 15-second | |
3e4, // 30-second | |
6e4, // 1-minute | |
3e5, // 5-minute | |
9e5, // 15-minute | |
18e5, // 30-minute | |
36e5, // 1-hour | |
108e5, // 3-hour | |
216e5, // 6-hour | |
432e5, // 12-hour | |
864e5, // 1-day | |
1728e5, // 2-day | |
4320e5, // 1-week // 5 days. original value 6048e5 = 7 days | |
1900.8e6, // 1-month // 22 days is 21 better?. orignal value 2592e6 = 30 days | |
5702.4e6, // 3-month // 66 days. is 63 better?. orignal value 7776e6 = 90 days | |
22550.4e6 // 1-year //261 days. is 260 better?. original value 31536e6 = 365 days | |
]; | |
var dayselect_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.day, 5], //.week, 1 | |
[d3.time.day, 22], //.month, 1 | |
[d3.time.day, 66], //.month, 3 | |
[d3.time.day, 261] //.year, 1 | |
]; | |
function dayselect_time_formatMulti(formats) { | |
var n = formats.length, i = -1; | |
while (++i < n) { | |
formats[i][0] = d3.time.format(formats[i][0]); | |
} | |
return function(date) { | |
date = mapFunction.invert(date); | |
var i = 0, f = formats[i]; | |
while (!f[1](date)) { | |
f = formats[++i]; | |
} | |
return f[0](date); | |
}; | |
} | |
var dayselect_time_scaleLocalFormat = dayselect_time_formatMulti(([ | |
[".%L", function(d) { return d.getMilliseconds(); }], | |
[":%S", function(d) { return d.getSeconds(); }], | |
["%I:%M", function(d) { return d.getMinutes(); }], | |
["%I %p", function(d) { return d.getHours(); }], | |
["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }], | |
["%b %d", function(d) { return d.getDate() != 1; }], | |
["%B", function(d) { return d.getMonth(); }], | |
["%Y", function() { return true; }] | |
])); | |
var dayselect_time_scaleMilliseconds = { | |
range: function(start, stop, step) { return d3.range(Math.ceil(start / step) * step, +stop, step).map(dayselect_time_scaleDate); }, | |
floor: d3.identity, | |
ceil: d3.identity | |
}; | |
dayselect_time_scaleLocalMethods.year = d3.time.year; | |
return dayselect_scale(d3.scale.linear(), dayselect_time_scaleLocalMethods, dayselect_time_scaleLocalFormat, mapFunction); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="weekday.js"></script> | |
<script src="dayselect.js"></script> | |
<title>Scale test</title> | |
<style> | |
body { | |
font-family: 'helvetica neue'; | |
font-size: .8em; | |
} | |
.line { | |
fill: none; | |
stroke: black; | |
stroke-width: 1px; | |
} | |
.axis line, | |
.axis path { | |
stroke-width: 1px; | |
stroke: black; | |
fill: none; | |
} | |
.dots circle { | |
fill: #555; | |
fill-opacity: .5; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="chart"></div> | |
<script> | |
var margin = {top: 20, right: 50, bottom: 50, left: 20}, | |
width = 960 - margin.left - margin.right, | |
height = 502 - margin.top - margin.bottom; | |
var parseDate = d3.time.format("%d/%m/%Y").parse; | |
var dayCount = 0; | |
var x = d3.scale.dayselect(weekday) | |
.range([0, width - margin.right]); | |
var y = d3.scale.linear() | |
.range([0, height - margin.left]); | |
var dateFormat = d3.time.format('%a %b %d'); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("below"); | |
// .tickFormat(function (d) { return dateFormat(weekday.invert(d)); }); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("right"); | |
var line = d3.svg.line() | |
.x(function(d) { return x(d.weekday); }) | |
.y(function(d) { return y(d.value); }); | |
var svg = d3.select("#chart").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom); | |
d3.csv("data.csv", type, function(error, data) { | |
x.domain(d3.extent(data, function(d) { return d.weekday; })); | |
y.domain(d3.extent(data, function(d) { return parseFloat(d.value); })) | |
svg.append("path") | |
.datum(data) | |
.attr("class", "line") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
.attr("d", line); | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(" + margin.left + "," + (margin.top + height + 20) + ")") | |
.call(xAxis) | |
.selectAll("text") | |
.attr("dy", ".35em"); | |
svg.append("g") | |
.attr("class", "y axis") | |
.attr("transform", "translate(" + (width + 20) + "," + margin.top + ")") | |
.call(yAxis) | |
.selectAll("text") | |
.attr("dy", ".35em"); | |
svg.append("g") | |
.attr("class", "dots") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
.selectAll("circle") | |
.data(data) | |
.enter() | |
.append("circle") | |
.attr("r", 5) | |
.attr("cx", function(d) { return x(d.weekday); }) | |
.attr("cy", function(d) { return y(d.value); });; | |
}); | |
function type(d) { | |
d.date = parseDate(d.date); | |
d.weekday = weekday(d.date); | |
return d | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
weekday = (function() { | |
cache = {}; | |
// Returns the weekday number for the given date relative to January 1, 1970. | |
function weekday(date) { | |
c = cache[date]; | |
if (c != null) { | |
return c; | |
} | |
var weekdays = weekdayOfYear(date), | |
year = date.getFullYear(); | |
while (--year >= 1970) weekdays += weekdaysInYear(year); | |
cache[date] = weekdays; | |
//if we're looking up a weekend day, make sure we cache the correct weekday | |
if (cache[weekdays] == null) { | |
newDate = new Date(date); | |
offset = newDate.getDay() == 0 ? -2 : newDate.getDay() == 6 ? -1 : 0; | |
if (offset > 0) { | |
date.setDate(date.getDate() + offset); | |
//cache the new date as well | |
cache[newDate] = weekdays; | |
} | |
cache[weekdays] = newDate; | |
} | |
return weekdays; | |
} | |
//multiplier to go from weekday number to miliseconds (javascript timestamp) | |
weekday.factor = 864e5; | |
// Returns the date for the specified weekday number relative to January 1, 1970. | |
weekday.invert = function(weekdays) { | |
c = cache[weekdays]; | |
if (c != null) { | |
return c; | |
} | |
var year = 1970, | |
yearWeekdays; | |
// Compute the year. | |
while ((yearWeekdays = weekdaysInYear(year)) <= weekdays) { | |
++year; | |
weekdays -= yearWeekdays; | |
} | |
// Compute the date from the remaining weekdays. | |
var days = weekdays % 5, | |
day0 = ((new Date(year, 0, 1)).getDay() + 6) % 7; | |
if (day0 + days > 4) days += 2; | |
date = new Date(year, 0, (weekdays / 5 | 0) * 7 + days + 1); | |
cache[date] = weekdays; | |
cache[weekdays] = date; | |
return date; | |
}; | |
// Returns the number of weekdays in the specified year. | |
function weekdaysInYear(year) { | |
return weekdayOfYear(new Date(year, 11, 31)) + 1; | |
} | |
// Returns the weekday number for the given date relative to the start of the year. | |
function weekdayOfYear(date) { | |
var days = d3.time.dayOfYear(date), | |
weeks = days / 7 | 0, | |
day0 = (d3.time.year(date).getDay() + 6) % 7, | |
day1 = day0 + days - weeks * 7; | |
return Math.max(0, days - weeks * 2 | |
- (day0 <= 5 && day1 >= 5 || day0 <= 12 && day1 >= 12) // extra saturday | |
- (day0 <= 6 && day1 >= 6 || day0 <= 13 && day1 >= 13)); // extra sunday | |
} | |
return weekday; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment