rendering.ts 22 KB

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