rendering.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. import _ from "lodash";
  2. import $ from "jquery";
  3. import moment from "moment";
  4. import * as d3 from "d3";
  5. import kbn from "app/core/utils/kbn";
  6. import { appEvents, contextSrv } from "app/core/core";
  7. import {
  8. tickStep,
  9. getScaledDecimals,
  10. getFlotTickSize
  11. } from "app/core/utils/ticks";
  12. import { HeatmapTooltip } from "./heatmap_tooltip";
  13. import { mergeZeroBuckets } from "./heatmap_data_converter";
  14. import { getColorScale, getOpacityScale } from "./color_scale";
  15. let MIN_CARD_SIZE = 1,
  16. CARD_PADDING = 1,
  17. CARD_ROUND = 0,
  18. DATA_RANGE_WIDING_FACTOR = 1.2,
  19. DEFAULT_X_TICK_SIZE_PX = 100,
  20. DEFAULT_Y_TICK_SIZE_PX = 50,
  21. X_AXIS_TICK_PADDING = 10,
  22. Y_AXIS_TICK_PADDING = 5,
  23. MIN_SELECTION_WIDTH = 2;
  24. export default function link(scope, elem, attrs, ctrl) {
  25. let data, timeRange, panel, heatmap;
  26. // $heatmap is JQuery object, but heatmap is D3
  27. let $heatmap = elem.find(".heatmap-panel");
  28. let tooltip = new HeatmapTooltip($heatmap, scope);
  29. let width,
  30. height,
  31. yScale,
  32. xScale,
  33. chartWidth,
  34. chartHeight,
  35. chartTop,
  36. chartBottom,
  37. yAxisWidth,
  38. xAxisHeight,
  39. cardPadding,
  40. cardRound,
  41. cardWidth,
  42. cardHeight,
  43. colorScale,
  44. opacityScale,
  45. mouseUpHandler;
  46. let selection = {
  47. active: false,
  48. x1: -1,
  49. x2: -1
  50. };
  51. let padding = { left: 0, right: 0, top: 0, bottom: 0 },
  52. margin = { left: 25, right: 15, top: 10, bottom: 20 },
  53. dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
  54. ctrl.events.on("render", () => {
  55. render();
  56. ctrl.renderingCompleted();
  57. });
  58. function setElementHeight() {
  59. try {
  60. var height = ctrl.height || panel.height || ctrl.row.height;
  61. if (_.isString(height)) {
  62. height = parseInt(height.replace("px", ""), 10);
  63. }
  64. height -= 5; // padding
  65. height -= panel.title ? 24 : 9; // subtract panel title bar
  66. $heatmap.css("height", height + "px");
  67. return true;
  68. } catch (e) {
  69. // IE throws errors sometimes
  70. return false;
  71. }
  72. }
  73. function getYAxisWidth(elem) {
  74. let axis_text = elem.selectAll(".axis-y text").nodes();
  75. let max_text_width = _.max(
  76. _.map(axis_text, text => {
  77. // Use SVG getBBox method
  78. return text.getBBox().width;
  79. })
  80. );
  81. return max_text_width;
  82. }
  83. function getXAxisHeight(elem) {
  84. let axis_line = elem.select(".axis-x line");
  85. if (!axis_line.empty()) {
  86. let axis_line_position = parseFloat(
  87. elem.select(".axis-x line").attr("y2")
  88. );
  89. let canvas_width = parseFloat(elem.attr("height"));
  90. return canvas_width - axis_line_position;
  91. } else {
  92. // Default height
  93. return 30;
  94. }
  95. }
  96. function addXAxis() {
  97. scope.xScale = xScale = d3
  98. .scaleTime()
  99. .domain([timeRange.from, timeRange.to])
  100. .range([0, chartWidth]);
  101. let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
  102. let grafanaTimeFormatter = grafanaTimeFormat(
  103. ticks,
  104. timeRange.from,
  105. timeRange.to
  106. );
  107. let timeFormat;
  108. let dashboardTimeZone = ctrl.dashboard.getTimezone();
  109. if (dashboardTimeZone === "utc") {
  110. timeFormat = d3.utcFormat(grafanaTimeFormatter);
  111. } else {
  112. timeFormat = d3.timeFormat(grafanaTimeFormatter);
  113. }
  114. let xAxis = d3
  115. .axisBottom(xScale)
  116. .ticks(ticks)
  117. .tickFormat(timeFormat)
  118. .tickPadding(X_AXIS_TICK_PADDING)
  119. .tickSize(chartHeight);
  120. let posY = margin.top;
  121. let posX = yAxisWidth;
  122. heatmap
  123. .append("g")
  124. .attr("class", "axis axis-x")
  125. .attr("transform", "translate(" + posX + "," + posY + ")")
  126. .call(xAxis);
  127. // Remove horizontal line in the top of axis labels (called domain in d3)
  128. heatmap
  129. .select(".axis-x")
  130. .select(".domain")
  131. .remove();
  132. }
  133. function addYAxis() {
  134. let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
  135. let tick_interval = tickStep(
  136. data.heatmapStats.min,
  137. data.heatmapStats.max,
  138. ticks
  139. );
  140. let { y_min, y_max } = wideYAxisRange(
  141. data.heatmapStats.min,
  142. data.heatmapStats.max,
  143. tick_interval
  144. );
  145. // Rewrite min and max if it have been set explicitly
  146. y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
  147. y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
  148. // Adjust ticks after Y range widening
  149. tick_interval = tickStep(y_min, y_max, ticks);
  150. ticks = Math.ceil((y_max - y_min) / tick_interval);
  151. let decimalsAuto = getPrecision(tick_interval);
  152. let decimals =
  153. panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
  154. // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
  155. let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
  156. let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
  157. ctrl.decimals = decimals;
  158. ctrl.scaledDecimals = scaledDecimals;
  159. // Set default Y min and max if no data
  160. if (_.isEmpty(data.buckets)) {
  161. y_max = 1;
  162. y_min = -1;
  163. ticks = 3;
  164. decimals = 1;
  165. }
  166. data.yAxis = {
  167. min: y_min,
  168. max: y_max,
  169. ticks: ticks
  170. };
  171. scope.yScale = yScale = d3
  172. .scaleLinear()
  173. .domain([y_min, y_max])
  174. .range([chartHeight, 0]);
  175. let yAxis = d3
  176. .axisLeft(yScale)
  177. .ticks(ticks)
  178. .tickFormat(tickValueFormatter(decimals, scaledDecimals))
  179. .tickSizeInner(0 - width)
  180. .tickSizeOuter(0)
  181. .tickPadding(Y_AXIS_TICK_PADDING);
  182. heatmap
  183. .append("g")
  184. .attr("class", "axis axis-y")
  185. .call(yAxis);
  186. // Calculate Y axis width first, then move axis into visible area
  187. let posY = margin.top;
  188. let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  189. heatmap
  190. .select(".axis-y")
  191. .attr("transform", "translate(" + posX + "," + posY + ")");
  192. // Remove vertical line in the right of axis labels (called domain in d3)
  193. heatmap
  194. .select(".axis-y")
  195. .select(".domain")
  196. .remove();
  197. }
  198. // Wide Y values range and anjust to bucket size
  199. function wideYAxisRange(min, max, tickInterval) {
  200. let y_widing =
  201. (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) /
  202. 2;
  203. let y_min, y_max;
  204. if (tickInterval === 0) {
  205. y_max = max * dataRangeWidingFactor;
  206. y_min = min - min * (dataRangeWidingFactor - 1);
  207. tickInterval = (y_max - y_min) / 2;
  208. } else {
  209. y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
  210. y_min = Math.floor((min - y_widing) / tickInterval) * tickInterval;
  211. }
  212. // Don't wide axis below 0 if all values are positive
  213. if (min >= 0 && y_min < 0) {
  214. y_min = 0;
  215. }
  216. return { y_min, y_max };
  217. }
  218. function addLogYAxis() {
  219. let log_base = panel.yAxis.logBase;
  220. let { y_min, y_max } = adjustLogRange(
  221. data.heatmapStats.minLog,
  222. data.heatmapStats.max,
  223. log_base
  224. );
  225. y_min =
  226. panel.yAxis.min && panel.yAxis.min !== "0"
  227. ? adjustLogMin(panel.yAxis.min, log_base)
  228. : y_min;
  229. y_max =
  230. panel.yAxis.max !== null
  231. ? adjustLogMax(panel.yAxis.max, log_base)
  232. : y_max;
  233. // Set default Y min and max if no data
  234. if (_.isEmpty(data.buckets)) {
  235. y_max = Math.pow(log_base, 2);
  236. y_min = 1;
  237. }
  238. scope.yScale = yScale = d3
  239. .scaleLog()
  240. .base(panel.yAxis.logBase)
  241. .domain([y_min, y_max])
  242. .range([chartHeight, 0]);
  243. let domain = yScale.domain();
  244. let tick_values = logScaleTickValues(domain, log_base);
  245. let decimalsAuto = getPrecision(y_min);
  246. let decimals = panel.yAxis.decimals || decimalsAuto;
  247. // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
  248. let flot_tick_size = getFlotTickSize(
  249. y_min,
  250. y_max,
  251. tick_values.length,
  252. decimalsAuto
  253. );
  254. let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
  255. ctrl.decimals = decimals;
  256. ctrl.scaledDecimals = scaledDecimals;
  257. data.yAxis = {
  258. min: y_min,
  259. max: y_max,
  260. ticks: tick_values.length
  261. };
  262. let yAxis = d3
  263. .axisLeft(yScale)
  264. .tickValues(tick_values)
  265. .tickFormat(tickValueFormatter(decimals, scaledDecimals))
  266. .tickSizeInner(0 - width)
  267. .tickSizeOuter(0)
  268. .tickPadding(Y_AXIS_TICK_PADDING);
  269. heatmap
  270. .append("g")
  271. .attr("class", "axis axis-y")
  272. .call(yAxis);
  273. // Calculate Y axis width first, then move axis into visible area
  274. let posY = margin.top;
  275. let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  276. heatmap
  277. .select(".axis-y")
  278. .attr("transform", "translate(" + posX + "," + posY + ")");
  279. // Set first tick as pseudo 0
  280. if (y_min < 1) {
  281. heatmap
  282. .select(".axis-y")
  283. .select(".tick text")
  284. .text("0");
  285. }
  286. // Remove vertical line in the right of axis labels (called domain in d3)
  287. heatmap
  288. .select(".axis-y")
  289. .select(".domain")
  290. .remove();
  291. }
  292. // Adjust data range to log base
  293. function adjustLogRange(min, max, logBase) {
  294. let y_min, y_max;
  295. y_min = data.heatmapStats.minLog;
  296. if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
  297. y_min = 1;
  298. } else {
  299. y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
  300. }
  301. // Adjust max Y value to log base
  302. y_max = adjustLogMax(data.heatmapStats.max, logBase);
  303. return { y_min, y_max };
  304. }
  305. function adjustLogMax(max, base) {
  306. return Math.pow(base, Math.ceil(logp(max, base)));
  307. }
  308. function adjustLogMin(min, base) {
  309. return Math.pow(base, Math.floor(logp(min, base)));
  310. }
  311. function logScaleTickValues(domain, base) {
  312. let domainMin = domain[0];
  313. let domainMax = domain[1];
  314. let tickValues = [];
  315. if (domainMin < 1) {
  316. let under_one_ticks = Math.floor(logp(domainMin, base));
  317. for (let i = under_one_ticks; i < 0; i++) {
  318. let tick_value = Math.pow(base, i);
  319. tickValues.push(tick_value);
  320. }
  321. }
  322. let ticks = Math.ceil(logp(domainMax, base));
  323. for (let i = 0; i <= ticks; i++) {
  324. let tick_value = Math.pow(base, i);
  325. tickValues.push(tick_value);
  326. }
  327. return tickValues;
  328. }
  329. function tickValueFormatter(decimals, scaledDecimals = null) {
  330. let format = panel.yAxis.format;
  331. return function(value) {
  332. return kbn.valueFormats[format](value, decimals, scaledDecimals);
  333. };
  334. }
  335. function fixYAxisTickSize() {
  336. heatmap
  337. .select(".axis-y")
  338. .selectAll(".tick line")
  339. .attr("x2", chartWidth);
  340. }
  341. function addAxes() {
  342. chartHeight = height - margin.top - margin.bottom;
  343. chartTop = margin.top;
  344. chartBottom = chartTop + chartHeight;
  345. if (panel.yAxis.logBase === 1) {
  346. addYAxis();
  347. } else {
  348. addLogYAxis();
  349. }
  350. yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  351. chartWidth = width - yAxisWidth - margin.right;
  352. fixYAxisTickSize();
  353. addXAxis();
  354. xAxisHeight = getXAxisHeight(heatmap);
  355. if (!panel.yAxis.show) {
  356. heatmap
  357. .select(".axis-y")
  358. .selectAll("line")
  359. .style("opacity", 0);
  360. }
  361. if (!panel.xAxis.show) {
  362. heatmap
  363. .select(".axis-x")
  364. .selectAll("line")
  365. .style("opacity", 0);
  366. }
  367. }
  368. function addHeatmapCanvas() {
  369. let heatmap_elem = $heatmap[0];
  370. width = Math.floor($heatmap.width()) - padding.right;
  371. height = Math.floor($heatmap.height()) - padding.bottom;
  372. cardPadding =
  373. panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
  374. cardRound =
  375. panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
  376. if (heatmap) {
  377. heatmap.remove();
  378. }
  379. heatmap = d3
  380. .select(heatmap_elem)
  381. .append("svg")
  382. .attr("width", width)
  383. .attr("height", height);
  384. }
  385. function addHeatmap() {
  386. addHeatmapCanvas();
  387. addAxes();
  388. if (panel.yAxis.logBase !== 1) {
  389. let log_base = panel.yAxis.logBase;
  390. let domain = yScale.domain();
  391. let tick_values = logScaleTickValues(domain, log_base);
  392. data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
  393. }
  394. let cardsData = data.cards;
  395. let maxValueAuto = data.cardStats.max;
  396. let maxValue = panel.color.max || maxValueAuto;
  397. let minValue = panel.color.min || 0;
  398. let colorScheme = _.find(ctrl.colorSchemes, {
  399. value: panel.color.colorScheme
  400. });
  401. colorScale = getColorScale(
  402. colorScheme,
  403. contextSrv.user.lightTheme,
  404. maxValue,
  405. minValue
  406. );
  407. opacityScale = getOpacityScale(panel.color, maxValue);
  408. setCardSize();
  409. let cards = heatmap.selectAll(".heatmap-card").data(cardsData);
  410. cards.append("title");
  411. cards = cards
  412. .enter()
  413. .append("rect")
  414. .attr("x", getCardX)
  415. .attr("width", getCardWidth)
  416. .attr("y", getCardY)
  417. .attr("height", getCardHeight)
  418. .attr("rx", cardRound)
  419. .attr("ry", cardRound)
  420. .attr("class", "bordered heatmap-card")
  421. .style("fill", getCardColor)
  422. .style("stroke", getCardColor)
  423. .style("stroke-width", 0)
  424. .style("opacity", getCardOpacity);
  425. let $cards = $heatmap.find(".heatmap-card");
  426. $cards
  427. .on("mouseenter", event => {
  428. tooltip.mouseOverBucket = true;
  429. highlightCard(event);
  430. })
  431. .on("mouseleave", event => {
  432. tooltip.mouseOverBucket = false;
  433. resetCardHighLight(event);
  434. });
  435. }
  436. function highlightCard(event) {
  437. let color = d3.select(event.target).style("fill");
  438. let highlightColor = d3.color(color).darker(2);
  439. let strokeColor = d3.color(color).brighter(4);
  440. let current_card = d3.select(event.target);
  441. tooltip.originalFillColor = color;
  442. current_card
  443. .style("fill", highlightColor.toString())
  444. .style("stroke", strokeColor.toString())
  445. .style("stroke-width", 1);
  446. }
  447. function resetCardHighLight(event) {
  448. d3
  449. .select(event.target)
  450. .style("fill", tooltip.originalFillColor)
  451. .style("stroke", tooltip.originalFillColor)
  452. .style("stroke-width", 0);
  453. }
  454. function setCardSize() {
  455. let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
  456. let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));
  457. if (panel.yAxis.logBase !== 1) {
  458. let base = panel.yAxis.logBase;
  459. let splitFactor = data.yBucketSize || 1;
  460. yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
  461. }
  462. cardWidth = xGridSize - cardPadding * 2;
  463. cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
  464. }
  465. function getCardX(d) {
  466. let x;
  467. if (xScale(d.x) < 0) {
  468. // Cut card left to prevent overlay
  469. x = yAxisWidth + cardPadding;
  470. } else {
  471. x = xScale(d.x) + yAxisWidth + cardPadding;
  472. }
  473. return x;
  474. }
  475. function getCardWidth(d) {
  476. let w;
  477. if (xScale(d.x) < 0) {
  478. // Cut card left to prevent overlay
  479. let cutted_width = xScale(d.x) + cardWidth;
  480. w = cutted_width > 0 ? cutted_width : 0;
  481. } else if (xScale(d.x) + cardWidth > chartWidth) {
  482. // Cut card right to prevent overlay
  483. w = chartWidth - xScale(d.x) - cardPadding;
  484. } else {
  485. w = cardWidth;
  486. }
  487. // Card width should be MIN_CARD_SIZE at least
  488. w = Math.max(w, MIN_CARD_SIZE);
  489. return w;
  490. }
  491. function getCardY(d) {
  492. let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
  493. if (panel.yAxis.logBase !== 1 && d.y === 0) {
  494. y = chartBottom - cardHeight - cardPadding;
  495. } else {
  496. if (y < chartTop) {
  497. y = chartTop;
  498. }
  499. }
  500. return y;
  501. }
  502. function getCardHeight(d) {
  503. let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
  504. let h = cardHeight;
  505. if (panel.yAxis.logBase !== 1 && d.y === 0) {
  506. return cardHeight;
  507. }
  508. // Cut card height to prevent overlay
  509. if (y < chartTop) {
  510. h = yScale(d.y) - cardPadding;
  511. } else if (yScale(d.y) > chartBottom) {
  512. h = chartBottom - y;
  513. } else if (y + cardHeight > chartBottom) {
  514. h = chartBottom - y;
  515. }
  516. // Height can't be more than chart height
  517. h = Math.min(h, chartHeight);
  518. // Card height should be MIN_CARD_SIZE at least
  519. h = Math.max(h, MIN_CARD_SIZE);
  520. return h;
  521. }
  522. function getCardColor(d) {
  523. if (panel.color.mode === "opacity") {
  524. return panel.color.cardColor;
  525. } else {
  526. return colorScale(d.count);
  527. }
  528. }
  529. function getCardOpacity(d) {
  530. if (panel.color.mode === "opacity") {
  531. return opacityScale(d.count);
  532. } else {
  533. return 1;
  534. }
  535. }
  536. /////////////////////////////
  537. // Selection and crosshair //
  538. /////////////////////////////
  539. // Shared crosshair and tooltip
  540. appEvents.on(
  541. "graph-hover",
  542. event => {
  543. drawSharedCrosshair(event.pos);
  544. },
  545. scope
  546. );
  547. appEvents.on(
  548. "graph-hover-clear",
  549. () => {
  550. clearCrosshair();
  551. },
  552. scope
  553. );
  554. function onMouseDown(event) {
  555. selection.active = true;
  556. selection.x1 = event.offsetX;
  557. mouseUpHandler = function() {
  558. onMouseUp();
  559. };
  560. $(document).one("mouseup", mouseUpHandler);
  561. }
  562. function onMouseUp() {
  563. $(document).unbind("mouseup", mouseUpHandler);
  564. mouseUpHandler = null;
  565. selection.active = false;
  566. let selectionRange = Math.abs(selection.x2 - selection.x1);
  567. if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
  568. let timeFrom = xScale.invert(
  569. Math.min(selection.x1, selection.x2) - yAxisWidth
  570. );
  571. let timeTo = xScale.invert(
  572. Math.max(selection.x1, selection.x2) - yAxisWidth
  573. );
  574. ctrl.timeSrv.setTime({
  575. from: moment.utc(timeFrom),
  576. to: moment.utc(timeTo)
  577. });
  578. }
  579. clearSelection();
  580. }
  581. function onMouseLeave() {
  582. appEvents.emit("graph-hover-clear");
  583. clearCrosshair();
  584. }
  585. function onMouseMove(event) {
  586. if (!heatmap) {
  587. return;
  588. }
  589. if (selection.active) {
  590. // Clear crosshair and tooltip
  591. clearCrosshair();
  592. tooltip.destroy();
  593. selection.x2 = limitSelection(event.offsetX);
  594. drawSelection(selection.x1, selection.x2);
  595. } else {
  596. emitGraphHoverEvet(event);
  597. drawCrosshair(event.offsetX);
  598. tooltip.show(event, data);
  599. }
  600. }
  601. function emitGraphHoverEvet(event) {
  602. let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
  603. let y = yScale.invert(event.offsetY);
  604. let pos = {
  605. pageX: event.pageX,
  606. pageY: event.pageY,
  607. x: x,
  608. x1: x,
  609. y: y,
  610. y1: y,
  611. panelRelY: null
  612. };
  613. // Set minimum offset to prevent showing legend from another panel
  614. pos.panelRelY = Math.max(event.offsetY / height, 0.001);
  615. // broadcast to other graph panels that we are hovering
  616. appEvents.emit("graph-hover", { pos: pos, panel: panel });
  617. }
  618. function limitSelection(x2) {
  619. x2 = Math.max(x2, yAxisWidth);
  620. x2 = Math.min(x2, chartWidth + yAxisWidth);
  621. return x2;
  622. }
  623. function drawSelection(posX1, posX2) {
  624. if (heatmap) {
  625. heatmap.selectAll(".heatmap-selection").remove();
  626. let selectionX = Math.min(posX1, posX2);
  627. let selectionWidth = Math.abs(posX1 - posX2);
  628. if (selectionWidth > MIN_SELECTION_WIDTH) {
  629. heatmap
  630. .append("rect")
  631. .attr("class", "heatmap-selection")
  632. .attr("x", selectionX)
  633. .attr("width", selectionWidth)
  634. .attr("y", chartTop)
  635. .attr("height", chartHeight);
  636. }
  637. }
  638. }
  639. function clearSelection() {
  640. selection.x1 = -1;
  641. selection.x2 = -1;
  642. if (heatmap) {
  643. heatmap.selectAll(".heatmap-selection").remove();
  644. }
  645. }
  646. function drawCrosshair(position) {
  647. if (heatmap) {
  648. heatmap.selectAll(".heatmap-crosshair").remove();
  649. let posX = position;
  650. posX = Math.max(posX, yAxisWidth);
  651. posX = Math.min(posX, chartWidth + yAxisWidth);
  652. heatmap
  653. .append("g")
  654. .attr("class", "heatmap-crosshair")
  655. .attr("transform", "translate(" + posX + ",0)")
  656. .append("line")
  657. .attr("x1", 1)
  658. .attr("y1", chartTop)
  659. .attr("x2", 1)
  660. .attr("y2", chartBottom)
  661. .attr("stroke-width", 1);
  662. }
  663. }
  664. function drawSharedCrosshair(pos) {
  665. if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
  666. let posX = xScale(pos.x) + yAxisWidth;
  667. drawCrosshair(posX);
  668. }
  669. }
  670. function clearCrosshair() {
  671. if (heatmap) {
  672. heatmap.selectAll(".heatmap-crosshair").remove();
  673. }
  674. }
  675. function render() {
  676. data = ctrl.data;
  677. panel = ctrl.panel;
  678. timeRange = ctrl.range;
  679. if (!setElementHeight() || !data) {
  680. return;
  681. }
  682. // Draw default axes and return if no data
  683. if (_.isEmpty(data.buckets)) {
  684. addHeatmapCanvas();
  685. addAxes();
  686. return;
  687. }
  688. addHeatmap();
  689. scope.yAxisWidth = yAxisWidth;
  690. scope.xAxisHeight = xAxisHeight;
  691. scope.chartHeight = chartHeight;
  692. scope.chartWidth = chartWidth;
  693. scope.chartTop = chartTop;
  694. }
  695. // Register selection listeners
  696. $heatmap.on("mousedown", onMouseDown);
  697. $heatmap.on("mousemove", onMouseMove);
  698. $heatmap.on("mouseleave", onMouseLeave);
  699. }
  700. function grafanaTimeFormat(ticks, min, max) {
  701. if (min && max && ticks) {
  702. let range = max - min;
  703. let secPerTick = range / ticks / 1000;
  704. let oneDay = 86400000;
  705. let oneYear = 31536000000;
  706. if (secPerTick <= 45) {
  707. return "%H:%M:%S";
  708. }
  709. if (secPerTick <= 7200 || range <= oneDay) {
  710. return "%H:%M";
  711. }
  712. if (secPerTick <= 80000) {
  713. return "%m/%d %H:%M";
  714. }
  715. if (secPerTick <= 2419200 || range <= oneYear) {
  716. return "%m/%d";
  717. }
  718. return "%Y-%m";
  719. }
  720. return "%H:%M";
  721. }
  722. function logp(value, base) {
  723. return Math.log(value) / Math.log(base);
  724. }
  725. function getPrecision(num) {
  726. let str = num.toString();
  727. let dot_index = str.indexOf(".");
  728. if (dot_index === -1) {
  729. return 0;
  730. } else {
  731. return str.length - dot_index - 1;
  732. }
  733. }