This is a Binned Line Chart version of Mike Bostock‘s zoomable area chart. You can find his source here. The graph shows number of flights per day in the United States.
A detailed publication as part of Matt Woelk‘s Masters Thesis will be available in Fall, 2013.
<!DOCTYPE html>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="readme-binnedChart.js"></script>
<script src="readme-underscore.js"></script>
<script src="readme-msToCentury.js"></script>
<style>
body {
background-color : #FFF;
font-family : sans-serif;
color : #000;
font-size : 20px;
}
#controls ul {
float: left;
}
#chart {
font: 10px sans-serif;
overflow: none;
}
path, line {
/*shape-rendering: crispEdges;*/
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis path {
/* Don't show the axis lines. */
display: none;
}
.y.axis line {
stroke: #999;
stroke-dasharray: 3, 3;
}
#chart_container {
position: relative;
overflow: none;
}
#chartContainer {
overflow : none;
width : 100%;
overflow : none;
float : left;
}
#zoomSVG {
position : absolute;
top : 0;
left : 0;
}
#zoomRect {
border-style : solid;
border-width : 1px;
cursor : col-resize;
}
</style>
<body>
<script>
var thePlot;
var margin = {top: 20, right: 10, bottom: 25, left: 70};
var height = 200;
var parseDate = d3.time.format("%Y-%m-%d").parse;
d3.csv("readme-flights.csv", function (error, data) {
// FORMATTING DATA //
if (error) {
alert("Try refreshing your browser.");
return;
}
var json = data.map(function (d) {
return {val: +d.value, ms: parseDate(d.date).getTime()};
});
// INITIALIZING THE PLOT //
var xScale = d3.scale.linear().domain([570693600000, 1201845600000]).range([0, document.getElementById("chartContainer").offsetWidth]);
var yScale = d3.scale.linear();
var plotHeight = function () { return thePlot.height(); };
var pl;
thePlot = binnedLineChart(json, "TODO-SERVER", "1");
thePlot.xScale(xScale);
pl = d3.select("#chartContainer").insert("svg").call(thePlot);
var sps = 864e5; // 864e5 is the number of milliseconds in a day
thePlot.containerWidth(document.getElementById("chartContainer").offsetWidth).height(height).showTimeContext(true).milliSecondsPerSample(sps).update();
d3.select("#chartContainer").attr("height", plotHeight).attr("width", document.getElementById("chartContainer").offsetWidth);
// ZOOMING //
var zoomSVG = d3.select("#zoomSVG");
var zoomRect = d3.select("#zoomRect");
var zoom = d3.behavior.zoom()
.scaleExtent([Math.pow(2, -2), Math.pow(2,10)])
.on("zoom", function () {
thePlot.xScale(xScale).update();
});
zoomSVG.attr("width", document.getElementById("chartContainer").offsetWidth)
.attr("height", plotHeight);
zoomRect.attr("width", document.getElementById("chartContainer").offsetWidth - margin.left - margin.right)
.attr("height", plotHeight)
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
zoomRect.attr("fill", "rgba(0,0,0,0)")
.call(zoom);
// apply zooming
xScale = thePlot.xScale();
yScale = thePlot.yScale();
zoom.x(xScale);
zoom.y(yScale);
// UPDATING LINES //
function changeLines () {
thePlot.setSelectedLines().update();
}
document.getElementById("render-lines").addEventListener("change", changeLines, false);
document.getElementById("render-depth").addEventListener("change", changeLines, false);
document.getElementById("render-method").addEventListener("change", changeLines, false);
});
</script>
<div id="chartContainer">
<svg id="zoomSVG"><rect id="zoomRect" /></svg>
</div>
<div id="controls">
<form>
<ul id="render-lines">
Which functions<br />to render:
<li><label><input type="checkbox" checked value="average"/>Averages</label></li>
<li><label><input type="checkbox" checked value="maxes"/>Maximums</label></li>
<li><label><input type="checkbox" checked value="mins"/>Minimums</label></li>
<li><label><input type="checkbox" value="q1"/>1st Quartile</label></li>
<li><label><input type="checkbox" value="q3"/>3st Quartile</label></li>
<li><label><input type="checkbox" checked value="quartiles"/>Quartile Area</label></li>
</ul>
<ul id="render-depth">
Maximum Bin<br />Render Size:
<br />
<label><input type="range" min="1" max="200" id="renderdepth" value="40" style="width:200px"/>
</ul>
<ul id="render-method">
Interpolation<br />Method:
<li><label><input type="radio" name="render-method" value="linear"/>Linear</li>
<li><label><input type="radio" name="render-method" value="step-after"/>Step-After</li>
<li><label><input type="radio" checked name="render-method" value="monotone"/>Monotone</li>
</ul>
</form>
</div>
///////////////////////////////////////
// Custom Time Axis //
// ---------------- //
// - Allows for smooth scrolling at //
// smaller-than-millisecond zoom //
// levels //
// - Provides time axis labeling //
// see customTimeFormat //
// - Depends on underscore.js, d3.js //
///////////////////////////////////////
// USAGE
// ----------------
// xAxis = d3.svg.axis()
// .tickFormat(msToCenturyTickFormat)
// .tickValues(msToCenturyTickValues(xScale, width))
// .scale(xScale).orient("bottom");
///////////////////////////////////////
var MIN_DISTANCE_BETWEEN_X_AXIS_LABELS = 75;
function millisecond(val) {
var newdate = new Date();
newdate.setTime(roundDownToNearestTime(val, times.ms));
return newdate;
}
function roundUpToNearestTime(val, tim) {
return Math.ceil(val/tim) * tim;
}
function roundDownToNearestTime(val, tim) {
return Math.floor(val/tim) * tim;
}
function getNumberOfDaysInCurrentMonth(dat) {
var curmo = dat.getMonth();
var addYear;
if (( curmo + 1 ) / 12.0 >= 1.0) {
// we rolled over to the next year
addYear = dat.getFullYear() + 1;
} else {
addYear = dat.getFullYear();
}
var newdate = new Date(
addYear,
(curmo + 1) % 12,
1,
1,
1,
1,
1);
newdate = dt(newdate.getTime() - 4000000);
return newdate.getDate();
}
var times = {
ms: 1, //milliseconds
s: 1000, //seconds
m: 6e4, //minutes
h: 36e5, //hours
d: 864e5, //days
// These are approximations:
mo: 2592e6, //months
y: 31536e6, //years
};
function getNumberOfDaysInCurrentYear(dat) {
var newdateStart = new Date(dat.getFullYear() , 0, 0);
var newdateEnd = new Date(dat.getFullYear() + 1, 0, 0);
var diff = newdateEnd.getTime() - newdateStart.getTime();
var oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay);
}
// custom formatting for x axis time
function msToCenturyTickFormat(ti) {
function timeFormat(formats) {
return function(date) {
var newdate = new Date();
newdate.setTime(date);
var i = formats.length - 1, f = formats[i];
while (!f[1](newdate)) f = formats[--i];
return f[0](newdate);
};
}
var customTimeFormat = timeFormat([
[ d3.time.format("%Y") , function() { return true; } ],
[ d3.time.format("%b") , function(d) { return d.getMonth(); } ],
[ d3.time.format("%a %d") , function(d) { return d.getDate() != 1; } ],
[ d3.time.format("%H %p") , function(d) { return d.getHours(); } ],
[ d3.time.format("%H:%M") , function(d) { return d.getMinutes(); } ],
[ d3.time.format("%Ss") , function(d) { return d.getSeconds(); } ],
[ d3.time.format("%Lms") , function(d) { return d.getMilliseconds(); } ]
]);
return function(d) { return customTimeFormat(ti); }();
}
function onScreenSizeOfLabels(millisecondsPerLabel, screenWidth, distanceBtwnLabels) {
return millisecondsPerLabel * screenWidth / distanceBtwnLabels;
}
function findLevel(dom, wid) {
for (i = 0; i < rounding_scales.length; i++) {
var ro = rounding_scales[i];
var compr = onScreenSizeOfLabels(ro[0]*ro[1], wid, MIN_DISTANCE_BETWEEN_X_AXIS_LABELS);
if (dom[1] - dom[0] <= compr ) {
var result = makeTickRange(dom[0], dom[1], ro[1], ro[3], ro[2], ro[0]*ro[1], wid);
// filter this for only what is actually on-screen.
result = _.filter(result, function (num) {
return num < dom[1] && num > dom[0];
});
return i;
}
}
return -1;
}
function msToCenturyTickValues(scal, wid) {
var dom = scal.domain();
var lvl = findLevel(dom, wid);
// This should never occur if the zoom limits are correct
if (lvl === -1) {
return [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14];
}
var ro = rounding_scales[lvl];
var rng = makeTickRange(dom[0], dom[1], ro[1], ro[3], ro[2], ro[0]*ro[1], wid);
// filter this for only what is actually on-screen.
var result = _.filter(rng, function (num) {
return num < dom[1] && num > dom[0];
});
return result;
}
function msToCenturyTickSubDivide(scal, wid) {
var dom = scal.domain();
var lvl = findLevel(dom, wid);
if (lvl === -1) { return 0; }
var baseSize = rounding_scales[lvl][1];
var tickSpace = rounding_ticks[baseSize];
return ((baseSize / tickSpace) - 1);
}
function makeTickRange(start, end, increment, incrementOf, baseFunc, smallInc, wid) {
if ( incrementOf === d3.time.year ) {
// For Years
var startyear = d3.time.year.floor(dt(start));
var endyear = d3.time.year.ceil( dt(end ));
var curange = d3.range(startyear.getFullYear(), endyear.getFullYear());
// Filter for proper increments
curange = _.filter(curange, function (d, i) {
return d % increment == 0;
});
curange = _.map(curange, function (d) { return new Date(d, 0); });
return curange;
} else if ( incrementOf === d3.time.month ) {
// For Months
var startyear = d3.time.year.floor(dt(start));
var endyear = d3.time.year.ceil( dt(end ));
var curange = d3.range(startyear.getFullYear(), endyear.getFullYear());
// for each year, get all of the months for it
curange = _.map(curange, function (d, i) {
return _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11], function (f) {
// For each month of the year
return new Date(d, f);
});
});
curange = _.flatten(curange);
curange = _.filter(curange, function (d, i) {
// Filter for proper increments
return i % increment == 0;
});
return curange;
} else if (baseFunc === d3.time.month){
// For Days
var startyear = d3.time.year.floor(dt(start));
var endyear = d3.time.year.ceil( dt(end ));
var curange = d3.range(startyear.getFullYear(), endyear.getFullYear());
// For each year, get all of the months for it
curange = _.map(curange, function (year, i) {
return _.map([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11], function (month) {
// For each month of the year
var monthDays = getNumberOfDaysInCurrentMonth(new Date(year, month));
return _.map(d3.range(1, monthDays + 1), function (day) {
// For each day of the month
// Filter for proper increments
// and remove ones which are too close
// together near the ends of the months
if ((day - 1) % increment == 0 && monthDays + 1 - day >= increment ) {
return new Date(year, month, day);
} else {
return [];
}
});
});
});
curange = _.flatten(curange);
return curange;
} else {
// For everything smaller than days
return d3.range( baseFunc.floor( dt(start) ).getTime(),
baseFunc.ceil( dt( end ) ).getTime(),
roundUpToNearestTime(
smallInc*MIN_DISTANCE_BETWEEN_X_AXIS_LABELS/wid,
smallInc));
}
}
// for major ticks every 'a' units,
// have a minor tick every 'b' units
// [a, b]
// b should always be a factor of a
var rounding_ticks = {
1 : 0.5, // when we are showing 1 of something, display a tick every 0.5
2 : 1 ,
3 : 1 ,
5 : 1 ,
6 : 3 , // when we are showing 6 of something, display a tick every 3
10 : 5 ,
12 : 3 ,
15 : 5 ,
20 : 5 ,
25 : 5 ,
30 : 10 ,
50 : 10 ,
100 : 50 ,
200 : 50 ,
500 : 100,
}
// Data object to help make custom axis' tick values
// [ estimate size in milliseconds,
// how many to increment,
// precise time rounder for anchoring,
// precise time rounder ]
var rounding_scales = [
[ times.ms , 1 , d3.time.second , millisecond],
[ times.ms , 2 , d3.time.second , millisecond],
[ times.ms , 5 , d3.time.second , millisecond],
[ times.ms , 10 , d3.time.second , millisecond],
[ times.ms , 20 , d3.time.second , millisecond],
[ times.ms , 50 , d3.time.second , millisecond],
[ times.ms , 100 , d3.time.second , millisecond],
[ times.ms , 200 , d3.time.second , millisecond],
[ times.ms , 500 , d3.time.second , millisecond],
[ times.s , 1 , d3.time.minute , d3.time.second],
[ times.s , 2 , d3.time.minute , d3.time.second],
[ times.s , 5 , d3.time.minute , d3.time.second],
[ times.s , 15 , d3.time.minute , d3.time.second],
[ times.s , 30 , d3.time.minute , d3.time.second],
[ times.m , 1 , d3.time.hour , d3.time.minute],
[ times.m , 2 , d3.time.hour , d3.time.minute],
[ times.m , 5 , d3.time.hour , d3.time.minute],
[ times.m , 15 , d3.time.hour , d3.time.minute],
[ times.m , 30 , d3.time.hour , d3.time.minute],
[ times.h , 1 , d3.time.day , d3.time.hour],
[ times.h , 3 , d3.time.day , d3.time.hour],
[ times.h , 6 , d3.time.day , d3.time.hour],
[ times.h , 12 , d3.time.day , d3.time.hour],
[ times.d , 1 , d3.time.month , d3.time.day],
[ times.d , 2 , d3.time.month , d3.time.day],
[ times.d , 5 , d3.time.month , d3.time.day],
[ times.d , 10 , d3.time.month , d3.time.day],
[ times.d , 15 , d3.time.month , d3.time.day],
[ times.mo , 1 , d3.time.year , d3.time.month],
[ times.mo , 2 , d3.time.year , d3.time.month],
[ times.mo , 3 , d3.time.year , d3.time.month],
[ times.mo , 6 , d3.time.year , d3.time.month],
[ times.mo , 12 , d3.time.year , d3.time.month],
[ times.y , 1 , d3.time.year , d3.time.year],
[ times.y , 2 , d3.time.year , d3.time.year],
[ times.y , 5 , d3.time.year , d3.time.year],
[ times.y , 10 , d3.time.year , d3.time.year],
[ times.y , 25 , d3.time.year , d3.time.year],
[ times.y , 50 , d3.time.year , d3.time.year],
[ times.y , 100, d3.time.year , d3.time.year],
[ times.y , 100, d3.time.year , d3.time.year],
];