module.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. /** @scratch /panels/5
  2. * include::panels/histogram.asciidoc[]
  3. */
  4. /** @scratch /panels/histogram/0
  5. * == Histogram
  6. * Status: *Stable*
  7. *
  8. * The histogram panel allow for the display of time charts. It includes several modes and tranformations
  9. * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter
  10. * fields.
  11. *
  12. */
  13. define([
  14. 'angular',
  15. 'app',
  16. 'jquery',
  17. 'underscore',
  18. 'kbn',
  19. 'moment',
  20. './timeSeries',
  21. './graphiteSrv',
  22. 'jquery.flot',
  23. 'jquery.flot.events',
  24. 'jquery.flot.selection',
  25. 'jquery.flot.time',
  26. 'jquery.flot.byte',
  27. 'jquery.flot.stack',
  28. 'jquery.flot.stackpercent'
  29. ],
  30. function (angular, app, $, _, kbn, moment, timeSeries, graphiteSrv) {
  31. 'use strict';
  32. var module = angular.module('kibana.panels.graphite', []);
  33. app.useModule(module);
  34. module.controller('graphite', function($scope, $rootScope, querySrv, dashboard, filterSrv) {
  35. $scope.panelMeta = {
  36. modals : [
  37. {
  38. description: "Inspect",
  39. icon: "icon-info-sign",
  40. partial: "app/partials/inspector.html",
  41. show: $scope.panel.spyable
  42. }
  43. ],
  44. editorTabs : [
  45. {
  46. title:'Style',
  47. src:'app/panels/graphite/styleEditor.html'
  48. }
  49. ],
  50. status : "Stable",
  51. description : "A bucketed time series chart of the current query or queries. Uses the "+
  52. "Elasticsearch date_histogram facet. If using time stamped indices this panel will query"+
  53. " them sequentially to attempt to apply the lighest possible load to your Elasticsearch cluster"
  54. };
  55. // Set and populate defaults
  56. var _d = {
  57. /** @scratch /panels/histogram/3
  58. * x-axis:: Show the x-axis
  59. */
  60. 'x-axis' : true,
  61. /** @scratch /panels/histogram/3
  62. * y-axis:: Show the y-axis
  63. */
  64. 'y-axis' : true,
  65. /** @scratch /panels/histogram/3
  66. * scale:: Scale the y-axis by this factor
  67. */
  68. scale : 1,
  69. /** @scratch /panels/histogram/3
  70. * y_format:: 'none','bytes','short '
  71. */
  72. y_format : 'none',
  73. /** @scratch /panels/histogram/5
  74. * grid object:: Min and max y-axis values
  75. * grid.min::: Minimum y-axis value
  76. * grid.max::: Maximum y-axis value
  77. */
  78. grid : {
  79. max: null,
  80. min: 0
  81. },
  82. /** @scratch /panels/histogram/3
  83. * ==== Annotations
  84. * annotate object:: A query can be specified, the results of which will be displayed as markers on
  85. * the chart. For example, for noting code deploys.
  86. * annotate.enable::: Should annotations, aka markers, be shown?
  87. * annotate.query::: Lucene query_string syntax query to use for markers.
  88. * annotate.size::: Max number of markers to show
  89. * annotate.field::: Field from documents to show
  90. * annotate.sort::: Sort array in format [field,order], For example [`@timestamp',`desc']
  91. */
  92. annotate : {
  93. enable : false,
  94. query : "*",
  95. size : 20,
  96. field : '_type',
  97. sort : ['_score','desc']
  98. },
  99. /** @scratch /panels/histogram/3
  100. * ==== Interval options
  101. * auto_int:: Automatically scale intervals?
  102. */
  103. auto_int : true,
  104. /** @scratch /panels/histogram/3
  105. * resolution:: If auto_int is true, shoot for this many bars.
  106. */
  107. resolution : 100,
  108. /** @scratch /panels/histogram/3
  109. * interval:: If auto_int is set to false, use this as the interval.
  110. */
  111. interval : '5m',
  112. /** @scratch /panels/histogram/3
  113. * interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h']
  114. */
  115. intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'],
  116. /** @scratch /panels/histogram/3
  117. * ==== Drawing options
  118. * lines:: Show line chart
  119. */
  120. lines : true,
  121. /** @scratch /panels/histogram/3
  122. * fill:: Area fill factor for line charts, 1-10
  123. */
  124. fill : 0,
  125. /** @scratch /panels/histogram/3
  126. * linewidth:: Weight of lines in pixels
  127. */
  128. linewidth : 1,
  129. /** @scratch /panels/histogram/3
  130. * points:: Show points on chart
  131. */
  132. points : false,
  133. /** @scratch /panels/histogram/3
  134. * pointradius:: Size of points in pixels
  135. */
  136. pointradius : 5,
  137. /** @scratch /panels/histogram/3
  138. * bars:: Show bars on chart
  139. */
  140. bars : false,
  141. /** @scratch /panels/histogram/3
  142. * stack:: Stack multiple series
  143. */
  144. stack : true,
  145. /** @scratch /panels/histogram/3
  146. * spyable:: Show inspect icon
  147. */
  148. spyable : true,
  149. /** @scratch /panels/histogram/3
  150. * zoomlinks:: Show `Zoom Out' link
  151. */
  152. zoomlinks : false,
  153. /** @scratch /panels/histogram/3
  154. * options:: Show quick view options section
  155. */
  156. options : false,
  157. /** @scratch /panels/histogram/3
  158. * legend:: Display the legond
  159. */
  160. legend : true,
  161. /** @scratch /panels/histogram/3
  162. * interactive:: Enable click-and-drag to zoom functionality
  163. */
  164. interactive : true,
  165. /** @scratch /panels/histogram/3
  166. * legend_counts:: Show counts in legend
  167. */
  168. legend_counts : true,
  169. /** @scratch /panels/histogram/3
  170. * ==== Transformations
  171. * timezone:: Correct for browser timezone?. Valid values: browser, utc
  172. */
  173. timezone : 'browser', // browser or utc
  174. /** @scratch /panels/histogram/3
  175. * percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple
  176. * queries
  177. */
  178. percentage : false,
  179. /** @scratch /panels/histogram/3
  180. * zerofill:: Improves the accuracy of line charts at a small performance cost.
  181. */
  182. zerofill : true,
  183. /** @scratch /panels/histogram/3
  184. * derivative:: Show each point on the x-axis as the change from the previous point
  185. */
  186. tooltip : {
  187. value_type: 'cumulative',
  188. query_as_alias: true
  189. },
  190. targets: []
  191. };
  192. _.defaults($scope.panel,_d);
  193. _.defaults($scope.panel.tooltip,_d.tooltip);
  194. _.defaults($scope.panel.annotate,_d.annotate);
  195. _.defaults($scope.panel.grid,_d.grid);
  196. $scope.init = function() {
  197. // Hide view options by default
  198. $scope.options = false;
  199. $scope.$on('refresh',function(){
  200. $scope.get_data();
  201. });
  202. // Always show the query if an alias isn't set. Users can set an alias if the query is too
  203. // long
  204. $scope.panel.tooltip.query_as_alias = true;
  205. $scope.get_data();
  206. };
  207. $scope.set_interval = function(interval) {
  208. if(interval !== 'auto') {
  209. $scope.panel.auto_int = false;
  210. $scope.panel.interval = interval;
  211. } else {
  212. $scope.panel.auto_int = true;
  213. }
  214. };
  215. $scope.remove_panel_from_row = function(row, panel) {
  216. if ($scope.inEditMode) {
  217. $rootScope.$emit('fullEditMode', false);
  218. }
  219. else {
  220. $scope.$parent.remove_panel_from_row(row, panel);
  221. }
  222. };
  223. $scope.closeEditMode = function() {
  224. $rootScope.$emit('fullEditMode', false);
  225. };
  226. $scope.interval_label = function(interval) {
  227. return $scope.panel.auto_int && interval === $scope.panel.interval ? interval+" (auto)" : interval;
  228. };
  229. /**
  230. * The time range effecting the panel
  231. * @return {[type]} [description]
  232. */
  233. $scope.get_time_range = function () {
  234. var range = $scope.range = filterSrv.timeRange('last');
  235. return range;
  236. };
  237. $scope.get_interval = function () {
  238. var interval = $scope.panel.interval;
  239. var range;
  240. if ($scope.panel.auto_int) {
  241. range = $scope.get_time_range();
  242. if (range) {
  243. interval = kbn.secondsToHms(
  244. kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000
  245. );
  246. }
  247. }
  248. $scope.panel.interval = interval || '10m';
  249. return $scope.panel.interval;
  250. };
  251. $scope.colors = [
  252. "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
  253. "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
  254. "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
  255. "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
  256. "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
  257. "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
  258. "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
  259. ];
  260. /**
  261. * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies
  262. * need to be consulted (like timestamped logstash indicies)
  263. *
  264. * The results of this function are stored on the scope's data property. This property will be an
  265. * array of objects with the properties info, time_series, and hits. These objects are used in the
  266. * render_panel function to create the historgram.
  267. *
  268. */
  269. $scope.get_data = function() {
  270. delete $scope.panel.error;
  271. $scope.panelMeta.loading = true;
  272. var range = $scope.get_time_range();
  273. var interval = $scope.get_interval(range);
  274. console.log('Interval: ' + interval);
  275. var graphiteLoadOptions = {
  276. range: range,
  277. targets: $scope.panel.targets,
  278. maxDataPoints: $scope.panel.span * 50
  279. };
  280. var result = RQ.sequence([
  281. graphiteSrv.loadGraphiteData(graphiteLoadOptions),
  282. $scope.receiveGraphiteData(range, interval)
  283. ]);
  284. result(function (success, failure) {
  285. if (failure) {
  286. $scope.panel.error = 'Failed to do fetch graphite data: ' + failure;
  287. return;
  288. }
  289. $scope.panelMeta.loading = false;
  290. $scope.$apply();
  291. // Tell the histogram directive to render.
  292. $scope.$emit('render');
  293. });
  294. };
  295. $scope.receiveGraphiteData = function(range, interval) {
  296. return function receive_graphite_data_requestor(requestion, results) {
  297. $scope.data = [];
  298. if(results.length == 0 ) {
  299. requestion('no data in response from graphite');
  300. }
  301. console.log('Data from graphite:', results);
  302. console.log('nr datapoints from graphite: %d', results[0].datapoints.length);
  303. var tsOpts = {
  304. interval: interval,
  305. start_date: range && range.from,
  306. end_date: range && range.to,
  307. fill_style: 'connect'
  308. };
  309. _.each(results, function(targetData) {
  310. var time_series = new timeSeries.ZeroFilled(tsOpts);
  311. _.each(targetData.datapoints, function(valueArray) {
  312. if (valueArray[0]) {
  313. time_series.addValue(valueArray[1] * 1000, valueArray[0]);
  314. }
  315. });
  316. $scope.data.push({
  317. info: {
  318. alias: targetData.target,
  319. color: $scope.colors[$scope.data.length],
  320. enable: true
  321. },
  322. time_series: time_series,
  323. hits: 0
  324. });
  325. });
  326. requestion('ok');
  327. };
  328. };
  329. $scope.add_target = function() {
  330. $scope.panel.targets.push({target: ''});
  331. };
  332. // function $scope.zoom
  333. // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
  334. $scope.zoom = function(factor) {
  335. var _range = filterSrv.timeRange('last');
  336. var _timespan = (_range.to.valueOf() - _range.from.valueOf());
  337. var _center = _range.to.valueOf() - _timespan/2;
  338. var _to = (_center + (_timespan*factor)/2);
  339. var _from = (_center - (_timespan*factor)/2);
  340. // If we're not already looking into the future, don't.
  341. if(_to > Date.now() && _range.to < Date.now()) {
  342. var _offset = _to - Date.now();
  343. _from = _from - _offset;
  344. _to = Date.now();
  345. }
  346. if(factor > 1) {
  347. filterSrv.removeByType('time');
  348. }
  349. filterSrv.set({
  350. type:'time',
  351. from:moment.utc(_from).toDate(),
  352. to:moment.utc(_to).toDate(),
  353. field:$scope.panel.time_field
  354. });
  355. };
  356. $scope.openConfigureModal = function($event) {
  357. $event.preventDefault();
  358. $event.stopPropagation();
  359. var closeEditMode = $rootScope.$on('fullEditMode', function(evt, enabled) {
  360. $scope.inEditMode = enabled;
  361. if (!enabled) {
  362. closeEditMode();
  363. }
  364. setTimeout(function() {
  365. $scope.$emit('render');
  366. }, 200);
  367. });
  368. $rootScope.$emit('fullEditMode', true);
  369. };
  370. // I really don't like this function, too much dom manip. Break out into directive?
  371. $scope.populate_modal = function(request) {
  372. $scope.inspector = angular.toJson(request,true);
  373. };
  374. $scope.set_refresh = function (state) {
  375. $scope.refresh = state;
  376. };
  377. $scope.close_edit = function() {
  378. if($scope.refresh) {
  379. $scope.get_data();
  380. }
  381. $scope.refresh = false;
  382. $scope.$emit('render');
  383. };
  384. $scope.render = function() {
  385. $scope.$emit('render');
  386. };
  387. });
  388. module.directive('histogramChart', function(dashboard, filterSrv) {
  389. return {
  390. restrict: 'A',
  391. template: '<div></div>',
  392. link: function(scope, elem) {
  393. // Receive render events
  394. scope.$on('render',function(){
  395. render_panel();
  396. });
  397. // Re-render if the window is resized
  398. angular.element(window).bind('resize', function(){
  399. render_panel();
  400. });
  401. var scale = function(series,factor) {
  402. return _.map(series,function(p) {
  403. return [p[0],p[1]*factor];
  404. });
  405. };
  406. var scaleSeconds = function(series,interval) {
  407. return _.map(series,function(p) {
  408. return [p[0],p[1]/kbn.interval_to_seconds(interval)];
  409. });
  410. };
  411. var derivative = function(series) {
  412. return _.map(series, function(p,i) {
  413. var _v;
  414. if(i === 0 || p[1] === null) {
  415. _v = [p[0],null];
  416. } else {
  417. _v = series[i-1][1] === null ? [p[0],null] : [p[0],p[1]-(series[i-1][1])];
  418. }
  419. return _v;
  420. });
  421. };
  422. // Function for rendering panel
  423. function render_panel() {
  424. // IE doesn't work without this
  425. elem.css({height:scope.panel.height || scope.row.height});
  426. // Populate from the query service
  427. try {
  428. _.each(scope.data, function(series) {
  429. series.label = series.info.alias;
  430. series.color = series.info.color;
  431. });
  432. } catch(e) {return;}
  433. // Set barwidth based on specified interval
  434. var barwidth = kbn.interval_to_ms(scope.panel.interval);
  435. var stack = scope.panel.stack ? true : null;
  436. // Populate element
  437. try {
  438. var options = {
  439. legend: { show: false },
  440. series: {
  441. stackpercent: scope.panel.stack ? scope.panel.percentage : false,
  442. stack: scope.panel.percentage ? null : stack,
  443. lines: {
  444. show: scope.panel.lines,
  445. // Silly, but fixes bug in stacked percentages
  446. fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10,
  447. lineWidth: scope.panel.linewidth,
  448. steps: false
  449. },
  450. bars: {
  451. show: scope.panel.bars,
  452. fill: 1,
  453. barWidth: barwidth/1.5,
  454. zero: false,
  455. lineWidth: 0
  456. },
  457. points: {
  458. show: scope.panel.points,
  459. fill: 1,
  460. fillColor: false,
  461. radius: scope.panel.pointradius
  462. },
  463. shadowSize: 1
  464. },
  465. yaxis: {
  466. show: scope.panel['y-axis'],
  467. min: scope.panel.grid.min,
  468. max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max
  469. },
  470. xaxis: {
  471. timezone: scope.panel.timezone,
  472. show: scope.panel['x-axis'],
  473. mode: "time",
  474. min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
  475. max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
  476. timeformat: time_format(scope.panel.interval),
  477. label: "Datetime",
  478. ticks: elem.width()/100
  479. },
  480. grid: {
  481. backgroundColor: null,
  482. borderWidth: 0,
  483. hoverable: true,
  484. color: '#c8c8c8'
  485. }
  486. };
  487. if(scope.panel.y_format === 'bytes') {
  488. options.yaxis.mode = "byte";
  489. }
  490. if(scope.panel.y_format === 'short') {
  491. options.yaxis.tickFormatter = function(val) {
  492. return kbn.shortFormat(val,0);
  493. };
  494. }
  495. if(scope.panel.annotate.enable) {
  496. options.events = {
  497. levels: 1,
  498. data: scope.annotations,
  499. types: {
  500. 'annotation': {
  501. level: 1,
  502. icon: {
  503. icon: "icon-tag icon-flip-vertical",
  504. size: 20,
  505. color: "#222",
  506. outline: "#bbb"
  507. }
  508. }
  509. }
  510. //xaxis: int // the x axis to attach events to
  511. };
  512. }
  513. if(scope.panel.interactive) {
  514. options.selection = { mode: "x", color: '#666' };
  515. }
  516. // when rendering stacked bars, we need to ensure each point that has data is zero-filled
  517. // so that the stacking happens in the proper order
  518. var required_times = [];
  519. if (scope.data.length > 1) {
  520. required_times = Array.prototype.concat.apply([], _.map(scope.data, function (query) {
  521. return query.time_series.getOrderedTimes();
  522. }));
  523. required_times = _.uniq(required_times.sort(function (a, b) {
  524. // decending numeric sort
  525. return a-b;
  526. }), true);
  527. }
  528. for (var i = 0; i < scope.data.length; i++) {
  529. var _d = scope.data[i].time_series.getFlotPairs(required_times);
  530. scope.data[i].data = _d;
  531. }
  532. var totalDataPoints = _.reduce(scope.data, function(num, series) { return series.data.length + num; }, 0);
  533. console.log('Datapoints[0] count:', scope.data[0].data.length);
  534. console.log('Datapoints.Total count:', totalDataPoints);
  535. scope.plot = $.plot(elem, scope.data, options);
  536. } catch(e) {
  537. console.log(e);
  538. // Nothing to do here
  539. }
  540. }
  541. function time_format(interval) {
  542. var _int = kbn.interval_to_seconds(interval);
  543. if(_int >= 2628000) {
  544. return "%Y-%m";
  545. }
  546. if(_int >= 86400) {
  547. return "%Y-%m-%d";
  548. }
  549. if(_int >= 60) {
  550. return "%H:%M<br>%m-%d";
  551. }
  552. return "%H:%M:%S";
  553. }
  554. var $tooltip = $('<div>');
  555. elem.bind("plothover", function (event, pos, item) {
  556. var group, value, timestamp;
  557. if (item) {
  558. if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
  559. group = '<small style="font-size:0.9em;">' +
  560. '<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
  561. (item.series.info.alias || item.series.info.query)+
  562. '</small><br>';
  563. } else {
  564. group = kbn.query_color_dot(item.series.color, 15) + ' ';
  565. }
  566. value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ?
  567. item.datapoint[1] - item.datapoint[2] :
  568. item.datapoint[1];
  569. if(scope.panel.y_format === 'bytes') {
  570. value = kbn.byteFormat(value,2);
  571. }
  572. if(scope.panel.y_format === 'short') {
  573. value = kbn.shortFormat(value,2);
  574. }
  575. timestamp = scope.panel.timezone === 'browser' ?
  576. moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
  577. moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
  578. $tooltip
  579. .html(
  580. group + value + " @ " + timestamp
  581. )
  582. .place_tt(pos.pageX, pos.pageY);
  583. } else {
  584. $tooltip.detach();
  585. }
  586. });
  587. elem.bind("plotselected", function (event, ranges) {
  588. filterSrv.set({
  589. type : 'time',
  590. from : moment.utc(ranges.xaxis.from).toDate(),
  591. to : moment.utc(ranges.xaxis.to).toDate(),
  592. field : scope.panel.time_field
  593. });
  594. });
  595. }
  596. };
  597. });
  598. });