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

Merge branch 'series_style_overrides'

Torkel Ödegaard 11 лет назад
Родитель
Сommit
5bf794e24e

+ 1 - 1
package.json

@@ -34,7 +34,7 @@
     "grunt-string-replace": "~0.2.4",
     "grunt-usemin": "^2.1.1",
     "jshint-stylish": "~0.1.5",
-    "karma": "~0.12.16",
+    "karma": "~0.12.21",
     "karma-chrome-launcher": "~0.1.4",
     "karma-coffee-preprocessor": "~0.1.2",
     "karma-coverage": "^0.2.5",

+ 49 - 6
src/app/panels/graph/timeSeries.js → src/app/components/timeSeries.js

@@ -5,15 +5,57 @@ define([
 function (_, kbn) {
   'use strict';
 
-  var ts = {};
-
-  ts.ZeroFilled = function (opts) {
+  function TimeSeries(opts) {
     this.datapoints = opts.datapoints;
     this.info = opts.info;
     this.label = opts.info.alias;
+  }
+
+  function matchSeriesOverride(aliasOrRegex, seriesAlias) {
+    if (!aliasOrRegex) { return false; }
+
+    if (aliasOrRegex[0] === '/') {
+      var match = aliasOrRegex.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
+      var regex = new RegExp(match[1], match[2]);
+      return seriesAlias.match(regex) != null;
+    }
+
+    return aliasOrRegex === seriesAlias;
+  }
+
+  function translateFillOption(fill) {
+    return fill === 0 ? 0.001 : fill/10;
+  }
+
+  TimeSeries.prototype.applySeriesOverrides = function(overrides) {
+    this.lines = {};
+    this.points = {};
+    this.bars = {};
+    this.info.yaxis = 1;
+    this.zindex = 0;
+    delete this.stack;
+
+    for (var i = 0; i < overrides.length; i++) {
+      var override = overrides[i];
+      if (!matchSeriesOverride(override.alias, this.info.alias)) {
+        continue;
+      }
+      if (override.lines !== void 0) { this.lines.show = override.lines; }
+      if (override.points !== void 0) { this.points.show = override.points; }
+      if (override.bars !== void 0) { this.bars.show = override.bars; }
+      if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); }
+      if (override.stack !== void 0) { this.stack = override.stack; }
+      if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; }
+      if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
+      if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
+      if (override.zindex !== void 0) { this.zindex = override.zindex; }
+      if (override.yaxis !== void 0) {
+        this.info.yaxis = override.yaxis;
+      }
+    }
   };
 
-  ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle, yFormats) {
+  TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
     var result = [];
 
     this.color = this.info.color;
@@ -74,5 +116,6 @@ function (_, kbn) {
     return result;
   };
 
-  return ts;
-});
+  return TimeSeries;
+
+});

+ 1 - 1
src/app/directives/addGraphiteFunc.js

@@ -97,4 +97,4 @@ function (angular, app, _, $, gfunc) {
       };
     });
   }
-});
+});

+ 13 - 6
src/app/directives/grafanaGraph.js

