Torkel Ödegaard 12 лет назад
Родитель
Сommit
bbcc1ef9ee

+ 13 - 1
src/app/components/require.config.js

@@ -43,6 +43,9 @@ require.config({
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
     elasticjs:                '../vendor/elasticjs/elastic-angular-client',
     elasticjs:                '../vendor/elasticjs/elastic-angular-client',
+
+    'ts-widget':            '../vendor/timeserieswidget/jquery.tswidget',
+    'ts-graphite-helpers':  '../vendor/timeserieswidget/graphite_helpers'
   },
   },
   shim: {
   shim: {
     underscore: {
     underscore: {
@@ -91,7 +94,16 @@ require.config({
     timepicker:             ['jquery', 'bootstrap'],
     timepicker:             ['jquery', 'bootstrap'],
     datepicker:             ['jquery', 'bootstrap'],
     datepicker:             ['jquery', 'bootstrap'],
 
 
-    elasticjs:              ['angular', '../vendor/elasticjs/elastic']
+    elasticjs:              ['angular', '../vendor/elasticjs/elastic'],
+
+    'ts-widget': [
+        'jquery',
+        'jquery.flot',
+        'jquery.flot.selection',
+        'jquery.flot.stack',
+        'jquery.flot.time',
+        'ts-graphite-helpers'
+    ]
   },
   },
   waitSeconds: 60,
   waitSeconds: 60,
 });
 });

+ 2 - 1
src/app/dashboards/default.json

@@ -50,7 +50,8 @@
           "group": [
           "group": [
             "default"
             "default"
           ],
           ],
-          "type": "graph"
+          "type": "graph",
+          "someprop": "hej from config"
         }
         }
       ],
       ],
       "notice": true
       "notice": true

+ 6 - 2
src/app/panels/graph/module.html

@@ -1,9 +1,13 @@
 <div ng-controller='graph' ng-init="init()">
 <div ng-controller='graph' ng-init="init()">
-	<h2>{{saySomething}}</h2>
+	<h2>{{panel.someprop}}</h2>
 
 
 	<ul>
 	<ul>
 		<li>{{panel.graphiteUrl}}</li>
 		<li>{{panel.graphiteUrl}}</li>
 		<li>{{panel.targets}}</li>
 		<li>{{panel.targets}}</li>
 	</ul>
 	</ul>
-
+	<mychart ng-model='data'></mychart>
+	<div class="chart_container flot" id="chart_container_flot">
+        <div class="chart" id="chart_flot" height="300px" width="700px"></div>
+        <div class="legend" id="legend_flot_simple"></div>
+    </div>
 </div>
 </div>

+ 50 - 14
src/app/panels/graph/module.js

@@ -1,21 +1,11 @@
-/** @scratch /panels/5
- * include::panels/text.asciidoc[]
- */
-
-/** @scratch /panels/text/0
- * == text
- * Status: *Stable*
- *
- * The text panel is used for displaying static text formated as markdown, sanitized html or as plain
- * text.
- *
- */
 define([
 define([
+  'jquery',
   'angular',
   'angular',
   'app',
   'app',
-  'underscore'
+  'underscore',
+  'ts-widget'
 ],
 ],
-function (angular, app, _) {
+function ($, angular, app, _, timeseriesWidget) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('kibana.panels.graph', []);
   var module = angular.module('kibana.panels.graph', []);
@@ -40,4 +30,50 @@ function (angular, app, _) {
 
 
   });
   });
 
 
+  angular
+    .module('kibana.directives')
+    .directive('mychart', function () {
+    return {
+        restrict: 'E',
+        link: function (scope, elem, attrs) {
+            var tsData = {
+              graphite_url: 'http://localhost:3030/data',
+              from: '-24hours',
+              until: 'now',
+              height: '300',
+              width: '740',
+              targets: [
+                  {
+                    name: 'series 1',
+                    color: '#CC6699',
+                    target: 'random1',
+                  }
+              ],
+              title: 'horizontal title',
+              vtitle: 'vertical title',
+              drawNullAsZero: false,
+              legend: { container: '#legend_flot_simple', noColumns: 1 },
+          };
+          $("#chart_flot").graphiteFlot(tsData, function(err) {
+            console.log(err);
+          });
+
+            console.log('asd');
+            $(elem).html('NJEEEJ!');
+            /*// If the data changes somehow, update it in the chart
+            scope.$watch('data', function(v){
+                 if(!chart){
+                    chart = $.plot(elem, v , options);
+                    elem.show();
+                }else{
+                    chart.setData(v);
+                    chart.setupGrid();
+                    chart.draw();
+                }
+            });*/
+        }
+    };
+
+  });
+
 });
 });

+ 2 - 14
src/config.js

@@ -38,21 +38,9 @@ function (Settings) {
      * dashboard, but this list is used in the "add panel" interface.
      * dashboard, but this list is used in the "add panel" interface.
      */
      */
     panel_names: [
     panel_names: [
-      'histogram',
-      'map',
-      'pie',
-      'table',
-      'filtering',
-      'timepicker',
+      'graph',
       'text',
       'text',
-      'hits',
-      'dashcontrol',
-      'column',
-      'trends',
-      'bettermap',
-      'query',
-      'terms',
-      'sparklines'
+      'column'
     ]
     ]
   });
   });
 });
 });

+ 29 - 0
src/vendor/timeserieswidget/graphite_helpers.js

@@ -0,0 +1,29 @@
+String.prototype.graphiteGlob = function(glob) {
+    var regex = '^';
+    for (var i = 0; i < glob.length; i++ ) {
+        var c = glob.charAt(i);
+        switch (c) {
+            case '*':
+                regex += '[^\.]+';
+                break;
+            case '.':
+                regex += '\\.';
+                break;
+            default:
+                regex += c;
+        }
+    }
+    regex += '$';
+    return this.match(regex);
+}
+/*
+if (!"stats.dfs4.timer".graphiteGlob('stats.*.timer')) {
+    console.log('fail 1');
+}
+if ("stats.dfs4.timer".graphiteGlob('statsd.*.timer')) {
+    console.log('fail 2');
+}
+if ("stats.dfs4.foo.timer".graphiteGlob('stats.*.timer')) {
+    console.log('fail 3');
+}
+*/

+ 416 - 0
src/vendor/timeserieswidget/jquery.flot.axislabels.js

