From 3490099bdf267755cf80e7084a45aaf273e42bd7 Mon Sep 17 00:00:00 2001 From: tvvmb Date: Fri, 6 Jun 2014 13:26:29 +0200 Subject: [PATCH 1/8] add multifieldhistogram panel --- .../panels/multifieldhistogram/editor.html | 100 +++ .../panels/multifieldhistogram/interval.js | 57 ++ src/app/panels/multifieldhistogram/meta.html | 22 + .../panels/multifieldhistogram/module.html | 108 +++ src/app/panels/multifieldhistogram/module.js | 829 ++++++++++++++++++ .../multifieldhistogram/queriesEditor.html | 39 + .../multifieldhistogram/styleEditor.html | 88 ++ .../panels/multifieldhistogram/timeSeries.js | 235 +++++ src/config.js | 1 + 9 files changed, 1479 insertions(+) create mode 100644 src/app/panels/multifieldhistogram/editor.html create mode 100644 src/app/panels/multifieldhistogram/interval.js create mode 100644 src/app/panels/multifieldhistogram/meta.html create mode 100644 src/app/panels/multifieldhistogram/module.html create mode 100644 src/app/panels/multifieldhistogram/module.js create mode 100644 src/app/panels/multifieldhistogram/queriesEditor.html create mode 100644 src/app/panels/multifieldhistogram/styleEditor.html create mode 100644 src/app/panels/multifieldhistogram/timeSeries.js diff --git a/src/app/panels/multifieldhistogram/editor.html b/src/app/panels/multifieldhistogram/editor.html new file mode 100644 index 00000000000000..80dc958318cfe7 --- /dev/null +++ b/src/app/panels/multifieldhistogram/editor.html @@ -0,0 +1,100 @@ + +
+
+
Values
+ + + + + + + + + + + + + + + + + + + +
Chart valueTransform SeriesQueriesDelete
+ + +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + {{querySrv.list()[id].alias || querySrv.list()[id].query}} + +
+
+ + +
+ +
+
+
+
+
Time Options
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
diff --git a/src/app/panels/multifieldhistogram/interval.js b/src/app/panels/multifieldhistogram/interval.js new file mode 100644 index 00000000000000..caf2c61949cf18 --- /dev/null +++ b/src/app/panels/multifieldhistogram/interval.js @@ -0,0 +1,57 @@ +define([ + 'kbn' +], +function (kbn) { + 'use strict'; + + /** + * manages the interval logic + * @param {[type]} interval_string An interval string in the format '1m', '1y', etc + */ + function Interval(interval_string) { + this.string = interval_string; + + var info = kbn.describe_interval(interval_string); + this.type = info.type; + this.ms = Math.ceil(info.sec * 1000 * info.count); + + // does the length of the interval change based on the current time? + if (this.type === 'y' || this.type === 'M') { + // we will just modify this time object rather that create a new one constantly + this.get = this.get_complex; + this.date = new Date(0); + } else { + this.get = this.get_simple; + } + } + + Interval.prototype = { + toString: function () { + return this.string; + }, + after: function(current_ms) { + return this.get(current_ms, 1); + }, + before: function (current_ms) { + return this.get(current_ms, -1); + }, + get_complex: function (current, delta) { + this.date.setTime(current); + switch(this.type) { + case 'M': + this.date.setUTCMonth(this.date.getUTCMonth() + delta); + break; + case 'y': + this.date.setUTCFullYear(this.date.getUTCFullYear() + delta); + break; + } + return this.date.getTime(); + }, + get_simple: function (current, delta) { + return current + (delta * this.ms); + } + }; + + return Interval; + +}); \ No newline at end of file diff --git a/src/app/panels/multifieldhistogram/meta.html b/src/app/panels/multifieldhistogram/meta.html new file mode 100644 index 00000000000000..ce6599b01da251 --- /dev/null +++ b/src/app/panels/multifieldhistogram/meta.html @@ -0,0 +1,22 @@ +
+ + +
+ + +
+ +
+
+ +
+ +
+ Remove color + +
+
diff --git a/src/app/panels/multifieldhistogram/module.html b/src/app/panels/multifieldhistogram/module.html new file mode 100644 index 00000000000000..3239b43ce43cff --- /dev/null +++ b/src/app/panels/multifieldhistogram/module.html @@ -0,0 +1,108 @@ +
+ +
+ + + View + |  + + + + Zoom Out |  + + + + + {{series.query.alias || series.query.query}} + {{series.query.alias}} + ({{series.hits}}) + + + change in {{panel.value_field}} {{panel.mode}} per {{panel.interval}}1s | ({{hits}} hits) +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + + +
+
+
+
diff --git a/src/app/panels/multifieldhistogram/module.js b/src/app/panels/multifieldhistogram/module.js new file mode 100644 index 00000000000000..e26c130c918b5b --- /dev/null +++ b/src/app/panels/multifieldhistogram/module.js @@ -0,0 +1,829 @@ +/** @scratch /panels/5 + * + * include::panels/histogram.asciidoc[] + */ + +/** @scratch /panels/histogram/0 + * + * == Histogram + * Status: *Stable* + * + * The histogram panel allow for the display of time charts. It includes several modes and tranformations + * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter + * fields. + * + */ +define([ + 'angular', + 'app', + 'jquery', + 'lodash', + 'kbn', + 'moment', + './timeSeries', + 'numeral', + 'jquery.flot', + 'jquery.flot.events', + 'jquery.flot.selection', + 'jquery.flot.time', + 'jquery.flot.byte', + 'jquery.flot.stack', + 'jquery.flot.stackpercent' +], +function (angular, app, $, _, kbn, moment, timeSeries, numeral) { + + 'use strict'; + + var module = angular.module('kibana.panels.multifieldhistogram', []); + app.useModule(module); + + module.controller('multifieldhistogram', function($scope, querySrv, dashboard, filterSrv) { + $scope.panelMeta = { + modals : [ + { + description: "Inspect", + icon: "icon-info-sign", + partial: "app/partials/inspector.html", + show: $scope.panel.spyable + } + ], + editorTabs : [ + { + title:'Style', + src:'app/panels/multifieldhistogram/styleEditor.html' + }, + { + title:'Queries', + src:'app/panels/multifieldhistogram/queriesEditor.html' + }, + ], + status : "Stable", + description : "A bucketed time series chart of the current query or queries. Uses the "+ + "Elasticsearch date_histogram facet. If using time stamped indices this panel will query"+ + " them sequentially to attempt to apply the lighest possible load to your Elasticsearch cluster" + }; + + // Set and populate defaults + $scope.defaultValue = { + /** mode:: Value to use for the y-axis. For all modes other than count, +value_field+ must be + * defined. Possible values: count, mean, max, min, total. + */ + mode : 'count', + /** @scratch /panels/histogram/3 + * value_field:: y-axis field if +mode+ is set to mean, max, min or total. Must be numeric. + */ + value_field : null, + /** @scratch /panels/histogram/3 + * scale:: Scale the y-axis by this factor + */ + scale : 1, + /** @scratch /panels/histogram/3 + * zerofill:: Improves the accuracy of line charts at a small performance cost. + */ + zerofill : true, + /** @scratch /panels/histogram/3 + * derivative:: Show each point on the x-axis as the change from the previous point + */ + derivative : false, + /** @scratch /panels/histogram/5 + * queries array:: which query ids are selected. + */ + queries : querySrv.ids(), + color : null, + alias : null + }; + + var _d = { + /** @scratch /panels/histogram/3 + * + * === Parameters + * ==== Axis options + * + */ + /** @scratch /panels/histogram/3 + * values:: array of values. + */ + values : [angular.copy($scope.defaultValue)], + /** @scratch /panels/histogram/3 + * time_field:: x-axis field. This must be defined as a date type in Elasticsearch. + */ + time_field : '@timestamp', + /** @scratch /panels/histogram/3 + * x-axis:: Show the x-axis + */ + 'x-axis' : true, + /** @scratch /panels/histogram/3 + * y-axis:: Show the y-axis + */ + 'y-axis' : true, + /** @scratch /panels/histogram/3 + * y_format:: 'none','bytes','short ' + */ + y_format : 'none', + /** @scratch /panels/histogram/5 + * grid object:: Min and max y-axis values + * grid.min::: Minimum y-axis value + * grid.max::: Maximum y-axis value + */ + grid : { + max: null, + min: 0 + }, + /** @scratch /panels/histogram/3 + * + * ==== Annotations + * annotate object:: A query can be specified, the results of which will be displayed as markers on + * the chart. For example, for noting code deploys. + * annotate.enable::: Should annotations, aka markers, be shown? + * annotate.query::: Lucene query_string syntax query to use for markers. + * annotate.size::: Max number of markers to show + * annotate.field::: Field from documents to show + * annotate.sort::: Sort array in format [field,order], For example [`@timestamp',`desc'] + */ + annotate : { + enable : false, + query : "*", + size : 20, + field : '_type', + sort : ['_score','desc'] + }, + /** @scratch /panels/histogram/3 + * ==== Interval options + * auto_int:: Automatically scale intervals? + */ + auto_int : true, + /** @scratch /panels/histogram/3 + * resolution:: If auto_int is true, shoot for this many bars. + */ + resolution : 100, + /** @scratch /panels/histogram/3 + * interval:: If auto_int is set to false, use this as the interval. + */ + interval : '5m', + /** @scratch /panels/histogram/3 + * interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h'] + */ + intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'], + /** @scratch /panels/histogram/3 + * ==== Drawing options + * lines:: Show line chart + */ + lines : false, + /** @scratch /panels/histogram/3 + * fill:: Area fill factor for line charts, 1-10 + */ + fill : 0, + /** @scratch /panels/histogram/3 + * linewidth:: Weight of lines in pixels + */ + linewidth : 3, + /** @scratch /panels/histogram/3 + * points:: Show points on chart + */ + points : false, + /** @scratch /panels/histogram/3 + * pointradius:: Size of points in pixels + */ + pointradius : 5, + /** @scratch /panels/histogram/3 + * bars:: Show bars on chart + */ + bars : true, + /** @scratch /panels/histogram/3 + * stack:: Stack multiple series + */ + stack : true, + /** @scratch /panels/histogram/3 + * spyable:: Show inspect icon + */ + spyable : true, + /** @scratch /panels/histogram/3 + * zoomlinks:: Show `Zoom Out' link + */ + zoomlinks : true, + /** @scratch /panels/histogram/3 + * options:: Show quick view options section + */ + options : true, + /** @scratch /panels/histogram/3 + * legend:: Display the legond + */ + legend : true, + /** @scratch /panels/histogram/3 + * show_query:: If no alias is set, should the query be displayed? + */ + show_query : true, + /** @scratch /panels/histogram/3 + * interactive:: Enable click-and-drag to zoom functionality + */ + interactive : true, + /** @scratch /panels/histogram/3 + * legend_counts:: Show counts in legend + */ + legend_counts : true, + /** @scratch /panels/histogram/3 + * ==== Transformations + * timezone:: Correct for browser timezone?. Valid values: browser, utc + */ + timezone : 'browser', // browser or utc + /** @scratch /panels/histogram/3 + * percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple + * queries + */ + percentage : false, + /** @scratch /panels/histogram/3 + * tooltip object:: + * tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts + * tooltip.query_as_alias::: If no alias is set, should the query be displayed? + */ + tooltip : { + value_type: 'cumulative', + query_as_alias: true + } + }; + + _.defaults($scope.panel,_d); + _.defaults($scope.panel.tooltip,_d.tooltip); + _.defaults($scope.panel.annotate,_d.annotate); + _.defaults($scope.panel.grid,_d.grid); + + + + $scope.init = function() { + // Hide view options by default + $scope.options = false; + + // Always show the query if an alias isn't set. Users can set an alias if the query is too + // long + $scope.panel.tooltip.query_as_alias = true; + + $scope.get_data(); + + }; + + $scope.set_interval = function(interval) { + if(interval !== 'auto') { + $scope.panel.auto_int = false; + $scope.panel.interval = interval; + } else { + $scope.panel.auto_int = true; + } + }; + + $scope.interval_label = function(interval) { + return $scope.panel.auto_int && interval === $scope.panel.interval ? interval+" (auto)" : interval; + }; + + /** + * The time range effecting the panel + * @return {[type]} [description] + */ + $scope.get_time_range = function () { + var range = $scope.range = filterSrv.timeRange('last'); + return range; + }; + + $scope.get_interval = function () { + var interval = $scope.panel.interval, + range; + if ($scope.panel.auto_int) { + range = $scope.get_time_range(); + if (range) { + interval = kbn.secondsToHms( + kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000 + ); + } + } + $scope.panel.interval = interval || '10m'; + return $scope.panel.interval; + }; + + /** + * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies + * need to be consulted (like timestamped logstash indicies) + * + * The results of this function are stored on the scope's data property. This property will be an + * array of objects with the properties info, time_series, and hits. These objects are used in the + * render_panel function to create the historgram. + * + * @param {number} segment The segment count, (0 based) + * @param {number} query_id The id of the query, generated on the first run and passed back when + * this call is made recursively for more segments + */ + $scope.get_data = function(data, segment, query_id) { + var + _range, + _interval, + request, + queries, + results; + + if (_.isUndefined(segment)) { + segment = 0; + } + delete $scope.panel.error; + + // Make sure we have everything for the request to complete + if(dashboard.indices.length === 0) { + return; + } + _range = $scope.get_time_range(); + _interval = $scope.get_interval(_range); + + if ($scope.panel.auto_int) { + $scope.panel.interval = kbn.secondsToHms( + kbn.calculate_interval(_range.from,_range.to,$scope.panel.resolution,0)/1000); + } + + $scope.panelMeta.loading = true; + request = $scope.ejs.Request().indices(dashboard.indices[segment]); + if (!$scope.panel.annotate.enable) { + request.searchType("count"); + } + + // Build the queries + _.each($scope.panel.values, function(panel_value, panel_value_index) { + queries = querySrv.getQueryObjs(panel_value.queries); + _.each(queries, function(q) { + var serie_id = panel_value_index.toString()+"_"+q.id.toString(); + var query = $scope.ejs.FilteredQuery( + querySrv.toEjsObj(q), + filterSrv.getBoolFilter(filterSrv.ids()) + ); + + var facet = $scope.ejs.DateHistogramFacet(serie_id); + + if(panel_value.mode === 'count') { + facet = facet.field($scope.panel.time_field).global(true); + } else { + if(_.isNull(panel_value.value_field)) { + $scope.panel.error = "In " + panel_value.mode + " mode a field must be specified"; + return; + } + facet = facet.keyField($scope.panel.time_field).valueField(panel_value.value_field).global(true); + } + facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query)); + request = request.facet(facet) + .size($scope.panel.annotate.enable ? $scope.panel.annotate.size : 0); + }); + }); + + // Annotate query + if($scope.panel.annotate.enable) { + var query = $scope.ejs.FilteredQuery( + $scope.ejs.QueryStringQuery($scope.panel.annotate.query || '*'), + filterSrv.getBoolFilter(filterSrv.idsByType('time')) + ); + request = request.query(query); + + // This is a hack proposed by @boaz to work around the fact that we can't get + // to field data values directly, and we need timestamps as normalized longs + request = request.sort([ + $scope.ejs.Sort($scope.panel.annotate.sort[0]).order($scope.panel.annotate.sort[1]).ignoreUnmapped(true), + $scope.ejs.Sort($scope.panel.time_field).desc().ignoreUnmapped(true) + ]); + } + + // Populate the inspector panel + $scope.populate_modal(request); + + // Then run it + results = request.doSearch(); + + // Populate scope when we have results + return results.then(function(results) { + $scope.panelMeta.loading = false; + if(segment === 0) { + $scope.legend = {}; + $scope.hits = 0; + data = {}; + $scope.annotations = []; + query_id = $scope.query_id = new Date().getTime(); + } + + // Check for error and abort if found + if(!(_.isUndefined(results.error))) { + $scope.panel.error = $scope.parse_error(results.error); + } + // Make sure we're still on the same query/queries + else if($scope.query_id === query_id) { + + var time_series, + hits, + counters; // Stores the bucketed hit counts. + + _.each($scope.panel.values, function(panel_value, panel_value_index) { + queries = querySrv.getQueryObjs(panel_value.queries); + _.each(queries, function(q) { + var serie_id = panel_value_index.toString()+"_"+q.id.toString(); + var query_results = results.facets[serie_id]; + // we need to initialize the data variable on the first run, + // and when we are working on the first segment of the data. + if(_.isUndefined(data[serie_id]) || segment === 0) { + var tsOpts = { + interval: _interval, + start_date: _range && _range.from, + end_date: _range && _range.to, + fill_style: panel_value.derivative ? 'null' : panel_value.zerofill ? 'minimal' : 'no' + }; + time_series = new timeSeries.ZeroFilled(tsOpts); + hits = 0; + counters = {}; + } else { + time_series = data[serie_id].time_series; + hits = data[serie_id].hits; + counters = data[serie_id].counters; + } + + // push each entry into the time series, while incrementing counters + _.each(query_results.entries, function(entry) { + var value; + + hits += entry.count; // The series level hits counter + $scope.hits += entry.count; // Entire dataset level hits counter + counters[entry.time] = (counters[entry.time] || 0) + entry.count; + + if(panel_value.mode === 'count') { + value = (time_series._data[entry.time] || 0) + entry.count; + } else if (panel_value.mode === 'mean') { + // Compute the ongoing mean by + // multiplying the existing mean by the existing hits + // plus the new mean multiplied by the new hits + // divided by the total hits + value = (((time_series._data[entry.time] || 0)*(counters[entry.time]-entry.count)) + + entry.mean*entry.count)/(counters[entry.time]); + } else if (panel_value.mode === 'min'){ + if(_.isUndefined(time_series._data[entry.time])) { + value = entry.min; + } else { + value = time_series._data[entry.time] < entry.min ? time_series._data[entry.time] : entry.min; + } + } else if (panel_value.mode === 'max'){ + if(_.isUndefined(time_series._data[entry.time])) { + value = entry.max; + } else { + value = time_series._data[entry.time] > entry.max ? time_series._data[entry.time] : entry.max; + } + } else if (panel_value.mode === 'total'){ + value = (time_series._data[entry.time] || 0) + entry.total; + } + time_series.addValue(entry.time, value); + }); + + var info = { + color: panel_value.color || q.color, + alias: panel_value.alias || q.alias || q.query, + }; + + $scope.legend[serie_id] = {query:info,hits:hits}; + + data[serie_id] = { + info: info, + time_series: time_series, + hits: hits, + counters: counters + }; + }); + }); + + if($scope.panel.annotate.enable) { + $scope.annotations = $scope.annotations.concat(_.map(results.hits.hits, function(hit) { + var _p = _.omit(hit,'_source','sort','_score'); + var _h = _.extend(kbn.flatten_json(hit._source),_p); + return { + min: hit.sort[1], + max: hit.sort[1], + eventType: "annotation", + title: null, + description: " "+ + _h[$scope.panel.annotate.field]+"
"+ + moment(hit.sort[1]).format('YYYY-MM-DD HH:mm:ss'), + score: hit.sort[0] + }; + })); + // Sort the data + $scope.annotations = _.sortBy($scope.annotations, function(v){ + // Sort in reverse + return v.score*($scope.panel.annotate.sort[1] === 'desc' ? -1 : 1); + }); + // And slice to the right size + $scope.annotations = $scope.annotations.slice(0,$scope.panel.annotate.size); + } + } + + // Tell the histogram directive to render. + $scope.$emit('render', data); + + // If we still have segments left, get them + if(segment < dashboard.indices.length-1) { + $scope.get_data(data,segment+1,query_id); + } + + }); + + }; + + // function $scope.zoom + // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan + $scope.zoom = function(factor) { + var _range = filterSrv.timeRange('last'); + var _timespan = (_range.to.valueOf() - _range.from.valueOf()); + var _center = _range.to.valueOf() - _timespan/2; + + var _to = (_center + (_timespan*factor)/2); + var _from = (_center - (_timespan*factor)/2); + + // If we're not already looking into the future, don't. + if(_to > Date.now() && _range.to < Date.now()) { + var _offset = _to - Date.now(); + _from = _from - _offset; + _to = Date.now(); + } + + if(factor > 1) { + filterSrv.removeByType('time'); + } + filterSrv.set({ + type:'time', + from:moment.utc(_from).toDate(), + to:moment.utc(_to).toDate(), + field:$scope.panel.time_field + }); + }; + + // I really don't like this function, too much dom manip. Break out into directive? + $scope.populate_modal = function(request) { + $scope.inspector = angular.toJson(JSON.parse(request.toString()),true); + }; + + $scope.set_refresh = function (state) { + $scope.refresh = state; + }; + + $scope.close_edit = function() { + if($scope.refresh) { + $scope.get_data(); + } + $scope.refresh = false; + $scope.$emit('render'); + }; + + $scope.render = function() { + $scope.$emit('render'); + }; + + $scope.add_new_value = function(panel) { + panel.values.push(angular.copy($scope.defaultValue)); + }; + + }); + + module.directive('histogramChart', function(dashboard, filterSrv) { + return { + restrict: 'A', + template: '
', + link: function(scope, elem) { + var data, plot; + + scope.$on('refresh',function(){ + scope.get_data(); + }); + + // Receive render events + scope.$on('render',function(event,d){ + data = d || data; + render_panel(_.values(data)); + }); + + var scale = function(series,factor) { + return _.map(series,function(p) { + return [p[0],p[1]*factor]; + }); + }; + + var scaleSeconds = function(series,interval) { + return _.map(series,function(p) { + return [p[0],p[1]/kbn.interval_to_seconds(interval)]; + }); + }; + + var derivative = function(series) { + return _.map(series, function(p,i) { + var _v; + if(i === 0 || p[1] === null) { + _v = [p[0],null]; + } else { + _v = series[i-1][1] === null ? [p[0],null] : [p[0],p[1]-(series[i-1][1])]; + } + return _v; + }); + }; + + // Function for rendering panel + function render_panel(data) { + // IE doesn't work without this + try { + elem.css({height:scope.panel.height||scope.row.height}); + } catch(e) {return;} + + // Populate from the query service + try { + _.each(data, function(series) { + series.label = series.info.alias; + series.color = series.info.color; + }); + } catch(e) {return;} + + // Set barwidth based on specified interval + var barwidth = kbn.interval_to_ms(scope.panel.interval); + + var stack = scope.panel.stack ? true : null; + + // Populate element + try { + var options = { + legend: { show: false }, + series: { + stackpercent: scope.panel.stack ? scope.panel.percentage : false, + stack: scope.panel.percentage ? null : stack, + lines: { + show: scope.panel.lines, + // Silly, but fixes bug in stacked percentages + fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10, + lineWidth: scope.panel.linewidth, + steps: false + }, + bars: { + show: scope.panel.bars, + fill: 1, + barWidth: barwidth/1.5, + zero: false, + lineWidth: 0 + }, + points: { + show: scope.panel.points, + fill: 1, + fillColor: false, + radius: scope.panel.pointradius + }, + shadowSize: 1 + }, + yaxis: { + show: scope.panel['y-axis'], + min: scope.panel.grid.min, + max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max + }, + xaxis: { + timezone: scope.panel.timezone, + show: scope.panel['x-axis'], + mode: "time", + min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(), + max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(), + timeformat: time_format(scope.panel.interval), + label: "Datetime", + ticks: elem.width()/100 + }, + grid: { + backgroundColor: null, + borderWidth: 0, + hoverable: true, + color: '#c8c8c8' + } + }; + + if (scope.panel.y_format === 'bytes') { + options.yaxis.mode = "byte"; + options.yaxis.tickFormatter = function (val, axis) { + return kbn.byteFormat(val, 0, axis.tickSize); + }; + } + + if (scope.panel.y_format === 'short') { + options.yaxis.tickFormatter = function (val, axis) { + return kbn.shortFormat(val, 0, axis.tickSize); + }; + } + + if(scope.panel.annotate.enable) { + options.events = { + clustering: true, + levels: 1, + data: scope.annotations, + types: { + 'annotation': { + level: 1, + icon: { + width: 20, + height: 21, + icon: "histogram-marker" + } + } + } + //xaxis: int // the x axis to attach events to + }; + } + + if(scope.panel.interactive) { + options.selection = { mode: "x", color: '#666' }; + } + + // when rendering stacked bars, we need to ensure each point that has data is zero-filled + // so that the stacking happens in the proper order + var required_times = []; + if (data.length > 1) { + required_times = Array.prototype.concat.apply([], _.map(data, function (query) { + return query.time_series.getOrderedTimes(); + })); + required_times = _.uniq(required_times.sort(function (a, b) { + // decending numeric sort + return a-b; + }), true); + } + + + for (var i = 0; i < data.length; i++) { + var _d = data[i].time_series.getFlotPairs(required_times); + if(scope.panel.values[0].derivative) { + _d = derivative(_d); + } + if(scope.panel.values[0].scale !== 1) { + _d = scale(_d,scope.panel.values[0].scale); + } + if(scope.panel.scaleSeconds) { + _d = scaleSeconds(_d,scope.panel.interval); + } + data[i].data = _d; + } + + plot = $.plot(elem, data, options); + + } catch(e) { + // Nothing to do here + } + } + + function time_format(interval) { + var _int = kbn.interval_to_seconds(interval); + if(_int >= 2628000) { + return "%Y-%m"; + } + if(_int >= 86400) { + return "%Y-%m-%d"; + } + if(_int >= 60) { + return "%H:%M
%m-%d"; + } + + return "%H:%M:%S"; + } + + var $tooltip = $('
'); + elem.bind("plothover", function (event, pos, item) { + var group, value, timestamp, interval; + interval = " per " + (scope.panel.scaleSeconds ? '1s' : scope.panel.interval); + if (item) { + if (item.series.info.alias || scope.panel.tooltip.query_as_alias) { + group = '' + + '' + ' ' + + (item.series.info.alias || item.series.info.query)+ + '
'; + } else { + group = kbn.query_color_dot(item.series.color, 15) + ' '; + } + value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ? + item.datapoint[1] - item.datapoint[2] : + item.datapoint[1]; + if(scope.panel.y_format === 'bytes') { + value = kbn.byteFormat(value,2); + } + if(scope.panel.y_format === 'short') { + value = kbn.shortFormat(value,2); + } else { + value = numeral(value).format('0,0[.]000'); + } + timestamp = scope.panel.timezone === 'browser' ? + moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') : + moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss'); + $tooltip + .html( + group + value + interval + " @ " + timestamp + ) + .place_tt(pos.pageX, pos.pageY); + } else { + $tooltip.detach(); + } + }); + + elem.bind("plotselected", function (event, ranges) { + filterSrv.set({ + type : 'time', + from : moment.utc(ranges.xaxis.from).toDate(), + to : moment.utc(ranges.xaxis.to).toDate(), + field : scope.panel.time_field + }); + }); + } + }; + }); + +}); diff --git a/src/app/panels/multifieldhistogram/queriesEditor.html b/src/app/panels/multifieldhistogram/queriesEditor.html new file mode 100644 index 00000000000000..18ff0272120b87 --- /dev/null +++ b/src/app/panels/multifieldhistogram/queriesEditor.html @@ -0,0 +1,39 @@ +
+

Markers

+
+ Here you can specify a query to be plotted on your chart as a marker. Hovering over a marker will display the field you specify below. If more documents are found than the limit you set, they will be scored by Elasticsearch and events that best match your query will be displayed. +
+ +

+

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
diff --git a/src/app/panels/multifieldhistogram/styleEditor.html b/src/app/panels/multifieldhistogram/styleEditor.html new file mode 100644 index 00000000000000..874a22640b5ae3 --- /dev/null +++ b/src/app/panels/multifieldhistogram/styleEditor.html @@ -0,0 +1,88 @@ +
+
+
Chart Options
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Multiple Series
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+
+
Header
+
+ +
+
+ +
+
+
+
Legend
+
+ +
+
+ +
+
+ +
+
+ +
+
Grid
+
+ + +
+
+ + +
+
+ +
diff --git a/src/app/panels/multifieldhistogram/timeSeries.js b/src/app/panels/multifieldhistogram/timeSeries.js new file mode 100644 index 00000000000000..bce065e640c323 --- /dev/null +++ b/src/app/panels/multifieldhistogram/timeSeries.js @@ -0,0 +1,235 @@ +define([ + './interval', + 'lodash' +], +function (Interval, _) { + 'use strict'; + + var ts = {}; + + // map compatable parseInt + function base10Int(val) { + return parseInt(val, 10); + } + + // trim the ms off of a time, but return it with empty ms. + function getDatesTime(date) { + return Math.floor(date.getTime() / 1000)*1000; + } + + /** + * Certain graphs require 0 entries to be specified for them to render + * properly (like the line graph). So with this we will caluclate all of + * the expected time measurements, and fill the missing ones in with 0 + * @param {object} opts An object specifying some/all of the options + * + * OPTIONS: + * @opt {string} interval The interval notion describing the expected spacing between + * each data point. + * @opt {date} start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt {date} end_date (optional) The end point for the time series, see start_date + * @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. + */ + ts.ZeroFilled = function (opts) { + opts = _.defaults(opts, { + interval: '10m', + start_date: null, + end_date: null, + fill_style: 'minimal' + }); + + // the expected differenece between readings. + this.interval = new Interval(opts.interval); + + // will keep all values here, keyed by their time + this._data = {}; + // For each bucket in _data, store a corresponding counter of how many times it was written to. + this._counters = {}; + this.start_time = opts.start_date && getDatesTime(opts.start_date); + this.end_time = opts.end_date && getDatesTime(opts.end_date); + this.opts = opts; + }; + + /** + * Add a row + * @param {int} time The time for the value, in + * @param {any} value The value at this time + */ + ts.ZeroFilled.prototype.addValue = function (time, value) { + this._counters[time] = (this._counters[time] || 0) + 1; + if (time instanceof Date) { + time = getDatesTime(time); + } else { + time = base10Int(time); + } + if (!isNaN(time)) { + this._data[time] = (_.isUndefined(value) ? 0 : value); + } + this._cached_times = null; + }; + + /** + * Get an array of the times that have been explicitly set in the series + * @param {array} include (optional) list of timestamps to include in the response + * @return {array} An array of integer times. + */ + ts.ZeroFilled.prototype.getOrderedTimes = function (include) { + var times = _.map(_.keys(this._data), base10Int); + if (_.isArray(include)) { + times = times.concat(include); + } + return _.uniq(times.sort(function (a, b) { + // decending numeric sort + return a - b; + }), true); + }; + + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * + * Heavy lifting is done by _get(Min|Default|All)FlotPairs() + * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @return {array} + */ + ts.ZeroFilled.prototype.getFlotPairs = function (required_times) { + var times = this.getOrderedTimes(required_times), + strategy, + pairs; + + if(this.opts.fill_style === 'all') { + strategy = this._getAllFlotPairs; + } else if(this.opts.fill_style === 'null') { + strategy = this._getNullFlotPairs; + } else if(this.opts.fill_style === 'no') { + strategy = this._getNoZeroFlotPairs; + } else { + strategy = this._getMinFlotPairs; + } + + pairs = _.reduce( + times, // what + strategy, // how + [], // where + this // context + ); + + // if the first or last pair is inside either the start or end time, + // add those times to the series with null values so the graph will stretch to contain them. + // Removing, flot 0.8.1's max/min params satisfy this + /* + if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) { + pairs.unshift([this.start_time, null]); + } + if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) { + pairs.push([this.end_time, null]); + } + */ + + return pairs; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's on either side of the current time, unless there is already a measurement there or + * we are looking at an edge. + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = this.interval.before(time); + if (prev < expected_prev) { + result.push([expected_prev, 0]); + } + } + + // add the current time + result.push([ time, this._data[time] || 0]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = this.interval.after(time); + if (next > expected_next) { + result.push([expected_next, 0]); + } + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's to the right of each time, until the next measurement is reached or we are at the + * last measurement + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) { + var next, expected_next; + + result.push([ times[i], this._data[times[i]] || 0 ]); + next = times[i + 1]; + expected_next = this.interval.after(time); + for(; times.length > i && next > expected_next; expected_next = this.interval.after(expected_next)) { + result.push([expected_next, 0]); + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Same as min, but fills with nulls + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getNullFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = this.interval.before(time); + if (prev < expected_prev) { + result.push([expected_prev, null]); + } + } + + // add the current time + result.push([ time, this._data[time] || null]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = this.interval.after(time); + if (next > expected_next) { + result.push([expected_next, null]); + } + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Not fill zero's on either side of the current time, only the current time + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getNoZeroFlotPairs = function (result, time) { + + // add the current time + if(this._data[time]){ + result.push([ time, this._data[time]]); + } + + return result; + }; + + return ts; +}); \ No newline at end of file diff --git a/src/config.js b/src/config.js index 69d02f2b359d80..f87a536e81bc87 100644 --- a/src/config.js +++ b/src/config.js @@ -77,6 +77,7 @@ function (Settings) { 'stats', 'sparklines', 'heatmap' + 'multifieldhistogram' ] }); }); From d2e7258ba8cbd5fe8bddef819a3300772173473c Mon Sep 17 00:00:00 2001 From: tvvmb Date: Fri, 13 Jun 2014 14:41:40 +0200 Subject: [PATCH 2/8] rename chart directive to not conflict with histogram --- src/app/panels/multifieldhistogram/module.html | 2 +- src/app/panels/multifieldhistogram/module.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/panels/multifieldhistogram/module.html b/src/app/panels/multifieldhistogram/module.html index 3239b43ce43cff..9c2f88d015a836 100644 --- a/src/app/panels/multifieldhistogram/module.html +++ b/src/app/panels/multifieldhistogram/module.html @@ -104,5 +104,5 @@
-
+
diff --git a/src/app/panels/multifieldhistogram/module.js b/src/app/panels/multifieldhistogram/module.js index e26c130c918b5b..e3c45c1c49fb49 100644 --- a/src/app/panels/multifieldhistogram/module.js +++ b/src/app/panels/multifieldhistogram/module.js @@ -578,7 +578,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { }); - module.directive('histogramChart', function(dashboard, filterSrv) { + module.directive('multifieldhistogramChart', function(dashboard, filterSrv) { return { restrict: 'A', template: '
', From ebf8b234fc3cb5a95a92d406cf4eca0a0510b072 Mon Sep 17 00:00:00 2001 From: tvvmb Date: Mon, 16 Jun 2014 09:51:53 +0200 Subject: [PATCH 3/8] remove bars, extend fields table, quesries->markers, improve default aliases --- .../panels/multifieldhistogram/editor.html | 4 +- ...{queriesEditor.html => markersEditor.html} | 0 .../panels/multifieldhistogram/module.html | 8 +- src/app/panels/multifieldhistogram/module.js | 132 ++++++++++-------- .../multifieldhistogram/styleEditor.html | 5 +- 5 files changed, 77 insertions(+), 72 deletions(-) rename src/app/panels/multifieldhistogram/{queriesEditor.html => markersEditor.html} (100%) diff --git a/src/app/panels/multifieldhistogram/editor.html b/src/app/panels/multifieldhistogram/editor.html index 80dc958318cfe7..ac7350cada70a7 100644 --- a/src/app/panels/multifieldhistogram/editor.html +++ b/src/app/panels/multifieldhistogram/editor.html @@ -9,8 +9,8 @@ border: 0px solid; } -
-
+
+
Values
diff --git a/src/app/panels/multifieldhistogram/queriesEditor.html b/src/app/panels/multifieldhistogram/markersEditor.html similarity index 100% rename from src/app/panels/multifieldhistogram/queriesEditor.html rename to src/app/panels/multifieldhistogram/markersEditor.html diff --git a/src/app/panels/multifieldhistogram/module.html b/src/app/panels/multifieldhistogram/module.html index 9c2f88d015a836..f77d0340f17429 100644 --- a/src/app/panels/multifieldhistogram/module.html +++ b/src/app/panels/multifieldhistogram/module.html @@ -66,14 +66,14 @@
diff --git a/src/app/panels/multifieldhistogram/module.js b/src/app/panels/multifieldhistogram/module.js index e3c45c1c49fb49..70091e58fc78a2 100644 --- a/src/app/panels/multifieldhistogram/module.js +++ b/src/app/panels/multifieldhistogram/module.js @@ -1,16 +1,17 @@ /** @scratch /panels/5 * - * include::panels/histogram.asciidoc[] + * include::panels/multifieldhistogram.asciidoc[] */ -/** @scratch /panels/histogram/0 +/** @scratch /panels/multifieldhistogram/0 * - * == Histogram + * == multifieldhistogram * Status: *Stable* * - * The histogram panel allow for the display of time charts. It includes several modes and tranformations + * The multifieldhistogram panel allow for the display of time charts. It includes several modes and tranformations * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter - * fields. + * fields, just like the histogram panel. It provides a bit more flexibility than the histogram by allowing + * to use different fields. * */ define([ @@ -53,8 +54,8 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { src:'app/panels/multifieldhistogram/styleEditor.html' }, { - title:'Queries', - src:'app/panels/multifieldhistogram/queriesEditor.html' + title:'Markers', + src:'app/panels/multifieldhistogram/markersEditor.html' }, ], status : "Stable", @@ -65,27 +66,27 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { // Set and populate defaults $scope.defaultValue = { - /** mode:: Value to use for the y-axis. For all modes other than count, +value_field+ must be + /** m ode:: Value to use for the y-axis. For all modes other than count, +value_field+ must be * defined. Possible values: count, mean, max, min, total. */ mode : 'count', - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * value_field:: y-axis field if +mode+ is set to mean, max, min or total. Must be numeric. */ value_field : null, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * scale:: Scale the y-axis by this factor */ scale : 1, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * zerofill:: Improves the accuracy of line charts at a small performance cost. */ zerofill : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * derivative:: Show each point on the x-axis as the change from the previous point */ derivative : false, - /** @scratch /panels/histogram/5 + /** @scratch /panels/multifieldhistogram/5 * queries array:: which query ids are selected. */ queries : querySrv.ids(), @@ -94,33 +95,33 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { }; var _d = { - /** @scratch /panels/histogram/3 + /** @scra tch /panels/multifieldhistogram/3 * * === Parameters * ==== Axis options * */ - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * values:: array of values. */ values : [angular.copy($scope.defaultValue)], - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * time_field:: x-axis field. This must be defined as a date type in Elasticsearch. */ time_field : '@timestamp', - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * x-axis:: Show the x-axis */ 'x-axis' : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * y-axis:: Show the y-axis */ 'y-axis' : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * y_format:: 'none','bytes','short ' */ y_format : 'none', - /** @scratch /panels/histogram/5 + /** @scratch /panels/multifieldhistogram/5 * grid object:: Min and max y-axis values * grid.min::: Minimum y-axis value * grid.max::: Maximum y-axis value @@ -129,7 +130,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { max: null, min: 0 }, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * * ==== Annotations * annotate object:: A query can be specified, the results of which will be displayed as markers on @@ -147,91 +148,87 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { field : '_type', sort : ['_score','desc'] }, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * ==== Interval options * auto_int:: Automatically scale intervals? */ auto_int : true, - /** @scratch /panels/histogram/3 - * resolution:: If auto_int is true, shoot for this many bars. + /** @scratch /panels/multifieldhistogram/3 + * resolution:: If auto_int is true, shoot for this many points. */ resolution : 100, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * interval:: If auto_int is set to false, use this as the interval. */ interval : '5m', - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h'] */ intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'], - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * ==== Drawing options * lines:: Show line chart */ - lines : false, - /** @scratch /panels/histogram/3 + lines : true, + /** @scratch /panels/multifieldhistogram/3 * fill:: Area fill factor for line charts, 1-10 */ fill : 0, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * linewidth:: Weight of lines in pixels */ linewidth : 3, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * points:: Show points on chart */ points : false, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * pointradius:: Size of points in pixels */ pointradius : 5, - /** @scratch /panels/histogram/3 - * bars:: Show bars on chart - */ - bars : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * stack:: Stack multiple series */ stack : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * spyable:: Show inspect icon */ spyable : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * zoomlinks:: Show `Zoom Out' link */ zoomlinks : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * options:: Show quick view options section */ options : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * legend:: Display the legond */ legend : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * show_query:: If no alias is set, should the query be displayed? */ show_query : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * interactive:: Enable click-and-drag to zoom functionality */ interactive : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * legend_counts:: Show counts in legend */ legend_counts : true, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * ==== Transformations * timezone:: Correct for browser timezone?. Valid values: browser, utc */ timezone : 'browser', // browser or utc - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple * queries */ percentage : false, - /** @scratch /panels/histogram/3 + /** @scratch /panels/multifieldhistogram/3 * tooltip object:: * tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts * tooltip.query_as_alias::: If no alias is set, should the query be displayed? @@ -283,6 +280,27 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { return range; }; + $scope.get_alias = function (value, query) { + var alias = ''; + var isCount = value.mode === 'count'; + if (value.alias) { + alias += value.alias; + } else { + if (query.alias) { + alias += query.alias; + } else { + if (isCount) { + alias += $scope.panel.show_query ? query.query||'*' : ''; + } else { + alias += $scope.panel.show_query ? '('+(query.query||'*')+')' : ''; + } + } + alias += !isCount && value.value_field ? (alias && '.')+value.value_field : ''; + alias = alias ? value.mode + '(' + alias + ')' : value.mode; + } + return alias; + }; + $scope.get_interval = function () { var interval = $scope.panel.interval, range; @@ -472,7 +490,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { var info = { color: panel_value.color || q.color, - alias: panel_value.alias || q.alias || q.query, + alias: $scope.get_alias(panel_value, q), }; $scope.legend[serie_id] = {query:info,hits:hits}; @@ -511,7 +529,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { } } - // Tell the histogram directive to render. + // Tell the multifieldhistogram directive to render. $scope.$emit('render', data); // If we still have segments left, get them @@ -634,8 +652,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { }); } catch(e) {return;} - // Set barwidth based on specified interval - var barwidth = kbn.interval_to_ms(scope.panel.interval); var stack = scope.panel.stack ? true : null; @@ -653,13 +669,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { lineWidth: scope.panel.linewidth, steps: false }, - bars: { - show: scope.panel.bars, - fill: 1, - barWidth: barwidth/1.5, - zero: false, - lineWidth: 0 - }, points: { show: scope.panel.points, fill: 1, @@ -727,7 +736,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { options.selection = { mode: "x", color: '#666' }; } - // when rendering stacked bars, we need to ensure each point that has data is zero-filled + // when rendering stacked, we need to ensure each point that has data is zero-filled // so that the stacking happens in the proper order var required_times = []; if (data.length > 1) { @@ -784,9 +793,8 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { if (item) { if (item.series.info.alias || scope.panel.tooltip.query_as_alias) { group = '' + - '' + ' ' + - (item.series.info.alias || item.series.info.query)+ - '
'; + '' + + ' ' + item.series.info.alias + '
'; } else { group = kbn.query_color_dot(item.series.color, 15) + ' '; } diff --git a/src/app/panels/multifieldhistogram/styleEditor.html b/src/app/panels/multifieldhistogram/styleEditor.html index 874a22640b5ae3..fb011846dd0b0f 100644 --- a/src/app/panels/multifieldhistogram/styleEditor.html +++ b/src/app/panels/multifieldhistogram/styleEditor.html @@ -1,9 +1,6 @@
Chart Options
-
- -
@@ -66,7 +63,7 @@
Legend
- +
From adf310a00a11b43d2970709d48d501f13f3ee2ae Mon Sep 17 00:00:00 2001 From: tvvmb Date: Thu, 31 Jul 2014 13:52:57 +0200 Subject: [PATCH 4/8] fix scale and derivative in multifieldhistogram --- src/app/panels/multifieldhistogram/module.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/panels/multifieldhistogram/module.js b/src/app/panels/multifieldhistogram/module.js index 70091e58fc78a2..5bc74c0b7cd17e 100644 --- a/src/app/panels/multifieldhistogram/module.js +++ b/src/app/panels/multifieldhistogram/module.js @@ -89,7 +89,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { /** @scratch /panels/multifieldhistogram/5 * queries array:: which query ids are selected. */ - queries : querySrv.ids(), + queries : [], color : null, alias : null }; @@ -752,11 +752,11 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { for (var i = 0; i < data.length; i++) { var _d = data[i].time_series.getFlotPairs(required_times); - if(scope.panel.values[0].derivative) { + if(scope.panel.values[i].derivative) { _d = derivative(_d); } - if(scope.panel.values[0].scale !== 1) { - _d = scale(_d,scope.panel.values[0].scale); + if(scope.panel.values[i].scale !== 1) { + _d = scale(_d,scope.panel.values[i].scale); } if(scope.panel.scaleSeconds) { _d = scaleSeconds(_d,scope.panel.interval); From 2c0e869df84c4f86b5c0f10e723c63a59975da4e Mon Sep 17 00:00:00 2001 From: Luca Milanesio Date: Thu, 31 Jul 2014 12:15:28 +0100 Subject: [PATCH 5/8] Include jquery.flot.stack* for using in multiple panels When using multiple panels that use the stacked graphs (e.g. histogram and multifieldhistogram) we need to inform grunt-requirejs plugin that jquery.flot.stack* is one the modules to prevent double-inclusion and then problems in the histogram rendering. See https://github.com/elasticsearch/kibana/pull/1296 NOTE: this change is needed for getting pull#1296 to work properly but can be merged indipendently from it. --- tasks/options/requirejs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks/options/requirejs.js b/tasks/options/requirejs.js index 3c71c5a9596761..c31207057b97ad 100644 --- a/tasks/options/requirejs.js +++ b/tasks/options/requirejs.js @@ -53,6 +53,8 @@ module.exports = function(config,grunt) { 'lodash', 'filters/all', 'jquery.flot', + 'jquery.flot.stack', + 'jquery.flot.stackpercent', 'services/all', 'angular-strap', 'directives/all', From 38309019fd164185b5d36e3e42bafccaf714ca6c Mon Sep 17 00:00:00 2001 From: Ian Babrou Date: Tue, 4 Mar 2014 20:34:50 +0400 Subject: [PATCH 6/8] integral support for histogram panel --- src/app/panels/histogram/editor.html | 7 +++++-- src/app/panels/histogram/module.html | 2 +- src/app/panels/histogram/module.js | 23 ++++++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/app/panels/histogram/editor.html b/src/app/panels/histogram/editor.html index 9557e5d283a4f9..dc63ae980462f4 100644 --- a/src/app/panels/histogram/editor.html +++ b/src/app/panels/histogram/editor.html @@ -19,8 +19,11 @@
Transform Series
-
- +
+ +
+
+
diff --git a/src/app/panels/histogram/module.html b/src/app/panels/histogram/module.html index e1a24ad5caa3ab..83e667f5519b8e 100644 --- a/src/app/panels/histogram/module.html +++ b/src/app/panels/histogram/module.html @@ -60,7 +60,7 @@ ({{series.hits}}) - change in {{panel.value_field}} {{panel.mode}} per {{panel.interval}}1s | ({{hits}} hits) + change in integral over {{panel.value_field}} {{panel.mode}} per {{panel.interval}}1s | ({{hits}} hits)
diff --git a/src/app/panels/histogram/module.js b/src/app/panels/histogram/module.js index 8438b7f748367a..124e11fecb5b6f 100644 --- a/src/app/panels/histogram/module.js +++ b/src/app/panels/histogram/module.js @@ -9,7 +9,7 @@ * Status: *Stable* * * The histogram panel allow for the display of time charts. It includes several modes and tranformations - * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter + * to display event counts, mean, min, max and total of numeric fields, derivatives, and integrals of counter * fields. * */ @@ -228,6 +228,10 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { */ derivative : false, + /** @scratch /panels/histogram/3 + * integral:: Show each point on the x-axis as the sum with the previous point + */ + integral : false, /** @scratch /panels/histogram/3 * tooltip object:: * tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts @@ -418,7 +422,8 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { interval: _interval, start_date: _range && _range.from, end_date: _range && _range.to, - fill_style: $scope.panel.derivative ? 'null' : $scope.panel.zerofill ? 'minimal' : 'no' + fill_style: $scope.panel.derivative ? 'null' : $scope.panel.zerofill ? 'minimal' : + ($scope.panel.integral ? 'all' : 'minimal') }; time_series = new timeSeries.ZeroFilled(tsOpts); hits = 0; @@ -597,12 +602,21 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { if(i === 0 || p[1] === null) { _v = [p[0],null]; } else { - _v = series[i-1][1] === null ? [p[0],null] : [p[0],p[1]-(series[i-1][1])]; + _v = series[i-1][1] === null ? [p[0],null] : [p[0],p[1]-series[i-1][1]]; } return _v; }); }; + var integral = function(series) { + var sum = 0; + return _.map(series, function(p) { + p[1] = p[1] || 0; + sum = sum + p[1]; + return [p[0], sum]; + }); + }; + // Function for rendering panel function render_panel(data) { // IE doesn't work without this @@ -730,6 +744,9 @@ function (angular, app, $, _, kbn, moment, timeSeries, numeral) { if(scope.panel.derivative) { _d = derivative(_d); } + if(scope.panel.integral){ + _d = integral(_d); + } if(scope.panel.scale !== 1) { _d = scale(_d,scope.panel.scale); } From 369c247e224cd12721766cf8e09de4098e37dbb9 Mon Sep 17 00:00:00 2001 From: Ron Kuris Date: Sat, 23 Aug 2014 17:05:22 -0700 Subject: [PATCH 7/8] Add download option, supporting JSON and CSV formats --- docs/kibana/panels/table.asciidoc | 2 + src/app/panels/table/editor.html | 8 ++- src/app/panels/table/module.js | 90 ++++++++++++++++++++++++++++++- src/app/partials/download.html | 39 ++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/app/partials/download.html diff --git a/docs/kibana/panels/table.asciidoc b/docs/kibana/panels/table.asciidoc index 5fbad39b4587b0..4f85101259dae5 100644 --- a/docs/kibana/panels/table.asciidoc +++ b/docs/kibana/panels/table.asciidoc @@ -28,6 +28,8 @@ still available in the expanded view of the event. localTime:: Set to true to adjust the timeField to the browser's local time timeField:: If localTime is set to true, this field will be adjusted to the browsers local time spyable:: Set to false to disable the inspect icon +download:: Set to false to disable the download icon +headings:: Add headings to CSV output ==== Queries queries object:: This object describes the queries to use on this panel. diff --git a/src/app/panels/table/editor.html b/src/app/panels/table/editor.html index 8988a9aebd1ec7..c1f17fc04c5419 100644 --- a/src/app/panels/table/editor.html +++ b/src/app/panels/table/editor.html @@ -20,6 +20,12 @@
Options

+
+ +
+
+ +
@@ -42,4 +48,4 @@
Highlighted Fields

{{field}}
-
\ No newline at end of file +
diff --git a/src/app/panels/table/module.js b/src/app/panels/table/module.js index 7dbfb2d37cd8fe..316a80291dfb94 100644 --- a/src/app/panels/table/module.js +++ b/src/app/panels/table/module.js @@ -31,6 +31,12 @@ function (angular, app, _, kbn, moment) { $scope.panelMeta = { modals : [ { + description: "Download", + icon: "icon-share", + partial: "app/partials/download.html", + show: $scope.panel.download + }, + { description: "Inspect", icon: "icon-info-sign", partial: "app/partials/inspector.html", @@ -125,6 +131,14 @@ function (angular, app, _, kbn, moment) { * spyable:: Set to false to disable the inspect icon */ spyable : true, + /** @scratch /panels/table/5 + * download:: Set to false to disable the download icon + */ + download : true, + /** @scratch /panels/table/5 + * headings:: Add headings to CSV output + */ + headings : true, /** @scratch /panels/table/5 * * ==== Queries @@ -469,6 +483,80 @@ function (angular, app, _, kbn, moment) { }); + module.controller('DownloadController', function($rootScope, $scope, $modal, $q, $compile, $timeout, + fields, querySrv, dashboard, filterSrv, alertSrv) { + $scope.suffix = 'json'; + $scope.savefilename = 'data-' + dashboard.current.title + '.' + $scope.suffix; + $scope.maxrows = 1000; + $scope.refetch = true; + $scope.fixsuffix = function() { + $scope.savefilename = $scope.savefilename.replace(/(.json|.csv)$/, '.' + $scope.suffix); + }; + $scope.json2csv = function(objArray) { + var array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray; + var str = ''; + if ($scope.headings) { + var hline = ''; + for (var hindex in array[0]) { + var hvalue = hindex + ""; + hline += '"' + hvalue.replace(/"/g, '""') + '",'; + } + + hline = hline.slice(0, -1); + str += hline + '\r\n'; + } + for (var i = 0; i < array.length; i++) { + var line = ''; + + for (var index in array[i]) { + var value = array[i][index] + ""; + line += '"' + value.replace(/"/g, '""') + '",'; + } + + line = line.slice(0, -1); + str += line + '\r\n'; + } + return str; + }; + $scope.downloadData = function() { + var request = $scope.ejs.Request().indices(dashboard.indices[0]); + $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); + var queries = querySrv.getQueryObjs($scope.panel.queries.ids); + var boolQuery = $scope.ejs.BoolQuery(); + _.each(queries,function(q) { + boolQuery = boolQuery.should(querySrv.toEjsObj(q)); + }); + request = request.query( + $scope.ejs.FilteredQuery( + boolQuery, + filterSrv.getBoolFilter(filterSrv.ids()) + )) + .size($scope.maxrows); + // TODO: should this query be sorted based on the tool's settings? + request.doSearch().then(function(results) { + // Check for error and abort if found + if(!(_.isUndefined(results.error))) { + $scope.panel.error = $scope.parse_error(results.error); + return; + } + if (results.hits.total > $scope.maxrows) { + alertSrv.set('Not all rows were fetched (' + $scope.maxrows + ' of ' + results.hits.total + ')','warn',5000); + } else { + alertSrv.set('All ' + results.hits.total + ' rows fetched','info',5000); + } + if ($scope.suffix === 'json') { + var jblob = new Blob([angular.toJson(results.hits)], {type: "application/json;charset=utf-8"}); + window.saveAs(jblob, $scope.savefilename); + } else { + var cblob = new Blob([$scope.json2csv(results.hits.hits.map(function(data) { + return data._source; + }))], {type: "text/csv;charset=utf-8"}); + window.saveAs(cblob, $scope.savefilename); + } + }); + }; + }); + // This also escapes some xml sequences module.filter('tableHighlight', function() { return function(text) { @@ -533,4 +621,4 @@ function (angular, app, _, kbn, moment) { }; }); -}); \ No newline at end of file +}); diff --git a/src/app/partials/download.html b/src/app/partials/download.html new file mode 100644 index 00000000000000..b7b5002b5b953f --- /dev/null +++ b/src/app/partials/download.html @@ -0,0 +1,39 @@ + +
+ + +
From 43fbf920407f35868339a58b1892bb9e5232a9e1 Mon Sep 17 00:00:00 2001 From: Joe MacMahon Date: Wed, 10 Sep 2014 14:28:22 +0200 Subject: [PATCH 8/8] Stats panel: Make formats more flexible. * Adds capability to change the formatting individually for modes. E.g. 'count' shouldn't be displayed in bytes, rather it should just be a natural number. * Implementation: define individual formatting functions for each mode and patch up the angular template as needed. * (Closes #1285.) Signed-off-by: Joe MacMahon --- src/app/panels/stats/module.html | 12 ++++---- src/app/panels/stats/module.js | 49 +++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/app/panels/stats/module.html b/src/app/panels/stats/module.html index aca958e790688c..6bbed463ffa2f2 100644 --- a/src/app/panels/stats/module.html +++ b/src/app/panels/stats/module.html @@ -11,24 +11,24 @@ -

{{data.value|formatstats:panel.format}} {{panel.unit}} ({{panel.mode}})

+

{{data.value|formatstats:panel.format:data}} {{panel.unit}} ({{panel.mode}})

- - + diff --git a/src/app/panels/stats/module.js b/src/app/panels/stats/module.js index 94c905e5f9dcb4..edbb58f540a1af 100644 --- a/src/app/panels/stats/module.js +++ b/src/app/panels/stats/module.js @@ -48,7 +48,36 @@ define([ description: 'A statistical panel for displaying aggregations using the Elastic Search statistical facet query.' }; - $scope.modes = ['count','min','max','mean','total','variance','std_deviation','sum_of_squares']; + $scope.modes = [ + { + 'name': 'count', + 'format': function (value, format_string) { return numeral(value).format('0,0'); } + }, + { + 'name': 'min', + 'format': function (value, format_string) { return numeral(value).format(format_string); } + }, + { + 'name': 'max', + 'format': function (value, format_string) { return numeral(value).format(format_string); } + }, + { + 'name': 'total', + 'format': function (value, format_string) { return numeral(value).format(format_string); } + }, + { + 'name': 'variance', + 'format': function (value, format_string) { return numeral(value).format(format_string) + "²"; } + }, + { + 'name': 'std_deviation', + 'format': function (value, format_string) { return numeral(value).format(format_string); } + }, + { + 'name': 'sum_of_squares', + 'format': function (value, format_string) { return numeral(value).format(format_string) + "²"; } + }, + ]; var defaults = { queries : { @@ -166,9 +195,12 @@ define([ return obj; }); + var format = _.find( $scope.modes, function (mode) { return mode.name == $scope.panel.mode; } ).format; + $scope.data = { value: value, - rows: rows + rows: rows, + format: format }; console.log($scope.data); @@ -192,19 +224,22 @@ define([ }); module.filter('formatstats', function(){ - return function (value,format) { + return function (value,format,stat) { + if (stat == undefined) // Catch a weird bug in Angular. + return; + switch (format) { case 'money': - value = numeral(value).format('$0,0.00'); + value = stat.format(value, '$0,0.00'); break; case 'bytes': - value = numeral(value).format('0.00b'); + value = stat.format(value, '0.00b'); break; case 'float': - value = numeral(value).format('0.000'); + value = stat.format(value, '0.000'); break; default: - value = numeral(value).format('0,0'); + value = stat.format(value, '0,0'); } return value; };
{{panel.label_name}} + - {{stat}} + ng-click="set_sort(stat.name)" + ng-class="{'icon-chevron-down': panel.sort_field == stat.name && panel.sort_reverse == true, 'icon-chevron-up': panel.sort_field == stat.name && panel.sort_reverse == false}"> + {{stat.name}}
{{item.label}}{{item.value[stat]|formatstats:panel.format}} {{panel.unit}}{{item.value[stat.name]|formatstats:panel.format:stat}} {{panel.unit}}