diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 54d17146532..e3318ea7e23 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -19,56 +19,94 @@ let MIN_CARD_SIZE = 1, Y_AXIS_TICK_PADDING = 5, MIN_SELECTION_WIDTH = 2; -export default function link(scope, elem, attrs, ctrl) { - let data, timeRange, panel, heatmap; +export default function rendering(scope, elem, attrs, ctrl) { + return new HeatmapRenderer(scope, elem, attrs, ctrl); +} +export class HeatmapRenderer { + width: number; + height: number; + yScale: any; + xScale: any; + chartWidth: number; + chartHeight: number; + chartTop: number; + chartBottom: number; + yAxisWidth: number; + xAxisHeight: number; + cardPadding: number; + cardRound: number; + cardWidth: number; + cardHeight: number; + colorScale: any; + opacityScale: any; + mouseUpHandler: any; + data: any; + panel: any; + $heatmap: any; + tooltip: HeatmapTooltip; + heatmap: any; + timeRange: any; - // $heatmap is JQuery object, but heatmap is D3 - let $heatmap = elem.find('.heatmap-panel'); - let tooltip = new HeatmapTooltip($heatmap, scope); + selection: any; + padding: any; + margin: any; + dataRangeWidingFactor: number; + constructor(private scope, private elem, attrs, private ctrl) { + // $heatmap is JQuery object, but heatmap is D3 + this.$heatmap = this.elem.find('.heatmap-panel'); + this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope); - let width, - height, - yScale, - xScale, - chartWidth, - chartHeight, - chartTop, - chartBottom, - yAxisWidth, - xAxisHeight, - cardPadding, - cardRound, - cardWidth, - cardHeight, - colorScale, - opacityScale, - mouseUpHandler; + this.selection = { + active: false, + x1: -1, + x2: -1, + }; - let selection = { - active: false, - x1: -1, - x2: -1, - }; + this.padding = { left: 0, right: 0, top: 0, bottom: 0 }; + this.margin = { left: 25, right: 15, top: 10, bottom: 20 }; + this.dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR; - let padding = { left: 0, right: 0, top: 0, bottom: 0 }, - margin = { left: 25, right: 15, top: 10, bottom: 20 }, - dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR; + this.ctrl.events.on('render', this.onRender.bind(this)); - ctrl.events.on('render', () => { - render(); - ctrl.renderingCompleted(); - }); + this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this); + ///////////////////////////// + // Selection and crosshair // + ///////////////////////////// - function setElementHeight() { + // Shared crosshair and tooltip + appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope); + + appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope); + + // Register selection listeners + this.$heatmap.on('mousedown', this.onMouseDown.bind(this)); + this.$heatmap.on('mousemove', this.onMouseMove.bind(this)); + this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this)); + } + + onGraphHoverClear() { + this.clearCrosshair(); + } + + onGraphHover(event) { + this.drawSharedCrosshair(event.pos); + } + + onRender() { + this.render(); + this.ctrl.renderingCompleted(); + } + + setElementHeight() { try { - var height = ctrl.height || panel.height || ctrl.row.height; + var height = this.ctrl.height || this.panel.height || this.ctrl.row.height; if (_.isString(height)) { height = parseInt(height.replace('px', ''), 10); } - height -= panel.legend.show ? 28 : 11; // bottom padding and space for legend + height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend - $heatmap.css('height', height + 'px'); + this.$heatmap.css('height', height + 'px'); return true; } catch (e) { @@ -77,7 +115,7 @@ export default function link(scope, elem, attrs, ctrl) { } } - function getYAxisWidth(elem) { + getYAxisWidth(elem) { let axis_text = elem.selectAll('.axis-y text').nodes(); let max_text_width = _.max( _.map(axis_text, text => { @@ -89,7 +127,7 @@ export default function link(scope, elem, attrs, ctrl) { return max_text_width; } - function getXAxisHeight(elem) { + getXAxisHeight(elem) { let axis_line = elem.select('.axis-x line'); if (!axis_line.empty()) { let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2')); @@ -101,16 +139,16 @@ export default function link(scope, elem, attrs, ctrl) { } } - function addXAxis() { - scope.xScale = xScale = d3 + addXAxis() { + this.scope.xScale = this.xScale = d3 .scaleTime() - .domain([timeRange.from, timeRange.to]) - .range([0, chartWidth]); + .domain([this.timeRange.from, this.timeRange.to]) + .range([0, this.chartWidth]); - let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; - let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; + let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); let timeFormat; - let dashboardTimeZone = ctrl.dashboard.getTimezone(); + let dashboardTimeZone = this.ctrl.dashboard.getTimezone(); if (dashboardTimeZone === 'utc') { timeFormat = d3.utcFormat(grafanaTimeFormatter); } else { @@ -118,100 +156,100 @@ export default function link(scope, elem, attrs, ctrl) { } let xAxis = d3 - .axisBottom(xScale) + .axisBottom(this.xScale) .ticks(ticks) .tickFormat(timeFormat) .tickPadding(X_AXIS_TICK_PADDING) - .tickSize(chartHeight); + .tickSize(this.chartHeight); - let posY = margin.top; - let posX = yAxisWidth; - heatmap + let posY = this.margin.top; + let posX = this.yAxisWidth; + this.heatmap .append('g') .attr('class', 'axis axis-x') .attr('transform', 'translate(' + posX + ',' + posY + ')') .call(xAxis); // Remove horizontal line in the top of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-x') .select('.domain') .remove(); } - function addYAxis() { - let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX); - let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); - let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval); + addYAxis() { + let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX); + let tick_interval = ticksUtils.tickStep(this.data.heatmapStats.min, this.data.heatmapStats.max, ticks); + let { y_min, y_max } = this.wideYAxisRange(this.data.heatmapStats.min, this.data.heatmapStats.max, tick_interval); // Rewrite min and max if it have been set explicitly - y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min; - y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max; + y_min = this.panel.yAxis.min !== null ? this.panel.yAxis.min : y_min; + y_max = this.panel.yAxis.max !== null ? this.panel.yAxis.max : y_max; // Adjust ticks after Y range widening tick_interval = ticksUtils.tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); let decimalsAuto = ticksUtils.getPrecision(tick_interval); - let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; + let decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); - ctrl.decimals = decimals; - ctrl.scaledDecimals = scaledDecimals; + this.ctrl.decimals = decimals; + this.ctrl.scaledDecimals = scaledDecimals; // Set default Y min and max if no data - if (_.isEmpty(data.buckets)) { + if (_.isEmpty(this.data.buckets)) { y_max = 1; y_min = -1; ticks = 3; decimals = 1; } - data.yAxis = { + this.data.yAxis = { min: y_min, max: y_max, ticks: ticks, }; - scope.yScale = yScale = d3 + this.scope.yScale = this.yScale = d3 .scaleLinear() .domain([y_min, y_max]) - .range([chartHeight, 0]); + .range([this.chartHeight, 0]); let yAxis = d3 - .axisLeft(yScale) + .axisLeft(this.yScale) .ticks(ticks) - .tickFormat(tickValueFormatter(decimals, scaledDecimals)) - .tickSizeInner(0 - width) + .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) + .tickSizeInner(0 - this.width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); - heatmap + this.heatmap .append('g') .attr('class', 'axis axis-y') .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = margin.top; - let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + let posY = this.margin.top; + let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Remove vertical line in the right of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-y') .select('.domain') .remove(); } // Wide Y values range and anjust to bucket size - function wideYAxisRange(min, max, tickInterval) { - let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2; + wideYAxisRange(min, max, tickInterval) { + let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; let y_min, y_max; if (tickInterval === 0) { - y_max = max * dataRangeWidingFactor; - y_min = min - min * (dataRangeWidingFactor - 1); + y_max = max * this.dataRangeWidingFactor; + y_min = min - min * (this.dataRangeWidingFactor - 1); tickInterval = (y_max - y_min) / 2; } else { y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval; @@ -226,89 +264,91 @@ export default function link(scope, elem, attrs, ctrl) { return { y_min, y_max }; } - function addLogYAxis() { - let log_base = panel.yAxis.logBase; - let { y_min, y_max } = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base); + addLogYAxis() { + let log_base = this.panel.yAxis.logBase; + let { y_min, y_max } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, log_base); - y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min; - y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max; + y_min = + this.panel.yAxis.min && this.panel.yAxis.min !== '0' ? this.adjustLogMin(this.panel.yAxis.min, log_base) : y_min; + y_max = this.panel.yAxis.max !== null ? this.adjustLogMax(this.panel.yAxis.max, log_base) : y_max; // Set default Y min and max if no data - if (_.isEmpty(data.buckets)) { + if (_.isEmpty(this.data.buckets)) { y_max = Math.pow(log_base, 2); y_min = 1; } - scope.yScale = yScale = d3 + this.scope.yScale = this.yScale = d3 .scaleLog() - .base(panel.yAxis.logBase) + .base(this.panel.yAxis.logBase) .domain([y_min, y_max]) - .range([chartHeight, 0]); + .range([this.chartHeight, 0]); - let domain = yScale.domain(); - let tick_values = logScaleTickValues(domain, log_base); + let domain = this.yScale.domain(); + let tick_values = this.logScaleTickValues(domain, log_base); let decimalsAuto = ticksUtils.getPrecision(y_min); - let decimals = panel.yAxis.decimals || decimalsAuto; + let decimals = this.panel.yAxis.decimals || decimalsAuto; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); - ctrl.decimals = decimals; - ctrl.scaledDecimals = scaledDecimals; + this.ctrl.decimals = decimals; + this.ctrl.scaledDecimals = scaledDecimals; - data.yAxis = { + this.data.yAxis = { min: y_min, max: y_max, ticks: tick_values.length, }; let yAxis = d3 - .axisLeft(yScale) + .axisLeft(this.yScale) .tickValues(tick_values) - .tickFormat(tickValueFormatter(decimals, scaledDecimals)) - .tickSizeInner(0 - width) + .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) + .tickSizeInner(0 - this.width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); - heatmap + this.heatmap .append('g') .attr('class', 'axis axis-y') .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = margin.top; - let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + let posY = this.margin.top; + let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Set first tick as pseudo 0 if (y_min < 1) { - heatmap + this.heatmap .select('.axis-y') .select('.tick text') .text('0'); } // Remove vertical line in the right of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-y') .select('.domain') .remove(); } - function addYAxisFromBuckets() { - const tsBuckets = data.tsBuckets; + addYAxisFromBuckets() { + const tsBuckets = this.data.tsBuckets; - scope.yScale = yScale = d3 + this.scope.yScale = this.yScale = d3 .scaleLinear() .domain([0, tsBuckets.length - 1]) - .range([chartHeight, 0]); + .range([this.chartHeight, 0]); const tick_values = _.map(tsBuckets, (b, i) => i); const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision)); - const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; - ctrl.decimals = decimals; + const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; + this.ctrl.decimals = decimals; + let tickValueFormatter = this.tickValueFormatter.bind(this); function tickFormatter(valIndex) { let valueFormatted = tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { @@ -319,59 +359,59 @@ export default function link(scope, elem, attrs, ctrl) { } const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i)); - data.tsBucketsFormatted = tsBucketsFormatted; + this.data.tsBucketsFormatted = tsBucketsFormatted; let yAxis = d3 - .axisLeft(yScale) + .axisLeft(this.yScale) .tickValues(tick_values) .tickFormat(tickFormatter) - .tickSizeInner(0 - width) + .tickSizeInner(0 - this.width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); - heatmap + this.heatmap .append('g') .attr('class', 'axis axis-y') .call(yAxis); // Calculate Y axis width first, then move axis into visible area - const posY = margin.top; - const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + const posY = this.margin.top; + const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Remove vertical line in the right of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-y') .select('.domain') .remove(); } // Adjust data range to log base - function adjustLogRange(min, max, logBase) { + adjustLogRange(min, max, logBase) { let y_min, y_max; - y_min = data.heatmapStats.minLog; - if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) { + y_min = this.data.heatmapStats.minLog; + if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) { y_min = 1; } else { - y_min = adjustLogMin(data.heatmapStats.minLog, logBase); + y_min = this.adjustLogMin(this.data.heatmapStats.minLog, logBase); } // Adjust max Y value to log base - y_max = adjustLogMax(data.heatmapStats.max, logBase); + y_max = this.adjustLogMax(this.data.heatmapStats.max, logBase); return { y_min, y_max }; } - function adjustLogMax(max, base) { + adjustLogMax(max, base) { return Math.pow(base, Math.ceil(ticksUtils.logp(max, base))); } - function adjustLogMin(min, base) { + adjustLogMin(min, base) { return Math.pow(base, Math.floor(ticksUtils.logp(min, base))); } - function logScaleTickValues(domain, base) { + logScaleTickValues(domain, base) { let domainMin = domain[0]; let domainMax = domain[1]; let tickValues = []; @@ -393,8 +433,8 @@ export default function link(scope, elem, attrs, ctrl) { return tickValues; } - function tickValueFormatter(decimals, scaledDecimals = null) { - let format = panel.yAxis.format; + tickValueFormatter(decimals, scaledDecimals = null) { + let format = this.panel.yAxis.format; return function(value) { try { return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; @@ -405,181 +445,178 @@ export default function link(scope, elem, attrs, ctrl) { }; } - ctrl.tickValueFormatter = tickValueFormatter; - - function fixYAxisTickSize() { - heatmap + fixYAxisTickSize() { + this.heatmap .select('.axis-y') .selectAll('.tick line') - .attr('x2', chartWidth); + .attr('x2', this.chartWidth); } - function addAxes() { - chartHeight = height - margin.top - margin.bottom; - chartTop = margin.top; - chartBottom = chartTop + chartHeight; - - if (panel.dataFormat === 'tsbuckets') { - addYAxisFromBuckets(); + addAxes() { + this.chartHeight = this.height - this.margin.top - this.margin.bottom; + this.chartTop = this.margin.top; + this.chartBottom = this.chartTop + this.chartHeight; + if (this.panel.dataFormat === 'tsbuckets') { + this.addYAxisFromBuckets(); } else { - if (panel.yAxis.logBase === 1) { - addYAxis(); + if (this.panel.yAxis.logBase === 1) { + this.addYAxis(); } else { - addLogYAxis(); + this.addLogYAxis(); } } - yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - chartWidth = width - yAxisWidth - margin.right; - fixYAxisTickSize(); + this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.chartWidth = this.width - this.yAxisWidth - this.margin.right; + this.fixYAxisTickSize(); - addXAxis(); - xAxisHeight = getXAxisHeight(heatmap); + this.addXAxis(); + this.xAxisHeight = this.getXAxisHeight(this.heatmap); - if (!panel.yAxis.show) { - heatmap + if (!this.panel.yAxis.show) { + this.heatmap .select('.axis-y') .selectAll('line') .style('opacity', 0); } - if (!panel.xAxis.show) { - heatmap + if (!this.panel.xAxis.show) { + this.heatmap .select('.axis-x') .selectAll('line') .style('opacity', 0); } } - function addHeatmapCanvas() { - let heatmap_elem = $heatmap[0]; + addHeatmapCanvas() { + let heatmap_elem = this.$heatmap[0]; - width = Math.floor($heatmap.width()) - padding.right; - height = Math.floor($heatmap.height()) - padding.bottom; + this.width = Math.floor(this.$heatmap.width()) - this.padding.right; + this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom; - cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING; - cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND; + this.cardPadding = this.panel.cards.cardPadding !== null ? this.panel.cards.cardPadding : CARD_PADDING; + this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND; - if (heatmap) { - heatmap.remove(); + if (this.heatmap) { + this.heatmap.remove(); } - heatmap = d3 + this.heatmap = d3 .select(heatmap_elem) .append('svg') - .attr('width', width) - .attr('height', height); + .attr('width', this.width) + .attr('height', this.height); } - function addHeatmap() { - addHeatmapCanvas(); - addAxes(); + addHeatmap() { + this.addHeatmapCanvas(); + this.addAxes(); - if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') { - let log_base = panel.yAxis.logBase; - let domain = yScale.domain(); - let tick_values = logScaleTickValues(domain, log_base); - data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values)); + if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') { + let log_base = this.panel.yAxis.logBase; + let domain = this.yScale.domain(); + let tick_values = this.logScaleTickValues(domain, log_base); + this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values)); } - let cardsData = data.cards; - let maxValueAuto = data.cardStats.max; - let maxValue = panel.color.max || maxValueAuto; - let minValue = panel.color.min || 0; + let cardsData = this.data.cards; + let maxValueAuto = this.data.cardStats.max; + let maxValue = this.panel.color.max || maxValueAuto; + let minValue = this.panel.color.min || 0; - let colorScheme = _.find(ctrl.colorSchemes, { - value: panel.color.colorScheme, + let colorScheme = _.find(this.ctrl.colorSchemes, { + value: this.panel.color.colorScheme, }); - colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); - opacityScale = getOpacityScale(panel.color, maxValue); - setCardSize(); + this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); + this.opacityScale = getOpacityScale(this.panel.color, maxValue); + this.setCardSize(); - let cards = heatmap.selectAll('.heatmap-card').data(cardsData); + let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData); cards.append('title'); cards = cards .enter() .append('rect') - .attr('x', getCardX) - .attr('width', getCardWidth) - .attr('y', getCardY) - .attr('height', getCardHeight) - .attr('rx', cardRound) - .attr('ry', cardRound) + .attr('x', this.getCardX.bind(this)) + .attr('width', this.getCardWidth.bind(this)) + .attr('y', this.getCardY.bind(this)) + .attr('height', this.getCardHeight.bind(this)) + .attr('rx', this.cardRound) + .attr('ry', this.cardRound) .attr('class', 'bordered heatmap-card') - .style('fill', getCardColor) - .style('stroke', getCardColor) + .style('fill', this.getCardColor.bind(this)) + .style('stroke', this.getCardColor.bind(this)) .style('stroke-width', 0) - .style('opacity', getCardOpacity); + .style('opacity', this.getCardOpacity.bind(this)); - let $cards = $heatmap.find('.heatmap-card'); + let $cards = this.$heatmap.find('.heatmap-card'); $cards .on('mouseenter', event => { - tooltip.mouseOverBucket = true; - highlightCard(event); + this.tooltip.mouseOverBucket = true; + this.highlightCard(event); }) .on('mouseleave', event => { - tooltip.mouseOverBucket = false; - resetCardHighLight(event); + this.tooltip.mouseOverBucket = false; + this.resetCardHighLight(event); }); } - function highlightCard(event) { + highlightCard(event) { let color = d3.select(event.target).style('fill'); let highlightColor = d3.color(color).darker(2); let strokeColor = d3.color(color).brighter(4); let current_card = d3.select(event.target); - tooltip.originalFillColor = color; + this.tooltip.originalFillColor = color; current_card .style('fill', highlightColor.toString()) .style('stroke', strokeColor.toString()) .style('stroke-width', 1); } - function resetCardHighLight(event) { + resetCardHighLight(event) { d3 .select(event.target) - .style('fill', tooltip.originalFillColor) - .style('stroke', tooltip.originalFillColor) + .style('fill', this.tooltip.originalFillColor) + .style('stroke', this.tooltip.originalFillColor) .style('stroke-width', 0); } - function setCardSize() { - let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0)); - let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize)); + setCardSize() { + let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0)); + let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize)); - if (panel.yAxis.logBase !== 1) { - let base = panel.yAxis.logBase; - let splitFactor = data.yBucketSize || 1; - yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor); + if (this.panel.yAxis.logBase !== 1) { + let base = this.panel.yAxis.logBase; + let splitFactor = this.data.yBucketSize || 1; + yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor); } - cardWidth = xGridSize - cardPadding * 2; - cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0; + this.cardWidth = xGridSize - this.cardPadding * 2; + this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0; } - function getCardX(d) { + getCardX(d) { let x; - if (xScale(d.x) < 0) { + if (this.xScale(d.x) < 0) { // Cut card left to prevent overlay - x = yAxisWidth + cardPadding; + x = this.yAxisWidth + this.cardPadding; } else { - x = xScale(d.x) + yAxisWidth + cardPadding; + x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding; } return x; } - function getCardWidth(d) { + getCardWidth(d) { let w; - if (xScale(d.x) < 0) { + if (this.xScale(d.x) < 0) { // Cut card left to prevent overlay - let cutted_width = xScale(d.x) + cardWidth; + let cutted_width = this.xScale(d.x) + this.cardWidth; w = cutted_width > 0 ? cutted_width : 0; - } else if (xScale(d.x) + cardWidth > chartWidth) { + } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) { // Cut card right to prevent overlay - w = chartWidth - xScale(d.x) - cardPadding; + w = this.chartWidth - this.xScale(d.x) - this.cardPadding; } else { - w = cardWidth; + w = this.cardWidth; } // Card width should be MIN_CARD_SIZE at least @@ -587,138 +624,117 @@ export default function link(scope, elem, attrs, ctrl) { return w; } - function getCardY(d) { - let y = yScale(d.y) + chartTop - cardHeight - cardPadding; - if (panel.yAxis.logBase !== 1 && d.y === 0) { - y = chartBottom - cardHeight - cardPadding; + getCardY(d) { + let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; + if (this.panel.yAxis.logBase !== 1 && d.y === 0) { + y = this.chartBottom - this.cardHeight - this.cardPadding; } else { - if (y < chartTop) { - y = chartTop; + if (y < this.chartTop) { + y = this.chartTop; } } return y; } - function getCardHeight(d) { - let y = yScale(d.y) + chartTop - cardHeight - cardPadding; - let h = cardHeight; + getCardHeight(d) { + let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; + let h = this.cardHeight; - if (panel.yAxis.logBase !== 1 && d.y === 0) { - return cardHeight; + if (this.panel.yAxis.logBase !== 1 && d.y === 0) { + return this.cardHeight; } // Cut card height to prevent overlay - if (y < chartTop) { - h = yScale(d.y) - cardPadding; - } else if (yScale(d.y) > chartBottom) { - h = chartBottom - y; - } else if (y + cardHeight > chartBottom) { - h = chartBottom - y; + if (y < this.chartTop) { + h = this.yScale(d.y) - this.cardPadding; + } else if (this.yScale(d.y) > this.chartBottom) { + h = this.chartBottom - y; + } else if (y + this.cardHeight > this.chartBottom) { + h = this.chartBottom - y; } // Height can't be more than chart height - h = Math.min(h, chartHeight); + h = Math.min(h, this.chartHeight); // Card height should be MIN_CARD_SIZE at least h = Math.max(h, MIN_CARD_SIZE); return h; } - function getCardColor(d) { - if (panel.color.mode === 'opacity') { - return panel.color.cardColor; + getCardColor(d) { + if (this.panel.color.mode === 'opacity') { + return this.panel.color.cardColor; } else { - return colorScale(d.count); + return this.colorScale(d.count); } } - function getCardOpacity(d) { - if (panel.color.mode === 'opacity') { - return opacityScale(d.count); + getCardOpacity(d) { + if (this.panel.color.mode === 'opacity') { + return this.opacityScale(d.count); } else { return 1; } } - ///////////////////////////// - // Selection and crosshair // - ///////////////////////////// + onMouseDown(event) { + this.selection.active = true; + this.selection.x1 = event.offsetX; - // Shared crosshair and tooltip - appEvents.on( - 'graph-hover', - event => { - drawSharedCrosshair(event.pos); - }, - scope - ); - - appEvents.on( - 'graph-hover-clear', - () => { - clearCrosshair(); - }, - scope - ); - - function onMouseDown(event) { - selection.active = true; - selection.x1 = event.offsetX; - - mouseUpHandler = function() { - onMouseUp(); + this.mouseUpHandler = () => { + this.onMouseUp(); }; - $(document).one('mouseup', mouseUpHandler); + $(document).one('mouseup', this.mouseUpHandler.bind(this)); } - function onMouseUp() { - $(document).unbind('mouseup', mouseUpHandler); - mouseUpHandler = null; - selection.active = false; + onMouseUp() { + $(document).unbind('mouseup', this.mouseUpHandler.bind(this)); + this.mouseUpHandler = null; + this.selection.active = false; - let selectionRange = Math.abs(selection.x2 - selection.x1); - if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { - let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth); - let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth); + let selectionRange = Math.abs(this.selection.x2 - this.selection.x1); + if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { + let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth); + let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth); - ctrl.timeSrv.setTime({ + this.ctrl.timeSrv.setTime({ from: moment.utc(timeFrom), to: moment.utc(timeTo), }); } - clearSelection(); + this.clearSelection(); } - function onMouseLeave() { + onMouseLeave() { appEvents.emit('graph-hover-clear'); - clearCrosshair(); + this.clearCrosshair(); } - function onMouseMove(event) { - if (!heatmap) { + onMouseMove(event) { + if (!this.heatmap) { return; } - if (selection.active) { + if (this.selection.active) { // Clear crosshair and tooltip - clearCrosshair(); - tooltip.destroy(); + this.clearCrosshair(); + this.tooltip.destroy(); - selection.x2 = limitSelection(event.offsetX); - drawSelection(selection.x1, selection.x2); + this.selection.x2 = this.limitSelection(event.offsetX); + this.drawSelection(this.selection.x1, this.selection.x2); } else { - emitGraphHoverEvent(event); - drawCrosshair(event.offsetX); - tooltip.show(event, data); + this.emitGraphHoverEvent(event); + this.drawCrosshair(event.offsetX); + this.tooltip.show(event, this.data); } } - function emitGraphHoverEvent(event) { - let x = xScale.invert(event.offsetX - yAxisWidth).valueOf(); - let y = yScale.invert(event.offsetY); + emitGraphHoverEvent(event) { + let x = this.xScale.invert(event.offsetX - this.yAxisWidth).valueOf(); + let y = this.yScale.invert(event.offsetY); let pos = { pageX: event.pageX, pageY: event.pageY, @@ -730,105 +746,100 @@ export default function link(scope, elem, attrs, ctrl) { }; // Set minimum offset to prevent showing legend from another panel - pos.panelRelY = Math.max(event.offsetY / height, 0.001); + pos.panelRelY = Math.max(event.offsetY / this.height, 0.001); // broadcast to other graph panels that we are hovering - appEvents.emit('graph-hover', { pos: pos, panel: panel }); + appEvents.emit('graph-hover', { pos: pos, panel: this.panel }); } - function limitSelection(x2) { - x2 = Math.max(x2, yAxisWidth); - x2 = Math.min(x2, chartWidth + yAxisWidth); + limitSelection(x2) { + x2 = Math.max(x2, this.yAxisWidth); + x2 = Math.min(x2, this.chartWidth + this.yAxisWidth); return x2; } - function drawSelection(posX1, posX2) { - if (heatmap) { - heatmap.selectAll('.heatmap-selection').remove(); + drawSelection(posX1, posX2) { + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-selection').remove(); let selectionX = Math.min(posX1, posX2); let selectionWidth = Math.abs(posX1 - posX2); if (selectionWidth > MIN_SELECTION_WIDTH) { - heatmap + this.heatmap .append('rect') .attr('class', 'heatmap-selection') .attr('x', selectionX) .attr('width', selectionWidth) - .attr('y', chartTop) - .attr('height', chartHeight); + .attr('y', this.chartTop) + .attr('height', this.chartHeight); } } } - function clearSelection() { - selection.x1 = -1; - selection.x2 = -1; + clearSelection() { + this.selection.x1 = -1; + this.selection.x2 = -1; - if (heatmap) { - heatmap.selectAll('.heatmap-selection').remove(); + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-selection').remove(); } } - function drawCrosshair(position) { - if (heatmap) { - heatmap.selectAll('.heatmap-crosshair').remove(); + drawCrosshair(position) { + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-crosshair').remove(); let posX = position; - posX = Math.max(posX, yAxisWidth); - posX = Math.min(posX, chartWidth + yAxisWidth); + posX = Math.max(posX, this.yAxisWidth); + posX = Math.min(posX, this.chartWidth + this.yAxisWidth); - heatmap + this.heatmap .append('g') .attr('class', 'heatmap-crosshair') .attr('transform', 'translate(' + posX + ',0)') .append('line') .attr('x1', 1) - .attr('y1', chartTop) + .attr('y1', this.chartTop) .attr('x2', 1) - .attr('y2', chartBottom) + .attr('y2', this.chartBottom) .attr('stroke-width', 1); } } - function drawSharedCrosshair(pos) { - if (heatmap && ctrl.dashboard.graphTooltip !== 0) { - let posX = xScale(pos.x) + yAxisWidth; - drawCrosshair(posX); + drawSharedCrosshair(pos) { + if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) { + let posX = this.xScale(pos.x) + this.yAxisWidth; + this.drawCrosshair(posX); } } - function clearCrosshair() { - if (heatmap) { - heatmap.selectAll('.heatmap-crosshair').remove(); + clearCrosshair() { + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-crosshair').remove(); } } - function render() { - data = ctrl.data; - panel = ctrl.panel; - timeRange = ctrl.range; + render() { + this.data = this.ctrl.data; + this.panel = this.ctrl.panel; + this.timeRange = this.ctrl.range; - if (!setElementHeight() || !data) { + if (!this.setElementHeight() || !this.data) { return; } // Draw default axes and return if no data - if (_.isEmpty(data.buckets)) { - addHeatmapCanvas(); - addAxes(); + if (_.isEmpty(this.data.buckets)) { + this.addHeatmapCanvas(); + this.addAxes(); return; } - addHeatmap(); - scope.yAxisWidth = yAxisWidth; - scope.xAxisHeight = xAxisHeight; - scope.chartHeight = chartHeight; - scope.chartWidth = chartWidth; - scope.chartTop = chartTop; + this.addHeatmap(); + this.scope.yAxisWidth = this.yAxisWidth; + this.scope.xAxisHeight = this.xAxisHeight; + this.scope.chartHeight = this.chartHeight; + this.scope.chartWidth = this.chartWidth; + this.scope.chartTop = this.chartTop; } - - // Register selection listeners - $heatmap.on('mousedown', onMouseDown); - $heatmap.on('mousemove', onMouseMove); - $heatmap.on('mouseleave', onMouseLeave); } diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts deleted file mode 100644 index f52b6d1d985..00000000000 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; - -import '../module'; -import angular from 'angular'; -import $ from 'jquery'; -import helpers from 'test/specs/helpers'; -import TimeSeries from 'app/core/time_series2'; -import moment from 'moment'; -import { Emitter } from 'app/core/core'; -import rendering from '../rendering'; -import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; - -describe('grafanaHeatmap', function() { - beforeEach(angularMocks.module('grafana.core')); - - function heatmapScenario(desc, func, elementWidth = 500) { - describe(desc, function() { - var ctx: any = {}; - - ctx.setup = function(setupFunc) { - beforeEach( - angularMocks.module(function($provide) { - $provide.value('timeSrv', new helpers.TimeSrvStub()); - }) - ); - - beforeEach( - angularMocks.inject(function($rootScope, $compile) { - var ctrl: any = { - colorSchemes: [ - { - name: 'Oranges', - value: 'interpolateOranges', - invert: 'dark', - }, - { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, - ], - events: new Emitter(), - height: 200, - panel: { - heatmap: {}, - cards: { - cardPadding: null, - cardRound: null, - }, - color: { - mode: 'spectrum', - cardColor: '#b4ff00', - colorScale: 'linear', - exponent: 0.5, - colorScheme: 'interpolateOranges', - fillBackground: false, - }, - legend: { - show: false, - }, - xBucketSize: 1000, - xBucketNumber: null, - yBucketSize: 1, - yBucketNumber: null, - xAxis: { - show: true, - }, - yAxis: { - show: true, - format: 'short', - decimals: null, - logBase: 1, - splitFactor: null, - min: null, - max: null, - removeZeroValues: false, - }, - tooltip: { - show: true, - seriesStat: false, - showHistogram: false, - }, - highlightCards: true, - }, - renderingCompleted: sinon.spy(), - hiddenSeries: {}, - dashboard: { - getTimezone: sinon.stub().returns('utc'), - }, - range: { - from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'), - to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'), - }, - }; - - var scope = $rootScope.$new(); - scope.ctrl = ctrl; - - ctx.series = []; - ctx.series.push( - new TimeSeries({ - datapoints: [[1, 1422774000000], [2, 1422774060000]], - alias: 'series1', - }) - ); - ctx.series.push( - new TimeSeries({ - datapoints: [[2, 1422774000000], [3, 1422774060000]], - alias: 'series2', - }) - ); - - ctx.data = { - heatmapStats: { - min: 1, - max: 3, - minLog: 1, - }, - xBucketSize: ctrl.panel.xBucketSize, - yBucketSize: ctrl.panel.yBucketSize, - }; - - setupFunc(ctrl, ctx); - - let logBase = ctrl.panel.yAxis.logBase; - let bucketsData; - if (ctrl.panel.dataFormat === 'tsbuckets') { - bucketsData = histogramToHeatmap(ctx.series); - } else { - bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); - } - ctx.data.buckets = bucketsData; - - let { cards, cardStats } = convertToCards(bucketsData); - ctx.data.cards = cards; - ctx.data.cardStats = cardStats; - - let elemHtml = ` -
-
-
-
-
`; - - var element = angular.element(elemHtml); - $compile(element)(scope); - scope.$digest(); - - ctrl.data = ctx.data; - ctx.element = element; - rendering(scope, $(element), [], ctrl); - ctrl.events.emit('render'); - }) - ); - }; - - func(ctx); - }); - } - - heatmapScenario('default options', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '2', '3']); - }); - - it('should draw correct X axis', function() { - var xTicks = getTicks(ctx.element, '.axis-x'); - let expectedTicks = [ - formatTime('01 Mar 2017 10:00:00'), - formatTime('01 Mar 2017 10:15:00'), - formatTime('01 Mar 2017 10:30:00'), - formatTime('01 Mar 2017 10:45:00'), - formatTime('01 Mar 2017 11:00:00'), - ]; - expect(xTicks).to.eql(expectedTicks); - }); - }); - - heatmapScenario('when logBase is 2', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 2; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '2', '4']); - }); - }); - - heatmapScenario('when logBase is 10', function(ctx) { - ctx.setup(function(ctrl, ctx) { - ctrl.panel.yAxis.logBase = 10; - - ctx.series.push( - new TimeSeries({ - datapoints: [[10, 1422774000000], [20, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 20; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '10', '100']); - }); - }); - - heatmapScenario('when logBase is 32', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 32; - - ctx.series.push( - new TimeSeries({ - datapoints: [[10, 1422774000000], [100, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 100; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '32', '1.0 K']); - }); - }); - - heatmapScenario('when logBase is 1024', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1024; - - ctx.series.push( - new TimeSeries({ - datapoints: [[2000, 1422774000000], [300000, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 300000; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '1 K', '1.0 Mil']); - }); - }); - - heatmapScenario('when Y axis format set to "none"', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - ctrl.panel.yAxis.format = 'none'; - ctx.data.heatmapStats.max = 10000; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['0', '2000', '4000', '6000', '8000', '10000', '12000']); - }); - }); - - heatmapScenario('when Y axis format set to "second"', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - ctrl.panel.yAxis.format = 's'; - ctx.data.heatmapStats.max = 3600; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']); - }); - }); - - heatmapScenario('when data format is Time series buckets', function(ctx) { - ctx.setup(function(ctrl, ctx) { - ctrl.panel.dataFormat = 'tsbuckets'; - - const series = [ - { - alias: '1', - datapoints: [[1000, 1422774000000], [200000, 1422774060000]], - }, - { - alias: '2', - datapoints: [[3000, 1422774000000], [400000, 1422774060000]], - }, - { - alias: '3', - datapoints: [[2000, 1422774000000], [300000, 1422774060000]], - }, - ]; - ctx.series = series.map(s => new TimeSeries(s)); - - ctx.data.tsBuckets = series.map(s => s.alias).concat(''); - ctx.data.yBucketSize = 1; - let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]); - ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet); - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '2', '3', '']); - }); - }); -}); - -function getTicks(element, axisSelector) { - return element - .find(axisSelector) - .find('text') - .map(function() { - return this.textContent; - }) - .get(); -} - -function formatTime(timeStr) { - let format = 'HH:mm'; - return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format); -}