@@ -0,0 +1,416 @@
+/*
+Axis Labels Plugin for flot.
+http://github.com/markrcote/flot-axislabels
+
+Original code is Copyright (c) 2010 Xuan Luo.
+Original code was released under the GPLv3 license by Xuan Luo, September 2010.
+Original code was rereleased under the MIT license by Xuan Luo, April 2012.
+
+Improvements by Mark Cote.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ */
+(function ($) {
+    var options = { };
+
+    function canvasSupported() {
+        return !!document.createElement('canvas').getContext;
+    }
+
+    function canvasTextSupported() {
+        if (!canvasSupported()) {
+            return false;
+        }
+        var dummy_canvas = document.createElement('canvas');
+        var context = dummy_canvas.getContext('2d');
+        return typeof context.fillText == 'function';
+    }
+
+    function css3TransitionSupported() {
+        var div = document.createElement('div');
+        return typeof div.style.MozTransition != 'undefined'    // Gecko
+            || typeof div.style.OTransition != 'undefined'      // Opera
+            || typeof div.style.webkitTransition != 'undefined' // WebKit
+            || typeof div.style.transition != 'undefined';
+    }
+
+
+    function AxisLabel(axisName, position, padding, plot, opts) {
+        this.axisName = axisName;
+        this.position = position;
+        this.padding = padding;
+        this.plot = plot;
+        this.opts = opts;
+        this.width = 0;
+        this.height = 0;
+    }
+
+
+    CanvasAxisLabel.prototype = new AxisLabel();
+    CanvasAxisLabel.prototype.constructor = CanvasAxisLabel;
+    function CanvasAxisLabel(axisName, position, padding, plot, opts) {
+        AxisLabel.prototype.constructor.call(this, axisName, position, padding,
+                                             plot, opts);
+    }
+
+    CanvasAxisLabel.prototype.calculateSize = function() {
+        if (!this.opts.axisLabelFontSizePixels)
+            this.opts.axisLabelFontSizePixels = 14;
+        if (!this.opts.axisLabelFontFamily)
+            this.opts.axisLabelFontFamily = 'sans-serif';
+
+        var textWidth = this.opts.axisLabelFontSizePixels + this.padding;
+        var textHeight = this.opts.axisLabelFontSizePixels + this.padding;
+        if (this.position == 'left' || this.position == 'right') {
+            this.width = this.opts.axisLabelFontSizePixels + this.padding;
+            this.height = 0;
+        } else {
+            this.width = 0;
+            this.height = this.opts.axisLabelFontSizePixels + this.padding;
+        }
+    };
+
+    CanvasAxisLabel.prototype.draw = function(box) {
+        var ctx = this.plot.getCanvas().getContext('2d');
+        ctx.save();
+        ctx.font = this.opts.axisLabelFontSizePixels + 'px ' +
+            this.opts.axisLabelFontFamily;
+        var width = ctx.measureText(this.opts.axisLabel).width;
+        var height = this.opts.axisLabelFontSizePixels;
+        var x, y, angle = 0;
+        if (this.position == 'top') {
+            x = box.left + box.width/2 - width/2;
+            y = box.top + height*0.72;
+        } else if (this.position == 'bottom') {
+            x = box.left + box.width/2 - width/2;
+            y = box.top + box.height - height*0.72;
+        } else if (this.position == 'left') {
+            x = box.left + height*0.72;
+            y = box.height/2 + box.top + width/2;
+            angle = -Math.PI/2;
+        } else if (this.position == 'right') {
+            x = box.left + box.width - height*0.72;
+            y = box.height/2 + box.top - width/2;
+            angle = Math.PI/2;
+        }
+        ctx.translate(x, y);
+        ctx.rotate(angle);
+        ctx.fillText(this.opts.axisLabel, 0, 0);
+        ctx.restore();
+    };
+
+
+    HtmlAxisLabel.prototype = new AxisLabel();
+    HtmlAxisLabel.prototype.constructor = HtmlAxisLabel;
+    function HtmlAxisLabel(axisName, position, padding, plot, opts) {
+        AxisLabel.prototype.constructor.call(this, axisName, position,
+                                             padding, plot, opts);
+    }
+
+    HtmlAxisLabel.prototype.calculateSize = function() {
+        var elem = $('<div class="axisLabels" style="position:absolute;">' +
+                     this.opts.axisLabel + '</div>');
+        this.plot.getPlaceholder().append(elem);
+        // store height and width of label itself, for use in draw()
+        this.labelWidth = elem.outerWidth(true);
+        this.labelHeight = elem.outerHeight(true);
+        elem.remove();
+
+        this.width = this.height = 0;
+        if (this.position == 'left' || this.position == 'right') {
+            this.width = this.labelWidth + this.padding;
+        } else {
+            this.height = this.labelHeight + this.padding;
+        }
+    };
+
+    HtmlAxisLabel.prototype.draw = function(box) {
+        this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
+        var elem = $('<div id="' + this.axisName + 
+                     'Label" " class="axisLabels" style="position:absolute;">'
+                     + this.opts.axisLabel + '</div>');
+        this.plot.getPlaceholder().append(elem);
+        if (this.position == 'top') {
+            elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px');
+            elem.css('top', box.top + 'px');
+        } else if (this.position == 'bottom') {
+            elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px');
+            elem.css('top', box.top + box.height - this.labelHeight + 'px');
+        } else if (this.position == 'left') {
+            elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px');
+            elem.css('left', box.left + 'px');
+        } else if (this.position == 'right') {
+            elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px');
+            elem.css('left', box.left + box.width - this.labelWidth + 'px');
+        }
+    };
+
+
+    CssTransformAxisLabel.prototype = new HtmlAxisLabel();
+    CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
+    function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
+        HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
+                                                 padding, plot, opts);
+    }
+
+    CssTransformAxisLabel.prototype.calculateSize = function() {
+        HtmlAxisLabel.prototype.calculateSize.call(this);
+        this.width = this.height = 0;
+        if (this.position == 'left' || this.position == 'right') {
+            this.width = this.labelHeight + this.padding;
+        } else {
+            this.height = this.labelHeight + this.padding;
+        }
+    };
+
+    CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
+        var stransforms = {
+            '-moz-transform': '',
+            '-webkit-transform': '',
+            '-o-transform': '',
+            '-ms-transform': ''
+        };
+        if (x != 0 || y != 0) {
+            var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
+            stransforms['-moz-transform'] += stdTranslate;
+            stransforms['-webkit-transform'] += stdTranslate;
+            stransforms['-o-transform'] += stdTranslate;
+            stransforms['-ms-transform'] += stdTranslate;
+        }
+        if (degrees != 0) {
+            var rotation = degrees / 90;
+            var stdRotate = ' rotate(' + degrees + 'deg)';
+            stransforms['-moz-transform'] += stdRotate;
+            stransforms['-webkit-transform'] += stdRotate;
+            stransforms['-o-transform'] += stdRotate;
+            stransforms['-ms-transform'] += stdRotate;
+        }
+        var s = 'top: 0; left: 0; ';
+        for (var prop in stransforms) {
+            if (stransforms[prop]) {
+                s += prop + ':' + stransforms[prop] + ';';
+            }
+        }
+        s += ';';
+        return s;
+    };
+ 
+    CssTransformAxisLabel.prototype.calculateOffsets = function(box) {
+        var offsets = { x: 0, y: 0, degrees: 0 };
+        if (this.position == 'bottom') {
+            offsets.x = box.left + box.width/2 - this.labelWidth/2;
+            offsets.y = box.top + box.height - this.labelHeight;
+        } else if (this.position == 'top') {
+            offsets.x = box.left + box.width/2 - this.labelWidth/2;
+            offsets.y = box.top;
+        } else if (this.position == 'left') {
+            offsets.degrees = -90;
+            offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2;
+            offsets.y = box.height/2 + box.top;
+        } else if (this.position == 'right') {
+            offsets.degrees = 90;
+            offsets.x = box.left + box.width - this.labelWidth/2
+                        - this.labelHeight/2;
+            offsets.y = box.height/2 + box.top;
+        }
+        return offsets;
+    };
+
+    CssTransformAxisLabel.prototype.draw = function(box) {
+        this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
+        var offsets = this.calculateOffsets(box);
+        var elem = $('<div class="axisLabels ' + this.axisName +
+                     'Label" style="position:absolute; ' +
+                     'color: ' + this.opts.color + '; ' +
+                     this.transforms(offsets.degrees, offsets.x, offsets.y) +
+                     '">' + this.opts.axisLabel + '</div>');
+        this.plot.getPlaceholder().append(elem);
+    };
+
+
+    IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
+    IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
+    function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
+        CssTransformAxisLabel.prototype.constructor.call(this, axisName,
+                                                         position, padding,
+                                                         plot, opts);
+        this.requiresResize = false;
+    }
+
+    IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
+        // I didn't feel like learning the crazy Matrix stuff, so this uses
+        // a combination of the rotation transform and CSS positioning.
+        var s = '';
+        if (degrees != 0) {
+            var rotation = degrees/90;
+            while (rotation < 0) {
+                rotation += 4;
+            }
+            s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
+            // see below
+            this.requiresResize = (this.position == 'right');
+        }
+        if (x != 0) {
+            s += 'left: ' + x + 'px; ';
+        }
+        if (y != 0) {
+            s += 'top: ' + y + 'px; ';
+        }
+        return s;
+    };
+
+    IeTransformAxisLabel.prototype.calculateOffsets = function(box) {
+        var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
+                          this, box);
+        // adjust some values to take into account differences between
+        // CSS and IE rotations.
+        if (this.position == 'top') {
+            // FIXME: not sure why, but placing this exactly at the top causes 
+            // the top axis label to flip to the bottom...
+            offsets.y = box.top + 1;
+        } else if (this.position == 'left') {
+            offsets.x = box.left;
+            offsets.y = box.height/2 + box.top - this.labelWidth/2;
+        } else if (this.position == 'right') {
+            offsets.x = box.left + box.width - this.labelHeight;
+            offsets.y = box.height/2 + box.top - this.labelWidth/2;
+        }
+        return offsets;
+    };
+
+    IeTransformAxisLabel.prototype.draw = function(box) {
+        CssTransformAxisLabel.prototype.draw.call(this, box);
+        if (this.requiresResize) {
+            var elem = this.plot.getPlaceholder().find("." + this.axisName + "Label");
+            // Since we used CSS positioning instead of transforms for
+            // translating the element, and since the positioning is done
+            // before any rotations, we have to reset the width and height
+            // in case the browser wrapped the text (specifically for the
+            // y2axis).
+            elem.css('width', this.labelWidth);
+            elem.css('height', this.labelHeight);
+        }
+    };
+
+
+    function init(plot) {
+        // This is kind of a hack. There are no hooks in Flot between
+        // the creation and measuring of the ticks (setTicks, measureTickLabels
+        // in setupGrid() ) and the drawing of the ticks and plot box
+        // (insertAxisLabels in setupGrid() ).
+        //
+        // Therefore, we use a trick where we run the draw routine twice:
+        // the first time to get the tick measurements, so that we can change
+        // them, and then have it draw it again.
+        var secondPass = false;
+
+        var axisLabels = {};
+        var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };
+
+        var defaultPadding = 2;  // padding between axis and tick labels
+        plot.hooks.draw.push(function (plot, ctx) {
+            var hasAxisLabels = false;
+            if (!secondPass) {
+                // MEASURE AND SET OPTIONS
+                $.each(plot.getAxes(), function(axisName, axis) {
+                    var opts = axis.options // Flot 0.7
+                        || plot.getOptions()[axisName]; // Flot 0.6
+                    if (!opts || !opts.axisLabel || !axis.show)
+                        return;
+
+                    hasAxisLabels = true;
+                    var renderer = null;
+
+                    if (!opts.axisLabelUseHtml &&
+                        navigator.appName == 'Microsoft Internet Explorer') {
+                        var ua = navigator.userAgent;
+                        var re  = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+                        if (re.exec(ua) != null) {
+                            rv = parseFloat(RegExp.$1);
+                        }
+                        if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
+                            renderer = CssTransformAxisLabel;
+                        } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
+                            renderer = IeTransformAxisLabel;
+                        } else if (opts.axisLabelUseCanvas) {
+                            renderer = CanvasAxisLabel;
+                        } else {
+                            renderer = HtmlAxisLabel;
+                        }
+                    } else {
+                        if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
+                            renderer = HtmlAxisLabel;
+                        } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
+                            renderer = CanvasAxisLabel;
+                        } else {
+                            renderer = CssTransformAxisLabel;
+                        }
+                    }
+
+                    var padding = opts.axisLabelPadding === undefined ?
+                                  defaultPadding : opts.axisLabelPadding;
+
+                    axisLabels[axisName] = new renderer(axisName,
+                                                        axis.position, padding,
+                                                        plot, opts);
+
+                    // flot interprets axis.labelHeight and .labelWidth as
+                    // the height and width of the tick labels. We increase
+                    // these values to make room for the axis label and
+                    // padding.
+
+                    axisLabels[axisName].calculateSize();
+
+                    // AxisLabel.height and .width are the size of the
+                    // axis label and padding.
+                    axis.labelHeight += axisLabels[axisName].height;
+                    axis.labelWidth += axisLabels[axisName].width;
+                    opts.labelHeight = axis.labelHeight;
+                    opts.labelWidth = axis.labelWidth;
+                });
+                // if there are axis labels re-draw with new label widths and heights
+                if (hasAxisLabels) {
+                    secondPass = true;
+                    plot.setupGrid();
+                    plot.draw();
+                }
+            } else {
+                // DRAW
+                $.each(plot.getAxes(), function(axisName, axis) {
+                    var opts = axis.options // Flot 0.7
+                        || plot.getOptions()[axisName]; // Flot 0.6
+                    if (!opts || !opts.axisLabel || !axis.show)
+                        return;
+
+                    axisLabels[axisName].draw(axis.box);
+                });
+            }
+        });
+    }
+
+
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'axisLabels',
+        version: '2.0b0'
+    });
+})(jQuery);

