rendering.ts 24 KB

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