grafana/public/app/plugins/panel/heatmap-old/rendering.ts

866 lines
27 KiB
TypeScript
Raw Normal View History

import * as d3 from 'd3';
import $ from 'jquery';
import { find, isEmpty, isNaN, isNil, isString, map, max, min, toNumber } from 'lodash';
import {
DateTime: adding support to select preferred timezone for presentation of date and time values. (#23586) * added moment timezone package. * added a qnd way of selecting timezone. * added a first draft to display how it can be used. * fixed failing tests. * made moment.local to be in utc when running tests. * added tests to verify that the timeZone support works as expected. * Fixed so we use the formatter in the graph context menu. * changed so we will format d3 according to timeZone. * changed from class base to function based for easier consumption. * fixed so tests got green. * renamed to make it shorter. * fixed formatting in logRow. * removed unused value. * added time formatter to flot. * fixed failing tests. * changed so history will use the formatting with support for timezone. * added todo. * added so we append the correct abbrivation behind time. * added time zone abbrevation in timepicker. * adding timezone in rangeutil tool. * will use timezone when formatting range. * changed so we use new functions to format date so timezone is respected. * wip - dashboard settings. * changed so the time picker settings is in react. * added force update. * wip to get the react graph to work. * fixed formatting and parsing on the timepicker. * updated snap to be correct. * fixed so we format values properly in time picker. * make sure we pass timezone on all the proper places. * fixed so we use correct timeZone in explore. * fixed failing tests. * fixed so we always parse from local to selected timezone. * removed unused variable. * reverted back. * trying to fix issue with directive. * fixed issue. * fixed strict null errors. * fixed so we still can select default. * make sure we reads the time zone from getTimezone
2020-04-27 08:28:06 -05:00
dateTimeFormat,
formattedValueToString,
getValueFormat,
LegacyGraphHoverClearEvent,
LegacyGraphHoverEvent,
PanelEvents,
toUtc,
} from '@grafana/data';
import { graphTimeFormat } from '@grafana/ui';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core';
import * as ticksUtils from 'app/core/utils/ticks';
import { getColorScale, getOpacityScale } from './color_scale';
import { mergeZeroBuckets } from './heatmap_data_converter';
import { HeatmapTooltip } from './heatmap_tooltip';
2017-03-24 05:35:19 -05:00
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 = 22.5,
X_AXIS_TICK_PADDING = 10,
Y_AXIS_TICK_PADDING = 5,
MIN_SELECTION_WIDTH = 2;
2017-03-24 05:35:19 -05:00
export default function rendering(scope: any, elem: any, attrs: any, ctrl: any) {
2018-08-13 07:24:15 -05:00
return new HeatmapRenderer(scope, elem, attrs, ctrl);
2018-08-13 03:57:32 -05:00
}
2018-08-13 07:24:15 -05:00
export class HeatmapRenderer {
width = 200;
height = 200;
2018-08-10 08:35:47 -05:00
yScale: any;
xScale: any;
chartWidth = 0;
chartHeight = 0;
chartTop = 0;
chartBottom = 0;
yAxisWidth = 0;
xAxisHeight = 0;
cardPadding = 0;
cardRound = 0;
cardWidth = 0;
cardHeight = 0;
2018-08-10 08:35:47 -05:00
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;
GraphNG - shared cursor (#33433) * Initial work * WIP add cursor in debug panel * shared cursor.sync filter * explicit uplot events * explicit uplot events * uplot events * uplot events * depend on master uplot * sync sync sync * Fix merge * Get rid of PlotSyncContext and sync tooltip positions * make sync optional * Improve shared tooltip positioning * Plugins: add level and signature badges to plugin details page (#33553) * feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons * feat(plugins): add PluginSignatureType type * feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin * feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type * Variables: Removes the never refresh option (#33533) * Variables: Removes the never refresh option * Tests: fixes DashboardModel repeat tests * Tests: fixs snapshots * Tests: fixes processVariable test * Tests: fixes DashboardModel tests * PageLayout: Fixes max-width breakpoint so that it triggers only when there is room for margin+ (#33558) * Alerting: Remove datasource (name) from migration (#33544) no longer needed as of https://github.com/grafana/grafana/pull/33416 for https://github.com/grafana/alerting-squad/issues/126 * Library panels: Adds description to library panels tab (#33428) * CodeOwners: Set owners of unified alerting migration (#33571) * ButtonSelect: updates component with the new theme model (#33565) * EmptySearchResult: updates component with the new theme model (#33573) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design (#33575) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design * Fixed font weight * Removed comment * Update * gitignore: Ignore files for accesscontrol provisioning (#33577) * Alerting/metrics (#33547) * moves alerting metrics to their own pkg * adds grafana_alerting_alerts (by state) metric * alerts_received_{total,invalid} * embed alertmanager alerting struct in ng metrics & remove duplicated notification metrics (already embed alertmanager notifier metrics) * use silence metrics from alertmanager lib * fix - manager has metrics * updates ngalert tests * comment lint Signed-off-by: Owen Diehl <ow.diehl@gmail.com> * cleaner prom registry code * removes ngalert global metrics * new registry use in all tests * ngalert metrics impl service, hack testinfra code to prevent duplicate metric registrations * nilmetrics unexported * Add note to Snapshot API doc to specify that user has to provide the entire dashboard model (#33572) * Added note as suggested by Macus E. * Update docs/sources/http_api/snapshot.md Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Alerting: backend "ng" code cleanup (#33578) * AlertMigration: remove alert_rule UID db check (#33568) do not believe this is needed due to uniqueness promised by shortid lib since there is no provisioning yet. https://github.com/teris-io/shortid * Live: persisting last message in cache for broadcast scope (#32938) * Alerting: Load annotations from rule into State cache (#33542) for https://github.com/grafana/alerting-squad/issues/127 * add template for dashboard url parameters (#33549) * Update dashboard-links.md parameters with plain text like `var-something=value` can make confusion. template it to clarify . * describe way for template link. * AlertingMigration: Create alert_rule_version entry (#33585) Create the alert rule version entry during the migration so it is consistent with rules created via api. for https://github.com/grafana/alerting-squad/issues/123 * Build: Fix with cleanup call maybe? (#33590) * add selector for code editor (#33554) * broadcast over eventBus * broadcasting to eventbus (but not useing it yet) * merge master * moved to context * fix yarn.lock * update snapshot * Fix direct state mutation * Persist location state on partial updates * GraphNG- use getStream rather than subscribe * Sync LegacyGraphHoverEvent with GraphNG * Chenge plotRef signature * use subscription * subscription * one fewer file * Update types * Remove unnecessary filtering * Disable cursor sync when in edit mode * GraphNG - bring back logging Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Kyle Brandt <kyle@grafana.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Uchechukwu Obasi <obasiuche62@gmail.com> Co-authored-by: gamab <gamab@users.noreply.github.com> Co-authored-by: Owen Diehl <ow.diehl@gmail.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Alexander Emelin <frvzmb@gmail.com> Co-authored-by: Nagle Zhang <nagle.zhang@sap.com> Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
2021-05-10 07:24:23 -05:00
hoverEvent: LegacyGraphHoverEvent;
constructor(private scope: any, private elem: any, attrs: any, private ctrl: any) {
2018-08-10 08:35:47 -05:00
// $heatmap is JQuery object, but heatmap is D3
2018-08-13 03:57:32 -05:00
this.$heatmap = this.elem.find('.heatmap-panel');
2018-08-10 08:35:47 -05:00
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(PanelEvents.render, this.onRender.bind(this));
2018-08-13 03:57:32 -05:00
this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
2018-08-22 07:29:09 -05:00
2018-08-10 08:35:47 -05:00
/////////////////////////////
// Selection and crosshair //
/////////////////////////////
// Shared crosshair and tooltip
this.ctrl.dashboard.events.on(LegacyGraphHoverEvent.type, this.onGraphHover.bind(this), this.scope);
this.ctrl.dashboard.events.on(LegacyGraphHoverClearEvent.type, this.onGraphHoverClear.bind(this), this.scope);
2018-08-10 08:35:47 -05:00
// 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));
GraphNG - shared cursor (#33433) * Initial work * WIP add cursor in debug panel * shared cursor.sync filter * explicit uplot events * explicit uplot events * uplot events * uplot events * depend on master uplot * sync sync sync * Fix merge * Get rid of PlotSyncContext and sync tooltip positions * make sync optional * Improve shared tooltip positioning * Plugins: add level and signature badges to plugin details page (#33553) * feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons * feat(plugins): add PluginSignatureType type * feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin * feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type * Variables: Removes the never refresh option (#33533) * Variables: Removes the never refresh option * Tests: fixes DashboardModel repeat tests * Tests: fixs snapshots * Tests: fixes processVariable test * Tests: fixes DashboardModel tests * PageLayout: Fixes max-width breakpoint so that it triggers only when there is room for margin+ (#33558) * Alerting: Remove datasource (name) from migration (#33544) no longer needed as of https://github.com/grafana/grafana/pull/33416 for https://github.com/grafana/alerting-squad/issues/126 * Library panels: Adds description to library panels tab (#33428) * CodeOwners: Set owners of unified alerting migration (#33571) * ButtonSelect: updates component with the new theme model (#33565) * EmptySearchResult: updates component with the new theme model (#33573) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design (#33575) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design * Fixed font weight * Removed comment * Update * gitignore: Ignore files for accesscontrol provisioning (#33577) * Alerting/metrics (#33547) * moves alerting metrics to their own pkg * adds grafana_alerting_alerts (by state) metric * alerts_received_{total,invalid} * embed alertmanager alerting struct in ng metrics & remove duplicated notification metrics (already embed alertmanager notifier metrics) * use silence metrics from alertmanager lib * fix - manager has metrics * updates ngalert tests * comment lint Signed-off-by: Owen Diehl <ow.diehl@gmail.com> * cleaner prom registry code * removes ngalert global metrics * new registry use in all tests * ngalert metrics impl service, hack testinfra code to prevent duplicate metric registrations * nilmetrics unexported * Add note to Snapshot API doc to specify that user has to provide the entire dashboard model (#33572) * Added note as suggested by Macus E. * Update docs/sources/http_api/snapshot.md Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Alerting: backend "ng" code cleanup (#33578) * AlertMigration: remove alert_rule UID db check (#33568) do not believe this is needed due to uniqueness promised by shortid lib since there is no provisioning yet. https://github.com/teris-io/shortid * Live: persisting last message in cache for broadcast scope (#32938) * Alerting: Load annotations from rule into State cache (#33542) for https://github.com/grafana/alerting-squad/issues/127 * add template for dashboard url parameters (#33549) * Update dashboard-links.md parameters with plain text like `var-something=value` can make confusion. template it to clarify . * describe way for template link. * AlertingMigration: Create alert_rule_version entry (#33585) Create the alert rule version entry during the migration so it is consistent with rules created via api. for https://github.com/grafana/alerting-squad/issues/123 * Build: Fix with cleanup call maybe? (#33590) * add selector for code editor (#33554) * broadcast over eventBus * broadcasting to eventbus (but not useing it yet) * merge master * moved to context * fix yarn.lock * update snapshot * Fix direct state mutation * Persist location state on partial updates * GraphNG- use getStream rather than subscribe * Sync LegacyGraphHoverEvent with GraphNG * Chenge plotRef signature * use subscription * subscription * one fewer file * Update types * Remove unnecessary filtering * Disable cursor sync when in edit mode * GraphNG - bring back logging Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Kyle Brandt <kyle@grafana.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Uchechukwu Obasi <obasiuche62@gmail.com> Co-authored-by: gamab <gamab@users.noreply.github.com> Co-authored-by: Owen Diehl <ow.diehl@gmail.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Alexander Emelin <frvzmb@gmail.com> Co-authored-by: Nagle Zhang <nagle.zhang@sap.com> Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
2021-05-10 07:24:23 -05:00
this.hoverEvent = new LegacyGraphHoverEvent({ pos: {}, point: {}, panel: this.panel });
2018-08-10 08:35:47 -05:00
}
onGraphHoverClear() {
this.clearCrosshair();
}
onGraphHover(event: { pos: any }) {
2018-08-10 08:35:47 -05:00
this.drawSharedCrosshair(event.pos);
}
onRender() {
this.render();
this.ctrl.renderingCompleted();
}
setElementHeight() {
2017-03-24 05:35:19 -05:00
try {
let height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
if (isString(height)) {
height = parseInt(height.replace('px', ''), 10);
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
this.$heatmap.css('height', height + 'px');
2017-03-24 05:35:19 -05:00
return true;
} catch (e) {
// IE throws errors sometimes
2017-03-24 05:35:19 -05:00
return false;
}
}
getYAxisWidth(elem: any) {
const panelYAxisWidth = this.getPanelYAxisWidth();
if (panelYAxisWidth !== null) {
return panelYAxisWidth + Y_AXIS_TICK_PADDING;
}
const axisText = elem.selectAll('.axis-y text').nodes();
const maxTextWidth = max(
map(axisText, (text) => {
// Use SVG getBBox method
return text.getBBox().width;
})
);
2017-03-24 05:35:19 -05:00
return maxTextWidth;
2017-03-24 05:35:19 -05:00
}
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;
2017-03-24 05:35:19 -05:00
} else {
// Default height
return 30;
}
}
2018-08-10 08:35:47 -05:00
addXAxis() {
this.scope.xScale = this.xScale = d3
.scaleTime()
2018-08-10 08:35:47 -05:00
.domain([this.timeRange.from, this.timeRange.to])
.range([0, this.chartWidth]);
2017-03-24 05:35:19 -05:00
const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
const format = graphTimeFormat(ticks, this.timeRange.from.valueOf(), this.timeRange.to.valueOf());
DateTime: adding support to select preferred timezone for presentation of date and time values. (#23586) * added moment timezone package. * added a qnd way of selecting timezone. * added a first draft to display how it can be used. * fixed failing tests. * made moment.local to be in utc when running tests. * added tests to verify that the timeZone support works as expected. * Fixed so we use the formatter in the graph context menu. * changed so we will format d3 according to timeZone. * changed from class base to function based for easier consumption. * fixed so tests got green. * renamed to make it shorter. * fixed formatting in logRow. * removed unused value. * added time formatter to flot. * fixed failing tests. * changed so history will use the formatting with support for timezone. * added todo. * added so we append the correct abbrivation behind time. * added time zone abbrevation in timepicker. * adding timezone in rangeutil tool. * will use timezone when formatting range. * changed so we use new functions to format date so timezone is respected. * wip - dashboard settings. * changed so the time picker settings is in react. * added force update. * wip to get the react graph to work. * fixed formatting and parsing on the timepicker. * updated snap to be correct. * fixed so we format values properly in time picker. * make sure we pass timezone on all the proper places. * fixed so we use correct timeZone in explore. * fixed failing tests. * fixed so we always parse from local to selected timezone. * removed unused variable. * reverted back. * trying to fix issue with directive. * fixed issue. * fixed strict null errors. * fixed so we still can select default. * make sure we reads the time zone from getTimezone
2020-04-27 08:28:06 -05:00
const timeZone = this.ctrl.dashboard.getTimezone();
const formatter = (date: d3.AxisDomain) =>
dateTimeFormat(date.valueOf(), {
format: format,
timeZone: timeZone,
});
2018-08-14 02:15:14 -05:00
const xAxis = d3
2018-08-10 08:35:47 -05:00
.axisBottom(this.xScale)
2017-03-24 05:35:19 -05:00
.ticks(ticks)
DateTime: adding support to select preferred timezone for presentation of date and time values. (#23586) * added moment timezone package. * added a qnd way of selecting timezone. * added a first draft to display how it can be used. * fixed failing tests. * made moment.local to be in utc when running tests. * added tests to verify that the timeZone support works as expected. * Fixed so we use the formatter in the graph context menu. * changed so we will format d3 according to timeZone. * changed from class base to function based for easier consumption. * fixed so tests got green. * renamed to make it shorter. * fixed formatting in logRow. * removed unused value. * added time formatter to flot. * fixed failing tests. * changed so history will use the formatting with support for timezone. * added todo. * added so we append the correct abbrivation behind time. * added time zone abbrevation in timepicker. * adding timezone in rangeutil tool. * will use timezone when formatting range. * changed so we use new functions to format date so timezone is respected. * wip - dashboard settings. * changed so the time picker settings is in react. * added force update. * wip to get the react graph to work. * fixed formatting and parsing on the timepicker. * updated snap to be correct. * fixed so we format values properly in time picker. * make sure we pass timezone on all the proper places. * fixed so we use correct timeZone in explore. * fixed failing tests. * fixed so we always parse from local to selected timezone. * removed unused variable. * reverted back. * trying to fix issue with directive. * fixed issue. * fixed strict null errors. * fixed so we still can select default. * make sure we reads the time zone from getTimezone
2020-04-27 08:28:06 -05:00
.tickFormat(formatter)
2017-03-24 05:35:19 -05:00
.tickPadding(X_AXIS_TICK_PADDING)
2018-08-10 08:35:47 -05:00
.tickSize(this.chartHeight);
2017-03-24 05:35:19 -05:00
const posY = this.margin.top;
const posX = this.yAxisWidth;
2018-08-10 08:35:47 -05:00
this.heatmap
.append('g')
.attr('class', 'axis axis-x')
.attr('transform', 'translate(' + posX + ',' + posY + ')')
2017-03-24 05:35:19 -05:00
.call(xAxis);
// Remove horizontal line in the top of axis labels (called domain in d3)
this.heatmap.select('.axis-x').select('.domain').remove();
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
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);
2017-03-24 05:35:19 -05:00
// 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;
2017-03-24 05:35:19 -05:00
// Adjust ticks after Y range widening
tickInterval = ticksUtils.tickStep(yMin, yMax, ticks);
ticks = Math.ceil((yMax - yMin) / tickInterval);
2017-03-24 05:35:19 -05:00
const decimalsAuto = ticksUtils.getPrecision(tickInterval);
2018-08-10 08:35:47 -05:00
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);
2018-08-10 08:35:47 -05:00
this.ctrl.decimals = decimals;
this.ctrl.scaledDecimals = scaledDecimals;
2017-03-24 05:35:19 -05:00
// Set default Y min and max if no data
if (isEmpty(this.data.buckets)) {
yMax = 1;
yMin = -1;
2017-03-24 05:35:19 -05:00
ticks = 3;
decimals = 1;
}
2018-08-10 08:35:47 -05:00
this.data.yAxis = {
min: yMin,
max: yMax,
ticks: ticks,
2017-03-24 05:35:19 -05:00
};
this.scope.yScale = this.yScale = d3.scaleLinear().domain([yMin, yMax]).range([this.chartHeight, 0]);
2017-03-24 05:35:19 -05:00
const yAxis = d3
2018-08-10 08:35:47 -05:00
.axisLeft(this.yScale)
2017-03-24 05:35:19 -05:00
.ticks(ticks)
2018-08-10 08:35:47 -05:00
.tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
.tickSizeInner(0 - this.width)
2017-03-24 05:35:19 -05:00
.tickSizeOuter(0)
.tickPadding(Y_AXIS_TICK_PADDING);
this.heatmap.append('g').attr('class', 'axis axis-y').call(yAxis);
2017-03-24 05:35:19 -05:00
// 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;
2018-08-10 08:35:47 -05:00
this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
2017-03-24 05:35:19 -05:00
// Remove vertical line in the right of axis labels (called domain in d3)
this.heatmap.select('.axis-y').select('.domain').remove();
2017-03-24 05:35:19 -05:00
}
// 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;
2017-03-24 05:35:19 -05:00
if (tickInterval === 0) {
yMax = max * this.dataRangeWidingFactor;
yMin = min - min * (this.dataRangeWidingFactor - 1);
2017-03-24 05:35:19 -05:00
} else {
yMax = Math.ceil((max + yWiding) / tickInterval) * tickInterval;
yMin = Math.floor((min - yWiding) / tickInterval) * tickInterval;
2017-03-24 05:35:19 -05:00
}
// Don't wide axis below 0 if all values are positive
if (min >= 0 && yMin < 0) {
yMin = 0;
2017-03-24 05:35:19 -05:00
}
return { yMin, yMax };
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
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;
2017-03-24 05:35:19 -05:00
// Set default Y min and max if no data
if (isEmpty(this.data.buckets)) {
yMax = Math.pow(logBase, 2);
yMin = 1;
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
this.scope.yScale = this.yScale = d3
.scaleLog()
2018-08-10 08:35:47 -05:00
.base(this.panel.yAxis.logBase)
.domain([yMin, yMax])
2018-08-10 08:35:47 -05:00
.range([this.chartHeight, 0]);
2017-03-24 05:35:19 -05:00
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);
2018-08-10 08:35:47 -05:00
this.ctrl.decimals = decimals;
this.ctrl.scaledDecimals = scaledDecimals;
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
this.data.yAxis = {
min: yMin,
max: yMax,
ticks: tickValues.length,
2017-03-24 05:35:19 -05:00
};
const yAxis = d3
2018-08-10 08:35:47 -05:00
.axisLeft(this.yScale)
.tickValues(tickValues)
2018-08-10 08:35:47 -05:00
.tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
.tickSizeInner(0 - this.width)
2017-03-24 05:35:19 -05:00
.tickSizeOuter(0)
.tickPadding(Y_AXIS_TICK_PADDING);
this.heatmap.append('g').attr('class', 'axis axis-y').call(yAxis);
2017-03-24 05:35:19 -05:00
// 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;
2018-08-10 08:35:47 -05:00
this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
2017-03-24 05:35:19 -05:00
// Set first tick as pseudo 0
if (yMin < 1) {
this.heatmap.select('.axis-y').select('.tick text').text('0');
2017-03-24 05:35:19 -05:00
}
// Remove vertical line in the right of axis labels (called domain in d3)
this.heatmap.select('.axis-y').select('.domain').remove();
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
addYAxisFromBuckets() {
const tsBuckets = this.data.tsBuckets;
let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX);
2018-08-10 08:35:47 -05:00
this.scope.yScale = this.yScale = d3
.scaleLinear()
.domain([0, tsBuckets.length - 1])
2018-08-10 08:35:47 -05:00
.range([this.chartHeight, 0]);
const tickValues = map(tsBuckets, (b, i) => i);
const decimalsAuto = max(map(tsBuckets, ticksUtils.getStringPrecision));
2018-08-10 08:35:47 -05:00
const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
this.ctrl.decimals = decimals;
const tickValueFormatter = this.tickValueFormatter.bind(this);
function tickFormatter(yAxisWidth: number | null) {
return function (valIndex: d3.AxisDomain) {
let valueFormatted = tsBuckets[valIndex.valueOf()];
if (!isNaN(toNumber(valueFormatted)) && valueFormatted !== '') {
// Try to format numeric tick labels
valueFormatted = tickValueFormatter(decimals)(toNumber(valueFormatted));
} else if (valueFormatted && typeof valueFormatted === 'string' && valueFormatted !== '') {
if (yAxisWidth) {
const scale = 0.15; // how to have a better calculation for this
const trimmed = valueFormatted.substring(0, Math.floor(yAxisWidth * scale));
const postfix = trimmed.length < valueFormatted.length ? '...' : '';
valueFormatted = `${trimmed}${postfix}`;
}
}
return valueFormatted;
};
}
const tsBucketsFormatted = map(tsBuckets, (v, i) => tickFormatter(null)(i));
2018-08-10 08:35:47 -05:00
this.data.tsBucketsFormatted = tsBucketsFormatted;
2018-03-07 07:33:33 -06:00
const yAxis = d3
2018-08-10 08:35:47 -05:00
.axisLeft(this.yScale)
.tickFormat(tickFormatter(this.getPanelYAxisWidth()))
2018-08-10 08:35:47 -05:00
.tickSizeInner(0 - this.width)
.tickSizeOuter(0)
.tickPadding(Y_AXIS_TICK_PADDING);
if (tickValues && tickValues.length <= ticks) {
yAxis.tickValues(tickValues);
} else {
yAxis.ticks(ticks);
}
this.heatmap.append('g').attr('class', 'axis axis-y').call(yAxis);
// Calculate Y axis width first, then move axis into visible area
2018-08-10 08:35:47 -05:00
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();
}
2017-03-24 05:35:19 -05:00
// Adjust data range to log base
adjustLogRange(min: number, max: number, logBase: number) {
2019-01-05 08:19:54 -06:00
let yMin = this.data.heatmapStats.minLog;
2018-08-10 08:35:47 -05:00
if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
yMin = 1;
2017-03-24 05:35:19 -05:00
} else {
yMin = this.adjustLogMin(this.data.heatmapStats.minLog, logBase);
2017-03-24 05:35:19 -05:00
}
// Adjust max Y value to log base
2019-01-05 08:19:54 -06:00
const yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase);
2017-03-24 05:35:19 -05:00
return { yMin, yMax };
2017-03-24 05:35:19 -05:00
}
adjustLogMax(max: number, base: number) {
2018-03-07 07:33:33 -06:00
return Math.pow(base, Math.ceil(ticksUtils.logp(max, base)));
2017-03-24 05:35:19 -05:00
}
adjustLogMin(min: number, base: number) {
2018-03-07 07:33:33 -06:00
return Math.pow(base, Math.floor(ticksUtils.logp(min, base)));
2017-03-24 05:35:19 -05:00
}
logScaleTickValues(domain: any[], base: number) {
const domainMin = domain[0];
const domainMax = domain[1];
const tickValues = [];
2017-03-24 05:35:19 -05:00
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);
2017-03-24 05:35:19 -05:00
}
}
const ticks = Math.ceil(ticksUtils.logp(domainMax, base));
2017-03-24 05:35:19 -05:00
for (let i = 0; i <= ticks; i++) {
const tickValue = Math.pow(base, i);
tickValues.push(tickValue);
2017-03-24 05:35:19 -05:00
}
return tickValues;
}
tickValueFormatter(decimals: number, scaledDecimals: any = null) {
const format = this.panel.yAxis.format;
return (value: any) => {
try {
if (format !== 'none') {
const v = getValueFormat(format)(value, decimals, scaledDecimals);
return formattedValueToString(v);
}
} catch (err) {
console.error(err instanceof Error ? err.message : err);
}
return value;
2017-03-24 05:35:19 -05:00
};
}
2018-08-10 08:35:47 -05:00
fixYAxisTickSize() {
this.heatmap.select('.axis-y').selectAll('.tick line').attr('x2', this.chartWidth);
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
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();
2017-03-24 05:35:19 -05:00
} else {
2018-08-10 08:35:47 -05:00
if (this.panel.yAxis.logBase === 1) {
this.addYAxis();
} else {
2018-08-10 08:35:47 -05:00
this.addLogYAxis();
}
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
this.chartWidth = this.width - this.yAxisWidth - this.margin.right;
this.fixYAxisTickSize();
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
this.addXAxis();
this.xAxisHeight = this.getXAxisHeight(this.heatmap);
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
if (!this.panel.yAxis.show) {
this.heatmap.select('.axis-y').selectAll('line').style('opacity', 0);
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
if (!this.panel.xAxis.show) {
this.heatmap.select('.axis-x').selectAll('line').style('opacity', 0);
2017-03-24 05:35:19 -05:00
}
}
2018-08-10 08:35:47 -05:00
addHeatmapCanvas() {
const heatmapElem = this.$heatmap[0];
2017-03-28 09:46:10 -05:00
2018-08-10 08:35:47 -05:00
this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom;
2017-03-28 09:46:10 -05:00
2018-08-10 08:35:47 -05:00
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;
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
if (this.heatmap) {
this.heatmap.remove();
2017-03-24 05:35:19 -05:00
}
this.heatmap = d3.select(heatmapElem).append('svg').attr('width', this.width).attr('height', this.height);
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
addHeatmap() {
this.addHeatmapCanvas();
this.addAxes();
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
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)!);
2017-03-24 05:35:19 -05:00
}
const cardsData = this.data.cards;
2019-03-12 12:30:24 -05:00
const cardStats = this.data.cardStats;
const maxValueAuto = cardStats.max;
const minValueAuto = Math.max(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, {
2018-08-10 08:35:47 -05:00
value: this.panel.color.colorScheme,
});
2018-08-10 08:35:47 -05:00
this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
2019-03-12 12:30:24 -05:00
this.opacityScale = getOpacityScale(this.panel.color, maxValue, minValue);
2018-08-10 08:35:47 -05:00
this.setCardSize();
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
cards.append('title');
cards = cards
.enter()
.append('rect')
2018-08-13 03:57:32 -05:00
.attr('x', this.getCardX.bind(this))
.attr('width', this.getCardWidth.bind(this))
.attr('y', this.getCardY.bind(this))
.attr('height', this.getCardHeight.bind(this))
2018-08-10 08:35:47 -05:00
.attr('rx', this.cardRound)
.attr('ry', this.cardRound)
.attr('class', 'bordered heatmap-card')
2018-08-13 03:57:32 -05:00
.style('fill', this.getCardColor.bind(this))
.style('stroke', this.getCardColor.bind(this))
.style('stroke-width', 0)
2018-08-13 03:57:32 -05:00
.style('opacity', this.getCardOpacity.bind(this));
const $cards = this.$heatmap.find('.heatmap-card');
$cards
.on('mouseenter', (event: any) => {
2018-08-10 08:35:47 -05:00
this.tooltip.mouseOverBucket = true;
this.highlightCard(event);
})
.on('mouseleave', (event: any) => {
2018-08-10 08:35:47 -05:00
this.tooltip.mouseOverBucket = false;
this.resetCardHighLight(event);
});
2017-03-24 05:35:19 -05:00
}
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);
2018-08-10 08:35:47 -05:00
this.tooltip.originalFillColor = color;
currentCard
.style('fill', highlightColor.toString())
.style('stroke', strokeColor.toString())
.style('stroke-width', 1);
2017-03-24 05:35:19 -05:00
}
resetCardHighLight(event: any) {
d3.select(event.target)
2018-08-10 08:35:47 -05:00
.style('fill', this.tooltip.originalFillColor)
.style('stroke', this.tooltip.originalFillColor)
.style('stroke-width', 0);
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
setCardSize() {
const xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0));
2018-08-10 08:35:47 -05:00
let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize));
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
if (this.panel.yAxis.logBase !== 1) {
const base = this.panel.yAxis.logBase;
const splitFactor = this.data.yBucketSize || 1;
2018-08-10 08:35:47 -05:00
yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor);
2017-03-24 05:35:19 -05:00
}
const cardWidth = xGridSize - this.cardPadding * 2;
this.cardWidth = Math.max(cardWidth, MIN_CARD_SIZE);
2018-08-10 08:35:47 -05:00
this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0;
2017-03-24 05:35:19 -05:00
}
getCardX(d: { x: any }) {
2017-03-24 05:35:19 -05:00
let x;
2018-08-10 08:35:47 -05:00
if (this.xScale(d.x) < 0) {
2017-03-24 05:35:19 -05:00
// Cut card left to prevent overlay
2018-08-10 08:35:47 -05:00
x = this.yAxisWidth + this.cardPadding;
2017-03-24 05:35:19 -05:00
} else {
2018-08-10 08:35:47 -05:00
x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding;
2017-03-24 05:35:19 -05:00
}
return x;
}
getCardWidth(d: { x: any }) {
let w = this.cardWidth;
2018-08-10 08:35:47 -05:00
if (this.xScale(d.x) < 0) {
2017-03-24 05:35:19 -05:00
// Cut card left to prevent overlay
w = this.xScale(d.x) + this.cardWidth;
2018-08-10 08:35:47 -05:00
} else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) {
2017-03-24 05:35:19 -05:00
// Cut card right to prevent overlay
2018-08-10 08:35:47 -05:00
w = this.chartWidth - this.xScale(d.x) - this.cardPadding;
2017-03-24 05:35:19 -05:00
}
2019-03-11 04:42:16 -05:00
// 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;
2017-03-24 05:35:19 -05:00
return w;
}
getCardY(d: { y: number }) {
2018-08-10 08:35:47 -05:00
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;
2017-03-24 05:35:19 -05:00
} else {
2018-08-10 08:35:47 -05:00
if (y < this.chartTop) {
y = this.chartTop;
2017-03-24 05:35:19 -05:00
}
}
return y;
}
getCardHeight(d: { y: number }) {
const y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
2018-08-10 08:35:47 -05:00
let h = this.cardHeight;
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
return this.cardHeight;
2017-03-24 05:35:19 -05:00
}
// Cut card height to prevent overlay
2018-08-10 08:35:47 -05:00
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;
2017-03-24 05:35:19 -05:00
}
// Height can't be more than chart height
2018-08-10 08:35:47 -05:00
h = Math.min(h, this.chartHeight);
2017-03-24 05:35:19 -05:00
// Card height should be MIN_CARD_SIZE at least
h = Math.max(h, MIN_CARD_SIZE);
return h;
}
getCardColor(d: { count: any }) {
2018-08-10 08:35:47 -05:00
if (this.panel.color.mode === 'opacity') {
return config.theme2.visualization.getColorByName(this.panel.color.cardColor);
2017-03-24 05:35:19 -05:00
} else {
2018-08-10 08:35:47 -05:00
return this.colorScale(d.count);
2017-03-24 05:35:19 -05:00
}
}
getCardOpacity(d: { count: any }) {
2018-08-10 08:35:47 -05:00
if (this.panel.color.mode === 'opacity') {
return this.opacityScale(d.count);
2017-03-24 05:35:19 -05:00
} else {
return 1;
}
}
getEventOffset(event: any) {
2018-08-22 07:29:09 -05:00
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) {
2018-08-22 07:29:09 -05:00
const offset = this.getEventOffset(event);
2018-08-10 08:35:47 -05:00
this.selection.active = true;
2018-08-22 07:29:09 -05:00
this.selection.x1 = offset.x;
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
this.mouseUpHandler = () => {
this.onMouseUp();
2017-03-24 05:35:19 -05:00
};
2018-08-13 03:57:32 -05:00
$(document).one('mouseup', this.mouseUpHandler.bind(this));
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
onMouseUp() {
2018-08-13 03:57:32 -05:00
$(document).unbind('mouseup', this.mouseUpHandler.bind(this));
2018-08-10 08:35:47 -05:00
this.mouseUpHandler = null;
this.selection.active = false;
2017-03-24 05:35:19 -05:00
const selectionRange = Math.abs(this.selection.x2 - this.selection.x1);
2018-08-10 08:35:47 -05:00
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);
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
this.ctrl.timeSrv.setTime({
from: toUtc(timeFrom),
to: toUtc(timeTo),
2017-03-24 05:35:19 -05:00
});
}
2018-08-10 08:35:47 -05:00
this.clearSelection();
2017-03-24 05:35:19 -05:00
}
2018-08-10 08:35:47 -05:00
onMouseLeave() {
this.ctrl.dashboard.events.publish(new LegacyGraphHoverClearEvent());
2018-08-10 08:35:47 -05:00
this.clearCrosshair();
2017-03-24 05:35:19 -05:00
}
onMouseMove(event: any) {
2018-08-10 08:35:47 -05:00
if (!this.heatmap) {
return;
}
2017-03-24 05:35:19 -05:00
2018-08-22 07:29:09 -05:00
const offset = this.getEventOffset(event);
2018-08-10 08:35:47 -05:00
if (this.selection.active) {
2017-03-24 05:35:19 -05:00
// Clear crosshair and tooltip
2018-08-10 08:35:47 -05:00
this.clearCrosshair();
this.tooltip.destroy();
2017-03-24 05:35:19 -05:00
2018-08-22 07:29:09 -05:00
this.selection.x2 = this.limitSelection(offset.x);
2018-08-10 08:35:47 -05:00
this.drawSelection(this.selection.x1, this.selection.x2);
2017-03-24 05:35:19 -05:00
} else {
2018-08-22 07:29:09 -05:00
const pos = this.getEventPos(event, offset);
this.drawCrosshair(offset.x);
this.tooltip.show(pos, this.data);
this.emitGraphHoverEvent(pos);
2017-03-24 05:35:19 -05:00
}
}
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 = {
2017-03-24 05:35:19 -05:00
pageX: event.pageX,
pageY: event.pageY,
x: x,
x1: x,
y: y,
y1: y,
panelRelY: null,
2018-08-22 07:29:09 -05:00
offset,
2017-03-24 05:35:19 -05:00
};
2018-08-22 07:29:09 -05:00
return pos;
}
2017-03-24 05:35:19 -05:00
emitGraphHoverEvent(pos: { panelRelY: number; offset: { y: number } }) {
2018-08-22 07:29:09 -05:00
// Set minimum offset to prevent showing legend from another panel
pos.panelRelY = Math.max(pos.offset.y / this.height, 0.001);
2017-03-24 05:35:19 -05:00
// broadcast to other graph panels that we are hovering
GraphNG - shared cursor (#33433) * Initial work * WIP add cursor in debug panel * shared cursor.sync filter * explicit uplot events * explicit uplot events * uplot events * uplot events * depend on master uplot * sync sync sync * Fix merge * Get rid of PlotSyncContext and sync tooltip positions * make sync optional * Improve shared tooltip positioning * Plugins: add level and signature badges to plugin details page (#33553) * feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons * feat(plugins): add PluginSignatureType type * feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin * feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type * Variables: Removes the never refresh option (#33533) * Variables: Removes the never refresh option * Tests: fixes DashboardModel repeat tests * Tests: fixs snapshots * Tests: fixes processVariable test * Tests: fixes DashboardModel tests * PageLayout: Fixes max-width breakpoint so that it triggers only when there is room for margin+ (#33558) * Alerting: Remove datasource (name) from migration (#33544) no longer needed as of https://github.com/grafana/grafana/pull/33416 for https://github.com/grafana/alerting-squad/issues/126 * Library panels: Adds description to library panels tab (#33428) * CodeOwners: Set owners of unified alerting migration (#33571) * ButtonSelect: updates component with the new theme model (#33565) * EmptySearchResult: updates component with the new theme model (#33573) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design (#33575) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design * Fixed font weight * Removed comment * Update * gitignore: Ignore files for accesscontrol provisioning (#33577) * Alerting/metrics (#33547) * moves alerting metrics to their own pkg * adds grafana_alerting_alerts (by state) metric * alerts_received_{total,invalid} * embed alertmanager alerting struct in ng metrics & remove duplicated notification metrics (already embed alertmanager notifier metrics) * use silence metrics from alertmanager lib * fix - manager has metrics * updates ngalert tests * comment lint Signed-off-by: Owen Diehl <ow.diehl@gmail.com> * cleaner prom registry code * removes ngalert global metrics * new registry use in all tests * ngalert metrics impl service, hack testinfra code to prevent duplicate metric registrations * nilmetrics unexported * Add note to Snapshot API doc to specify that user has to provide the entire dashboard model (#33572) * Added note as suggested by Macus E. * Update docs/sources/http_api/snapshot.md Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Alerting: backend "ng" code cleanup (#33578) * AlertMigration: remove alert_rule UID db check (#33568) do not believe this is needed due to uniqueness promised by shortid lib since there is no provisioning yet. https://github.com/teris-io/shortid * Live: persisting last message in cache for broadcast scope (#32938) * Alerting: Load annotations from rule into State cache (#33542) for https://github.com/grafana/alerting-squad/issues/127 * add template for dashboard url parameters (#33549) * Update dashboard-links.md parameters with plain text like `var-something=value` can make confusion. template it to clarify . * describe way for template link. * AlertingMigration: Create alert_rule_version entry (#33585) Create the alert rule version entry during the migration so it is consistent with rules created via api. for https://github.com/grafana/alerting-squad/issues/123 * Build: Fix with cleanup call maybe? (#33590) * add selector for code editor (#33554) * broadcast over eventBus * broadcasting to eventbus (but not useing it yet) * merge master * moved to context * fix yarn.lock * update snapshot * Fix direct state mutation * Persist location state on partial updates * GraphNG- use getStream rather than subscribe * Sync LegacyGraphHoverEvent with GraphNG * Chenge plotRef signature * use subscription * subscription * one fewer file * Update types * Remove unnecessary filtering * Disable cursor sync when in edit mode * GraphNG - bring back logging Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Kyle Brandt <kyle@grafana.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Uchechukwu Obasi <obasiuche62@gmail.com> Co-authored-by: gamab <gamab@users.noreply.github.com> Co-authored-by: Owen Diehl <ow.diehl@gmail.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Alexander Emelin <frvzmb@gmail.com> Co-authored-by: Nagle Zhang <nagle.zhang@sap.com> Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
2021-05-10 07:24:23 -05:00
this.hoverEvent.payload.pos = pos;
this.hoverEvent.payload.panel = this.panel;
this.hoverEvent.payload.point['time'] = (pos as any).x;
this.ctrl.dashboard.events.publish(this.hoverEvent);
2017-03-24 05:35:19 -05:00
}
limitSelection(x2: number) {
2018-08-10 08:35:47 -05:00
x2 = Math.max(x2, this.yAxisWidth);
x2 = Math.min(x2, this.chartWidth + this.yAxisWidth);
2017-03-24 05:35:19 -05:00
return x2;
}
drawSelection(posX1: number, posX2: number) {
2018-08-10 08:35:47 -05:00
if (this.heatmap) {
this.heatmap.selectAll('.heatmap-selection').remove();
const selectionX = Math.min(posX1, posX2);
const selectionWidth = Math.abs(posX1 - posX2);
2017-03-24 05:35:19 -05:00
if (selectionWidth > MIN_SELECTION_WIDTH) {
2018-08-10 08:35:47 -05:00
this.heatmap
.append('rect')
.attr('class', 'heatmap-selection')
.attr('x', selectionX)
.attr('width', selectionWidth)
2018-08-10 08:35:47 -05:00
.attr('y', this.chartTop)
.attr('height', this.chartHeight);
2017-03-24 05:35:19 -05:00
}
}
}
2018-08-10 08:35:47 -05:00
clearSelection() {
this.selection.x1 = -1;
this.selection.x2 = -1;
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
if (this.heatmap) {
this.heatmap.selectAll('.heatmap-selection').remove();
2017-03-24 05:35:19 -05:00
}
}
drawCrosshair(position: number) {
2018-08-10 08:35:47 -05:00
if (this.heatmap) {
this.heatmap.selectAll('.heatmap-crosshair').remove();
2017-03-24 05:35:19 -05:00
let posX = position;
2018-08-10 08:35:47 -05:00
posX = Math.max(posX, this.yAxisWidth);
posX = Math.min(posX, this.chartWidth + this.yAxisWidth);
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
this.heatmap
.append('g')
.attr('class', 'heatmap-crosshair')
.attr('transform', 'translate(' + posX + ',0)')
.append('line')
.attr('x1', 1)
2018-08-10 08:35:47 -05:00
.attr('y1', this.chartTop)
.attr('x2', 1)
2018-08-10 08:35:47 -05:00
.attr('y2', this.chartBottom)
.attr('stroke-width', 1);
2017-03-24 05:35:19 -05:00
}
}
drawSharedCrosshair(pos: { x: any }) {
2018-08-10 08:35:47 -05:00
if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) {
const posX = this.xScale(pos.x) + this.yAxisWidth;
2018-08-10 08:35:47 -05:00
this.drawCrosshair(posX);
2017-03-24 05:35:19 -05:00
}
}
2018-08-10 08:35:47 -05:00
clearCrosshair() {
if (this.heatmap) {
this.heatmap.selectAll('.heatmap-crosshair').remove();
2017-03-24 05:35:19 -05:00
}
}
2018-08-10 08:35:47 -05:00
render() {
this.data = this.ctrl.data;
this.panel = this.ctrl.panel;
this.timeRange = this.ctrl.range;
2017-03-24 05:35:19 -05:00
2018-08-10 08:35:47 -05:00
if (!this.setElementHeight() || !this.data) {
return;
2017-03-24 05:35:19 -05:00
}
// Draw default axes and return if no data
if (isEmpty(this.data.buckets)) {
2018-08-10 08:35:47 -05:00
this.addHeatmapCanvas();
this.addAxes();
return;
}
2018-08-10 08:35:47 -05:00
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;
2017-03-24 05:35:19 -05:00
}
private getPanelYAxisWidth(): number | null {
if (!this.panel.yAxis.width) {
return null;
}
return isNaN(this.panel.yAxis.width) ? null : parseInt(this.panel.yAxis.width, 10);
}
2017-03-24 05:35:19 -05:00
}