/** @scratch /panels/5 * * include::panels/sparklines.asciidoc[] */ /** @scratch /panels/sparklines/0 * * == Sparklines * Status: *Experimental* * * The sparklines panel shows tiny time charts. The purpose of these is not to give an exact value, * but rather to show the shape of the time series in a compact manner * */ define([ 'angular', 'app', 'jquery', 'lodash', 'kbn', 'moment', './timeSeries', 'jquery.flot', 'jquery.flot.time' ], function (angular, app, $, _, kbn, moment, timeSeries) { 'use strict'; var module = angular.module('kibana.panels.sparklines', []); app.useModule(module); module.controller('sparklines', 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:'Queries', src:'app/partials/querySelect.html' } ], status : "Experimental", description : "Sparklines are tiny, simple, time series charts, shown separately. Because "+ "sparklines are uncluttered by grids, axis markers and colors, they are perfect for spotting"+ " change in a series" }; // Set and populate defaults var _d = { /** @scratch /panels/sparklines/3 * * === Parameters * 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/sparklines/3 * time_field:: x-axis field. This must be defined as a date type in Elasticsearch. */ time_field : '@timestamp', /** @scratch /panels/sparklines/3 * value_field:: y-axis field if +mode+ is set to mean, max, min or total. Must be numeric. */ value_field : null, /** @scratch /panels/sparklines/3 * interval:: Sparkline intervals are computed automatically as long as there is a time filter * present. In the absence of a time filter, use this interval. */ interval : '5m', /** @scratch /panels/sparklines/3 * spyable:: Show inspect icon */ spyable : true, /** @scratch /panels/sparklines/5 * * ==== Queries * queries object:: This object describes the queries to use on this panel. * queries.mode::: Of the queries available, which to use. Options: +all, pinned, unpinned, selected+ * queries.ids::: In +selected+ mode, which query ids are selected. */ queries : { mode : 'all', ids : [] }, }; _.defaults($scope.panel,_d); $scope.init = function() { $scope.$on('refresh',function(){ $scope.get_data(); }); $scope.get_data(); }; $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; range = $scope.get_time_range(); if (range) { interval = kbn.secondsToHms( kbn.calculate_interval(range.from, range.to, 10, 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(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); $scope.panelMeta.loading = true; request = $scope.ejs.Request().indices(dashboard.indices[segment]); $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); queries = querySrv.getQueryObjs($scope.panel.queries.ids); // Build the query _.each(queries, function(q) { var query = $scope.ejs.FilteredQuery( querySrv.toEjsObj(q), filterSrv.getBoolFilter(filterSrv.ids()) ); var facet = $scope.ejs.DateHistogramFacet(q.id); if($scope.panel.mode === 'count') { facet = facet.field($scope.panel.time_field).global(true); } else { if(_.isNull($scope.panel.value_field)) { $scope.panel.error = "In " + $scope.panel.mode + " mode a field must be specified"; return; } facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field); } facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query)); request = request.facet(facet) .size(0); }); // Populate the inspector panel $scope.populate_modal(request); // Then run it results = request.doSearch(); // Populate scope when we have results results.then(function(results) { $scope.panelMeta.loading = false; if(segment === 0) { $scope.hits = 0; $scope.data = []; 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); return; } // Make sure we're still on the same query/queries if($scope.query_id === query_id) { var i = 0, time_series, hits; _.each(queries, function(q) { var query_results = results.facets[q.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($scope.data[i]) || segment === 0) { var tsOpts = { interval: _interval, start_date: _range && _range.from, end_date: _range && _range.to, fill_style: 'minimal' }; time_series = new timeSeries.ZeroFilled(tsOpts); hits = 0; } else { time_series = $scope.data[i].time_series; hits = $scope.data[i].hits; } // push each entry into the time series, while incrementing counters _.each(query_results.entries, function(entry) { time_series.addValue(entry.time, entry[$scope.panel.mode]); hits += entry.count; // The series level hits counter $scope.hits += entry.count; // Entire dataset level hits counter }); $scope.data[i] = { info: q, range: $scope.range, time_series: time_series, hits: hits }; i++; }); // If we still have segments left, get them if(segment < dashboard.indices.length-1) { $scope.get_data(segment+1,query_id); } } }); }; // 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; }; }); module.directive('sparklinesChart', function() { return { restrict: 'A', scope: { series: '=', panel: '=' }, template: '
', link: function(scope, elem) { // Receive render events scope.$watch('series',function(){ render_panel(); }); 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() { // IE doesn't work without this elem.css({height:"30px",width:"100px"}); // Populate element //try { var options = { legend: { show: false }, series: { lines: { show: true, // Silly, but fixes bug in stacked percentages fill: 0, lineWidth: 2, steps: false }, points: { radius:2 }, shadowSize: 1 }, yaxis: { show: false }, xaxis: { show: false, mode: "time", min: _.isUndefined(scope.series.range.from) ? null : scope.series.range.from.getTime(), max: _.isUndefined(scope.series.range.to) ? null : scope.series.range.to.getTime() }, grid: { hoverable: false, show: false } }; // 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 = []; required_times = scope.series.time_series.getOrderedTimes(); required_times = _.uniq(required_times.sort(function (a, b) { // decending numeric sort return a-b; }), true); var _d = { data : scope.panel.derivative ? derivative(scope.series.time_series.getFlotPairs(required_times)) : scope.series.time_series.getFlotPairs(required_times), label : scope.series.info.alias, color : elem.css('color'), }; $.plot(elem, [_d], options); //} catch(e) { // console.log(e); //} } var $tooltip = $('
'); elem.bind("plothover", function (event, pos, item) { if (item) { $tooltip .html( item.datapoint[1] + " @ " + moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') ) .place_tt(pos.pageX, pos.pageY); } else { $tooltip.detach(); } }); } }; }); });