Просмотр исходного кода

AMBARI-889. Provide cluster metric graphs on Ambari main dashboard. (Srimanth Gunturi via yusaku)

git-svn-id: https://svn.apache.org/repos/asf/incubator/ambari/branches/AMBARI-666@1400490 13f79535-47bb-0310-9956-ffa450edef68
Yusaku Sako 12 лет назад
Родитель
Сommit
5a6d0aa7d9
24 измененных файлов с 3581 добавлено и 12 удалено
  1. 3 0
      AMBARI-666-CHANGES.txt
  2. 0 0
      ambari-web/app/assets/data/cluster_metrics/cpu_1hr.json
  3. 0 0
      ambari-web/app/assets/data/cluster_metrics/cpu_2hr.json
  4. 0 0
      ambari-web/app/assets/data/cluster_metrics/load_1hr.json
  5. 0 0
      ambari-web/app/assets/data/cluster_metrics/load_2hr.json
  6. 0 0
      ambari-web/app/assets/data/cluster_metrics/load_4hr.json
  7. 0 0
      ambari-web/app/assets/data/cluster_metrics/load_day.json
  8. 0 0
      ambari-web/app/assets/data/cluster_metrics/memory_1hr.json
  9. 0 0
      ambari-web/app/assets/data/cluster_metrics/memory_2hr.json
  10. 0 0
      ambari-web/app/assets/data/cluster_metrics/network_1hr.json
  11. 1 0
      ambari-web/app/assets/data/cluster_metrics/network_2hr.json
  12. 5 2
      ambari-web/app/assets/licenses/NOTICE.txt
  13. 53 1
      ambari-web/app/styles/application.less
  14. 23 0
      ambari-web/app/templates/main/charts/linear_time.hbs
  15. 29 6
      ambari-web/app/templates/main/dashboard.hbs
  16. 6 1
      ambari-web/app/views.js
  17. 282 0
      ambari-web/app/views/common/chart/linear_time.js
  18. 66 0
      ambari-web/app/views/main/dashboard/cluster_metrics/cpu.js
  19. 52 0
      ambari-web/app/views/main/dashboard/cluster_metrics/load.js
  20. 59 0
      ambari-web/app/views/main/dashboard/cluster_metrics/memory.js
  21. 53 0
      ambari-web/app/views/main/dashboard/cluster_metrics/network.js
  22. 4 2
      ambari-web/config.coffee
  23. 2638 0
      ambari-web/vendor/scripts/rickshaw.js
  24. 307 0
      ambari-web/vendor/styles/rickshaw.css

+ 3 - 0
AMBARI-666-CHANGES.txt

@@ -12,6 +12,9 @@ AMBARI-666 branch (unreleased changes)
 
   NEW FEATURES
 
+  AMBARI-889. Provide cluster metric graphs on Ambari main dashboard.
+  (yusaku)
+
   AMBARI-886. Support filters in controller get* apis. (hitesh)
 
   AMBARI-880. Implement Review Page (Step 8) for the Ambari Installer

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/cpu_1hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/cpu_2hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/load_1hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/load_2hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/load_4hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/load_day.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/memory_1hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/memory_2hr.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ambari-web/app/assets/data/cluster_metrics/network_1hr.json


+ 1 - 0
ambari-web/app/assets/data/cluster_metrics/network_2hr.json

@@ -0,0 +1 @@
+[{"ds_name":"a0","cluster_name":"","graph_type":"line","host_name":"","metric_name":"In ","datapoints":[[352.942,1350519840],[236.59,1350520200],[240.60791667,1350520560],[351.89730556,1350520920],[294.35841667,1350521280],[374.38777778,1350521640],[249.21122222,1350522000],[259.88830556,1350522360],[382.79436111,1350522720],[617.02863889,1350523080],[240.46,1350523440],[247.0425,1350523800],[503.92527778,1350524160],[233.43291667,1350524520],[240.355,1350524880],[229.16444444,1350525240],[227.12472222,1350525600],[239.61583333,1350525960],[345.57472222,1350526320],[451.08116667,1350526680],[0,1350527040]]},{"ds_name":"a1","cluster_name":"","graph_type":"line","host_name":"","metric_name":"Out","datapoints":[[2044.9526667,1350519840],[186.601,1350520200],[322.66416667,1350520560],[1746.4213333,1350520920],[801.19655556,1350521280],[2100.7752778,1350521640],[196.62205556,1350522000],[544.16102778,1350522360],[2467.9166389,1350522720],[6349.9070833,1350523080],[189.792,1350523440],[195.6495,1350523800],[3345.7549444,1350524160],[302.33,1350524520],[190.26427778,1350524880],[179.69261111,1350525240],[178.87130556,1350525600],[189.15422222,1350525960],[1709.4773889,1350526320],[2826.0326667,1350526680],[0,1350527040]]}]

+ 5 - 2
ambari-web/app/assets/licenses/NOTICE.txt