Разница между файлами не показана из-за своего большого размера
+ 31 - 0
src/vendor/timeserieswidget/jquery.flot.js


+ 360 - 0
src/vendor/timeserieswidget/jquery.flot.selection.js

@@ -0,0 +1,360 @@
+/* Flot plugin for selecting regions of a plot.
+
+Copyright (c) 2007-2013 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+The plugin supports these options:
+
+selection: {
+	mode: null or "x" or "y" or "xy",
+	color: color,
+	shape: "round" or "miter" or "bevel",
+	minSize: number of pixels
+}
+
+Selection support is enabled by setting the mode to one of "x", "y" or "xy".
+In "x" mode, the user will only be able to specify the x range, similarly for
+"y" mode. For "xy", the selection becomes a rectangle where both ranges can be
+specified. "color" is color of the selection (if you need to change the color
+later on, you can get to it with plot.getOptions().selection.color). "shape"
+is the shape of the corners of the selection.
+
+"minSize" is the minimum size a selection can be in pixels. This value can
+be customized to determine the smallest size a selection can be and still
+have the selection rectangle be displayed. When customizing this value, the
+fact that it refers to pixels, not axis units must be taken into account.
+Thus, for example, if there is a bar graph in time mode with BarWidth set to 1
+minute, setting "minSize" to 1 will not make the minimum selection size 1
+minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent
+"plotunselected" events from being fired when the user clicks the mouse without
+dragging.
+
+When selection support is enabled, a "plotselected" event will be emitted on
+the DOM element you passed into the plot function. The event handler gets a
+parameter with the ranges selected on the axes, like this:
+
+	placeholder.bind( "plotselected", function( event, ranges ) {
+		alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
+		// similar for yaxis - with multiple axes, the extra ones are in
+		// x2axis, x3axis, ...
+	});
+
+The "plotselected" event is only fired when the user has finished making the
+selection. A "plotselecting" event is fired during the process with the same
+parameters as the "plotselected" event, in case you want to know what's
+happening while it's happening,
+
+A "plotunselected" event with no arguments is emitted when the user clicks the
+mouse to remove the selection. As stated above, setting "minSize" to 0 will
+destroy this behavior.
+
+The plugin allso adds the following methods to the plot object:
+
+- setSelection( ranges, preventEvent )
+
+  Set the selection rectangle. The passed in ranges is on the same form as
+  returned in the "plotselected" event. If the selection mode is "x", you
+  should put in either an xaxis range, if the mode is "y" you need to put in
+  an yaxis range and both xaxis and yaxis if the selection mode is "xy", like
+  this:
+
+	setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
+
+  setSelection will trigger the "plotselected" event when called. If you don't
+  want that to happen, e.g. if you're inside a "plotselected" handler, pass
+  true as the second parameter. If you are using multiple axes, you can
+  specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of
+  xaxis, the plugin picks the first one it sees.
+
+- clearSelection( preventEvent )
+
+  Clear the selection rectangle. Pass in true to avoid getting a
+  "plotunselected" event.
+
+- getSelection()
+
+  Returns the current selection in the same format as the "plotselected"
+  event. If there's currently no selection, the function returns null.
+
+*/
+
+(function ($) {
+    function init(plot) {
+        var selection = {
+                first: { x: -1, y: -1}, second: { x: -1, y: -1},
+                show: false,
+                active: false
+            };
+
+        // FIXME: The drag handling implemented here should be
+        // abstracted out, there's some similar code from a library in
+        // the navigation plugin, this should be massaged a bit to fit
+        // the Flot cases here better and reused. Doing this would
+        // make this plugin much slimmer.
+        var savedhandlers = {};
+
+        var mouseUpHandler = null;
+        
+        function onMouseMove(e) {
+            if (selection.active) {
+                updateSelection(e);
+                
+                plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
+            }
+        }
+
+        function onMouseDown(e) {
+            if (e.which != 1)  // only accept left-click
+                return;
+            
+            // cancel out any text selections
+            document.body.focus();
+
+            // prevent text selection and drag in old-school browsers
+            if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
+                savedhandlers.onselectstart = document.onselectstart;
+                document.onselectstart = function () { return false; };
+            }
+            if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
+                savedhandlers.ondrag = document.ondrag;
+                document.ondrag = function () { return false; };
+            }
+
+            setSelectionPos(selection.first, e);
+
+            selection.active = true;
+
+            // this is a bit silly, but we have to use a closure to be
+            // able to whack the same handler again
+            mouseUpHandler = function (e) { onMouseUp(e); };
+            
+            $(document).one("mouseup", mouseUpHandler);
+        }
+
+        function onMouseUp(e) {
+            mouseUpHandler = null;
+            
+            // revert drag stuff for old-school browsers
+            if (document.onselectstart !== undefined)
+                document.onselectstart = savedhandlers.onselectstart;
+            if (document.ondrag !== undefined)
+                document.ondrag = savedhandlers.ondrag;
+
+            // no more dragging
+            selection.active = false;
+            updateSelection(e);
+
+            if (selectionIsSane())
+                triggerSelectedEvent();
+            else {
+                // this counts as a clear
+                plot.getPlaceholder().trigger("plotunselected", [ ]);
+                plot.getPlaceholder().trigger("plotselecting", [ null ]);
+            }
+
+            return false;
+        }
+
+        function getSelection() {
+            if (!selectionIsSane())
+                return null;
+            
+            if (!selection.show) return null;
+
+            var r = {}, c1 = selection.first, c2 = selection.second;
+            $.each(plot.getAxes(), function (name, axis) {
+                if (axis.used) {
+                    var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); 
+                    r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
+                }
+            });
+            return r;
+        }
+
+        function triggerSelectedEvent() {
+            var r = getSelection();
+
+            plot.getPlaceholder().trigger("plotselected", [ r ]);
+
+            // backwards-compat stuff, to be removed in future
+            if (r.xaxis && r.yaxis)
+                plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
+        }
+
+        function clamp(min, value, max) {
+            return value < min ? min: (value > max ? max: value);
+        }
+
+        function setSelectionPos(pos, e) {
+            var o = plot.getOptions();
+            var offset = plot.getPlaceholder().offset();
+            var plotOffset = plot.getPlotOffset();
+            pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
+            pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
+
+            if (o.selection.mode == "y")
+                pos.x = pos == selection.first ? 0 : plot.width();
+
+            if (o.selection.mode == "x")
+                pos.y = pos == selection.first ? 0 : plot.height();
+        }
+
+        function updateSelection(pos) {
+            if (pos.pageX == null)
+                return;
+
+            setSelectionPos(selection.second, pos);
+            if (selectionIsSane()) {
+                selection.show = true;
+                plot.triggerRedrawOverlay();
+            }
+            else
+                clearSelection(true);
+        }
+
+        function clearSelection(preventEvent) {
+            if (selection.show) {
+                selection.show = false;
+                plot.triggerRedrawOverlay();
+                if (!preventEvent)
+                    plot.getPlaceholder().trigger("plotunselected", [ ]);
+            }
+        }
+
+        // function taken from markings support in Flot
+        function extractRange(ranges, coord) {
+            var axis, from, to, key, axes = plot.getAxes();
+
+            for (var k in axes) {
+                axis = axes[k];
+                if (axis.direction == coord) {
+                    key = coord + axis.n + "axis";
+                    if (!ranges[key] && axis.n == 1)
+                        key = coord + "axis"; // support x1axis as xaxis
+                    if (ranges[key]) {
+                        from = ranges[key].from;
+                        to = ranges[key].to;
+                        break;
+                    }
+                }
+            }
+
+            // backwards-compat stuff - to be removed in future
+            if (!ranges[key]) {
+                axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
+                from = ranges[coord + "1"];
+                to = ranges[coord + "2"];
+            }
+
+            // auto-reverse as an added bonus
+            if (from != null && to != null && from > to) {
+                var tmp = from;
+                from = to;
+                to = tmp;
+            }
+            
+            return { from: from, to: to, axis: axis };
+        }
+        
+        function setSelection(ranges, preventEvent) {
+            var axis, range, o = plot.getOptions();
+
+            if (o.selection.mode == "y") {
+                selection.first.x = 0;
+                selection.second.x = plot.width();
+            }
+            else {
+                range = extractRange(ranges, "x");
+
+                selection.first.x = range.axis.p2c(range.from);
+                selection.second.x = range.axis.p2c(range.to);
+            }
+
+            if (o.selection.mode == "x") {
+                selection.first.y = 0;
+                selection.second.y = plot.height();
+            }
+            else {
+                range = extractRange(ranges, "y");
+
+                selection.first.y = range.axis.p2c(range.from);
+                selection.second.y = range.axis.p2c(range.to);
+            }
+
+            selection.show = true;
+            plot.triggerRedrawOverlay();
+            if (!preventEvent && selectionIsSane())
+                triggerSelectedEvent();
+        }
+
+        function selectionIsSane() {
+            var minSize = plot.getOptions().selection.minSize;
+            return Math.abs(selection.second.x - selection.first.x) >= minSize &&
+                Math.abs(selection.second.y - selection.first.y) >= minSize;
+        }
+
+        plot.clearSelection = clearSelection;
+        plot.setSelection = setSelection;
+        plot.getSelection = getSelection;
+
+        plot.hooks.bindEvents.push(function(plot, eventHolder) {
+            var o = plot.getOptions();
+            if (o.selection.mode != null) {
+                eventHolder.mousemove(onMouseMove);
+                eventHolder.mousedown(onMouseDown);
+            }
+        });
+
+
+        plot.hooks.drawOverlay.push(function (plot, ctx) {
+            // draw selection
+            if (selection.show && selectionIsSane()) {
+                var plotOffset = plot.getPlotOffset();
+                var o = plot.getOptions();
+
+                ctx.save();
+                ctx.translate(plotOffset.left, plotOffset.top);
+
+                var c = $.color.parse(o.selection.color);
+
+                ctx.strokeStyle = c.scale('a', 0.8).toString();
+                ctx.lineWidth = 1;
+                ctx.lineJoin = o.selection.shape;
+                ctx.fillStyle = c.scale('a', 0.4).toString();
+
+                var x = Math.min(selection.first.x, selection.second.x) + 0.5,
+                    y = Math.min(selection.first.y, selection.second.y) + 0.5,
+                    w = Math.abs(selection.second.x - selection.first.x) - 1,
+                    h = Math.abs(selection.second.y - selection.first.y) - 1;
+
+                ctx.fillRect(x, y, w, h);
+                ctx.strokeRect(x, y, w, h);
+
+                ctx.restore();
+            }
+        });
+        
+        plot.hooks.shutdown.push(function (plot, eventHolder) {
+            eventHolder.unbind("mousemove", onMouseMove);
+            eventHolder.unbind("mousedown", onMouseDown);
+            
+            if (mouseUpHandler)
+                $(document).unbind("mouseup", mouseUpHandler);
+        });
+
+    }
+
+    $.plot.plugins.push({
+        init: init,
+        options: {
+            selection: {
+                mode: null, // one of null, "x", "y" or "xy"
+                color: "#e8cfac",
+                shape: "round", // one of "round", "miter", or "bevel"
+                minSize: 5 // minimum number of pixels
+            }
+        },
+        name: 'selection',
+        version: '1.1'
+    });
+})(jQuery);

