You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by sr...@apache.org on 2014/05/29 23:02:13 UTC

[4/7] AMBARI-5928. Create and populate Metrics section of a slider app - only Slider view changes. (onechiporenko via srimanth)

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js b/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js
new file mode 100644
index 0000000..5af5241
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js
@@ -0,0 +1,915 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+var string_utils = require('helpers/string_utils');
+
+/**
+ * @class
+ *
+ * This is a view which GETs data from a URL and shows it as a time based line
+ * graph. Time is shown on the X axis with data series shown on Y axis. It
+ * optionally also has the ability to auto refresh itself over a given time
+ * interval.
+ *
+ * This is an abstract class which is meant to be extended.
+ *
+ * Extending classes should override the following:
+ * <ul>
+ * <li>url - from where the data can be retrieved
+ * <li>title - Title to be displayed when showing the chart
+ * <li>id - which uniquely identifies this chart in any page
+ * <li>#transformToSeries(jsonData) - function to map server data into graph
+ * series
+ * </ul>
+ *
+ * Extending classes could optionally override the following:
+ * <ul>
+ * <li>#colorForSeries(series) - function to get custom colors per series
+ * </ul>
+ *
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.ChartView = Ember.View.extend({
+
+  templateName: 'common/chart',
+
+  /**
+   * The URL from which data can be retrieved.
+   *
+   * This property must be provided for the graph to show properly.
+   *
+   * @type String
+   * @default null
+   */
+  url: null,
+
+  /**
+   * A unique ID for this chart.
+   *
+   * @type String
+   * @default null
+   */
+  id: null,
+
+  /**
+   * Title to be shown under the chart.
+   *
+   * @type String
+   * @default null
+   */
+  title: null,
+
+  /**
+   * @private
+   *
+   * @type Rickshaw.Graph
+   * @default null
+   */
+  _graph: null,
+
+  /**
+   * Array of classnames for each series (in widget)
+   * @type Rickshaw.Graph
+   */
+  _popupGraph: null,
+
+  /**
+   * Array of classnames for each series
+   * @type Array
+   */
+  _seriesProperties: null,
+
+  /**
+   * Array of classnames for each series (in widget)
+   * @type Array
+   */
+  _seriesPropertiesWidget: null,
+
+  /**
+   * Renderer type
+   * See <code>Rickshaw.Graph.Renderer</code> for more info
+   * @type String
+   */
+  renderer: 'area',
+
+  /**
+   * Suffix used in DOM-elements selectors
+   * @type String
+   */
+  popupSuffix: '-popup',
+
+  /**
+   * Is popup for current graph open
+   * @type Boolean
+   */
+  isPopup: false,
+
+  /**
+   * Is graph ready
+   * @type Boolean
+   */
+  isReady: false,
+
+  /**
+   * Is popup-graph ready
+   * @type Boolean
+   */
+  isPopupReady: false,
+
+  /**
+   * Is data for graph available
+   * @type Boolean
+   */
+  hasData: true,
+
+  containerId: null,
+  containerClass: null,
+  yAxisId: null,
+  yAxisClass: null,
+  xAxisId: null,
+  xAxisClass: null,
+  legendId: null,
+  legendClass: null,
+  chartId: null,
+  chartClass: null,
+  titleId: null,
+  titleClass: null,
+
+  didInsertElement: function() {
+    var id = this.get('id');
+    var idTemplate = id + '-{element}';
+
+    this.set('containerId', idTemplate.replace('{element}', 'container'));
+    this.set('containerClass', 'chart-container');
+    this.set('yAxisId', idTemplate.replace('{element}', 'yaxis'));
+    this.set('yAxisClass', this.get('yAxisId'));
+    this.set('xAxisId', idTemplate.replace('{element}', 'xaxis'));
+    this.set('xAxisClass', this.get('xAxisId'));
+    this.set('legendId', idTemplate.replace('{element}', 'legend'));
+    this.set('legendClass', this.get('legendId'));
+    this.set('chartId', idTemplate.replace('{element}', 'chart'));
+    this.set('chartClass', this.get('chartId'));
+    this.set('titleId', idTemplate.replace('{element}', 'title'));
+    this.set('titleClass', this.get('titleId'));
+    this.loadData();
+  },
+
+
+  loadData: function() {
+    App.ajax.send({
+      name: this.get('ajaxIndex'),
+      sender: this,
+      data: this.getDataForAjaxRequest(),
+      success: '_refreshGraph',
+      error: 'loadDataErrorCallback'
+    });
+  },
+
+  getDataForAjaxRequest: function() {
+    return {};
+  },
+
+  loadDataErrorCallback: function(xhr, textStatus, errorThrown) {
+    this.set('isReady', true);
+    if (xhr.readyState == 4 && xhr.status) {
+      textStatus = xhr.status + " " + textStatus;
+    }
+    this._showMessage('warn', 'graphs.error.title', textStatus + ' ' + errorThrown);
+    this.set('isPopup', false);
+    this.set('hasData', false);
+  },
+
+  /**
+   * Shows a yellow warning message in place of the chart.
+   *
+   * @param type  Can be any of 'warn', 'error', 'info', 'success'
+   * @param title Bolded title for the message
+   * @param message String representing the message
+   * @type: Function
+   */
+  _showMessage: function(type, title, message) {
+    var chartOverlay = '#' + this.id;
+    var chartOverlayId = chartOverlay + '-chart';
+    var chartOverlayY = chartOverlay + '-yaxis';
+    var chartOverlayX = chartOverlay + '-xaxis';
+    var chartOverlayLegend = chartOverlay + '-legend';
+    var chartOverlayTimeline = chartOverlay + '-timeline';
+    if (this.get('isPopup')) {
+      chartOverlayId += this.get('popupSuffix');
+      chartOverlayY += this.get('popupSuffix');
+      chartOverlayX += this.get('popupSuffix');
+      chartOverlayLegend += this.get('popupSuffix');
+      chartOverlayTimeline += this.get('popupSuffix');
+    }
+    var typeClass;
+    switch (type) {
+      case 'error':
+        typeClass = 'alert-error';
+        break;
+      case 'success':
+        typeClass = 'alert-success';
+        break;
+      case 'info':
+        typeClass = 'alert-info';
+        break;
+      default:
+        typeClass = '';
+        break;
+    }
+    $(chartOverlayId+', '+chartOverlayY+', '+chartOverlayX+', '+chartOverlayLegend+', '+chartOverlayTimeline).html('');
+    $(chartOverlayId).append('<div class=\"alert '+typeClass+'\"><strong>'+title+'</strong> '+message+'</div>');
+  },
+
+  /**
+   * Transforms the JSON data retrieved from the server into the series
+   * format that Rickshaw.Graph understands.
+   *
+   * The series object is generally in the following format: [ { name :
+   * "Series 1", data : [ { x : 0, y : 0 }, { x : 1, y : 1 } ] } ]
+   *
+   * Extending classes should override this method.
+   *
+   * @param seriesData
+   *          Data retrieved from the server
+   * @param displayName
+   *          Graph title
+   * @type: Function
+   *
+   */
+  transformData: function (seriesData, displayName) {
+    var seriesArray = [];
+    if (seriesData != null) {
+      // Is it a string?
+      if ("string" == typeof seriesData) {
+        seriesData = JSON.parse(seriesData);
+      }
+      // Is it a number?
+      if ("number" == typeof seriesData) {
+        // Same number applies to all time.
+        var number = seriesData;
+        seriesData = [];
+        seriesData.push([number, (new Date().getTime())-(60*60)]);
+        seriesData.push([number, (new Date().getTime())]);
+      }
+      // We have valid data
+      var series = {};
+      series.name = displayName;
+      series.data = [];
+      for ( var index = 0; index < seriesData.length; index++) {
+        series.data.push({
+          x: seriesData[index][1],
+          y: seriesData[index][0]
+        });
+      }
+      return series;
+    }
+    return null;
+  },
+
+  /**
+   * Provides the formatter to use in displaying Y axis.
+   *
+   * Uses the App.ChartLinearTimeView.DefaultFormatter which shows 10K,
+   * 300M etc.
+   *
+   * @type Function
+   */
+  yAxisFormatter: function(y) {
+    return App.ChartView.DefaultFormatter(y);
+  },
+
+  /**
+   * Provides the color (in any HTML color format) to use for a particular
+   * series.
+   * May be redefined in child views
+   *
+   * @param series
+   *          Series for which color is being requested
+   * @return color String. Returning null allows this chart to pick a color
+   *         from palette.
+   * @default null
+   * @type Function
+   */
+  colorForSeries: function (series) {
+    return null;
+  },
+
+  /**
+   * Check whether seriesData is correct data for chart drawing
+   * @param {Array} seriesData
+   * @return {Boolean}
+   */
+  checkSeries : function(seriesData) {
+    if(!seriesData || !seriesData.length) {
+      return false;
+    }
+    var result = true;
+    seriesData.forEach(function(item) {
+      if(!item.data || !item.data.length || !item.data[0] || typeof item.data[0].x === 'undefined') {
+        result = false;
+      }
+    });
+    return result;
+  },
+
+  /**
+   * @private
+   *
+   * Refreshes the graph with the latest JSON data.
+   *
+   * @type Function
+   */
+  _refreshGraph: function (jsonData) {
+    if(this.get('isDestroyed')){
+      return;
+    }
+    var seriesData = this.transformToSeries(jsonData);
+
+    //if graph opened as modal popup
+    var popup_path = $("#" + this.get('id') + "-container" + this.get('popupSuffix'));
+    var graph_container = $("#" + this.get('id') + "-container");
+    if(popup_path.length) {
+      popup_path.children().each(function () {
+        $(this).children().remove();
+      });
+      this.set('isPopup', true);
+    }
+    else {
+      graph_container.children().each(function (index, value) {
+        $(value).children().remove();
+      });
+    }
+    if (this.checkSeries(seriesData)) {
+      // Check container exists (may be not, if we go to another page and wait while graphs loading)
+      if (graph_container.length) {
+        this.draw(seriesData);
+        this.set('hasData', true);
+        //move yAxis value lower to make them fully visible
+        $("#" + this.id + "-container").find('.y_axis text').attr('y',8);
+      }
+    }
+    else {
+      this.set('isReady', true);
+      //if Axis X time interval is default(60 minutes)
+      if(this.get('timeUnitSeconds') === 3600){
+        this._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noData.message'));
+        this.set('hasData', false);
+      }
+      else {
+        this._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noDataAtTime.message'));
+      }
+      this.set('isPopup', false);
+    }
+  },
+
+  /**
+   * Returns a custom time unit, that depends on X axis interval length, for the graph's X axis.
+   * This is needed as Rickshaw's default time X axis uses UTC time, which can be confusing
+   * for users expecting locale specific time.
+   *
+   * If <code>null</code> is returned, Rickshaw's default time unit is used.
+   *
+   * @type Function
+   * @return Rickshaw.Fixtures.Time
+   */
+  localeTimeUnit: function(timeUnitSeconds) {
+    var timeUnit = new Rickshaw.Fixtures.Time();
+    switch (timeUnitSeconds){
+      case 604800:
+        timeUnit = timeUnit.unit('day');
+        break;
+      case 2592000:
+        timeUnit = timeUnit.unit('week');
+        break;
+      case 31104000:
+        timeUnit = timeUnit.unit('month');
+        break;
+      default:
+        timeUnit = {
+          name: timeUnitSeconds / 240 + ' minute',
+          seconds: timeUnitSeconds / 4,
+          formatter: function (d) {
+            return d.toLocaleString().match(/(\d+:\d+):/)[1];
+          }
+        };
+    }
+    return timeUnit;
+  },
+
+  /**
+   * temporary fix for incoming data for graph
+   * to shift data time to correct time point
+   * @param {Array} data
+   */
+  dataShiftFix: function(data) {
+    var nowTime = Math.round((new Date().getTime()) / 1000);
+    data.forEach(function(series){
+      var l = series.data.length;
+      var shiftDiff = nowTime - series.data[l - 1].x;
+      if(shiftDiff > 3600){
+        for(var i = 0;i < l;i++){
+          series.data[i].x = series.data[i].x + shiftDiff;
+        }
+        series.data.unshift({
+          x: nowTime - this.get('timeUnitSeconds'),
+          y: 0
+        });
+      }
+    }, this);
+  },
+
+  /**
+   * calculate statistic data for popup legend and set proper colors for series
+   * @param {Array} data
+   */
+  dataPreProcess: function(data) {
+    var self = this;
+    var palette = new Rickshaw.Color.Palette({ scheme: 'munin'});
+    // Format series for display
+    var series_min_length = 100000000;
+    data.forEach(function (series, index) {
+      var seriesColor = self.colorForSeries(series);
+      if (seriesColor == null) {
+        seriesColor = palette.color();
+      }
+      series.color = seriesColor;
+      series.stroke = 'rgba(0,0,0,0.3)';
+      if (this.get('isPopup')) {
+        // calculate statistic data for popup legend
+        var avg = 0;
+        var min = Number.MAX_VALUE;
+        var max = Number.MIN_VALUE;
+        for (var i = 0; i < series.data.length; i++) {
+          avg += series.data[i]['y'];
+          if (series.data[i]['y'] < min) {
+            min = series.data[i]['y'];
+          }
+          else {
+            if (series.data[i]['y'] > max) {
+              max = series.data[i]['y'];
+            }
+          }
+        }
+        series.name = string_utils.pad(series.name, 30, '&nbsp;', 2) +
+          string_utils.pad('min', 5, '&nbsp;', 3) +
+          string_utils.pad(this.get('yAxisFormatter')(min), 12, '&nbsp;', 3) +
+          string_utils.pad('avg', 5, '&nbsp;', 3) +
+          string_utils.pad(this.get('yAxisFormatter')(avg/series.data.length), 12, '&nbsp;', 3) +
+          string_utils.pad('max', 12, '&nbsp;', 3) +
+          string_utils.pad(this.get('yAxisFormatter')(max), 5, '&nbsp;', 3);
+      }
+      if (series.data.length < series_min_length) {
+        series_min_length = series.data.length;
+      }
+    }.bind(this));
+
+    // All series should have equal length
+    data.forEach(function(series, index) {
+      if (series.data.length > series_min_length) {
+        series.data.length = series_min_length;
+      }
+    });
+  },
+
+  draw: function(seriesData) {
+    var self = this;
+    var isPopup = this.get('isPopup');
+    var p = isPopup ? this.get('popupSuffix') : '';
+
+    this.dataShiftFix(seriesData);
+    this.dataPreProcess(seriesData);
+
+    var chartId = "#" + this.get('id') + "-chart" + p;
+    var chartOverlayId = "#" + this.get('id') + "-container" + p;
+    var xaxisElementId = "#" + this.get('id') + "-xaxis" + p;
+    var yaxisElementId = "#" + this.get('id') + "-yaxis" + p;
+    var legendElementId = "#" + this.get('id') + "-legend" + p;
+
+    var chartElement = document.querySelector(chartId);
+    var overlayElement = document.querySelector(chartOverlayId);
+    var xaxisElement = document.querySelector(xaxisElementId);
+    var yaxisElement = document.querySelector(yaxisElementId);
+    var legendElement = document.querySelector(legendElementId);
+
+    var height = 150;
+    var width = 400;
+    var diff = 32;
+
+    if(this.get('inWidget')) {
+      height = 105; // for widgets view
+      diff = 22;
+    }
+    if (isPopup) {
+      height = 180;
+      width = 670;
+    }
+    else {
+      // If not in popup, the width could vary.
+      // We determine width based on div's size.
+      var thisElement = this.get('element');
+      if (thisElement!=null) {
+        var calculatedWidth = $(thisElement).width();
+        if (calculatedWidth > diff) {
+          width = calculatedWidth - diff;
+        }
+      }
+    }
+
+    var _graph = new Rickshaw.Graph({
+      height: height,
+      width: width,
+      element: chartElement,
+      series: seriesData,
+      interpolation: 'step-after',
+      stroke: true,
+      renderer: this.get('renderer'),
+      strokeWidth: (this.get('renderer') != 'area' ? 2 : 1)
+    });
+
+    if (this.get('renderer') === 'area') {
+      _graph.renderer.unstack = false;
+    }
+
+    new Rickshaw.Graph.Axis.Time({
+      graph: _graph,
+      timeUnit: this.localeTimeUnit(this.get('timeUnitSeconds'))
+    });
+
+    new Rickshaw.Graph.Axis.Y({
+      tickFormat: this.yAxisFormatter,
+      element: yaxisElement,
+      orientation: (isPopup ? 'left' : 'right'),
+      graph: _graph
+    });
+
+    var legend = new Rickshaw.Graph.Legend({
+      graph: _graph,
+      element: legendElement
+    });
+
+    new Rickshaw.Graph.Behavior.Series.Toggle({
+      graph: _graph,
+      legend: legend
+    });
+
+    new Rickshaw.Graph.Behavior.Series.Order({
+      graph: _graph,
+      legend: legend
+    });
+
+    if (!isPopup) {
+      overlayElement.addEventListener('mousemove', function () {
+        $(xaxisElement).removeClass('hide');
+        $(legendElement).removeClass('hide');
+        $(chartElement).children("div").removeClass('hide');
+      });
+      overlayElement.addEventListener('mouseout', function () {
+        $(legendElement).addClass('hide');
+      });
+      _graph.onUpdate(function () {
+        $(legendElement).addClass('hide');
+      });
+    }
+
+    //show the graph when it's loaded
+    _graph.onUpdate(function() {
+      self.set('isReady', true);
+    });
+    _graph.render();
+
+    if (isPopup) {
+      new Rickshaw.Graph.HoverDetail({
+        graph: _graph,
+        yFormatter:function (y) {
+          return self.yAxisFormatter(y);
+        },
+        xFormatter:function (x) {
+          return (new Date(x * 1000)).toLocaleTimeString();
+        },
+        formatter:function (series, x, y, formattedX, formattedY, d) {
+          return formattedY + '<br />' + formattedX;
+        }
+      });
+    }
+
+    _graph = this.updateSeriesInGraph(_graph);
+    if (isPopup) {
+      //show the graph when it's loaded
+      _graph.onUpdate(function() {
+        self.set('isPopupReady', true);
+      });
+      _graph.update();
+
+      var selector = '#'+this.get('id')+'-container'+this.get('popupSuffix');
+      $(selector + ' li.line').click(function() {
+        var series = [];
+        $(selector + ' a.action').each(function(index, v) {
+          series[index] = v.parentNode.classList;
+        });
+        self.set('_seriesProperties', series);
+      });
+
+      this.set('_popupGraph', _graph);
+    }
+    else {
+      _graph.update();
+      var selector = '#'+this.get('id')+'-container';
+      $(selector + ' li.line').click(function() {
+        var series = [];
+        $(selector + ' a.action').each(function(index, v) {
+          series[index] = v.parentNode.classList;
+        });
+        self.set('_seriesPropertiesWidget', series);
+      });
+
+      this.set('_graph', _graph);
+    }
+  },
+
+  /**
+   *
+   * @param {Rickshaw.Graph} graph
+   * @returns {Rickshaw.Graph}
+   */
+  updateSeriesInGraph: function(graph) {
+    var id = this.get('id');
+    var isPopup = this.get('isPopup');
+    var popupSuffix = this.get('popupSuffix');
+    var _series = isPopup ? this.get('_seriesProperties') : this.get('_seriesPropertiesWidget');
+    graph.series.forEach(function(series, index) {
+      if (_series !== null && _series[index] !== null && _series[index] !== undefined ) {
+        if(_series[_series.length - index - 1].length > 1) {
+          var s = '#' + id + '-container' + (isPopup ? popupSuffix : '') + ' a.action:eq(' + (_series.length - index - 1) + ')';
+          $(s).parent('li').addClass('disabled');
+          series.disable();
+        }
+      }
+    });
+    return graph;
+  },
+
+  showGraphInPopup: function() {
+    if(!this.get('hasData')) {
+      return;
+    }
+
+    this.set('isPopup', true);
+    var self = this;
+
+    App.ModalPopup.show({
+      bodyClass: Em.View.extend({
+
+        containerId: null,
+        containerClass: null,
+        yAxisId: null,
+        yAxisClass: null,
+        xAxisId: null,
+        xAxisClass: null,
+        legendId: null,
+        legendClass: null,
+        chartId: null,
+        chartClass: null,
+        titleId: null,
+        titleClass: null,
+
+        isReady: function() {
+          return this.get('parentView.graph.isPopupReady');
+        }.property('parentView.graph.isPopupReady'),
+
+        didInsertElement: function() {
+          $('#modal').addClass('modal-graph-line');
+          var popupSuffix = this.get('parentView.graph.popupSuffix');
+          var id = this.get('parentView.graph.id');
+          var idTemplate = id + '-{element}' + popupSuffix;
+
+          this.set('containerId', idTemplate.replace('{element}', 'container'));
+          this.set('containerClass', 'chart-container' + popupSuffix);
+          this.set('yAxisId', idTemplate.replace('{element}', 'yaxis'));
+          this.set('yAxisClass', this.get('yAxisId').replace(popupSuffix, ''));
+          this.set('xAxisId', idTemplate.replace('{element}', 'xaxis'));
+          this.set('xAxisClass', this.get('xAxisId').replace(popupSuffix, ''));
+          this.set('legendId', idTemplate.replace('{element}', 'legend'));
+          this.set('legendClass', this.get('legendId').replace(popupSuffix, ''));
+          this.set('chartId', idTemplate.replace('{element}', 'chart'));
+          this.set('chartClass', this.get('chartId').replace(popupSuffix, ''));
+          this.set('titleId', idTemplate.replace('{element}', 'title'));
+          this.set('titleClass', this.get('titleId').replace(popupSuffix, ''));
+        },
+
+        templateName: require('templates/common/chart/linear_time'),
+        /**
+         * check is time paging feature is enable for graph
+         */
+        isTimePagingEnable: function() {
+          return !self.get('isTimePagingDisable');
+        }.property(),
+        rightArrowVisible: function() {
+          return (this.get('isReady') && (this.get('parentView.currentTimeIndex') != 0));
+        }.property('isReady', 'parentView.currentTimeIndex'),
+        leftArrowVisible: function() {
+          return (this.get('isReady') && (this.get('parentView.currentTimeIndex') != 7));
+        }.property('isReady', 'parentView.currentTimeIndex')
+      }),
+      header: this.get('title'),
+      /**
+       * App.ChartLinearTimeView
+       */
+      graph: self,
+      secondary: null,
+      onPrimary: function() {
+        this.hide();
+        self.set('isPopup', false);
+        self.set('timeUnitSeconds', 3600);
+      },
+      onClose: function() {
+        this.onPrimary();
+      },
+      /**
+       * move graph back by time
+       * @param event
+       */
+      switchTimeBack: function(event) {
+        var index = this.get('currentTimeIndex');
+        // 7 - number of last time state
+        if(index < 7) {
+          this.reloadGraphByTime(++index);
+        }
+      },
+      /**
+       * move graph forward by time
+       * @param event
+       */
+      switchTimeForward: function(event) {
+        var index = this.get('currentTimeIndex');
+        if(index > 0) {
+          this.reloadGraphByTime(--index);
+        }
+      },
+      /**
+       * reload graph depending on the time
+       * @param index
+       */
+      reloadGraphByTime: function(index) {
+        this.set('currentTimeIndex', index);
+        self.set('timeUnitSeconds', this.get('timeStates')[index].seconds);
+        self.loadData();
+      },
+      timeStates: [
+        {name: Em.I18n.t('graphs.timeRange.hour'), seconds: 3600},
+        {name: Em.I18n.t('graphs.timeRange.twoHours'), seconds: 7200},
+        {name: Em.I18n.t('graphs.timeRange.fourHours'), seconds: 14400},
+        {name: Em.I18n.t('graphs.timeRange.twelveHours'), seconds: 43200},
+        {name: Em.I18n.t('graphs.timeRange.day'), seconds: 86400},
+        {name: Em.I18n.t('graphs.timeRange.week'), seconds: 604800},
+        {name: Em.I18n.t('graphs.timeRange.month'), seconds: 2592000},
+        {name: Em.I18n.t('graphs.timeRange.year'), seconds: 31104000}
+      ],
+      currentTimeIndex: 0,
+      currentTimeState: function() {
+        return this.get('timeStates').objectAt(this.get('currentTimeIndex'));
+      }.property('currentTimeIndex')
+    });
+    Ember.run.next(function() {
+      self.loadData();
+      self.set('isPopupReady', false);
+    });
+  },
+  //60 minute interval on X axis.
+  timeUnitSeconds: 3600
+});
+
+/**
+ * A formatter which will turn a number into computer storage sizes of the
+ * format '23 GB' etc.
+ *
+ * @type {Function}
+ */
+App.ChartView.BytesFormatter = function (y) {
+  if (y == 0) return '0 B';
+  var value = Rickshaw.Fixtures.Number.formatBase1024KMGTP(y);
+  if (!y || y.length < 1) {
+    value = '0 B';
+  }
+  else {
+    if ("number" == typeof value) {
+      value = String(value);
+    }
+    if ("string" == typeof value) {
+      value = value.replace(/\.\d(\d+)/, function($0, $1){ // Remove only 1-digit after decimal part
+        return $0.replace($1, '');
+      });
+      // Either it ends with digit or ends with character
+      value = value.replace(/(\d$)/, '$1 '); // Ends with digit like '120'
+      value = value.replace(/([a-zA-Z]$)/, ' $1'); // Ends with character like
+      // '120M'
+      value = value + 'B'; // Append B to make B, MB, GB etc.
+    }
+  }
+  return value;
+};
+
+/**
+ * A formatter which will turn a number into percentage display like '42%'
+ *
+ * @type {Function}
+ */
+App.ChartView.PercentageFormatter = function (percentage) {
+  var value = percentage;
+  if (!value || value.length < 1) {
+    value = '0 %';
+  } else {
+    value = value.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + '%';
+  }
+  return value;
+};
+
+/**
+ * A formatter which will turn elapsed time into display time like '50 ms',
+ * '5s', '10 m', '3 hr' etc. Time is expected to be provided in milliseconds.
+ *
+ * @type {Function}
+ */
+App.ChartView.TimeElapsedFormatter = function (millis) {
+  var value = millis;
+  if (!value || value.length < 1) {
+    value = '0 ms';
+  } else if ("number" == typeof millis) {
+    var seconds = millis > 1000 ? Math.round(millis / 1000) : 0;
+    var minutes = seconds > 60 ? Math.round(seconds / 60) : 0;
+    var hours = minutes > 60 ? Math.round(minutes / 60) : 0;
+    var days = hours > 24 ? Math.round(hours / 24) : 0;
+    if (days > 0) {
+      value = days + ' d';
+    } else if (hours > 0) {
+      value = hours + ' hr';
+    } else if (minutes > 0) {
+      value = minutes + ' m';
+    } else if (seconds > 0) {
+      value = seconds + ' s';
+    } else if (millis > 0) {
+      value = millis.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + ' ms';
+    } else {
+      value = millis.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + ' ms';
+    }
+  }
+  return value;
+};
+
+/**
+ * The default formatter which uses Rickshaw.Fixtures.Number.formatKMBT
+ * which shows 10K, 300M etc.
+ *
+ * @type {Function}
+ */
+App.ChartView.DefaultFormatter = function(y) {
+  if(isNaN(y)){
+    return 0;
+  }
+  var value = Rickshaw.Fixtures.Number.formatKMBT(y);
+  if (value == '') return '0';
+  value = String(value);
+  var c = value[value.length - 1];
+  if (!isNaN(parseInt(c))) {
+    // c is digit
+    value = parseFloat(value).toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
+  }
+  else {
+    // c in not digit
+    value = parseFloat(value.substr(0, value.length - 1)).toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + c;
+  }
+  return value;
+};
+
+
+/**
+ * Creates and returns a formatter that can convert a 'value'
+ * to 'value units/s'.
+ *
+ * @param unitsPrefix Prefix which will be used in 'unitsPrefix/s'
+ * @param valueFormatter  Value itself will need further processing
+ *        via provided formatter. Ex: '10M requests/s'. Generally
+ *        should be App.ChartLinearTimeView.DefaultFormatter.
+ * @return {Function}
+ */
+App.ChartView.CreateRateFormatter = function (unitsPrefix, valueFormatter) {
+  var suffix = " "+unitsPrefix+"/s";
+  return function (value) {
+    value = valueFormatter(value) + suffix;
+    return value;
+  };
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js
new file mode 100644
index 0000000..f9a6487
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/**
+ * @class
+ *
+ * This is a view for showing cluster CPU metrics
+ *
+ * @extends App.ChartView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.Metric2View = App.ChartView.extend({
+  id: "service-metrics-hdfs-jvm-threads",
+  title: 'jvm Threads',
+  renderer: 'line',
+
+  ajaxIndex: 'metrics2',
+
+  transformToSeries: function (jsonData) {
+    var seriesArray = [];
+    if (jsonData && jsonData.metrics && jsonData.metrics.jvm) {
+      for ( var name in jsonData.metrics.jvm) {
+        var displayName;
+        var seriesData = jsonData.metrics.jvm[name];
+        switch (name) {
+          case "threadsBlocked":
+            displayName = 'Threads Blocked';
+            break;
+          case "threadsWaiting":
+            displayName = 'Threads Waiting';
+            break;
+          case "threadsTimedWaiting":
+            displayName = 'Threads Timed Waiting';
+            break;
+          case "threadsRunnable":
+            displayName = 'Threads Runnable';
+            break;
+          default:
+            break;
+        }
+        if (seriesData) {
+          seriesArray.push(this.transformData(seriesData, displayName));
+        }
+      }
+    }
+    return seriesArray;
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js
new file mode 100644
index 0000000..b47d8dc
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/**
+ * @class
+ *
+ * This is a view for showing cluster CPU metrics
+ *
+ * @extends App.ChartView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.Metric3View = App.ChartView.extend({
+  id: "service-metrics-hdfs-file-operations",
+  title: 'File Operations',
+  renderer: 'line',
+
+  ajaxIndex: 'metrics3',
+  yAxisFormatter: App.ChartView.CreateRateFormatter('ops', App.ChartView.DefaultFormatter),
+
+  transformToSeries: function (jsonData) {
+    var seriesArray = [];
+    if (jsonData && jsonData.metrics && jsonData.metrics.dfs && jsonData.metrics.dfs.namenode) {
+      for ( var name in jsonData.metrics.dfs.namenode) {
+        var displayName;
+        var seriesData = jsonData.metrics.dfs.namenode[name];
+        switch (name) {
+          case "FileInfoOps":
+            displayName = 'File Info Ops';
+            break;
+          case "DeleteFileOps":
+            displayName = 'Delete File Ops';
+            break;
+          case "CreateFileOps":
+            displayName = 'Create File Ops';
+            break;
+          default:
+            break;
+        }
+        if (seriesData) {
+          seriesArray.push(this.transformData(seriesData, displayName));
+        }
+      }
+    }
+    return seriesArray;
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js
new file mode 100644
index 0000000..9897a30
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/**
+ * @class
+ *
+ * This is a view for showing cluster CPU metrics
+ *
+ * @extends App.ChartView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.Metric4View = App.ChartView.extend({
+  id: "service-metrics-hdfs-rpc",
+  title: 'RPC',
+  yAxisFormatter: App.ChartView.TimeElapsedFormatter,
+
+  ajaxIndex: 'metrics4',
+
+  transformToSeries: function (jsonData) {
+    var seriesArray = [];
+    if (jsonData && jsonData.metrics && jsonData.metrics.rpc) {
+      for ( var name in jsonData.metrics.rpc) {
+        var displayName;
+        var seriesData = jsonData.metrics.rpc[name];
+        switch (name) {
+          case "RpcQueueTime_avg_time":
+            displayName = 'RPC Queue Time Avg Time';
+            break;
+          default:
+            break;
+        }
+        if (seriesData) {
+          seriesArray.push(this.transformData(seriesData, displayName));
+        }
+      }
+    }
+    return seriesArray;
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js
new file mode 100644
index 0000000..5f6c671
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/**
+ * @class
+ *
+ * This is a view for showing cluster CPU metrics
+ *
+ * @extends App.ChartView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.MetricView = App.ChartView.extend({
+
+  id: "service-metrics-hdfs-space-utilization",
+
+  title: 'Space Utilization',
+
+  yAxisFormatter: App.ChartView.BytesFormatter,
+
+  renderer: 'line',
+
+  ajaxIndex: 'metrics',
+
+  transformToSeries: function (jsonData) {
+    var seriesArray = [];
+    var GB = Math.pow(2, 30);
+    if (jsonData && jsonData.metrics && jsonData.metrics.dfs && jsonData.metrics.dfs.FSNamesystem) {
+      for ( var name in jsonData.metrics.dfs.FSNamesystem) {
+        var displayName;
+        var seriesData = jsonData.metrics.dfs.FSNamesystem[name];
+        switch (name) {
+          case "CapacityRemainingGB":
+            displayName = 'Capacity Remaining GB';
+            break;
+          case "CapacityUsedGB":
+            displayName = 'Capacity Used GB';
+            break;
+          case "CapacityTotalGB":
+            displayName = 'Capacity Total GB';
+            break;
+          default:
+            break;
+        }
+        if (seriesData) {
+          var s = this.transformData(seriesData, displayName);
+          for (var i = 0; i < s.data.length; i++) {
+            s.data[i].y *= GB;
+          }
+          seriesArray.push(s);
+        }
+      }
+    }
+    return seriesArray;
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
new file mode 100644
index 0000000..bad2eb9
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.SliderAppSummaryView = Ember.View.extend({
+
+  classNames: ['app_summary'],
+
+  graphs: [
+    [App.MetricView, App.Metric2View, App.Metric3View, App.Metric4View]
+  ]
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/config.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/config.js b/contrib/views/slider/src/main/resources/ui/config.js
index 8ba0087..978d984 100755
--- a/contrib/views/slider/src/main/resources/ui/config.js
+++ b/contrib/views/slider/src/main/resources/ui/config.js
@@ -28,8 +28,15 @@ exports.config = {
       },
       order: {
         before: [
+          'vendor/scripts/common/d3.v2.js',
+          'vendor/scripts/common/cubism.v1.js',
+          'vendor/scripts/common/rickshaw.js',
           'vendor/scripts/common/console-polyfill.js',
           'vendor/scripts/common/jquery.js',
+          'vendor/scripts/common/jquery.ui.core.js',
+          'vendor/scripts/common/jquery.ui.widget.js',
+          'vendor/scripts/common/jquery.ui.mouse.js',
+          'vendor/scripts/common/jquery.ui.sortable.js',
           'vendor/scripts/common/handlebars.js',
           'vendor/scripts/development/ember.js',
           'vendor/scripts/production/ember-data.js',
@@ -49,6 +56,8 @@ exports.config = {
       },
       order: {
         before: [
+          'vendor/styles/cubism.css',
+          'vendor/styles/rickshaw.css',
           'vendor/styles/bootstrap.css',
           'vendor/styles/font-awesome.css',
           'vendor/styles/font-awesome-ie7.css'
@@ -75,6 +84,9 @@ exports.config = {
           },
           order: {
             before: [
+              'vendor/scripts/common/d3.v2.js',
+              'vendor/scripts/common/cubism.v1.js',
+              'vendor/scripts/common/rickshaw.js',
               'vendor/scripts/common/console-polyfill.js',
               'vendor/scripts/common/jquery.js',
               'vendor/scripts/common/handlebars.js',

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/package.json
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/package.json b/contrib/views/slider/src/main/resources/ui/package.json
index 2d2ea16..daf0943 100755
--- a/contrib/views/slider/src/main/resources/ui/package.json
+++ b/contrib/views/slider/src/main/resources/ui/package.json
@@ -26,7 +26,6 @@
     "stylus-brunch": "~1.7.0",
     "uglify-js-brunch": "~1.7.0",
     "clean-css-brunch": "~1.7.0",
-    "auto-reload-brunch": "~1.7.0",
     "ember-handlebars-brunch": "git://github.com/fuseelements/ember-handlebars-brunch#fix/ember-1.3.0"
   },
   "devDependencies": {

http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js b/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js
new file mode 100644
index 0000000..be9ff4d
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js
@@ -0,0 +1,1085 @@
+(function (exports) {
+  var cubism = exports.cubism = {version:"1.2.0"};
+  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 + "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;
+      });
+  }
+
+  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(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(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", 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);