var m = [19, 20, 20, 19], // top right bottom left margin w = 952 - m[1] - m[3], // width h = 136 - m[0] - m[2], // height z = 17, // cell size d3_debug = 0, start_date = 1993; var day = d3.time.format("%w"), week = d3.time.format("%U"), format = d3.time.format("%Y-%m-%d"), unixtime = function(dt) { return Math.round(dt.getTime() / 1000); }; var color = d3.scale.quantile() .range(d3.range(9).reverse()); // keep the years tightly packed together, with just enough space for the year text in between: var year_position = function(y1, y0) { var start = new Date(y0, 0, 1); var swkd = +day(start); var pos = 0; // make sure we start on a sunday for this calculation (entire week to start with, sort of...): if (swkd) { start = new Date(y0, 0, 1 + 7 - swkd); pos = z; } var t = new Date(y1, 0, 1); var delta = unixtime(t) - unixtime(start); var w = delta / (60 * 60 * 24 * 7); y1 -= y0; pos += Math.floor(w) * z + y1 * m[1]; return pos; }; var calendar_timer_func = function() { var now = new Date(); var g_el = d3.select('#scrollable-time-strip'); g_el.each(function(d, i, g) { // make this behave like a setInterval() -- yet without the flooding of the JS stack when the system is loaded: d.t = setTimeout(d.tf, Math.round(1000 / d.fps)); var dx; var strip_dims = g_el.node().getBBox(); var svg_el = d3.select('#chart svg'); var svg_dims = { width: parseFloat(svg_el.attr('width')), height: parseFloat(svg_el.attr('height')) }; var svg_bb = svg_el.node().getBBox(); var dt = now.getTime() - d.last_time.getTime(); dt = dt / d.fps; dx = d.x + (d.direction ? 1 : -1) * d.step * dt; dx = Math.max(0, dx); dx = Math.min(strip_dims.width - svg_dims.width + m[1] + m[3], dx); if (d3_debug) console.log('periodical: ', d.step, dt, Math.round(dx), Math.round(d.x), dx, d.x); // stop the timer when we've hit the edge: if (d.x == dx && dt != 0) { stop_calendar_timer(g_el); return; } d.x = dx; d.last_time = now; // update the stored data: g_el.data(function(parent_data, i) { return [d]; }) .attr("transform", function(d, i, g) { // this = the SVG element var g_el = d3.select(this); if (d3_debug) console.log('periodical: each.attr: ', this, d, i, g); // round the x/y coordinates to integer values for improved rendering: return "translate(" + Math.round(-d.x) + "," + d.y + ")"; }); }); }; function stop_calendar_timer(g_el) { g_el.each(function(d, i, g) { if (d.t !== false) { clearInterval(d.t); } d.t = false; g_el.data(function(parent_data, i) { // update the stored data: return [d]; }); }); }; var svg = d3.select("#chart") /* .selectAll("svg") -- or the first .data().each() sequence won't work! (--> .data().enter().each() ) */ /* .data(d3.range(1993, 2011)) -- range doesn't belong with SVG node, at least not where I come from... .enter() */ .append("svg") .attr("width", w + m[1] + m[3]) .attr("height", h + m[0] + m[2]) .attr("class", "RdYlGn") .append("g") .attr("id", 'scrollable-time-strip') .data(function() { // just a bit of bogus; this will have you jump to the second year in the calendar strip var x = year_position(1994, start_date); // produce array with one element as we bind to a single group element, i.e. array of size 1 return [{ x: x, y: 0, t: false, // no timer set initially (see below for more info why we need a timer) tf: calendar_timer_func, // no timer function set initially last_time: null, // the last time the timer func was executed fps: 50, // the desired scroll update rate ('frames per second') step: 0, // scroll step per timer tick direction: false // false: left, true: right }]; }) // the way to access the DATA stored in the SVG element is by invoking each(): .each(function(d, i, group_idx) { var g_el = d3.select(this); if (d3_debug) console.log('each: ', this, d3.select(this), svg, d, i, group_idx); g_el.attr("transform", function(d, i, g) { // this = the SVG element if (d3_debug) console.log('each.attr: ', this, d3.select(this), svg, d, i, g); return "translate(" + (-d.x) + "," + d.y + ")"; }); }) .selectAll("g.year-chunk") .data(d3.range(start_date, 2011)) .enter() .append("g") .attr("class", "year-chunk") .attr("transform", function(d, i) { var x = year_position(d, start_date); var y = m[0] + (h - z * 7) / 2; x += m[1]; return "translate(" + x + "," + y + ")"; }); svg.append("text") .attr("transform", function(d) { var t0 = new Date(d, 0, 1), d0 = +day(t0), w0 = +week(t0), x = -6, y = z * 3.5; // move year text up/down and possibly to the right when previous year would otherwise partly cover the year text: switch (d0) { case 0: case 1: case 2: break; case 3: y += z; break; case 4: y = z * 2.5; x += z; break; default: x += z; break; } return "translate(" + x + "," + y + ")rotate(-90)"; }) .attr("text-anchor", "middle") .attr("class", "year") .text(String); var rect = svg.selectAll("rect.day") .data(function(d) { return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) .enter().append("rect") .attr("class", "day") .attr("width", z) .attr("height", z) .attr("x", function(d) { return week(d) * z; }) .attr("y", function(d) { return day(d) * z; }) .map(format); rect.append("title") .text(function(d) { return d; }); svg.selectAll("path.month") .data(function(d) { return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) .enter().append("path") .attr("class", "month") .attr("d", monthPath); d3.csv("vix.csv", function(csv) { var data = d3.nest() .key(function(d) { return d.Date; }) .rollup(function(d) { return d[0].Open; }) .map(csv); color.domain(d3.values(data)); rect.filter(function(d) { return d in data; }) .attr("class", function(d) { return "day q" + color(data[d]) + "-9"; }) .select("title") .text(function(d) { return d + ": " + data[d]; }); /* and add event listeners to facilitate horizontal scrolling on mouseover. To do it right, the mousemove event should be attached to the SVG node rather than the tranlated strip itself, as all calculations and sensitive areas really are related to that outer SVG node! ----------------------------------------------------------- When the scroll-sensitive area is entered, we start a timer. We need the timer for smooth scrolling as the 'mousemove' event will only happen when the mouse MOVES, i.e. positioning the mouse some place in the scroll zone and then keeping it there, will NOT refire the mousemove event after we scrolled the content ONCE due to the actual mousemove event. Hence, for SMOOTH scrolling, we need 'mousemove' to tell us 'how much', while the timer event will ensure that the scrolling is actually taking place 'continuously'. To keep CPU load reasonable, we destroy the timer when the mouse moves OUTSIDE the scroll-sensitive areas. */ var svg_el = d3.select('#chart svg') .on("mousemove", function() { // this = the SVG element var svg_el = d3.select(this); var g_el = d3.select('#scrollable-time-strip'); var mouse = d3.svg.mouse(svg_el.node()); // do not use getBBox() as it won't take the set width/height into account but instead reports the unclipped dimensions: var svg_dims = { width: parseFloat(svg_el.attr('width')), height: parseFloat(svg_el.attr('height')) }; var svg_bb = svg_el.node().getBBox(); var x, delta, dir; var scroll_strip_width = 150; x = mouse[0]; dir = (x >= svg_dims.width / 2); if (!dir) { delta = Math.max(0, scroll_strip_width - x); } else { delta = Math.max(0, scroll_strip_width + x - svg_dims.width - 1); } if (!delta) { stop_calendar_timer(g_el); return; } delta /= scroll_strip_width; g_el.each(function(d, i, g) { // accelerated horizontal scroll: delta = (delta * delta * 150 + delta * 1) * scroll_strip_width / d.fps; // create timer: stop_calendar_timer(g_el); d.t = setTimeout(d.tf, 1); d.last_time = new Date(); // update the stored data: d.step = delta / d.fps; d.direction = dir; g_el.data(function(parent_data, i) { return [d]; }); }); if (d3_debug) console.log('mousemove: ', this, svg_el, g_el, mouse, svg_dims, delta); }) .on("mouseout", function(a, b, c, d) { // this = the SVG element var svg_el = d3.select(this); var g_el = d3.select('#scrollable-time-strip'); stop_calendar_timer(g_el); if (d3_debug) console.log('mouseout: ', this, svg_el, g_el, svg, a, b, c, d); }); }); function monthPath(t0) { var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0), d0 = +day(t0), w0 = +week(t0), d1 = +day(t1), w1 = +week(t1); return "M" + (w0 + 1) * z + "," + d0 * z + "H" + w0 * z + "V" + 7 * z + "H" + w1 * z + "V" + (d1 + 1) * z + "H" + (w1 + 1) * z + "V" + 0 + "H" + (w0 + 1) * z + "Z"; }