+ 188 - 0
src/vendor/timeserieswidget/jquery.flot.stack.js

@@ -0,0 +1,188 @@
+/* Flot plugin for stacking data sets rather than overlyaing them.
+
+Copyright (c) 2007-2013 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+The plugin assumes the data is sorted on x (or y if stacking horizontally).
+For line charts, it is assumed that if a line has an undefined gap (from a
+null point), then the line above it should have the same gap - insert zeros
+instead of "null" if you want another behaviour. This also holds for the start
+and end of the chart. Note that stacking a mix of positive and negative values
+in most instances doesn't make sense (so it looks weird).
+
+Two or more series are stacked when their "stack" attribute is set to the same
+key (which can be any number or string or just "true"). To specify the default
+stack, you can set the stack option like this:
+
+	series: {
+		stack: null/false, true, or a key (number/string)
+	}
+
+You can also specify it for a single series, like this:
+
+	$.plot( $("#placeholder"), [{
+		data: [ ... ],
+		stack: true
+	}])
+
+The stacking order is determined by the order of the data series in the array
+(later series end up on top of the previous).
+
+Internally, the plugin modifies the datapoints in each series, adding an
+offset to the y value. For line series, extra data points are inserted through
+interpolation. If there's a second y value, it's also adjusted (e.g for bar
+charts or filled areas).
+
+*/
+
+(function ($) {
+    var options = {
+        series: { stack: null } // or number/string
+    };
+    
+    function init(plot) {
+        function findMatchingSeries(s, allseries) {
+            var res = null;
+            for (var i = 0; i < allseries.length; ++i) {
+                if (s == allseries[i])
+                    break;
+                
+                if (allseries[i].stack == s.stack)
+                    res = allseries[i];
+            }
+            
+            return res;
+        }
+        
+        function stackData(plot, s, datapoints) {
+            if (s.stack == null || s.stack === false)
+                return;
+
+            var other = findMatchingSeries(s, plot.getData());
+            if (!other)
+                return;
+
+            var ps = datapoints.pointsize,
+                points = datapoints.points,
+                otherps = other.datapoints.pointsize,
+                otherpoints = other.datapoints.points,
+                newpoints = [],
+                px, py, intery, qx, qy, bottom,
+                withlines = s.lines.show,
+                horizontal = s.bars.horizontal,
+                withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
+                withsteps = withlines && s.lines.steps,
+                fromgap = true,
+                keyOffset = horizontal ? 1 : 0,
+                accumulateOffset = horizontal ? 0 : 1,
+                i = 0, j = 0, l, m;
+
+            while (true) {
+                if (i >= points.length)
+                    break;
+
+                l = newpoints.length;
+
+                if (points[i] == null) {
+                    // copy gaps
+                    for (m = 0; m < ps; ++m)
+                        newpoints.push(points[i + m]);
+                    i += ps;
+                }
+                else if (j >= otherpoints.length) {
+                    // for lines, we can't use the rest of the points
+                    if (!withlines) {
+                        for (m = 0; m < ps; ++m)
+                            newpoints.push(points[i + m]);
+                    }
+                    i += ps;
+                }
+                else if (otherpoints[j] == null) {
+                    // oops, got a gap
+                    for (m = 0; m < ps; ++m)
+                        newpoints.push(null);
+                    fromgap = true;
+                    j += otherps;
+                }
+                else {
+                    // cases where we actually got two points
+                    px = points[i + keyOffset];
+                    py = points[i + accumulateOffset];
+                    qx = otherpoints[j + keyOffset];
+                    qy = otherpoints[j + accumulateOffset];
+                    bottom = 0;
+
+                    if (px == qx) {
+                        for (m = 0; m < ps; ++m)
+                            newpoints.push(points[i + m]);
+
+                        newpoints[l + accumulateOffset] += qy;
+                        bottom = qy;
+                        
+                        i += ps;
+                        j += otherps;
+                    }
+                    else if (px > qx) {
+                        // we got past point below, might need to
+                        // insert interpolated extra point
+                        if (withlines && i > 0 && points[i - ps] != null) {
+                            intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
+                            newpoints.push(qx);
+                            newpoints.push(intery + qy);
+                            for (m = 2; m < ps; ++m)
+                                newpoints.push(points[i + m]);
+                            bottom = qy; 
+                        }
+
+                        j += otherps;
+                    }
+                    else { // px < qx
+                        if (fromgap && withlines) {
+                            // if we come from a gap, we just skip this point
+                            i += ps;
+                            continue;
+                        }
+                            
+                        for (m = 0; m < ps; ++m)
+                            newpoints.push(points[i + m]);
+                        
+                        // we might be able to interpolate a point below,
+                        // this can give us a better y
+                        if (withlines && j > 0 && otherpoints[j - otherps] != null)
+                            bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
+
+                        newpoints[l + accumulateOffset] += bottom;
+                        
+                        i += ps;
+                    }
+
+                    fromgap = false;
+                    
+                    if (l != newpoints.length && withbottom)
+                        newpoints[l + 2] += bottom;
+                }
+
+                // maintain the line steps invariant
+                if (withsteps && l != newpoints.length && l > 0
+                    && newpoints[l] != null
+                    && newpoints[l] != newpoints[l - ps]
+                    && newpoints[l + 1] != newpoints[l - ps + 1]) {
+                    for (m = 0; m < ps; ++m)
+                        newpoints[l + ps + m] = newpoints[l + m];
+                    newpoints[l + 1] = newpoints[l - ps + 1];
+                }
+            }
+
+            datapoints.points = newpoints;
+        }
+        
+        plot.hooks.processDatapoints.push(stackData);
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'stack',
+        version: '1.2'
+    });
+})(jQuery);

+ 424 - 0
src/vendor/timeserieswidget/jquery.flot.time.js

