text: (function(exports){
  var cubism = exports.cubism = {version: "1.2.2"};
  var cubism_id = 0;
  function cubism_identity(d) { return d; }
  cubism.option = function(name, defaultValue) {
    var values = cubism.options(name);
    return values.length ? values[0] : defaultValue;
  };
  
  cubism.options = function(name, defaultValues) {
    var options = location.search.substring(1).split("&"),
        values = [],
        i = -1,
        n = options.length,
        o;
    while (++i < n) {
      if ((o = options[i].split("="))[0] == name) {
        values.push(decodeURIComponent(o[1]));
      }
    }
    return values.length || arguments.length < 2 ? values : defaultValues;
  };
  cubism.context = function() {
    var context = new cubism_context,
        step = 1e4, // ten seconds, in milliseconds
        size = 1440, // four hours at ten seconds, in pixels
        start0, stop0, // the start and stop for the previous change event
        start1, stop1, // the start and stop for the next prepare event
        serverDelay = 5e3,
        clientDelay = 5e3,
        event = d3.dispatch("prepare", "beforechange", "change", "focus"),
        scale = context.scale = d3.time.scale().range([0, size]),
        timeout,
        focus;
  
    function update() {
      var now = Date.now();
      stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step);
      start0 = new Date(stop0 - size * step);
      stop1 = new Date(Math.floor((now - serverDelay) / step) * step);
      start1 = new Date(stop1 - size * step);
      scale.domain([start0, stop0]);
      return context;
    }
  
    context.start = function() {
      if (timeout) clearTimeout(timeout);
      var delay = +stop1 + serverDelay - Date.now();
  
      // If we're too late for the first prepare event, skip it.
      if (delay < clientDelay) delay += step;
  
      timeout = setTimeout(function prepare() {
        stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step);
        start1 = new Date(stop1 - size * step);
        event.prepare.call(context, start1, stop1);
  
        setTimeout(function() {
          scale.domain([start0 = start1, stop0 = stop1]);
          event.beforechange.call(context, start1, stop1);
          event.change.call(context, start1, stop1);
          event.focus.call(context, focus);
        }, clientDelay);
  
        timeout = setTimeout(prepare, step);
      }, delay);
      return context;
    };
  
    context.stop = function() {
      timeout = clearTimeout(timeout);
      return context;
    };
  
    timeout = setTimeout(context.start, 10);
  
    // Set or get the step interval in milliseconds.
    // Defaults to ten seconds.
    context.step = function(_) {
      if (!arguments.length) return step;
      step = +_;
      return update();
    };
  
    // Set or get the context size (the count of metric values).
    // Defaults to 1440 (four hours at ten seconds).
    context.size = function(_) {
      if (!arguments.length) return size;
      scale.range([0, size = +_]);
      return update();
    };
  
    // The server delay is the amount of time we wait for the server to compute a
    // metric. This delay may result from clock skew or from delays collecting
    // metrics from various hosts. Defaults to 4 seconds.
    context.serverDelay = function(_) {
      if (!arguments.length) return serverDelay;
      serverDelay = +_;
      return update();
    };
  
    // The client delay is the amount of additional time we wait to fetch those
    // metrics from the server. The client and server delay combined represent the
    // age of the most recent displayed metric. Defaults to 1 second.
    context.clientDelay = function(_) {
      if (!arguments.length) return clientDelay;
      clientDelay = +_;
      return update();
    };
  
    // Sets the focus to the specified index, and dispatches a "focus" event.
    context.focus = function(i) {
      event.focus.call(context, focus = i);
      return context;
    };
  
    // Add, remove or get listeners for events.
    context.on = function(type, listener) {
      if (arguments.length < 2) return event.on(type);
  
      event.on(type, listener);
  
      // Notify the listener of the current start and stop time, as appropriate.
      // This way, metrics can make requests for data immediately,
      // and likewise the axis can display itself synchronously.
      if (listener != null) {
        if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1);
        if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0);
        if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0);
        if (/^focus(\.|$)/.test(type)) listener.call(context, focus);
      }
  
      return context;
    };
  
    d3.select(window).on("keydown.context-" + ++cubism_id, function() {
      switch (!d3.event.metaKey && d3.event.keyCode) {
        case 37: // left
          if (focus == null) focus = size - 1;
          if (focus > 0) context.focus(--focus);
          break;
        case 39: // right
          if (focus == null) focus = size - 2;
          if (focus < size - 1) context.focus(++focus);
          break;
        default: return;
      }
      d3.event.preventDefault();
    });
  
    return update();
  };
  
  function cubism_context() {}
  
  var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype;
  
  cubism_contextPrototype.constant = function(value) {
    return new cubism_metricConstant(this, +value);
  };
  cubism_contextPrototype.cube = function(host) {
    if (!arguments.length) host = "";
    var source = {},
        context = this;
  
    source.metric = function(expression) {
      return context.metric(function(start, stop, step, callback) {
        d3.json(host + "/1.0/metric"
            + "?expression=" + encodeURIComponent(expression)
            + "&start=" + cubism_cubeFormatDate(start)
            + "&stop=" + cubism_cubeFormatDate(stop)
            + "&step=" + step, function(data) {
          if (!data) return callback(new Error("unable to load data"));
          callback(null, data.map(function(d) { return d.value; }));
        });
      }, expression += "");
    };
  
    // Returns the Cube host.
    source.toString = function() {
      return host;
    };
  
    return source;
  };
  
  var cubism_cubeFormatDate = d3.time.format.iso;
  cubism_contextPrototype.graphite = function(host) {
    if (!arguments.length) host = "";
    var source = {},
        context = this;
  
    source.metric = function(expression) {
      var sum = "sum";
  
      var metric = context.metric(function(start, stop, step, callback) {
        var target = expression;
  
        // Apply the summarize, if necessary.
        if (step !== 1e4) target = "summarize(" + target + ",'"
            + (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step / 1e3 + "sec")
            + "','" + sum + "')";
  
        d3.text(host + "/render?format=raw"
            + "&target=" + encodeURIComponent("alias(" + target + ",'')")
            + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two?
            + "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) {
          if (!text) return callback(new Error("unable to load data"));
          callback(null, cubism_graphiteParse(text));
        });
      }, expression += "");
  
      metric.summarize = function(_) {
        sum = _;
        return metric;
      };
  
      return metric;
    };
  
    source.find = function(pattern, callback) {
      d3.json(host + "/metrics/find?format=completer"
          + "&query=" + encodeURIComponent(pattern), function(result) {
        if (!result) return callback(new Error("unable to find metrics"));
        callback(null, result.metrics.map(function(d) { return d.path; }));
      });
    };
  
    // Returns the graphite host.
    source.toString = function() {
      return host;
    };
  
    return source;
  };
  
  // Graphite understands seconds since UNIX epoch.
  function cubism_graphiteFormatDate(time) {
    return Math.floor(time / 1000);
  }
  
  // Helper method for parsing graphite's raw format.
  function cubism_graphiteParse(text) {
    var i = text.indexOf("|"),
        meta = text.substring(0, i),
        c = meta.lastIndexOf(","),
        b = meta.lastIndexOf(",", c - 1),
        a = meta.lastIndexOf(",", b - 1),
        start = meta.substring(a + 1, b) * 1000,
        step = meta.substring(c + 1) * 1000;
    return text
        .substring(i + 1)
        .split(",")
        .slice(1) // the first value is always None?
        .map(function(d) { return +d; });
  }
  
  cubism_contextPrototype.cm = function(host, tenant, token) {
    if (!arguments.length) host = "";
    var source = {},
        context = this,
        TOKEN;
  
    function makeid()
    {
      var text = "";
      var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  
      for( var i=0; i < 5; i++ )
          text += possible.charAt(Math.floor(Math.random() * possible.length));
  
      return text;
    }
  
    TOKEN = makeid();
  
  
    function wrapJsonP(url, callback) {
      var sep = '&';
      if (url.indexOf('?') === -1) {
        sep = '?'
      }
  
      window[TOKEN] = function() {};
  
      d3.text(url
            + sep
            + "JSONP=" + TOKEN
            + "&x-auth-token=" + token, 'application/json', function(data) {
        if (!data) {
          return callback(new Error("unable to load data"));
        }
        if (!data.indexOf(TOKEN) === 0) {
          return callback(new Error("Token not found at beginning."));
        }
  
        data = JSON.parse(data.substring(TOKEN.length + 1, data.length - 2));
        data = data.values;
        callback(null, data);
      });
    }
  
    function processTitle(expression) {
      var title = [source.data[expression[0]].label,
                  source.data[expression[0]].checks[expression[1]].label,
                  expression[2]].join(' - ');
      return title;
  
    }
  
    source.metric = function(expression) {
      return context.metric(function(start, stop, step, callback) {
        var points = (stop - start) / step;
        // Hard code 30s min
        step = Math.min(step, 30000);
        wrapJsonP(host + "/v1.0/" + tenant + "/entities/" + expression[0] + "/checks/" + expression[1] + "/metrics/" + expression[2] + '/plot'
            + "?from=" + start.getTime()
            + "&to=" + stop.getTime()
            + "&points=" + points, function(err, data) {
  
          if (err) {
            callback(err, data);
            return;
          }
  
          callback(null, data.map(function(d) { 
            return d.average;
          }));
        });
      }, expression[3]);
    };
  
    source.initialize = function(callback) {
      if (source.initialized) {
        callback();
        return;
      }
  
      wrapJsonP(host + "/v1.0/" + tenant + "/views/overview", function(err, data) {
        var i,
            j,
            k,
            payload,
            check;
        if (err) {
          callback(err, data);
          return;
        }
  
        source.initialized = true;
        source.data = {};
        for (i = 0; i < data.length; i++) {
          payload = data[i];
          source.data[payload.entity.id] = payload.entity;
          payload.entity.checks = {};
          for (j = 0; j < payload.checks.length; j++) {
            check = payload.checks[j];
            payload.entity.checks[check.id] = check;
            check.alarms = {};
          }
          for (j = 0; j < payload.alarms.length; j++) {
            alarm = payload.alarms[j];
            payload.entity.checks[alarm.check_id].alarms[alarm.id] = alarm;
          }
        }
        callback();
      });
    };
  
  
    source.find_by_label_and_check_type = function(label, type, metric, callback) {
      var en, ch, i, k, accum = [], result = [], tmp;
  
      source.initialize(function() {
        for (k in source.data) {
          en = source.data[k];
          if (en.label.indexOf(label) !== -1) {
            accum.push(en);
          }
        }
  
        for (i = 0; i < accum.length; i++) {
          en = accum[i];
          for (k in en.checks) {
            ch = en.checks[k];
            if (ch.type === type) {
              tmp = [en.id, ch.id, metric];
              tmp.push(processTitle(tmp));
              result.push(tmp);
            }
          }
        }
        callback(null, result);
      });
    };
  
    // Returns the Cube host.
    source.toString = function() {
      return host;
    };
  
    return source;
  };
  function cubism_metric(context) {
    if (!(context instanceof cubism_context)) throw new Error("invalid context");
    this.context = context;
  }
  
  var cubism_metricPrototype = cubism_metric.prototype;
  
  cubism.metric = cubism_metric;
  
  cubism_metricPrototype.valueAt = function() {
    return NaN;
  };
  
  cubism_metricPrototype.alias = function(name) {
    this.toString = function() { return name; };
    return this;
  };
  
  cubism_metricPrototype.extent = function() {
    var i = 0,
        n = this.context.size(),
        value,
        min = Infinity,
        max = -Infinity;
    while (++i < n) {
      value = this.valueAt(i);
      if (value < min) min = value;
      if (value > max) max = value;
    }
    return [min, max];
  };
  
  cubism_metricPrototype.on = function(type, listener) {
    return arguments.length < 2 ? null : this;
  };
  
  cubism_metricPrototype.shift = function() {
    return this;
  };
  
  cubism_metricPrototype.on = function() {
    return arguments.length < 2 ? null : this;
  };
  
  cubism_contextPrototype.metric = function(request, name) {
    var context = this,
        metric = new cubism_metric(context),
        id = ".metric-" + ++cubism_id,
        start = -Infinity,
        stop,
        step = context.step(),
        size = context.size(),
        values = [],
        event = d3.dispatch("change"),
        listening = 0,
        fetching;
  
    // Prefetch new data into a temporary array.
    function prepare(start1, stop) {
      var steps = Math.min(size, Math.round((start1 - start) / step));
      if (!steps || fetching) return; // already fetched, or fetching!
      fetching = true;
      steps = Math.min(size, steps + cubism_metricOverlap);
      var start0 = new Date(stop - steps * step);
      request(start0, stop, step, function(error, data) {
        fetching = false;
        if (error) return console.warn(error);
        var i = isFinite(start) ? Math.round((start0 - start) / step) : 0;
        for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j];
        event.change.call(metric, start, stop);
      });
    }
  
    // When the context changes, switch to the new data, ready-or-not!
    function beforechange(start1, stop1) {
      if (!isFinite(start)) start = start1;
      values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step))));
      start = start1;
      stop = stop1;
    }
  
    //
    metric.valueAt = function(i) {
      return values[i];
    };
  
    //
    metric.shift = function(offset) {
      return context.metric(cubism_metricShift(request, +offset));
    };
  
    //
    metric.on = function(type, listener) {
      if (!arguments.length) return event.on(type);
  
      // If there are no listeners, then stop listening to the context,
      // and avoid unnecessary fetches.
      if (listener == null) {
        if (event.on(type) != null && --listening == 0) {
          context.on("prepare" + id, null).on("beforechange" + id, null);
        }
      } else {
        if (event.on(type) == null && ++listening == 1) {
          context.on("prepare" + id, prepare).on("beforechange" + id, beforechange);
        }
      }
  
      event.on(type, listener);
  
      // Notify the listener of the current start and stop time, as appropriate.
      // This way, charts can display synchronous metrics immediately.
      if (listener != null) {
        if (/^change(\.|$)/.test(type)) listener.call(context, start, stop);
      }
  
      return metric;
    };
  
    //
    if (arguments.length > 1) metric.toString = function() {
      return name;
    };
  
    return metric;
  };
  
  // Number of metric to refetch each period, in case of lag.
  var cubism_metricOverlap = 6;
  
  // Wraps the specified request implementation, and shifts time by the given offset.
  function cubism_metricShift(request, offset) {
    return function(start, stop, step, callback) {
      request(new Date(+start + offset), new Date(+stop + offset), step, callback);
    };
  }
  function cubism_metricConstant(context, value) {
    cubism_metric.call(this, context);
    value = +value;
    var name = value + "";
    this.valueOf = function() { return value; };
    this.toString = function() { return name; };
  }
  
  var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype);
  
  cubism_metricConstantPrototype.valueAt = function() {
    return +this;
  };
  
  cubism_metricConstantPrototype.extent = function() {
    return [+this, +this];
  };
  function cubism_metricOperator(name, operate) {
  
    function cubism_metricOperator(left, right) {
      if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right);
      else if (left.context !== right.context) throw new Error("mismatch context");
      cubism_metric.call(this, left.context);
      this.left = left;
      this.right = right;
      this.toString = function() { return left + " " + name + " " + right; };
    }
  
    var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype);
  
    cubism_metricOperatorPrototype.valueAt = function(i) {
      return operate(this.left.valueAt(i), this.right.valueAt(i));
    };
  
    cubism_metricOperatorPrototype.shift = function(offset) {
      return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset));
    };
  
    cubism_metricOperatorPrototype.on = function(type, listener) {
      if (arguments.length < 2) return this.left.on(type);
      this.left.on(type, listener);
      this.right.on(type, listener);
      return this;
    };
  
    return function(right) {
      return new cubism_metricOperator(this, right);
    };
  }
  
  cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) {
    return left + right;
  });
  
  cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) {
    return left - right;
  });
  
  cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) {
    return left * right;
  });
  
  cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) {
    return left / right;
  });
  cubism_contextPrototype.horizon = function() {
    var context = this,
        mode = "offset",
        buffer = document.createElement("canvas"),
        width = buffer.width = context.size(),
        height = buffer.height = 30,
        scale = d3.scale.linear().interpolate(d3.interpolateRound),
        metric = cubism_identity,
        extent = null,
        title = cubism_identity,
        format = d3.format(".2s"),
        colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];
  
    function horizon(selection) {
  
      selection
          .on("mousemove.horizon", function() { context.focus(Math.round(d3.mouse(this)[0])); })
          .on("mouseout.horizon", function() { context.focus(null); });
  
      selection.append("canvas")
          .attr("width", width)
          .attr("height", height);
  
      selection.append("span")
          .attr("class", "title")
          .text(title);
  
      selection.append("span")
          .attr("class", "value");
  
      selection.each(function(d, i) {
        var that = this,
            id = ++cubism_id,
            metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric,
            colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors,
            extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
            start = -Infinity,
            step = context.step(),
            canvas = d3.select(that).select("canvas"),
            span = d3.select(that).select(".value"),
            max_,
            m = colors_.length >> 1,
            ready;
  
        canvas.datum({id: id, metric: metric_});
        canvas = canvas.node().getContext("2d");
  
        function change(start1, stop) {
          canvas.save();
  
          // compute the new extent and ready flag
          var extent = metric_.extent();
          ready = extent.every(isFinite);
          if (extent_ != null) extent = extent_;
  
          // if this is an update (with no extent change), copy old values!
          var i0 = 0, max = Math.max(-extent[0], extent[1]);
          if (this === context) {
            if (max == max_) {
              i0 = width - cubism_metricOverlap;
              var dx = (start1 - start) / step;
              if (dx < width) {
                var canvas0 = buffer.getContext("2d");
                canvas0.clearRect(0, 0, width, height);
                canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height);
                canvas.clearRect(0, 0, width, height);
                canvas.drawImage(canvas0.canvas, 0, 0);
              }
            }
            start = start1;
          }
  
          // update the domain
          scale.domain([0, max_ = max]);
  
          // clear for the new data
          canvas.clearRect(i0, 0, width - i0, height);
  
          // record whether there are negative values to display
          var negative;
  
          // positive bands
          for (var j = 0; j < m; ++j) {
            canvas.fillStyle = colors_[m + j];
  
            // Adjust the range based on the current band index.
            var y0 = (j - m + 1) * height;
            scale.range([m * height + y0, y0]);
            y0 = scale(0);
  
            for (var i = i0, n = width, y1; i < n; ++i) {
              y1 = metric_.valueAt(i);
              if (y1 <= 0) { negative = true; continue; }
              canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1);
            }
          }
  
          if (negative) {
            // enable offset mode
            if (mode === "offset") {
              canvas.translate(0, height);
              canvas.scale(1, -1);
            }
  
            // negative bands
            for (var j = 0; j < m; ++j) {
              canvas.fillStyle = colors_[m - 1 - j];
  
              // Adjust the range based on the current band index.
              var y0 = (j - m + 1) * height;
              scale.range([m * height + y0, y0]);
              y0 = scale(0);
  
              for (var i = i0, n = width, y1; i < n; ++i) {
                y1 = metric_.valueAt(i);
                if (y1 >= 0) continue;
                canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1));
              }
            }
          }
  
          canvas.restore();
        }
  
        function focus(i) {
          if (i == null) i = width - 1;
          var value = metric_.valueAt(i);
          span.datum(value).text(isNaN(value) ? null : format);
        }
  
        // Update the chart when the context changes.
        context.on("change.horizon-" + id, change);
        context.on("focus.horizon-" + id, focus);
  
        // Display the first metric change immediately,
        // but defer subsequent updates to the canvas change.
        // Note that someone still needs to listen to the metric,
        // so that it continues to update automatically.
        metric_.on("change.horizon-" + id, function(start, stop) {
          change(start, stop), focus();
          if (ready) metric_.on("change.horizon-" + id, cubism_identity);
        });
      });
    }
  
    horizon.remove = function(selection) {
  
      selection
          .on("mousemove.horizon", null)
          .on("mouseout.horizon", null);
  
      selection.selectAll("canvas")
          .each(remove)
          .remove();
  
      selection.selectAll(".title,.value")
          .remove();
  
      function remove(d) {
        d.metric.on("change.horizon-" + d.id, null);
        context.on("change.horizon-" + d.id, null);
        context.on("focus.horizon-" + d.id, null);
      }
    };
  
    horizon.mode = function(_) {
      if (!arguments.length) return mode;
      mode = _ + "";
      return horizon;
    };
  
    horizon.height = function(_) {
      if (!arguments.length) return height;
      buffer.height = height = +_;
      return horizon;
    };
  
    horizon.metric = function(_) {
      if (!arguments.length) return metric;
      metric = _;
      return horizon;
    };
  
    horizon.scale = function(_) {
      if (!arguments.length) return scale;
      scale = _;
      return horizon;
    };
  
    horizon.extent = function(_) {
      if (!arguments.length) return extent;
      extent = _;
      return horizon;
    };
  
    horizon.title = function(_) {
      if (!arguments.length) return title;
      title = _;
      return horizon;
    };
  
    horizon.format = function(_) {
      if (!arguments.length) return format;
      format = _;
      return horizon;
    };
  
    horizon.colors = function(_) {
      if (!arguments.length) return colors;
      colors = _;
      return horizon;
    };
  
    return horizon;
  };
  cubism_contextPrototype.comparison = function() {
    var context = this,
        width = context.size(),
        height = 120,
        scale = d3.scale.linear().interpolate(d3.interpolateRound),
        primary = function(d) { return d[0]; },
        secondary = function(d) { return d[1]; },
        extent = null,
        title = cubism_identity,
        formatPrimary = cubism_comparisonPrimaryFormat,
        formatChange = cubism_comparisonChangeFormat,
        colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"],
        strokeWidth = 1.5;
  
    function comparison(selection) {
  
      selection
          .on("mousemove.comparison", function() { context.focus(Math.round(d3.mouse(this)[0])); })
          .on("mouseout.comparison", function() { context.focus(null); });
  
      selection.append("canvas")
          .attr("width", width)
          .attr("height", height);
  
      selection.append("span")
          .attr("class", "title")
          .text(title);
  
      selection.append("span")
          .attr("class", "value primary");
  
      selection.append("span")
          .attr("class", "value change");
  
      selection.each(function(d, i) {
        var that = this,
            id = ++cubism_id,
            primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary,
            secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary,
            extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
            div = d3.select(that),
            canvas = div.select("canvas"),
            spanPrimary = div.select(".value.primary"),
            spanChange = div.select(".value.change"),
            ready;
  
        canvas.datum({id: id, primary: primary_, secondary: secondary_});
        canvas = canvas.node().getContext("2d");
  
        function change(start, stop) {
          canvas.save();
          canvas.clearRect(0, 0, width, height);
  
          // update the scale
          var primaryExtent = primary_.extent(),
              secondaryExtent = secondary_.extent(),
              extent = extent_ == null ? primaryExtent : extent_;
          scale.domain(extent).range([height, 0]);
          ready = primaryExtent.concat(secondaryExtent).every(isFinite);
  
          // consistent overplotting
          var round = start / context.step() & 1
              ? cubism_comparisonRoundOdd
              : cubism_comparisonRoundEven;
  
          // positive changes
          canvas.fillStyle = colors[2];
          for (var i = 0, n = width; i < n; ++i) {
            var y0 = scale(primary_.valueAt(i)),
                y1 = scale(secondary_.valueAt(i));
            if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0);
          }
  
          // negative changes
          canvas.fillStyle = colors[0];
          for (i = 0; i < n; ++i) {
            var y0 = scale(primary_.valueAt(i)),
                y1 = scale(secondary_.valueAt(i));
            if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1);
          }
  
          // positive values
          canvas.fillStyle = colors[3];
          for (i = 0; i < n; ++i) {
            var y0 = scale(primary_.valueAt(i)),
                y1 = scale(secondary_.valueAt(i));
            if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth);
          }
  
          // negative values
          canvas.fillStyle = colors[1];
          for (i = 0; i < n; ++i) {
            var y0 = scale(primary_.valueAt(i)),
                y1 = scale(secondary_.valueAt(i));
            if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth);
          }
  
          canvas.restore();
        }
  
        function focus(i) {
          if (i == null) i = width - 1;
          var valuePrimary = primary_.valueAt(i),
              valueSecondary = secondary_.valueAt(i),
              valueChange = (valuePrimary - valueSecondary) / valueSecondary;
  
          spanPrimary
              .datum(valuePrimary)
              .text(isNaN(valuePrimary) ? null : formatPrimary);
  
          spanChange
              .datum(valueChange)
              .text(isNaN(valueChange) ? null : formatChange)
              .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : ""));
        }
  
        // Display the first primary change immediately,
        // but defer subsequent updates to the context change.
        // Note that someone still needs to listen to the metric,
        // so that it continues to update automatically.
        primary_.on("change.comparison-" + id, firstChange);
        secondary_.on("change.comparison-" + id, firstChange);
        function firstChange(start, stop) {
          change(start, stop), focus();
          if (ready) {
            primary_.on("change.comparison-" + id, cubism_identity);
            secondary_.on("change.comparison-" + id, cubism_identity);
          }
        }
  
        // Update the chart when the context changes.
        context.on("change.comparison-" + id, change);
        context.on("focus.comparison-" + id, focus);
      });
    }
  
    comparison.remove = function(selection) {
  
      selection
          .on("mousemove.comparison", null)
          .on("mouseout.comparison", null);
  
      selection.selectAll("canvas")
          .each(remove)
          .remove();
  
      selection.selectAll(".title,.value")
          .remove();
  
      function remove(d) {
        d.primary.on("change.comparison-" + d.id, null);
        d.secondary.on("change.comparison-" + d.id, null);
        context.on("change.comparison-" + d.id, null);
        context.on("focus.comparison-" + d.id, null);
      }
    };
  
    comparison.height = function(_) {
      if (!arguments.length) return height;
      height = +_;
      return comparison;
    };
  
    comparison.primary = function(_) {
      if (!arguments.length) return primary;
      primary = _;
      return comparison;
    };
  
    comparison.secondary = function(_) {
      if (!arguments.length) return secondary;
      secondary = _;
      return comparison;
    };
  
    comparison.scale = function(_) {
      if (!arguments.length) return scale;
      scale = _;
      return comparison;
    };
  
    comparison.extent = function(_) {
      if (!arguments.length) return extent;
      extent = _;
      return comparison;
    };
  
    comparison.title = function(_) {
      if (!arguments.length) return title;
      title = _;
      return comparison;
    };
  
    comparison.formatPrimary = function(_) {
      if (!arguments.length) return formatPrimary;
      formatPrimary = _;
      return comparison;
    };
  
    comparison.formatChange = function(_) {
      if (!arguments.length) return formatChange;
      formatChange = _;
      return comparison;
    };
  
    comparison.colors = function(_) {
      if (!arguments.length) return colors;
      colors = _;
      return comparison;
    };
  
    comparison.strokeWidth = function(_) {
      if (!arguments.length) return strokeWidth;
      strokeWidth = _;
      return comparison;
    };
  
    return comparison;
  };
  
  var cubism_comparisonPrimaryFormat = d3.format(".2s"),
      cubism_comparisonChangeFormat = d3.format("+.0%");
  
  function cubism_comparisonRoundEven(i) {
    return i & 0xfffffe;
  }
  
  function cubism_comparisonRoundOdd(i) {
    return ((i + 1) & 0xfffffe) - 1;
  }
  cubism_contextPrototype.axis = function() {
    var context = this,
        scale = context.scale,
        axis_ = d3.svg.axis().scale(scale);
  
    var format = context.step() < 6e4 ? cubism_axisFormatSeconds
        : context.step() < 864e5 ? cubism_axisFormatMinutes
        : cubism_axisFormatDays;
  
    function axis(selection) {
      var id = ++cubism_id,
          tick;
  
      var g = selection.append("svg")
          .datum({id: id})
          .attr("width", context.size())
          .attr("height", Math.max(28, -axis.tickSize()))
        .append("g")
          .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")")
          .call(axis_);
  
      context.on("change.axis-" + id, function() {
        g.call(axis_);
        if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true)))
            .style("display", "none")
            .text(null);
      });
  
      context.on("focus.axis-" + id, function(i) {
        if (tick) {
          if (i == null) {
            tick.style("display", "none");
            g.selectAll("text").style("fill-opacity", null);
          } else {
            tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
            var dx = tick.node().getComputedTextLength() + 6;
            g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; });
          }
        }
      });
    }
  
    axis.remove = function(selection) {
  
      selection.selectAll("svg")
          .each(remove)
          .remove();
  
      function remove(d) {
        context.on("change.axis-" + d.id, null);
        context.on("focus.axis-" + d.id, null);
      }
    };
  
    return d3.rebind(axis, axis_,
        "orient",
        "ticks",
        "tickSubdivide",
        "tickSize",
        "tickPadding",
        "tickFormat");
  };
  
  var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"),
      cubism_axisFormatMinutes = d3.time.format("%I:%M %p"),
      cubism_axisFormatDays = d3.time.format("%B %d");
  cubism_contextPrototype.rule = function() {
    var context = this,
        metric = cubism_identity;
  
    function rule(selection) {
      var id = ++cubism_id;
  
      var line = selection.append("div")
          .datum({id: id})
          .attr("class", "line")
          .call(cubism_ruleStyle);
  
      selection.each(function(d, i) {
        var that = this,
            id = ++cubism_id,
            metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric;
  
        if (!metric_) return;
  
        function change(start, stop) {
          var values = [];
  
          for (var i = 0, n = context.size(); i < n; ++i) {
            if (metric_.valueAt(i)) {
              values.push(i);
            }
          }
  
          var lines = selection.selectAll(".metric").data(values);
          lines.exit().remove();
          lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle);
          lines.style("left", cubism_ruleLeft);
        }
  
        context.on("change.rule-" + id, change);
        metric_.on("change.rule-" + id, change);
      });
  
      context.on("focus.rule-" + id, function(i) {
        line.datum(i)
            .style("display", i == null ? "none" : null)
            .style("left", i == null ? null : cubism_ruleLeft);
      });
    }
  
    rule.remove = function(selection) {
  
      selection.selectAll(".line")
          .each(remove)
          .remove();
  
      function remove(d) {
        context.on("focus.rule-" + d.id, null);
      }
    };
  
    rule.metric = function(_) {
      if (!arguments.length) return metric;
      metric = _;
      return rule;
    };
  
    return rule;
  };
  
  function cubism_ruleStyle(line) {
    line
        .style("position", "absolute")
        .style("top", 0)
        .style("bottom", 0)
        .style("width", "1px")
        .style("pointer-events", "none");
  }
  
  function cubism_ruleLeft(i) {
    return i + "px";
  }
  })(this);
  