Heatmap: Remove legacy angular based implementation (#59249)

remove angular heatmap
This commit is contained in:
Ryan McKinley 2022-11-23 10:46:21 -08:00 committed by GitHub
parent 183b279274
commit 0a7a5b13fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2 additions and 3299 deletions

View File

@ -7453,188 +7453,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/panel/heatmap-old/axes_editor.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/plugins/panel/heatmap-old/color_legend.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Do not use any type assertions.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Do not use any type assertions.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Do not use any type assertions.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"]
],
"public/app/plugins/panel/heatmap-old/color_scale.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/panel/heatmap-old/display_editor.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/panel/heatmap-old/heatmap_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"]
],
"public/app/plugins/panel/heatmap-old/heatmap_data_converter.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"]
],
"public/app/plugins/panel/heatmap-old/heatmap_tooltip.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
],
"public/app/plugins/panel/heatmap-old/rendering.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
[0, 0, 0, "Unexpected any. Specify a different type.", "31"],
[0, 0, 0, "Unexpected any. Specify a different type.", "32"],
[0, 0, 0, "Unexpected any. Specify a different type.", "33"],
[0, 0, 0, "Unexpected any. Specify a different type.", "34"],
[0, 0, 0, "Unexpected any. Specify a different type.", "35"],
[0, 0, 0, "Unexpected any. Specify a different type.", "36"],
[0, 0, 0, "Unexpected any. Specify a different type.", "37"],
[0, 0, 0, "Unexpected any. Specify a different type.", "38"],
[0, 0, 0, "Unexpected any. Specify a different type.", "39"],
[0, 0, 0, "Unexpected any. Specify a different type.", "40"],
[0, 0, 0, "Unexpected any. Specify a different type.", "41"],
[0, 0, 0, "Unexpected any. Specify a different type.", "42"],
[0, 0, 0, "Unexpected any. Specify a different type.", "43"],
[0, 0, 0, "Do not use any type assertions.", "44"],
[0, 0, 0, "Unexpected any. Specify a different type.", "45"],
[0, 0, 0, "Unexpected any. Specify a different type.", "46"]
],
"public/app/plugins/panel/heatmap-old/specs/heatmap_ctrl.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/panel/heatmap-old/specs/heatmap_data_converter.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/panel/heatmap-old/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/plugins/panel/heatmap/HeatmapHoverView.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -59,7 +59,6 @@ export interface FeatureToggles {
prometheusWideSeries?: boolean;
canvasPanelNesting?: boolean;
scenes?: boolean;
useLegacyHeatmapPanel?: boolean;
disableSecretsCompatibility?: boolean;
logRequestsInstrumentedAsUnknown?: boolean;
dataConnectionsConsole?: boolean;

View File

@ -164,7 +164,6 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *store.Serv
"gettingstarted": {},
"graph": {},
"heatmap": {},
"heatmap-old": {},
"histogram": {},
"icon": {},
"live": {},

View File

@ -16,7 +16,7 @@ seqs: [
// grafana.com, then the plugin id has to follow the naming
// conventions.
id: string & strings.MinRunes(1)
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|gauge|geomap|gettingstarted|graph|heatmap|heatmap-old|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|testdata|zipkin|phlare|parca)$"
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|testdata|zipkin|phlare|parca)$"
// The set of all plugin types. This hidden field exists solely
// so that the set can be string-interpolated into other fields.

View File

