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

Restored the "fill all the holes" strategy to the ZeroFill class, and introduced the fill_style option ("minimal" and
"all" are possible values). Also allowed a set of required times to be specified when asking the times series for it's
data. This way, we can ensure in the stacked bar chart that each data point in use has a value preventing the bars
from stacking incorrectly.

Spencer Alger 12 лет назад
Родитель
Сommit
d891d00fa4
1 измененных файлов с 138 добавлено и 52 удалено
  1. 138 52
      panels/histogram/module.js

+ 138 - 52
panels/histogram/module.js

@@ -197,12 +197,12 @@ angular.module('kibana.histogram', [])
           // we need to initialize the data variable on the first run,
           // and when we are working on the first segment of the data.
           if(_.isUndefined($scope.data[i]) || segment === 0) {
-            time_series = new timeSeries.ZeroFilled(
-              _interval,
-              // range may be false
-              _range && _range.from,
-              _range && _range.to
-            );
+            time_series = new timeSeries.ZeroFilled({
+              interval: _interval,
+              start_date: _range && _range.from,
+              end_date: _range && _range.to,
+              fillStyle: 'minimal'
+            });
             hits = 0;
           } else {
             time_series = $scope.data[i].time_series;
@@ -216,9 +216,8 @@ angular.module('kibana.histogram', [])
             $scope.hits += entry.count; // Entire dataset level hits counter
           });
           $scope.data[i] = {
-            time_series: time_series,
             info: querySrv.list[id],
-            data: time_series.getFlotPairs(),
+            time_series: time_series,
             hits: hits
           };
 
