(function(){d3.chart = {}; // Inspired by http://informationandvisualization.de/blog/box-plot d3.chart.boxplot = function() { var width = 1, height = 1, duration = 0, domain = null, value = Number, whiskers = d3_chart_boxplotWhiskers, tickFormat = null; // For each small multiple… function boxplot(g) { g.each(function(d, i) { d = d.map(value).sort(d3.ascending); var whiskerIndices = whiskers.call(this, d), whiskerData = whiskerIndices.map(function(i) { return d[i]; }), firstWhisker = whiskerIndices[0], lastWhisker = whiskerIndices[whiskerIndices.length - 1], whiskerRange = lastWhisker - firstWhisker, min = d[0], q1 = d[firstWhisker + Math.floor(.25 * whiskerRange)], median = d[firstWhisker + Math.floor(.5 * whiskerRange)], q3 = d[firstWhisker + Math.floor(.75 * whiskerRange)], max = d[d.length-1], g = d3.select(this); // Compute the new x-scale. var x1 = d3.scale.linear() .domain(domain ? domain.call(this, d, i) : [min, max]) .range([height, 0]); // Retrieve the old x-scale, if this is an update. var x0 = this.__chart__ || d3.scale.linear() .domain([0, Infinity]) .range(x1.range()); // Stash the new scale. this.__chart__ = x1; // Update center line. var center = g.selectAll("line.center") .data([[min, max]]); center.enter().append("svg:line") .attr("class", "center") .attr("x1", width / 2) .attr("y1", function(d) { return x0(d[0]); }) .attr("x2", width / 2) .attr("y2", function(d) { return x0(d[1]); }) .transition() .duration(duration) .attr("y1", function(d) { return x1(d[0]); }) .attr("y2", function(d) { return x1(d[1]); }); center.transition() .duration(duration) .attr("y1", function(d) { return x1(d[0]); }) .attr("y2", function(d) { return x1(d[1]); }); center.exit().remove(); // Update boxes. var box = g.selectAll("rect.box") .data([[q1, q3]]); box.enter().append("svg:rect") .attr("class", "box") .attr("x", 0) .attr("y", function(d) { return x0(d[1]); }) .attr("width", width) .attr("height", function(d) { return x0(d[0]) - x0(d[1]); }) .transition() .duration(duration) .attr("y", function(d) { return x1(d[1]); }) .attr("height", function(d) { return x1(d[0]) - x1(d[1]); }); box.transition() .duration(duration) .attr("y", function(d) { return x1(d[1]); }) .attr("height", function(d) { return x1(d[0]) - x1(d[1]); }); box.exit().remove(); // Update median line var medianLine = g.selectAll("line.median") .data([median]); medianLine.enter().append("svg:line") .attr("class", "median") .attr("x1", 0) .attr("y1", x0) .attr("x2", width) .attr("y2", x0) .transition() .duration(duration) .attr("y1", x1) .attr("y2", x1); medianLine.transition() .duration(duration) .attr("y1", x1) .attr("y2", x1); medianLine.exit().remove(); // Update whiskers. var whisker = g.selectAll("line.whisker") .data(whiskerData); whisker.enter().append("svg:line") .attr("class", "whisker") .attr("x1", 0) .attr("y1", x0) .attr("x2", width) .attr("y2", x0) .transition() .duration(duration) .attr("y1", x1) .attr("y2", x1); whisker.transition() .duration(duration) .attr("y1", x1) .attr("y2", x1); whisker.exit().remove(); // Compute the tick format. var format = tickFormat || x1.tickFormat(8); // Update ticks. var tick = g.selectAll("text") .data([min, q1, median, q3, max]); tick.enter().append("svg:text") .attr("dy", ".3em") .attr("dx", function(d, i) { return i&1 ? 8 : -8 }) .attr("x", function(d, i) { return i&1 ? width : 0 }) .attr("y", x0) .attr("text-anchor", function(d, i) { return i&1 ? "start" : "end"; }) .text(format) .transition() .duration(duration) .attr("y", x1); tick.text(format) .transition() .duration(duration) .attr("y", x1); }); } boxplot.width = function(x) { if (!arguments.length) return width; width = x; return boxplot; }; boxplot.height = function(x) { if (!arguments.length) return height; height = x; return boxplot; }; boxplot.tickFormat = function(x) { if (!arguments.length) return tickFormat; tickFormat = x; return boxplot; }; boxplot.duration = function(x) { if (!arguments.length) return duration; duration = x; return boxplot; }; boxplot.domain = function(x) { if (!arguments.length) return domain; domain = d3.functor(x); return boxplot; }; boxplot.value = function(x) { if (!arguments.length) return value; value = x; return boxplot; }; boxplot.whiskers = function(x) { if (!arguments.length) return value; whiskers = x; return boxplot; }; return boxplot; }; function d3_chart_boxplotWhiskers(d) { return [0, d.length-1]; } // ranges (bad, satisfactory, good) // measures (actual, forecast) // markers (previous, goal) /* * Chart design based on the recommendations of Stephen Few. Implementation * based on the work of Clint Ivy, Jamie Love, and Jason Davies. * http://projects.instantcognition.com/protovis/bulletchart/ */ d3.chart.bullet = function() { var orient = "left", // TODO top & bottom reverse = false, duration = 0, ranges = d3_chart_bulletRanges, markers = d3_chart_bulletMarkers, measures = d3_chart_bulletMeasures, width = 380, height = 30, tickFormat = null; // For each small multiple… function bullet(g) { g.each(function(d, i) { var rangez = ranges.call(this, d, i).slice().sort(d3.descending), markerz = markers.call(this, d, i).slice().sort(d3.descending), measurez = measures.call(this, d, i).slice().sort(d3.descending), g = d3.select(this); // Compute the new x-scale. var x1 = d3.scale.linear() .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) .range(reverse ? [width, 0] : [0, width]); // Retrieve the old x-scale, if this is an update. var x0 = this.__chart__ || d3.scale.linear() .domain([0, Infinity]) .range(x1.range()); // Stash the new scale. this.__chart__ = x1; // Derive width-scales from the x-scales. var w0 = d3_chart_bulletWidth(x0), w1 = d3_chart_bulletWidth(x1); // Update the range rects. var range = g.selectAll("rect.range") .data(rangez); range.enter().append("svg:rect") .attr("class", function(d, i) { return "range s" + i; }) .attr("width", w0) .attr("height", height) .attr("x", reverse ? x0 : 0) .transition() .duration(duration) .attr("width", w1) .attr("x", reverse ? x1 : 0); range.transition() .duration(duration) .attr("x", reverse ? x1 : 0) .attr("width", w1) .attr("height", height); // Update the measure rects. var measure = g.selectAll("rect.measure") .data(measurez); measure.enter().append("svg:rect") .attr("class", function(d, i) { return "measure s" + i; }) .attr("width", w0) .attr("height", height / 3) .attr("x", reverse ? x0 : 0) .attr("y", height / 3) .transition() .duration(duration) .attr("width", w1) .attr("x", reverse ? x1 : 0); measure.transition() .duration(duration) .attr("width", w1) .attr("height", height / 3) .attr("x", reverse ? x1 : 0) .attr("y", height / 3); // Update the marker lines. var marker = g.selectAll("line.marker") .data(markerz); marker.enter().append("svg:line") .attr("class", "marker") .attr("x1", x0) .attr("x2", x0) .attr("y1", height / 6) .attr("y2", height * 5 / 6) .transition() .duration(duration) .attr("x1", x1) .attr("x2", x1); marker.transition() .duration(duration) .attr("x1", x1) .attr("x2", x1) .attr("y1", height / 6) .attr("y2", height * 5 / 6); // Compute the tick format. var format = tickFormat || x1.tickFormat(8); // Update the tick groups. var tick = g.selectAll("g.tick") .data(x1.ticks(8), function(d) { return this.textContent || format(d); }); // Initialize the ticks with the old scale, x0. var tickEnter = tick.enter().append("svg:g") .attr("class", "tick") .attr("transform", d3_chart_bulletTranslate(x0)) .attr("opacity", 1e-6); tickEnter.append("svg:line") .attr("y1", height) .attr("y2", height * 7 / 6); tickEnter.append("svg:text") .attr("text-anchor", "middle") .attr("dy", "1em") .attr("y", height * 7 / 6) .text(format); // Transition the entering ticks to the new scale, x1. tickEnter.transition() .duration(duration) .attr("transform", d3_chart_bulletTranslate(x1)) .attr("opacity", 1); // Transition the updating ticks to the new scale, x1. var tickUpdate = tick.transition() .duration(duration) .attr("transform", d3_chart_bulletTranslate(x1)) .attr("opacity", 1); tickUpdate.select("line") .attr("y1", height) .attr("y2", height * 7 / 6); tickUpdate.select("text") .attr("y", height * 7 / 6); // Transition the exiting ticks to the new scale, x1. tick.exit().transition() .duration(duration) .attr("transform", d3_chart_bulletTranslate(x1)) .attr("opacity", 1e-6) .remove(); }); } // left, right, top, bottom bullet.orient = function(x) { if (!arguments.length) return orient; orient = x; reverse = orient == "right" || orient == "bottom"; return bullet; }; bullet.ranges = function(x) { if (!arguments.length) return ranges; ranges = x; return bullet; }; bullet.markers = function(x) { if (!arguments.length) return markers; markers = x; return bullet; }; bullet.measures = function(x) { if (!arguments.length) return measures; measures = x; return bullet; }; bullet.width = function(x) { if (!arguments.length) return width; width = x; return bullet; }; bullet.height = function(x) { if (!arguments.length) return height; height = x; return bullet; }; bullet.tickFormat = function(x) { if (!arguments.length) return tickFormat; tickFormat = x; return bullet; }; bullet.duration = function(x) { if (!arguments.length) return duration; duration = x; return bullet; }; return bullet; }; function d3_chart_bulletRanges(d) { return d.ranges; } function d3_chart_bulletMarkers(d) { return d.markers; } function d3_chart_bulletMeasures(d) { return d.measures; } function d3_chart_bulletTranslate(x) { return function(d) { return "translate(" + x(d) + ",0)"; }; } function d3_chart_bulletWidth(x) { var x0 = x(0); return function(d) { return Math.abs(x(d) - x0); }; } })()