rendering.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  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 -= panel.legend.show ? 28 : 11; // bottom padding and space for legend
  61. $heatmap.css('height', height + 'px');
  62. return true;
  63. } catch (e) {
  64. // IE throws errors sometimes
  65. return false;
  66. }
  67. }
  68. function getYAxisWidth(elem) {
  69. let axis_text = elem.selectAll('.axis-y text').nodes();
  70. let max_text_width = _.max(
  71. _.map(axis_text, text => {
  72. // Use SVG getBBox method
  73. return text.getBBox().width;
  74. })
  75. );
  76. return max_text_width;
  77. }
  78. function getXAxisHeight(elem) {
  79. let axis_line = elem.select('.axis-x line');
  80. if (!axis_line.empty()) {
  81. let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2'));
  82. let canvas_width = parseFloat(elem.attr('height'));
  83. return canvas_width - axis_line_position;
  84. } else {
  85. // Default height
  86. return 30;
  87. }
  88. }
  89. function addXAxis() {
  90. scope.xScale = xScale = d3
  91. .scaleTime()
  92. .domain([timeRange.from, timeRange.to])
  93. .range([0, chartWidth]);
  94. let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
  95. let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
  96. let timeFormat;
  97. let dashboardTimeZone = ctrl.dashboard.getTimezone();
  98. if (dashboardTimeZone === 'utc') {
  99. timeFormat = d3.utcFormat(grafanaTimeFormatter);
  100. } else {
  101. timeFormat = d3.timeFormat(grafanaTimeFormatter);
  102. }
  103. let xAxis = d3
  104. .axisBottom(xScale)
  105. .ticks(ticks)
  106. .tickFormat(timeFormat)
  107. .tickPadding(X_AXIS_TICK_PADDING)
  108. .tickSize(chartHeight);
  109. let posY = margin.top;
  110. let posX = yAxisWidth;
  111. heatmap
  112. .append('g')
  113. .attr('class', 'axis axis-x')
  114. .attr('transform', 'translate(' + posX + ',' + posY + ')')
  115. .call(xAxis);
  116. // Remove horizontal line in the top of axis labels (called domain in d3)
  117. heatmap
  118. .select('.axis-x')
  119. .select('.domain')
  120. .remove();
  121. }
  122. function addYAxis() {
  123. let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
  124. let tick_interval = tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks);
  125. let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);
  126. // Rewrite min and max if it have been set explicitly
  127. y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
  128. y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
  129. // Adjust ticks after Y range widening
  130. tick_interval = tickStep(y_min, y_max, ticks);
  131. ticks = Math.ceil((y_max - y_min) / tick_interval);
  132. let decimalsAuto = getPrecision(tick_interval);
  133. let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
  134. // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
  135. let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
  136. let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
  137. ctrl.decimals = decimals;
  138. ctrl.scaledDecimals = scaledDecimals;
  139. // Set default Y min and max if no data
  140. if (_.isEmpty(data.buckets)) {
  141. y_max = 1;
  142. y_min = -1;
  143. ticks = 3;
  144. decimals = 1;
  145. }
  146. data.yAxis = {
  147. min: y_min,
  148. max: y_max,
  149. ticks: ticks,
  150. };
  151. scope.yScale = yScale = d3
  152. .scaleLinear()
  153. .domain([y_min, y_max])
  154. .range([chartHeight, 0]);
  155. let yAxis = d3
  156. .axisLeft(yScale)
  157. .ticks(ticks)
  158. .tickFormat(tickValueFormatter(decimals, scaledDecimals))
  159. .tickSizeInner(0 - width)
  160. .tickSizeOuter(0)
  161. .tickPadding(Y_AXIS_TICK_PADDING);
  162. heatmap
  163. .append('g')
  164. .attr('class', 'axis axis-y')
  165. .call(yAxis);
  166. // Calculate Y axis width first, then move axis into visible area
  167. let posY = margin.top;
  168. let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  169. heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
  170. // Remove vertical line in the right of axis labels (called domain in d3)
  171. heatmap
  172. .select('.axis-y')
  173. .select('.domain')
  174. .remove();
  175. }
  176. // Wide Y values range and anjust to bucket size
  177. function wideYAxisRange(min, max, tickInterval) {
  178. let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
  179. let y_min, y_max;
  180. if (tickInterval === 0) {
  181. y_max = max * dataRangeWidingFactor;
  182. y_min = min - min * (dataRangeWidingFactor - 1);
  183. tickInterval = (y_max - y_min) / 2;
  184. } else {
  185. y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
  186. y_min = Math.floor((min - y_widing) / tickInterval) * tickInterval;
  187. }
  188. // Don't wide axis below 0 if all values are positive
  189. if (min >= 0 && y_min < 0) {
  190. y_min = 0;
  191. }
  192. return { y_min, y_max };
  193. }
  194. function addLogYAxis() {
  195. let log_base = panel.yAxis.logBase;
  196. let { y_min, y_max } = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base);
  197. y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min;
  198. y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max;
  199. // Set default Y min and max if no data
  200. if (_.isEmpty(data.buckets)) {
  201. y_max = Math.pow(log_base, 2);
  202. y_min = 1;
  203. }
  204. scope.yScale = yScale = d3
  205. .scaleLog()
  206. .base(panel.yAxis.logBase)
  207. .domain([y_min, y_max])
  208. .range([chartHeight, 0]);
  209. let domain = yScale.domain();
  210. let tick_values = logScaleTickValues(domain, log_base);
  211. let decimalsAuto = getPrecision(y_min);
  212. let decimals = panel.yAxis.decimals || decimalsAuto;
  213. // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
  214. let flot_tick_size = getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
  215. let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
  216. ctrl.decimals = decimals;
  217. ctrl.scaledDecimals = scaledDecimals;
  218. data.yAxis = {
  219. min: y_min,
  220. max: y_max,
  221. ticks: tick_values.length,
  222. };
  223. let yAxis = d3
  224. .axisLeft(yScale)
  225. .tickValues(tick_values)
  226. .tickFormat(tickValueFormatter(decimals, scaledDecimals))
  227. .tickSizeInner(0 - width)
  228. .tickSizeOuter(0)
  229. .tickPadding(Y_AXIS_TICK_PADDING);
  230. heatmap
  231. .append('g')
  232. .attr('class', 'axis axis-y')
  233. .call(yAxis);
  234. // Calculate Y axis width first, then move axis into visible area
  235. let posY = margin.top;
  236. let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  237. heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
  238. // Set first tick as pseudo 0
  239. if (y_min < 1) {
  240. heatmap
  241. .select('.axis-y')
  242. .select('.tick text')
  243. .text('0');
  244. }
  245. // Remove vertical line in the right of axis labels (called domain in d3)
  246. heatmap
  247. .select('.axis-y')
  248. .select('.domain')
  249. .remove();
  250. }
  251. function addYAxisFromBuckets() {
  252. const tsBuckets = data.tsBuckets;
  253. scope.yScale = yScale = d3
  254. .scaleLinear()
  255. .domain([0, tsBuckets.length - 1])
  256. .range([chartHeight, 0]);
  257. const tick_values = _.map(tsBuckets, (b, i) => i);
  258. const decimalsAuto = _.max(_.map(tsBuckets, getStringPrecision));
  259. const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
  260. ctrl.decimals = decimals;
  261. function tickFormatter(valIndex) {
  262. let valueFormatted = tsBuckets[valIndex];
  263. if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
  264. // Try to format numeric tick labels
  265. valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted));
  266. }
  267. return valueFormatted;
  268. }
  269. let yAxis = d3
  270. .axisLeft(yScale)
  271. .tickValues(tick_values)
  272. .tickFormat(tickFormatter)
  273. .tickSizeInner(0 - width)
  274. .tickSizeOuter(0)
  275. .tickPadding(Y_AXIS_TICK_PADDING);
  276. heatmap
  277. .append('g')
  278. .attr('class', 'axis axis-y')
  279. .call(yAxis);
  280. // Calculate Y axis width first, then move axis into visible area
  281. const posY = margin.top;
  282. const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  283. heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
  284. // Remove vertical line in the right of axis labels (called domain in d3)
  285. heatmap
  286. .select('.axis-y')
  287. .select('.domain')
  288. .remove();
  289. }
  290. // Adjust data range to log base
  291. function adjustLogRange(min, max, logBase) {
  292. let y_min, y_max;
  293. y_min = data.heatmapStats.minLog;
  294. if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
  295. y_min = 1;
  296. } else {
  297. y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
  298. }
  299. // Adjust max Y value to log base
  300. y_max = adjustLogMax(data.heatmapStats.max, logBase);
  301. return { y_min, y_max };
  302. }
  303. function adjustLogMax(max, base) {
  304. return Math.pow(base, Math.ceil(logp(max, base)));
  305. }
  306. function adjustLogMin(min, base) {
  307. return Math.pow(base, Math.floor(logp(min, base)));
  308. }
  309. function logScaleTickValues(domain, base) {
  310. let domainMin = domain[0];
  311. let domainMax = domain[1];
  312. let tickValues = [];
  313. if (domainMin < 1) {
  314. let under_one_ticks = Math.floor(logp(domainMin, base));
  315. for (let i = under_one_ticks; i < 0; i++) {
  316. let tick_value = Math.pow(base, i);
  317. tickValues.push(tick_value);
  318. }
  319. }
  320. let ticks = Math.ceil(logp(domainMax, base));
  321. for (let i = 0; i <= ticks; i++) {
  322. let tick_value = Math.pow(base, i);
  323. tickValues.push(tick_value);
  324. }
  325. return tickValues;
  326. }
  327. function tickValueFormatter(decimals, scaledDecimals = null) {
  328. let format = panel.yAxis.format;
  329. return function(value) {
  330. try {
  331. return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
  332. } catch (err) {
  333. console.error(err.message || err);
  334. return value;
  335. }
  336. };
  337. }
  338. function fixYAxisTickSize() {
  339. heatmap
  340. .select('.axis-y')
  341. .selectAll('.tick line')
  342. .attr('x2', chartWidth);
  343. }
  344. function addAxes() {
  345. chartHeight = height - margin.top - margin.bottom;
  346. chartTop = margin.top;
  347. chartBottom = chartTop + chartHeight;
  348. if (panel.dataFormat === 'tsbuckets') {
  349. addYAxisFromBuckets();
  350. } else {
  351. if (panel.yAxis.logBase === 1) {
  352. addYAxis();
  353. } else {
  354. addLogYAxis();
  355. }
  356. }
  357. yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  358. chartWidth = width - yAxisWidth - margin.right;
  359. fixYAxisTickSize();
  360. addXAxis();
  361. xAxisHeight = getXAxisHeight(heatmap);
  362. if (!panel.yAxis.show) {
  363. heatmap
  364. .select('.axis-y')
  365. .selectAll('line')
  366. .style('opacity', 0);
  367. }
  368. if (!panel.xAxis.show) {
  369. heatmap
  370. .select('.axis-x')
  371. .selectAll('line')
  372. .style('opacity', 0);
  373. }
  374. }
  375. function addHeatmapCanvas() {
  376. let heatmap_elem = $heatmap[0];
  377. width = Math.floor($heatmap.width()) - padding.right;
  378. height = Math.floor($heatmap.height()) - padding.bottom;
  379. cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
  380. cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
  381. if (heatmap) {
  382. heatmap.remove();
  383. }
  384. heatmap = d3
  385. .select(heatmap_elem)
  386. .append('svg')
  387. .attr('width', width)
  388. .attr('height', height);
  389. }
  390. function addHeatmap() {
  391. addHeatmapCanvas();
  392. addAxes();
  393. if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') {
  394. let log_base = panel.yAxis.logBase;
  395. let domain = yScale.domain();
  396. let tick_values = logScaleTickValues(domain, log_base);
  397. data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
  398. }
  399. let cardsData = data.cards;
  400. let maxValueAuto = data.cardStats.max;
  401. let maxValue = panel.color.max || maxValueAuto;
  402. let minValue = panel.color.min || 0;
  403. let colorScheme = _.find(ctrl.colorSchemes, {
  404. value: panel.color.colorScheme,
  405. });
  406. colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
  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(Math.min(selection.x1, selection.x2) - yAxisWidth);
  569. let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);
  570. ctrl.timeSrv.setTime({
  571. from: moment.utc(timeFrom),
  572. to: moment.utc(timeTo),
  573. });
  574. }
  575. clearSelection();
  576. }
  577. function onMouseLeave() {
  578. appEvents.emit('graph-hover-clear');
  579. clearCrosshair();
  580. }
  581. function onMouseMove(event) {
  582. if (!heatmap) {
  583. return;
  584. }
  585. if (selection.active) {
  586. // Clear crosshair and tooltip
  587. clearCrosshair();
  588. tooltip.destroy();
  589. selection.x2 = limitSelection(event.offsetX);
  590. drawSelection(selection.x1, selection.x2);
  591. } else {
  592. emitGraphHoverEvet(event);
  593. drawCrosshair(event.offsetX);
  594. tooltip.show(event, data);
  595. }
  596. }
  597. function emitGraphHoverEvet(event) {
  598. let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
  599. let y = yScale.invert(event.offsetY);
  600. let pos = {
  601. pageX: event.pageX,
  602. pageY: event.pageY,
  603. x: x,
  604. x1: x,
  605. y: y,
  606. y1: y,
  607. panelRelY: null,
  608. };
  609. // Set minimum offset to prevent showing legend from another panel
  610. pos.panelRelY = Math.max(event.offsetY / height, 0.001);
  611. // broadcast to other graph panels that we are hovering
  612. appEvents.emit('graph-hover', { pos: pos, panel: panel });
  613. }
  614. function limitSelection(x2) {
  615. x2 = Math.max(x2, yAxisWidth);
  616. x2 = Math.min(x2, chartWidth + yAxisWidth);
  617. return x2;
  618. }
  619. function drawSelection(posX1, posX2) {
  620. if (heatmap) {
  621. heatmap.selectAll('.heatmap-selection').remove();
  622. let selectionX = Math.min(posX1, posX2);
  623. let selectionWidth = Math.abs(posX1 - posX2);
  624. if (selectionWidth > MIN_SELECTION_WIDTH) {
  625. heatmap
  626. .append('rect')
  627. .attr('class', 'heatmap-selection')
  628. .attr('x', selectionX)
  629. .attr('width', selectionWidth)
  630. .attr('y', chartTop)
  631. .attr('height', chartHeight);
  632. }
  633. }
  634. }
  635. function clearSelection() {
  636. selection.x1 = -1;
  637. selection.x2 = -1;
  638. if (heatmap) {
  639. heatmap.selectAll('.heatmap-selection').remove();
  640. }
  641. }
  642. function drawCrosshair(position) {
  643. if (heatmap) {
  644. heatmap.selectAll('.heatmap-crosshair').remove();
  645. let posX = position;
  646. posX = Math.max(posX, yAxisWidth);
  647. posX = Math.min(posX, chartWidth + yAxisWidth);
  648. heatmap
  649. .append('g')
  650. .attr('class', 'heatmap-crosshair')
  651. .attr('transform', 'translate(' + posX + ',0)')
  652. .append('line')
  653. .attr('x1', 1)
  654. .attr('y1', chartTop)
  655. .attr('x2', 1)
  656. .attr('y2', chartBottom)
  657. .attr('stroke-width', 1);
  658. }
  659. }
  660. function drawSharedCrosshair(pos) {
  661. if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
  662. let posX = xScale(pos.x) + yAxisWidth;
  663. drawCrosshair(posX);
  664. }
  665. }
  666. function clearCrosshair() {
  667. if (heatmap) {
  668. heatmap.selectAll('.heatmap-crosshair').remove();
  669. }
  670. }
  671. function render() {
  672. data = ctrl.data;
  673. panel = ctrl.panel;
  674. timeRange = ctrl.range;
  675. if (!setElementHeight() || !data) {
  676. return;
  677. }
  678. // Draw default axes and return if no data
  679. if (_.isEmpty(data.buckets)) {
  680. addHeatmapCanvas();
  681. addAxes();
  682. return;
  683. }
  684. addHeatmap();
  685. scope.yAxisWidth = yAxisWidth;
  686. scope.xAxisHeight = xAxisHeight;
  687. scope.chartHeight = chartHeight;
  688. scope.chartWidth = chartWidth;
  689. scope.chartTop = chartTop;
  690. }
  691. // Register selection listeners
  692. $heatmap.on('mousedown', onMouseDown);
  693. $heatmap.on('mousemove', onMouseMove);
  694. $heatmap.on('mouseleave', onMouseLeave);
  695. }
  696. function grafanaTimeFormat(ticks, min, max) {
  697. if (min && max && ticks) {
  698. let range = max - min;
  699. let secPerTick = range / ticks / 1000;
  700. let oneDay = 86400000;
  701. let oneYear = 31536000000;
  702. if (secPerTick <= 45) {
  703. return '%H:%M:%S';
  704. }
  705. if (secPerTick <= 7200 || range <= oneDay) {
  706. return '%H:%M';
  707. }
  708. if (secPerTick <= 80000) {
  709. return '%m/%d %H:%M';
  710. }
  711. if (secPerTick <= 2419200 || range <= oneYear) {
  712. return '%m/%d';
  713. }
  714. return '%Y-%m';
  715. }
  716. return '%H:%M';
  717. }
  718. function logp(value, base) {
  719. return Math.log(value) / Math.log(base);
  720. }
  721. function getPrecision(num: number): number {
  722. let str = num.toString();
  723. return getStringPrecision(str);
  724. }
  725. function getStringPrecision(num: string): number {
  726. let dot_index = num.indexOf('.');
  727. if (dot_index === -1) {
  728. return 0;
  729. } else {
  730. return num.length - dot_index - 1;
  731. }
  732. }