module.js 22 KB

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