rendering.ts 23 KB

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