@@ -310,7 +309,7 @@ angular.module('kibana.histogram', [])
 
         // Populate from the query service
         try {
-          _.each(scope.data,function(series) {
+          _.each(scope.data, function(series) {
             series.label = series.info.alias;
             series.color = series.info.color;
           });
@@ -383,6 +382,19 @@ angular.module('kibana.histogram', [])
               options.selection = { mode: "x", color: '#666' };
             }
 
+            // when rendering stacked bars, we need to ensure each point that has data is zero-filled
+            // so that the stacking happens in the proper order
+            var required_times = [];
+            if (scope.panel.bars && stack) {
+              required_times = Array.prototype.concat.apply([], _.map(scope.data, function (series) {
+                return series.time_series.getOrderedTimes();
+              }));
+            }
+
+            for (var i = 0; i < scope.data.length; i++) {
+              scope.data[i].data = scope.data[i].time_series.getFlotPairs(required_times);
+            }
+
             scope.plot = $.plot(elem, scope.data, options);
 
           } catch(e) {
@@ -448,36 +460,53 @@ angular.module('kibana.histogram', [])
   };
 })
 .service('timeSeries', function () {
+  // map compatable parseInt
+  function base10Int(val) {
+    return parseInt(val, 10);
+  }
+
   /**
    * Certain graphs require 0 entries to be specified for them to render
    * properly (like the line graph). So with this we will caluclate all of
    * the expected time measurements, and fill the missing ones in with 0
-   * @param date     start     The start time for the result set
-   * @param date     end       The end time for the result set
-   * @param integer  interval  The length between measurements, in es interval
-   *                           notation (1m, 30s, 1h, 15d)
+   * @param object  opts  An object specifying some/all of the options
+   *
+   * OPTIONS:
+   * @opt string  interval    The interval notion describing the expected spacing between
+   *                          each data point.
+   * @opt date    start_date  (optional) The start point for the time series, setting this and the
+   *                          end_date will ensure that the series streches to resemble the entire
+   *                          expected result
+   * @opt date    end_date    (optional) The end point for the time series, see start_date
+   * @opt string  fill_style  Either "minimal", or "all" describing the strategy used to zero-fill
+   *                          the series.
    */
-  var undef;
-  function base10Int(val) {
-    return parseInt(val, 10);
-  }
-  this.ZeroFilled = function (interval, start, end) {
+  this.ZeroFilled = function (opts) {
+    this.opts = _.defaults(opts, {
+      interval: '10m',
+      start_date: null,
+      end_date: null,
+      fill_style: 'minimal'
+    });
+
     // the expected differenece between readings.
-    this.interval_ms = base10Int(kbn.interval_to_seconds(interval)) * 1000;
+    this.interval_ms = base10Int(kbn.interval_to_seconds(opts.interval)) * 1000;
+
     // will keep all values here, keyed by their time
     this._data = {};
 
-    if (start) {
-      this.addValue(start, null);
+    if (opts.start_date) {
+      this.addValue(opts.start_date, null);
     }
-    if (end) {
-      this.addValue(end, null);
+    if (opts.end_date) {
+      this.addValue(opts.end_date, null);
     }
   };
+
   /**
    * Add a row
-   * @param  int  time  The time for the value, in
-   * @param  any  value The value at this time
+   * @param {int}  time  The time for the value, in
+   * @param {any}  value The value at this time
    */
   this.ZeroFilled.prototype.addValue = function (time, value) {
     if (time instanceof Date) {
@@ -486,44 +515,101 @@ angular.module('kibana.histogram', [])
       time = base10Int(time);
     }
     if (!isNaN(time)) {
-      this._data[time] = (value === undef ? 0 : value);
+      this._data[time] = (_.isUndefined(value) ? 0 : value);
     }
+    this._cached_times = null;
   };
+
+  /**
+   * Get an array of the times that have been explicitly set in the series
+   * @param {array} include (optional) list of timestamps to include in the response
+   * @return {array} An array of integer times.
+   */
+  this.ZeroFilled.prototype.getOrderedTimes = function (include) {
+    var times = _.map(_.keys(this._data), base10Int).sort();
+    if (_.isArray(include)) {
+      times = times.concat(include);
+    }
+    return times;
+  };
+
   /**
    * return the rows in the format:
    * [ [time, value], [time, value], ... ]
-   * @return array
+   *
+   * Heavy lifting is done by _get(Min|All)FlotPairs()
+   * @param {array} required_times  An array of timestamps that must be in the resulting pairs
+   * @return {array}
    */
-  this.ZeroFilled.prototype.getFlotPairs = function () {
-    // var startTime = performance.now();
-    var times = _.map(_.keys(this._data), base10Int).sort(),
-      result = [];
-    _.each(times, function (time, i, times) {
-      var next, expected_next, prev, expected_prev;
-
-      // check for previous measurement
-      if (i > 0) {
-        prev = times[i - 1];
-        expected_prev = time - this.interval_ms;
-        if (prev < expected_prev) {
-          result.push([expected_prev, 0]);
-        }
+  this.ZeroFilled.prototype.getFlotPairs = function (required_times) {
+    var times = this.getOrderedTimes(required_times),
+      strategy,
+      pairs;
+
+    if(this.opts.fill_style === 'all') {
+      strategy = this._getAllFlotPairs;
+    } else {
+      strategy = this._getMinFlotPairs;
+    }
+
+    return _.reduce(
+      times,    // what
+      strategy, // how
+      [],       // where
+      this      // context
+    );
+  };
+
+  /**
+   * ** called as a reduce stragegy in getFlotPairs() **
+   * Fill zero's on either side of the current time, unless there is already a measurement there or
+   * we are looking at an edge.
+   * @return {array} An array of points to plot with flot
+   */
+  this.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) {
+    var next, expected_next, prev, expected_prev;
+
+    // check for previous measurement
+    if (i > 0) {
+      prev = times[i - 1];
+      expected_prev = time - this.interval_ms;
+      if (prev < expected_prev) {
+        result.push([expected_prev, 0]);
       }
+    }
 
-      // add the current time
-      result.push([ time, this._data[time] ]);
+    // add the current time
+    result.push([ time, this._data[time] || 0 ]);
 
-      // check for next measurement
-      if (times.length > i) {
-        next = times[i + 1];
-        expected_next = time + this.interval_ms;
-        if (next > expected_next) {
-          result.push([expected_next, 0]);
-        }
+    // check for next measurement
+    if (times.length > i) {
+      next = times[i + 1];
+      expected_next = time + this.interval_ms;
+      if (next > expected_next) {
+        result.push([expected_next, 0]);
       }
+    }
+
+    return result;
+  };
+
+  /**
+   * ** called as a reduce stragegy in getFlotPairs() **
+   * Fill zero's to the right of each time, until the next measurement is reached or we are at the
+   * last measurement
+   * @return {array}  An array of points to plot with flot
+   */
+  this.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) {
+    var next, expected_next;
+
+    result.push([ times[i], this._data[times[i]] || 0 ]);
+    next = times[i + 1];
+    expected_next = times[i] + this.interval_ms;
+    for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) {
+      result.push([expected_next, 0]);
+    }
 
-    }, this);
-    // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs');
     return result;
   };
+
 });