rendering.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  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. function tickFormatter(valIndex) {
  259. let valueFormatted = tsBuckets[valIndex];
  260. if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
  261. // Try to format numeric tick labels
  262. valueFormatted = tickValueFormatter(0)(valueFormatted);
  263. }
  264. return valueFormatted;
  265. }
  266. let yAxis = d3
  267. .axisLeft(yScale)
  268. .tickValues(tick_values)
  269. .tickFormat(tickFormatter)
  270. .tickSizeInner(0 - width)
  271. .tickSizeOuter(0)
  272. .tickPadding(Y_AXIS_TICK_PADDING);
  273. heatmap
  274. .append('g')
  275. .attr('class', 'axis axis-y')
  276. .call(yAxis);
  277. // Calculate Y axis width first, then move axis into visible area
  278. const posY = margin.top;
  279. const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  280. heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
  281. // Remove vertical line in the right of axis labels (called domain in d3)
  282. heatmap
  283. .select('.axis-y')
  284. .select('.domain')
  285. .remove();
  286. }
  287. // Adjust data range to log base
  288. function adjustLogRange(min, max, logBase) {
  289. let y_min, y_max;
  290. y_min = data.heatmapStats.minLog;
  291. if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
  292. y_min = 1;
  293. } else {
  294. y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
  295. }
  296. // Adjust max Y value to log base
  297. y_max = adjustLogMax(data.heatmapStats.max, logBase);
  298. return { y_min, y_max };
  299. }
  300. function adjustLogMax(max, base) {
  301. return Math.pow(base, Math.ceil(logp(max, base)));
  302. }
  303. function adjustLogMin(min, base) {
  304. return Math.pow(base, Math.floor(logp(min, base)));
  305. }
  306. function logScaleTickValues(domain, base) {
  307. let domainMin = domain[0];
  308. let domainMax = domain[1];
  309. let tickValues = [];
  310. if (domainMin < 1) {
  311. let under_one_ticks = Math.floor(logp(domainMin, base));
  312. for (let i = under_one_ticks; i < 0; i++) {
  313. let tick_value = Math.pow(base, i);
  314. tickValues.push(tick_value);
  315. }
  316. }
  317. let ticks = Math.ceil(logp(domainMax, base));
  318. for (let i = 0; i <= ticks; i++) {
  319. let tick_value = Math.pow(base, i);
  320. tickValues.push(tick_value);
  321. }
  322. return tickValues;
  323. }
  324. function tickValueFormatter(decimals, scaledDecimals = null) {
  325. let format = panel.yAxis.format;
  326. return function(value) {
  327. return kbn.valueFormats[format](value, decimals, scaledDecimals);
  328. };
  329. }
  330. function fixYAxisTickSize() {
  331. heatmap
  332. .select('.axis-y')
  333. .selectAll('.tick line')
  334. .attr('x2', chartWidth);
  335. }
  336. function addAxes() {
  337. chartHeight = height - margin.top - margin.bottom;
  338. chartTop = margin.top;
  339. chartBottom = chartTop + chartHeight;
  340. if (panel.dataFormat === 'tsbuckets') {
  341. addYAxisFromBuckets();
  342. } else {
  343. if (panel.yAxis.logBase === 1) {
  344. addYAxis();
  345. } else {
  346. addLogYAxis();
  347. }
  348. }
  349. yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
  350. chartWidth = width - yAxisWidth - margin.right;
  351. fixYAxisTickSize();
  352. addXAxis();
  353. xAxisHeight = getXAxisHeight(heatmap);
  354. if (!panel.yAxis.show) {
  355. heatmap
  356. .select('.axis-y')
  357. .selectAll('line')
  358. .style('opacity', 0);
  359. }
  360. if (!panel.xAxis.show) {
  361. heatmap
  362. .select('.axis-x')
  363. .selectAll('line')
  364. .style('opacity', 0);
  365. }
  366. }
  367. function addHeatmapCanvas() {
  368. let heatmap_elem = $heatmap[0];
  369. width = Math.floor($heatmap.width()) - padding.right;
  370. height = Math.floor($heatmap.height()) - padding.bottom;
  371. cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
  372. cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
  373. if (heatmap) {
  374. heatmap.remove();
  375. }
  376. heatmap = d3
  377. .select(heatmap_elem)
  378. .append('svg')
  379. .attr('width', width)
  380. .attr('height', height);
  381. }
  382. function addHeatmap() {
  383. addHeatmapCanvas();
  384. addAxes();
  385. if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') {
  386. let log_base = panel.yAxis.logBase;
  387. let domain = yScale.domain();
  388. let tick_values = logScaleTickValues(domain, log_base);
  389. data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
  390. }
  391. let cardsData = data.cards;
  392. let maxValueAuto = data.cardStats.max;
  393. let maxValue = panel.color.max || maxValueAuto;
  394. let minValue = panel.color.min || 0;
  395. let colorScheme = _.find(ctrl.colorSchemes, {
  396. value: panel.color.colorScheme,
  397. });
  398. colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
  399. opacityScale = getOpacityScale(panel.color, maxValue);
  400. setCardSize();
  401. let cards = heatmap.selectAll('.heatmap-card').data(cardsData);
  402. cards.append('title');
  403. cards = cards
  404. .enter()
  405. .append('rect')
  406. .attr('x', getCardX)
  407. .attr('width', getCardWidth)
  408. .attr('y', getCardY)
  409. .attr('height', getCardHeight)
  410. .attr('rx', cardRound)
  411. .attr('ry', cardRound)
  412. .attr('class', 'bordered heatmap-card')
  413. .style('fill', getCardColor)
  414. .style('stroke', getCardColor)
  415. .style('stroke-width', 0)
  416. .style('opacity', getCardOpacity);
  417. let $cards = $heatmap.find('.heatmap-card');
  418. $cards
  419. .on('mouseenter', event => {
  420. tooltip.mouseOverBucket = true;
  421. highlightCard(event);
  422. })
  423. .on('mouseleave', event => {
  424. tooltip.mouseOverBucket = false;
  425. resetCardHighLight(event);
  426. });
  427. }
  428. function highlightCard(event) {
  429. let color = d3.select(event.target).style('fill');
  430. let highlightColor = d3.color(color).darker(2);
  431. let strokeColor = d3.color(color).brighter(4);
  432. let current_card = d3.select(event.target);
  433. tooltip.originalFillColor = color;
  434. current_card
  435. .style('fill', highlightColor.toString())
  436. .style('stroke', strokeColor.toString())
  437. .style('stroke-width', 1);
  438. }
  439. function resetCardHighLight(event) {
  440. d3
  441. .select(event.target)
  442. .style('fill', tooltip.originalFillColor)
  443. .style('stroke', tooltip.originalFillColor)
  444. .style('stroke-width', 0);
  445. }
  446. function setCardSize() {
  447. let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
  448. let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));
  449. if (panel.yAxis.logBase !== 1) {
  450. let base = panel.yAxis.logBase;
  451. let splitFactor = data.yBucketSize || 1;
  452. yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
  453. }
  454. cardWidth = xGridSize - cardPadding * 2;
  455. cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
  456. }
  457. function getCardX(d) {
  458. let x;
  459. if (xScale(d.x) < 0) {
  460. // Cut card left to prevent overlay
  461. x = yAxisWidth + cardPadding;
  462. } else {
  463. x = xScale(d.x) + yAxisWidth + cardPadding;
  464. }
  465. return x;
  466. }
  467. function getCardWidth(d) {
  468. let w;
  469. if (xScale(d.x) < 0) {
  470. // Cut card left to prevent overlay
  471. let cutted_width = xScale(d.x) + cardWidth;
  472. w = cutted_width > 0 ? cutted_width : 0;
  473. } else if (xScale(d.x) + cardWidth > chartWidth) {
  474. // Cut card right to prevent overlay
  475. w = chartWidth - xScale(d.x) - cardPadding;
  476. } else {
  477. w = cardWidth;
  478. }
  479. // Card width should be MIN_CARD_SIZE at least
  480. w = Math.max(w, MIN_CARD_SIZE);
  481. return w;
  482. }
  483. function getCardY(d) {
  484. let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
  485. if (panel.yAxis.logBase !== 1 && d.y === 0) {
  486. y = chartBottom - cardHeight - cardPadding;
  487. } else {
  488. if (y < chartTop) {
  489. y = chartTop;
  490. }
  491. }
  492. return y;
  493. }
  494. function getCardHeight(d) {
  495. let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
  496. let h = cardHeight;
  497. if (panel.yAxis.logBase !== 1 && d.y === 0) {
  498. return cardHeight;
  499. }
  500. // Cut card height to prevent overlay
  501. if (y < chartTop) {
  502. h = yScale(d.y) - cardPadding;
  503. } else if (yScale(d.y) > chartBottom) {
  504. h = chartBottom - y;
  505. } else if (y + cardHeight > chartBottom) {
  506. h = chartBottom - y;
  507. }
  508. // Height can't be more than chart height
  509. h = Math.min(h, chartHeight);
  510. // Card height should be MIN_CARD_SIZE at least
  511. h = Math.max(h, MIN_CARD_SIZE);
  512. return h;
  513. }
  514. function getCardColor(d) {
  515. if (panel.color.mode === 'opacity') {
  516. return panel.color.cardColor;
  517. } else {
  518. return colorScale(d.count);
  519. }
  520. }
  521. function getCardOpacity(d) {
  522. if (panel.color.mode === 'opacity') {
  523. return opacityScale(d.count);
  524. } else {
  525. return 1;
  526. }
  527. }
  528. /////////////////////////////
  529. // Selection and crosshair //
  530. /////////////////////////////
  531. // Shared crosshair and tooltip
  532. appEvents.on(
  533. 'graph-hover',
  534. event => {
  535. drawSharedCrosshair(event.pos);
  536. },
  537. scope
  538. );
  539. appEvents.on(
  540. 'graph-hover-clear',
  541. () => {
  542. clearCrosshair();
  543. },
  544. scope
  545. );
  546. function onMouseDown(event) {
  547. selection.active = true;
  548. selection.x1 = event.offsetX;
  549. mouseUpHandler = function() {
  550. onMouseUp();
  551. };
  552. $(document).one('mouseup', mouseUpHandler);
  553. }
  554. function onMouseUp() {
  555. $(document).unbind('mouseup', mouseUpHandler);
  556. mouseUpHandler = null;
  557. selection.active = false;
  558. let selectionRange = Math.abs(selection.x2 - selection.x1);
  559. if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
  560. let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth);
  561. let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);
  562. ctrl.timeSrv.setTime({
  563. from: moment.utc(timeFrom),
  564. to: moment.utc(timeTo),
  565. });
  566. }
  567. clearSelection();
  568. }
  569. function onMouseLeave() {
  570. appEvents.emit('graph-hover-clear');
  571. clearCrosshair();
  572. }
  573. function onMouseMove(event) {
  574. if (!heatmap) {
  575. return;
  576. }
  577. if (selection.active) {
  578. // Clear crosshair and tooltip
  579. clearCrosshair();
  580. tooltip.destroy();
  581. selection.x2 = limitSelection(event.offsetX);
  582. drawSelection(selection.x1, selection.x2);
  583. } else {
  584. emitGraphHoverEvet(event);
  585. drawCrosshair(event.offsetX);
  586. tooltip.show(event, data);
  587. }
  588. }
  589. function emitGraphHoverEvet(event) {
  590. let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
  591. let y = yScale.invert(event.offsetY);
  592. let pos = {
  593. pageX: event.pageX,
  594. pageY: event.pageY,
  595. x: x,
  596. x1: x,
  597. y: y,
  598. y1: y,
  599. panelRelY: null,
  600. };
  601. // Set minimum offset to prevent showing legend from another panel
  602. pos.panelRelY = Math.max(event.offsetY / height, 0.001);
  603. // broadcast to other graph panels that we are hovering
  604. appEvents.emit('graph-hover', { pos: pos, panel: panel });
  605. }
  606. function limitSelection(x2) {
  607. x2 = Math.max(x2, yAxisWidth);
  608. x2 = Math.min(x2, chartWidth + yAxisWidth);
  609. return x2;
  610. }
  611. function drawSelection(posX1, posX2) {
  612. if (heatmap) {
  613. heatmap.selectAll('.heatmap-selection').remove();
  614. let selectionX = Math.min(posX1, posX2);
  615. let selectionWidth = Math.abs(posX1 - posX2);
  616. if (selectionWidth > MIN_SELECTION_WIDTH) {
  617. heatmap
  618. .append('rect')
  619. .attr('class', 'heatmap-selection')
  620. .attr('x', selectionX)
  621. .attr('width', selectionWidth)
  622. .attr('y', chartTop)
  623. .attr('height', chartHeight);
  624. }
  625. }
  626. }
  627. function clearSelection() {
  628. selection.x1 = -1;
  629. selection.x2 = -1;
  630. if (heatmap) {
  631. heatmap.selectAll('.heatmap-selection').remove();
  632. }
  633. }
  634. function drawCrosshair(position) {
  635. if (heatmap) {
  636. heatmap.selectAll('.heatmap-crosshair').remove();
  637. let posX = position;
  638. posX = Math.max(posX, yAxisWidth);
  639. posX = Math.min(posX, chartWidth + yAxisWidth);
  640. heatmap
  641. .append('g')
  642. .attr('class', 'heatmap-crosshair')
  643. .attr('transform', 'translate(' + posX + ',0)')
  644. .append('line')
  645. .attr('x1', 1)
  646. .attr('y1', chartTop)
  647. .attr('x2', 1)
  648. .attr('y2', chartBottom)
  649. .attr('stroke-width', 1);
  650. }
  651. }
  652. function drawSharedCrosshair(pos) {
  653. if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
  654. let posX = xScale(pos.x) + yAxisWidth;
  655. drawCrosshair(posX);
  656. }
  657. }
  658. function clearCrosshair() {
  659. if (heatmap) {
  660. heatmap.selectAll('.heatmap-crosshair').remove();
  661. }
  662. }
  663. function render() {
  664. data = ctrl.data;
  665. panel = ctrl.panel;
  666. timeRange = ctrl.range;
  667. if (!setElementHeight() || !data) {
  668. return;
  669. }
  670. // Draw default axes and return if no data
  671. if (_.isEmpty(data.buckets)) {
  672. addHeatmapCanvas();
  673. addAxes();
  674. return;
  675. }
  676. addHeatmap();
  677. scope.yAxisWidth = yAxisWidth;
  678. scope.xAxisHeight = xAxisHeight;
  679. scope.chartHeight = chartHeight;
  680. scope.chartWidth = chartWidth;
  681. scope.chartTop = chartTop;
  682. }
  683. // Register selection listeners
  684. $heatmap.on('mousedown', onMouseDown);
  685. $heatmap.on('mousemove', onMouseMove);
  686. $heatmap.on('mouseleave', onMouseLeave);
  687. }
  688. function grafanaTimeFormat(ticks, min, max) {
  689. if (min && max && ticks) {
  690. let range = max - min;
  691. let secPerTick = range / ticks / 1000;
  692. let oneDay = 86400000;
  693. let oneYear = 31536000000;
  694. if (secPerTick <= 45) {
  695. return '%H:%M:%S';
  696. }
  697. if (secPerTick <= 7200 || range <= oneDay) {
  698. return '%H:%M';
  699. }
  700. if (secPerTick <= 80000) {
  701. return '%m/%d %H:%M';
  702. }
  703. if (secPerTick <= 2419200 || range <= oneYear) {
  704. return '%m/%d';
  705. }
  706. return '%Y-%m';
  707. }
  708. return '%H:%M';
  709. }
  710. function logp(value, base) {
  711. return Math.log(value) / Math.log(base);
  712. }
  713. function getPrecision(num) {
  714. let str = num.toString();
  715. let dot_index = str.indexOf('.');
  716. if (dot_index === -1) {
  717. return 0;
  718. } else {
  719. return str.length - dot_index - 1;
  720. }
  721. }