@@ -0,0 +1,424 @@
+/* Pretty handling of time axes.
+
+Copyright (c) 2007-2013 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+Set axis.mode to "time" to enable. See the section "Time series data" in
+API.txt for details.
+
+*/
+
+(function($) {
+
+	var options = {
+		xaxis: {
+			timezone: null,		// "browser" for local to the client or timezone for timezone-js
+			timeformat: null,	// format string to use
+			twelveHourClock: false,	// 12 or 24 time in time mode
+			monthNames: null	// list of names of months
+		}
+	};
+
+	// round to nearby lower multiple of base
+
+	function floorInBase(n, base) {
+		return base * Math.floor(n / base);
+	}
+
+	// Returns a string with the date d formatted according to fmt.
+	// A subset of the Open Group's strftime format is supported.
+
+	function formatDate(d, fmt, monthNames, dayNames) {
+
+		if (typeof d.strftime == "function") {
+			return d.strftime(fmt);
+		}
+
+		var leftPad = function(n, pad) {
+			n = "" + n;
+			pad = "" + (pad == null ? "0" : pad);
+			return n.length == 1 ? pad + n : n;
+		};
+
+		var r = [];
+		var escape = false;
+		var hours = d.getHours();
+		var isAM = hours < 12;
+
+		if (monthNames == null) {
+			monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+		}
+
+		if (dayNames == null) {
+			dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+		}
+
+		var hours12;
+
+		if (hours > 12) {
+			hours12 = hours - 12;
+		} else if (hours == 0) {
+			hours12 = 12;
+		} else {
+			hours12 = hours;
+		}
+
+		for (var i = 0; i < fmt.length; ++i) {
+
+			var c = fmt.charAt(i);
+
+			if (escape) {
+				switch (c) {
+					case 'a': c = "" + dayNames[d.getDay()]; break;
+					case 'b': c = "" + monthNames[d.getMonth()]; break;
+					case 'd': c = leftPad(d.getDate()); break;
+					case 'e': c = leftPad(d.getDate(), " "); break;
+					case 'H': c = leftPad(hours); break;
+					case 'I': c = leftPad(hours12); break;
+					case 'l': c = leftPad(hours12, " "); break;
+					case 'm': c = leftPad(d.getMonth() + 1); break;
+					case 'M': c = leftPad(d.getMinutes()); break;
+					// quarters not in Open Group's strftime specification
+					case 'q':
+						c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
+					case 'S': c = leftPad(d.getSeconds()); break;
+					case 'y': c = leftPad(d.getFullYear() % 100); break;
+					case 'Y': c = "" + d.getFullYear(); break;
+					case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
+					case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
+					case 'w': c = "" + d.getDay(); break;
+				}
+				r.push(c);
+				escape = false;
+			} else {
+				if (c == "%") {
+					escape = true;
+				} else {
+					r.push(c);
+				}
+			}
+		}
+
+		return r.join("");
+	}
+
+	// To have a consistent view of time-based data independent of which time
+	// zone the client happens to be in we need a date-like object independent
+	// of time zones.  This is done through a wrapper that only calls the UTC
+	// versions of the accessor methods.
+
+	function makeUtcWrapper(d) {
+
+		function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
+			sourceObj[sourceMethod] = function() {
+				return targetObj[targetMethod].apply(targetObj, arguments);
+			};
+		};
+
+		var utc = {
+			date: d
+		};
+
+		// support strftime, if found
+
+		if (d.strftime != undefined) {
+			addProxyMethod(utc, "strftime", d, "strftime");
+		}
+
+		addProxyMethod(utc, "getTime", d, "getTime");
+		addProxyMethod(utc, "setTime", d, "setTime");
+
+		var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"];
+
+		for (var p = 0; p < props.length; p++) {
+			addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
+			addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
+		}
+
+		return utc;
+	};
+
+	// select time zone strategy.  This returns a date-like object tied to the
+	// desired timezone
+
+	function dateGenerator(ts, opts) {
+		if (opts.timezone == "browser") {
+			return new Date(ts);
+		} else if (!opts.timezone || opts.timezone == "utc") {
+			return makeUtcWrapper(new Date(ts));
+		} else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
+			var d = new timezoneJS.Date();
+			// timezone-js is fickle, so be sure to set the time zone before
+			// setting the time.
+			d.setTimezone(opts.timezone);
+			d.setTime(ts);
+			return d;
+		} else {
+			return makeUtcWrapper(new Date(ts));
+		}
+	}
+	
+	// map of app. size of time units in milliseconds
+
+	var timeUnitSize = {
+		"second": 1000,
+		"minute": 60 * 1000,
+		"hour": 60 * 60 * 1000,
+		"day": 24 * 60 * 60 * 1000,
+		"month": 30 * 24 * 60 * 60 * 1000,
+		"quarter": 3 * 30 * 24 * 60 * 60 * 1000,
+		"year": 365.2425 * 24 * 60 * 60 * 1000
+	};
+
+	// the allowed tick sizes, after 1 year we use
+	// an integer algorithm
+
+	var baseSpec = [
+		[1, "second"], [2, "second"], [5, "second"], [10, "second"],
+		[30, "second"], 
+		[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+		[30, "minute"], 
+		[1, "hour"], [2, "hour"], [4, "hour"],
+		[8, "hour"], [12, "hour"],
+		[1, "day"], [2, "day"], [3, "day"],
+		[0.25, "month"], [0.5, "month"], [1, "month"],
+		[2, "month"]
+	];
+
+	// we don't know which variant(s) we'll need yet, but generating both is
+	// cheap
+
+	var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
+		[1, "year"]]);
+	var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
+		[1, "year"]]);
+
+	function init(plot) {
+		plot.hooks.processDatapoints.push(function (plot, series, datapoints) {
+			$.each(plot.getAxes(), function(axisName, axis) {
+
+				var opts = axis.options;
+
+				if (opts.mode == "time") {
+					axis.tickGenerator = function(axis) {
+
+						var ticks = [];
+						var d = dateGenerator(axis.min, opts);
+						var minSize = 0;
+
+						// make quarter use a possibility if quarters are
+						// mentioned in either of these options
+
+						var spec = (opts.tickSize && opts.tickSize[1] ===
+							"quarter") ||
+							(opts.minTickSize && opts.minTickSize[1] ===
+							"quarter") ? specQuarters : specMonths;
+
+						if (opts.minTickSize != null) {
+							if (typeof opts.tickSize == "number") {
+								minSize = opts.tickSize;
+							} else {
+								minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
+							}
+						}
+
+						for (var i = 0; i < spec.length - 1; ++i) {
+							if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+											  + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
+								&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
+								break;
+							}
+						}
+
+						var size = spec[i][0];
+						var unit = spec[i][1];
+
+						// special-case the possibility of several years
+
+						if (unit == "year") {
+
+							// if given a minTickSize in years, just use it,
+							// ensuring that it's an integer
+
+							if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
+								size = Math.floor(opts.minTickSize[0]);
+							} else {
+
+								var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
+								var norm = (axis.delta / timeUnitSize.year) / magn;
+
+								if (norm < 1.5) {
+									size = 1;
+								} else if (norm < 3) {
+									size = 2;
+								} else if (norm < 7.5) {
+									size = 5;
+								} else {
+									size = 10;
+								}
+
+								size *= magn;
+							}
+
+							// minimum size for years is 1
+
+							if (size < 1) {
+								size = 1;
+							}
+						}
+
+						axis.tickSize = opts.tickSize || [size, unit];
+						var tickSize = axis.tickSize[0];
+						unit = axis.tickSize[1];
+
+						var step = tickSize * timeUnitSize[unit];
+
+						if (unit == "second") {
+							d.setSeconds(floorInBase(d.getSeconds(), tickSize));
+						} else if (unit == "minute") {
+							d.setMinutes(floorInBase(d.getMinutes(), tickSize));
+						} else if (unit == "hour") {
+							d.setHours(floorInBase(d.getHours(), tickSize));
+						} else if (unit == "month") {
+							d.setMonth(floorInBase(d.getMonth(), tickSize));
+						} else if (unit == "quarter") {
+							d.setMonth(3 * floorInBase(d.getMonth() / 3,
+								tickSize));
+						} else if (unit == "year") {
+							d.setFullYear(floorInBase(d.getFullYear(), tickSize));
+						}
+
+						// reset smaller components
+
+						d.setMilliseconds(0);
+
+						if (step >= timeUnitSize.minute) {
+							d.setSeconds(0);
+						} else if (step >= timeUnitSize.hour) {
+							d.setMinutes(0);
+						} else if (step >= timeUnitSize.day) {
+							d.setHours(0);
+						} else if (step >= timeUnitSize.day * 4) {
+							d.setDate(1);
+						} else if (step >= timeUnitSize.month * 2) {
+							d.setMonth(floorInBase(d.getMonth(), 3));
+						} else if (step >= timeUnitSize.quarter * 2) {
+							d.setMonth(floorInBase(d.getMonth(), 6));
+						} else if (step >= timeUnitSize.year) {
+							d.setMonth(0);
+						}
+
+						var carry = 0;
+						var v = Number.NaN;
+						var prev;
+
+						do {
+
+							prev = v;
+							v = d.getTime();
+							ticks.push(v);
+
+							if (unit == "month" || unit == "quarter") {
+								if (tickSize < 1) {
+
+									// a bit complicated - we'll divide the
+									// month/quarter up but we need to take
+									// care of fractions so we don't end up in
+									// the middle of a day
+
+									d.setDate(1);
+									var start = d.getTime();
+									d.setMonth(d.getMonth() +
+										(unit == "quarter" ? 3 : 1));
+									var end = d.getTime();
+									d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+									carry = d.getHours();
+									d.setHours(0);
+								} else {
+									d.setMonth(d.getMonth() +
+										tickSize * (unit == "quarter" ? 3 : 1));
+								}
+							} else if (unit == "year") {
+								d.setFullYear(d.getFullYear() + tickSize);
+							} else {
+								d.setTime(v + step);
+							}
+						} while (v < axis.max && v != prev);
+
+						return ticks;
+					};
+
+					axis.tickFormatter = function (v, axis) {
+
+						var d = dateGenerator(v, axis.options);
+
+						// first check global format
+
+						if (opts.timeformat != null) {
+							return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
+						}
+
+						// possibly use quarters if quarters are mentioned in
+						// any of these places
+
+						var useQuarters = (axis.options.tickSize &&
+								axis.options.tickSize[1] == "quarter") ||
+							(axis.options.minTickSize &&
+								axis.options.minTickSize[1] == "quarter");
+
+						var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+						var span = axis.max - axis.min;
+						var suffix = (opts.twelveHourClock) ? " %p" : "";
+						var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
+						var fmt;
+
+						if (t < timeUnitSize.minute) {
+							fmt = hourCode + ":%M:%S" + suffix;
+						} else if (t < timeUnitSize.day) {
+							if (span < 2 * timeUnitSize.day) {
+								fmt = hourCode + ":%M" + suffix;
+							} else {
+								fmt = "%b %d " + hourCode + ":%M" + suffix;
+							}
+						} else if (t < timeUnitSize.month) {
+							fmt = "%b %d";
+						} else if ((useQuarters && t < timeUnitSize.quarter) ||
+							(!useQuarters && t < timeUnitSize.year)) {
+							if (span < timeUnitSize.year) {
+								fmt = "%b";
+							} else {
+								fmt = "%b %Y";
+							}
+						} else if (useQuarters && t < timeUnitSize.year) {
+							if (span < timeUnitSize.year) {
+								fmt = "Q%q";
+							} else {
+								fmt = "Q%q %Y";
+							}
+						} else {
+							fmt = "%Y";
+						}
+
+						var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
+
+						return rt;
+					};
+				}
+			});
+		});
+	}
+
+	$.plot.plugins.push({
+		init: init,
+		options: options,
+		name: 'time',
+		version: '1.0'
+	});
+
+	// Time-axis support used to be in Flot core, which exposed the
+	// formatDate function on the plot object.  Various plugins depend
+	// on the function, so we need to re-expose it here.
+
+	$.plot.formatDate = formatDate;
+
+})(jQuery);

+ 838 - 0
src/vendor/timeserieswidget/jquery.tswidget.js

