Browse Source

Karma to Jest: graph (refactor) (#12860)

* Begin conversion

* Test setup started

* Begin rewrite of graph

* Rewrite as class

* Some tests passing

* Fix binding errors

* Half tests passing

* Call buildFlotPairs. More tests passing

* All tests passing

* Remove test test

* Remove Karma test

* Make methods out of event functions

* Rename GraphElement
Tobias Skarhed 7 years ago
parent
commit
739bee0207

+ 682 - 651
public/app/plugins/panel/graph/graph.ts

@@ -21,699 +21,730 @@ import { convertToHistogramData } from './histogram';
 import { alignYLevel } from './align_yaxes';
 import config from 'app/core/config';
 
-/** @ngInject **/
-function graphDirective(timeSrv, popoverSrv, contextSrv) {
-  return {
-    restrict: 'A',
-    template: '',
-    link: function(scope, elem) {
-      var ctrl = scope.ctrl;
-      var dashboard = ctrl.dashboard;
-      var panel = ctrl.panel;
-      var annotations = [];
-      var data;
-      var plot;
-      var sortedSeries;
-      var panelWidth = 0;
-      var eventManager = new EventManager(ctrl);
-      var thresholdManager = new ThresholdManager(ctrl);
-      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
-        return sortedSeries;
-      });
-
-      // panel events
-      ctrl.events.on('panel-teardown', () => {
-        thresholdManager = null;
-
-        if (plot) {
-          plot.destroy();
-          plot = null;
-        }
-      });
-
-      /**
-       * Split graph rendering into two parts.
-       * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
-       * (see ctrl.events.on('render') in legend.ts).
-       * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
-       */
-      ctrl.events.on('render', renderData => {
-        data = renderData || data;
-        if (!data) {
-          return;
-        }
-        annotations = ctrl.annotations || [];
-        buildFlotPairs(data);
-        const graphHeight = elem.height();
-        updateLegendValues(data, panel, graphHeight);
-
-        ctrl.events.emit('render-legend');
-      });
-
-      ctrl.events.on('legend-rendering-complete', () => {
-        render_panel();
+import { GraphCtrl } from './module';
+
+class GraphElement {
+  ctrl: GraphCtrl;
+  tooltip: any;
+  dashboard: any;
+  annotations: Array<object>;
+  panel: any;
+  plot: any;
+  sortedSeries: Array<any>;
+  data: Array<any>;
+  panelWidth: number;
+  eventManager: EventManager;
+  thresholdManager: ThresholdManager;
+
+  constructor(private scope, private elem, private timeSrv) {
+    this.ctrl = scope.ctrl;
+    this.dashboard = this.ctrl.dashboard;
+    this.panel = this.ctrl.panel;
+    this.annotations = [];
+
+    this.panelWidth = 0;
+    this.eventManager = new EventManager(this.ctrl);
+    this.thresholdManager = new ThresholdManager(this.ctrl);
+    this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
+      return this.sortedSeries;
+    });
+
+    // panel events
+    this.ctrl.events.on('panel-teardown', this.onPanelteardown.bind(this));
+
+    /**
+     * Split graph rendering into two parts.
+     * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
+     * (see ctrl.events.on('render') in legend.ts).
+     * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
+     */
+    this.ctrl.events.on('render', this.onRender.bind(this));
+    this.ctrl.events.on('legend-rendering-complete', this.onLegendRenderingComplete.bind(this));
+
+    // global events
+    appEvents.on('graph-hover', this.onGraphHover.bind(this), scope);
+
+    appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope);
+
+    this.elem.bind('plotselected', this.onPlotSelected.bind(this));
+
+    this.elem.bind('plotclick', this.onPlotClick.bind(this));
+    scope.$on('$destroy', this.onScopeDestroy.bind(this));
+  }
+
+  onRender(renderData) {
+    this.data = renderData || this.data;
+    if (!this.data) {
+      return;
+    }
+    this.annotations = this.ctrl.annotations || [];
+    this.buildFlotPairs(this.data);
+    const graphHeight = this.elem.height();
+    updateLegendValues(this.data, this.panel, graphHeight);
+
+    this.ctrl.events.emit('render-legend');
+  }
+
+  onGraphHover(evt) {
+    // ignore other graph hover events if shared tooltip is disabled
+    if (!this.dashboard.sharedTooltipModeEnabled()) {
+      return;
+    }
+
+    // ignore if we are the emitter
+    if (!this.plot || evt.panel.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) {
+      return;
+    }
+
+    this.tooltip.show(evt.pos);
+  }
+
+  onPanelteardown() {
+    this.thresholdManager = null;
+
+    if (this.plot) {
+      this.plot.destroy();
+      this.plot = null;
+    }
+  }
+
+  onLegendRenderingComplete() {
+    this.render_panel();
+  }
+
+  onGraphHoverClear(event, info) {
+    if (this.plot) {
+      this.tooltip.clear(this.plot);
+    }
+  }
+
+  onPlotSelected(event, ranges) {
+    if (this.panel.xaxis.mode !== 'time') {
+      // Skip if panel in histogram or series mode
+      this.plot.clearSelection();
+      return;
+    }
+
+    if ((ranges.ctrlKey || ranges.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) {
+      // Add annotation
+      setTimeout(() => {
+        this.eventManager.updateTime(ranges.xaxis);
+      }, 100);
+    } else {
+      this.scope.$apply(() => {
+        this.timeSrv.setTime({
+          from: moment.utc(ranges.xaxis.from),
+          to: moment.utc(ranges.xaxis.to),
+        });
       });
-
-      // global events
-      appEvents.on(
-        'graph-hover',
-        evt => {
-          // ignore other graph hover events if shared tooltip is disabled
-          if (!dashboard.sharedTooltipModeEnabled()) {
-            return;
-          }
-
-          // ignore if we are the emitter
-          if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) {
-            return;
-          }
-
-          tooltip.show(evt.pos);
-        },
-        scope
-      );
-
-      appEvents.on(
-        'graph-hover-clear',
-        (event, info) => {
-          if (plot) {
-            tooltip.clear(plot);
-          }
-        },
-        scope
-      );
-
-      function shouldAbortRender() {
-        if (!data) {
-          return true;
-        }
-
-        if (panelWidth === 0) {
-          return true;
-        }
-
-        return false;
+    }
+  }
+
+  onPlotClick(event, pos, item) {
+    if (this.panel.xaxis.mode !== 'time') {
+      // Skip if panel in histogram or series mode
+      return;
+    }
+
+    if ((pos.ctrlKey || pos.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) {
+      // Skip if range selected (added in "plotselected" event handler)
+      let isRangeSelection = pos.x !== pos.x1;
+      if (!isRangeSelection) {
+        setTimeout(() => {
+          this.eventManager.updateTime({ from: pos.x, to: null });
+        }, 100);
       }
-
-      function drawHook(plot) {
-        // add left axis labels
-        if (panel.yaxes[0].label && panel.yaxes[0].show) {
-          $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
-            .text(panel.yaxes[0].label)
-            .appendTo(elem);
-        }
-
-        // add right axis labels
-        if (panel.yaxes[1].label && panel.yaxes[1].show) {
-          $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
-            .text(panel.yaxes[1].label)
-            .appendTo(elem);
-        }
-
-        if (ctrl.dataWarning) {
-          $(`<div class="datapoints-warning flot-temp-elem">${ctrl.dataWarning.title}</div>`).appendTo(elem);
-        }
-
-        thresholdManager.draw(plot);
+    }
+  }
+
+  onScopeDestroy() {
+    this.tooltip.destroy();
+    this.elem.off();
+    this.elem.remove();
+  }
+
+  shouldAbortRender() {
+    if (!this.data) {
+      return true;
+    }
+
+    if (this.panelWidth === 0) {
+      return true;
+    }
+
+    return false;
+  }
+
+  drawHook(plot) {
+    // add left axis labels
+    if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) {
+      $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
+        .text(this.panel.yaxes[0].label)
+        .appendTo(this.elem);
+    }
+
+    // add right axis labels
+    if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) {
+      $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
+        .text(this.panel.yaxes[1].label)
+        .appendTo(this.elem);
+    }
+
+    if (this.ctrl.dataWarning) {
+      $(`<div class="datapoints-warning flot-temp-elem">${this.ctrl.dataWarning.title}</div>`).appendTo(this.elem);
+    }
+
+    this.thresholdManager.draw(plot);
+  }
+
+  processOffsetHook(plot, gridMargin) {
+    var left = this.panel.yaxes[0];
+    var right = this.panel.yaxes[1];
+    if (left.show && left.label) {
+      gridMargin.left = 20;
+    }
+    if (right.show && right.label) {
+      gridMargin.right = 20;
+    }
+
+    // apply y-axis min/max options
+    var yaxis = plot.getYAxes();
+    for (var i = 0; i < yaxis.length; i++) {
+      var axis = yaxis[i];
+      var panelOptions = this.panel.yaxes[i];
+      axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
+      axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
+    }
+  }
+
+  processRangeHook(plot) {
+    var yAxes = plot.getYAxes();
+    const align = this.panel.yaxis.align || false;
+
+    if (yAxes.length > 1 && align === true) {
+      const level = this.panel.yaxis.alignLevel || 0;
+      alignYLevel(yAxes, parseFloat(level));
+    }
+  }
+
+  // Series could have different timeSteps,
+  // let's find the smallest one so that bars are correctly rendered.
+  // In addition, only take series which are rendered as bars for this.
+  getMinTimeStepOfSeries(data) {
+    var min = Number.MAX_VALUE;
+
+    for (let i = 0; i < data.length; i++) {
+      if (!data[i].stats.timeStep) {
+        continue;
       }
-
-      function processOffsetHook(plot, gridMargin) {
-        var left = panel.yaxes[0];
-        var right = panel.yaxes[1];
-        if (left.show && left.label) {
-          gridMargin.left = 20;
-        }
-        if (right.show && right.label) {
-          gridMargin.right = 20;
+      if (this.panel.bars) {
+        if (data[i].bars && data[i].bars.show === false) {
+          continue;
         }
-
-        // apply y-axis min/max options
-        var yaxis = plot.getYAxes();
-        for (var i = 0; i < yaxis.length; i++) {
-          var axis = yaxis[i];
-          var panelOptions = panel.yaxes[i];
-          axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
-          axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
-        }
-      }
-
-      function processRangeHook(plot) {
-        var yAxes = plot.getYAxes();
-        const align = panel.yaxis.align || false;
-
-        if (yAxes.length > 1 && align === true) {
-          const level = panel.yaxis.alignLevel || 0;
-          alignYLevel(yAxes, parseFloat(level));
+      } else {
+        if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
+          continue;
         }
       }
 
-      // Series could have different timeSteps,
-      // let's find the smallest one so that bars are correctly rendered.
-      // In addition, only take series which are rendered as bars for this.
-      function getMinTimeStepOfSeries(data) {
-        var min = Number.MAX_VALUE;
-
-        for (let i = 0; i < data.length; i++) {
-          if (!data[i].stats.timeStep) {
-            continue;
-          }
-          if (panel.bars) {
-            if (data[i].bars && data[i].bars.show === false) {
-              continue;
-            }
-          } else {
-            if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
-              continue;
-            }
-          }
-
-          if (data[i].stats.timeStep < min) {
-            min = data[i].stats.timeStep;
-          }
-        }
-
-        return min;
+      if (data[i].stats.timeStep < min) {
+        min = data[i].stats.timeStep;
       }
-
-      // Function for rendering panel
-      function render_panel() {
-        panelWidth = elem.width();
-        if (shouldAbortRender()) {
-          return;
-        }
-
-        // give space to alert editing
-        thresholdManager.prepare(elem, data);
-
-        // un-check dashes if lines are unchecked
-        panel.dashes = panel.lines ? panel.dashes : false;
-
-        // Populate element
-        let options: any = buildFlotOptions(panel);
-        prepareXAxis(options, panel);
-        configureYAxisOptions(data, options);
-        thresholdManager.addFlotOptions(options, panel);
-        eventManager.addFlotEvents(annotations, options);
-
-        sortedSeries = sortSeries(data, panel);
-        callPlot(options, true);
+    }
+
+    return min;
+  }
+
+  // Function for rendering panel
+  render_panel() {
+    this.panelWidth = this.elem.width();
+    if (this.shouldAbortRender()) {
+      return;
+    }
+
+    // give space to alert editing
+    this.thresholdManager.prepare(this.elem, this.data);
+
+    // un-check dashes if lines are unchecked
+    this.panel.dashes = this.panel.lines ? this.panel.dashes : false;
+
+    // Populate element
+    let options: any = this.buildFlotOptions(this.panel);
+    this.prepareXAxis(options, this.panel);
+    this.configureYAxisOptions(this.data, options);
+    this.thresholdManager.addFlotOptions(options, this.panel);
+    this.eventManager.addFlotEvents(this.annotations, options);
+
+    this.sortedSeries = this.sortSeries(this.data, this.panel);
+    this.callPlot(options, true);
+  }
+
+  buildFlotPairs(data) {
+    for (let i = 0; i < data.length; i++) {
+      let series = data[i];
+      series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode);
+
+      // if hidden remove points and disable stack
+      if (this.ctrl.hiddenSeries[series.alias]) {
+        series.data = [];
+        series.stack = false;
       }
+    }
+  }
 
-      function buildFlotPairs(data) {
-        for (let i = 0; i < data.length; i++) {
-          let series = data[i];
-          series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
+  prepareXAxis(options, panel) {
+    switch (panel.xaxis.mode) {
+      case 'series': {
+        options.series.bars.barWidth = 0.7;
+        options.series.bars.align = 'center';
 
-          // if hidden remove points and disable stack
-          if (ctrl.hiddenSeries[series.alias]) {
-            series.data = [];
-            series.stack = false;
-          }
+        for (let i = 0; i < this.data.length; i++) {
+          let series = this.data[i];
+          series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
         }
-      }
 
-      function prepareXAxis(options, panel) {
-        switch (panel.xaxis.mode) {
-          case 'series': {
-            options.series.bars.barWidth = 0.7;
-            options.series.bars.align = 'center';
-
-            for (let i = 0; i < data.length; i++) {
-              let series = data[i];
-              series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
-            }
-
-            addXSeriesAxis(options);
-            break;
-          }
-          case 'histogram': {
-            let bucketSize: number;
-
-            if (data.length) {
-              let histMin = _.min(_.map(data, s => s.stats.min));
-              let histMax = _.max(_.map(data, s => s.stats.max));
-              let ticks = panel.xaxis.buckets || panelWidth / 50;
-              bucketSize = tickStep(histMin, histMax, ticks);
-              options.series.bars.barWidth = bucketSize * 0.8;
-              data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, histMin, histMax);
-            } else {
-              bucketSize = 0;
-            }
-
-            addXHistogramAxis(options, bucketSize);
-            break;
-          }
-          case 'table': {
-            options.series.bars.barWidth = 0.7;
-            options.series.bars.align = 'center';
-            addXTableAxis(options);
-            break;
-          }
-          default: {
-            options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5;
-            addTimeAxis(options);
-            break;
-          }
-        }
+        this.addXSeriesAxis(options);
+        break;
       }
-
-      function callPlot(options, incrementRenderCounter) {
-        try {
-          plot = $.plot(elem, sortedSeries, options);
-          if (ctrl.renderError) {
-            delete ctrl.error;
-            delete ctrl.inspector;
-          }
-        } catch (e) {
-          console.log('flotcharts error', e);
-          ctrl.error = e.message || 'Render Error';
-          ctrl.renderError = true;
-          ctrl.inspector = { error: e };
+      case 'histogram': {
+        let bucketSize: number;
+
+        if (this.data.length) {
+          let histMin = _.min(_.map(this.data, s => s.stats.min));
+          let histMax = _.max(_.map(this.data, s => s.stats.max));
+          let ticks = panel.xaxis.buckets || this.panelWidth / 50;
+          bucketSize = tickStep(histMin, histMax, ticks);
+          options.series.bars.barWidth = bucketSize * 0.8;
+          this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
+        } else {
+          bucketSize = 0;
         }
 
-        if (incrementRenderCounter) {
-          ctrl.renderingCompleted();
-        }
+        this.addXHistogramAxis(options, bucketSize);
+        break;
       }
-
-      function buildFlotOptions(panel) {
-        let gridColor = '#c8c8c8';
-        if (config.bootData.user.lightTheme === true) {
-          gridColor = '#a1a1a1';
-        }
-        const stack = panel.stack ? true : null;
-        let options = {
-          hooks: {
-            draw: [drawHook],
-            processOffset: [processOffsetHook],
-            processRange: [processRangeHook],
-          },
-          legend: { show: false },
-          series: {
-            stackpercent: panel.stack ? panel.percentage : false,
-            stack: panel.percentage ? null : stack,
-            lines: {
-              show: panel.lines,
-              zero: false,
-              fill: translateFillOption(panel.fill),
-              lineWidth: panel.dashes ? 0 : panel.linewidth,
-              steps: panel.steppedLine,
-            },
-            dashes: {
-              show: panel.dashes,
-              lineWidth: panel.linewidth,
-              dashLength: [panel.dashLength, panel.spaceLength],
-            },
-            bars: {
-              show: panel.bars,
-              fill: 1,
-              barWidth: 1,
-              zero: false,
-              lineWidth: 0,
-            },
-            points: {
-              show: panel.points,
-              fill: 1,
-              fillColor: false,
-              radius: panel.points ? panel.pointradius : 2,
-            },
-            shadowSize: 0,
-          },
-          yaxes: [],
-          xaxis: {},
-          grid: {
-            minBorderMargin: 0,
-            markings: [],
-            backgroundColor: null,
-            borderWidth: 0,
-            hoverable: true,
-            clickable: true,
-            color: gridColor,
-            margin: { left: 0, right: 0 },
-            labelMarginX: 0,
-          },
-          selection: {
-            mode: 'x',
-            color: '#666',
-          },
-          crosshair: {
-            mode: 'x',
-          },
-        };
-        return options;
+      case 'table': {
+        options.series.bars.barWidth = 0.7;
+        options.series.bars.align = 'center';
+        this.addXTableAxis(options);
+        break;
       }
-
-      function sortSeries(series, panel) {
-        var sortBy = panel.legend.sort;
-        var sortOrder = panel.legend.sortDesc;
-        var haveSortBy = sortBy !== null && sortBy !== undefined;
-        var haveSortOrder = sortOrder !== null && sortOrder !== undefined;
-        var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
-        var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
-
-        if (shouldSortBy) {
-          return _.sortBy(series, s => s.stats[sortBy] * sortDesc);
-        } else {
-          return _.sortBy(series, s => s.zindex);
-        }
+      default: {
+        options.series.bars.barWidth = this.getMinTimeStepOfSeries(this.data) / 1.5;
+        this.addTimeAxis(options);
+        break;
       }
-
-      function translateFillOption(fill) {
-        if (panel.percentage && panel.stack) {
-          return fill === 0 ? 0.001 : fill / 10;
-        } else {
-          return fill / 10;
+    }
+  }
+
+  callPlot(options, incrementRenderCounter) {
+    try {
+      this.plot = $.plot(this.elem, this.sortedSeries, options);
+      if (this.ctrl.renderError) {
+        delete this.ctrl.error;
+        delete this.ctrl.inspector;
+      }
+    } catch (e) {
+      console.log('flotcharts error', e);
+      this.ctrl.error = e.message || 'Render Error';
+      this.ctrl.renderError = true;
+      this.ctrl.inspector = { error: e };
+    }
+
+    if (incrementRenderCounter) {
+      this.ctrl.renderingCompleted();
+    }
+  }
+
+  buildFlotOptions(panel) {
+    let gridColor = '#c8c8c8';
+    if (config.bootData.user.lightTheme === true) {
+      gridColor = '#a1a1a1';
+    }
+    const stack = panel.stack ? true : null;
+    let options = {
+      hooks: {
+        draw: [this.drawHook.bind(this)],
+        processOffset: [this.processOffsetHook.bind(this)],
+        processRange: [this.processRangeHook.bind(this)],
+      },
+      legend: { show: false },
+      series: {
+        stackpercent: panel.stack ? panel.percentage : false,
+        stack: panel.percentage ? null : stack,
+        lines: {
+          show: panel.lines,
+          zero: false,
+          fill: this.translateFillOption(panel.fill),
+          lineWidth: panel.dashes ? 0 : panel.linewidth,
+          steps: panel.steppedLine,
+        },
+        dashes: {
+          show: panel.dashes,
+          lineWidth: panel.linewidth,
+          dashLength: [panel.dashLength, panel.spaceLength],
+        },
+        bars: {
+          show: panel.bars,
+          fill: 1,
+          barWidth: 1,
+          zero: false,
+          lineWidth: 0,
+        },
+        points: {
+          show: panel.points,
+          fill: 1,
+          fillColor: false,
+          radius: panel.points ? panel.pointradius : 2,
+        },
+        shadowSize: 0,
+      },
+      yaxes: [],
+      xaxis: {},
+      grid: {
+        minBorderMargin: 0,
+        markings: [],
+        backgroundColor: null,
+        borderWidth: 0,
+        hoverable: true,
+        clickable: true,
+        color: gridColor,
+        margin: { left: 0, right: 0 },
+        labelMarginX: 0,
+      },
+      selection: {
+        mode: 'x',
+        color: '#666',
+      },
+      crosshair: {
+        mode: 'x',
+      },
+    };
+    return options;
+  }
+
+  sortSeries(series, panel) {
+    var sortBy = panel.legend.sort;
+    var sortOrder = panel.legend.sortDesc;
+    var haveSortBy = sortBy !== null && sortBy !== undefined;
+    var haveSortOrder = sortOrder !== null && sortOrder !== undefined;
+    var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
+    var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
+
+    if (shouldSortBy) {
+      return _.sortBy(series, s => s.stats[sortBy] * sortDesc);
+    } else {
+      return _.sortBy(series, s => s.zindex);
+    }
+  }
+
+  translateFillOption(fill) {
+    if (this.panel.percentage && this.panel.stack) {
+      return fill === 0 ? 0.001 : fill / 10;
+    } else {
+      return fill / 10;
+    }
+  }
+
+  addTimeAxis(options) {
+    var ticks = this.panelWidth / 100;
+    var min = _.isUndefined(this.ctrl.range.from) ? null : this.ctrl.range.from.valueOf();
+    var max = _.isUndefined(this.ctrl.range.to) ? null : this.ctrl.range.to.valueOf();
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: 'time',
+      min: min,
+      max: max,
+      label: 'Datetime',
+      ticks: ticks,
+      timeformat: this.time_format(ticks, min, max),
+    };
+  }
+
+  addXSeriesAxis(options) {
+    var ticks = _.map(this.data, function(series, index) {
+      return [index + 1, series.alias];
+    });
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: 0,
+      max: ticks.length + 1,
+      label: 'Datetime',
+      ticks: ticks,
+    };
+  }
+
+  addXHistogramAxis(options, bucketSize) {
+    let ticks, min, max;
+    let defaultTicks = this.panelWidth / 50;
+
+    if (this.data.length && bucketSize) {
+      let tick_values = [];
+      for (let d of this.data) {
+        for (let point of d.data) {
+          tick_values[point[0]] = true;
         }
       }
-
-      function addTimeAxis(options) {
-        var ticks = panelWidth / 100;
-        var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
-        var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: 'time',
-          min: min,
-          max: max,
-          label: 'Datetime',
-          ticks: ticks,
-          timeformat: time_format(ticks, min, max),
-        };
+      ticks = Object.keys(tick_values).map(v => Number(v));
+      min = _.min(ticks);
+      max = _.max(ticks);
+
+      // Adjust tick step
+      let tickStep = bucketSize;
+      let ticks_num = Math.floor((max - min) / tickStep);
+      while (ticks_num > defaultTicks) {
+        tickStep = tickStep * 2;
+        ticks_num = Math.ceil((max - min) / tickStep);
       }
 
-      function addXSeriesAxis(options) {
-        var ticks = _.map(data, function(series, index) {
-          return [index + 1, series.alias];
-        });
+      // Expand ticks for pretty view
+      min = Math.floor(min / tickStep) * tickStep;
+      // 1.01 is 101% - ensure we have enough space for last bar
+      max = Math.ceil(max * 1.01 / tickStep) * tickStep;
 
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: 0,
-          max: ticks.length + 1,
-          label: 'Datetime',
-          ticks: ticks,
-        };
+      ticks = [];
+      for (let i = min; i <= max; i += tickStep) {
+        ticks.push(i);
       }
-
-      function addXHistogramAxis(options, bucketSize) {
-        let ticks, min, max;
-        let defaultTicks = panelWidth / 50;
-
-        if (data.length && bucketSize) {
-          let tick_values = [];
-          for (let d of data) {
-            for (let point of d.data) {
-              tick_values[point[0]] = true;
-            }
-          }
-          ticks = Object.keys(tick_values).map(v => Number(v));
-          min = _.min(ticks);
-          max = _.max(ticks);
-
-          // Adjust tick step
-          let tickStep = bucketSize;
-          let ticks_num = Math.floor((max - min) / tickStep);
-          while (ticks_num > defaultTicks) {
-            tickStep = tickStep * 2;
-            ticks_num = Math.ceil((max - min) / tickStep);
-          }
-
-          // Expand ticks for pretty view
-          min = Math.floor(min / tickStep) * tickStep;
-          // 1.01 is 101% - ensure we have enough space for last bar
-          max = Math.ceil(max * 1.01 / tickStep) * tickStep;
-
-          ticks = [];
-          for (let i = min; i <= max; i += tickStep) {
-            ticks.push(i);
-          }
-        } else {
-          // Set defaults if no data
-          ticks = defaultTicks / 2;
-          min = 0;
-          max = 1;
+    } else {
+      // Set defaults if no data
+      ticks = defaultTicks / 2;
+      min = 0;
+      max = 1;
+    }
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: min,
+      max: max,
+      label: 'Histogram',
+      ticks: ticks,
+    };
+
+    // Use 'short' format for histogram values
+    this.configureAxisMode(options.xaxis, 'short');
+  }
+
+  addXTableAxis(options) {
+    var ticks = _.map(this.data, function(series, seriesIndex) {
+      return _.map(series.datapoints, function(point, pointIndex) {
+        var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
+        return [tickIndex + 1, point[1]];
+      });
+    });
+    ticks = _.flatten(ticks, true);
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: 0,
+      max: ticks.length + 1,
+      label: 'Datetime',
+      ticks: ticks,
+    };
+  }
+
+  configureYAxisOptions(data, options) {
+    var defaults = {
+      position: 'left',
+      show: this.panel.yaxes[0].show,
+      index: 1,
+      logBase: this.panel.yaxes[0].logBase || 1,
+      min: this.parseNumber(this.panel.yaxes[0].min),
+      max: this.parseNumber(this.panel.yaxes[0].max),
+      tickDecimals: this.panel.yaxes[0].decimals,
+    };
+
+    options.yaxes.push(defaults);
+
+    if (_.find(data, { yaxis: 2 })) {
+      var secondY = _.clone(defaults);
+      secondY.index = 2;
+      secondY.show = this.panel.yaxes[1].show;
+      secondY.logBase = this.panel.yaxes[1].logBase || 1;
+      secondY.position = 'right';
+      secondY.min = this.parseNumber(this.panel.yaxes[1].min);
+      secondY.max = this.parseNumber(this.panel.yaxes[1].max);
+      secondY.tickDecimals = this.panel.yaxes[1].decimals;
+      options.yaxes.push(secondY);
+
+      this.applyLogScale(options.yaxes[1], data);
+      this.configureAxisMode(
+        options.yaxes[1],
+        this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format
+      );
+    }
+    this.applyLogScale(options.yaxes[0], data);
+    this.configureAxisMode(
+      options.yaxes[0],
+      this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format
+    );
+  }
+
+  parseNumber(value: any) {
+    if (value === null || typeof value === 'undefined') {
+      return null;
+    }
+
+    return _.toNumber(value);
+  }
+
+  applyLogScale(axis, data) {
+    if (axis.logBase === 1) {
+      return;
+    }
+
+    const minSetToZero = axis.min === 0;
+
+    if (axis.min < Number.MIN_VALUE) {
+      axis.min = null;
+    }
+    if (axis.max < Number.MIN_VALUE) {
+      axis.max = null;
+    }
+
+    var series, i;
+    var max = axis.max,
+      min = axis.min;
+
+    for (i = 0; i < data.length; i++) {
+      series = data[i];
+      if (series.yaxis === axis.index) {
+        if (!max || max < series.stats.max) {
+          max = series.stats.max;
+        }
+        if (!min || min > series.stats.logmin) {
+          min = series.stats.logmin;
         }
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: min,
-          max: max,
-          label: 'Histogram',
-          ticks: ticks,
-        };
-
-        // Use 'short' format for histogram values
-        configureAxisMode(options.xaxis, 'short');
       }
-
-      function addXTableAxis(options) {
-        var ticks = _.map(data, function(series, seriesIndex) {
-          return _.map(series.datapoints, function(point, pointIndex) {
-            var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
-            return [tickIndex + 1, point[1]];
-          });
-        });
-        ticks = _.flatten(ticks, true);
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: 0,
-          max: ticks.length + 1,
-          label: 'Datetime',
-          ticks: ticks,
-        };
+    }
+
+    axis.transform = function(v) {
+      return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
+    };
+    axis.inverseTransform = function(v) {
+      return Math.pow(axis.logBase, v);
+    };
+
+    if (!max && !min) {
+      max = axis.inverseTransform(+2);
+      min = axis.inverseTransform(-2);
+    } else if (!max) {
+      max = min * axis.inverseTransform(+4);
+    } else if (!min) {
+      min = max * axis.inverseTransform(-4);
+    }
+
+    if (axis.min) {
+      min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
+    } else {
+      min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
+    }
+    if (axis.max) {
+      max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
+    } else {
+      max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
+    }
+
+    if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
+      return;
+    }
+
+    if (Number.isFinite(min) && Number.isFinite(max)) {
+      if (minSetToZero) {
+        axis.min = 0.1;
+        min = 1;
       }
 
-      function configureYAxisOptions(data, options) {
-        var defaults = {
-          position: 'left',
-          show: panel.yaxes[0].show,
-          index: 1,
-          logBase: panel.yaxes[0].logBase || 1,
-          min: parseNumber(panel.yaxes[0].min),
-          max: parseNumber(panel.yaxes[0].max),
-          tickDecimals: panel.yaxes[0].decimals,
-        };
-
-        options.yaxes.push(defaults);
-
-        if (_.find(data, { yaxis: 2 })) {
-          var secondY = _.clone(defaults);
-          secondY.index = 2;
-          secondY.show = panel.yaxes[1].show;
-          secondY.logBase = panel.yaxes[1].logBase || 1;
-          secondY.position = 'right';
-          secondY.min = parseNumber(panel.yaxes[1].min);
-          secondY.max = parseNumber(panel.yaxes[1].max);
-          secondY.tickDecimals = panel.yaxes[1].decimals;
-          options.yaxes.push(secondY);
-
-          applyLogScale(options.yaxes[1], data);
-          configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? 'percent' : panel.yaxes[1].format);
-        }
-        applyLogScale(options.yaxes[0], data);
-        configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? 'percent' : panel.yaxes[0].format);
+      axis.ticks = this.generateTicksForLogScaleYAxis(min, max, axis.logBase);
+      if (minSetToZero) {
+        axis.ticks.unshift(0.1);
       }
-
-      function parseNumber(value: any) {
-        if (value === null || typeof value === 'undefined') {
-          return null;
-        }
-
-        return _.toNumber(value);
+      if (axis.ticks[axis.ticks.length - 1] > axis.max) {
+        axis.max = axis.ticks[axis.ticks.length - 1];
       }
-
-      function applyLogScale(axis, data) {
-        if (axis.logBase === 1) {
-          return;
-        }
-
-        const minSetToZero = axis.min === 0;
-
-        if (axis.min < Number.MIN_VALUE) {
-          axis.min = null;
-        }
-        if (axis.max < Number.MIN_VALUE) {
-          axis.max = null;
-        }
-
-        var series, i;
-        var max = axis.max,
-          min = axis.min;
-
-        for (i = 0; i < data.length; i++) {
-          series = data[i];
-          if (series.yaxis === axis.index) {
-            if (!max || max < series.stats.max) {
-              max = series.stats.max;
-            }
-            if (!min || min > series.stats.logmin) {
-              min = series.stats.logmin;
-            }
-          }
-        }
-
-        axis.transform = function(v) {
-          return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
-        };
-        axis.inverseTransform = function(v) {
-          return Math.pow(axis.logBase, v);
-        };
-
-        if (!max && !min) {
-          max = axis.inverseTransform(+2);
-          min = axis.inverseTransform(-2);
-        } else if (!max) {
-          max = min * axis.inverseTransform(+4);
-        } else if (!min) {
-          min = max * axis.inverseTransform(-4);
-        }
-
-        if (axis.min) {
-          min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
-        } else {
-          min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
-        }
-        if (axis.max) {
-          max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
-        } else {
-          max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
-        }
-
-        if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
-          return;
-        }
-
-        if (Number.isFinite(min) && Number.isFinite(max)) {
-          if (minSetToZero) {
-            axis.min = 0.1;
-            min = 1;
-          }
-
-          axis.ticks = generateTicksForLogScaleYAxis(min, max, axis.logBase);
-          if (minSetToZero) {
-            axis.ticks.unshift(0.1);
-          }
-          if (axis.ticks[axis.ticks.length - 1] > axis.max) {
-            axis.max = axis.ticks[axis.ticks.length - 1];
-          }
-        } else {
-          axis.ticks = [1, 2];
-          delete axis.min;
-          delete axis.max;
-        }
+    } else {
+      axis.ticks = [1, 2];
+      delete axis.min;
+      delete axis.max;
+    }
+  }
+
+  generateTicksForLogScaleYAxis(min, max, logBase) {
+    let ticks = [];
+
+    var nextTick;
+    for (nextTick = min; nextTick <= max; nextTick *= logBase) {
+      ticks.push(nextTick);
+    }
+
+    const maxNumTicks = Math.ceil(this.ctrl.height / 25);
+    const numTicks = ticks.length;
+    if (numTicks > maxNumTicks) {
+      const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
+      ticks = [];
+
+      for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
+        ticks.push(nextTick);
       }
+    }
 
-      function generateTicksForLogScaleYAxis(min, max, logBase) {
-        let ticks = [];
-
-        var nextTick;
-        for (nextTick = min; nextTick <= max; nextTick *= logBase) {
-          ticks.push(nextTick);
-        }
-
-        const maxNumTicks = Math.ceil(ctrl.height / 25);
-        const numTicks = ticks.length;
-        if (numTicks > maxNumTicks) {
-          const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
-          ticks = [];
-
-          for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
-            ticks.push(nextTick);
-          }
-        }
+    return ticks;
+  }
 
-        return ticks;
+  configureAxisMode(axis, format) {
+    axis.tickFormatter = function(val, axis) {
+      if (!kbn.valueFormats[format]) {
+        throw new Error(`Unit '${format}' is not supported`);
       }
-
-      function configureAxisMode(axis, format) {
-        axis.tickFormatter = function(val, axis) {
-          if (!kbn.valueFormats[format]) {
-            throw new Error(`Unit '${format}' is not supported`);
-          }
-          return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
-        };
+      return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
+    };
+  }
+
+  time_format(ticks, min, max) {
+    if (min && max && ticks) {
+      var range = max - min;
+      var secPerTick = range / ticks / 1000;
+      var oneDay = 86400000;
+      var oneYear = 31536000000;
+
+      if (secPerTick <= 45) {
+        return '%H:%M:%S';
       }
-
-      function time_format(ticks, min, max) {
-        if (min && max && ticks) {
-          var range = max - min;
-          var secPerTick = range / ticks / 1000;
-          var oneDay = 86400000;
-          var oneYear = 31536000000;
-
-          if (secPerTick <= 45) {
-            return '%H:%M:%S';
-          }
-          if (secPerTick <= 7200 || range <= oneDay) {
-            return '%H:%M';
-          }
-          if (secPerTick <= 80000) {
-            return '%m/%d %H:%M';
-          }
-          if (secPerTick <= 2419200 || range <= oneYear) {
-            return '%m/%d';
-          }
-          return '%Y-%m';
-        }
-
+      if (secPerTick <= 7200 || range <= oneDay) {
         return '%H:%M';
       }
+      if (secPerTick <= 80000) {
+        return '%m/%d %H:%M';
+      }
+      if (secPerTick <= 2419200 || range <= oneYear) {
+        return '%m/%d';
+      }
+      return '%Y-%m';
+    }
 
-      elem.bind('plotselected', function(event, ranges) {
-        if (panel.xaxis.mode !== 'time') {
-          // Skip if panel in histogram or series mode
-          plot.clearSelection();
-          return;
-        }
-
-        if ((ranges.ctrlKey || ranges.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
-          // Add annotation
-          setTimeout(() => {
-            eventManager.updateTime(ranges.xaxis);
-          }, 100);
-        } else {
-          scope.$apply(function() {
-            timeSrv.setTime({
-              from: moment.utc(ranges.xaxis.from),
-              to: moment.utc(ranges.xaxis.to),
-            });
-          });
-        }
-      });
-
-      elem.bind('plotclick', function(event, pos, item) {
-        if (panel.xaxis.mode !== 'time') {
-          // Skip if panel in histogram or series mode
-          return;
-        }
-
-        if ((pos.ctrlKey || pos.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
-          // Skip if range selected (added in "plotselected" event handler)
-          let isRangeSelection = pos.x !== pos.x1;
-          if (!isRangeSelection) {
-            setTimeout(() => {
-              eventManager.updateTime({ from: pos.x, to: null });
-            }, 100);
-          }
-        }
-      });
+    return '%H:%M';
+  }
+}
 
-      scope.$on('$destroy', function() {
-        tooltip.destroy();
-        elem.off();
-        elem.remove();
-      });
+/** @ngInject **/
+function graphDirective(timeSrv, popoverSrv, contextSrv) {
+  return {
+    restrict: 'A',
+    template: '',
+    link: (scope, elem) => {
+      return new GraphElement(scope, elem, timeSrv);
     },
   };
 }
 
 coreModule.directive('grafanaGraph', graphDirective);
+export { GraphElement, graphDirective };

+ 1 - 0
public/app/plugins/panel/graph/module.ts

@@ -13,6 +13,7 @@ import { axesEditorComponent } from './axes_editor';
 class GraphCtrl extends MetricsPanelCtrl {
   static template = template;
 
+  renderError: boolean;
   hiddenSeries: any = {};
   seriesList: any = [];
   dataList: any = [];

+ 518 - 0
public/app/plugins/panel/graph/specs/graph.jest.ts

@@ -0,0 +1,518 @@
+jest.mock('app/features/annotations/all', () => ({
+  EventManager: function() {
+    return {
+      on: () => {},
+      addFlotEvents: () => {},
+    };
+  },
+}));
+
+jest.mock('app/core/core', () => ({
+  coreModule: {
+    directive: () => {},
+  },
+  appEvents: {
+    on: () => {},
+  },
+}));
+
+import '../module';
+import { GraphCtrl } from '../module';
+import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl';
+import { PanelCtrl } from 'app/features/panel/panel_ctrl';
+
+import config from 'app/core/config';
+
+import TimeSeries from 'app/core/time_series2';
+import moment from 'moment';
+import $ from 'jquery';
+import { graphDirective } from '../graph';
+
+let ctx = <any>{};
+let ctrl;
+let scope = {
+  ctrl: {},
+  range: {
+    from: moment([2015, 1, 1]),
+    to: moment([2015, 11, 20]),
+  },
+  $on: () => {},
+};
+let link;
+
+describe('grafanaGraph', function() {
+  const setupCtx = (beforeRender?) => {
+    config.bootData = {
+      user: {
+        lightTheme: false,
+      },
+    };
+    GraphCtrl.prototype = <any>{
+      ...MetricsPanelCtrl.prototype,
+      ...PanelCtrl.prototype,
+      ...GraphCtrl.prototype,
+      height: 200,
+      panel: {
+        events: {
+          on: () => {},
+        },
+        legend: {},
+        grid: {},
+        yaxes: [
+          {
+            min: null,
+            max: null,
+            format: 'short',
+            logBase: 1,
+          },
+          {
+            min: null,
+            max: null,
+            format: 'short',
+            logBase: 1,
+          },
+        ],
+        thresholds: [],
+        xaxis: {},
+        seriesOverrides: [],
+        tooltip: {
+          shared: true,
+        },
+      },
+      renderingCompleted: jest.fn(),
+      hiddenSeries: {},
+      dashboard: {
+        getTimezone: () => 'browser',
+      },
+      range: {
+        from: moment([2015, 1, 1, 10]),
+        to: moment([2015, 1, 1, 22]),
+      },
+    };
+
+    ctx.data = [];
+    ctx.data.push(
+      new TimeSeries({
+        datapoints: [[1, 1], [2, 2]],
+        alias: 'series1',
+      })
+    );
+    ctx.data.push(
+      new TimeSeries({
+        datapoints: [[10, 1], [20, 2]],
+        alias: 'series2',
+      })
+    );
+
+    ctrl = new GraphCtrl(
+      {
+        $on: () => {},
+      },
+      {
+        get: () => {},
+      },
+      {}
+    );
+
+    $.plot = ctrl.plot = jest.fn();
+    scope.ctrl = ctrl;
+
+    link = graphDirective({}, {}, {}).link(scope, { width: () => 500, mouseleave: () => {}, bind: () => {} });
+    if (typeof beforeRender === 'function') {
+      beforeRender();
+    }
+    link.data = ctx.data;
+
+    //Emulate functions called by event listeners
+    link.buildFlotPairs(link.data);
+    link.render_panel();
+    ctx.plotData = ctrl.plot.mock.calls[0][1];
+
+    ctx.plotOptions = ctrl.plot.mock.calls[0][2];
+  };
+
+  describe('simple lines options', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.fill = 5;
+        ctrl.panel.linewidth = 3;
+        ctrl.panel.steppedLine = true;
+      });
+    });
+
+    it('should configure plot with correct options', () => {
+      expect(ctx.plotOptions.series.lines.show).toBe(true);
+      expect(ctx.plotOptions.series.lines.fill).toBe(0.5);
+      expect(ctx.plotOptions.series.lines.lineWidth).toBe(3);
+      expect(ctx.plotOptions.series.lines.steps).toBe(true);
+    });
+  });
+
+  describe('sorting stacked series as legend. disabled', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = undefined;
+        ctrl.panel.stack = false;
+      });
+    });
+
+    it('should not modify order of time series', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. min descending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = true;
+      });
+    });
+    it('highest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('sorting stacked series as legend. min ascending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = false;
+        ctrl.panel.stack = true;
+      });
+    });
+    it('lowest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. stacking disabled', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = false;
+      });
+    });
+
+    it('highest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. current descending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'current';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = true;
+      });
+    });
+
+    it('highest last value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('when logBase is log 10', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
+          alias: 'seriesFixedscale',
+        });
+        ctx.data[1].yaxis = 2;
+        ctrl.panel.yaxes[0].logBase = 10;
+
+        ctrl.panel.yaxes[1].logBase = 10;
+        ctrl.panel.yaxes[1].min = '0.05';
+        ctrl.panel.yaxes[1].max = '1500';
+      });
+    });
+
+    it('should apply axis transform, autoscaling (if necessary) and ticks', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBeCloseTo(0.001);
+      expect(axisAutoscale.max).toBe(10000);
+      expect(axisAutoscale.ticks.length).toBeCloseTo(8);
+      expect(axisAutoscale.ticks[0]).toBeCloseTo(0.001);
+      if (axisAutoscale.ticks.length === 7) {
+        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBeCloseTo(1000);
+      } else {
+        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBe(10000);
+      }
+
+      var axisFixedscale = ctx.plotOptions.yaxes[1];
+      expect(axisFixedscale.min).toBe(0.05);
+      expect(axisFixedscale.max).toBe(1500);
+      expect(axisFixedscale.ticks.length).toBe(5);
+      expect(axisFixedscale.ticks[0]).toBe(0.1);
+      expect(axisFixedscale.ticks[4]).toBe(1000);
+    });
+  });
+
+  describe('when logBase is log 10 and data points contain only zeroes', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.yaxes[0].logBase = 10;
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+
+    it('should not set min and max and should create some fake ticks', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBe(undefined);
+      expect(axisAutoscale.max).toBe(undefined);
+      expect(axisAutoscale.ticks.length).toBe(2);
+      expect(axisAutoscale.ticks[0]).toBe(1);
+      expect(axisAutoscale.ticks[1]).toBe(2);
+    });
+  });
+
+  // y-min set 0 is a special case for log scale,
+  // this approximates it by setting min to 0.1
+  describe('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.yaxes[0].logBase = 10;
+        ctrl.panel.yaxes[0].min = '0';
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+    it('should set min to 0.1 and add a tick for 0.1', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBe(0.1);
+      expect(axisAutoscale.max).toBe(10000);
+      expect(axisAutoscale.ticks.length).toBe(6);
+      expect(axisAutoscale.ticks[0]).toBe(0.1);
+      expect(axisAutoscale.ticks[5]).toBe(10000);
+    });
+  });
+
+  describe('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        const heightForApprox5Ticks = 125;
+        ctrl.height = heightForApprox5Ticks;
+        ctrl.panel.yaxes[0].logBase = 2;
+        ctrl.panel.yaxes[0].min = '0';
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+
+    it('should regenerate ticks so that if fits on the y-axis', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.min).toBe(0.1);
+      expect(axisAutoscale.ticks.length).toBe(8);
+      expect(axisAutoscale.ticks[0]).toBe(0.1);
+      expect(axisAutoscale.ticks[7]).toBe(262144);
+      expect(axisAutoscale.max).toBe(262144);
+    });
+
+    it('should set axis max to be max tick value', function() {
+      expect(ctx.plotOptions.yaxes[0].max).toBe(262144);
+    });
+  });
+
+  describe('dashed lines options', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.linewidth = 2;
+        ctrl.panel.dashes = true;
+      });
+    });
+
+    it('should configure dashed plot with correct options', function() {
+      expect(ctx.plotOptions.series.lines.show).toBe(true);
+      expect(ctx.plotOptions.series.dashes.lineWidth).toBe(2);
+      expect(ctx.plotOptions.series.dashes.show).toBe(true);
+    });
+  });
+
+  describe('should use timeStep for barWidth', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.bars = true;
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[1, 10], [2, 20]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should set barWidth', function() {
+      expect(ctx.plotOptions.series.bars.barWidth).toBe(1 / 1.5);
+    });
+  });
+
+  describe('series option overrides, fill & points', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.fill = 5;
+        ctx.data[0].zindex = 10;
+        ctx.data[1].alias = 'test';
+        ctx.data[1].lines = { fill: 0.001 };
+        ctx.data[1].points = { show: true };
+      });
+    });
+
+    it('should match second series and fill zero, and enable points', function() {
+      expect(ctx.plotOptions.series.lines.fill).toBe(0.5);
+      expect(ctx.plotData[1].lines.fill).toBe(0.001);
+      expect(ctx.plotData[1].points.show).toBe(true);
+    });
+  });
+
+  describe('should order series order according to zindex', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctx.data[1].zindex = 1;
+        ctx.data[0].zindex = 10;
+      });
+    });
+
+    it('should move zindex 2 last', function() {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('when series is hidden', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.hiddenSeries = { series2: true };
+      });
+    });
+
+    it('should remove datapoints and disable stack', function() {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].data.length).toBe(0);
+      expect(ctx.plotData[1].stack).toBe(false);
+    });
+  });
+
+  describe('when stack and percent', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.percentage = true;
+        ctrl.panel.stack = true;
+      });
+    });
+
+    it('should show percentage', function() {
+      var axis = ctx.plotOptions.yaxes[0];
+      expect(axis.tickFormatter(100, axis)).toBe('100%');
+    });
+  });
+
+  describe('when panel too narrow to show x-axis dates in same granularity as wide panels', () => {
+    //Set width to 10px
+    describe('and the range is less than 24 hours', function() {
+      beforeEach(() => {
+        setupCtx(() => {
+          ctrl.range.from = moment([2015, 1, 1, 10]);
+          ctrl.range.to = moment([2015, 1, 1, 22]);
+        });
+      });
+
+      it('should format dates as hours minutes', function() {
+        var axis = ctx.plotOptions.xaxis;
+        expect(axis.timeformat).toBe('%H:%M');
+      });
+    });
+
+    describe('and the range is less than one year', function() {
+      beforeEach(() => {
+        setupCtx(() => {
+          ctrl.range.from = moment([2015, 1, 1]);
+          ctrl.range.to = moment([2015, 11, 20]);
+        });
+      });
+
+      it('should format dates as month days', function() {
+        var axis = ctx.plotOptions.xaxis;
+        expect(axis.timeformat).toBe('%m/%d');
+      });
+    });
+  });
+
+  describe('when graph is histogram, and enable stack', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.stack = true;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series2',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', function() {
+      expect(ctx.plotData[0].data[0][0]).toBe(100);
+      expect(ctx.plotData[0].data[0][1]).toBe(2);
+      expect(ctx.plotData[1].data[0][0]).toBe(100);
+      expect(ctx.plotData[1].data[0][1]).toBe(2);
+    });
+  });
+
+  describe('when graph is histogram, and some series are hidden', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = { series2: true };
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series2',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', function() {
+      expect(ctx.plotData[0].data[0][0]).toBe(100);
+      expect(ctx.plotData[0].data[0][1]).toBe(2);
+    });
+  });
+});

