graph.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. import "vendor/flot/jquery.flot";
  2. import "vendor/flot/jquery.flot.selection";
  3. import "vendor/flot/jquery.flot.time";
  4. import "vendor/flot/jquery.flot.stack";
  5. import "vendor/flot/jquery.flot.stackpercent";
  6. import "vendor/flot/jquery.flot.fillbelow";
  7. import "vendor/flot/jquery.flot.crosshair";
  8. import "vendor/flot/jquery.flot.dashes";
  9. import "./jquery.flot.events";
  10. import $ from "jquery";
  11. import _ from "lodash";
  12. import moment from "moment";
  13. import kbn from "app/core/utils/kbn";
  14. import { tickStep } from "app/core/utils/ticks";
  15. import { appEvents, coreModule } from "app/core/core";
  16. import GraphTooltip from "./graph_tooltip";
  17. import { ThresholdManager } from "./threshold_manager";
  18. import { EventManager } from "app/features/annotations/all";
  19. import { convertValuesToHistogram, getSeriesValues } from "./histogram";
  20. /** @ngInject **/
  21. function graphDirective(timeSrv, popoverSrv, contextSrv) {
  22. return {
  23. restrict: "A",
  24. template: "",
  25. link: function(scope, elem) {
  26. var ctrl = scope.ctrl;
  27. var dashboard = ctrl.dashboard;
  28. var panel = ctrl.panel;
  29. var annotations = [];
  30. var data;
  31. var plot;
  32. var sortedSeries;
  33. var panelWidth = 0;
  34. var eventManager = new EventManager(ctrl);
  35. var thresholdManager = new ThresholdManager(ctrl);
  36. var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
  37. return sortedSeries;
  38. });
  39. // panel events
  40. ctrl.events.on("panel-teardown", () => {
  41. thresholdManager = null;
  42. if (plot) {
  43. plot.destroy();
  44. plot = null;
  45. }
  46. });
  47. /**
  48. * Split graph rendering into two parts.
  49. * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
  50. * (see ctrl.events.on('render') in legend.ts).
  51. * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
  52. */
  53. ctrl.events.on("render", renderData => {
  54. data = renderData || data;
  55. if (!data) {
  56. return;
  57. }
  58. annotations = ctrl.annotations || [];
  59. buildFlotPairs(data);
  60. ctrl.events.emit("render-legend");
  61. });
  62. ctrl.events.on("legend-rendering-complete", () => {
  63. render_panel();
  64. });
  65. // global events
  66. appEvents.on(
  67. "graph-hover",
  68. evt => {
  69. // ignore other graph hover events if shared tooltip is disabled
  70. if (!dashboard.sharedTooltipModeEnabled()) {
  71. return;
  72. }
  73. // ignore if we are the emitter
  74. if (
  75. !plot ||
  76. evt.panel.id === panel.id ||
  77. ctrl.otherPanelInFullscreenMode()
  78. ) {
  79. return;
  80. }
  81. tooltip.show(evt.pos);
  82. },
  83. scope
  84. );
  85. appEvents.on(
  86. "graph-hover-clear",
  87. (event, info) => {
  88. if (plot) {
  89. tooltip.clear(plot);
  90. }
  91. },
  92. scope
  93. );
  94. function shouldAbortRender() {
  95. if (!data) {
  96. return true;
  97. }
  98. if (panelWidth === 0) {
  99. return true;
  100. }
  101. return false;
  102. }
  103. function drawHook(plot) {
  104. // add left axis labels
  105. if (panel.yaxes[0].label && panel.yaxes[0].show) {
  106. $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
  107. .text(panel.yaxes[0].label)
  108. .appendTo(elem);
  109. }
  110. // add right axis labels
  111. if (panel.yaxes[1].label && panel.yaxes[1].show) {
  112. $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
  113. .text(panel.yaxes[1].label)
  114. .appendTo(elem);
  115. }
  116. if (ctrl.dataWarning) {
  117. $(
  118. `<div class="datapoints-warning flot-temp-elem">${
  119. ctrl.dataWarning.title
  120. }</div>`
  121. ).appendTo(elem);
  122. }
  123. thresholdManager.draw(plot);
  124. }
  125. function processOffsetHook(plot, gridMargin) {
  126. var left = panel.yaxes[0];
  127. var right = panel.yaxes[1];
  128. if (left.show && left.label) {
  129. gridMargin.left = 20;
  130. }
  131. if (right.show && right.label) {
  132. gridMargin.right = 20;
  133. }
  134. // apply y-axis min/max options
  135. var yaxis = plot.getYAxes();
  136. for (var i = 0; i < yaxis.length; i++) {
  137. var axis = yaxis[i];
  138. var panelOptions = panel.yaxes[i];
  139. axis.options.max =
  140. axis.options.max !== null ? axis.options.max : panelOptions.max;
  141. axis.options.min =
  142. axis.options.min !== null ? axis.options.min : panelOptions.min;
  143. }
  144. }
  145. // Series could have different timeSteps,
  146. // let's find the smallest one so that bars are correctly rendered.
  147. // In addition, only take series which are rendered as bars for this.
  148. function getMinTimeStepOfSeries(data) {
  149. var min = Number.MAX_VALUE;
  150. for (let i = 0; i < data.length; i++) {
  151. if (!data[i].stats.timeStep) {
  152. continue;
  153. }
  154. if (panel.bars) {
  155. if (data[i].bars && data[i].bars.show === false) {
  156. continue;
  157. }
  158. } else {
  159. if (
  160. typeof data[i].bars === "undefined" ||
  161. typeof data[i].bars.show === "undefined" ||
  162. !data[i].bars.show
  163. ) {
  164. continue;
  165. }
  166. }
  167. if (data[i].stats.timeStep < min) {
  168. min = data[i].stats.timeStep;
  169. }
  170. }
  171. return min;
  172. }
  173. // Function for rendering panel
  174. function render_panel() {
  175. panelWidth = elem.width();
  176. if (shouldAbortRender()) {
  177. return;
  178. }
  179. // give space to alert editing
  180. thresholdManager.prepare(elem, data);
  181. // un-check dashes if lines are unchecked
  182. panel.dashes = panel.lines ? panel.dashes : false;
  183. // Populate element
  184. let options: any = buildFlotOptions(panel);
  185. prepareXAxis(options, panel);
  186. configureYAxisOptions(data, options);
  187. thresholdManager.addFlotOptions(options, panel);
  188. eventManager.addFlotEvents(annotations, options);
  189. sortedSeries = sortSeries(data, panel);
  190. callPlot(options, true);
  191. }
  192. function buildFlotPairs(data) {
  193. for (let i = 0; i < data.length; i++) {
  194. let series = data[i];
  195. series.data = series.getFlotPairs(
  196. series.nullPointMode || panel.nullPointMode
  197. );
  198. // if hidden remove points and disable stack
  199. if (ctrl.hiddenSeries[series.alias]) {
  200. series.data = [];
  201. series.stack = false;
  202. }
  203. }
  204. }
  205. function prepareXAxis(options, panel) {
  206. switch (panel.xaxis.mode) {
  207. case "series": {
  208. options.series.bars.barWidth = 0.7;
  209. options.series.bars.align = "center";
  210. for (let i = 0; i < data.length; i++) {
  211. let series = data[i];
  212. series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
  213. }
  214. addXSeriesAxis(options);
  215. break;
  216. }
  217. case "histogram": {
  218. let bucketSize: number;
  219. let values = getSeriesValues(data);
  220. if (data.length && values.length) {
  221. let histMin = _.min(_.map(data, s => s.stats.min));
  222. let histMax = _.max(_.map(data, s => s.stats.max));
  223. let ticks = panel.xaxis.buckets || panelWidth / 50;
  224. bucketSize = tickStep(histMin, histMax, ticks);
  225. let histogram = convertValuesToHistogram(values, bucketSize);
  226. data[0].data = histogram;
  227. options.series.bars.barWidth = bucketSize * 0.8;
  228. } else {
  229. bucketSize = 0;
  230. }
  231. addXHistogramAxis(options, bucketSize);
  232. break;
  233. }
  234. case "table": {
  235. options.series.bars.barWidth = 0.7;
  236. options.series.bars.align = "center";
  237. addXTableAxis(options);
  238. break;
  239. }
  240. default: {
  241. options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5;
  242. addTimeAxis(options);
  243. break;
  244. }
  245. }
  246. }
  247. function callPlot(options, incrementRenderCounter) {
  248. try {
  249. plot = $.plot(elem, sortedSeries, options);
  250. if (ctrl.renderError) {
  251. delete ctrl.error;
  252. delete ctrl.inspector;
  253. }
  254. } catch (e) {
  255. console.log("flotcharts error", e);
  256. ctrl.error = e.message || "Render Error";
  257. ctrl.renderError = true;
  258. ctrl.inspector = { error: e };
  259. }
  260. if (incrementRenderCounter) {
  261. ctrl.renderingCompleted();
  262. }
  263. }
  264. function buildFlotOptions(panel) {
  265. const stack = panel.stack ? true : null;
  266. let options = {
  267. hooks: {
  268. draw: [drawHook],
  269. processOffset: [processOffsetHook]
  270. },
  271. legend: { show: false },
  272. series: {
  273. stackpercent: panel.stack ? panel.percentage : false,
  274. stack: panel.percentage ? null : stack,
  275. lines: {
  276. show: panel.lines,
  277. zero: false,
  278. fill: translateFillOption(panel.fill),
  279. lineWidth: panel.dashes ? 0 : panel.linewidth,
  280. steps: panel.steppedLine
  281. },
  282. dashes: {
  283. show: panel.dashes,
  284. lineWidth: panel.linewidth,
  285. dashLength: [panel.dashLength, panel.spaceLength]
  286. },
  287. bars: {
  288. show: panel.bars,
  289. fill: 1,
  290. barWidth: 1,
  291. zero: false,
  292. lineWidth: 0
  293. },
  294. points: {
  295. show: panel.points,
  296. fill: 1,
  297. fillColor: false,
  298. radius: panel.points ? panel.pointradius : 2
  299. },
  300. shadowSize: 0
  301. },
  302. yaxes: [],
  303. xaxis: {},
  304. grid: {
  305. minBorderMargin: 0,
  306. markings: [],
  307. backgroundColor: null,
  308. borderWidth: 0,
  309. hoverable: true,
  310. clickable: true,
  311. color: "#c8c8c8",
  312. margin: { left: 0, right: 0 },
  313. labelMarginX: 0
  314. },
  315. selection: {
  316. mode: "x",
  317. color: "#666"
  318. },
  319. crosshair: {
  320. mode: "x"
  321. }
  322. };
  323. return options;
  324. }
  325. function sortSeries(series, panel) {
  326. var sortBy = panel.legend.sort;
  327. var sortOrder = panel.legend.sortDesc;
  328. var haveSortBy = sortBy !== null || sortBy !== undefined;
  329. var haveSortOrder = sortOrder !== null || sortOrder !== undefined;
  330. var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
  331. var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
  332. series.sort((x, y) => {
  333. if (x.zindex > y.zindex) {
  334. return 1;
  335. }
  336. if (x.zindex < y.zindex) {
  337. return -1;
  338. }
  339. if (shouldSortBy) {
  340. if (x.stats[sortBy] > y.stats[sortBy]) {
  341. return 1 * sortDesc;
  342. }
  343. if (x.stats[sortBy] < y.stats[sortBy]) {
  344. return -1 * sortDesc;
  345. }
  346. }
  347. return 0;
  348. });
  349. return series;
  350. }
  351. function translateFillOption(fill) {
  352. if (panel.percentage && panel.stack) {
  353. return fill === 0 ? 0.001 : fill / 10;
  354. } else {
  355. return fill / 10;
  356. }
  357. }
  358. function addTimeAxis(options) {
  359. var ticks = panelWidth / 100;
  360. var min = _.isUndefined(ctrl.range.from)
  361. ? null
  362. : ctrl.range.from.valueOf();
  363. var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
  364. options.xaxis = {
  365. timezone: dashboard.getTimezone(),
  366. show: panel.xaxis.show,
  367. mode: "time",
  368. min: min,
  369. max: max,
  370. label: "Datetime",
  371. ticks: ticks,
  372. timeformat: time_format(ticks, min, max)
  373. };
  374. }
  375. function addXSeriesAxis(options) {
  376. var ticks = _.map(data, function(series, index) {
  377. return [index + 1, series.alias];
  378. });
  379. options.xaxis = {
  380. timezone: dashboard.getTimezone(),
  381. show: panel.xaxis.show,
  382. mode: null,
  383. min: 0,
  384. max: ticks.length + 1,
  385. label: "Datetime",
  386. ticks: ticks
  387. };
  388. }
  389. function addXHistogramAxis(options, bucketSize) {
  390. let ticks, min, max;
  391. let defaultTicks = panelWidth / 50;
  392. if (data.length && bucketSize) {
  393. ticks = _.map(data[0].data, point => point[0]);
  394. min = _.min(ticks);
  395. max = _.max(ticks);
  396. // Adjust tick step
  397. let tickStep = bucketSize;
  398. let ticks_num = Math.floor((max - min) / tickStep);
  399. while (ticks_num > defaultTicks) {
  400. tickStep = tickStep * 2;
  401. ticks_num = Math.ceil((max - min) / tickStep);
  402. }
  403. // Expand ticks for pretty view
  404. min = Math.floor(min / tickStep) * tickStep;
  405. max = Math.ceil(max / tickStep) * tickStep;
  406. ticks = [];
  407. for (let i = min; i <= max; i += tickStep) {
  408. ticks.push(i);
  409. }
  410. } else {
  411. // Set defaults if no data
  412. ticks = defaultTicks / 2;
  413. min = 0;
  414. max = 1;
  415. }
  416. options.xaxis = {
  417. timezone: dashboard.getTimezone(),
  418. show: panel.xaxis.show,
  419. mode: null,
  420. min: min,
  421. max: max,
  422. label: "Histogram",
  423. ticks: ticks
  424. };
  425. // Use 'short' format for histogram values
  426. configureAxisMode(options.xaxis, "short");
  427. }
  428. function addXTableAxis(options) {
  429. var ticks = _.map(data, function(series, seriesIndex) {
  430. return _.map(series.datapoints, function(point, pointIndex) {
  431. var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
  432. return [tickIndex + 1, point[1]];
  433. });
  434. });
  435. ticks = _.flatten(ticks, true);
  436. options.xaxis = {
  437. timezone: dashboard.getTimezone(),
  438. show: panel.xaxis.show,
  439. mode: null,
  440. min: 0,
  441. max: ticks.length + 1,
  442. label: "Datetime",
  443. ticks: ticks
  444. };
  445. }
  446. function configureYAxisOptions(data, options) {
  447. var defaults = {
  448. position: "left",
  449. show: panel.yaxes[0].show,
  450. index: 1,
  451. logBase: panel.yaxes[0].logBase || 1,
  452. min: parseNumber(panel.yaxes[0].min),
  453. max: parseNumber(panel.yaxes[0].max),
  454. tickDecimals: panel.yaxes[0].decimals
  455. };
  456. options.yaxes.push(defaults);
  457. if (_.find(data, { yaxis: 2 })) {
  458. var secondY = _.clone(defaults);
  459. secondY.index = 2;
  460. secondY.show = panel.yaxes[1].show;
  461. secondY.logBase = panel.yaxes[1].logBase || 1;
  462. secondY.position = "right";
  463. secondY.min = parseNumber(panel.yaxes[1].min);
  464. secondY.max = parseNumber(panel.yaxes[1].max);
  465. secondY.tickDecimals = panel.yaxes[1].decimals;
  466. options.yaxes.push(secondY);
  467. applyLogScale(options.yaxes[1], data);
  468. configureAxisMode(
  469. options.yaxes[1],
  470. panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format
  471. );
  472. }
  473. applyLogScale(options.yaxes[0], data);
  474. configureAxisMode(
  475. options.yaxes[0],
  476. panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format
  477. );
  478. }
  479. function parseNumber(value: any) {
  480. if (value === null || typeof value === "undefined") {
  481. return null;
  482. }
  483. return _.toNumber(value);
  484. }
  485. function applyLogScale(axis, data) {
  486. if (axis.logBase === 1) {
  487. return;
  488. }
  489. const minSetToZero = axis.min === 0;
  490. if (axis.min < Number.MIN_VALUE) {
  491. axis.min = null;
  492. }
  493. if (axis.max < Number.MIN_VALUE) {
  494. axis.max = null;
  495. }
  496. var series, i;
  497. var max = axis.max,
  498. min = axis.min;
  499. for (i = 0; i < data.length; i++) {
  500. series = data[i];
  501. if (series.yaxis === axis.index) {
  502. if (!max || max < series.stats.max) {
  503. max = series.stats.max;
  504. }
  505. if (!min || min > series.stats.logmin) {
  506. min = series.stats.logmin;
  507. }
  508. }
  509. }
  510. axis.transform = function(v) {
  511. return v < Number.MIN_VALUE
  512. ? null
  513. : Math.log(v) / Math.log(axis.logBase);
  514. };
  515. axis.inverseTransform = function(v) {
  516. return Math.pow(axis.logBase, v);
  517. };
  518. if (!max && !min) {
  519. max = axis.inverseTransform(+2);
  520. min = axis.inverseTransform(-2);
  521. } else if (!max) {
  522. max = min * axis.inverseTransform(+4);
  523. } else if (!min) {
  524. min = max * axis.inverseTransform(-4);
  525. }
  526. if (axis.min) {
  527. min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
  528. } else {
  529. min = axis.min = axis.inverseTransform(
  530. Math.floor(axis.transform(min))
  531. );
  532. }
  533. if (axis.max) {
  534. max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
  535. } else {
  536. max = axis.max = axis.inverseTransform(
  537. Math.ceil(axis.transform(max))
  538. );
  539. }
  540. if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
  541. return;
  542. }
  543. if (Number.isFinite(min) && Number.isFinite(max)) {
  544. if (minSetToZero) {
  545. axis.min = 0.1;
  546. min = 1;
  547. }
  548. axis.ticks = generateTicksForLogScaleYAxis(min, max, axis.logBase);
  549. if (minSetToZero) {
  550. axis.ticks.unshift(0.1);
  551. }
  552. if (axis.ticks[axis.ticks.length - 1] > axis.max) {
  553. axis.max = axis.ticks[axis.ticks.length - 1];
  554. }
  555. } else {
  556. axis.ticks = [1, 2];
  557. delete axis.min;
  558. delete axis.max;
  559. }
  560. }
  561. function generateTicksForLogScaleYAxis(min, max, logBase) {
  562. let ticks = [];
  563. var nextTick;
  564. for (nextTick = min; nextTick <= max; nextTick *= logBase) {
  565. ticks.push(nextTick);
  566. }
  567. const maxNumTicks = Math.ceil(ctrl.height / 25);
  568. const numTicks = ticks.length;
  569. if (numTicks > maxNumTicks) {
  570. const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
  571. ticks = [];
  572. for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
  573. ticks.push(nextTick);
  574. }
  575. }
  576. return ticks;
  577. }
  578. function configureAxisMode(axis, format) {
  579. axis.tickFormatter = function(val, axis) {
  580. return kbn.valueFormats[format](
  581. val,
  582. axis.tickDecimals,
  583. axis.scaledDecimals
  584. );
  585. };
  586. }
  587. function time_format(ticks, min, max) {
  588. if (min && max && ticks) {
  589. var range = max - min;
  590. var secPerTick = range / ticks / 1000;
  591. var oneDay = 86400000;
  592. var oneYear = 31536000000;
  593. if (secPerTick <= 45) {
  594. return "%H:%M:%S";
  595. }
  596. if (secPerTick <= 7200 || range <= oneDay) {
  597. return "%H:%M";
  598. }
  599. if (secPerTick <= 80000) {
  600. return "%m/%d %H:%M";
  601. }
  602. if (secPerTick <= 2419200 || range <= oneYear) {
  603. return "%m/%d";
  604. }
  605. return "%Y-%m";
  606. }
  607. return "%H:%M";
  608. }
  609. elem.bind("plotselected", function(event, ranges) {
  610. if (panel.xaxis.mode !== "time") {
  611. // Skip if panel in histogram or series mode
  612. plot.clearSelection();
  613. return;
  614. }
  615. if ((ranges.ctrlKey || ranges.metaKey) && contextSrv.isEditor) {
  616. // Add annotation
  617. setTimeout(() => {
  618. eventManager.updateTime(ranges.xaxis);
  619. }, 100);
  620. } else {
  621. scope.$apply(function() {
  622. timeSrv.setTime({
  623. from: moment.utc(ranges.xaxis.from),
  624. to: moment.utc(ranges.xaxis.to)
  625. });
  626. });
  627. }
  628. });
  629. elem.bind("plotclick", function(event, pos, item) {
  630. if (panel.xaxis.mode !== "time") {
  631. // Skip if panel in histogram or series mode
  632. return;
  633. }
  634. if ((pos.ctrlKey || pos.metaKey) && contextSrv.isEditor) {
  635. // Skip if range selected (added in "plotselected" event handler)
  636. let isRangeSelection = pos.x !== pos.x1;
  637. if (!isRangeSelection) {
  638. setTimeout(() => {
  639. eventManager.updateTime({ from: pos.x, to: null });
  640. }, 100);
  641. }
  642. }
  643. });
  644. scope.$on("$destroy", function() {
  645. tooltip.destroy();
  646. elem.off();
  647. elem.remove();
  648. });
  649. }
  650. };
  651. }
  652. coreModule.directive("grafanaGraph", graphDirective);