@ -248,11 +248,6 @@ var (
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "useLegacyHeatmapPanel",
Description: "Continue to use the angular/flot based heatmap panel",
State: FeatureStateStable,
},
{
Name: "disableSecretsCompatibility",
Description: "Disable duplicated secret storage in legacy tables",

View File

@ -179,10 +179,6 @@ const (
// Experimental framework to build interactive dashboards
FlagScenes = "scenes"
// FlagUseLegacyHeatmapPanel
// Continue to use the angular/flot based heatmap panel
FlagUseLegacyHeatmapPanel = "useLegacyHeatmapPanel"
// FlagDisableSecretsCompatibility
// Disable duplicated secret storage in legacy tables
FlagDisableSecretsCompatibility = "disableSecretsCompatibility"

View File

@ -724,51 +724,6 @@
"signatureType": "",
"signatureOrg": ""
},
{
"name": "Heatmap (legacy)",
"type": "panel",
"id": "heatmap-old",
"enabled": true,
"pinned": false,
"info": {
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
},
"description": "Legacy heatmap panel based on angular, d3, and flot",
"links": [
{
"name": "Brendan Gregg - Heatmaps",
"url": "http://www.brendangregg.com/heatmaps.html"
},
{
"name": "Brendan Gregg - Latency Heatmaps",
"url": " http://www.brendangregg.com/HeatMaps/latency.html"
}
],
"logos": {
"small": "public/app/plugins/panel/heatmap-old/img/icn-heatmap-panel.svg",
"large": "public/app/plugins/panel/heatmap-old/img/icn-heatmap-panel.svg"
},
"build": {},
"screenshots": null,
"version": "",
"updated": ""
},
"dependencies": {
"grafanaDependency": "",
"grafanaVersion": "*",
"plugins": []
},
"latestVersion": "",
"hasUpdate": false,
"defaultNavUrl": "/plugins/heatmap-old/",
"category": "",
"state": "deprecated",
"signature": "internal",
"signatureType": "",
"signatureOrg": ""
},
{
"name": "Histogram",
"type": "panel",

View File

@ -44,7 +44,6 @@ const phlarePlugin = async () =>
const parcaPlugin = async () =>
await import(/* webpackChunkName: "parcaPlugin" */ 'app/plugins/datasource/parca/module');
import { config } from '@grafana/runtime';
import * as alertGroupsPanel from 'app/plugins/panel/alertGroups/module';
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
import * as annoListPanel from 'app/plugins/panel/annolist/module';
@ -79,23 +78,10 @@ const iconPanel = async () => await import(/* webpackChunkName: "iconPanel" */ '
const graphPanel = async () => await import(/* webpackChunkName: "graphPlugin" */ 'app/plugins/panel/graph/module');
const heatmapPanel = async () =>
await import(/* webpackChunkName: "heatmapPanel" */ 'app/plugins/panel/heatmap/module');
const heatmapPanelOLD = async () =>
await import(/* webpackChunkName: "heatmapPanelOLD" */ 'app/plugins/panel/heatmap-old/module');
const tableOldPanel = async () =>
await import(/* webpackChunkName: "tableOldPlugin" */ 'app/plugins/panel/table-old/module');
// Automatically migrate heatmap panel.
if (config.featureToggles.useLegacyHeatmapPanel) {
const heatmap = config.panels['heatmap'];
const legacy = config.panels['heatmap-old'];
legacy.id = heatmap.id;
legacy.module = heatmap.module;
legacy.state = heatmap.state;
config.panels['heatmap'] = legacy;
}
delete config.panels['heatmap-old'];
const builtInPlugins: any = {
'app/plugins/datasource/graphite/module': graphitePlugin,
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
@ -133,7 +119,7 @@ const builtInPlugins: any = {
'app/plugins/panel/dashlist/module': dashListPanel,
'app/plugins/panel/alertlist/module': alertListPanel,
'app/plugins/panel/annolist/module': annoListPanel,
'app/plugins/panel/heatmap/module': config.featureToggles.useLegacyHeatmapPanel ? heatmapPanelOLD : heatmapPanel,
'app/plugins/panel/heatmap/module': heatmapPanel,
'app/plugins/panel/table/module': tablePanel,
'app/plugins/panel/table-old/module': tableOldPanel,
'app/plugins/panel/news/module': newsPanel,

View File

@ -19,7 +19,6 @@ import (
var skipPlugins = map[string]bool{
"canvas": true,
"heatmap": true,
"heatmap-old": true,
"candlestick": true,
"state-timeline": true,
"status-history": true,

View File

@ -1,7 +0,0 @@
# Heatmap Panel - Native Plugin
The Heatmap panel allows you to view histograms over time and is **included** with Grafana.
Read more about it here:
[http://docs.grafana.org/features/panels/heatmap/](http://docs.grafana.org/features/panels/heatmap/)

View File

@ -1,50 +0,0 @@
export class AxesEditorCtrl {
panel: any;
panelCtrl: any;
logScales: any;
dataFormats: any;
yBucketBoundModes: any;
/** @ngInject */
constructor($scope: any, uiSegmentSrv: any) {
$scope.editor = this;
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
this.logScales = {
linear: 1,
'log (base 2)': 2,
'log (base 10)': 10,
'log (base 32)': 32,
'log (base 1024)': 1024,
};
this.dataFormats = {
'Time series': 'timeseries',
'Time series buckets': 'tsbuckets',
};
this.yBucketBoundModes = {
Auto: 'auto',
Upper: 'upper',
Lower: 'lower',
Middle: 'middle',
};
}
setUnitFormat = (unit: string) => {
this.panel.yAxis.format = unit;
this.panelCtrl.render();
};
}
/** @ngInject */
export function axesEditor() {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/app/plugins/panel/heatmap/partials/axes_editor.html',
controller: AxesEditorCtrl,
};
}

View File

@ -1,347 +0,0 @@
import * as d3 from 'd3';
import $ from 'jquery';
import { find, isEmpty, isNil, sortBy, uniq } from 'lodash';
import { PanelEvents } from '@grafana/data';
import coreModule from 'app/angular/core_module';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { tickStep } from 'app/core/utils/ticks';
import { getColorScale, getOpacityScale } from './color_scale';
const LEGEND_HEIGHT_PX = 6;
const LEGEND_WIDTH_PX = 100;
const LEGEND_TICK_SIZE = 0;
const LEGEND_VALUE_MARGIN = 0;
const LEGEND_PADDING_LEFT = 10;
const LEGEND_SEGMENT_WIDTH = 10;
/**
* Color legend for heatmap editor.
*/
coreModule.directive('colorLegend', () => {
return {
restrict: 'E',
template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
link: (scope: any, elem, attrs) => {
const ctrl = scope.ctrl;
const panel = scope.ctrl.panel;
render();
ctrl.events.on(PanelEvents.render, () => {
render();
});
function render() {
const legendElem = $(elem).find('svg');
const legendWidth = Math.floor(legendElem.outerWidth() ?? 10);
if (panel.color.mode === 'spectrum') {
const colorScheme: any = find(ctrl.colorSchemes, {
value: panel.color.colorScheme,
});
const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, legendWidth);
drawSimpleColorLegend(elem, colorScale);
} else if (panel.color.mode === 'opacity') {
const colorOptions = panel.color;
drawSimpleOpacityLegend(elem, colorOptions);
}
}
},
};
});
/**
* Heatmap legend with scale values.
*/
coreModule.directive('heatmapLegend', () => {
return {
restrict: 'E',
template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
link: (scope: any, elem, attrs) => {
const ctrl = scope.ctrl;
const panel = scope.ctrl.panel;
render();
ctrl.events.on(PanelEvents.render, () => {
render();
});
function render() {
clearLegend(elem);
if (!isEmpty(ctrl.data) && !isEmpty(ctrl.data.cards)) {
const cardStats = ctrl.data.cardStats;
const rangeFrom = isNil(panel.color.min) ? Math.max(cardStats.min, 0) : panel.color.min;
const rangeTo = isNil(panel.color.max) ? cardStats.max : panel.color.max;
const maxValue = cardStats.max;
const minValue = cardStats.min;
if (panel.color.mode === 'spectrum') {
const colorScheme: any = find(ctrl.colorSchemes, {
value: panel.color.colorScheme,
});
drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
} else if (panel.color.mode === 'opacity') {
const colorOptions = panel.color;
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
}
}
}
},
};
});
function drawColorLegend(
elem: JQuery,
colorScheme: any,
rangeFrom: number,
rangeTo: number,
maxValue: number,
minValue: number
) {
const legendElem = $(elem).find('svg');
const legendDomElement = legendElem.get(0);
if (legendDomElement) {
const legend = d3.select(legendDomElement);
clearLegend(elem);
const legendWidth = Math.floor(legendElem.outerWidth() ?? 10) - 30;
const legendHeight = legendElem.attr('height') as any;
const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
const widthFactor = legendWidth / (rangeTo - rangeFrom);
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, rangeTo, rangeFrom);
legend
.append('g')
.attr('class', 'legend-color-bar')
.attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
.selectAll('.heatmap-color-legend-rect')
.data(valuesRange)
.enter()
.append('rect')
.attr('x', (d) => Math.round((d - rangeFrom) * widthFactor))
.attr('y', 0)
.attr('width', Math.round(rangeStep * widthFactor + 1)) // Overlap rectangles to prevent gaps
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', (d) => colorScale(d));
drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
}
}
function drawOpacityLegend(
elem: JQuery,
options: { cardColor: null },
rangeFrom: number,
rangeTo: number,
maxValue: any,
minValue: number
) {
const legendElem = $(elem).find('svg');
const legendDomElement = legendElem.get(0);
if (legendDomElement) {
const legend = d3.select(legendDomElement);
clearLegend(elem);
const legendWidth = Math.floor(legendElem.outerWidth() ?? 30) - 30;
const legendHeight = legendElem.attr('height') as any;
const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
const widthFactor = legendWidth / (rangeTo - rangeFrom);
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
const opacityScale = getOpacityScale(options, rangeTo, rangeFrom);
legend
.append('g')
.attr('class', 'legend-color-bar')
.attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
.selectAll('.heatmap-opacity-legend-rect')
.data(valuesRange)
.enter()
.append('rect')
.attr('x', (d) => Math.round((d - rangeFrom) * widthFactor))
.attr('y', 0)
.attr('width', Math.round(rangeStep * widthFactor))
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', options.cardColor)
.style('opacity', (d) => opacityScale(d));
drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
}
}
function drawLegendValues(
elem: JQuery,
rangeFrom: number,
rangeTo: number,
maxValue: any,
minValue: any,
legendWidth: number,
valuesRange: number[]
) {
const legendElem = $(elem).find('svg');
const legendDomElement = legendElem.get(0);
if (legendDomElement) {
const legend = d3.select(legendDomElement);
if (legendWidth <= 0 || legendDomElement.childNodes.length === 0) {
return;
}
const legendValueScale = d3.scaleLinear().domain([rangeFrom, rangeTo]).range([0, legendWidth]);
const ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
const xAxis = d3.axisBottom(legendValueScale).tickValues(ticks).tickSize(LEGEND_TICK_SIZE);
const colorRect = legendElem.find(':first-child');
const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
const posX = getSvgElemX(colorRect) + LEGEND_PADDING_LEFT;
d3.select(legendDomElement)
.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + posX + ',' + posY + ')')
.call(xAxis);
legend.select('.axis').select('.domain').remove();
}
}
function drawSimpleColorLegend(elem: JQuery, colorScale: any) {
const legendElem = $(elem).find('svg');
clearLegend(elem);
const legendWidth = Math.floor(legendElem.outerWidth() ?? 30);
const legendHeight = legendElem.attr('height') as any;
if (legendWidth) {
const valuesNumber = Math.floor(legendWidth / 2);
const rangeStep = Math.floor(legendWidth / valuesNumber);
const valuesRange = d3.range(0, legendWidth, rangeStep);
const legendDomElement = legendElem.get(0);
if (legendDomElement) {
const legend = d3.select(legendDomElement);
const legendRects = legend.selectAll('.heatmap-color-legend-rect').data(valuesRange);
legendRects
.enter()
.append('rect')
.attr('x', (d) => d)
.attr('y', 0)
.attr('width', rangeStep + 1) // Overlap rectangles to prevent gaps
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', (d) => colorScale(d));
}
}
}
function drawSimpleOpacityLegend(elem: JQuery, options: { colorScale: string; exponent: number; cardColor: string }) {
const legendElem = $(elem).find('svg');
const legendDomElement = legendElem.get(0);
if (legendDomElement) {
clearLegend(elem);
const legend = d3.select(legendDomElement);
const legendWidth = Math.floor(legendElem.outerWidth() ?? 30);
const legendHeight = legendElem.attr('height') as any;
if (legendWidth) {
let legendOpacityScale: any;
if (options.colorScale === 'linear') {
legendOpacityScale = d3.scaleLinear().domain([0, legendWidth]).range([0, 1]);
} else if (options.colorScale === 'sqrt') {
legendOpacityScale = d3.scalePow().exponent(options.exponent).domain([0, legendWidth]).range([0, 1]);
}
const rangeStep = 10;
const valuesRange = d3.range(0, legendWidth, rangeStep);
const legendRects = legend.selectAll('.heatmap-opacity-legend-rect').data(valuesRange);
legendRects
.enter()
.append('rect')
.attr('x', (d) => d)
.attr('y', 0)
.attr('width', rangeStep)
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', config.theme2.visualization.getColorByName(options.cardColor))
.style('opacity', (d) => legendOpacityScale(d));
}
}
}
function clearLegend(elem: JQuery) {
const legendElem = $(elem).find('svg');
legendElem.empty();
}
function getSvgElemX(elem: JQuery) {
const svgElem: any = elem.get(0) as any;
if (svgElem && svgElem.x && svgElem.x.baseVal) {
return svgElem.x.baseVal.value;
} else {
return 0;
}
}
function getSvgElemHeight(elem: JQuery<any>) {
const svgElem: any = elem.get(0);
if (svgElem && svgElem.height && svgElem.height.baseVal) {
return svgElem.height.baseVal.value;
} else {
return 0;
}
}
function buildLegendTicks(rangeFrom: number, rangeTo: number, maxValue: number, minValue: number) {
const range = rangeTo - rangeFrom;
const tickStepSize = tickStep(rangeFrom, rangeTo, 3);
const ticksNum = Math.ceil(range / tickStepSize);
const firstTick = getFirstCloseTick(rangeFrom, tickStepSize);
let ticks = [];
for (let i = 0; i < ticksNum; i++) {
const current = firstTick + tickStepSize * i;
// Add user-defined min and max if it had been set
if (isValueCloseTo(minValue, current, tickStepSize)) {
ticks.push(minValue);
continue;
} else if (minValue < current) {
ticks.push(minValue);
}
if (isValueCloseTo(maxValue, current, tickStepSize)) {
ticks.push(maxValue);
continue;
} else if (maxValue < current) {
ticks.push(maxValue);
}
ticks.push(current);
}
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
ticks.push(maxValue);
}
ticks.push(rangeTo);
ticks = sortBy(uniq(ticks));
return ticks;
}
function isValueCloseTo(val: number, valueTo: number, step: number) {
const diff = Math.abs(val - valueTo);
return diff < step * 0.3;
}
function getFirstCloseTick(minValue: number, step: number) {
if (minValue < 0) {
return Math.floor(minValue / step) * step;
}
return 0;
}

View File

@ -1,27 +0,0 @@
import * as d3 from 'd3';
import * as d3ScaleChromatic from 'd3-scale-chromatic';
export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any {
//@ts-ignore
const colorInterpolator = d3ScaleChromatic[colorScheme.value];
const colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark');
const start = colorScaleInverted ? maxValue : minValue;
const end = colorScaleInverted ? minValue : maxValue;
return d3.scaleSequential(colorInterpolator).domain([start, end]);
}
export function getOpacityScale(
options: { cardColor?: null; colorScale?: any; exponent?: any },
maxValue: number,
minValue = 0
): any {
let legendOpacityScale;
if (options.colorScale === 'linear') {
legendOpacityScale = d3.scaleLinear().domain([minValue, maxValue]).range([0, 1]);
} else if (options.colorScale === 'sqrt') {
legendOpacityScale = d3.scalePow().exponent(options.exponent).domain([minValue, maxValue]).range([0, 1]);
}
return legendOpacityScale;
}

View File

@ -1,22 +0,0 @@
export class HeatmapDisplayEditorCtrl {
panel: any;
panelCtrl: any;
/** @ngInject */
constructor($scope: any) {
$scope.editor = this;
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
}
}
/** @ngInject */
export function heatmapDisplayEditor() {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/app/plugins/panel/heatmap/partials/display_editor.html',
controller: HeatmapDisplayEditorCtrl,
};
}

View File

@ -1,385 +0,0 @@
import { auto } from 'angular';
import { defaultsDeep, includes, keys, map, reduce, min as _min, max as _max } from 'lodash';
import { LegacyResponseData, PanelEvents, DataFrame, rangeUtil } from '@grafana/data';
import appEvents from 'app/core/app_events';
import TimeSeries from 'app/core/time_series2';
import kbn from 'app/core/utils/kbn';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getProcessedDataFrames } from 'app/features/query/state/runRequest';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { ZoomOutEvent } from 'app/types/events';
import { DataProcessor } from '../graph/data_processor';
import { axesEditor } from './axes_editor';
import { heatmapDisplayEditor } from './display_editor';
import {
convertToHeatMap,
convertToCards,
histogramToHeatmap,
calculateBucketSize,
sortSeriesByLabel,
} from './heatmap_data_converter';
import rendering from './rendering';
const X_BUCKET_NUMBER_DEFAULT = 30;
const Y_BUCKET_NUMBER_DEFAULT = 10;
const panelDefaults: any = {
heatmap: {},
cards: {
cardPadding: null,
cardRound: null,
},
color: {
mode: 'spectrum',
cardColor: '#b4ff00',
colorScale: 'sqrt',
exponent: 0.5,
colorScheme: 'interpolateOranges',
},
legend: {
show: false,
},
dataFormat: 'timeseries',
yBucketBound: 'auto',
reverseYBuckets: false,
xAxis: {
show: true,
},
yAxis: {
show: true,
format: 'short',
decimals: null,
logBase: 1,
splitFactor: null,
min: null,
max: null,
},
xBucketSize: null,
xBucketNumber: null,
yBucketSize: null,
yBucketNumber: null,
tooltip: {
show: true,
showHistogram: false,
},
highlightCards: true,
hideZeroBuckets: false,
};
const colorModes = ['opacity', 'spectrum'];
const opacityScales = ['linear', 'sqrt'];
// Schemes from d3-scale-chromatic
// https://github.com/d3/d3-scale-chromatic
const colorSchemes = [
// Diverging
{ name: 'Spectral', value: 'interpolateSpectral', invert: 'always' },
{ name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always' },
// Sequential (Single Hue)
{ name: 'Blues', value: 'interpolateBlues', invert: 'dark' },
{ name: 'Greens', value: 'interpolateGreens', invert: 'dark' },
{ name: 'Greys', value: 'interpolateGreys', invert: 'dark' },
{ name: 'Oranges', value: 'interpolateOranges', invert: 'dark' },
{ name: 'Purples', value: 'interpolatePurples', invert: 'dark' },
{ name: 'Reds', value: 'interpolateReds', invert: 'dark' },
// Sequential (Multi-Hue)
{ name: 'Turbo', value: 'interpolateTurbo', invert: 'light' },
{ name: 'Cividis', value: 'interpolateCividis', invert: 'light' },
{ name: 'Viridis', value: 'interpolateViridis', invert: 'light' },
{ name: 'Magma', value: 'interpolateMagma', invert: 'light' },
{ name: 'Inferno', value: 'interpolateInferno', invert: 'light' },
{ name: 'Plasma', value: 'interpolatePlasma', invert: 'light' },
{ name: 'Warm', value: 'interpolateWarm', invert: 'light' },
{ name: 'Cool', value: 'interpolateCool', invert: 'light' },
{ name: 'Cubehelix', value: 'interpolateCubehelixDefault', invert: 'light' },
{ name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' },
{ name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' },
{ name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' },
{ name: 'OrRd', value: 'interpolateOrRd', invert: 'dark' },
{ name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark' },
{ name: 'PuBu', value: 'interpolatePuBu', invert: 'dark' },
{ name: 'PuRd', value: 'interpolatePuRd', invert: 'dark' },
{ name: 'RdPu', value: 'interpolateRdPu', invert: 'dark' },
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
{ name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' },
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
];
const dsSupportHistogramSort = ['elasticsearch'];
export class HeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
opacityScales: any = [];
colorModes: any = [];
colorSchemes: any = [];
selectionActivated: boolean;
unitFormats: any;
data: any;
series: TimeSeries[] = [];
dataWarning: any;
decimals = 0;
scaledDecimals = 0;
processor: DataProcessor; // Shared with graph panel
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, templateSrv: TemplateSrv, timeSrv: TimeSrv) {
super($scope, $injector);
this.selectionActivated = false;
defaultsDeep(this.panel, panelDefaults);
this.opacityScales = opacityScales;
this.colorModes = colorModes;
this.colorSchemes = colorSchemes;
// Use DataFrames
this.useDataFrames = true;
this.processor = new DataProcessor({
xaxis: { mode: 'custom' }, // NOT: 'histogram' :)
aliasColors: {}, // avoids null reference
});
// Bind grafana panel events
this.events.on(PanelEvents.render, this.onRender.bind(this));
this.events.on(PanelEvents.dataFramesReceived, this.onDataFramesReceived.bind(this));
this.events.on(PanelEvents.dataSnapshotLoad, this.onSnapshotLoad.bind(this));
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
this.onCardColorChange = this.onCardColorChange.bind(this);
// Make sure we do not save the version so migration to react version is kicked off
delete this.panel.pluginVersion;
}
onInitEditMode() {
this.addEditorTab('Axes', axesEditor, 2);
this.addEditorTab('Display', heatmapDisplayEditor, 3);
this.unitFormats = kbn.getUnitFormats();
}
zoomOut(evt: any) {
appEvents.publish(new ZoomOutEvent({ scale: 2 }));
}
onRender() {
if (this.panel.dataFormat === 'tsbuckets') {
this.convertHistogramToHeatmapData();
} else {
this.convertTimeSeriesToHeatmapData();
}
}
convertTimeSeriesToHeatmapData() {
if (!this.range || !this.series) {
return;
}
let xBucketSize, yBucketSize, bucketsData, heatmapStats;
const logBase = this.panel.yAxis.logBase;
const xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT;
const xBucketSizeByNumber = Math.floor((this.range.to.valueOf() - this.range.from.valueOf()) / xBucketNumber);
// Parse X bucket size (number or interval)
const isIntervalString = kbn.intervalRegex.test(this.panel.xBucketSize);
if (isIntervalString) {
xBucketSize = rangeUtil.intervalToMs(this.panel.xBucketSize);
} else if (
isNaN(Number(this.panel.xBucketSize)) ||
this.panel.xBucketSize === '' ||
this.panel.xBucketSize === null
) {
xBucketSize = xBucketSizeByNumber;
} else {
xBucketSize = Number(this.panel.xBucketSize);
}
// Calculate Y bucket size
heatmapStats = this.parseSeries(this.series);
const yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT;
if (logBase !== 1) {
yBucketSize = this.panel.yAxis.splitFactor;
} else {
if (heatmapStats.max === heatmapStats.min) {
if (heatmapStats.max) {
yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT;
} else {
yBucketSize = 1;
}
} else {
yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber;
}
yBucketSize = this.panel.yBucketSize || yBucketSize;
}
bucketsData = convertToHeatMap(this.series, yBucketSize, xBucketSize, logBase);
// Set default Y range if no data
if (!heatmapStats.min && !heatmapStats.max) {
heatmapStats = { min: -1, max: 1, minLog: 1 };
yBucketSize = 1;
}
const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
this.data = {
buckets: bucketsData,
heatmapStats: heatmapStats,
xBucketSize: xBucketSize,
yBucketSize: yBucketSize,
cards: cards,
cardStats: cardStats,
};
}
convertHistogramToHeatmapData() {
if (!this.range || !this.series) {
return;
}
const panelDatasource = this.getPanelDataSourceType();
let xBucketSize, yBucketSize, bucketsData, tsBuckets;
// Try to sort series by bucket bound, if datasource doesn't do it.
if (!includes(dsSupportHistogramSort, panelDatasource)) {
this.series.sort(sortSeriesByLabel);
}
if (this.panel.reverseYBuckets) {
this.series.reverse();
}
// Convert histogram to heatmap. Each histogram bucket represented by the series which name is
// a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as Y axis labels.
bucketsData = histogramToHeatmap(this.series);
tsBuckets = map(this.series, 'label');
const yBucketBound = this.panel.yBucketBound;
if (
(panelDatasource === 'prometheus' && yBucketBound !== 'lower' && yBucketBound !== 'middle') ||
yBucketBound === 'upper'
) {
// Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
tsBuckets = [''].concat(tsBuckets);
} else {
// Elasticsearch uses labels as lower bucket bounds, so add empty top bucket label.
// Use this as a default mode as well.
tsBuckets.push('');
}
// Calculate bucket size based on heatmap data
const xBucketBoundSet = map(keys(bucketsData), (key) => Number(key));
xBucketSize = calculateBucketSize(xBucketBoundSet);
// Always let yBucketSize=1 in 'tsbuckets' mode
yBucketSize = 1;
const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
this.data = {
buckets: bucketsData,
xBucketSize: xBucketSize,
yBucketSize: yBucketSize,
tsBuckets: tsBuckets,
cards: cards,
cardStats: cardStats,
};
}
getPanelDataSourceType() {
if (this.datasource && this.datasource.meta && this.datasource.meta.id) {
return this.datasource.meta.id;
} else {
return 'unknown';
}
}
// This should only be called from the snapshot callback
onSnapshotLoad(dataList: LegacyResponseData[]) {
this.onDataFramesReceived(getProcessedDataFrames(dataList));
}
// Directly support DataFrame
onDataFramesReceived(data: DataFrame[]) {
this.series = this.processor.getSeriesList({ dataList: data, range: this.range }).map((ts) => {
ts.color = undefined; // remove whatever the processor set
ts.flotpairs = ts.getFlotPairs(this.panel.nullPointMode);
return ts;
});
this.dataWarning = null;
const datapointsCount = reduce(
this.series,
(sum, series) => {
return sum + series.datapoints.length;
},
0
);
if (datapointsCount === 0) {
this.dataWarning = {
title: 'No data points',
tip: 'No datapoints returned from data query',
};
} else {
for (const series of this.series) {
if (series.isOutsideRange) {
this.dataWarning = {
title: 'Data points outside time range',
tip: 'Can be caused by timezone mismatch or missing time filter in query',
};
break;
}
}
}
this.render();
}
onDataError() {
this.series = [];
this.render();
}
onCardColorChange(newColor: any) {
this.panel.color.cardColor = newColor;
this.render();
}
parseSeries(series: TimeSeries[]) {
const min = _min(map(series, (s) => s.stats.min));
const minLog = _min(map(series, (s) => s.stats.logmin));
const max = _max(map(series, (s) => s.stats.max));
return {
max,
min,
minLog,
};
}
parseHistogramSeries(series: TimeSeries[]) {
const bounds = map(series, (s) => Number(s.alias));
const min = _min(bounds);
const minLog = _min(bounds);
const max = _max(bounds);
return {
max: max,
min: min,
minLog: minLog,
};
}
link(scope: any, elem: any, attrs: any, ctrl: any) {
rendering(scope, elem, attrs, ctrl);
}
}

View File

@ -1,491 +0,0 @@
import { concat, forEach, isEmpty, isEqual, isNumber, sortBy } from 'lodash';
import { TimeSeries } from 'app/core/core';
import { Bucket, HeatmapCard, HeatmapCardStats, YBucket, XBucket } from './types';
const VALUE_INDEX = 0;
const TIME_INDEX = 1;
/**
* Convert histogram represented by the list of series to heatmap object.
* @param seriesList List of time series
*/
function histogramToHeatmap(seriesList: TimeSeries[]) {
const heatmap: any = {};
for (let i = 0; i < seriesList.length; i++) {
const series = seriesList[i];
const bound = i;
if (isNaN(bound)) {
return heatmap;
}
for (const point of series.datapoints) {
const count = point[VALUE_INDEX];
const time = point[TIME_INDEX];
if (!isNumber(count)) {
continue;
}
let bucket = heatmap[time];
if (!bucket) {
bucket = heatmap[time] = { x: time, buckets: {} };
}
bucket.buckets[bound] = {
y: bound,
count: count,
bounds: {
top: null,
bottom: bound,
},
values: [],
points: [],
};
}
}
return heatmap;
}
/**
* Sort series representing histogram by label value.
*/
function sortSeriesByLabel(s1: { label: string }, s2: { label: string }) {
let label1, label2;
try {
// fail if not integer. might happen with bad queries
label1 = parseHistogramLabel(s1.label);
label2 = parseHistogramLabel(s2.label);
} catch (err) {
console.error(err instanceof Error ? err.message : err);
return 0;
}
if (label1 > label2) {
return 1;
}
if (label1 < label2) {
return -1;
}
return 0;
}
function parseHistogramLabel(label: string): number {
if (label === '+Inf' || label === 'inf') {
return +Infinity;
}
const value = Number(label);
if (isNaN(value)) {
throw new Error(`Error parsing histogram label: ${label} is not a number`);
}
return value;
}
/**
* Convert buckets into linear array of "cards" - objects, represented heatmap elements.
* @param {Object} buckets
* @returns {Object} Array of "card" objects and stats
*/
function convertToCards(buckets: any, hideZero = false): { cards: HeatmapCard[]; cardStats: HeatmapCardStats } {
let min = 0,
max = 0;
const cards: HeatmapCard[] = [];
forEach(buckets, (xBucket) => {
forEach(xBucket.buckets, (yBucket) => {
const card: HeatmapCard = {
x: xBucket.x,
y: yBucket.y,
yBounds: yBucket.bounds,
values: yBucket.values,
count: yBucket.count,
};
if (!hideZero || card.count !== 0) {
cards.push(card);
}
if (cards.length === 1) {
min = yBucket.count;
max = yBucket.count;
}
min = yBucket.count < min ? yBucket.count : min;
max = yBucket.count > max ? yBucket.count : max;
});
});
const cardStats = { min, max };
return { cards, cardStats };
}
/**
* Special method for log scales. When series converted into buckets with log scale,
* for simplification, 0 values are converted into 0, not into -Infinity. On the other hand, we mean
* that all values less than series minimum, is 0 values, and we create special "minimum" bucket for
* that values (actually, there're no values less than minimum, so this bucket is empty).
* 8-16| | ** | | * | **|
* 4-8| * |* *|* |** *| * |
* 2-4| * *| | ***| |* |
* 1-2|* | | | | | This bucket contains minimum series value
* 0.5-1|____|____|____|____|____| This bucket should be displayed as 0 on graph
* 0|____|____|____|____|____| This bucket is for 0 values (should actually be -Infinity)
* So we should merge two bottom buckets into one (0-value bucket).
*
* @param {Object} buckets Heatmap buckets
* @param {Number} minValue Minimum series value
* @returns {Object} Transformed buckets
*/
function mergeZeroBuckets(buckets: any, minValue: number) {
forEach(buckets, (xBucket) => {
const yBuckets = xBucket.buckets;
const emptyBucket: any = {
bounds: { bottom: 0, top: 0 },
values: [],
points: [],
count: 0,
};
const nullBucket = yBuckets[0] || emptyBucket;
const minBucket = yBuckets[minValue] || emptyBucket;
const newBucket: any = {
y: 0,
bounds: { bottom: minValue, top: minBucket.bounds.top || minValue },
values: [],
points: [],
count: 0,
};
newBucket.points = nullBucket.points.concat(minBucket.points);
newBucket.values = nullBucket.values.concat(minBucket.values);
newBucket.count = newBucket.values.length;
if (newBucket.count === 0) {
return;
}
delete yBuckets[minValue];
yBuckets[0] = newBucket;
});
return buckets;
}
/**
* Convert set of time series into heatmap buckets
* @returns {Object} Heatmap object:
* {
* xBucketBound_1: {
* x: xBucketBound_1,
* buckets: {
* yBucketBound_1: {
* y: yBucketBound_1,
* bounds: {bottom, top}
* values: [val_1, val_2, ..., val_K],
* points: [[val_Y, val_X, series_name], ..., [...]],
* seriesStat: {seriesName_1: val_1, seriesName_2: val_2}
* },
* ...
* yBucketBound_M: {}
* },
* values: [val_1, val_2, ..., val_K],
* points: [
* [val_Y, val_X, series_name], (point_1)
* ...
* [...] (point_K)
* ]
* },
* xBucketBound_2: {},
* ...
* xBucketBound_N: {}
* }
*/
function convertToHeatMap(seriesList: TimeSeries[], yBucketSize: number, xBucketSize: number, logBase = 1) {
const heatmap = {};
for (const series of seriesList) {
const datapoints = series.datapoints;
const seriesName = series.label;
// Slice series into X axis buckets
// | | ** | | * | **|
// | * |* *|* |** *| * |
// |** *| | ***| |* |
// |____|____|____|____|____|_
//
forEach(datapoints, (point) => {
const bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize);
pushToXBuckets(heatmap, point, bucketBound, seriesName);
});
}
// Slice X axis buckets into Y (value) buckets
// | **| |2|,
// | * | --\ |1|,
// |* | --/ |1|,
// |____| |0|
//
forEach(heatmap, (xBucket: any) => {
if (logBase !== 1) {
xBucket.buckets = convertToLogScaleValueBuckets(xBucket, yBucketSize, logBase);
} else {
xBucket.buckets = convertToValueBuckets(xBucket, yBucketSize);
}
});
return heatmap;
}
function pushToXBuckets(buckets: any, point: any[], bucketNum: number, seriesName: string) {
const value = point[VALUE_INDEX];
if (value === null || value === undefined || isNaN(value)) {
return;
}
// Add series name to point for future identification
const pointExt = concat(point, seriesName);
if (buckets[bucketNum] && buckets[bucketNum].values) {
buckets[bucketNum].values.push(value);
buckets[bucketNum].points.push(pointExt);
} else {
buckets[bucketNum] = {
x: bucketNum,
values: [value],
points: [pointExt],
};
}
}
function pushToYBuckets(
buckets: Bucket,
bucketNum: number,
value: any,
point: string[],
bounds: { bottom: number; top: number }
) {
let count = 1;
// Use the 3rd argument as scale/count
if (point.length > 3) {
count = parseInt(point[2], 10);
}
if (buckets[bucketNum]) {
buckets[bucketNum].values.push(value);
buckets[bucketNum].points?.push(point);
buckets[bucketNum].count += count;
} else {
buckets[bucketNum] = {
y: bucketNum,
bounds: bounds,
values: [value],
points: [point],
count: count,
};
}
}
function getValueBucketBound(value: any, yBucketSize: number, logBase: number) {
if (logBase === 1) {
return getBucketBound(value, yBucketSize);
} else {
return getLogScaleBucketBound(value, yBucketSize, logBase);
}
}
/**
* Find bucket for given value (for linear scale)
*/
function getBucketBounds(value: number, bucketSize: number) {
let bottom, top;
bottom = Math.floor(value / bucketSize) * bucketSize;
top = (Math.floor(value / bucketSize) + 1) * bucketSize;
return { bottom, top };
}
function getBucketBound(value: number, bucketSize: number) {
const bounds = getBucketBounds(value, bucketSize);
return bounds.bottom;
}
function convertToValueBuckets(xBucket: { values: any; points: any }, bucketSize: number) {
const values = xBucket.values;
const points = xBucket.points;
const buckets = {};
forEach(values, (val, index) => {
const bounds = getBucketBounds(val, bucketSize);
const bucketNum = bounds.bottom;
pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
});
return buckets;
}
/**
* Find bucket for given value (for log scales)
*/
function getLogScaleBucketBounds(value: number, yBucketSplitFactor: number, logBase: number) {
let top, bottom;
if (value === 0) {
return { bottom: 0, top: 0 };
}
const valueLog = logp(value, logBase);
let pow, powTop;
if (yBucketSplitFactor === 1 || !yBucketSplitFactor) {
pow = Math.floor(valueLog);
powTop = pow + 1;
} else {
const additionalBucketSize = 1 / yBucketSplitFactor;
let additionalLog = valueLog - Math.floor(valueLog);
additionalLog = Math.floor(additionalLog / additionalBucketSize) * additionalBucketSize;
pow = Math.floor(valueLog) + additionalLog;
powTop = pow + additionalBucketSize;
}
bottom = Math.pow(logBase, pow);
top = Math.pow(logBase, powTop);
return { bottom, top };
}
function getLogScaleBucketBound(value: number, yBucketSplitFactor: number, logBase: number) {
const bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase);
return bounds.bottom;
}
function convertToLogScaleValueBuckets(
xBucket: { values: any; points: any },
yBucketSplitFactor: number,
logBase: number
) {
const values = xBucket.values;
const points = xBucket.points;
const buckets = {};
forEach(values, (val, index) => {
const bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase);
const bucketNum = bounds.bottom;
pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
});
return buckets;
}
/**
* Logarithm for custom base
* @param value
* @param base logarithm base
*/
function logp(value: number, base: number) {
return Math.log(value) / Math.log(base);
}
/**
* Calculate size of Y bucket from given buckets bounds.
* @param bounds Array of Y buckets bounds
* @param logBase Logarithm base
*/
function calculateBucketSize(bounds: number[], logBase = 1): number {
let bucketSize = Infinity;
if (bounds.length === 0) {
return 0;
} else if (bounds.length === 1) {
return bounds[0];
} else {
bounds = sortBy(bounds);
for (let i = 1; i < bounds.length; i++) {
const distance = getDistance(bounds[i], bounds[i - 1], logBase);
bucketSize = distance < bucketSize ? distance : bucketSize;
}
}
return bucketSize;
}
/**
* Calculate distance between two numbers in given scale (linear or logarithmic).
* @param a
* @param b
* @param logBase
*/
function getDistance(a: number, b: number, logBase = 1): number {
if (logBase === 1) {
// Linear distance
return Math.abs(b - a);
} else {
// logarithmic distance
const ratio = Math.max(a, b) / Math.min(a, b);
return logp(ratio, logBase);
}
}
/**
* Compare two heatmap data objects
* @param objA
* @param objB
*/
function isHeatmapDataEqual(objA: any, objB: any): boolean {
let isEql = !emptyXOR(objA, objB);
forEach(objA, (xBucket: XBucket, x) => {
if (objB[x]) {
if (emptyXOR(xBucket.buckets, objB[x].buckets)) {
isEql = false;
return false;
}
forEach(xBucket.buckets, (yBucket: YBucket, y) => {
if (objB[x].buckets && objB[x].buckets[y]) {
if (objB[x].buckets[y].values) {
isEql = isEqual(sortBy(yBucket.values), sortBy(objB[x].buckets[y].values));
if (!isEql) {
return false;
} else {
return true;
}
} else {
isEql = false;
return false;
}
} else {
isEql = false;
return false;
}
});
if (!isEql) {
return false;
} else {
return true;
}
} else {
isEql = false;
return false;
}
});
return isEql;
}
function emptyXOR(foo: any, bar: any): boolean {
return (isEmpty(foo) || isEmpty(bar)) && !(isEmpty(foo) && isEmpty(bar));
}
export {
convertToHeatMap,
histogramToHeatmap,
convertToCards,
mergeZeroBuckets,
getValueBucketBound,
isHeatmapDataEqual,
calculateBucketSize,
sortSeriesByLabel,
};

View File

@ -1,277 +0,0 @@
import * as d3 from 'd3';
import $ from 'jquery';
import { filter, find, isNumber, map, reduce } from 'lodash';
import { getValueFormat, formattedValueToString } from '@grafana/data';
import { getValueBucketBound } from './heatmap_data_converter';
const TOOLTIP_PADDING_X = 30;
const TOOLTIP_PADDING_Y = 5;
const HISTOGRAM_WIDTH = 160;
const HISTOGRAM_HEIGHT = 40;
export class HeatmapTooltip {
tooltip: any;
scope: any;
dashboard: any;
panelCtrl: any;
panel: any;
heatmapPanel: any;
mouseOverBucket: boolean;
originalFillColor: any;
constructor(elem: JQuery, scope: any) {
this.scope = scope;
this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel;
this.heatmapPanel = elem;
this.mouseOverBucket = false;
this.originalFillColor = null;
elem.on('mouseleave', this.onMouseLeave.bind(this));
}
onMouseLeave() {
this.destroy();
}
onMouseMove(e: any) {
if (!this.panel.tooltip.show) {
return;
}
this.move(e);
}
add() {
this.tooltip = d3.select('body').append('div').attr('class', 'heatmap-tooltip graph-tooltip grafana-tooltip');
}
destroy() {
if (this.tooltip) {
this.tooltip.remove();
}
this.tooltip = null;
}
show(pos: { panelRelY: any }, data: any) {
if (!this.panel.tooltip.show || !data) {
return;
}
// shared tooltip mode
if (pos.panelRelY) {
return;
}
const { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data);
if (!data.buckets[xBucketIndex]) {
this.destroy();
return;
}
if (!this.tooltip) {
this.add();
}
let boundBottom, boundTop, valuesNumber;
const xData = data.buckets[xBucketIndex];
// Search in special 'zero' bucket also
const yData: any = find(xData.buckets, (bucket, bucketIndex) => {
return bucket.bounds.bottom === yBucketIndex || bucketIndex === yBucketIndex.toString();
});
const tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
const time = this.dashboard.formatDate(xData.x, tooltipTimeFormat);
// Decimals override. Code from panel/graph/graph.ts
let countValueFormatter, bucketBoundFormatter;
if (isNumber(this.panel.tooltipDecimals)) {
countValueFormatter = this.countValueFormatter(this.panel.tooltipDecimals, null);
bucketBoundFormatter = this.panelCtrl.tickValueFormatter(this.panelCtrl.decimals, null);
} else {
// auto decimals
// legend and tooltip gets one more decimal precision
// than graph legend ticks
const decimals = (this.panelCtrl.decimals || -1) + 1;
countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2);
bucketBoundFormatter = this.panelCtrl.tickValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2);
}
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
<div class="heatmap-histogram"></div>`;
if (yData) {
if (yData.bounds) {
if (data.tsBuckets) {
// Use Y-axis labels
const tickFormatter = (valIndex: string | number) => {
return data.tsBucketsFormatted ? data.tsBucketsFormatted[valIndex] : data.tsBuckets[valIndex];
};
boundBottom = tickFormatter(yBucketIndex);
if (this.panel.yBucketBound !== 'middle') {
boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
}
} else {
// Display 0 if bucket is a special 'zero' bucket
const bottom = yData.y ? yData.bounds.bottom : 0;
boundBottom = bucketBoundFormatter(bottom);
boundTop = bucketBoundFormatter(yData.bounds.top);
}
valuesNumber = countValueFormatter(yData.count);
const boundStr = boundTop && boundBottom ? `${boundBottom} - ${boundTop}` : boundBottom || boundTop;
tooltipHtml += `<div>
bucket: <b>${boundStr}</b> <br>
count: <b>${valuesNumber}</b> <br>
</div>`;
} else {
// currently no bounds for pre bucketed data
tooltipHtml += `<div>count: <b>${yData.count}</b><br></div>`;
}
} else {
if (!this.panel.tooltip.showHistogram) {
this.destroy();
return;
}
boundBottom = yBucketIndex;
boundTop = '';
valuesNumber = 0;
}
this.tooltip.html(tooltipHtml);
if (this.panel.tooltip.showHistogram) {
this.addHistogram(xData);
}
this.move(pos);
}
getBucketIndexes(pos: { panelRelY?: any; x?: any; y?: any }, data: any) {
const xBucketIndex = this.getXBucketIndex(pos.x, data);
const yBucketIndex = this.getYBucketIndex(pos.y, data);
return { xBucketIndex, yBucketIndex };
}
getXBucketIndex(x: number, data: { buckets: any; xBucketSize: number }) {
// First try to find X bucket by checking x pos is in the
// [bucket.x, bucket.x + xBucketSize] interval
const xBucket: any = find(data.buckets, (bucket) => {
return x > bucket.x && x - bucket.x <= data.xBucketSize;
});
return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1);
}
getYBucketIndex(y: number, data: { tsBuckets: any; yBucketSize: number }) {
if (data.tsBuckets) {
return Math.floor(y);
}
const yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase);
return yBucketIndex;
}
getSharedTooltipPos(pos: { pageX: any; x: any; pageY: any; panelRelY: number }) {
// get pageX from position on x axis and pageY from relative position in original panel
pos.pageX = this.heatmapPanel.offset().left + this.scope.xScale(pos.x);
pos.pageY = this.heatmapPanel.offset().top + this.scope.chartHeight * pos.panelRelY;
return pos;
}
addHistogram(data: { x: string | number }) {
const xBucket = this.scope.ctrl.data.buckets[data.x];
const yBucketSize = this.scope.ctrl.data.yBucketSize;
let min: number, max: number, ticks: number;
if (this.scope.ctrl.data.tsBuckets) {
min = 0;
max = this.scope.ctrl.data.tsBuckets.length - 1;
ticks = this.scope.ctrl.data.tsBuckets.length;
} else {
min = this.scope.ctrl.data.yAxis.min;
max = this.scope.ctrl.data.yAxis.max;
ticks = this.scope.ctrl.data.yAxis.ticks;
}
let histogramData = map(xBucket.buckets, (bucket) => {
const count = bucket.count !== undefined ? bucket.count : bucket.values.length;
return [bucket.bounds.bottom, count];
});
histogramData = filter(histogramData, (d) => {
return d[0] >= min && d[0] <= max;
});
const scale = this.scope.yScale.copy();
const histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]);
let barWidth: number;
if (this.panel.yAxis.logBase === 1) {
barWidth = Math.floor((HISTOGRAM_WIDTH / (max - min)) * yBucketSize * 0.9);
} else {
const barNumberFactor = yBucketSize ? yBucketSize : 1;
barWidth = Math.floor((HISTOGRAM_WIDTH / ticks / barNumberFactor) * 0.9);
}
barWidth = Math.max(barWidth, 1);
// Normalize histogram Y axis
const histogramDomain = reduce(
map(histogramData, (d) => d[1]),
(sum, val) => sum + val,
0
);
const histYScale = d3.scaleLinear().domain([0, histogramDomain]).range([0, HISTOGRAM_HEIGHT]);
const histogram = this.tooltip
.select('.heatmap-histogram')
.append('svg')
.attr('width', HISTOGRAM_WIDTH)
.attr('height', HISTOGRAM_HEIGHT);
histogram
.selectAll('.bar')
.data(histogramData)
.enter()
.append('rect')
.attr('x', (d: any[]) => {
return histXScale(d[0]);
})
.attr('width', barWidth)
.attr('y', (d: any[]) => {
return HISTOGRAM_HEIGHT - histYScale(d[1]);
})
.attr('height', (d: any[]) => {
return histYScale(d[1]);
});
}
move(pos: { panelRelY?: any; pageX?: any; pageY?: any }) {
if (!this.tooltip) {
return;
}
const elem = $(this.tooltip.node())[0];
const tooltipWidth = elem.clientWidth;
const tooltipHeight = elem.clientHeight;
let left = pos.pageX + TOOLTIP_PADDING_X;
let top = pos.pageY + TOOLTIP_PADDING_Y;
if (pos.pageX + tooltipWidth + 40 > window.innerWidth) {
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
}
if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
}
return this.tooltip.style('left', left + 'px').style('top', top + 'px');
}
countValueFormatter(decimals: number, scaledDecimals: any = null) {
const fmt = getValueFormat('short');
return (value: number) => {
return formattedValueToString(fmt(value, decimals, scaledDecimals));
};
}
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 80.09 80.09"><defs><style>.cls-1{fill:#3865ab;}.cls-2{fill:#84aff1;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}</style><linearGradient id="linear-gradient" y1="19.02" x2="66.08" y2="19.02" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient><linearGradient id="linear-gradient-2" y1="54.06" x2="66.08" y2="54.06" xlink:href="#linear-gradient"/></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><rect class="cls-1" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="28.03" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="42.05" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="56.06" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="70.08" y="14.02" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="28.03" y="28.03" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="42.05" y="28.03" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="56.06" y="28.03" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="70.08" y="28.03" width="10.02" height="10.02" rx="1"/><rect class="cls-1" y="42.05" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="42.05" y="42.05" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="56.06" y="42.05" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="70.08" y="42.05" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="14.02" y="56.06" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="14.02" y="70.08" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="28.03" y="70.08" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="42.05" y="70.08" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="14.02" width="10.02" height="10.02" rx="1"/><rect class="cls-2" y="28.03" width="10.02" height="10.02" rx="1" transform="translate(38.05 28.03) rotate(90)"/><rect class="cls-1" x="70.08" y="56.06" width="10.02" height="10.02" rx="1" transform="translate(136.16 -14.02) rotate(90)"/><rect class="cls-1" x="70.08" width="10.02" height="10.02" rx="1" transform="translate(80.09 -70.08) rotate(90)"/><path class="cls-3" d="M9,24H1a1,1,0,0,1-1-1V15a1,1,0,0,1,1-1H9a1,1,0,0,1,1,1v8A1,1,0,0,1,9,24Zm15-1V15a1,1,0,0,0-1-1H15a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,24,23Zm14,0V15a1,1,0,0,0-1-1H29a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,38.05,23Zm28,0V15a1,1,0,0,0-1-1h-8a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,66.08,23Zm-15,1h-8a1,1,0,0,1-1-1V15a1,1,0,0,1,1-1h8a1,1,0,0,1,1,1v8A1,1,0,0,1,51.06,24Z"/><rect class="cls-1" x="14.02" y="28.03" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="28.03" y="56.06" width="10.02" height="10.02" rx="1"/><path class="cls-4" d="M37.05,52.06H29a1,1,0,0,1-1-1v-8a1,1,0,0,1,1-1h8a1,1,0,0,1,1,1v8A1,1,0,0,1,37.05,52.06Zm-14,0H15a1,1,0,0,1-1-1v-8a1,1,0,0,1,1-1h8a1,1,0,0,1,1,1v8A1,1,0,0,1,23,52.06Zm-13,13v-8a1,1,0,0,0-1-1H1a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1H9A1,1,0,0,0,10,65.08Zm42,0v-8a1,1,0,0,0-1-1h-8a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,52.06,65.08Zm14,0v-8a1,1,0,0,0-1-1h-8a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h8A1,1,0,0,0,66.08,65.08Z"/><rect class="cls-1" y="70.08" width="10.02" height="10.02" rx="1"/><rect class="cls-1" x="56.06" y="70.08" width="10.02" height="10.02" rx="1"/><rect class="cls-2" x="70.08" y="70.08" width="10.02" height="10.02" rx="1"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,14 +0,0 @@
<div class="heatmap-wrapper">
<div class="heatmap-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.dataWarning">
<span class="small" bs-tooltip="ctrl.dataWarning.tip">{{ctrl.dataWarning.title}}</span>
</div>
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div>
<div class="heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
<heatmap-legend></heatmap-legend>
</div>
</div>
<div class="clearfix"></div>

View File

@ -1,4 +0,0 @@
import './color_legend';
import { HeatmapCtrl } from './heatmap_ctrl';
export { HeatmapCtrl as PanelCtrl };

View File

@ -1,22 +0,0 @@
{
"type": "panel",
"name": "Heatmap (legacy)",
"id": "heatmap-old",
"state": "deprecated",
"info": {
"description": "Legacy heatmap panel based on angular, d3, and flot",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
},
"logos": {
"small": "img/icn-heatmap-panel.svg",
"large": "img/icn-heatmap-panel.svg"
},
"links": [
{ "name": "Brendan Gregg - Heatmaps", "url": "http://www.brendangregg.com/heatmaps.html" },
{ "name": "Brendan Gregg - Latency Heatmaps", "url": " http://www.brendangregg.com/HeatMaps/latency.html" }
]
}
}

View File

@ -1,865 +0,0 @@
import * as d3 from 'd3';
import $ from 'jquery';
import { find, isEmpty, isNaN, isNil, isString, map, max, min, toNumber } from 'lodash';
import {
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';
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;
export default function rendering(scope: any, elem: any, attrs: any, ctrl: any) {
return new HeatmapRenderer(scope, elem, attrs, ctrl);
}
export class HeatmapRenderer {
width = 200;
height = 200;
yScale: any;
xScale: any;
chartWidth = 0;
chartHeight = 0;
chartTop = 0;
chartBottom = 0;
yAxisWidth = 0;
xAxisHeight = 0;
cardPadding = 0;
cardRound = 0;
cardWidth = 0;
cardHeight = 0;
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;
hoverEvent: LegacyGraphHoverEvent;
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(PanelEvents.render, this.onRender.bind(this));
this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
/////////////////////////////
// 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);
// 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));
this.hoverEvent = new LegacyGraphHoverEvent({ pos: {}, point: {}, panel: this.panel });
}
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 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;
})
);
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 format = graphTimeFormat(ticks, this.timeRange.from.valueOf(), this.timeRange.to.valueOf());
const timeZone = this.ctrl.dashboard.getTimezone();
const formatter = (date: d3.AxisDomain) =>
dateTimeFormat(date.valueOf(), {
format: format,
timeZone: timeZone,
});
const xAxis = d3
.axisBottom(this.xScale)
.ticks(ticks)
.tickFormat(formatter)
.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;
let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX);
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(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));
this.data.tsBucketsFormatted = tsBucketsFormatted;
const yAxis = d3
.axisLeft(this.yScale)
.tickFormat(tickFormatter(this.getPanelYAxisWidth()))
.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
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 {
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;
};
}
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.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, {
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 config.theme2.visualization.getColorByName(this.panel.color.cardColor);
} 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() {
this.ctrl.dashboard.events.publish(new LegacyGraphHoverClearEvent());
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
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);
}
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;
}
private getPanelYAxisWidth(): number | null {
if (!this.panel.yAxis.width) {
return null;
}
return isNaN(this.panel.yAxis.width) ? null : parseInt(this.panel.yAxis.width, 10);
}
}

View File

@ -1,93 +0,0 @@
import { dateTime } from '@grafana/data';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { HeatmapCtrl } from '../heatmap_ctrl';
describe('HeatmapCtrl', () => {
const ctx = {} as any;
const $injector = {
get: () => {},
};
HeatmapCtrl.prototype.panel = {
events: {
on: () => {},
emit: () => {},
},
};
const $scope = {
$on: () => {},
$parent: {
panel: HeatmapCtrl.prototype.panel,
dashboard: {},
},
};
beforeEach(() => {
//@ts-ignore
ctx.ctrl = new HeatmapCtrl($scope, $injector, {} as TimeSrv);
});
describe('when time series are outside range', () => {
beforeEach(() => {
const data: any = [
{
target: 'test.cpu1',
datapoints: [
[45, 1234567890],
[60, 1234567899],
],
},
];
ctx.ctrl.range = { from: dateTime().valueOf(), to: dateTime().valueOf() };
ctx.ctrl.onSnapshotLoad(data);
});
it('should set datapointsOutside', () => {
expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
});
});
describe('when time series are inside range', () => {
beforeEach(() => {
const range = {
from: dateTime().subtract(1, 'days').valueOf(),
to: dateTime().valueOf(),
};
const data: any = [
{
target: 'test.cpu1',
datapoints: [
[45, range.from + 1000],
[60, range.from + 10000],
],
},
];
ctx.ctrl.range = range;
ctx.ctrl.onSnapshotLoad(data);
});
it('should set datapointsOutside', () => {
expect(ctx.ctrl.dataWarning).toBe(null);
});
});
describe('datapointsCount given 2 series', () => {
beforeEach(() => {
const data: any = [
{ target: 'test.cpu1', datapoints: [] },
{ target: 'test.cpu2', datapoints: [] },
];
ctx.ctrl.onSnapshotLoad(data);
});
it('should set datapointsCount warning', () => {
expect(ctx.ctrl.dataWarning.title).toBe('No data points');
});
});
});

View File

@ -1,397 +0,0 @@
import { cloneDeep, each, map } from 'lodash';
import TimeSeries from 'app/core/time_series2';
import {
convertToHeatMap,
convertToCards,
histogramToHeatmap,
calculateBucketSize,
isHeatmapDataEqual,
} from '../heatmap_data_converter';
import { HeatmapData } from '../types';
describe('isHeatmapDataEqual', () => {
const ctx: any = {};
beforeEach(() => {
ctx.heatmapA = {
'1422774000000': {
x: 1422774000000,
buckets: {
'1': { y: 1, values: [1, 1.5] },
'2': { y: 2, values: [1] },
},
},
};
ctx.heatmapB = {
'1422774000000': {
x: 1422774000000,
buckets: {
'1': { y: 1, values: [1.5, 1] },
'2': { y: 2, values: [1] },
},
},
};
});
it('should proper compare objects', () => {
const heatmapC = cloneDeep(ctx.heatmapA);
heatmapC['1422774000000'].buckets['1'].values = [1, 1.5];
const heatmapD = cloneDeep(ctx.heatmapA);
heatmapD['1422774000000'].buckets['1'].values = [1.5, 1, 1.6];
const heatmapE = cloneDeep(ctx.heatmapA);
heatmapE['1422774000000'].buckets['1'].values = [1, 1.6];
const empty = {};
const emptyValues = cloneDeep(ctx.heatmapA);
emptyValues['1422774000000'].buckets['1'].values = [];
expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).toBe(true);
expect(isHeatmapDataEqual(ctx.heatmapB, ctx.heatmapA)).toBe(true);
expect(isHeatmapDataEqual(ctx.heatmapA, heatmapC)).toBe(true);
expect(isHeatmapDataEqual(heatmapC, ctx.heatmapA)).toBe(true);
expect(isHeatmapDataEqual(ctx.heatmapA, heatmapD)).toBe(false);
expect(isHeatmapDataEqual(heatmapD, ctx.heatmapA)).toBe(false);
expect(isHeatmapDataEqual(ctx.heatmapA, heatmapE)).toBe(false);
expect(isHeatmapDataEqual(heatmapE, ctx.heatmapA)).toBe(false);
expect(isHeatmapDataEqual(empty, ctx.heatmapA)).toBe(false);
expect(isHeatmapDataEqual(ctx.heatmapA, empty)).toBe(false);
expect(isHeatmapDataEqual(emptyValues, ctx.heatmapA)).toBe(false);
expect(isHeatmapDataEqual(ctx.heatmapA, emptyValues)).toBe(false);
});
});
describe('calculateBucketSize', () => {
const ctx: any = {};
describe('when logBase is 1 (linear scale)', () => {
beforeEach(() => {
ctx.logBase = 1;
ctx.bounds_set = [
{ bounds: [], size: 0 },
{ bounds: [0], size: 0 },
{ bounds: [4], size: 4 },
{ bounds: [0, 1, 2, 3, 4], size: 1 },
{ bounds: [0, 1, 3, 5, 7], size: 1 },
{ bounds: [0, 3, 7, 9, 15], size: 2 },
{ bounds: [0, 7, 3, 15, 9], size: 2 },
{ bounds: [0, 5, 10, 15, 50], size: 5 },
];
});
it('should properly calculate bucket size', () => {
each(ctx.bounds_set, (b) => {
const bucketSize = calculateBucketSize(b.bounds, ctx.logBase);
expect(bucketSize).toBe(b.size);
});
});
});
describe('when logBase is 2', () => {
beforeEach(() => {
ctx.logBase = 2;
ctx.bounds_set = [
{ bounds: [], size: 0 },
{ bounds: [0], size: 0 },
{ bounds: [4], size: 4 },
{ bounds: [1, 2, 4, 8], size: 1 },
{ bounds: [1, Math.SQRT2, 2, 8, 16], size: 0.5 },
];
});
it('should properly calculate bucket size', () => {
each(ctx.bounds_set, (b) => {
const bucketSize = calculateBucketSize(b.bounds, ctx.logBase);
expect(isEqual(bucketSize, b.size)).toBe(true);
});
});
});
});
describe('HeatmapDataConverter', () => {
const ctx: any = {};
beforeEach(() => {
ctx.series = [];
ctx.series.push(
new TimeSeries({
datapoints: [
[1, 1422774000000],
[1, 1422774000010],
[2, 1422774060000],
],
alias: 'series1',
})
);
ctx.series.push(
new TimeSeries({
datapoints: [
[2, 1422774000000],
[2, 1422774000010],
[3, 1422774060000],
],
alias: 'series2',
})
);
ctx.series.push(
new TimeSeries({
datapoints: [
[5, 1422774000000],
[3, 1422774000010],
[4, 1422774060000],
],
alias: 'series3',
})
);
ctx.xBucketSize = 60000; // 60s
ctx.yBucketSize = 2;
ctx.logBase = 1;
});
describe('when logBase is 1 (linear scale)', () => {
beforeEach(() => {
ctx.logBase = 1;
});
it('should build proper heatmap data', () => {
const expectedHeatmap = {
'1422774000000': {
x: 1422774000000,
buckets: {
'0': {
y: 0,
values: [1, 1],
count: 2,
bounds: { bottom: 0, top: 2 },
},
'2': {
y: 2,
values: [2, 2, 3],
count: 3,
bounds: { bottom: 2, top: 4 },
},
'4': { y: 4, values: [5], count: 1, bounds: { bottom: 4, top: 6 } },
},
},
'1422774060000': {
x: 1422774060000,
buckets: {
'2': {
y: 2,
values: [2, 3],
count: 3,
bounds: { bottom: 2, top: 4 },
},
'4': { y: 4, values: [4], count: 1, bounds: { bottom: 4, top: 6 } },
},
},
};
const heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase);
expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true);
});
});
describe.skip('when logBase is 2', () => {
beforeEach(() => {
ctx.logBase = 2;
});
it('should build proper heatmap data', () => {
const expectedHeatmap = {
'1422774000000': {
x: 1422774000000,
buckets: {
'1': { y: 1, values: [1] },
'2': { y: 2, values: [2] },
},
},
'1422774060000': {
x: 1422774060000,
buckets: {
'2': { y: 2, values: [2, 3] },
},
},
};
const heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase);
expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true);
});
});
});
describe('Histogram converter', () => {
const ctx: any = {};
beforeEach(() => {
ctx.series = [];
ctx.series.push(
new TimeSeries({
datapoints: [
[1, 1422774000000],
[0, 1422774060000],
],
alias: '1',
label: '1',
})
);
ctx.series.push(
new TimeSeries({
datapoints: [
[5, 1422774000000],
[3, 1422774060000],
],
alias: '2',
label: '2',
})
);
ctx.series.push(
new TimeSeries({
datapoints: [
[0, 1422774000000],
[1, 1422774060000],
],
alias: '3',
label: '3',
})
);
});
describe('when converting histogram', () => {
beforeEach(() => {});
it('should build proper heatmap data', () => {
const expectedHeatmap: HeatmapData = {
'1422774000000': {
x: 1422774000000,
buckets: {
'0': {
y: 0,
count: 1,
bounds: { bottom: 0, top: null },
values: [],
points: [],
},
'1': {
y: 1,
count: 5,
bounds: { bottom: 1, top: null },
values: [],
points: [],
},
'2': {
y: 2,
count: 0,
bounds: { bottom: 2, top: null },
values: [],
points: [],
},
},
},
'1422774060000': {
x: 1422774060000,
buckets: {
'0': {
y: 0,
count: 0,
bounds: { bottom: 0, top: null },
values: [],
points: [],
},
'1': {
y: 1,
count: 3,
bounds: { bottom: 1, top: null },
values: [],
points: [],
},
'2': {
y: 2,
count: 1,
bounds: { bottom: 2, top: null },
values: [],
points: [],
},
},
},
};
const heatmap = histogramToHeatmap(ctx.series);
expect(heatmap).toEqual(expectedHeatmap);
});
it('should use bucket index as a bound', () => {
const heatmap = histogramToHeatmap(ctx.series);
const bucketLabels = map(heatmap['1422774000000'].buckets, (b, label) => label);
const bucketYs = map(heatmap['1422774000000'].buckets, 'y');
const bucketBottoms = map(heatmap['1422774000000'].buckets, (b) => b.bounds.bottom);
const expectedBounds = [0, 1, 2];
expect(bucketLabels).toEqual(map(expectedBounds, (b) => b.toString()));
expect(bucketYs).toEqual(expectedBounds);
expect(bucketBottoms).toEqual(expectedBounds);
});
});
});
describe('convertToCards', () => {
let buckets: HeatmapData = {};
beforeEach(() => {
buckets = {
'1422774000000': {
x: 1422774000000,
buckets: {
'1': { y: 1, values: [1], count: 1, bounds: {} },
'2': { y: 2, values: [2], count: 1, bounds: {} },
},
},
'1422774060000': {
x: 1422774060000,
buckets: {
'2': { y: 2, values: [2, 3], count: 2, bounds: {} },
},
},
};
});
it('should build proper cards data', () => {
const expectedCards = [
{ x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {} },
{ x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {} },
{ x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {} },
];
const res = convertToCards(buckets);
expect(res.cards).toMatchObject(expectedCards);
});
it('should build proper cards stats', () => {
const expectedStats = { min: 1, max: 2 };
const res = convertToCards(buckets);
expect(res.cardStats).toMatchObject(expectedStats);
});
});
/**
* Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss.
* @param a
* @param b
* @param precision
*/
function isEqual(a: number, b: number, precision = 0.000001): boolean {
if (a === b) {
return true;
} else {
return Math.abs(1 - a / b) <= precision;
}
}

View File

@ -1,42 +0,0 @@
export interface Bucket {
[x: string]: {
y: any;
bounds: any;
values: any[];
points?: any[];
count: number;
};
}
export interface XBucket {
x: number;
buckets: any;
}
export interface YBucket {
y: number;
values: number[];
}
export interface HeatmapCard {
x: number;
y: number;
yBounds: {
top: number | null;
bottom: number | null;
};
values: number[];
count: number;
}
export interface HeatmapCardStats {
min: number;
max: number;
}
export interface HeatmapData {
[key: string]: {
x: number;
buckets: Bucket;
};
}