@@ -0,0 +1,838 @@
+function strip_ending_slash(str) {
+    if(str.substr(-1) == '/') {
+        return str.substr(0, str.length - 1);
+    }
+    return str;
+}
+
+function truncate_str(str) {
+    if (str.length >= 147) {
+        return str.substring(0, 148) + "...";
+    }
+    return str
+}
+
+function build_graphite_options(options, raw) {
+    raw = raw || false;
+    var clean_options = [];
+    internal_options = ['_t'];
+    graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format'];
+    graphite_png_options = ['areaMode', 'width', 'height', 'template', 'margin', 'bgcolor',
+                         'fgcolor', 'fontName', 'fontSize', 'fontBold', 'fontItalic',
+                         'yMin', 'yMax', 'colorList', 'title', 'vtitle', 'lineMode',
+                         'lineWith', 'hideLegend', 'hideAxes', 'hideGrid', 'minXstep',
+                         'majorGridlineColor', 'minorGridLineColor', 'minorY',
+                         'thickness', 'min', 'max', 'tz'];
+
+    if(raw) {
+        options['format'] = 'json';
+    } else {
+        // use random parameter to force image refresh
+        options["_t"] = options["_t"] || Math.random();
+    }
+
+    $.each(options, function (key, value) {
+        if(raw) {
+            if ($.inArray(key, graphite_options) == -1) {
+                return;
+            }
+        } else {
+            if ($.inArray(key, graphite_options) == -1 && $.inArray(key, graphite_png_options) == -1) {
+                return;
+            }
+        }
+        if (key === "targets") {
+            $.each(value, function (index, value) {
+                    if (raw) {
+                        // it's normally pointless to use alias() in raw mode, because we apply an alias (name) ourself
+                        // in the client rendering step.  we just need graphite to return the target.
+                        // but graphite sometimes alters the name of the target in the returned data
+                        // (https://github.com/graphite-project/graphite-web/issues/248)
+                        // so we need a good string identifier and set it using alias() (which graphite will honor)
+                        // so that we recognize the returned output. simplest is just to include the target spec again
+                        // though this duplicates a lot of info in the url.
+                        clean_options.push("target=alias(" + encodeURIComponent(value.target) + ",'" + value.target +"')");
+                    } else {
+                        clean_options.push("target=alias(color(" +encodeURIComponent(value.target + ",'" + value.color) +"'),'" + value.name +"')");
+                    }
+            });
+        } else if (value !== null) {
+            clean_options.push(key + "=" + encodeURIComponent(value));
+        }
+    });
+    return clean_options;
+}
+
+// build url for an image. but GET url's are limited in length, so if you have many/long params, some may be missing.
+// could be made smarter for example to favor non-target options because we usually don't want to loose any of those.
+function build_graphite_url(options) {
+    var limit = 2000;  // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
+    var url = options.graphite_url + "?";
+
+    options = build_graphite_options(options, false);
+    $.map(options, function(option) {
+        if (url.length + option.length < limit) {
+            url += '&' + option;
+        }
+    });
+    return url.replace(/\?&/, "?");
+}
+
+function build_anthracite_url(options) {
+    url = strip_ending_slash(options.anthracite_url) + '/events/json';
+    if ('events_query' in options) {
+        url += '?q=' + options['events_query'];
+    }
+    return url;
+}
+
+function find_definition (target_graphite, options) {
+    var matching_i = undefined;
+    for (var cfg_i = 0; cfg_i < options.targets.length && matching_i == undefined; cfg_i++) {
+        // string match (no globbing)
+        if(options.targets[cfg_i].target == target_graphite.target) {
+            matching_i = cfg_i;
+        }
+        // glob match?
+        else if(target_graphite.target.graphiteGlob(options.targets[cfg_i].target)) {
+            matching_i = cfg_i;
+        }
+    }
+    if (matching_i == undefined) {
+        console.error ("internal error: could not figure out which target_option target_graphite '" +
+                target_graphite.target + "' comes from");
+        return [];
+    }
+    return options.targets[matching_i];
+}
+
+(function ($) {
+    /*
+    from graphite-web-0.9.9/graphTemplates.conf.example:
+
+    [default]
+    background = black
+    foreground = white
+    majorLine = white
+    minorLine = grey
+    lineColors = blue,green,red,purple,brown,yellow,aqua,grey,magenta,pink,gold,rose
+    fontName = Sans
+    fontSize = 10
+    fontBold = False
+    fontItalic = False
+
+    definitions below are from http://graphite.readthedocs.org/en/1.0/url-api.html
+    */
+    var default_graphite_options = {
+        'bgcolor': '#000000', // background color of the graph
+        'fgcolor' : '#ffffff',  // title, legend text, and axis labels
+        'majorLine': '#ffffff',
+        'minorLine': '#afafaf'
+    }
+    var default_tswidget_options = {
+        'events_color': '#ccff66',
+        'es_events_color': '#ff0066',
+        'events_text_color': '#5C991F'
+    }
+
+    $.fn.graphite = function (options) {
+        if (options === "update") {
+            $.fn.graphite.update(this, arguments[1]);
+            return this;
+        }
+
+        // Initialize plugin //
+        options = options || {};
+        var settings = $.extend({}, $.fn.graphite.defaults, options);
+
+        return this.each(function () {
+            $this = $(this);
+
+            $this.data("graphOptions", settings);
+            $.fn.graphite.render($this, settings);
+        });
+
+    };
+
+    $.fn.graphite.render = function($img, options) {
+        $img.attr("src", build_graphite_url(options));
+        $img.attr("height", options.height);
+        $img.attr("width", options.width);
+    };
+
+    $.fn.graphite.update = function($img, options) {
+        options = options || {};
+        $img.each(function () {
+            $this = $(this);
+            var settings = $.extend({}, $this.data("graphOptions"), options);
+            $this.data("graphOptions", settings);
+            $.fn.graphite.render($this, settings);
+        });
+    };
+
+    // note: graphite json output is a list of dicts like:
+    // {"datapoints": [...], "target": "<metricname>" }
+    // if you did alias(series, "foo") then "target" will contain the alias
+    // (loosing the metricname which is bad, esp. when you had a glob with an alias, then you don't know what's what)
+    // rickshaw: options.series is a list of dicts like:
+    // { name: "alias", color: "foo", data: [{x: (...), y: (...)} , ...]}
+    // we basically tell users to use this dict, with extra 'target' to specify graphite target string
+    // flot: d = [[<ts>, <val>], (...)]
+    // plot ($(..), [d], ..)
+    $.fn.graphiteRick = function (options, on_error) {
+        options = options || {};
+        var settings = $.extend({}, default_graphite_options, default_tswidget_options, $.fn.graphite.defaults, options);
+
+        return this.each(function () {
+            $this = $(this);
+            $this.data("graphOptions", settings);
+            $.fn.graphiteRick.render(this, settings, on_error);
+        });
+    };
+
+    $.fn.graphiteFlot = function (options, on_error) {
+        if ('zoneFileBasePath' in options) {
+            timezoneJS.timezone.zoneFileBasePath = options['zoneFileBasePath'];
+            timezoneJS.timezone.init();
+        }
+        options = options || {};
+        var settings = $.extend({}, default_graphite_options, default_tswidget_options, $.fn.graphite.defaults, options);
+
+        return this.each(function () {
+            $this = $(this);
+            $this.data("graphOptions", settings);
+            $.fn.graphiteFlot.render(this, settings, on_error);
+        });
+    };
+
+    $.fn.graphiteHighcharts = function (options, on_error) {
+        if ('zoneFileBasePath' in options) {
+            timezoneJS.timezone.zoneFileBasePath = options['zoneFileBasePath'];
+            timezoneJS.timezone.init();
+        }
+        options = options || {};
+        var settings = $.extend({}, default_graphite_options, default_tswidget_options, $.fn.graphite.defaults, options);
+
+        return this.each(function () {
+            $this = $(this);
+            $this.data("graphOptions", settings);
+            $.fn.graphiteHighcharts.render(this, settings, on_error);
+        });
+    };
+
+    $.fn.graphiteFlot.render = function(div, options, on_error) {
+        var id = div.getAttribute('id');
+        $div = $(div);
+        $div.height(options.height);
+        $div.width(options.width);
+        var events = [];
+        var es_events = [];
+        var all_targets = [];
+        var add_targets = function(response_data) {
+            for (var res_i = 0; res_i < response_data.length; res_i++) {
+                var target = find_definition(response_data[res_i], options);
+                target.label = target.name; // flot wants 'label'
+                target.data = [];
+                var nulls = 0;
+                var non_nulls = 0;
+                for (var i in response_data[res_i].datapoints) {
+                    if(response_data[res_i].datapoints[i][0] == null) {
+                        nulls++;
+                        if('drawNullAsZero' in options && options['drawNullAsZero']) {
+                            response_data[res_i].datapoints[i][0] = 0;
+                        } else {
+                            // don't tell flot about null values, it prevents adjacent non-null values from
+                            // being rendered correctly
+                            continue;
+                        }
+                    } else {
+                        non_nulls++;
+                    }
+                    target.data.push([response_data[res_i].datapoints[i][1] * 1000, response_data[res_i].datapoints[i][0]]);
+                }
+                if (nulls/non_nulls > 0.3) {
+                    console.log("warning: rendered target contains " + nulls + " null values, " + non_nulls + " non_nulls");
+                }
+                all_targets.push(target);
+            }
+        }
+
+        var drawFlot = function(es_data, anthracite_data) {
+
+            // default config state modifiers (you can override them in your config objects)
+            var states = {
+                'stacked': {
+                    'series': {'stack': true, 'lines': {'show': true, 'lineWidth': 0, 'fill': 1}},
+                },
+                'lines': {
+                    // flot lib wants 0 or null. not false o_O
+                    'series': {'stack': null, 'lines': { 'show': true, 'lineWidth': 0.6, 'fill': false }}
+                }
+            };
+            if(!('states' in options)) {
+                options['states'] = {};
+            }
+            $.extend(options['states'], states);
+
+            function suffixFormatterSI(val, axis) {
+                range = axis.max - axis.min;
+                lowest = Math.min (range,val);
+                if (lowest >= Math.pow(10,12))
+                    return (val / Math.pow(10,12)).toFixed(axis.tickDecimals) + " T";
+                if (lowest >= Math.pow(10,9))
+                    return (val / Math.pow(10,9)).toFixed(axis.tickDecimals) + " G";
+                if (lowest >= Math.pow(10,6))
+                    return (val / Math.pow(10,6)).toFixed(axis.tickDecimals) + " M";
+                if (lowest >= Math.pow(10,3))
+                    return (val / Math.pow(10,3)).toFixed(axis.tickDecimals) + " k";
+                return val.toFixed(axis.tickDecimals);
+            }
+            function suffixFormatterBinary(val, axis) {
+                range = axis.max - axis.min;
+                lowest = Math.min (range,val);
+                if (lowest >= Math.pow(2,40))
+                    return (val / Math.pow(2,40)).toFixed(axis.tickDecimals) + " Ti";
+                if (lowest >= Math.pow(2,30))
+                    return (val / Math.pow(2,30)).toFixed(axis.tickDecimals) + " Gi";
+                if (lowest >= Math.pow(2,20))
+                    return (val / Math.pow(2,20)).toFixed(axis.tickDecimals) + " Mi";
+                if (lowest >= Math.pow(2,10))
+                    return (val / Math.pow(2,10)).toFixed(axis.tickDecimals) + " Ki";
+                return val.toFixed(axis.tickDecimals);
+            }
+
+            var buildFlotOptions = function(options) {
+                // xaxis color = title color and horizontal lines in grid
+                // yaxis color = vtitle color and vertical lines in grid
+                // xaxis tickcolor = override vertical lines in grid
+                // yaxis tickcolor = override horizontal lines in grid
+                // note: flot doesn't distinguish between major and minor line
+                // so i use minor, because graphite uses very thin lines which make them look less intense,
+                // in flot they seem to be a bit thicker, so generally make them less intense to have them look similar.
+                // although they still look more intense than in graphite though.
+                // tuning tickLength doesn't seem to help (and no lineWidth for axis..). maybe a graphite bug
+                options['tickColor'] = options['minorLine'];
+                options['xaxis'] = options['xaxis'] || {};
+                $.extend(options['xaxis'], { color: options['fgcolor'], tickColor: options['tickColor'], mode: 'time'});
+                if ('tz' in options) {
+                    options['xaxis']['timezone'] = options['tz'];
+                }
+                options['yaxis'] = options['yaxis'] || {};
+                $.extend(options['yaxis'], { color: options['fgcolor'], tickColor: options['tickColor'], tickFormatter: suffixFormatterSI});
+                if('suffixes' in options) {
+                    if(options['suffixes'] == 'binary') {
+                        options['yaxis']['tickFormatter'] = suffixFormatterBinary;
+                    } else if(!options['suffixes']) {
+                        delete options['yaxis']['tickFormatter'];
+                    }
+                }
+                if('title' in options) {
+                    options['xaxes'] = [{axisLabel: options['title']}];
+                }
+                if('vtitle' in options) {
+                    options['yaxes'] = [{position: 'left', axisLabel: options['vtitle']}];
+                }
+                for (i = 0; i < options['targets'].length; i++ ) {
+                    options['targets'][i]['color'] = options['targets'][i]['color'];
+                }
+                if(!('grid' in options)) {
+                    options['grid'] = {};
+                }
+                if(options['hover_details']) {
+                    options['grid']['hoverable'] = true;
+                    options['grid']['autoHighlight'] = true;  // show datapoint being hilighted. true by default but hardcode to make sure
+                }
+                if(!('markings' in options['grid'])) {
+                    options['grid']['markings'] = [];
+                }
+                if(!('selection' in options)) {
+                    options['selection'] = {
+                        'mode': "xy"
+                    };
+                }
+
+                for (var i = 0; i < events.length; i++) {
+                    x = events[i].date * 1000;
+                    options['grid']['markings'].push({ color: options['events_color'], lineWidth: 1, xaxis: { from: x, to: x} });
+                }
+                // custom es_events loop
+                for (var i = 0; i < es_events.length; i++) {
+                    x = Date.parse(es_events[i]['_source']['@timestamp']);
+                    options['grid']['markings'].push({ color: options['es_events_color'], lineWidth: 1, xaxis: { from: x, to: x} });
+                }
+                state = options['state'] || 'lines';
+                return $.extend(options, options['states'][state]);
+            }
+            var plot = $.plot(div, all_targets, buildFlotOptions(options));
+            $div.bind('plotselected', function (event, ranges) {
+                // clamp the zooming to prevent eternal zoom
+
+                if (ranges.xaxis.to - ranges.xaxis.from < 0.00001) {
+                    ranges.xaxis.to = ranges.xaxis.from + 0.00001;
+                }
+
+                if (ranges.yaxis.to - ranges.yaxis.from < 0.00001) {
+                    ranges.yaxis.to = ranges.yaxis.from + 0.00001;
+                }
+
+                // do the zooming
+                zoomed_options = buildFlotOptions(options);
+                zoomed_options['xaxis']['min'] = ranges.xaxis.from;
+                zoomed_options['xaxis']['max'] = ranges.xaxis.to;
+                zoomed_options['yaxis']['min'] = ranges.yaxis.from;
+                zoomed_options['yaxis']['max'] = ranges.yaxis.to;
+                plot = $.plot(div, all_targets, zoomed_options);
+            });
+            // add labels
+            var o;
+            /*for (var i = 0; i < events.length; i++) {
+                o = plot.pointOffset({ x: events[i].date * 1000, y: 0});
+                msg = '<div style="position:absolute;left:' + (o.left) + 'px;top:' + ( o.top + 35 ) + 'px;';
+                msg += 'color:' + options['events_text_color'] + ';font-size:smaller">';
+                msg += '<b>' + events[i].type + '</b></br>';
+                msg += events[i].desc
+                msg += '</div>';
+                $div.append(msg);
+            }
+            for (var i = 0; i < es_events.length; i++) {
+                o = plot.pointOffset({ x: Date.parse(es_events[i]['_source']['@timestamp']), y: 0});
+                msg = '<div style="background-color:#40FF00;position:absolute;left:' + (o.left) + 'px;top:' + ( o.top + 35 ) + 'px;';
+                msg += 'color:' + '#FF0066' + ';font-size:smaller">';
+                msg += '<b>tags</b>: ' + es_events[i]['_source']['@tags'].join(' ') + '</br>';
+                msg += "<b>env</b>: " + es_events[i]['_source']['@fields']['environment'] + '</br>';
+                msg += "<b>msg</b>: " + es_events[i]['_source']['@message'] + '</br>';
+                msg += '</div>';
+                $div.append(msg);
+            }*/
+            if (options['line_stack_toggle']) {
+                var form = document.getElementById(options['line_stack_toggle']);
+                if(options['state'] == 'stacked') {
+                    lines_checked = '';
+                    stacked_checked = ' checked';
+                } else {
+                    lines_checked = ' checked';
+                    stacked_checked = '';
+                }
+                form.innerHTML= '<input type="radio" name="offset" id="lines" value="lines"'+ lines_checked +'>' +
+                    '<label class="lines" for="lines">lines</label>' +
+                    '<br/><input type="radio" name="offset" id="stacked" value="stacked"' + stacked_checked + '>' +
+                    '<label class="stack" for="stack">stack</label>';
+
+                form.addEventListener('change', function(e) {
+                    var mode = e.target.value;
+                    options['state'] = mode;
+                    $.plot(div, all_targets, buildFlotOptions(options));
+                }, false);
+            }
+           function showTooltip(x, y, contents) {
+                $("<div id='tooltip_" + id + "'>" + contents + "</div>").css({
+                    position: "absolute",
+                    display: "none",
+                    top: y + 5,
+                    left: x + 5,
+                    border: "1px solid #fdd",
+                    padding: "2px",
+                    "background-color": "#fee",
+                    opacity: 0.80
+                }).appendTo("body").fadeIn(200);
+            }
+          var previousPoint = null;
+        $(div).bind("plothover", function (event, pos, item) {
+            if (item) {
+                if (previousPoint != item.dataIndex) {
+                    previousPoint = item.dataIndex;
+                    $("#tooltip_" + id).remove();
+                    var x = item.datapoint[0],
+                    y = item.datapoint[1].toFixed(2);
+                    var date = new Date(x);
+                    showTooltip(item.pageX, item.pageY,
+                        "Series: " + item.series.label +
+                        "<br/>Local Time: " + date.toLocaleString() +
+                        "<br/>UTC Time: " + date.toUTCString() + ")" +
+                        "<br/>Value: " + y);
+                }
+            } else {
+                $("#tooltip_" + id).remove();
+                previousPoint = null;
+            }
+        });
+
+        }
+        data = build_graphite_options(options, true);
+        var requests = [];
+        requests.push($.ajax({
+            accepts: {text: 'application/json'},
+            cache: false,
+            dataType: 'json',
+            url: options['graphite_url'],
+            type: "POST",
+            data: data.join('&'),
+            success: function(data, textStatus, jqXHR ) {
+                if(data.length == 0 ) {
+                    console.warn("no data in graphite response");
+                }
+                add_targets(data);
+            },
+            error: function(xhr, textStatus, errorThrown) {
+                on_error("Failed to do graphite POST request to " + truncate_str(options['graphite_url']) +
+                       ": " + textStatus + ": " + errorThrown);
+            }
+        }));
+        if('anthracite_url' in options){
+            anthracite_url = build_anthracite_url(options, true);
+            requests.push($.ajax({
+                accepts: {text: 'application/json'},
+                cache: false,
+                dataType: 'json',
+                url: anthracite_url,
+                success: function(data, textStatus, jqXHR ) {
+                    events = data.events;
+                },
+                error: function(xhr, textStatus, errorThrown) {
+                    on_error("Failed to do anthracite GET request to " + truncate_str(anthracite_url) +
+                           ": " + textStatus + ": " + errorThrown);
+                }
+            }));
+        }
+        if('es_url' in options){
+            requests.push($.ajax({
+                accepts: {text: 'application/json'},
+                cache: false,
+                dataType: 'json',
+                jsonp: 'json',
+                url: options['es_url'],
+                success: function(data, textStatus, jqXHR ) { es_events = data.hits.hits },
+                error: function(xhr, textStatus, errorThrown) {
+                    on_error("Failed to do elasticsearch request to " + truncate_str(options['es_url']) +
+                           ": " + textStatus + ": " + errorThrown);
+                }
+            }));
+        }
+
+        $.when.apply($, requests).done(drawFlot);
+    };
+
+    $.fn.graphiteRick.render = function(div, options, on_error) {
+        $div = $(div);
+        $div.attr("height", options.height);
+        $div.attr("width", options.width);
+        var drawRick = function(resp_graphite) {
+            // note that resp_graphite.length can be != options.targets.length.  let's call:
+            // * target_graphite a targetstring as returned by graphite
+            // * target_option a targetstring configuration
+            // if a target_option contains * graphite will return all matches separately unless you use something to aggregate like sumSeries()
+            // we must render all target_graphite's, but we must merge in the config from the corresponding target_option.
+            // example: for a target_graphite 'stats.foo.bar' we must find a target_option 'stats.foo.bar' *or*
+            // anything that causes graphite to match it, such as 'stats.*.bar' (this would be a bit cleaner if graphite's json
+            // would include also the originally specified target string)
+            // note that this code assumes each target_graphite can only be originating from one target_option,
+            // in some unlikely cases this is not correct (there might be overlap between different target_options with globs)
+            // but in that case I don't see why taking the settings of any of the possible originating target_options wouldn't be fine.
+            var all_targets = [];
+            if(resp_graphite.length == 0 ) {
+                console.warn("no data in graphite response");
+            }
+            for (var res_i = 0; res_i < resp_graphite.length; res_i++) {
+                var target = find_definition(resp_graphite[res_i], options);
+                target.data = [];
+                for (var i in resp_graphite[res_i].datapoints) {
+                    target.data[i] = { x: resp_graphite[res_i].datapoints[i][1], y: resp_graphite[res_i].datapoints[i][0] || 0 };
+                }
+                all_targets.push(target);
+            }
+            options['element'] = div;
+            options['series'] = all_targets
+            for (i = 0; i < options['targets'].length; i++ ) {
+                options['targets'][i]['color'] = options['targets'][i]['color'];
+            }
+            var graph = new Rickshaw.Graph(options);
+            if(options['x_axis']) {
+                var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } );
+            }
+            if(options['y_axis']) {
+                var y_axis = new Rickshaw.Graph.Axis.Y( {
+                    graph: graph,
+                    orientation: 'left',
+                    tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
+                    element: document.getElementById(options['y_axis']),
+                });
+            }
+            if(options['hover_details']) {
+                var hoverDetail = new Rickshaw.Graph.HoverDetail( {
+                    graph: graph
+                } );
+            }
+            var setRickshawOptions = function (options, graph) {
+                if ('state' in options && options['state'] == 'stacked') {
+                    graph.setRenderer('stack');
+                    graph.offset = 'zero';
+                }
+                else { // 'state' is lines
+                    graph.setRenderer('line');
+                    graph.offset = 'zero';
+                }
+                return graph;
+            }
+            graph = setRickshawOptions(options, graph);
+            graph.render();
+            if (options['legend']) {
+                var legend = new Rickshaw.Graph.Legend({
+                    graph: graph,
+                    element: document.getElementById(options['legend'])
+                });
+                if(options['legend_toggle']) {
+                    var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({
+                        graph: graph,
+                        legend: legend
+                    });
+                }
+                if(options['legend_reorder']) {
+                    var order = new Rickshaw.Graph.Behavior.Series.Order({
+                        graph: graph,
+                        legend: legend
+                    });
+                }
+                if(options['legend_highlight']) {
+                    var highlighter = new Rickshaw.Graph.Behavior.Series.Highlight({
+                    graph: graph,
+                    legend: legend
+                    });
+                }
+            }
+            if (options['line_stack_toggle']) {
+                var form = document.getElementById(options['line_stack_toggle']);
+                if(!options['renderer'] || options['renderer'] == 'area') {
+                    lines_checked = '';
+                    stack_checked = ' checked';
+                } else {
+                    lines_checked = ' checked';
+                    stack_checked = '';
+                }
+                form.innerHTML= '<input type="radio" name="mode" id="lines" value="lines"'+ lines_checked +'>' +
+                    '<label class="lines" for="lines">lines</label>' +
+                    '<br/><input type="radio" name="mode" id="stacked" value="stacked"' + stack_checked + '>' +
+                    '<label class="stack" for="stacked">stacked</label>';
+
+                form.addEventListener('change', function(e) {
+                    options['state'] = e.target.value;
+                    graph = setRickshawOptions(options, graph);
+                    graph.render();
+                }, false);
+            }
+        }
+        data = build_graphite_options(options, true);
+        $.ajax({
+            accepts: {text: 'application/json'},
+            cache: false,
+            dataType: 'json',
+            type: 'POST',
+            data: data.join('&'),
+            url: options['graphite_url'],
+            error: function(xhr, textStatus, errorThrown) {
+                on_error("Failed to do graphite POST request to " + truncate_str(options['graphite_url']) +
+                       ": " + textStatus + ": " + errorThrown);
+            }
+          }).done(drawRick);
+    };
+
+    $.fn.graphiteHighcharts.render = function(div, options, on_error) {
+        var id = div.getAttribute('id');
+        $div = $(div);
+        $div.height(options.height);
+        $div.width(options.width);
+        var drawHighcharts = function(resp_graphite) {
+            var hsoptions = {
+                chart: {
+                    renderTo: id,
+                    type: 'area',
+                    zoomType: 'xy',
+                    backgroundColor: options.bgcolor,
+                    animation: false
+                },
+                exporting: {
+                    enabled: false
+                },
+                credits: {
+                    enabled: false
+                },
+                legend: {
+                    borderWidth: 0,
+                    useHTML: true,
+                    itemHoverStyle: {
+                        color: 'red',
+                    },
+                    itemStyle: {
+                        color: options.fgcolor
+                    }
+                },
+                plotOptions: {
+                    line: {
+                        lineWidth: 0.8,
+                        marker: {
+                            enabled: false
+                        },
+                    },
+                    spline: {
+                        lineWidth: 0.8,
+                        marker: {
+                            enabled: false
+                        },
+                    },
+                    area: {
+                        stacking: 'normal',
+                        marker: {
+                            enabled: false
+                        },
+                        lineWidth: 0.8
+                    },
+                    areaspline: {
+                        stacking: 'normal',
+                        marker: {
+                            enabled: false
+                        },
+                        lineWidth: 0.8
+                    }
+                },
+                title: {
+                    text: options.title,
+                    style: {
+                        color: options.fgcolor
+                    }
+                },
+                xAxis: {
+                    type: 'datetime',
+                    tickPixelInterval: 50,
+                    labels: {
+                        rotation: -45,
+                        align: 'right'
+                    },
+                    lineColor: '#777',
+                    tickColor: '#777',
+                    maxPadding: 0.01,
+                    minPadding: 0.01,
+                    gridLineWidth: 0.2
+                },
+                yAxis: {
+                    gridLineColor: 'rgba(255, 255, 255, .3)',
+                    minorGridLineColor: 'rgba(255,255,255,0.1)',
+                    title: {
+                        text: options.vtitle,
+                        useHTML: true
+                    },
+                    maxPadding: 0.01,
+                    minPadding: 0.01
+                },
+                tooltip: {
+                    enabled: options.hover_details,
+                    crosshairs:[{width:1, color:'#ccc'},{width:1, color:'#ccc'}],
+                    borderWidth: 0,
+                    backgroundColor: {
+                        linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
+                        stops: [
+                            [0, 'rgba(96, 96, 96, .8)'],
+                            [1, 'rgba(16, 16, 16, .8)']
+                        ]
+                    },
+                    style: {
+                        color: '#FFF'
+                    },
+                    useHTML: true,
+                    formatter: function() {
+                        return "Series: " + this.series.name +
+                               "<br/>Local Time: " + Highcharts.dateFormat('%A %B %e %Y %H:%M', this.x) +
+                               "<br/>Value: " + Highcharts.numberFormat(this.y, 2);
+                    }
+                },
+                series: []
+            }; 
+            for (var res_i = 0; res_i < resp_graphite.length; res_i++) {
+                var target = find_definition(resp_graphite[res_i], options);
+                var hstarget = {
+                    data: [],
+                    events: {
+                        click: function() {
+                            var q;
+                            if($.isArray(this.graphite_metric)) {
+                                q = '^' + this.options.graphite_metric.join('$|^') + '$';
+                            } else {
+                                q = '^' + this.options.graphite_metric + '$';
+                            }
+                            window.location = "/inspect/" + q;
+                        }
+                    },
+                    type: "line",
+                    animation: false
+                };
+                if (options.legend && options.legend.labelFormatter) {
+                    hstarget.name = options.legend.labelFormatter(target.name);
+                }
+                hstarget.graphite_metric = target.graphite_metric;
+                if (options["series"] && options["series"].stack) {
+                    hstarget.type = "area";
+                }
+                for (var i in resp_graphite[res_i].datapoints) {
+                    hstarget.data.push([
+                        resp_graphite[res_i].datapoints[i][1] * 1000,
+                        resp_graphite[res_i].datapoints[i][0]
+                    ]);
+                }
+                hsoptions.series.push(hstarget)
+            }
+
+            var hschart = new Highcharts.Chart(hsoptions);
+            if (options['line_stack_toggle'])
+            {
+                var form = document.getElementById(options['line_stack_toggle']);
+                var optionshtml = '';
+
+                if (options["series"] && options["series"].stack)
+                {
+                    optionshtml += '<option value="stack">stack</option>';
+                    optionshtml += '<option value="line">lines</option>';
+                } else {
+                    optionshtml += '<option value="line">lines</option>';
+                    optionshtml += '<option value="stack">stack</option>';
+                }
+                form.innerHTML = '<select>' + optionshtml + '</select>';
+
+                $("select", form).change(function() {
+                    for (var i in hsoptions.series) {
+                        var series = hsoptions.series[i];
+                        series.stack = i;
+                        if (this.value == "stack") {
+                            series.type = "area";
+                            series.stack = 1;
+                        } else {
+                            series.type = this.value;
+                        }
+                    }
+                    hschart = new Highcharts.Chart(hsoptions);
+                });
+            }
+        };
+        data = build_graphite_options(options, true);
+        $.ajax({
+            accepts: {text: 'application/json'},
+            cache: false,
+            dataType: 'json',
+            url: options['graphite_url'],
+            type: "POST",
+            data: data.join('&'),
+            error: function(xhr, textStatus, errorThrown) {
+                on_error("Failed to do graphite POST request to " + truncate_str(options['graphite_url']) +
+                       ": " + textStatus + ": " + errorThrown);
+            }
+        }).done(drawHighcharts);
+    };
+    // Default settings. 
+    // Override with the options argument for per-case setup
+    // or set $.fn.graphite.defaults.<value> for global changes
+    $.fn.graphite.defaults = {
+        from: "-1hour",
+        height: "300",
+        until: "now",
+        graphite_url: "/render/",
+        width: "940"
+    };
+
+}(jQuery));

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