rendering.ts 21 KB

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