rendering.ts 21 KB

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