+ 0 - 454
public/app/plugins/panel/graph/specs/graph_specs.ts

@@ -1,454 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
-
-import '../module';
-import angular from 'angular';
-import $ from 'jquery';
-import helpers from 'test/specs/helpers';
-import TimeSeries from 'app/core/time_series2';
-import moment from 'moment';
-import { Emitter } from 'app/core/core';
-
-describe('grafanaGraph', function() {
-  beforeEach(angularMocks.module('grafana.core'));
-
-  function graphScenario(desc, func, elementWidth = 500) {
-    describe(desc, () => {
-      var ctx: any = {};
-
-      ctx.setup = setupFunc => {
-        beforeEach(
-          angularMocks.module($provide => {
-            $provide.value('timeSrv', new helpers.TimeSrvStub());
-          })
-        );
-
-        beforeEach(
-          angularMocks.inject(($rootScope, $compile) => {
-            var ctrl: any = {
-              height: 200,
-              panel: {
-                events: new Emitter(),
-                legend: {},
-                grid: {},
-                yaxes: [
-                  {
-                    min: null,
-                    max: null,
-                    format: 'short',
-                    logBase: 1,
-                  },
-                  {
-                    min: null,
-                    max: null,
-                    format: 'short',
-                    logBase: 1,
-                  },
-                ],
-                thresholds: [],
-                xaxis: {},
-                seriesOverrides: [],
-                tooltip: {
-                  shared: true,
-                },
-              },
-              renderingCompleted: sinon.spy(),
-              hiddenSeries: {},
-              dashboard: {
-                getTimezone: sinon.stub().returns('browser'),
-              },
-              range: {
-                from: moment([2015, 1, 1, 10]),
-                to: moment([2015, 1, 1, 22]),
-              },
-            };
-
-            var scope = $rootScope.$new();
-            scope.ctrl = ctrl;
-            scope.ctrl.events = ctrl.panel.events;
-
-            $rootScope.onAppEvent = sinon.spy();
-
-            ctx.data = [];
-            ctx.data.push(
-              new TimeSeries({
-                datapoints: [[1, 1], [2, 2]],
-                alias: 'series1',
-              })
-            );
-            ctx.data.push(
-              new TimeSeries({
-                datapoints: [[10, 1], [20, 2]],
-                alias: 'series2',
-              })
-            );
-
-            setupFunc(ctrl, ctx.data);
-
-            var element = angular.element("<div style='width:" + elementWidth + "px' grafana-graph><div>");
-            $compile(element)(scope);
-            scope.$digest();
-
-            $.plot = ctx.plotSpy = sinon.spy();
-            ctrl.events.emit('render', ctx.data);
-            ctrl.events.emit('render-legend');
-            ctrl.events.emit('legend-rendering-complete');
-            ctx.plotData = ctx.plotSpy.getCall(0).args[1];
-            ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
-          })
-        );
-      };
-
-      func(ctx);
-    });
-  }
-
-  graphScenario('simple lines options', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.lines = true;
-      ctrl.panel.fill = 5;
-      ctrl.panel.linewidth = 3;
-      ctrl.panel.steppedLine = true;
-    });
-
-    it('should configure plot with correct options', () => {
-      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('sorting stacked series as legend. disabled', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = undefined;
-      ctrl.panel.stack = false;
-    });
-
-    it('should not modify order of time series', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. min descending order', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('highest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. min ascending order', ctx => {
-    ctx.setup((ctrl, data) => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = false;
-      ctrl.panel.stack = true;
-    });
-
-    it('lowest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. stacking disabled', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = false;
-    });
-
-    it('highest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. current descending order', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'current';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('highest last value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('when logBase is log 10', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-      ctrl.panel.yaxes[1].logBase = 10;
-      ctrl.panel.yaxes[1].min = '0.05';
-      ctrl.panel.yaxes[1].max = '1500';
-      data[1] = new TimeSeries({
-        datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
-        alias: 'seriesFixedscale',
-      });
-      data[1].yaxis = 2;
-    });
-
-    it('should apply axis transform, autoscaling (if necessary) and ticks', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.max).to.be(10000);
-      expect(axisAutoscale.ticks.length).to.within(7, 8);
-      expect(axisAutoscale.ticks[0]).to.within(0.00099999999, 0.00100000001);
-      if (axisAutoscale.ticks.length === 7) {
-        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.within(999.9999, 1000.0001);
-      } else {
-        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.be(10000);
-      }
-
-      var axisFixedscale = ctx.plotOptions.yaxes[1];
-      expect(axisFixedscale.min).to.be(0.05);
-      expect(axisFixedscale.max).to.be(1500);
-      expect(axisFixedscale.ticks.length).to.be(5);
-      expect(axisFixedscale.ticks[0]).to.be(0.1);
-      expect(axisFixedscale.ticks[4]).to.be(1000);
-    });
-  });
-
-  graphScenario('when logBase is log 10 and data points contain only zeroes', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      data[0] = new TimeSeries({
-        datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should not set min and max and should create some fake ticks', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.be(undefined);
-      expect(axisAutoscale.max).to.be(undefined);
-      expect(axisAutoscale.ticks.length).to.be(2);
-      expect(axisAutoscale.ticks[0]).to.be(1);
-      expect(axisAutoscale.ticks[1]).to.be(2);
-    });
-  });
-
-  // y-min set 0 is a special case for log scale,
-  // this approximates it by setting min to 0.1
-  graphScenario('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      ctrl.panel.yaxes[0].min = '0';
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should set min to 0.1 and add a tick for 0.1', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.be(0.1);
-      expect(axisAutoscale.max).to.be(10000);
-      expect(axisAutoscale.ticks.length).to.be(6);
-      expect(axisAutoscale.ticks[0]).to.be(0.1);
-      expect(axisAutoscale.ticks[5]).to.be(10000);
-    });
-  });
-
-  graphScenario('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      const heightForApprox5Ticks = 125;
-      ctrl.height = heightForApprox5Ticks;
-      ctrl.panel.yaxes[0].logBase = 2;
-      ctrl.panel.yaxes[0].min = '0';
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should regenerate ticks so that if fits on the y-axis', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.min).to.be(0.1);
-      expect(axisAutoscale.ticks.length).to.be(8);
-      expect(axisAutoscale.ticks[0]).to.be(0.1);
-      expect(axisAutoscale.ticks[7]).to.be(262144);
-      expect(axisAutoscale.max).to.be(262144);
-    });
-
-    it('should set axis max to be max tick value', function() {
-      expect(ctx.plotOptions.yaxes[0].max).to.be(262144);
-    });
-  });
-
-  graphScenario('dashed lines options', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.lines = true;
-      ctrl.panel.linewidth = 2;
-      ctrl.panel.dashes = true;
-    });
-
-    it('should configure dashed plot with correct options', function() {
-      expect(ctx.plotOptions.series.lines.show).to.be(true);
-      expect(ctx.plotOptions.series.dashes.lineWidth).to.be(2);
-      expect(ctx.plotOptions.series.dashes.show).to.be(true);
-    });
-  });
-
-  graphScenario('should use timeStep for barWidth', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.bars = true;
-      data[0] = new TimeSeries({
-        datapoints: [[1, 10], [2, 20]],
-        alias: 'series1',
-      });
-    });
-
-    it('should set barWidth', function() {
-      expect(ctx.plotOptions.series.bars.barWidth).to.be(1 / 1.5);
-    });
-  });
-
-  graphScenario('series option overrides, fill & points', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.lines = true;
-      ctrl.panel.fill = 5;
-      data[0].zindex = 10;
-      data[1].alias = 'test';
-      data[1].lines = { fill: 0.001 };
-      data[1].points = { show: true };
-    });
-
-    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(ctrl, data) {
-      data[1].zindex = 1;
-      data[0].zindex = 10;
-    });
-
-    it('should move zindex 2 last', function() {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('when series is hidden', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.hiddenSeries = { series2: true };
-    });
-
-    it('should remove datapoints and disable stack', function() {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].data.length).to.be(0);
-      expect(ctx.plotData[1].stack).to.be(false);
-    });
-  });
-
-  graphScenario('when stack and percent', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.percentage = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('should show percentage', function() {
-      var axis = ctx.plotOptions.yaxes[0];
-      expect(axis.tickFormatter(100, axis)).to.be('100%');
-    });
-  });
-
-  graphScenario(
-    'when panel too narrow to show x-axis dates in same granularity as wide panels',
-    function(ctx) {
-      describe('and the range is less than 24 hours', function() {
-        ctx.setup(function(ctrl) {
-          ctrl.range.from = moment([2015, 1, 1, 10]);
-          ctrl.range.to = moment([2015, 1, 1, 22]);
-        });
-
-        it('should format dates as hours minutes', function() {
-          var axis = ctx.plotOptions.xaxis;
-          expect(axis.timeformat).to.be('%H:%M');
-        });
-      });
-
-      describe('and the range is less than one year', function() {
-        ctx.setup(function(scope) {
-          scope.range.from = moment([2015, 1, 1]);
-          scope.range.to = moment([2015, 11, 20]);
-        });
-
-        it('should format dates as month days', function() {
-          var axis = ctx.plotOptions.xaxis;
-          expect(axis.timeformat).to.be('%m/%d');
-        });
-      });
-    },
-    10
-  );
-
-  graphScenario('when graph is histogram, and enable stack', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.xaxis.mode = 'histogram';
-      ctrl.panel.stack = true;
-      ctrl.hiddenSeries = {};
-      data[0] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series1',
-      });
-      data[1] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series2',
-      });
-    });
-
-    it('should calculate correct histogram', function() {
-      expect(ctx.plotData[0].data[0][0]).to.be(100);
-      expect(ctx.plotData[0].data[0][1]).to.be(2);
-      expect(ctx.plotData[1].data[0][0]).to.be(100);
-      expect(ctx.plotData[1].data[0][1]).to.be(2);
-    });
-  });
-
-  graphScenario('when graph is histogram, and some series are hidden', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.xaxis.mode = 'histogram';
-      ctrl.panel.stack = false;
-      ctrl.hiddenSeries = { series2: true };
-      data[0] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series1',
-      });
-      data[1] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series2',
-      });
-    });
-
-    it('should calculate correct histogram', function() {
-      expect(ctx.plotData[0].data[0][0]).to.be(100);
-      expect(ctx.plotData[0].data[0][1]).to.be(2);
-    });
-  });
-});