Files
grafana-statusmap/src/status_heatmap_ctrl.js
T

333 lines
9.9 KiB
JavaScript

import { MetricsPanelCtrl } from 'app/plugins/sdk';
import _ from 'lodash';
import { contextSrv } from 'app/core/core';
import kbn from 'app/core/utils/kbn';
import './color_legend';
import rendering from './rendering';
// import aggregates, { aggregatesMap } from './aggregates';
// import fragments, { fragmentsMap } from './fragments';
// import { labelFormats } from './xAxisLabelFormats';
import {statusHeatmapOptionsEditor} from './options_editor';
import {ColorModeDiscrete} from "./color_mode_discrete";
const CANVAS = 'CANVAS';
const SVG = 'SVG';
const VALUE_INDEX = 0,
TIME_INDEX = 1;
const panelDefaults = {
// aggregate: aggregates.AVG,
// fragment: fragments.HOUR,
color: {
mode: 'spectrum',
cardColor: '#b4ff00',
colorScale: 'sqrt',
exponent: 0.5,
colorScheme: 'interpolateGnYlRd',
// discrete mode settings
defaultColor: '#757575',
thresholds: [] // manual colors
},
cards: {
cardMinWidth: 5,
cardVSpacing: 2,
cardHSpacing: 2,
cardRound: null
},
xAxis: {
show: true,
showWeekends: true,
minBucketWidthToShowWeekends: 4,
showCrosshair: true,
labelFormat: '%a %m/%d'
},
yAxis: {
show: true,
showCrosshair: false
},
tooltip: {
show: true
},
legend: {
show: true
},
data: {
unitFormat: 'short',
decimals: null
},
// how null points should be handled
nullPointMode: 'as empty',
yAxisSort: 'metrics',
highlightCards: true,
useMax: true
};
const renderer = CANVAS;
const colorSchemes = [
// Diverging
{ name: 'Spectral', value: 'interpolateSpectral', invert: 'always' },
{ name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always' },
{ name: 'GnYlRd', value: 'interpolateGnYlRd', 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: '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' }
];
let colorModes = ['opacity', 'spectrum', 'discrete'];
let opacityScales = ['linear', 'sqrt'];
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
constructor($scope, $injector, $rootScope, timeSrv) {
super($scope, $injector);
_.defaultsDeep(this.panel, panelDefaults);
this.opacityScales = opacityScales;
this.colorModes = colorModes;
this.colorSchemes = colorSchemes;
// default graph width for discrete card width calculation
this.graph = {
"chartWidth" : -1
};
this.multipleValues = false;
this.noColorDefined = false;
this.discreteHelper = new ColorModeDiscrete($scope);
this.dataWarnings = {
"noColorDefined": {
title: 'Data has value with undefined color',
tip: 'Check metric values, color values or define a new color',
},
"multipleValues": {
title: 'Data has multiple values for one target',
tip: 'Change targets definitions or set "use max value"',
}
};
this.events.on('data-received', this.onDataReceived);
this.events.on('data-snapshot-load', this.onDataReceived);
this.events.on('data-error', this.onDataError);
this.events.on('init-edit-mode', this.onInitEditMode);
this.events.on('render', this.onRender);
this.events.on('refresh', this.postRefresh);
// custom event from rendering.js
this.events.on('render-complete', this.onRenderComplete);
}
onRenderComplete = (data) => {
this.graph.chartWidth = data.chartWidth;
this.renderingCompleted();
};
// override calculateInterval for discrete color mode
calculateInterval = () => {
let panelWidth = Math.ceil($(window).width() * (this.panel.gridPos.w / 24));
// approximate chartWidth because y axis ticks not rendered yet on first data receive.
let chartWidth = _.max([
panelWidth - 200,
panelWidth/2
]);
let minCardWidth = this.panel.cards.cardMinWidth;
let minSpacing = this.panel.cards.cardHSpacing;
let maxCardsCount = Math.ceil((chartWidth-minCardWidth) / (minCardWidth + minSpacing));
let intervalMs;
let rangeMs = this.range.to.valueOf() - this.range.from.valueOf();
// this is minimal interval! kbn.round_interval will lower it.
intervalMs = this.discreteHelper.roundIntervalCeil(rangeMs / maxCardsCount);
// Calculate low limit of interval
let lowLimitMs = 1; // 1 millisecond default low limit
let intervalOverride = this.panel.interval;
// if no panel interval check datasource
if (intervalOverride) {
intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars);
} else if (this.datasource && this.datasource.interval) {
intervalOverride = this.datasource.interval;
}
if (intervalOverride) {
if (intervalOverride[0] === '>') {
intervalOverride = intervalOverride.slice(1);
}
lowLimitMs = kbn.interval_to_ms(intervalOverride);
}
if (lowLimitMs > intervalMs) {
intervalMs = lowLimitMs;
}
this.intervalMs = intervalMs;
this.interval = kbn.secondsToHms(intervalMs / 1000);
};
onDataReceived = (dataList) => {
this.data = dataList;
this.cardsData = this.convertToCards(this.data);
this.render();
};
onInitEditMode = () => {
this.addEditorTab('Options', statusHeatmapOptionsEditor, 2);
this.unitFormats = kbn.getUnitFormats();
};
onRender = () => {
if (!this.data) { return; }
this.multipleValues = false;
if (!this.panel.useMax) {
if (this.cardsData) {
this.multipleValues = this.cardsData.multipleValues;
}
}
this.noColorDefined = false;
if (this.panel.color.mode === 'discrete') {
this.discreteHelper.updateCardsValuesHasColorInfo();
if (this.cardsData) {
this.noColorDefined = this.cardsData.noColorDefined;
}
}
};
onCardColorChange = (newColor) => {
this.panel.color.cardColor = newColor;
this.render();
};
onDataError = () => {
this.data = [];
this.render();
};
postRefresh = () => {
this.noColorDefined = false;
};
onEditorAddThreshold = () => {
this.panel.color.thresholds.push({ color: this.panel.defaultColor });
this.render();
};
onEditorRemoveThreshold = (index) => {
this.panel.color.thresholds.splice(index, 1);
this.render();
};
onEditorAddThreeLights = () => {
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
this.render();
};
link = (scope, elem, attrs, ctrl) => {
rendering(scope, elem, attrs, ctrl);
};
// group values into buckets by target
convertToCards = (data) => {
let cardsData = {
cards: [],
xBucketSize: 0,
yBucketSize: 0,
maxValue: 0,
minValue: 0,
multipleValues: false,
noColorDefined: false,
};
if (!data || data.length == 0) { return cardsData;}
// collect uniq targets and their indexes in data array
cardsData.targetIndex = {};
for (let i = 0; i < data.length; i++) {
let ts = data[i];
let target = ts.target;
if (cardsData.targetIndex[target] == undefined) {
cardsData.targetIndex[target] = []
}
cardsData.targetIndex[target].push(i);
}
// TODO add some logic for targets heirarchy
cardsData.targets = _.keys(cardsData.targetIndex);
cardsData.yBucketSize = cardsData.targets.length;
cardsData.xBucketSize = _.min(_.map(data, d => d.datapoints.length));
// Collect all values for each bucket from datapoints with similar target.
for(let i = 0; i < cardsData.targets.length; i++) {
let target = cardsData.targets[i];
for (let j = 0; j < cardsData.xBucketSize; j++) {
let card = {
id: i*cardsData.xBucketSize + j,
values: [],
multipleValues: false,
noColorDefined: false,
};
// collect values from all timeseries with target
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
let s = data[cardsData.targetIndex[target][si]];
let datapoint = s.datapoints[j];
if (card.values.length === 0) {
card.x = datapoint[TIME_INDEX];
card.y = s.target;
}
card.values.push(datapoint[VALUE_INDEX]);
}
card.minValue = _.min(card.values);
card.maxValue = _.max(card.values);
if (card.values.length > 1) {
cardsData.multipleValues = true;
card.multipleValues = true;
card.value = card.maxValue; // max value by default
} else {
card.value = card.maxValue; // max value by default
}
if (cardsData.maxValue < card.maxValue)
cardsData.maxValue = card.maxValue;
if (cardsData.minValue > card.minValue)
cardsData.minValue = card.minValue;
cardsData.cards.push(card);
}
}
return cardsData;
};
}