@@ -28,6 +28,9 @@ This product includes D3.js (http://d3js.org - BSD license)
 Copyright (c) 2012, Michael Bostock.
 
 This product includes bootstrap-datepicker.js (http://www.eyecon.ro/bootstrap-datepicker - Apache License, Version 2.0)
-Copyright 2012 Stefan Petre
+Copyright (c) 2012 Stefan Petre
 
-This product includes Font Awesome 2.0 (http://fortawesome.github.com/Font-Awesome - Creative Commons 3.0)
+This product includes Font Awesome 2.0 (http://fortawesome.github.com/Font-Awesome - Creative Commons 3.0)
+
+This product incudes Rickshaw 1.1.2 (http://code.shutterstock.com/rickshaw/ - MIT License)
+Copyright (C) 2011 by Shutterstock Images, LLC

+ 53 - 1
ambari-web/app/styles/application.less

@@ -164,6 +164,14 @@ h1 {
   }
 }
 
+.hide{
+  visibility: hidden;
+}
+
+.show{
+  visibility: visible;
+}
+
 /***************
  * Ambari wide icon colors
  ***************/
@@ -544,6 +552,50 @@ a:focus {
 
 /*end alerts summary*/
 
+/*start cluster metrics*/
+.cluster-metrics{
+  .chart-container{
+    position: relative;
+    margin: 20px 15px 0px 15px;
+  }
+  .chart{
+    padding-bottom: 0px !important;
+    position: relative;
+    height: 160px;
+    z-index: 1;
+  }
+  .chart-y-axis{
+    position: absolute;
+    top: 0;
+    bottom: 0px;
+    width: 100px;
+    z-index: 2;
+    margin-top: 15px;
+  }
+  .chart-x-axis{
+    position: absolute;
+    top: 180px;
+    left: 35%;
+    width: 30%;
+    z-index: 2;
+  }
+  .rickshaw_legend{
+    background-color: #999999 !important;
+  }
+  .chart-overlay{
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    width: 100%;
+    z-index: 5;
+  }
+  .chart-title{
+    text-align: center;
+    font-size: small;
+  }
+}
+/*end cluster metrics*/
+
 /*****end styles for dashboard page*****/
 
 /*Services*/
@@ -1168,4 +1220,4 @@ ul.inline li {
 .ui-timepicker-rtl dl { text-align: right; }
 .ui-timepicker-rtl dl dd { margin: 0 65px 10px 10px; }
 
-/* TIME RANGE WIDGET END */
+/* TIME RANGE WIDGET END */

+ 23 - 0
ambari-web/app/templates/main/charts/linear_time.hbs

@@ -0,0 +1,23 @@
+<!--
+* 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.
+-->
+<div id="{{unbound view.id}}-container" class="chart-container">
+  <div id="{{unbound view.id}}-yaxis" class="chart-y-axis"></div>
+  <div id="{{unbound view.id}}-xaxis" class="chart-x-axis"></div>
+  <div id="{{unbound view.id}}-overlay" class="chart-overlay"></div>
+  <div id="{{unbound view.id}}-chart" class="chart"></div>
+</div>

+ 29 - 6
ambari-web/app/templates/main/dashboard.hbs

@@ -37,11 +37,38 @@
     </div>
   </div>
   <div class="span6">
-    <div class="box">
+		<div class="box">
+			<div class="box-header">
+				<h4>Cluster Metrics</h4>
+				<div class="btn-group">
+          <a class="btn" href="http://gangliaserver/ganglia/?t=yes" target="_blank" rel="tooltip" title="Go to Ganglia"><i class="icon-link"></i></a>
+        </div>
+			</div>
+			<div class="cluster-metrics">
+				<div class="row">
+					<div class="span6">
+						<div class="row">
+							<div class="span3">{{view App.ChartClusterMetricsNetwork}}<div class="chart-title">Network Usage</div></div>
+							<div class="span3">{{view App.ChartClusterMetricsLoad}}<div class="chart-title">Cluster Load</div></div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<div class="span6">
+						<div class="row">
+							<div class="span3">{{view App.ChartClusterMetricsMemory}}<div class="chart-title">Memory Usage</div></div>
+							<div class="span3">{{view App.ChartClusterMetricsCPU}}<div class="chart-title">CPU Usage</div></div>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+    <div class="box alerts-section">
       <div class="box-header">
         <h4>Alerts</h4>
         <div class="btn-group">
           <button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">{{controller.alertsFilteredBy}} <span class="caret"></span></button>
+          <a class="btn" href="http://nagiosserver/nagios" target="_blank" rel="tooltip" title="Go to Nagios"><i class="icon-link"></i></a>
           <ul class="dropdown-menu">
             <li><a {{action alertsFilter target="controller"}} href="javascript:void(null)">All</a></li>
             {{#each service in controller.services}}
@@ -78,10 +105,6 @@
         </li>
         {{/each}}
       </ul>
-      <div class="box-footer">
-        <hr />
-        <a class="go-to" href="http://nagiosserver/nagios" target="_blank">Go to Nagios</a>
-      </div>
     </div>
-  </div>
+	</div>
 </div>

+ 6 - 1
ambari-web/app/views.js

@@ -24,6 +24,7 @@ require('views/application');
 require('views/common/chart');
 require('views/common/chart/pie');
 require('views/common/chart/linear');
+require('views/common/chart/linear_time');
 require('views/common/modal_popup');
 require('views/common/metric');
 require('views/common/time_range');
@@ -57,6 +58,10 @@ require('views/main/dashboard/service/hbase');
 require('views/main/dashboard/service/hive');
 require('views/main/dashboard/service/zookeeper');
 require('views/main/dashboard/service/oozie');
+require('views/main/dashboard/cluster_metrics/cpu');
+require('views/main/dashboard/cluster_metrics/load');
+require('views/main/dashboard/cluster_metrics/memory');
+require('views/main/dashboard/cluster_metrics/network');
 require('views/main/service');
 require('views/main/service/menu');
 require('views/main/service/item');
@@ -87,4 +92,4 @@ require('views/wizard/step2_view');
 require('views/wizard/step3_view');
 require('views/wizard/step4_view');
 require('views/wizard/step5_view');
-require('views/wizard/step6_view');
+require('views/wizard/step6_view');

+ 282 - 0
ambari-web/app/views/common/chart/linear_time.js

@@ -0,0 +1,282 @@
+/**
+ * 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 App = require('app');
+
+/**
+ * @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>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.ChartLinearTimeView = Ember.View
+    .extend({
+      templateName: require('templates/main/charts/linear_time'),
+
+      /**
+       * 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,
+
+      /**
+       * @private
+       * 
+       * @type Rickshaw.Graph
+       * @default null
+       */
+      _graph: null,
+
+      /**
+       * Color palette used for this chart
+       * 
+       * @private
+       * @type String[]
+       */
+      _paletteScheme: [ 'rgba(181,182,169,0.4)', 'rgba(133,135,114,0.4)',
+          'rgba(120,95,67,0.4)', 'rgba(150,85,126,0.4)',
+          'rgba(70,130,180,0.4)', 'rgba(0,255,204,0.4)',
+          'rgba(255,105,180,0.4)', 'rgba(101,185,172,0.4)',
+          'rgba(115,192,58,0.4)', 'rgba(203,81,58,0.4)' ].reverse(),
+
+      init: function () {
+        this._super();
+      },
+
+      selector: function () {
+        return '#' + this.get('elementId');
+      }.property('elementId'),
+
+      didInsertElement: function () {
+        this._super();
+        if (this.url != null) {
+          var hash = {};
+          hash.url = this.url;
+          hash.type = 'GET';
+          hash.dataType = 'json';
+          hash.contentType = 'application/json; charset=utf-8';
+          hash.context = this;
+          hash.success = this._refreshGraph;
+          jQuery.ajax(hash);
+        }
+      },
+
+      /**
+       * 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 jsonData
+       *          Data retrieved from the server
+       * @type: Function
+       * 
+       */
+      transformToSeries: function (jsonData) {
+        return [ {
+          name: "Series 1",
+          data: [ {
+            x: 0,
+            y: 0
+          }, {
+            x: 1,
+            y: 1
+          } ],
+        } ]
+      },
+
+      /**
+       * Provides the formatter to use in displaying Y axis.
+       * 
+       * The default is Rickshaw.Fixtures.Number.formatKMBT which shows 10K,
+       * 300M etc.
+       * 
+       * @type Function
+       */
+      yAxisFormatter: Rickshaw.Fixtures.Number.formatKMBT,
+
+      /**
+       * Provides the color (in any HTML color format) to use for a particular
+       * series.
+       * 
+       * @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;
+      },
+
+      /**
+       * @private
+       * 
+       * Refreshes the graph with the latest JSON data.
+       * 
+       * @type Function
+       */
+      _refreshGraph: function (jsonData) {
+        var seriesData = this.transformToSeries(jsonData);
+        if (seriesData instanceof Array) {
+          var palette = new Rickshaw.Color.Palette({
+            scheme: this._paletteScheme
+          });
+          seriesData.forEach(function (series) {
+            series.color = this.colorForSeries(series) || palette.color();
+            series.stroke = 'rgba(0,0,0,0.3)';
+          }.bind(this));
+        }
+        if (this._graph == null) {
+          var chartId = "#" + this.id + "-chart";
+          var chartOverlayId = "#" + this.id + "-overlay";
+          var xaxisElementId = "#" + this.id + "-xaxis";
+          var yaxisElementId = "#" + this.id + "-yaxis";
+          var chartElement = document.querySelector(chartId);
+          var overlayElement = document.querySelector(chartOverlayId);
+          var xaxisElement = document.querySelector(xaxisElementId);
+          var yaxisElement = document.querySelector(yaxisElementId);
+
+          this._graph = new Rickshaw.Graph({
+            height: 150,
+            element: chartElement,
+            series: seriesData,
+            interpolation: 'step-after',
+            stroke: true,
+            renderer: 'area',
+            strokeWidth: 1,
+          });
+          this._graph.renderer.unstack = true;
+
+          xAxis = new Rickshaw.Graph.Axis.Time({
+            graph: this._graph,
+          });
+          yAxis = new Rickshaw.Graph.Axis.Y({
+            tickFormat: this.yAxisFormatter,
+            element: yaxisElement,
+            graph: this._graph
+          });
+
+          overlayElement.addEventListener('mousemove', function (e) {
+            $(xaxisElement).removeClass('hide');
+            $(yaxisElement).removeClass('hide');
+            $(chartElement).children("div").removeClass('hide');
+          });
+          overlayElement.addEventListener('mouseout', function (e) {
+            $(xaxisElement).addClass('hide');
+            $(yaxisElement).addClass('hide');
+            $(chartElement).children("div").addClass('hide');
+          });
+          // Hide axes
+          this._graph.onUpdate(function () {
+            $(xaxisElement).addClass('hide');
+            $(yaxisElement).addClass('hide');
+            $(chartElement).children('div').addClass('hide');
+          });
+
+          var legend = new Rickshaw.Graph.Legend({
+            graph: this._graph,
+            element: xaxisElement
+          });
+
+          // The below code will be needed if we ever use curve
+          // smoothing in our graphs. (see rickshaw defect below)
+          // this._graph.onUpdate(jQuery.proxy(function () {
+          // this._adjustSVGHeight();
+          // }, this));
+        }
+        this._graph.render();
+
+      },
+
+      /**
+       * @private
+       * 
+       * When a graph is given a particular width and height,the lines are drawn
+       * in a slightly bigger area thereby chopping off some of the UI. Hence
+       * after the rendering, we adjust the SVGs size in the DOM to compensate.
+       * 
+       * Opened https://github.com/shutterstock/rickshaw/issues/141
+       * 
+       * @type Function
+       */
+      _adjustSVGHeight: function () {
+        if (this._graph && this._graph.element
+            && this._graph.element.firstChild) {
+          var svgElement = this._graph.element.firstChild;
+          svgElement.setAttribute('height', $(this._graph.element).height()
+              + "px");
+          svgElement.setAttribute('width', $(this._graph.element).width()
+              + "px");
+        }
+      }
+    });
+
+/**
+ * A formatter which will turn a number into computer storage sizes of the
+ * format '23 GB' etc.
+ * 
+ * @type Function
+ */
+App.ChartLinearTimeView.BytesFormatter = function (y) {
+  var value = Rickshaw.Fixtures.Number.formatBase1024KMGTP(y);
+  if (!y || y.length < 1) {
+    value = "0";
+  } else {
+    if ("string" == typeof value) {
+      value = value.replace(/\.\d+/, " ")
+      value = value + "B";
+    }
+  }
+  return value;
+}

+ 66 - 0
ambari-web/app/views/main/dashboard/cluster_metrics/cpu.js

@@ -0,0 +1,66 @@
+/**
+ * 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 App = require('app');
+
+/**
+ * @class
+ * 
+ * This is a view for showing cluster CPU metrics
+ * 
+ * @extends App.ChartLinearTimeView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.ChartClusterMetricsCPU = App.ChartLinearTimeView.extend({
+  id: "cluster-metrics-cpu",
+  url: "/data/cluster_metrics/cpu_1hr.json",
+  
+  yAxisFormatter: function (y) {
+    var value = this._super(y);
+    if (!value || value.length < 1) {
+      value = "0";
+    }
+    return value + "%";
+  },
+  
+  transformToSeries: function (jsonData) {
+    var seriesArray = [];
+    if (jsonData instanceof Array) {
+      jsonData.forEach(function (data) {
+        var series = {};
+        series.name = data.metric_name.replace(/\\g/, '');
+        series.data = [];
+        for ( var index = 0; index < data.datapoints.length; index++) {
+          series.data.push({
+            x: data.datapoints[index][1],
+            y: data.datapoints[index][0]
+          });
+        }
+        seriesArray.push(series);
+      });
+    }
+    return seriesArray;
+  },
+  
+  colorForSeries: function (series) {
+    if ("Idle" == series.name){
+      return 'rgba(255,255,255,1)';
+    }
+    return null;
+  }
+});

+ 52 - 0
ambari-web/app/views/main/dashboard/cluster_metrics/load.js

@@ -0,0 +1,52 @@
+/**
+ * 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 App = require('app');
+
+/**
+ * @class
+ * 
+ * This is a view for showing cluster load
+ * 
+ * @extends App.ChartLinearTimeView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.ChartClusterMetricsLoad = App.ChartLinearTimeView.extend({
+  id: "cluster-metrics-load",
+  url: "/data/cluster_metrics/load_1hr.json",
+  
+  transformToSeries: function(jsonData){
+    var seriesArray = [];
+    if(jsonData instanceof Array){
+      jsonData.forEach(function(data){
+        var series = {};
+        series.name = data.metric_name;
+        series.data = [];
+        for(var index=0; index<data.datapoints.length; index++){
+          series.data.push({
+            x: data.datapoints[index][1],
+            y: data.datapoints[index][0]
+          });
+        }
+        seriesArray.push(series);
+      });
+    }
+    return seriesArray;
+  }
+});

+ 59 - 0
ambari-web/app/views/main/dashboard/cluster_metrics/memory.js

@@ -0,0 +1,59 @@
+/**
+ * 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 App = require('app');
+
+/**
+ * @class
+ * 
+ * This is a view for showing cluster memory metrics
+ * 
+ * @extends App.ChartLinearTimeView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.ChartClusterMetricsMemory = App.ChartLinearTimeView.extend({
+  id: "cluster-metrics-memory",
+  url: "/data/cluster_metrics/memory_1hr.json",
+  
+  yAxisFormatter: App.ChartLinearTimeView.BytesFormatter,
+  transformToSeries: function (jsonData) {
+    var seriesArray = [];
+    if (jsonData instanceof Array) {
+      jsonData.forEach(function (data) {
+        var series = {};
+        series.name = data.metric_name.replace(/\\g/, '');
+        series.data = [];
+        for ( var index = 0; index < data.datapoints.length; index++) {
+          series.data.push({
+            x: data.datapoints[index][1],
+            y: data.datapoints[index][0]
+          });
+        }
+        seriesArray.push(series);
+      });
+    }
+    return seriesArray;
+  },
+  
+  colorForSeries: function (series) {
+    if("Total"==series.name){
+      return 'rgba(255,255,255,1)';
+    }
+    return null;
+  }
+});

+ 53 - 0
ambari-web/app/views/main/dashboard/cluster_metrics/network.js

@@ -0,0 +1,53 @@
+/**
+ * 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 App = require('app');
+
+/**
+ * @class
+ * 
+ * This is a view for showing cluster network metrics
+ * 
+ * @extends App.ChartLinearTimeView
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.ChartClusterMetricsNetwork = App.ChartLinearTimeView.extend({
+  id: "cluster-metrics-network",
+  url : "/data/cluster_metrics/network_1hr.json",
+  yAxisFormatter: App.ChartLinearTimeView.BytesFormatter,
+  
+  transformToSeries : function (jsonData) {
+    var seriesArray = [];
+    if (jsonData instanceof Array) {
+      jsonData.forEach(function (data) {
+        var series = {};
+        series.name = data.metric_name;
+        series.data = [];
+        for ( var index = 0; index < data.datapoints.length; index++) {
+          series.data.push({
+            x : data.datapoints[index][1],
+            y : data.datapoints[index][0]
+          });
+        }
+        seriesArray.push(series);
+      });
+    }
+    return seriesArray;
+  }
+});

+ 4 - 2
ambari-web/config.coffee

@@ -42,7 +42,8 @@ exports.config =
           'vendor/scripts/ember-i18n-1.2.0.js',
           'vendor/scripts/bootstrap.js',
           'vendor/scripts/d3.v2.js',
-          'vendor/scripts/sinon-1.4.2.js'
+          'vendor/scripts/sinon-1.4.2.js',
+          'vendor/scripts/rickshaw.js'
           ]
 
     stylesheets:
@@ -53,7 +54,8 @@ exports.config =
           'vendor/styles/bootstrap.css',
           'vendor/styles/datepicker.css'
           'vendor/styles/font-awesome.css'
-          'vendor/styles/font-awesome-ie7.css'
+          'vendor/styles/font-awesome-ie7.css',
+          'vendor/styles/rickshaw.css'
         ]
 
     templates:

+ 2638 - 0
ambari-web/vendor/scripts/rickshaw.js

@@ -0,0 +1,2638 @@
+var Rickshaw = {
+
+	namespace: function(namespace, obj) {
+
+		var parts = namespace.split('.');
+
+		var parent = Rickshaw;
+
+		for(var i = 1, length = parts.length; i < length; i++) {
+			currentPart = parts[i];
+			parent[currentPart] = parent[currentPart] || {};
+			parent = parent[currentPart];
+		}
+		return parent;
+	},
+
+	keys: function(obj) {
+		var keys = [];
+		for (var key in obj) keys.push(key);
+		return keys;
+	},
+
+	extend: function(destination, source) {
+
+		for (var property in source) {
+			destination[property] = source[property];
+		}
+		return destination;
+	}
+};
+
+if (typeof module !== 'undefined' && module.exports) {
+	var d3 = require('d3');
+	module.exports = Rickshaw;
+}
+
+/* Adapted from https://github.com/Jakobo/PTClass */
+
+/*
+Copyright (c) 2005-2010 Sam Stephenson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+/* Based on Alex Arnell's inheritance implementation. */
+/** section: Language
+ * class Class
+ *
+ *  Manages Prototype's class-based OOP system.
+ *
+ *  Refer to Prototype's web site for a [tutorial on classes and
+ *  inheritance](http://prototypejs.org/learn/class-inheritance).
+**/
+(function(globalContext) {
+/* ------------------------------------ */
+/* Import from object.js                */
+/* ------------------------------------ */
+var _toString = Object.prototype.toString,
+    NULL_TYPE = 'Null',
+    UNDEFINED_TYPE = 'Undefined',
+    BOOLEAN_TYPE = 'Boolean',
+    NUMBER_TYPE = 'Number',
+    STRING_TYPE = 'String',
+    OBJECT_TYPE = 'Object',
+    FUNCTION_CLASS = '[object Function]';
+function isFunction(object) {
+  return _toString.call(object) === FUNCTION_CLASS;
+}
+function extend(destination, source) {
+  for (var property in source) if (source.hasOwnProperty(property)) // modify protect primitive slaughter
+    destination[property] = source[property];
+  return destination;
+}
+function keys(object) {
+  if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); }
+  var results = [];
+  for (var property in object) {
+    if (object.hasOwnProperty(property)) {
+      results.push(property);
+    }
+  }
+  return results;
+}
+function Type(o) {
+  switch(o) {
+    case null: return NULL_TYPE;
+    case (void 0): return UNDEFINED_TYPE;
+  }
+  var type = typeof o;
+  switch(type) {
+    case 'boolean': return BOOLEAN_TYPE;
+    case 'number':  return NUMBER_TYPE;
+    case 'string':  return STRING_TYPE;
+  }
+  return OBJECT_TYPE;
+}
+function isUndefined(object) {
+  return typeof object === "undefined";
+}
+/* ------------------------------------ */
+/* Import from Function.js              */
+/* ------------------------------------ */
+var slice = Array.prototype.slice;
+function argumentNames(fn) {
+  var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
+    .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
+    .replace(/\s+/g, '').split(',');
+  return names.length == 1 && !names[0] ? [] : names;
+}
+function wrap(fn, wrapper) {
+  var __method = fn;
+  return function() {
+    var a = update([bind(__method, this)], arguments);
+    return wrapper.apply(this, a);
+  }
+}
+function update(array, args) {
+  var arrayLength = array.length, length = args.length;
+  while (length--) array[arrayLength + length] = args[length];
+  return array;
+}
+function merge(array, args) {
+  array = slice.call(array, 0);
+  return update(array, args);
+}
+function bind(fn, context) {
+  if (arguments.length < 2 && isUndefined(arguments[0])) return this;
+  var __method = fn, args = slice.call(arguments, 2);
+  return function() {
+    var a = merge(args, arguments);
+    return __method.apply(context, a);
+  }
+}
+
+/* ------------------------------------ */
+/* Import from Prototype.js             */
+/* ------------------------------------ */
+var emptyFunction = function(){};
+
+var Class = (function() {
+  
+  // Some versions of JScript fail to enumerate over properties, names of which 
+  // correspond to non-enumerable properties in the prototype chain
+  var IS_DONTENUM_BUGGY = (function(){
+    for (var p in { toString: 1 }) {
+      // check actual property name, so that it works with augmented Object.prototype
+      if (p === 'toString') return false;
+    }
+    return true;
+  })();
+  
+  function subclass() {};
+  function create() {
+    var parent = null, properties = [].slice.apply(arguments);
+    if (isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
+      this.initialize.apply(this, arguments);
+    }
+
+    extend(klass, Class.Methods);
+    klass.superclass = parent;
+    klass.subclasses = [];
+
+    if (parent) {
+      subclass.prototype = parent.prototype;
+      klass.prototype = new subclass;
+      try { parent.subclasses.push(klass) } catch(e) {}
+    }
+
+    for (var i = 0, length = properties.length; i < length; i++)
+      klass.addMethods(properties[i]);
+
+    if (!klass.prototype.initialize)
+      klass.prototype.initialize = emptyFunction;
+
+    klass.prototype.constructor = klass;
+    return klass;
+  }
+
+  function addMethods(source) {
+    var ancestor   = this.superclass && this.superclass.prototype,
+        properties = keys(source);
+
+    // IE6 doesn't enumerate `toString` and `valueOf` (among other built-in `Object.prototype`) properties,
+    // Force copy if they're not Object.prototype ones.
+    // Do not copy other Object.prototype.* for performance reasons
+    if (IS_DONTENUM_BUGGY) {
+      if (source.toString != Object.prototype.toString)
+        properties.push("toString");
+      if (source.valueOf != Object.prototype.valueOf)
+        properties.push("valueOf");
+    }
+
+    for (var i = 0, length = properties.length; i < length; i++) {
+      var property = properties[i], value = source[property];
+      if (ancestor && isFunction(value) &&
+          argumentNames(value)[0] == "$super") {
+        var method = value;
+        value = wrap((function(m) {
+          return function() { return ancestor[m].apply(this, arguments); };
+        })(property), method);
+
+        value.valueOf = bind(method.valueOf, method);
+        value.toString = bind(method.toString, method);
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
+  }
+
+  return {
+    create: create,
+    Methods: {
+      addMethods: addMethods
+    }
+  };
+})();
+
+if (globalContext.exports) {
+  globalContext.exports.Class = Class;
+}
+else {
+  globalContext.Class = Class;
+}
+})(Rickshaw);
+Rickshaw.namespace('Rickshaw.Compat.ClassList');
+
+Rickshaw.Compat.ClassList = function() {
+
+	/* adapted from http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
+
+	if (typeof document !== "undefined" && !("classList" in document.createElement("a"))) {
+
+	(function (view) {
+
+	"use strict";
+
+	var
+		  classListProp = "classList"
+		, protoProp = "prototype"
+		, elemCtrProto = (view.HTMLElement || view.Element)[protoProp]
+		, objCtr = Object
+		, strTrim = String[protoProp].trim || function () {
+			return this.replace(/^\s+|\s+$/g, "");
+		}
+		, arrIndexOf = Array[protoProp].indexOf || function (item) {
+			var
+				  i = 0
+				, len = this.length
+			;
+			for (; i < len; i++) {
+				if (i in this && this[i] === item) {
+					return i;
+				}
+			}
+			return -1;
+		}
+		// Vendors: please allow content code to instantiate DOMExceptions
+		, DOMEx = function (type, message) {
+			this.name = type;
+			this.code = DOMException[type];
+			this.message = message;
+		}
+		, checkTokenAndGetIndex = function (classList, token) {
+			if (token === "") {
+				throw new DOMEx(
+					  "SYNTAX_ERR"
+					, "An invalid or illegal string was specified"
+				);
+			}
+			if (/\s/.test(token)) {
+				throw new DOMEx(
+					  "INVALID_CHARACTER_ERR"
+					, "String contains an invalid character"
+				);
+			}
+			return arrIndexOf.call(classList, token);
+		}
+		, ClassList = function (elem) {
+			var
+				  trimmedClasses = strTrim.call(elem.className)
+				, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
+				, i = 0
+				, len = classes.length
+			;
+			for (; i < len; i++) {
+				this.push(classes[i]);
+			}
+			this._updateClassName = function () {
+				elem.className = this.toString();
+			};
+		}
+		, classListProto = ClassList[protoProp] = []
+		, classListGetter = function () {
+			return new ClassList(this);
+		}
+	;
+	// Most DOMException implementations don't allow calling DOMException's toString()
+	// on non-DOMExceptions. Error's toString() is sufficient here.
+	DOMEx[protoProp] = Error[protoProp];
+	classListProto.item = function (i) {
+		return this[i] || null;
+	};
+	classListProto.contains = function (token) {
+		token += "";
+		return checkTokenAndGetIndex(this, token) !== -1;
+	};
+	classListProto.add = function (token) {
+		token += "";
+		if (checkTokenAndGetIndex(this, token) === -1) {
+			this.push(token);
+			this._updateClassName();
+		}
+	};
+	classListProto.remove = function (token) {
+		token += "";
+		var index = checkTokenAndGetIndex(this, token);
+		if (index !== -1) {
+			this.splice(index, 1);
+			this._updateClassName();
+		}
+	};
+	classListProto.toggle = function (token) {
+		token += "";
+		if (checkTokenAndGetIndex(this, token) === -1) {
+			this.add(token);
+		} else {
+			this.remove(token);
+		}
+	};
+	classListProto.toString = function () {
+		return this.join(" ");
+	};
+
+	if (objCtr.defineProperty) {
+		var classListPropDesc = {
+			  get: classListGetter
+			, enumerable: true
+			, configurable: true
+		};
+		try {
+			objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
+		} catch (ex) { // IE 8 doesn't support enumerable:true
+			if (ex.number === -0x7FF5EC54) {
+				classListPropDesc.enumerable = false;
+				objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
+			}
+		}
+	} else if (objCtr[protoProp].__defineGetter__) {
+		elemCtrProto.__defineGetter__(classListProp, classListGetter);
+	}
+
+	}(window));
+
+	}
+};
+
+if ( (typeof RICKSHAW_NO_COMPAT !== "undefined" && !RICKSHAW_NO_COMPAT) || typeof RICKSHAW_NO_COMPAT === "undefined") {
+	new Rickshaw.Compat.ClassList();
+}
+Rickshaw.namespace('Rickshaw.Graph');
+
+Rickshaw.Graph = function(args) {
+
+	this.element = args.element;
+	this.series = args.series;
+
+	this.defaults = {
+		interpolation: 'cardinal',
+		offset: 'zero',
+		min: undefined,
+		max: undefined
+	};
+
+	Rickshaw.keys(this.defaults).forEach( function(k) {
+		this[k] = args[k] || this.defaults[k];
+	}, this );
+
+	this.window = {};
+
+	this.updateCallbacks = [];
+
+	var self = this;
+
+	this.initialize = function(args) {
+
+		this.validateSeries(args.series);
+
+		this.series.active = function() { return self.series.filter( function(s) { return !s.disabled } ) };
+
+		this.setSize({ width: args.width, height: args.height });
+
+		this.element.classList.add('rickshaw_graph');
+		this.vis = d3.select(this.element)
+			.append("svg:svg")
+			.attr('width', this.width)
+			.attr('height', this.height);
+
+		var renderers = [
+			Rickshaw.Graph.Renderer.Stack,
+			Rickshaw.Graph.Renderer.Line,
+			Rickshaw.Graph.Renderer.Bar,
+			Rickshaw.Graph.Renderer.Area,
+			Rickshaw.Graph.Renderer.ScatterPlot
+		];
+
+		renderers.forEach( function(r) {
+			if (!r) return;
+			self.registerRenderer(new r( { graph: self } ));
+		} );
+
+		this.setRenderer(args.renderer || 'stack', args);
+		this.discoverRange();
+	};
+
+	this.validateSeries = function(series) {
+
+		if (!(series instanceof Array) && !(series instanceof Rickshaw.Series)) {
+			var seriesSignature = Object.prototype.toString.apply(series);
+			throw "series is not an array: " + seriesSignature;
+		}
+
+		var pointsCount;
+
+		series.forEach( function(s) {
+
+			if (!(s instanceof Object)) {
+				throw "series element is not an object: " + s;
+			}
+			if (!(s.data)) {
+				throw "series has no data: " + JSON.stringify(s);
+			}
+			if (!(s.data instanceof Array)) {
+				throw "series data is not an array: " + JSON.stringify(s.data);
+			}
+
+			pointsCount = pointsCount || s.data.length;
+
+			if (pointsCount && s.data.length != pointsCount) {
+				throw "series cannot have differing numbers of points: " +
+					pointsCount	+ " vs " + s.data.length + "; see Rickshaw.Series.zeroFill()";
+			}
+
+			var dataTypeX = typeof s.data[0].x;
+			var dataTypeY = typeof s.data[0].y;
+
+			if (dataTypeX != 'number' || dataTypeY != 'number') {
+				throw "x and y properties of points should be numbers instead of " +
+					dataTypeX + " and " + dataTypeY;
+			}
+		} );
+	};
+
+	this.dataDomain = function() {
+
+		// take from the first series
+		var data = this.series[0].data;
+
+		return [ data[0].x, data.slice(-1).shift().x ];
+
+	};
+
+	this.discoverRange = function() {
+
+		var domain = this.renderer.domain();
+
+		this.x = d3.scale.linear().domain(domain.x).range([0, this.width]);
+
+		this.y = d3.scale.linear().domain(domain.y).range([this.height, 0]);
+
+		this.y.magnitude = d3.scale.linear()
+			.domain([domain.y[0] - domain.y[0], domain.y[1] - domain.y[0]])
+			.range([0, this.height]);
+	};
+
+	this.render = function() {
+
+		var stackedData = this.stackData();
+		this.discoverRange();
+
+		this.renderer.render();
+
+		this.updateCallbacks.forEach( function(callback) {
+			callback();
+		} );
+	};
+
+	this.update = this.render;
+
+	this.stackData = function() {
+
+		var data = this.series.active()
+			.map( function(d) { return d.data } )
+			.map( function(d) { return d.filter( function(d) { return this._slice(d) }, this ) }, this);
+
+		this.stackData.hooks.data.forEach( function(entry) {
+			data = entry.f.apply(self, [data]);
+		} );
+
+		var layout = d3.layout.stack();
+		layout.offset( self.offset );
+
+		var stackedData = layout(data);
+
+		this.stackData.hooks.after.forEach( function(entry) {
+			stackedData = entry.f.apply(self, [data]);
+		} );
+
+		var i = 0;
+		this.series.forEach( function(series) {
+			if (series.disabled) return;
+			series.stack = stackedData[i++];
+		} );
+
+		this.stackedData = stackedData;
+		return stackedData;
+	};
+
+	this.stackData.hooks = { data: [], after: [] };
+
+	this._slice = function(d) {
+
+		if (this.window.xMin || this.window.xMax) {
+
+			var isInRange = true;
+
+			if (this.window.xMin && d.x < this.window.xMin) isInRange = false;
+			if (this.window.xMax && d.x > this.window.xMax) isInRange = false;
+
+			return isInRange;
+		}
+
+		return true;
+	};
+
+	this.onUpdate = function(callback) {
+		this.updateCallbacks.push(callback);
+	};
+
+	this.registerRenderer = function(renderer) {
+		this._renderers = this._renderers || {};
+		this._renderers[renderer.name] = renderer;
+	};
+
+	this.configure = function(args) {
+
+		if (args.width || args.height) {
+			this.setSize(args);
+		}
+
+		Rickshaw.keys(this.defaults).forEach( function(k) {
+			this[k] = k in args ? args[k]
+				: k in this ? this[k]
+				: this.defaults[k];
+		}, this );
+
+		this.setRenderer(args.renderer || this.renderer.name, args);
+	};
+
+	this.setRenderer = function(name, args) {
+
+		if (!this._renderers[name]) {
+			throw "couldn't find renderer " + name;
+		}
+		this.renderer = this._renderers[name];
+
+		if (typeof args == 'object') {
+			this.renderer.configure(args);
+		}
+	};
+
+	this.setSize = function(args) {
+
+		args = args || {};
+
+		if (typeof window !== undefined) {
+			var style = window.getComputedStyle(this.element, null);
+			var elementWidth = parseInt(style.getPropertyValue('width'));
+			var elementHeight = parseInt(style.getPropertyValue('height'));
+		}
+
+		this.width = args.width || elementWidth || 400;
+		this.height = args.height || elementHeight || 250;
+
+		this.vis && this.vis
+			.attr('width', this.width)
+			.attr('height', this.height);
+	}
+
+	this.initialize(args);
+};
+Rickshaw.namespace('Rickshaw.Fixtures.Color');
+
+Rickshaw.Fixtures.Color = function() {
+
+	this.schemes = {};
+
+	this.schemes.spectrum14 = [
+		'#ecb796',
+		'#dc8f70',
+		'#b2a470',
+		'#92875a',
+		'#716c49',
+		'#d2ed82',
+		'#bbe468',
+		'#a1d05d',
+		'#e7cbe6',
+		'#d8aad6',
+		'#a888c2',
+		'#9dc2d3',
+		'#649eb9',
+		'#387aa3'
+	].reverse();
+
+	this.schemes.spectrum2000 = [
+		'#57306f',
+		'#514c76',
+		'#646583',
+		'#738394',
+		'#6b9c7d',
+		'#84b665',
+		'#a7ca50',
+		'#bfe746',
+		'#e2f528',
+		'#fff726',
+		'#ecdd00',
+		'#d4b11d',
+		'#de8800',
+		'#de4800',
+		'#c91515',
+		'#9a0000',
+		'#7b0429',
+		'#580839',
+		'#31082b'
+	];
+
+	this.schemes.spectrum2001 = [
+		'#2f243f',
+		'#3c2c55',
+		'#4a3768',
+		'#565270',
+		'#6b6b7c',
+		'#72957f',
+		'#86ad6e',
+		'#a1bc5e',
+		'#b8d954',
+		'#d3e04e',
+		'#ccad2a',
+		'#cc8412',
+		'#c1521d',
+		'#ad3821',
+		'#8a1010',
+		'#681717',
+		'#531e1e',
+		'#3d1818',
+		'#320a1b'
+	];
+
+	this.schemes.classic9 = [
+		'#423d4f',
+		'#4a6860',
+		'#848f39',
+		'#a2b73c',
+		'#ddcb53',
+		'#c5a32f',
+		'#7d5836',
+		'#963b20',
+		'#7c2626',
+		'#491d37',
+		'#2f254a'
+	].reverse();
+
+	this.schemes.httpStatus = {
+		503: '#ea5029',
+		502: '#d23f14',
+		500: '#bf3613',
+		410: '#efacea',
+		409: '#e291dc',
+		403: '#f457e8',
+		408: '#e121d2',
+		401: '#b92dae',
+		405: '#f47ceb',
+		404: '#a82a9f',
+		400: '#b263c6',
+		301: '#6fa024',
+		302: '#87c32b',
+		307: '#a0d84c',
+		304: '#28b55c',
+		200: '#1a4f74',
+		206: '#27839f',
+		201: '#52adc9',
+		202: '#7c979f',
+		203: '#a5b8bd',
+		204: '#c1cdd1'
+	};
+
+	this.schemes.colorwheel = [
+		'#b5b6a9',
+		'#858772',
+		'#785f43',
+		'#96557e',
+		'#4682b4',
+		'#65b9ac',
+		'#73c03a',
+		'#cb513a'
+	].reverse();
+
+	this.schemes.cool = [
+		'#5e9d2f',
+		'#73c03a',
+		'#4682b4',
+		'#7bc3b8',
+		'#a9884e',
+		'#c1b266',
+		'#a47493',
+		'#c09fb5'
+	];
+
+	this.schemes.munin = [
+		'#00cc00',
+		'#0066b3',
+		'#ff8000',
+		'#ffcc00',
+		'#330099',
+		'#990099',
+		'#ccff00',
+		'#ff0000',
+		'#808080',
+		'#008f00',
+		'#00487d',
+		'#b35a00',
+		'#b38f00',
+		'#6b006b',
+		'#8fb300',
+		'#b30000',
+		'#bebebe',
+		'#80ff80',
+		'#80c9ff',
+		'#ffc080',
+		'#ffe680',
+		'#aa80ff',
+		'#ee00cc',
+		'#ff8080',
+		'#666600',
+		'#ffbfff',
+		'#00ffcc',
+		'#cc6699',
+		'#999900'
+	];
+};
+Rickshaw.namespace('Rickshaw.Fixtures.RandomData');
+
+Rickshaw.Fixtures.RandomData = function(timeInterval) {
+
+	var addData;
+	timeInterval = timeInterval || 1;
+
+	var lastRandomValue = 200;
+
+	var timeBase = Math.floor(new Date().getTime() / 1000);
+
+	this.addData = function(data) {
+
+		var randomValue = Math.random() * 100 + 15 + lastRandomValue;
+		var index = data[0].length;
+
+		var counter = 1;
+
+		data.forEach( function(series) {
+			var randomVariance = Math.random() * 20;
+			var v = randomValue / 25  + counter++
+				+ (Math.cos((index * counter * 11) / 960) + 2) * 15 
+				+ (Math.cos(index / 7) + 2) * 7
+				+ (Math.cos(index / 17) + 2) * 1;
+
+			series.push( { x: (index * timeInterval) + timeBase, y: v + randomVariance } );
+		} );
+
+		lastRandomValue = randomValue * .85;
+	}
+};
+
+Rickshaw.namespace('Rickshaw.Fixtures.Time');
+
+Rickshaw.Fixtures.Time = function() {
+
+	var tzOffset = new Date().getTimezoneOffset() * 60;
+
+	var self = this;
+
+	this.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+
+	this.units = [
+		{
+			name: 'decade',
+			seconds: 86400 * 365.25 * 10,
+			formatter: function(d) { return (parseInt(d.getUTCFullYear() / 10) * 10) }
+		}, {
+			name: 'year',
+			seconds: 86400 * 365.25,
+			formatter: function(d) { return d.getUTCFullYear() }
+		}, {
+			name: 'month',
+			seconds: 86400 * 30.5,
+			formatter: function(d) { return self.months[d.getUTCMonth()] }
+		}, {
+			name: 'week',
+			seconds: 86400 * 7,
+			formatter: function(d) { return self.formatDate(d) }
+		}, {
+			name: 'day',
+			seconds: 86400,
+			formatter: function(d) { return d.getUTCDate() }
+		}, {
+			name: '6 hour',
+			seconds: 3600 * 6,
+			formatter: function(d) { return self.formatTime(d) }
+		}, {
+			name: 'hour',
+			seconds: 3600,
+			formatter: function(d) { return self.formatTime(d) }
+		}, {
+			name: '15 minute',
+			seconds: 60 * 15,
+			formatter: function(d) { return self.formatTime(d) }
+		}, {
+			name: 'minute',
+			seconds: 60,
+			formatter: function(d) { return d.getUTCMinutes() }
+		}, {
+			name: '15 second',
+			seconds: 15,
+			formatter: function(d) { return d.getUTCSeconds() + 's' }
+		}, {
+			name: 'second',
+			seconds: 1,
+			formatter: function(d) { return d.getUTCSeconds() + 's' }
+		}
+	];
+
+	this.unit = function(unitName) {
+		return this.units.filter( function(unit) { return unitName == unit.name } ).shift();
+	};
+
+	this.formatDate = function(d) {
+		return d.toUTCString().match(/, (\w+ \w+ \w+)/)[1];
+	};
+
+	this.formatTime = function(d) {
+		return d.toUTCString().match(/(\d+:\d+):/)[1];
+	};
+
+	this.ceil = function(time, unit) {
+
+		if (unit.name == 'month') {
+
+			var nearFuture = new Date((time + unit.seconds - 1) * 1000);
+
+			var rounded = new Date(0);
+			rounded.setUTCFullYear(nearFuture.getUTCFullYear());
+			rounded.setUTCMonth(nearFuture.getUTCMonth());
+			rounded.setUTCDate(1);
+			rounded.setUTCHours(0);
+			rounded.setUTCMinutes(0);
+			rounded.setUTCSeconds(0);
+			rounded.setUTCMilliseconds(0);
+
+			return rounded.getTime() / 1000;
+		}
+
+		if (unit.name == 'year') {
+
+			var nearFuture = new Date((time + unit.seconds - 1) * 1000);
+
+			var rounded = new Date(0);
+			rounded.setUTCFullYear(nearFuture.getUTCFullYear());
+			rounded.setUTCMonth(0);
+			rounded.setUTCDate(1);
+			rounded.setUTCHours(0);
+			rounded.setUTCMinutes(0);
+			rounded.setUTCSeconds(0);
+			rounded.setUTCMilliseconds(0);
+
+			return rounded.getTime() / 1000;
+		}
+
+		return Math.ceil(time / unit.seconds) * unit.seconds;
+	};
+};
+Rickshaw.namespace('Rickshaw.Fixtures.Number');
+
+Rickshaw.Fixtures.Number.formatKMBT = function(y) {
+	if (y >= 1000000000000)   { return y / 1000000000000 + "T" } 
+	else if (y >= 1000000000) { return y / 1000000000 + "B" } 
+	else if (y >= 1000000)    { return y / 1000000 + "M" } 
+	else if (y >= 1000)       { return y / 1000 + "K" }
+	else if (y < 1 && y > 0)  { return y.toFixed(2) }
+	else if (y == 0)          { return '' }
+	else                      { return y }
+};
+
+Rickshaw.Fixtures.Number.formatBase1024KMGTP = function(y) {
+    if (y >= 1125899906842624)  { return y / 1125899906842624 + "P" }
+    else if (y >= 1099511627776){ return y / 1099511627776 + "T" }
+    else if (y >= 1073741824)   { return y / 1073741824 + "G" }
+    else if (y >= 1048576)      { return y / 1048576 + "M" }
+    else if (y >= 1024)         { return y / 1024 + "K" }
+    else if (y < 1 && y > 0)    { return y.toFixed(2) }
+    else if (y == 0)            { return '' }
+    else                        { return y }
+};
+Rickshaw.namespace("Rickshaw.Color.Palette");
+
+Rickshaw.Color.Palette = function(args) {
+
+	var color = new Rickshaw.Fixtures.Color();
+
+	args = args || {};
+	this.schemes = {};
+
+	this.scheme = color.schemes[args.scheme] || args.scheme || color.schemes.colorwheel;
+	this.runningIndex = 0;
+	this.generatorIndex = 0;
+
+	if (args.interpolatedStopCount) {
+		var schemeCount = this.scheme.length - 1;
+		var i, j, scheme = [];
+		for (i = 0; i < schemeCount; i++) {
+			scheme.push(this.scheme[i]);
+			var generator = d3.interpolateHsl(this.scheme[i], this.scheme[i + 1]);
+			for (j = 1; j < args.interpolatedStopCount; j++) {
+				scheme.push(generator((1 / args.interpolatedStopCount) * j));
+			}
+		}
+		scheme.push(this.scheme[this.scheme.length - 1]);
+		this.scheme = scheme;
+	}
+	this.rotateCount = this.scheme.length;
+
+	this.color = function(key) {
+		return this.scheme[key] || this.scheme[this.runningIndex++] || this.interpolateColor() || '#808080';
+	};
+
+	this.interpolateColor = function() {
+		if (!Array.isArray(this.scheme)) return;
+		var color;
+		if (this.generatorIndex == this.rotateCount * 2 - 1) {
+			color = d3.interpolateHsl(this.scheme[this.generatorIndex], this.scheme[0])(0.5);
+			this.generatorIndex = 0;
+			this.rotateCount *= 2;
+		} else {
+			color = d3.interpolateHsl(this.scheme[this.generatorIndex], this.scheme[this.generatorIndex + 1])(0.5);
+			this.generatorIndex++;
+		}
+		this.scheme.push(color);
+		return color;
+	};
+
+};
+Rickshaw.namespace('Rickshaw.Graph.Ajax');
+
+Rickshaw.Graph.Ajax = Rickshaw.Class.create( {
+
+	initialize: function(args) {
+
+		this.dataURL = args.dataURL;
+
+		this.onData = args.onData || function(d) { return d };
+		this.onComplete = args.onComplete || function() {};
+		this.onError = args.onError || function() {};
+
+		this.args = args; // pass through to Rickshaw.Graph
+
+		this.request();
+	},
+
+	request: function() {
+
+		$.ajax( {
+			url: this.dataURL,
+			dataType: 'json',
+			success: this.success.bind(this),
+			error: this.error.bind(this)
+		} );
+	},
+
+	error: function() {
+
+		console.log("error loading dataURL: " + this.dataURL);
+		this.onError(this);
+	},
+
+	success: function(data, status) {
+
+		data = this.onData(data);
+		this.args.series = this._splice({ data: data, series: this.args.series });
+
+		this.graph = new Rickshaw.Graph(this.args);
+		this.graph.render();
+
+		this.onComplete(this);
+	},
+
+	_splice: function(args) {
+
+		var data = args.data;
+		var series = args.series;
+
+		if (!args.series) return data;
+
+		series.forEach( function(s) {
+
+			var seriesKey = s.key || s.name;
+			if (!seriesKey) throw "series needs a key or a name";
+
+			data.forEach( function(d) {
+
+				var dataKey = d.key || d.name;
+				if (!dataKey) throw "data needs a key or a name";
+
+				if (seriesKey == dataKey) {
+					var properties = ['color', 'name', 'data'];
+					properties.forEach( function(p) {
+						s[p] = s[p] || d[p];
+					} );
+				}
+			} );
+		} );
+
+		return series;
+	}
+} );
+
+Rickshaw.namespace('Rickshaw.Graph.Annotate');
+
+Rickshaw.Graph.Annotate = function(args) {
+
+	var graph = this.graph = args.graph;
+	this.elements = { timeline: args.element };
+	
+	var self = this;
+
+	this.data = {};
+
+	this.elements.timeline.classList.add('rickshaw_annotation_timeline');
+
+	this.add = function(time, content, end_time) {
+		self.data[time] = self.data[time] || {'boxes': []};
+		self.data[time].boxes.push({content: content, end: end_time});
+	};
+
+	this.update = function() {
+
+		Rickshaw.keys(self.data).forEach( function(time) {
+
+			var annotation = self.data[time];
+			var left = self.graph.x(time);
+
+			if (left < 0 || left > self.graph.x.range()[1]) {
+				if (annotation.element) {
+					annotation.line.classList.add('offscreen');
+					annotation.element.style.display = 'none';
+				}
+
+				annotation.boxes.forEach( function(box) {
+					if ( box.rangeElement ) box.rangeElement.classList.add('offscreen');
+				});
+
+				return;
+			}
+
+			if (!annotation.element) {
+				var element = annotation.element = document.createElement('div');
+				element.classList.add('annotation');
+				this.elements.timeline.appendChild(element);
+				element.addEventListener('click', function(e) {
+					element.classList.toggle('active');
+					annotation.line.classList.toggle('active');
+					annotation.boxes.forEach( function(box) {
+						if ( box.rangeElement ) box.rangeElement.classList.toggle('active');
+					});
+				}, false);
+					
+			}
+
+			annotation.element.style.left = left + 'px';
+			annotation.element.style.display = 'block';
+
+			annotation.boxes.forEach( function(box) {
+
+
+				var element = box.element;
+
+				if (!element) {
+					element = box.element = document.createElement('div');
+					element.classList.add('content');
+					element.innerHTML = box.content;
+					annotation.element.appendChild(element);
+
+					annotation.line = document.createElement('div');
+					annotation.line.classList.add('annotation_line');
+					self.graph.element.appendChild(annotation.line);
+
+					if ( box.end ) {
+						box.rangeElement = document.createElement('div');
+						box.rangeElement.classList.add('annotation_range');
+						self.graph.element.appendChild(box.rangeElement);
+					}
+
+				}
+
+				if ( box.end ) {
+
+					var annotationRangeStart = left;
+					var annotationRangeEnd   = Math.min( self.graph.x(box.end), self.graph.x.range()[1] );
+
+					// annotation makes more sense at end
+					if ( annotationRangeStart > annotationRangeEnd ) {
+						annotationRangeEnd   = left;
+						annotationRangeStart = Math.max( self.graph.x(box.end), self.graph.x.range()[0] );
+					}
+
+					var annotationRangeWidth = annotationRangeEnd - annotationRangeStart;
+
+					box.rangeElement.style.left  = annotationRangeStart + 'px';
+					box.rangeElement.style.width = annotationRangeWidth + 'px'
+
+					box.rangeElement.classList.remove('offscreen');
+				}
+
+				annotation.line.classList.remove('offscreen');
+				annotation.line.style.left = left + 'px';
+			} );
+		}, this );
+	};
+
+	this.graph.onUpdate( function() { self.update() } );
+};
+Rickshaw.namespace('Rickshaw.Graph.Axis.Time');
+
+Rickshaw.Graph.Axis.Time = function(args) {
+
+	var self = this;
+
+	this.graph = args.graph;
+	this.elements = [];
+	this.ticksTreatment = args.ticksTreatment || 'plain';
+	this.fixedTimeUnit = args.timeUnit;
+
+	var time = new Rickshaw.Fixtures.Time();
+
+	this.appropriateTimeUnit = function() {
+
+		var unit;
+		var units = time.units;
+
+		var domain = this.graph.x.domain();
+		var rangeSeconds = domain[1] - domain[0];
+
+		units.forEach( function(u) {
+			if (Math.floor(rangeSeconds / u.seconds) >= 2) {
+				unit = unit || u;
+			}
+		} );
+
+		return (unit || time.units[time.units.length - 1]);
+	};
+
+	this.tickOffsets = function() {
+
+		var domain = this.graph.x.domain();
+
+		var unit = this.fixedTimeUnit || this.appropriateTimeUnit();
+		var count = Math.ceil((domain[1] - domain[0]) / unit.seconds);
+
+		var runningTick = domain[0];
+
+		var offsets = [];
+
+		for (var i = 0; i < count; i++) {
+
+			tickValue = time.ceil(runningTick, unit);
+			runningTick = tickValue + unit.seconds / 2;
+
+			offsets.push( { value: tickValue, unit: unit } );
+		}
+
+		return offsets;
+	};
+
+	this.render = function() {
+
+		this.elements.forEach( function(e) {
+			e.parentNode.removeChild(e);
+		} );
+
+		this.elements = [];
+
+		var offsets = this.tickOffsets();
+
+		offsets.forEach( function(o) {
+			
+			if (self.graph.x(o.value) > self.graph.x.range()[1]) return;
+	
+			var element = document.createElement('div');
+			element.style.left = self.graph.x(o.value) + 'px';
+			element.classList.add('x_tick');
+			element.classList.add(self.ticksTreatment);
+
+			var title = document.createElement('div');
+			title.classList.add('title');
+			title.innerHTML = o.unit.formatter(new Date(o.value * 1000));
+			element.appendChild(title);
+
+			self.graph.element.appendChild(element);
+			self.elements.push(element);
+
+		} );
+	};
+
+	this.graph.onUpdate( function() { self.render() } );
+};
+
+Rickshaw.namespace('Rickshaw.Graph.Axis.Y');
+
+Rickshaw.Graph.Axis.Y = function(args) {
+
+	var self = this;
+	var berthRate = 0.10;
+
+	this.initialize = function(args) {
+
+		this.graph = args.graph;
+		this.orientation = args.orientation || 'right';
+
+		var pixelsPerTick = args.pixelsPerTick || 75;
+		this.ticks = args.ticks || Math.floor(this.graph.height / pixelsPerTick);
+		this.tickSize = args.tickSize || 4;
+		this.ticksTreatment = args.ticksTreatment || 'plain';
+
+		if (args.element) {
+
+			this.element = args.element;
+			this.vis = d3.select(args.element)
+				.append("svg:svg")
+				.attr('class', 'rickshaw_graph y_axis');
+
+			this.element = this.vis[0][0];
+			this.element.style.position = 'relative';
+
+			this.setSize({ width: args.width, height: args.height });
+
+		} else {
+			this.vis = this.graph.vis;
+		}
+
+		this.graph.onUpdate( function() { self.render() } );
+	};
+
+	this.setSize = function(args) {
+
+		args = args || {};
+
+		if (!this.element) return;
+
+		if (typeof window !== 'undefined') {
+
+			var style = window.getComputedStyle(this.element.parentNode, null);
+			var elementWidth = parseInt(style.getPropertyValue('width'));
+
+			if (!args.auto) {
+				var elementHeight = parseInt(style.getPropertyValue('height'));
+			}
+		}
+
+		this.width = args.width || elementWidth || this.graph.width * berthRate;
+		this.height = args.height || elementHeight || this.graph.height;
+
+		this.vis
+			.attr('width', this.width)
+			.attr('height', this.height * (1 + berthRate));
+
+		var berth = this.height * berthRate;
+		this.element.style.top = -1 * berth + 'px';
+	};
+
+	this.render = function() {
+
+		if (this.graph.height !== this._renderHeight) this.setSize({ auto: true });
+
+		var axis = d3.svg.axis().scale(this.graph.y).orient(this.orientation);
+		axis.tickFormat( args.tickFormat || function(y) { return y } );
+
+		if (this.orientation == 'left') {
+			var berth = this.height * berthRate;
+			var transform = 'translate(' + this.width + ', ' + berth + ')';
+		}
+
+		if (this.element) {
+			this.vis.selectAll('*').remove();
+		}
+
+		this.vis
+			.append("svg:g")
+			.attr("class", ["y_ticks", this.ticksTreatment].join(" "))
+			.attr("transform", transform)
+			.call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize))
+
+		var gridSize = (this.orientation == 'right' ? 1 : -1) * this.graph.width;
+
+		this.graph.vis
+			.append("svg:g")
+			.attr("class", "y_grid")
+			.call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize));
+
+		this._renderHeight = this.graph.height;
+	};
+
+	this.initialize(args);
+};
+
+Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Highlight');
+
+Rickshaw.Graph.Behavior.Series.Highlight = function(args) {
+
+	this.graph = args.graph;
+	this.legend = args.legend;
+
+	var self = this;
+
+	var colorSafe = {};
+
+	this.addHighlightEvents = function (l) {
+		l.element.addEventListener( 'mouseover', function(e) {
+
+			self.legend.lines.forEach( function(line) {
+				if (l === line) return;
+				colorSafe[line.series.name] = colorSafe[line.series.name] || line.series.color;
+				line.series.color = d3.interpolateRgb(line.series.color, d3.rgb('#d8d8d8'))(0.8).toString();
+			} );
+
+			self.graph.update();
+
+		}, false );
+
+		l.element.addEventListener( 'mouseout', function(e) {
+
+			self.legend.lines.forEach( function(line) {
+				if (colorSafe[line.series.name]) {
+					line.series.color = colorSafe[line.series.name];
+				}
+			} );
+
+			self.graph.update();
+
+		}, false );
+	};
+
+	if (this.legend) {
+		this.legend.lines.forEach( function(l) {
+			self.addHighlightEvents(l);
+		} );
+	}
+
+};
+Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Order');
+
+Rickshaw.Graph.Behavior.Series.Order = function(args) {
+
+	this.graph = args.graph;
+	this.legend = args.legend;
+
+	var self = this;
+
+	$(function() {
+		$(self.legend.list).sortable( { 
+			containment: 'parent',
+			tolerance: 'pointer',
+			update: function( event, ui ) {
+				var series = [];
+				$(self.legend.list).find('li').each( function(index, item) {
+					if (!item.series) return;
+					series.push(item.series);
+				} );
+
+				for (var i = self.graph.series.length - 1; i >= 0; i--) {
+					self.graph.series[i] = series.shift();
+				}
+
+				self.graph.update();
+			}
+		} );
+		$(self.legend.list).disableSelection();
+	});
+
+	//hack to make jquery-ui sortable behave
+	this.graph.onUpdate( function() { 
+		var h = window.getComputedStyle(self.legend.element).height;
+		self.legend.element.style.height = h;
+	} );
+};
+Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Toggle');
+
+Rickshaw.Graph.Behavior.Series.Toggle = function(args) {
+
+	this.graph = args.graph;
+	this.legend = args.legend;
+
+	var self = this;
+
+	this.addAnchor = function(line) {
+		var anchor = document.createElement('a');
+		anchor.innerHTML = '&#10004;';
+		anchor.classList.add('action');
+		line.element.insertBefore(anchor, line.element.firstChild);
+
+		anchor.onclick = function(e) {
+			if (line.series.disabled) {
+				line.series.enable();
+				line.element.classList.remove('disabled');
+			} else { 
+				line.series.disable();
+				line.element.classList.add('disabled');
+			}
+		}
+		
+                var label = line.element.getElementsByTagName('span')[0];
+                label.onclick = function(e){
+
+                        var disableAllOtherLines = line.series.disabled;
+                        if ( ! disableAllOtherLines ) {
+                                for ( var i = 0; i < self.legend.lines.length; i++ ) {
+                                        var l = self.legend.lines[i];
+                                        if ( line.series === l.series ) {
+                                                // noop
+                                        } else if ( l.series.disabled ) {
+                                                // noop
+                                        } else {
+                                                disableAllOtherLines = true;
+                                                break;
+                                        }
+                                }
+                        }
+
+                        // show all or none
+                        if ( disableAllOtherLines ) {
+
+                                // these must happen first or else we try ( and probably fail ) to make a no line graph
+                                line.series.enable();
+                                line.element.classList.remove('disabled');
+
+                                self.legend.lines.forEach(function(l){
+                                        if ( line.series === l.series ) {
+                                                // noop
+                                        } else {
+                                                l.series.disable();
+                                                l.element.classList.add('disabled');
+                                        }
+                                });
+
+                        } else {
+
+                                self.legend.lines.forEach(function(l){
+                                        l.series.enable();
+                                        l.element.classList.remove('disabled');
+                                });
+
+                        }
+
+                };
+
+	};
+
+	if (this.legend) {
+
+                $(this.legend.list).sortable( {
+                        start: function(event, ui) {
+                                ui.item.bind('no.onclick',
+                                        function(event) {
+                                                event.preventDefault();
+                                        }
+                                );
+                        },
+                        stop: function(event, ui) {
+                                setTimeout(function(){
+                                        ui.item.unbind('no.onclick');
+                                }, 250);
+                        }
+                })
+
+		this.legend.lines.forEach( function(l) {
+			self.addAnchor(l);
+		} );
+	}
+
+	this._addBehavior = function() {
+
+		this.graph.series.forEach( function(s) {
+			
+			s.disable = function() {
+
+				if (self.graph.series.length <= 1) {
+					throw('only one series left');
+				}
+				
+				s.disabled = true;
+				self.graph.update();
+			};
+
+			s.enable = function() {
+				s.disabled = false;
+				self.graph.update();
+			};
+		} );
+	};
+	this._addBehavior();
+
+	this.updateBehaviour = function () { this._addBehavior() };
+
+};
+Rickshaw.namespace('Rickshaw.Graph.HoverDetail');
+
+Rickshaw.Graph.HoverDetail = Rickshaw.Class.create({
+
+	initialize: function(args) {
+
+		var graph = this.graph = args.graph;
+
+		this.xFormatter = args.xFormatter || function(x) {
+			return new Date( x * 1000 ).toUTCString();
+		};
+
+		this.yFormatter = args.yFormatter || function(y) {
+			return y.toFixed(2);
+		};
+
+		var element = this.element = document.createElement('div');
+		element.className = 'detail';
+
+		this.visible = true;
+		graph.element.appendChild(element);
+
+		this.lastEvent = null;
+		this._addListeners();
+
+		this.onShow = args.onShow;
+		this.onHide = args.onHide;
+		this.onRender = args.onRender;
+
+		this.formatter = args.formatter || this.formatter;
+	},
+
+	formatter: function(series, x, y, formattedX, formattedY, d) {
+		return series.name + ':&nbsp;' + formattedY;
+	},
+
+	update: function(e) {
+
+		e = e || this.lastEvent;
+		if (!e) return;
+		this.lastEvent = e;
+
+		if (!e.target.nodeName.match(/^(path|svg|rect)$/)) return;
+
+		var graph = this.graph;
+
+		var eventX = e.offsetX || e.layerX;
+		var eventY = e.offsetY || e.layerY;
+
+		var domainX = graph.x.invert(eventX);
+		var stackedData = graph.stackedData;
+
+		var topSeriesData = stackedData.slice(-1).shift();
+
+		var domainIndexScale = d3.scale.linear()
+			.domain([topSeriesData[0].x, topSeriesData.slice(-1).shift().x])
+			.range([0, topSeriesData.length]);
+
+		var approximateIndex = Math.floor(domainIndexScale(domainX));
+		var dataIndex = Math.min(approximateIndex || 0, stackedData[0].length - 1);
+
+		for (var i = approximateIndex; i < stackedData[0].length - 1;) {
+
+			if (!stackedData[0][i] || !stackedData[0][i + 1]) {
+				break;
+			}
+
+			if (stackedData[0][i].x <= domainX && stackedData[0][i + 1].x > domainX) {
+				dataIndex = i;
+				break;
+			}
+			if (stackedData[0][i + 1] <= domainX) { i++ } else { i-- }
+		}
+
+		var domainX = stackedData[0][dataIndex].x;
+		var formattedXValue = this.xFormatter(domainX);
+		var graphX = graph.x(domainX);
+		var order = 0;
+
+		var detail = graph.series.active()
+			.map( function(s) { return { order: order++, series: s, name: s.name, value: s.stack[dataIndex] } } );
+
+		var activeItem;
+
+		var sortFn = function(a, b) {
+			return (a.value.y0 + a.value.y) - (b.value.y0 + b.value.y);
+		};
+
+		var domainMouseY = graph.y.magnitude.invert(graph.element.offsetHeight - eventY);
+
+		detail.sort(sortFn).forEach( function(d) {
+
+			d.formattedYValue = (this.yFormatter.constructor == Array) ?
+				this.yFormatter[detail.indexOf(d)](d.value.y) :
+				this.yFormatter(d.value.y);
+
+			d.graphX = graphX;
+			d.graphY = graph.y(d.value.y0 + d.value.y);
+
+			if (domainMouseY > d.value.y0 && domainMouseY < d.value.y0 + d.value.y && !activeItem) {
+				activeItem = d;
+				d.active = true;
+			}
+
+		}, this );
+
+		this.element.innerHTML = '';
+		this.element.style.left = graph.x(domainX) + 'px';
+
+		if (this.visible) {
+			this.render( {
+				detail: detail,
+				domainX: domainX,
+				formattedXValue: formattedXValue,
+				mouseX: eventX,
+				mouseY: eventY
+			} );
+		}
+	},
+
+	hide: function() {
+		this.visible = false;
+		this.element.classList.add('inactive');
+
+		if (typeof this.onHide == 'function') {
+			this.onHide();
+		}
+	},
+
+	show: function() {
+		this.visible = true;
+		this.element.classList.remove('inactive');
+
+		if (typeof this.onShow == 'function') {
+			this.onShow();
+		}
+	},
+
+	render: function(args) {
+
+		var detail = args.detail;
+		var domainX = args.domainX;
+
+		var mouseX = args.mouseX;
+		var mouseY = args.mouseY;
+
+		var formattedXValue = args.formattedXValue;
+
+		var xLabel = document.createElement('div');
+		xLabel.className = 'x_label';
+		xLabel.innerHTML = formattedXValue;
+		this.element.appendChild(xLabel);
+
+		detail.forEach( function(d) {
+
+			var item = document.createElement('div');
+			item.className = 'item';
+			item.innerHTML = this.formatter(d.series, domainX, d.value.y, formattedXValue, d.formattedYValue, d);
+			item.style.top = this.graph.y(d.value.y0 + d.value.y) + 'px';
+
+			this.element.appendChild(item);
+
+			var dot = document.createElement('div');
+			dot.className = 'dot';
+			dot.style.top = item.style.top;
+			dot.style.borderColor = d.series.color;
+
+			this.element.appendChild(dot);
+
+			if (d.active) {
+				item.className = 'item active';
+				dot.className = 'dot active';
+			}
+
+		}, this );
+
+		this.show();
+
+		if (typeof this.onRender == 'function') {
+			this.onRender(args);
+		}
+	},
+
+	_addListeners: function() {
+
+		this.graph.element.addEventListener(
+			'mousemove',
+			function(e) {
+				this.visible = true;
+				this.update(e)
+			}.bind(this),
+			false
+		);
+
+		this.graph.onUpdate( function() { this.update() }.bind(this) );
+
+		this.graph.element.addEventListener(
+			'mouseout',
+			function(e) {
+				if (e.relatedTarget && !(e.relatedTarget.compareDocumentPosition(this.graph.element) & Node.DOCUMENT_POSITION_CONTAINS)) {
+					this.hide();
+				}
+			 }.bind(this),
+			false
+		);
+	}
+});
+
+Rickshaw.namespace('Rickshaw.Graph.JSONP');
+
+Rickshaw.Graph.JSONP = Rickshaw.Class.create( Rickshaw.Graph.Ajax, {
+
+	request: function() {
+
+		$.ajax( {
+			url: this.dataURL,
+			dataType: 'jsonp',
+			success: this.success.bind(this),
+			error: this.error.bind(this)
+		} );
+	}
+} );
+Rickshaw.namespace('Rickshaw.Graph.Legend');
+
+Rickshaw.Graph.Legend = function(args) {
+
+	var element = this.element = args.element;
+	var graph = this.graph = args.graph;
+
+	var self = this;
+
+	element.classList.add('rickshaw_legend');
+
+	var list = this.list = document.createElement('ul');
+	element.appendChild(list);
+
+	var series = graph.series
+		.map( function(s) { return s } )
+		.reverse();
+
+	this.lines = [];
+
+	this.addLine = function (series) {
+		var line = document.createElement('li');
+		line.className = 'line';
+
+		var swatch = document.createElement('div');
+		swatch.className = 'swatch';
+		swatch.style.backgroundColor = series.color;
+
+		line.appendChild(swatch);
+
+		var label = document.createElement('span');
+		label.className = 'label';
+		label.innerHTML = series.name;
+
+		line.appendChild(label);
+		list.appendChild(line);
+
+		line.series = series;
+
+		if (series.noLegend) {
+			line.style.display = 'none';
+		}
+
+		var _line = { element: line, series: series };
+		if (self.shelving) {
+			self.shelving.addAnchor(_line);
+			self.shelving.updateBehaviour();
+		}
+		if (self.highlighter) {
+			self.highlighter.addHighlightEvents(_line);
+		}
+		self.lines.push(_line);
+	};
+
+	series.forEach( function(s) {
+		self.addLine(s);
+	} );
+
+	graph.onUpdate( function() {} );
+};
+Rickshaw.namespace('Rickshaw.Graph.RangeSlider');
+
+Rickshaw.Graph.RangeSlider = function(args) {
+
+	var element = this.element = args.element;
+	var graph = this.graph = args.graph;
+
+	$( function() {
+		$(element).slider( {
+
+			range: true,
+			min: graph.dataDomain()[0],
+			max: graph.dataDomain()[1],
+			values: [ 
+				graph.dataDomain()[0],
+				graph.dataDomain()[1]
+			],
+			slide: function( event, ui ) {
+
+				graph.window.xMin = ui.values[0];
+				graph.window.xMax = ui.values[1];
+				graph.update();
+
+				// if we're at an extreme, stick there
+				if (graph.dataDomain()[0] == ui.values[0]) {
+					graph.window.xMin = undefined;
+				}
+				if (graph.dataDomain()[1] == ui.values[1]) {
+					graph.window.xMax = undefined;
+				}
+			}
+		} );
+	} );
+
+	element[0].style.width = graph.width + 'px';
+
+	graph.onUpdate( function() {
+
+		var values = $(element).slider('option', 'values');
+
+		$(element).slider('option', 'min', graph.dataDomain()[0]);
+		$(element).slider('option', 'max', graph.dataDomain()[1]);
+
+		if (graph.window.xMin == undefined) {
+			values[0] = graph.dataDomain()[0];
+		}
+		if (graph.window.xMax == undefined) {
+			values[1] = graph.dataDomain()[1];
+		}
+
+		$(element).slider('option', 'values', values);
+
+	} );
+};
+
+Rickshaw.namespace("Rickshaw.Graph.Renderer");
+
+Rickshaw.Graph.Renderer = Rickshaw.Class.create( {
+
+	initialize: function(args) {
+		this.graph = args.graph;
+		this.tension = args.tension || this.tension;
+		this.graph.unstacker = this.graph.unstacker || new Rickshaw.Graph.Unstacker( { graph: this.graph } );
+		this.configure(args);
+	},
+
+	seriesPathFactory: function() {
+		//implement in subclass
+	},
+
+	seriesStrokeFactory: function() {
+		// implement in subclass
+	},
+
+	defaults: function() {
+		return {
+			tension: 0.8,
+			strokeWidth: 2,
+			unstack: true,
+			padding: { top: 0.01, right: 0, bottom: 0.01, left: 0 },
+			stroke: false,
+			fill: false
+		};
+	},
+
+	domain: function() {
+
+		var values = [];
+		var stackedData = this.graph.stackedData || this.graph.stackData();
+
+		var topSeriesData = this.unstack ? stackedData : [ stackedData.slice(-1).shift() ];
+
+		topSeriesData.forEach( function(series) {
+			series.forEach( function(d) {
+				values.push( d.y + d.y0 );
+			} );
+		} );
+
+		var xMin = stackedData[0][0].x;
+		var xMax = stackedData[0][ stackedData[0].length - 1 ].x;
+
+		xMin -= (xMax - xMin) * this.padding.left;
+		xMax += (xMax - xMin) * this.padding.right;
+
+		var yMin = this.graph.min === 'auto' ? d3.min( values ) : this.graph.min || 0;
+		var yMax = this.graph.max || d3.max( values );
+
+		if (this.graph.min === 'auto' || yMin < 0) {
+			yMin -= (yMax - yMin) * this.padding.bottom;
+		}
+
+		if (this.graph.max === undefined) {
+			yMax += (yMax - yMin) * this.padding.top;
+		}
+
+		return { x: [xMin, xMax], y: [yMin, yMax] };
+	},
+
+	render: function() {
+
+		var graph = this.graph;
+
+		graph.vis.selectAll('*').remove();
+
+		var nodes = graph.vis.selectAll("path")
+			.data(this.graph.stackedData)
+			.enter().append("svg:path")
+			.attr("d", this.seriesPathFactory());
+
+		var i = 0;
+		graph.series.forEach( function(series) {
+			if (series.disabled) return;
+			series.path = nodes[0][i++];
+			this._styleSeries(series);
+		}, this );
+	},
+
+	_styleSeries: function(series) {
+
+		var fill = this.fill ? series.color : 'none';
+		var stroke = this.stroke ? series.color : 'none';
+
+		series.path.setAttribute('fill', fill);
+		series.path.setAttribute('stroke', stroke);
+		series.path.setAttribute('stroke-width', this.strokeWidth);
+		series.path.setAttribute('class', series.className);
+	},
+
+	configure: function(args) {
+
+		args = args || {};
+
+		Rickshaw.keys(this.defaults()).forEach( function(key) {
+
+			if (!args.hasOwnProperty(key)) {
+				this[key] = this[key] || this.graph[key] || this.defaults()[key];
+				return;
+			}
+
+			if (typeof this.defaults()[key] == 'object') {
+
+				Rickshaw.keys(this.defaults()[key]).forEach( function(k) {
+
+					this[key][k] =
+						args[key][k] !== undefined ? args[key][k] :
+						this[key][k] !== undefined ? this[key][k] :
+						this.defaults()[key][k];
+				}, this );
+
+			} else {
+				this[key] =
+					args[key] !== undefined ? args[key] :
+					this[key] !== undefined ? this[key] :
+					this.graph[key] !== undefined ? this.graph[key] :
+					this.defaults()[key];
+			}
+
+		}, this );
+	},
+
+	setStrokeWidth: function(strokeWidth) {
+		if (strokeWidth !== undefined) {
+			this.strokeWidth = strokeWidth;
+		}
+	},
+
+	setTension: function(tension) {
+		if (tension !== undefined) {
+			this.tension = tension;
+		}
+	}
+} );
+
+Rickshaw.namespace('Rickshaw.Graph.Renderer.Line');
+
+Rickshaw.Graph.Renderer.Line = Rickshaw.Class.create( Rickshaw.Graph.Renderer, {
+
+	name: 'line',
+
+	defaults: function($super) {
+
+		return Rickshaw.extend( $super(), {
+			unstack: true,
+			fill: false,
+			stroke: true
+		} );
+	},
+
+	seriesPathFactory: function() {
+
+		var graph = this.graph;
+
+		return d3.svg.line()
+			.x( function(d) { return graph.x(d.x) } )
+			.y( function(d) { return graph.y(d.y) } )
+			.interpolate(this.graph.interpolation).tension(this.tension);
+	}
+} );
+
+Rickshaw.namespace('Rickshaw.Graph.Renderer.Stack');
+
+Rickshaw.Graph.Renderer.Stack = Rickshaw.Class.create( Rickshaw.Graph.Renderer, {
+
+	name: 'stack',
+
+	defaults: function($super) {
+
+		return Rickshaw.extend( $super(), {
+			fill: true,
+			stroke: false,
+			unstack: false
+		} );
+	},
+
+	seriesPathFactory: function() {
+
+		var graph = this.graph;
+
+		return d3.svg.area()
+			.x( function(d) { return graph.x(d.x) } )
+			.y0( function(d) { return graph.y(d.y0) } )
+			.y1( function(d) { return graph.y(d.y + d.y0) } )
+			.interpolate(this.graph.interpolation).tension(this.tension);
+	}
+} );
+
+Rickshaw.namespace('Rickshaw.Graph.Renderer.Bar');
+
+Rickshaw.Graph.Renderer.Bar = Rickshaw.Class.create( Rickshaw.Graph.Renderer, {
+
+	name: 'bar',
+
+	defaults: function($super) {
+
+		var defaults = Rickshaw.extend( $super(), {
+			gapSize: 0.05,
+			unstack: false
+		} );
+
+		delete defaults.tension;
+		return defaults;
+	},
+
+	initialize: function($super, args) {
+		args = args || {};
+		this.gapSize = args.gapSize || this.gapSize;
+		$super(args);
+	},
+
+	domain: function($super) {
+
+		var domain = $super();
+
+		var frequentInterval = this._frequentInterval();
+		domain.x[1] += parseInt(frequentInterval.magnitude);
+
+		return domain;
+	},
+
+	barWidth: function() {
+
+		var stackedData = this.graph.stackedData || this.graph.stackData();
+		var data = stackedData.slice(-1).shift();
+
+		var frequentInterval = this._frequentInterval();
+		var barWidth = this.graph.x(data[0].x + frequentInterval.magnitude * (1 - this.gapSize)); 
+
+		return barWidth;
+	},
+
+	render: function() {
+
+		var graph = this.graph;
+
+		graph.vis.selectAll('*').remove();
+
+		var barWidth = this.barWidth();
+		var barXOffset = 0;
+
+		var activeSeriesCount = graph.series.filter( function(s) { return !s.disabled; } ).length;
+		var seriesBarWidth = this.unstack ? barWidth / activeSeriesCount : barWidth;
+
+		var transform = function(d) {
+			// add a matrix transform for negative values
+			var matrix = [ 1, 0, 0, (d.y < 0 ? -1 : 1), 0, (d.y < 0 ? graph.y.magnitude(Math.abs(d.y)) * 2 : 0) ];
+			return "matrix(" + matrix.join(',') + ")";
+		};
+
+		graph.series.forEach( function(series) {
+
+			if (series.disabled) return;
+
+			var nodes = graph.vis.selectAll("path")
+				.data(series.stack)
+				.enter().append("svg:rect")
+				.attr("x", function(d) { return graph.x(d.x) + barXOffset })
+				.attr("y", function(d) { return (graph.y(d.y0 + Math.abs(d.y))) * (d.y < 0 ? -1 : 1 ) })
+				.attr("width", seriesBarWidth)
+				.attr("height", function(d) { return graph.y.magnitude(Math.abs(d.y)) })
+				.attr("transform", transform);
+
+			Array.prototype.forEach.call(nodes[0], function(n) {
+				n.setAttribute('fill', series.color);
+			} );
+
+			if (this.unstack) barXOffset += seriesBarWidth;
+
+		}, this );
+	},
+
+	_frequentInterval: function() {
+
+		var stackedData = this.graph.stackedData || this.graph.stackData();
+		var data = stackedData.slice(-1).shift();
+
+		var intervalCounts = {};
+
+		for (var i = 0; i < data.length - 1; i++) {
+			var interval = data[i + 1].x - data[i].x;
+			intervalCounts[interval] = intervalCounts[interval] || 0;
+			intervalCounts[interval]++;
+		}
+
+		var frequentInterval = { count: 0 };
+
+		Rickshaw.keys(intervalCounts).forEach( function(i) {
+			if (frequentInterval.count < intervalCounts[i]) {
+
+				frequentInterval = {
+					count: intervalCounts[i],
+					magnitude: i
+				};
+			}
+		} );
+
+		this._frequentInterval = function() { return frequentInterval };
+
+		return frequentInterval;
+	}
+} );
+
+Rickshaw.namespace('Rickshaw.Graph.Renderer.Area');
+
+Rickshaw.Graph.Renderer.Area = Rickshaw.Class.create( Rickshaw.Graph.Renderer, {
+
+	name: 'area',
+
+	defaults: function($super) {
+
+		return Rickshaw.extend( $super(), {
+			unstack: false,
+			fill: false,
+			stroke: false
+		} );
+	},
+
+	seriesPathFactory: function() {
+
+		var graph = this.graph;
+
+		return d3.svg.area()
+			.x( function(d) { return graph.x(d.x) } )
+			.y0( function(d) { return graph.y(d.y0) } )
+			.y1( function(d) { return graph.y(d.y + d.y0) } )
+			.interpolate(graph.interpolation).tension(this.tension);
+	},
+
+	seriesStrokeFactory: function() {
+
+		var graph = this.graph;
+
+		return d3.svg.line()
+			.x( function(d) { return graph.x(d.x) } )
+			.y( function(d) { return graph.y(d.y + d.y0) } )
+			.interpolate(graph.interpolation).tension(this.tension);
+	},
+
+	render: function() {
+
+		var graph = this.graph;
+
+		graph.vis.selectAll('*').remove();
+
+		var nodes = graph.vis.selectAll("path")
+			.data(this.graph.stackedData)
+			.enter().insert("svg:g", 'g');
+
+		nodes.append("svg:path")
+			.attr("d", this.seriesPathFactory())
+			.attr("class", 'area');
+
+		if (this.stroke) {
+			nodes.append("svg:path")
+				.attr("d", this.seriesStrokeFactory())
+				.attr("class", 'line');
+		}
+
+		var i = 0;
+		graph.series.forEach( function(series) {
+			if (series.disabled) return;
+			series.path = nodes[0][i++];
+			this._styleSeries(series);
+		}, this );
+	},
+
+	_styleSeries: function(series) {
+
+		if (!series.path) return;
+
+		d3.select(series.path).select('.area')
+			.attr('fill', series.color);
+
+		if (this.stroke) {
+			d3.select(series.path).select('.line')
+				.attr('fill', 'none')
+				.attr('stroke', series.stroke || d3.interpolateRgb(series.color, 'black')(0.125))
+				.attr('stroke-width', this.strokeWidth);
+		}
+
+		if (series.className) {
+			series.path.setAttribute('class', series.className);
+		}
+	}
+} );
+
+Rickshaw.namespace('Rickshaw.Graph.Renderer.ScatterPlot');
+
+Rickshaw.Graph.Renderer.ScatterPlot = Rickshaw.Class.create( Rickshaw.Graph.Renderer, {
+
+	name: 'scatterplot',
+
+	defaults: function($super) {
+
+		return Rickshaw.extend( $super(), {
+			unstack: true,
+			fill: true,
+			stroke: false,
+			padding:{ top: 0.01, right: 0.01, bottom: 0.01, left: 0.01 },
+			dotSize: 4
+		} );
+	},
+
+	initialize: function($super, args) {
+		$super(args);
+	},
+
+	render: function() {
+
+		var graph = this.graph;
+
+		graph.vis.selectAll('*').remove();
+
+		graph.series.forEach( function(series) {
+
+			if (series.disabled) return;
+
+			var nodes = graph.vis.selectAll("path")
+				.data(series.stack)
+				.enter().append("svg:circle")
+				.attr("cx", function(d) { return graph.x(d.x) })
+				.attr("cy", function(d) { return graph.y(d.y) })
+				.attr("r", function(d) { return ("r" in d) ? d.r : graph.renderer.dotSize});
+
+			Array.prototype.forEach.call(nodes[0], function(n) {
+				n.setAttribute('fill', series.color);
+			} );
+
+		}, this );
+	}
+} );
+Rickshaw.namespace('Rickshaw.Graph.Smoother');
+
+Rickshaw.Graph.Smoother = function(args) {
+
+	this.graph = args.graph;
+	this.element = args.element;
+
+	var self = this;
+
+	this.aggregationScale = 1;
+
+	if (this.element) {
+
+		$( function() {
+			$(self.element).slider( {
+				min: 1,
+				max: 100,
+				slide: function( event, ui ) {
+					self.setScale(ui.value);
+					self.graph.update();
+				}
+			} );
+		} );
+	}
+
+	self.graph.stackData.hooks.data.push( {
+		name: 'smoother',
+		orderPosition: 50,
+		f: function(data) {
+
+			var aggregatedData = [];
+
+			data.forEach( function(seriesData) {
+				
+				var aggregatedSeriesData = [];
+
+				while (seriesData.length) {
+
+					var avgX = 0, avgY = 0;
+					var slice = seriesData.splice(0, self.aggregationScale);
+
+					slice.forEach( function(d) {
+						avgX += d.x / slice.length;
+						avgY += d.y / slice.length;
+					} );
+
+					aggregatedSeriesData.push( { x: avgX, y: avgY } );
+				}
+
+				aggregatedData.push(aggregatedSeriesData);
+			} );
+
+			return aggregatedData;
+		}
+	} );
+
+	this.setScale = function(scale) {
+
+		if (scale < 1) {
+			throw "scale out of range: " + scale;
+		}
+
+		this.aggregationScale = scale;
+		this.graph.update();
+	}
+};
+
+Rickshaw.namespace('Rickshaw.Graph.Unstacker');
+
+Rickshaw.Graph.Unstacker = function(args) {
+
+	this.graph = args.graph;
+	var self = this;
+
+	this.graph.stackData.hooks.after.push( {
+		name: 'unstacker',
+		f: function(data) {
+
+			if (!self.graph.renderer.unstack) return data;
+
+			data.forEach( function(seriesData) {
+				seriesData.forEach( function(d) {
+					d.y0 = 0;
+				} );
+			} );
+
+			return data;
+		}
+	} );
+};
+
+Rickshaw.namespace('Rickshaw.Series');
+
+Rickshaw.Series = Rickshaw.Class.create( Array, {
+
+	initialize: function (data, palette, options) {
+
+		options = options || {}
+
+		this.palette = new Rickshaw.Color.Palette(palette);
+
+		this.timeBase = typeof(options.timeBase) === 'undefined' ? 
+			Math.floor(new Date().getTime() / 1000) : 
+			options.timeBase;
+
+		var timeInterval = typeof(options.timeInterval) == 'undefined' ?
+			1000 :
+			options.timeInterval;
+
+		this.setTimeInterval(timeInterval);
+
+		if (data && (typeof(data) == "object") && (data instanceof Array)) {
+			data.forEach( function(item) { this.addItem(item) }, this );
+		}
+	},
+
+	addItem: function(item) {
+
+		if (typeof(item.name) === 'undefined') {
+			throw('addItem() needs a name');
+		}
+
+		item.color = (item.color || this.palette.color(item.name));
+		item.data = (item.data || []);
+
+		// backfill, if necessary
+		if ((item.data.length == 0) && this.length && (this.getIndex() > 0)) {
+			this[0].data.forEach( function(plot) {
+				item.data.push({ x: plot.x, y: 0 });
+			} );
+		} else if (item.data.length == 0) {
+			item.data.push({ x: this.timeBase - (this.timeInterval || 0), y: 0 });
+		} 
+
+		this.push(item);
+
+		if (this.legend) {
+			this.legend.addLine(this.itemByName(item.name));
+		}
+	},
+
+	addData: function(data) {
+
+		var index = this.getIndex();
+
+		Rickshaw.keys(data).forEach( function(name) {
+			if (! this.itemByName(name)) {
+				this.addItem({ name: name });
+			}
+		}, this );
+
+		this.forEach( function(item) {
+			item.data.push({ 
+				x: (index * this.timeInterval || 1) + this.timeBase, 
+				y: (data[item.name] || 0) 
+			});
+		}, this );
+	},
+
+	getIndex: function () {
+		return (this[0] && this[0].data && this[0].data.length) ? this[0].data.length : 0;
+	},
+
+	itemByName: function(name) {
+
+		for (var i = 0; i < this.length; i++) {
+			if (this[i].name == name)
+				return this[i];
+		}
+	},
+
+	setTimeInterval: function(iv) {
+		this.timeInterval = iv / 1000;
+	},
+
+	setTimeBase: function (t) {
+		this.timeBase = t;
+	},
+
+	dump: function() {
+
+		var data = {
+			timeBase: this.timeBase,
+			timeInterval: this.timeInterval,
+			items: []
+		};
+
+		this.forEach( function(item) {
+
+			var newItem = {
+				color: item.color,
+				name: item.name,
+				data: []
+			};
+
+			item.data.forEach( function(plot) {
+				newItem.data.push({ x: plot.x, y: plot.y });
+			} );
+
+			data.items.push(newItem);
+		} );
+
+		return data;
+	},
+
+	load: function(data) {
+
+		if (data.timeInterval) {
+			this.timeInterval = data.timeInterval;
+		}
+
+		if (data.timeBase) {
+			this.timeBase = data.timeBase;
+		}
+
+		if (data.items) {
+			data.items.forEach( function(item) {
+				this.push(item);
+				if (this.legend) {
+					this.legend.addLine(this.itemByName(item.name));
+				}
+
+			}, this );
+		}
+	}
+} );
+
+Rickshaw.Series.zeroFill = function(series) {
+
+	var x;
+	var i = 0;
+
+	var data = series.map( function(s) { return s.data } );
+
+	while ( i < Math.max.apply(null, data.map( function(d) { return d.length } )) ) {
+
+		x = Math.min.apply( null, 
+			data
+				.filter(function(d) { return d[i] })
+				.map(function(d) { return d[i].x })
+		);
+
+		data.forEach( function(d) {
+			if (!d[i] || d[i].x != x) {
+				d.splice(i, 0, { x: x, y: 0 });
+			}
+		} );
+
+		i++;
+	}
+};
+Rickshaw.namespace('Rickshaw.Series.FixedDuration');
+
+Rickshaw.Series.FixedDuration = Rickshaw.Class.create(Rickshaw.Series, {
+
+	initialize: function (data, palette, options) {
+
+		var options = options || {}
+
+		if (typeof(options.timeInterval) === 'undefined') {
+			throw new Error('FixedDuration series requires timeInterval');
+		}
+
+		if (typeof(options.maxDataPoints) === 'undefined') {
+			throw new Error('FixedDuration series requires maxDataPoints');
+		}
+
+		this.palette = new Rickshaw.Color.Palette(palette);
+		this.timeBase = typeof(options.timeBase) === 'undefined' ? Math.floor(new Date().getTime() / 1000) : options.timeBase;
+		this.setTimeInterval(options.timeInterval);
+
+		if (this[0] && this[0].data && this[0].data.length) {
+			this.currentSize = this[0].data.length;
+			this.currentIndex = this[0].data.length;
+		} else {
+			this.currentSize  = 0;
+			this.currentIndex = 0;
+		}
+
+		this.maxDataPoints = options.maxDataPoints;
+
+
+		if (data && (typeof(data) == "object") && (data instanceof Array)) {
+			data.forEach( function (item) { this.addItem(item) }, this );
+			this.currentSize  += 1;
+			this.currentIndex += 1;
+		}
+
+		// reset timeBase for zero-filled values if needed
+		this.timeBase -= (this.maxDataPoints - this.currentSize) * this.timeInterval;
+
+		// zero-fill up to maxDataPoints size if we don't have that much data yet
+		if ((typeof(this.maxDataPoints) !== 'undefined') && (this.currentSize < this.maxDataPoints)) {
+			for (var i = this.maxDataPoints - this.currentSize - 1; i > 0; i--) {
+				this.currentSize  += 1;
+				this.currentIndex += 1;
+				this.forEach( function (item) {
+					item.data.unshift({ x: ((i-1) * this.timeInterval || 1) + this.timeBase, y: 0, i: i });
+				}, this );
+			}
+		}
+	},
+
+	addData: function($super, data) {
+
+		$super(data)
+
+		this.currentSize += 1;
+		this.currentIndex += 1;
+
+		if (this.maxDataPoints !== undefined) {
+			while (this.currentSize > this.maxDataPoints) {
+				this.dropData();
+			}
+		}
+	},
+
+	dropData: function() {
+
+		this.forEach(function(item) {
+			item.data.splice(0, 1);
+		} );
+
+		this.currentSize -= 1;
+	},
+
+	getIndex: function () {
+		return this.currentIndex;
+	}
+} );
+

+ 307 - 0
ambari-web/vendor/styles/rickshaw.css

@@ -0,0 +1,307 @@
+.rickshaw_graph .detail {
+	pointer-events: none;
+	position: absolute;
+	top: 0;
+	z-index: 2;
+	background: rgba(0, 0, 0, 0.1);
+	bottom: 0;
+	width: 1px;
+	transition: opacity 0.25s linear;
+	-moz-transition: opacity 0.25s linear;
+	-o-transition: opacity 0.25s linear;
+	-webkit-transition: opacity 0.25s linear;
+}
+.rickshaw_graph .detail.inactive {
+	opacity: 0;
+}
+.rickshaw_graph .detail .item.active {
+	opacity: 1;
+}
+.rickshaw_graph .detail .x_label {
+	font-family: Arial, sans-serif;
+	border-radius: 3px;
+	padding: 6px;
+	opacity: 0.5;
+	border: 1px solid #e0e0e0;
+	font-size: 12px;
+	position: absolute;
+	background: white;
+	white-space: nowrap;
+}
+.rickshaw_graph .detail .item {
+	position: absolute;
+	z-index: 2;
+	border-radius: 3px;
+	padding: 0.25em;
+	font-size: 12px;
+	font-family: Arial, sans-serif;
+	opacity: 0;
+	background: rgba(0, 0, 0, 0.4);
+	color: white;
+	border: 1px solid rgba(0, 0, 0, 0.4);
+	margin-left: 1em;
+	margin-top: -1em;
+	white-space: nowrap;
+}
+.rickshaw_graph .detail .item.active {
+	opacity: 1;
+	background: rgba(0, 0, 0, 0.8);
+}
+.rickshaw_graph .detail .item:before {
+	content: "\25c2";
+	position: absolute;
+	left: -0.5em;
+	color: rgba(0, 0, 0, 0.7);
+	width: 0;
+}
+.rickshaw_graph .detail .dot {
+	width: 4px;
+	height: 4px;
+	margin-left: -4px;
+	margin-top: -3px;
+	border-radius: 5px;
+	position: absolute;
+	box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
+	background: white;
+	border-width: 2px;
+	border-style: solid;
+	display: none;
+	background-clip: padding-box;
+}
+.rickshaw_graph .detail .dot.active {
+	display: block;
+}
+/* graph */
+
+.rickshaw_graph {
+	position: relative;
+}
+.rickshaw_graph svg {
+	display: block;	
+	overflow: hidden;
+}
+
+/* ticks */
+
+.rickshaw_graph .x_tick {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+	width: 0px;
+	border-left: 1px dotted rgba(0, 0, 0, 0.2);
+	pointer-events: none;
+}
+.rickshaw_graph .x_tick .title {
+	position: absolute;
+	font-size: 12px;
+	font-family: Arial, sans-serif;
+	opacity: 0.5;
+	white-space: nowrap;
+	margin-left: 3px;
+	bottom: 1px;
+}
+
+/* annotations */
+
+.rickshaw_annotation_timeline {
+	height: 1px;
+	border-top: 1px solid #e0e0e0;
+	margin-top: 10px;
+	position: relative;
+}
+.rickshaw_annotation_timeline .annotation {
+	position: absolute;
+	height: 6px;
+	width: 6px;
+	margin-left: -2px;
+	top: -3px;
+	border-radius: 5px;
+	background-color: rgba(0, 0, 0, 0.25);
+}
+.rickshaw_graph .annotation_line {
+	position: absolute;
+	top: 0;
+	bottom: -6px;
+	width: 0px;
+	border-left: 2px solid rgba(0, 0, 0, 0.3);
+	display: none;
+}
+.rickshaw_graph .annotation_line.active {
+	display: block;
+}
+
+.rickshaw_graph .annotation_range {
+        background: rgba(0, 0, 0, 0.1);
+        display: none;
+        position: absolute;
+        top: 0;
+        bottom: -6px;
+        z-index: -10;
+}
+.rickshaw_graph .annotation_range.active {
+        display: block;
+}
+.rickshaw_graph .annotation_range.active.offscreen {
+        display: none;
+}
+
+.rickshaw_annotation_timeline .annotation .content {
+	background: white;
+	color: black;
+	opacity: 0.9;
+	padding: 5px 5px;
+	box-shadow: 0 0 2px rgba(0, 0, 0, 0.8);
+	border-radius: 3px;
+	position: relative;
+	z-index: 20;
+	font-size: 12px;
+	padding: 6px 8px 8px;
+	top: 18px;
+	left: -11px;
+	width: 160px;
+	display: none;
+	cursor: pointer;
+}
+.rickshaw_annotation_timeline .annotation .content:before {
+	content: "\25b2";
+	position: absolute;
+	top: -11px;
+	color: white;
+	text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.8);
+}
+.rickshaw_annotation_timeline .annotation.active,
+.rickshaw_annotation_timeline .annotation:hover {
+	background-color: rgba(0, 0, 0, 0.8);
+	cursor: none;
+}
+.rickshaw_annotation_timeline .annotation .content:hover {
+	z-index: 50;
+}
+.rickshaw_annotation_timeline .annotation.active .content {
+	display: block;
+}
+.rickshaw_annotation_timeline .annotation:hover .content {
+	display: block;
+	z-index: 50;
+}
+.rickshaw_graph .y_axis {
+	fill: none;
+}
+.rickshaw_graph .y_ticks .tick {
+	stroke: rgba(0, 0, 0, 0.16);
+	stroke-width: 2px;
+	shape-rendering: crisp-edges;
+	pointer-events: none;
+}
+.rickshaw_graph .y_grid .tick {
+	z-index: -1;
+	stroke: rgba(0, 0, 0, 0.20);
+	stroke-width: 1px;
+	stroke-dasharray: 1 1;
+}
+.rickshaw_graph .y_grid path {
+	fill: none;
+	stroke: none;
+}
+.rickshaw_graph .y_ticks path {
+	fill: none;
+	stroke: #808080;
+}
+.rickshaw_graph .y_ticks text {
+	opacity: 0.5;
+	font-size: 12px;
+	pointer-events: none;
+}
+.rickshaw_graph .x_tick.glow .title,
+.rickshaw_graph .y_ticks.glow text {
+	fill: black;
+	color: black;
+	text-shadow: 
+		-1px 1px 0 rgba(255, 255, 255, 0.1),
+		1px -1px 0 rgba(255, 255, 255, 0.1),
+		1px 1px 0 rgba(255, 255, 255, 0.1),
+		0px 1px 0 rgba(255, 255, 255, 0.1),
+		0px -1px 0 rgba(255, 255, 255, 0.1),
+		1px 0px 0 rgba(255, 255, 255, 0.1),
+		-1px 0px 0 rgba(255, 255, 255, 0.1),
+		-1px -1px 0 rgba(255, 255, 255, 0.1);
+}
+.rickshaw_graph .x_tick.inverse .title,
+.rickshaw_graph .y_ticks.inverse text {
+	fill: white;
+	color: white;
+	text-shadow: 
+		-1px 1px 0 rgba(0, 0, 0, 0.8),
+		1px -1px 0 rgba(0, 0, 0, 0.8),
+		1px 1px 0 rgba(0, 0, 0, 0.8),
+		0px 1px 0 rgba(0, 0, 0, 0.8),
+		0px -1px 0 rgba(0, 0, 0, 0.8),
+		1px 0px 0 rgba(0, 0, 0, 0.8),
+		-1px 0px 0 rgba(0, 0, 0, 0.8),
+		-1px -1px 0 rgba(0, 0, 0, 0.8);
+}
+.rickshaw_legend {
+	font-family: Arial;
+	font-size: 12px;
+	color: white;
+	background: #404040;
+	display: inline-block;
+	padding: 12px 5px; 
+	border-radius: 2px;
+	position: relative;
+}
+.rickshaw_legend:hover {
+	z-index: 10;
+}
+.rickshaw_legend .swatch {
+	width: 10px;
+	height: 10px;
+	border: 1px solid rgba(0, 0, 0, 0.2);
+}
+.rickshaw_legend .line {
+	clear: both;
+	line-height: 140%;
+	padding-right: 15px;
+}
+.rickshaw_legend .line .swatch {
+	display: inline-block;
+	margin-right: 3px;
+	border-radius: 2px;
+}
+.rickshaw_legend .label {
+	white-space: nowrap;
+	display: inline;
+}
+.rickshaw_legend .action:hover {
+	opacity: 0.6;
+}
+.rickshaw_legend .action {
+	margin-right: 0.2em;
+	font-size: 10px;
+	opacity: 0.2;
+	cursor: pointer;
+	font-size: 14px;
+}
+.rickshaw_legend .line.disabled {
+	opacity: 0.4;
+}
+.rickshaw_legend ul {
+	list-style-type: none;
+	margin: 0;
+	padding: 0;
+	margin: 2px;
+	cursor: pointer;
+}
+.rickshaw_legend li {
+	padding: 0 0 0 2px;
+	min-width: 80px;
+	white-space: nowrap;
+}
+.rickshaw_legend li:hover {
+	background: rgba(255, 255, 255, 0.08);
+	border-radius: 3px;
+}
+.rickshaw_legend li:active {
+	background: rgba(255, 255, 255, 0.2);
+	border-radius: 3px;
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов