mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Heatmap: Remove legacy angular based implementation (#59249)
remove angular heatmap
This commit is contained in:
parent
183b279274
commit
0a7a5b13fe
@ -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"],
|
||||
|
@ -59,7 +59,6 @@ export interface FeatureToggles {
|
||||
prometheusWideSeries?: boolean;
|
||||
canvasPanelNesting?: boolean;
|
||||
scenes?: boolean;
|
||||
useLegacyHeatmapPanel?: boolean;
|
||||
disableSecretsCompatibility?: boolean;
|
||||
logRequestsInstrumentedAsUnknown?: boolean;
|
||||
dataConnectionsConsole?: boolean;
|
||||
|
@ -164,7 +164,6 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *store.Serv
|
||||
"gettingstarted": {},
|
||||
"graph": {},
|
||||
"heatmap": {},
|
||||
"heatmap-old": {},
|
||||
"histogram": {},
|
||||
"icon": {},
|
||||
"live": {},
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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/)
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -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));
|
||||
};
|
||||
}
|
||||
}
|
@ -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 |
@ -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>
|
@ -1,4 +0,0 @@
|
||||
import './color_legend';
|
||||
import { HeatmapCtrl } from './heatmap_ctrl';
|
||||
|
||||
export { HeatmapCtrl as PanelCtrl };
|
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user