Skip to content

Instantly share code, notes, and snippets.

@mayo
Last active August 29, 2015 14:01
Show Gist options
  • Save mayo/e27554b34bff1f177c05 to your computer and use it in GitHub Desktop.
Save mayo/e27554b34bff1f177c05 to your computer and use it in GitHub Desktop.
Dayselect Scale

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).

View at bl.ocks.org;

date value
01/5/2014 1
02/5/2014 2
05/5/2014 1
06/5/2014 2
08/5/2014 2
// 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);
};
<!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>
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 lookupWeekdays = weekdays;
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] = lookupWeekdays;
cache[lookupWeekdays] = 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