| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- import { MetricsPanelCtrl } from 'app/plugins/sdk';
- import _ from 'lodash';
- import kbn from 'app/core/utils/kbn';
- import TimeSeries from 'app/core/time_series2';
- import { axesEditor } from './axes_editor';
- import { heatmapDisplayEditor } from './display_editor';
- import rendering from './rendering';
- import {
- convertToHeatMap,
- convertToCards,
- histogramToHeatmap,
- calculateBucketSize,
- sortSeriesByLabel,
- } from './heatmap_data_converter';
- import { auto } from 'angular';
- import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
- 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: '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: any[];
- timeSrv: any;
- dataWarning: any;
- decimals: number;
- scaledDecimals: number;
- /** @ngInject */
- constructor($scope: any, $injector: auto.IInjectorService, timeSrv: TimeSrv) {
- super($scope, $injector);
- this.timeSrv = timeSrv;
- this.selectionActivated = false;
- _.defaultsDeep(this.panel, panelDefaults);
- this.opacityScales = opacityScales;
- this.colorModes = colorModes;
- this.colorSchemes = colorSchemes;
- // Bind grafana panel events
- this.events.on('render', this.onRender.bind(this));
- this.events.on('data-received', this.onDataReceived.bind(this));
- this.events.on('data-error', this.onDataError.bind(this));
- this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
- this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
- this.onCardColorChange = this.onCardColorChange.bind(this);
- }
- onInitEditMode() {
- this.addEditorTab('Axes', axesEditor, 2);
- this.addEditorTab('Display', heatmapDisplayEditor, 3);
- this.unitFormats = kbn.getUnitFormats();
- }
- zoomOut(evt: any) {
- this.publishAppEvent('zoom-out', 2);
- }
- onRender() {
- if (!this.range || !this.series) {
- return;
- }
- if (this.panel.dataFormat === 'tsbuckets') {
- this.convertHistogramToHeatmapData();
- } else {
- this.convertTimeSeriesToHeatmapData();
- }
- }
- convertTimeSeriesToHeatmapData() {
- 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.interval_regex.test(this.panel.xBucketSize);
- if (isIntervalString) {
- xBucketSize = kbn.interval_to_ms(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() {
- 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.meta && this.datasource.meta.id) {
- return this.datasource.meta.id;
- } else {
- return 'unknown';
- }
- }
- onDataReceived(dataList: any) {
- this.series = dataList.map(this.seriesHandler.bind(this));
- 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();
- }
- seriesHandler(dataFrame: any) {
- if (dataFrame.datapoints === undefined) {
- throw new Error('Heatmap error: data should be a time series');
- }
- const series = new TimeSeries({
- datapoints: dataFrame.datapoints,
- alias: dataFrame.target,
- });
- series.flotpairs = series.getFlotPairs(this.panel.nullPointMode);
- const datapoints = dataFrame.datapoints || [];
- if (datapoints && datapoints.length > 0) {
- const last = datapoints[datapoints.length - 1][1];
- const from = this.range.from;
- if (last - from.valueOf() < -10000) {
- series.isOutsideRange = true;
- }
- }
- return series;
- }
- parseSeries(series: any[]) {
- 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: any[]) {
- 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);
- }
- }
|