@@ -118,7 +118,7 @@ function (angular, $, kbn, moment, _) {
               lines:  {
                 show: panel.lines,
                 zero: false,
-                fill: panel.fill === 0 ? 0.001 : panel.fill/10,
+                fill: translateFillOption(panel.fill),
                 lineWidth: panel.linewidth,
                 steps: panel.steppedLine
               },
@@ -154,11 +154,12 @@ function (angular, $, kbn, moment, _) {
           };
 
           for (var i = 0; i < data.length; i++) {
-            var _d = data[i].getFlotPairs(panel.nullPointMode, panel.y_formats);
-            data[i].data = _d;
+            var series = data[i];
+            series.applySeriesOverrides(panel.seriesOverrides);
+            series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
           }
 
-          if (panel.bars && data.length && data[0].info.timeStep) {
+          if (data.length && data[0].info.timeStep) {
             options.series.bars.barWidth = data[0].info.timeStep / 1.5;
           }
 
@@ -167,21 +168,27 @@ function (angular, $, kbn, moment, _) {
           addAnnotations(options);
           configureAxisOptions(data, options);
 
+          var sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
+
           // if legend is to the right delay plot draw a few milliseconds
           // so the legend width calculation can be done
           if (shouldDelayDraw(panel)) {
             legendSideLastValue = panel.legend.rightSide;
             setTimeout(function() {
-              plot = $.plot(elem, data, options);
+              plot = $.plot(elem, sortedSeries, options);
               addAxisLabels();
             }, 50);
           }
           else {
-            plot = $.plot(elem, data, options);
+            plot = $.plot(elem, sortedSeries, options);
             addAxisLabels();
           }
         }
 
+        function translateFillOption(fill) {
+          return fill === 0 ? 0.001 : fill/10;
+        }
+
         function shouldDelayDraw(panel) {
           if (panel.legend.rightSide) {
             return true;

+ 2 - 2
src/app/panels/graph/legend.html

@@ -34,12 +34,12 @@
 
     <div class="editor-row small" style="padding-bottom: 0;">
       <label>Axis:</label>
-      <button ng-click="toggleYAxis(series)"
+      <button ng-click="toggleYAxis(series);dismiss();"
               class="btn btn-mini"
               ng-class="{'btn-success': series.yaxis === 1 }">
         Left
       </button>
-      <button ng-click="toggleYAxis(series)"
+      <button ng-click="toggleYAxis(series);dismiss();"
               class="btn btn-mini"
               ng-class="{'btn-success': series.yaxis === 2 }">
         Right

+ 1 - 1
src/app/panels/graph/module.html

@@ -27,7 +27,7 @@
       </div>
     </div>
 
-    <div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-show="editorTabs[editor.index] == tab.title">
+    <div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
       <div ng-include src="tab.src"></div>
     </div>
   </div>

+ 22 - 24
src/app/panels/graph/module.js

@@ -1,16 +1,3 @@
-/** @scratch /panels/5
- * include::panels/histogram.asciidoc[]
- */
-
-/** @scratch /panels/histogram/0
- * == Histogram
- * Status: *Stable*
- *
- * The histogram panel allow for the display of time charts. It includes several modes and tranformations
- * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter
- * fields.
- *
- */
 define([
   'angular',
   'app',
@@ -18,7 +5,8 @@ define([
   'lodash',
   'kbn',
   'moment',
-  './timeSeries',
+  'components/timeSeries',
+  './seriesOverridesCtrl',
   'services/panelSrv',
   'services/annotationsSrv',
   'services/datasourceSrv',
@@ -29,11 +17,10 @@ define([
   'jquery.flot.stack',
   'jquery.flot.stackpercent'
 ],
-function (angular, app, $, _, kbn, moment, timeSeries) {
-
+function (angular, app, $, _, kbn, moment, TimeSeries) {
   'use strict';
 
-  var module = angular.module('grafana.panels.graph', []);
+  var module = angular.module('grafana.panels.graph');
   app.useModule(module);
 
   module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) {
@@ -179,7 +166,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       targets: [{}],
 
       aliasColors: {},
-      aliasYAxis: {},
+
+      seriesOverrides: [],
     };
 
     _.defaults($scope.panel,_d);
@@ -258,18 +246,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       var datapoints = seriesData.datapoints;
       var alias = seriesData.target;
       var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
-      var yaxis = $scope.panel.aliasYAxis[alias] || 1;
 
       var seriesInfo = {
         alias: alias,
         color:  color,
-        enable: true,
-        yaxis: yaxis
       };
 
       $scope.legend.push(seriesInfo);
 
-      var series = new timeSeries.ZeroFilled({
+      var series = new TimeSeries({
         datapoints: datapoints,
         info: seriesInfo,
       });
@@ -347,8 +332,12 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
     };
 
     $scope.toggleYAxis = function(info) {
-      info.yaxis = info.yaxis === 2 ? 1 : 2;
-      $scope.panel.aliasYAxis[info.alias] = info.yaxis;
+      var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias });
+      if (!override) {
+        override = { alias: info.alias };
+        $scope.panel.seriesOverrides.push(override);
+      }
+      override.yaxis = info.yaxis === 2 ? 1 : 2;
       $scope.render();
     };
 
@@ -357,6 +346,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
       $scope.render();
     };
 
+    $scope.addSeriesOverride = function() {
+      $scope.panel.seriesOverrides.push({});
+    };
+
+    $scope.removeSeriesOverride = function(override) {
+      $scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override);
+      $scope.render();
+    };
+
     panelSrv.init($scope);
   });
 

+ 80 - 0
src/app/panels/graph/seriesOverridesCtrl.js

@@ -0,0 +1,80 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+], function(angular, app, _) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.graph', []);
+  app.useModule(module);
+
+  module.controller('SeriesOverridesCtrl', function($scope) {
+    $scope.overrideMenu = [];
+    $scope.currentOverrides = [];
+    $scope.override = $scope.override || {};
+
+    $scope.addOverrideOption = function(name, propertyName, values) {
+      var option = {};
+      option.text = name;
+      option.propertyName = propertyName;
+      option.index = $scope.overrideMenu.length;
+      option.values = values;
+
+      option.submenu = _.map(values, function(value, index) {
+        return {
+          text: String(value),
+          click: 'setOverride(' + option.index + ',' + index + ')'
+        };
+      });
+
+      $scope.overrideMenu.push(option);
+    };
+
+    $scope.setOverride = function(optionIndex, valueIndex) {
+      var option = $scope.overrideMenu[optionIndex];
+      var value = option.values[valueIndex];
+      $scope.override[option.propertyName] = value;
+      $scope.updateCurrentOverrides();
+      $scope.render();
+    };
+
+    $scope.removeOverride = function(option) {
+      delete $scope.override[option.propertyName];
+      $scope.updateCurrentOverrides();
+      $scope.render();
+    };
+
+    $scope.getSeriesNames = function() {
+      return _.map($scope.legend, function(info) {
+        return info.alias;
+      });
+    };
+
+    $scope.updateCurrentOverrides = function() {
+      $scope.currentOverrides = [];
+      _.each($scope.overrideMenu, function(option) {
+        var value = $scope.override[option.propertyName];
+        if (_.isUndefined(value)) { return; }
+        $scope.currentOverrides.push({
+          name: option.text,
+          propertyName: option.propertyName,
+          value: String(value)
+        });
+      });
+    };
+
+    $scope.addOverrideOption('Bars', 'bars', [true, false]);
+    $scope.addOverrideOption('Lines', 'lines', [true, false]);
+    $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
+    $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
+    $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
+    $scope.addOverrideOption('Points', 'points', [true, false]);
+    $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
+    $scope.addOverrideOption('Stack', 'stack', [true, false]);
+    $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
+    $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
+    $scope.updateCurrentOverrides();
+
+  });
+
+});

+ 46 - 2
src/app/panels/graph/styleEditor.html

@@ -1,5 +1,3 @@
-
-
 <div class="editor-row">
   <div class="section">
     <h5>Chart Options</h5>
@@ -64,3 +62,49 @@
     </div>
   </div>
 </div>
+
+<div class="editor-row">
+  <div class="section">
+		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
+		<div>
+		<div class="grafana-target" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
+			<div class="grafana-target-inner-wrapper">
+				<div class="grafana-target-inner">
+
+					<ul class="grafana-target-controls-left">
+						<li class="grafana-target-segment">
+							<i class="icon-remove pointer" ng-click="removeSeriesOverride(override)"></i>
+						</li>
+					</ul>
+
+					<ul class="grafana-segment-list">
+						<li class="grafana-target-segment">
+							alias or regex
+						</li>
+						<li>
+							<input type="text"
+										ng-model="override.alias"
+                    bs-typeahead="getSeriesNames"
+										ng-blur="render()"
+										data-min-length=0 data-items=100
+										class="input-medium grafana-target-segment-input" >
+						</li>
+						<li class="grafana-target-segment" ng-repeat="option in currentOverrides">
+							<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
+							{{option.name}}: {{option.value}}
+						</li>
+						<li class="dropdown">
+							<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'">
+								<i class="icon-plus"></i>
+							</a>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
+				</div>
+			</div>
+		</div>
+		</div>
+
+		<button class="btn btn-success" style="margin-top: 20px" ng-click="addSeriesOverride()">Add series override rule</button>
+	</div>
+</div>

+ 70 - 53
src/app/services/dashboard/dashboardSrv.js

@@ -144,80 +144,97 @@ function (angular, $, kbn, _, moment) {
     };
 
     p.updateSchema = function(old) {
-      var i, j, row, panel;
       var oldVersion = this.version;
-      this.version = 3;
+      var panelUpgrades = [];
+      this.version = 4;
 
-      if (oldVersion === 3) {
+      if (oldVersion === 4) {
         return;
       }
 
-      // Version 3 schema changes
-      // ensure panel ids
-      var maxId = this.getNextPanelId();
-      for (i = 0; i < this.rows.length; i++) {
-        row = this.rows[i];
-        for (j = 0; j < row.panels.length; j++) {
-          panel = row.panels[j];
-          if (!panel.id) {
-            panel.id = maxId;
-            maxId += 1;
-          }
-        }
-      }
+      // version 2 schema changes
+      if (oldVersion < 2) {
 
-      if (oldVersion === 2) {
-        return;
-      }
-
-      // Version 2 schema changes
-      if (old.services) {
-        if (old.services.filter) {
-          this.time = old.services.filter.time;
-          this.templating.list = old.services.filter.list;
+        if (old.services) {
+          if (old.services.filter) {
+            this.time = old.services.filter.time;
+            this.templating.list = old.services.filter.list;
+          }
+          delete this.services;
         }
-        delete this.services;
-      }
 
-      for (i = 0; i < this.rows.length; i++) {
-        row = this.rows[i];
-        for (j = 0; j < row.panels.length; j++) {
-          panel = row.panels[j];
+        panelUpgrades.push(function(panel) {
+          // rename panel type
           if (panel.type === 'graphite') {
             panel.type = 'graph';
           }
 
-          if (panel.type === 'graph') {
-            if (_.isBoolean(panel.legend)) {
-              panel.legend = { show: panel.legend };
-            }
+          if (panel.type !== 'graph') {
+            return;
+          }
 
-            if (panel.grid) {
-              if (panel.grid.min) {
-                panel.grid.leftMin = panel.grid.min;
-                delete panel.grid.min;
-              }
+          if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
 
-              if (panel.grid.max) {
-                panel.grid.leftMax = panel.grid.max;
-                delete panel.grid.max;
-              }
+          if (panel.grid) {
+            if (panel.grid.min) {
+              panel.grid.leftMin = panel.grid.min;
+              delete panel.grid.min;
             }
 
-            if (panel.y_format) {
-              panel.y_formats[0] = panel.y_format;
-              delete panel.y_format;
+            if (panel.grid.max) {
+              panel.grid.leftMax = panel.grid.max;
+              delete panel.grid.max;
             }
+          }
 
-            if (panel.y2_format) {
-              panel.y_formats[1] = panel.y2_format;
-              delete panel.y2_format;
-            }
+          if (panel.y_format) {
+            panel.y_formats[0] = panel.y_format;
+            delete panel.y_format;
           }
-        }
+
+          if (panel.y2_format) {
+            panel.y_formats[1] = panel.y2_format;
+            delete panel.y2_format;
+          }
+        });
+      }
+
+      // schema version 3 changes
+      if (oldVersion < 3) {
+        // ensure panel ids
+        var maxId = this.getNextPanelId();
+        panelUpgrades.push(function(panel) {
+          if (!panel.id) {
+            panel.id = maxId;
+            maxId += 1;
+          }
+        });
+      }
+
+      // schema version 4 changes
+      if (oldVersion < 4) {
+        // move aliasYAxis changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'graph') { return; }
+          _.each(panel.aliasYAxis, function(value, key) {
+            panel.seriesOverrides = [{ alias: key, yaxis: value }];
+          });
+          delete panel.aliasYAxis;
+        });
+      }
+
+      if (panelUpgrades.length === 0) {
+        return;
       }
 
-      this.version = 3;
+      for (var i = 0; i < this.rows.length; i++) {
+        var row = this.rows[i];
+        for (var j = 0; j < row.panels.length; j++) {
+          for (var k = 0; k < panelUpgrades.length; k++) {
+            panelUpgrades[k](row.panels[j]);
+          }
+        }
+      }
     };
 
     return {

+ 1 - 1
src/app/services/panelSrv.js

@@ -104,7 +104,7 @@ function (angular, _) {
 
       // Post init phase
       $scope.fullscreen = false;
-      $scope.editor = { index: 1 };
+      $scope.editor = { index: 3 };
       if ($scope.panelMeta.fullEditorTabs) {
         $scope.editorTabs = _.pluck($scope.panelMeta.fullEditorTabs, 'title');
       }

+ 14 - 0
src/css/less/graph.less

@@ -154,3 +154,17 @@
 .annotation-tags {
   color: @purple;
 }
+
+.graph-series-override {
+  input {
+    float: left;
+    margin-right: 10px;
+  }
+  .graph-series-override-option {
+    float: left;
+    padding: 2px 6px;
+  }
+  .graph-series-override-selector {
+    float: left;
+  }
+}

+ 7 - 1
src/test/specs/dashboardSrv-specs.js

@@ -97,6 +97,7 @@ define([
               {
                 type: 'graphite',
                 legend: true,
+                aliasYAxis: { test: 2 },
                 grid: { min: 1, max: 10 }
               }
             ]
@@ -134,8 +135,13 @@ define([
       expect(graph.grid.leftMax).to.be(10);
     });
 
+    it('move aliasYAxis to series override', function() {
+      expect(graph.seriesOverrides[0].alias).to.be("test");
+      expect(graph.seriesOverrides[0].yaxis).to.be(2);
+    });
+
     it('dashboard schema version should be set to latest', function() {
-      expect(model.version).to.be(3);
+      expect(model.version).to.be(4);
     });
 
   });

+ 107 - 0
src/test/specs/grafanaGraph-specs.js

@@ -0,0 +1,107 @@
+define([
+  './helpers',
+  'angular',
+  'jquery',
+  'components/timeSeries',
+  'directives/grafanaGraph'
+], function(helpers, angular, $, TimeSeries) {
+  'use strict';
+
+  describe('grafanaGraph', function() {
+
+    beforeEach(module('grafana.directives'));
+
+    function graphScenario(desc, func)  {
+      describe(desc, function() {
+        var ctx = {};
+        ctx.setup = function (setupFunc) {
+          beforeEach(inject(function($rootScope, $compile) {
+            var scope = $rootScope.$new();
+            var element = angular.element("<div style='width:500px' grafana-graph><div>");
+
+            scope.height = '200px';
+            scope.panel = {
+              legend: {},
+              grid: {},
+              y_formats: [],
+              seriesOverrides: []
+            };
+            scope.dashboard = { timezone: 'browser' };
+            scope.range = {
+              from: new Date('2014-08-09 10:00:00'),
+              to: new Date('2014-09-09 13:00:00')
+            };
+            ctx.data = [];
+            ctx.data.push(new TimeSeries({
+              datapoints: [[1,1],[2,2]],
+              info: { alias: 'series1', enable: true }
+            }));
+            ctx.data.push(new TimeSeries({
+              datapoints: [[1,1],[2,2]],
+              info: { alias: 'series2', enable: true }
+            }));
+
+            setupFunc(scope, ctx.data);
+
+            $compile(element)(scope);
+            scope.$digest();
+            $.plot = ctx.plotSpy = sinon.spy();
+
+            scope.$emit('render', ctx.data);
+            ctx.plotData = ctx.plotSpy.getCall(0).args[1];
+            ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
+          }));
+        };
+
+        func(ctx);
+      });
+    }
+
+    graphScenario('simple lines options', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.panel.lines = true;
+        scope.panel.fill = 5;
+        scope.panel.linewidth = 3;
+        scope.panel.steppedLine = true;
+      });
+
+      it('should configure plot with correct options', function() {
+        expect(ctx.plotOptions.series.lines.show).to.be(true);
+        expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
+        expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
+        expect(ctx.plotOptions.series.lines.steps).to.be(true);
+      });
+    });
+
+    graphScenario('series option overrides, fill & points', function(ctx) {
+      ctx.setup(function(scope, data) {
+        scope.panel.lines = true;
+        scope.panel.fill = 5;
+        scope.panel.seriesOverrides = [
+          { alias: 'test', fill: 0, points: true }
+        ];
+
+        data[1].info.alias = 'test';
+      });
+
+      it('should match second series and fill zero, and enable points', function() {
+        expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
+        expect(ctx.plotData[1].lines.fill).to.be(0.001);
+        expect(ctx.plotData[1].points.show).to.be(true);
+      });
+    });
+
+    graphScenario('should order series order according to zindex', function(ctx) {
+      ctx.setup(function(scope) {
+        scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
+      });
+
+      it('should move zindex 2 last', function() {
+        expect(ctx.plotData[0].info.alias).to.be('series2');
+        expect(ctx.plotData[1].info.alias).to.be('series1');
+      });
+    });
+
+  });
+});
+

+ 1 - 1
src/test/specs/helpers.js

@@ -69,7 +69,7 @@ define([
       }
       return {
         from : kbn.parseDate(this.time.from),
-             to : kbn.parseDate(this.time.to)
+        to : kbn.parseDate(this.time.to)
       };
     };
 

+ 51 - 0
src/test/specs/seriesOverridesCtrl-specs.js

@@ -0,0 +1,51 @@
+define([
+  './helpers',
+  'panels/graph/seriesOverridesCtrl'
+], function(helpers) {
+  'use strict';
+
+  describe('SeriesOverridesCtrl', function() {
+    var ctx = new helpers.ControllerTestContext();
+
+    beforeEach(module('grafana.services'));
+    beforeEach(module('grafana.panels.graph'));
+
+    beforeEach(ctx.providePhase());
+    beforeEach(ctx.createControllerPhase('SeriesOverridesCtrl'));
+    beforeEach(function() {
+      ctx.scope.render = function() {};
+    });
+
+    describe('Controller should init overrideMenu', function() {
+      it('click should include option and value index', function() {
+        expect(ctx.scope.overrideMenu[1].submenu[1].click).to.be('setOverride(1,1)');
+      });
+    });
+
+    describe('When setting an override', function() {
+      beforeEach(function() {
+        ctx.scope.setOverride(1, 0);
+      });
+
+      it('should set override property', function() {
+        expect(ctx.scope.override.lines).to.be(true);
+      });
+
+      it('should update view model', function() {
+        expect(ctx.scope.currentOverrides[0].name).to.be('Lines');
+        expect(ctx.scope.currentOverrides[0].value).to.be('true');
+      });
+    });
+
+    describe('When removing overide', function() {
+      it('click should include option and value index', function() {
+        ctx.scope.setOverride(1,0);
+        ctx.scope.removeOverride({ propertyName: 'lines' });
+        expect(ctx.scope.currentOverrides.length).to.be(0);
+      });
+    });
+
+  });
+
+});
+

+ 115 - 0
src/test/specs/timeSeries-specs.js

@@ -0,0 +1,115 @@
+define([
+  'components/timeSeries'
+], function(TimeSeries) {
+  'use strict';
+
+  describe("TimeSeries", function() {
+    var points, series;
+    var yAxisFormats = ['short', 'ms'];
+    var testData = {
+      info: { alias: 'test' },
+      datapoints: [
+        [1,2],[null,3],[10,4],[8,5]
+      ]
+    };
+
+    describe('when getting flot pairs', function() {
+      it('with connected style, should ignore nulls', function() {
+        series = new TimeSeries(testData);
+        points = series.getFlotPairs('connected', yAxisFormats);
+        expect(points.length).to.be(3);
+      });
+
+      it('with null as zero style, should replace nulls with zero', function() {
+        series = new TimeSeries(testData);
+        points = series.getFlotPairs('null as zero', yAxisFormats);
+        expect(points.length).to.be(4);
+        expect(points[1][1]).to.be(0);
+      });
+    });
+
+    describe('series overrides', function() {
+      var series;
+      beforeEach(function() {
+        series = new TimeSeries(testData);
+      });
+
+      describe('fill & points', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
+        });
+
+        it('should set fill zero, and enable points', function() {
+          expect(series.lines.fill).to.be(0.001);
+          expect(series.points.show).to.be(true);
+        });
+      });
+
+      describe('series option overrides, bars, true & lines false', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
+        });
+
+        it('should disable lines, and enable bars', function() {
+          expect(series.lines.show).to.be(false);
+          expect(series.bars.show).to.be(true);
+        });
+      });
+
+      describe('series option overrides, linewidth, stack', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
+        });
+
+        it('should disable stack, and set lineWidth', function() {
+          expect(series.stack).to.be(false);
+          expect(series.lines.lineWidth).to.be(5);
+        });
+      });
+
+      describe('series option overrides, pointradius, steppedLine', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
+        });
+
+        it('should set pointradius, and set steppedLine', function() {
+          expect(series.points.radius).to.be(5);
+          expect(series.lines.steps).to.be(true);
+        });
+      });
+
+      describe('override match on regex', function() {
+        beforeEach(function() {
+          series.info.alias = 'test_01';
+          series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
+        });
+
+        it('should match second series', function() {
+          expect(series.lines.show).to.be(false);
+        });
+      });
+
+      describe('override series y-axis, and z-index', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
+        });
+
+        it('should set yaxis', function() {
+          expect(series.info.yaxis).to.be(2);
+        });
+
+        it('should set zindex', function() {
+          expect(series.zindex).to.be(2);
+        });
+      });
+
+    });
+
+  });
+
+});

+ 3 - 0
src/test/test-main.js

@@ -118,10 +118,13 @@ require([
     'specs/lexer-specs',
     'specs/parser-specs',
     'specs/gfunc-specs',
+    'specs/timeSeries-specs',
     'specs/row-ctrl-specs',
     'specs/graphiteTargetCtrl-specs',
     'specs/influxdb-datasource-specs',
     'specs/graph-ctrl-specs',
+    'specs/grafanaGraph-specs',
+    'specs/seriesOverridesCtrl-specs',
     'specs/filterSrv-specs',
     'specs/kbn-format-specs',
     'specs/dashboardSrv-specs',