toolkit/javascript/d3/src/chart/bullet.js
changeset 47 c0b4a8b5a012
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/javascript/d3/src/chart/bullet.js	Thu Apr 10 14:20:23 2014 +0200
@@ -0,0 +1,237 @@
+// 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))
+          .style("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))
+          .style("opacity", 1);
+
+      // Transition the updating ticks to the new scale, x1.
+      var tickUpdate = tick.transition()
+          .duration(duration)
+          .attr("transform", d3_chart_bulletTranslate(x1))
+          .style("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))
+          .style("opacity", 1e-6)
+          .remove();
+    });
+    d3.timer.flush();
+  }
+
+  // left, right, top, bottom
+  bullet.orient = function(x) {
+    if (!arguments.length) return orient;
+    orient = x;
+    reverse = orient == "right" || orient == "bottom";
+    return bullet;
+  };
+
+  // ranges (bad, satisfactory, good)
+  bullet.ranges = function(x) {
+    if (!arguments.length) return ranges;
+    ranges = x;
+    return bullet;
+  };
+
+  // markers (previous, goal)
+  bullet.markers = function(x) {
+    if (!arguments.length) return markers;
+    markers = x;
+    return bullet;
+  };
+
+  // measures (actual, forecast)
+  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);
+  };
+}