| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864 |
- import _ from 'lodash';
- import $ from 'jquery';
- import * as d3 from 'd3';
- import { appEvents, contextSrv } from 'app/core/core';
- import * as ticksUtils from 'app/core/utils/ticks';
- import { HeatmapTooltip } from './heatmap_tooltip';
- import { mergeZeroBuckets } from './heatmap_data_converter';
- import { getColorScale, getOpacityScale } from './color_scale';
- import { GrafanaThemeType, getColorFromHexRgbOrName, getValueFormat } from '@grafana/ui';
- import { toUtc } from '@grafana/data';
- const MIN_CARD_SIZE = 1,
- CARD_PADDING = 1,
- CARD_ROUND = 0,
- DATA_RANGE_WIDING_FACTOR = 1.2,
- DEFAULT_X_TICK_SIZE_PX = 100,
- DEFAULT_Y_TICK_SIZE_PX = 50,
- X_AXIS_TICK_PADDING = 10,
- Y_AXIS_TICK_PADDING = 5,
- MIN_SELECTION_WIDTH = 2;
- export default function rendering(scope: any, elem: any, attrs: any, ctrl: any) {
- 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;
- selection: any;
- padding: any;
- margin: any;
- dataRangeWidingFactor: number;
- constructor(private scope: any, private elem: any, attrs: any, private ctrl: any) {
- // $heatmap is JQuery object, but heatmap is D3
- this.$heatmap = this.elem.find('.heatmap-panel');
- this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope);
- this.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;
- this.ctrl.events.on('render', this.onRender.bind(this));
- this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
- /////////////////////////////
- // Selection and crosshair //
- /////////////////////////////
- // 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: { pos: any }) {
- this.drawSharedCrosshair(event.pos);
- }
- onRender() {
- this.render();
- this.ctrl.renderingCompleted();
- }
- setElementHeight() {
- try {
- let height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
- if (_.isString(height)) {
- height = parseInt(height.replace('px', ''), 10);
- }
- height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend
- this.$heatmap.css('height', height + 'px');
- return true;
- } catch (e) {
- // IE throws errors sometimes
- return false;
- }
- }
- getYAxisWidth(elem: any) {
- const axisText = elem.selectAll('.axis-y text').nodes();
- const maxTextWidth = _.max(
- _.map(axisText, text => {
- // Use SVG getBBox method
- return text.getBBox().width;
- })
- );
- return maxTextWidth;
- }
- getXAxisHeight(elem: any) {
- const axisLine = elem.select('.axis-x line');
- if (!axisLine.empty()) {
- const axisLinePosition = parseFloat(elem.select('.axis-x line').attr('y2'));
- const canvasWidth = parseFloat(elem.attr('height'));
- return canvasWidth - axisLinePosition;
- } else {
- // Default height
- return 30;
- }
- }
- addXAxis() {
- this.scope.xScale = this.xScale = d3
- .scaleTime()
- .domain([this.timeRange.from, this.timeRange.to])
- .range([0, this.chartWidth]);
- const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
- const grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
- let timeFormat;
- const dashboardTimeZone = this.ctrl.dashboard.getTimezone();
- if (dashboardTimeZone === 'utc') {
- timeFormat = d3.utcFormat(grafanaTimeFormatter);
- } else {
- timeFormat = d3.timeFormat(grafanaTimeFormatter);
- }
- const xAxis = d3
- .axisBottom(this.xScale)
- .ticks(ticks)
- .tickFormat(timeFormat)
- .tickPadding(X_AXIS_TICK_PADDING)
- .tickSize(this.chartHeight);
- const posY = this.margin.top;
- const 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)
- this.heatmap
- .select('.axis-x')
- .select('.domain')
- .remove();
- }
- addYAxis() {
- let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX);
- let tickInterval = ticksUtils.tickStep(this.data.heatmapStats.min, this.data.heatmapStats.max, ticks);
- let { yMin, yMax } = this.wideYAxisRange(this.data.heatmapStats.min, this.data.heatmapStats.max, tickInterval);
- // Rewrite min and max if it have been set explicitly
- yMin = this.panel.yAxis.min !== null ? this.panel.yAxis.min : yMin;
- yMax = this.panel.yAxis.max !== null ? this.panel.yAxis.max : yMax;
- // Adjust ticks after Y range widening
- tickInterval = ticksUtils.tickStep(yMin, yMax, ticks);
- ticks = Math.ceil((yMax - yMin) / tickInterval);
- const decimalsAuto = ticksUtils.getPrecision(tickInterval);
- 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)
- const flotTickSize = ticksUtils.getFlotTickSize(yMin, yMax, ticks, decimalsAuto);
- const scaledDecimals = ticksUtils.getScaledDecimals(decimals, flotTickSize);
- this.ctrl.decimals = decimals;
- this.ctrl.scaledDecimals = scaledDecimals;
- // Set default Y min and max if no data
- if (_.isEmpty(this.data.buckets)) {
- yMax = 1;
- yMin = -1;
- ticks = 3;
- decimals = 1;
- }
- this.data.yAxis = {
- min: yMin,
- max: yMax,
- ticks: ticks,
- };
- this.scope.yScale = this.yScale = d3
- .scaleLinear()
- .domain([yMin, yMax])
- .range([this.chartHeight, 0]);
- const yAxis = d3
- .axisLeft(this.yScale)
- .ticks(ticks)
- .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
- .tickSizeInner(0 - this.width)
- .tickSizeOuter(0)
- .tickPadding(Y_AXIS_TICK_PADDING);
- this.heatmap
- .append('g')
- .attr('class', 'axis axis-y')
- .call(yAxis);
- // Calculate Y axis width first, then move axis into visible area
- 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)
- this.heatmap
- .select('.axis-y')
- .select('.domain')
- .remove();
- }
- // Wide Y values range and anjust to bucket size
- wideYAxisRange(min: number, max: number, tickInterval: number) {
- const yWiding = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2;
- let yMin, yMax;
- if (tickInterval === 0) {
- yMax = max * this.dataRangeWidingFactor;
- yMin = min - min * (this.dataRangeWidingFactor - 1);
- } else {
- yMax = Math.ceil((max + yWiding) / tickInterval) * tickInterval;
- yMin = Math.floor((min - yWiding) / tickInterval) * tickInterval;
- }
- // Don't wide axis below 0 if all values are positive
- if (min >= 0 && yMin < 0) {
- yMin = 0;
- }
- return { yMin, yMax };
- }
- addLogYAxis() {
- const logBase = this.panel.yAxis.logBase;
- let { yMin, yMax } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, logBase);
- yMin =
- this.panel.yAxis.min && this.panel.yAxis.min !== '0' ? this.adjustLogMin(this.panel.yAxis.min, logBase) : yMin;
- yMax = this.panel.yAxis.max !== null ? this.adjustLogMax(this.panel.yAxis.max, logBase) : yMax;
- // Set default Y min and max if no data
- if (_.isEmpty(this.data.buckets)) {
- yMax = Math.pow(logBase, 2);
- yMin = 1;
- }
- this.scope.yScale = this.yScale = d3
- .scaleLog()
- .base(this.panel.yAxis.logBase)
- .domain([yMin, yMax])
- .range([this.chartHeight, 0]);
- const domain = this.yScale.domain();
- const tickValues = this.logScaleTickValues(domain, logBase);
- const decimalsAuto = ticksUtils.getPrecision(yMin);
- const decimals = this.panel.yAxis.decimals || decimalsAuto;
- // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
- const flotTickSize = ticksUtils.getFlotTickSize(yMin, yMax, tickValues.length, decimalsAuto);
- const scaledDecimals = ticksUtils.getScaledDecimals(decimals, flotTickSize);
- this.ctrl.decimals = decimals;
- this.ctrl.scaledDecimals = scaledDecimals;
- this.data.yAxis = {
- min: yMin,
- max: yMax,
- ticks: tickValues.length,
- };
- const yAxis = d3
- .axisLeft(this.yScale)
- .tickValues(tickValues)
- .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
- .tickSizeInner(0 - this.width)
- .tickSizeOuter(0)
- .tickPadding(Y_AXIS_TICK_PADDING);
- this.heatmap
- .append('g')
- .attr('class', 'axis axis-y')
- .call(yAxis);
- // Calculate Y axis width first, then move axis into visible area
- 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 + ')');
- // Set first tick as pseudo 0
- if (yMin < 1) {
- this.heatmap
- .select('.axis-y')
- .select('.tick text')
- .text('0');
- }
- // Remove vertical line in the right of axis labels (called domain in d3)
- this.heatmap
- .select('.axis-y')
- .select('.domain')
- .remove();
- }
- addYAxisFromBuckets() {
- const tsBuckets = this.data.tsBuckets;
- this.scope.yScale = this.yScale = d3
- .scaleLinear()
- .domain([0, tsBuckets.length - 1])
- .range([this.chartHeight, 0]);
- const tickValues = _.map(tsBuckets, (b, i) => i);
- const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision));
- const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
- this.ctrl.decimals = decimals;
- const tickValueFormatter = this.tickValueFormatter.bind(this);
- function tickFormatter(valIndex: string) {
- let valueFormatted = tsBuckets[valIndex];
- if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
- // Try to format numeric tick labels
- valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted));
- }
- return valueFormatted;
- }
- const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i));
- this.data.tsBucketsFormatted = tsBucketsFormatted;
- const yAxis = d3
- .axisLeft(this.yScale)
- .tickValues(tickValues)
- .tickFormat(tickFormatter)
- .tickSizeInner(0 - this.width)
- .tickSizeOuter(0)
- .tickPadding(Y_AXIS_TICK_PADDING);
- this.heatmap
- .append('g')
- .attr('class', 'axis axis-y')
- .call(yAxis);
- // Calculate Y axis width first, then move axis into visible area
- 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 + ')');
- if (this.panel.yBucketBound === 'middle' && tickValues && tickValues.length) {
- // Shift Y axis labels to the middle of bucket
- const tickShift = 0 - this.chartHeight / (tickValues.length - 1) / 2;
- this.heatmap.selectAll('.axis-y text').attr('transform', 'translate(' + 0 + ',' + tickShift + ')');
- }
- // Remove vertical line in the right of axis labels (called domain in d3)
- this.heatmap
- .select('.axis-y')
- .select('.domain')
- .remove();
- }
- // Adjust data range to log base
- adjustLogRange(min: number, max: number, logBase: number) {
- let yMin = this.data.heatmapStats.minLog;
- if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
- yMin = 1;
- } else {
- yMin = this.adjustLogMin(this.data.heatmapStats.minLog, logBase);
- }
- // Adjust max Y value to log base
- const yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase);
- return { yMin, yMax };
- }
- adjustLogMax(max: number, base: number) {
- return Math.pow(base, Math.ceil(ticksUtils.logp(max, base)));
- }
- adjustLogMin(min: number, base: number) {
- return Math.pow(base, Math.floor(ticksUtils.logp(min, base)));
- }
- logScaleTickValues(domain: any[], base: number) {
- const domainMin = domain[0];
- const domainMax = domain[1];
- const tickValues = [];
- if (domainMin < 1) {
- const underOneTicks = Math.floor(ticksUtils.logp(domainMin, base));
- for (let i = underOneTicks; i < 0; i++) {
- const tickValue = Math.pow(base, i);
- tickValues.push(tickValue);
- }
- }
- const ticks = Math.ceil(ticksUtils.logp(domainMax, base));
- for (let i = 0; i <= ticks; i++) {
- const tickValue = Math.pow(base, i);
- tickValues.push(tickValue);
- }
- return tickValues;
- }
- tickValueFormatter(decimals: number, scaledDecimals: any = null) {
- const format = this.panel.yAxis.format;
- return (value: any) => {
- try {
- return format !== 'none' ? getValueFormat(format)(value, decimals, scaledDecimals) : value;
- } catch (err) {
- console.error(err.message || err);
- return value;
- }
- };
- }
- fixYAxisTickSize() {
- this.heatmap
- .select('.axis-y')
- .selectAll('.tick line')
- .attr('x2', this.chartWidth);
- }
- 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 (this.panel.yAxis.logBase === 1) {
- this.addYAxis();
- } else {
- this.addLogYAxis();
- }
- }
- this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
- this.chartWidth = this.width - this.yAxisWidth - this.margin.right;
- this.fixYAxisTickSize();
- this.addXAxis();
- this.xAxisHeight = this.getXAxisHeight(this.heatmap);
- if (!this.panel.yAxis.show) {
- this.heatmap
- .select('.axis-y')
- .selectAll('line')
- .style('opacity', 0);
- }
- if (!this.panel.xAxis.show) {
- this.heatmap
- .select('.axis-x')
- .selectAll('line')
- .style('opacity', 0);
- }
- }
- addHeatmapCanvas() {
- const heatmapElem = this.$heatmap[0];
- this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
- this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom;
- 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 (this.heatmap) {
- this.heatmap.remove();
- }
- this.heatmap = d3
- .select(heatmapElem)
- .append('svg')
- .attr('width', this.width)
- .attr('height', this.height);
- }
- addHeatmap() {
- this.addHeatmapCanvas();
- this.addAxes();
- if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') {
- const logBase = this.panel.yAxis.logBase;
- const domain = this.yScale.domain();
- const tickValues = this.logScaleTickValues(domain, logBase);
- this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tickValues)!);
- }
- const cardsData = this.data.cards;
- const cardStats = this.data.cardStats;
- const maxValueAuto = cardStats.max;
- const minValueAuto = Math.min(cardStats.min, 0);
- const maxValue = _.isNil(this.panel.color.max) ? maxValueAuto : this.panel.color.max;
- const minValue = _.isNil(this.panel.color.min) ? minValueAuto : this.panel.color.min;
- const colorScheme: any = _.find(this.ctrl.colorSchemes, {
- value: this.panel.color.colorScheme,
- });
- this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
- this.opacityScale = getOpacityScale(this.panel.color, maxValue, minValue);
- this.setCardSize();
- let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
- cards.append('title');
- cards = cards
- .enter()
- .append('rect')
- .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', this.getCardColor.bind(this))
- .style('stroke', this.getCardColor.bind(this))
- .style('stroke-width', 0)
- .style('opacity', this.getCardOpacity.bind(this));
- const $cards = this.$heatmap.find('.heatmap-card');
- $cards
- .on('mouseenter', (event: any) => {
- this.tooltip.mouseOverBucket = true;
- this.highlightCard(event);
- })
- .on('mouseleave', (event: any) => {
- this.tooltip.mouseOverBucket = false;
- this.resetCardHighLight(event);
- });
- }
- highlightCard(event: any) {
- const color = d3.select(event.target).style('fill');
- const highlightColor = d3.color(color).darker(2);
- const strokeColor = d3.color(color).brighter(4);
- const currentCard = d3.select(event.target);
- this.tooltip.originalFillColor = color;
- currentCard
- .style('fill', highlightColor.toString())
- .style('stroke', strokeColor.toString())
- .style('stroke-width', 1);
- }
- resetCardHighLight(event: any) {
- d3.select(event.target)
- .style('fill', this.tooltip.originalFillColor)
- .style('stroke', this.tooltip.originalFillColor)
- .style('stroke-width', 0);
- }
- setCardSize() {
- const 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 (this.panel.yAxis.logBase !== 1) {
- const base = this.panel.yAxis.logBase;
- const splitFactor = this.data.yBucketSize || 1;
- yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor);
- }
- const cardWidth = xGridSize - this.cardPadding * 2;
- this.cardWidth = Math.max(cardWidth, MIN_CARD_SIZE);
- this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0;
- }
- getCardX(d: { x: any }) {
- let x;
- if (this.xScale(d.x) < 0) {
- // Cut card left to prevent overlay
- x = this.yAxisWidth + this.cardPadding;
- } else {
- x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding;
- }
- return x;
- }
- getCardWidth(d: { x: any }) {
- let w = this.cardWidth;
- if (this.xScale(d.x) < 0) {
- // Cut card left to prevent overlay
- w = this.xScale(d.x) + this.cardWidth;
- } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) {
- // Cut card right to prevent overlay
- w = this.chartWidth - this.xScale(d.x) - this.cardPadding;
- }
- // Card width should be MIN_CARD_SIZE at least, but cut cards shouldn't be displayed
- w = w > 0 ? Math.max(w, MIN_CARD_SIZE) : 0;
- return w;
- }
- getCardY(d: { y: number }) {
- 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 < this.chartTop) {
- y = this.chartTop;
- }
- }
- return y;
- }
- getCardHeight(d: { y: number }) {
- const y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
- let h = this.cardHeight;
- if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
- return this.cardHeight;
- }
- // Cut card height to prevent overlay
- 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, this.chartHeight);
- // Card height should be MIN_CARD_SIZE at least
- h = Math.max(h, MIN_CARD_SIZE);
- return h;
- }
- getCardColor(d: { count: any }) {
- if (this.panel.color.mode === 'opacity') {
- return getColorFromHexRgbOrName(
- this.panel.color.cardColor,
- contextSrv.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark
- );
- } else {
- return this.colorScale(d.count);
- }
- }
- getCardOpacity(d: { count: any }) {
- if (this.panel.color.mode === 'opacity') {
- return this.opacityScale(d.count);
- } else {
- return 1;
- }
- }
- getEventOffset(event: any) {
- const elemOffset = this.$heatmap.offset();
- const x = Math.floor(event.clientX - elemOffset.left);
- const y = Math.floor(event.clientY - elemOffset.top);
- return { x, y };
- }
- onMouseDown(event: any) {
- const offset = this.getEventOffset(event);
- this.selection.active = true;
- this.selection.x1 = offset.x;
- this.mouseUpHandler = () => {
- this.onMouseUp();
- };
- $(document).one('mouseup', this.mouseUpHandler.bind(this));
- }
- onMouseUp() {
- $(document).unbind('mouseup', this.mouseUpHandler.bind(this));
- this.mouseUpHandler = null;
- this.selection.active = false;
- const selectionRange = Math.abs(this.selection.x2 - this.selection.x1);
- if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
- const timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth);
- const timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth);
- this.ctrl.timeSrv.setTime({
- from: toUtc(timeFrom),
- to: toUtc(timeTo),
- });
- }
- this.clearSelection();
- }
- onMouseLeave() {
- appEvents.emit('graph-hover-clear');
- this.clearCrosshair();
- }
- onMouseMove(event: any) {
- if (!this.heatmap) {
- return;
- }
- const offset = this.getEventOffset(event);
- if (this.selection.active) {
- // Clear crosshair and tooltip
- this.clearCrosshair();
- this.tooltip.destroy();
- this.selection.x2 = this.limitSelection(offset.x);
- this.drawSelection(this.selection.x1, this.selection.x2);
- } else {
- const pos = this.getEventPos(event, offset);
- this.drawCrosshair(offset.x);
- this.tooltip.show(pos, this.data);
- this.emitGraphHoverEvent(pos);
- }
- }
- getEventPos(event: { pageX: any; pageY: any }, offset: { x: any; y: any }) {
- const x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf();
- const y = this.yScale.invert(offset.y - this.chartTop);
- const pos: any = {
- pageX: event.pageX,
- pageY: event.pageY,
- x: x,
- x1: x,
- y: y,
- y1: y,
- panelRelY: null,
- offset,
- };
- return pos;
- }
- emitGraphHoverEvent(pos: { panelRelY: number; offset: { y: number } }) {
- // Set minimum offset to prevent showing legend from another panel
- pos.panelRelY = Math.max(pos.offset.y / this.height, 0.001);
- // broadcast to other graph panels that we are hovering
- appEvents.emit('graph-hover', { pos: pos, panel: this.panel });
- }
- limitSelection(x2: number) {
- x2 = Math.max(x2, this.yAxisWidth);
- x2 = Math.min(x2, this.chartWidth + this.yAxisWidth);
- return x2;
- }
- drawSelection(posX1: number, posX2: number) {
- if (this.heatmap) {
- this.heatmap.selectAll('.heatmap-selection').remove();
- const selectionX = Math.min(posX1, posX2);
- const selectionWidth = Math.abs(posX1 - posX2);
- if (selectionWidth > MIN_SELECTION_WIDTH) {
- this.heatmap
- .append('rect')
- .attr('class', 'heatmap-selection')
- .attr('x', selectionX)
- .attr('width', selectionWidth)
- .attr('y', this.chartTop)
- .attr('height', this.chartHeight);
- }
- }
- }
- clearSelection() {
- this.selection.x1 = -1;
- this.selection.x2 = -1;
- if (this.heatmap) {
- this.heatmap.selectAll('.heatmap-selection').remove();
- }
- }
- drawCrosshair(position: number) {
- if (this.heatmap) {
- this.heatmap.selectAll('.heatmap-crosshair').remove();
- let posX = position;
- posX = Math.max(posX, this.yAxisWidth);
- posX = Math.min(posX, this.chartWidth + this.yAxisWidth);
- this.heatmap
- .append('g')
- .attr('class', 'heatmap-crosshair')
- .attr('transform', 'translate(' + posX + ',0)')
- .append('line')
- .attr('x1', 1)
- .attr('y1', this.chartTop)
- .attr('x2', 1)
- .attr('y2', this.chartBottom)
- .attr('stroke-width', 1);
- }
- }
- drawSharedCrosshair(pos: { x: any }) {
- if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) {
- const posX = this.xScale(pos.x) + this.yAxisWidth;
- this.drawCrosshair(posX);
- }
- }
- clearCrosshair() {
- if (this.heatmap) {
- this.heatmap.selectAll('.heatmap-crosshair').remove();
- }
- }
- render() {
- this.data = this.ctrl.data;
- this.panel = this.ctrl.panel;
- this.timeRange = this.ctrl.range;
- if (!this.setElementHeight() || !this.data) {
- return;
- }
- // Draw default axes and return if no data
- if (_.isEmpty(this.data.buckets)) {
- this.addHeatmapCanvas();
- this.addAxes();
- return;
- }
- 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;
- }
- }
|