mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Move heatmap panel into core grafana.
This commit is contained in:
parent
44c9ba2edf
commit
68f5e75eba
5
public/app/headers/common.d.ts
vendored
5
public/app/headers/common.d.ts
vendored
@ -67,3 +67,8 @@ declare module 'remarkable' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'd3' {
|
||||
var d3: any;
|
||||
export default d3;
|
||||
}
|
||||
|
0
public/app/plugins/panel/heatmap/README.md
Normal file
0
public/app/plugins/panel/heatmap/README.md
Normal file
43
public/app/plugins/panel/heatmap/axes_editor.ts
Normal file
43
public/app/plugins/panel/heatmap/axes_editor.ts
Normal file
@ -0,0 +1,43 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export class AxesEditorCtrl {
|
||||
panel: any;
|
||||
panelCtrl: any;
|
||||
unitFormats: any;
|
||||
logScales: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
$scope.editor = this;
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
|
||||
this.unitFormats = kbn.getUnitFormats();
|
||||
|
||||
this.logScales = {
|
||||
'linear': 1,
|
||||
'log (base 2)': 2,
|
||||
'log (base 10)': 10,
|
||||
'log (base 32)': 32,
|
||||
'log (base 1024)': 1024
|
||||
};
|
||||
}
|
||||
|
||||
setUnitFormat(subItem) {
|
||||
this.panel.yAxis.format = subItem.value;
|
||||
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,
|
||||
};
|
||||
}
|
70
public/app/plugins/panel/heatmap/css/heatmap.dark.css
Normal file
70
public/app/plugins/panel/heatmap/css/heatmap.dark.css
Normal file
@ -0,0 +1,70 @@
|
||||
.axis text {
|
||||
}
|
||||
|
||||
.axis {
|
||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: smaller;
|
||||
fill: #D8D9DA;
|
||||
}
|
||||
|
||||
.axis path,
|
||||
.axis line {
|
||||
fill: none;
|
||||
stroke: #7B7B7B;
|
||||
/*shape-rendering: crispEdges;*/
|
||||
}
|
||||
|
||||
.axis .domain {
|
||||
/*opacity: 0;*/
|
||||
}
|
||||
|
||||
.tick line {
|
||||
opacity: 0.4;
|
||||
stroke: #7B7B7B;
|
||||
}
|
||||
|
||||
.tick text {
|
||||
fill: #D8D9DA;
|
||||
}
|
||||
|
||||
.heatmap-panel {
|
||||
cursor: crosshair;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.heatmap-tooltip {
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
min-width: 160px;
|
||||
padding: 12px;
|
||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
background: #141414;
|
||||
border: 0px;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-highlighted:hover {
|
||||
stroke: #D8D9DA;
|
||||
}
|
||||
|
||||
rect.heatmap-card {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.heatmap-histogram rect {
|
||||
fill: #828282;
|
||||
}
|
||||
|
||||
.heatmap-crosshair line {
|
||||
stroke: #9a1010;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.heatmap-selection {
|
||||
stroke-width: 1;
|
||||
opacity: 0.3;
|
||||
fill: #828282;
|
||||
stroke: #D8D9DA;
|
||||
}
|
58
public/app/plugins/panel/heatmap/css/heatmap.light.css
Normal file
58
public/app/plugins/panel/heatmap/css/heatmap.light.css
Normal file
@ -0,0 +1,58 @@
|
||||
.axis {
|
||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: smaller;
|
||||
fill: #555555;
|
||||
}
|
||||
|
||||
.axis path,
|
||||
.axis line {
|
||||
fill: none;
|
||||
stroke: #D8D9DA;
|
||||
/*shape-rendering: crispEdges;*/
|
||||
}
|
||||
|
||||
.tick line {
|
||||
opacity: 0.4;
|
||||
stroke: #D8D9DA;
|
||||
}
|
||||
|
||||
.tick text {
|
||||
fill: #555555;
|
||||
}
|
||||
|
||||
.heatmap-panel {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
div.heatmap-tooltip {
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
min-width: 160px;
|
||||
padding: 12px;
|
||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
background: #ECECEC;
|
||||
border: 0px;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-highlighted:hover {
|
||||
stroke: #D8D9DA;
|
||||
}
|
||||
|
||||
.heatmap-histogram rect {
|
||||
fill: #555555;
|
||||
}
|
||||
|
||||
.heatmap-crosshair line {
|
||||
stroke: #a25959;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.heatmap-selection {
|
||||
stroke-width: 1;
|
||||
opacity: 0.3;
|
||||
fill: #555555;
|
||||
stroke: #000;
|
||||
}
|
26
public/app/plugins/panel/heatmap/display_editor.ts
Normal file
26
public/app/plugins/panel/heatmap/display_editor.ts
Normal file
@ -0,0 +1,26 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
export class HeatmapDisplayEditorCtrl {
|
||||
panel: any;
|
||||
panelCtrl: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
$scope.editor = this;
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function heatmapDisplayEditor() {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
templateUrl: 'public/app/plugins/panel/heatmap/partials/display_editor.html',
|
||||
controller: HeatmapDisplayEditorCtrl,
|
||||
};
|
||||
}
|
220
public/app/plugins/panel/heatmap/heatmap_ctrl.ts
Normal file
220
public/app/plugins/panel/heatmap/heatmap_ctrl.ts
Normal file
@ -0,0 +1,220 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import TimeSeries from 'app/core/time_series';
|
||||
import {axesEditor} from './axes_editor';
|
||||
import {heatmapDisplayEditor} from './display_editor';
|
||||
import rendering from './rendering';
|
||||
import {convertToHeatMap, getMinLog} from './heatmap_data_converter';
|
||||
|
||||
let X_BUCKET_NUMBER_DEFAULT = 30;
|
||||
let Y_BUCKET_NUMBER_DEFAULT = 10;
|
||||
|
||||
let panelDefaults = {
|
||||
heatmap: {
|
||||
},
|
||||
cards: {
|
||||
cardPadding: null,
|
||||
cardRound: null
|
||||
},
|
||||
color: {
|
||||
mode: 'color',
|
||||
cardColor: '#b4ff00',
|
||||
colorScale: 'linear',
|
||||
exponent: 0.5,
|
||||
colorScheme: 'interpolateSpectral',
|
||||
fillBackground: false
|
||||
},
|
||||
xBucketSize: null,
|
||||
xBucketNumber: null,
|
||||
yBucketSize: null,
|
||||
yBucketNumber: null,
|
||||
xAxis: {
|
||||
show: true
|
||||
},
|
||||
yAxis: {
|
||||
show: true,
|
||||
format: 'short',
|
||||
decimals: null,
|
||||
logBase: 1,
|
||||
splitFactor: null,
|
||||
min: null,
|
||||
max: null,
|
||||
removeZeroValues: false
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
seriesStat: false,
|
||||
showHistogram: false
|
||||
},
|
||||
highlightCards: true
|
||||
};
|
||||
|
||||
let colorModes = ['opacity', 'color'];
|
||||
let opacityScales = ['linear', 'sqrt'];
|
||||
|
||||
// Schemes from d3-scale-chromatic
|
||||
// https://github.com/d3/d3-scale-chromatic
|
||||
let colorSchemes = [
|
||||
// Diverging
|
||||
{name: 'Spectral', value: 'interpolateSpectral'},
|
||||
{name: 'BrBG', value: 'interpolateBrBG'},
|
||||
{name: 'PRGn', value: 'interpolatePRGn'},
|
||||
{name: 'PiYG', value: 'interpolatePiYG'},
|
||||
{name: 'PuOr', value: 'interpolatePuOr'},
|
||||
{name: 'RdBu', value: 'interpolateRdBu'},
|
||||
{name: 'RdGy', value: 'interpolateRdGy'},
|
||||
{name: 'RdYlBu', value: 'interpolateRdYlBu'},
|
||||
{name: 'RdYlGn', value: 'interpolateRdYlGn'},
|
||||
|
||||
// Sequential (Single Hue)
|
||||
{name: 'Blues', value: 'interpolateBlues'},
|
||||
{name: 'Greens', value: 'interpolateGreens'},
|
||||
{name: 'Greys', value: 'interpolateGreys'},
|
||||
{name: 'Oranges', value: 'interpolateOranges'},
|
||||
{name: 'Purples', value: 'interpolatePurples'},
|
||||
{name: 'Reds', value: 'interpolateReds'},
|
||||
|
||||
// Sequential (Multi-Hue)
|
||||
{name: 'BuGn', value: 'interpolateBuGn'},
|
||||
{name: 'BuPu', value: 'interpolateBuPu'},
|
||||
{name: 'GnBu', value: 'interpolateGnBu'},
|
||||
{name: 'OrRd', value: 'interpolateOrRd'},
|
||||
{name: 'PuBuGn', value: 'interpolatePuBuGn'},
|
||||
{name: 'PuBu', value: 'interpolatePuBu'},
|
||||
{name: 'PuRd', value: 'interpolatePuRd'},
|
||||
{name: 'RdPu', value: 'interpolateRdPu'},
|
||||
{name: 'YlGnBu', value: 'interpolateYlGnBu'},
|
||||
{name: 'YlGn', value: 'interpolateYlGn'},
|
||||
{name: 'YlOrBr', value: 'interpolateYlOrBr'},
|
||||
{name: 'YlOrRd', value: 'interpolateYlOrRd'}
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
constructor($scope, $injector, private $rootScope, timeSrv) {
|
||||
super($scope, $injector);
|
||||
this.$rootScope = $rootScope;
|
||||
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));
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
this.addEditorTab('Axes', axesEditor, 2);
|
||||
this.addEditorTab('Display', heatmapDisplayEditor, 3);
|
||||
this.unitFormats = kbn.getUnitFormats();
|
||||
}
|
||||
|
||||
zoomOut(evt) {
|
||||
this.publishAppEvent('zoom-out', 2);
|
||||
}
|
||||
|
||||
onRender() {
|
||||
if (!this.range) { return; }
|
||||
|
||||
let xBucketSize, yBucketSize;
|
||||
let logBase = this.panel.yAxis.logBase;
|
||||
let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT;
|
||||
let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber);
|
||||
|
||||
// Parse X bucket size (number or interval)
|
||||
let 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
|
||||
let heatmapStats = this.parseSeries(this.series);
|
||||
let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT;
|
||||
if (logBase !== 1) {
|
||||
yBucketSize = this.panel.yAxis.splitFactor;
|
||||
} else {
|
||||
if (heatmapStats.max === heatmapStats.min) {
|
||||
yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT;
|
||||
} else {
|
||||
yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber;
|
||||
}
|
||||
yBucketSize = this.panel.yBucketSize || yBucketSize;
|
||||
}
|
||||
|
||||
let 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;
|
||||
}
|
||||
|
||||
this.data = {
|
||||
buckets: bucketsData,
|
||||
heatmapStats: heatmapStats,
|
||||
xBucketSize: xBucketSize,
|
||||
yBucketSize: yBucketSize
|
||||
};
|
||||
}
|
||||
|
||||
onDataReceived(dataList) {
|
||||
this.series = dataList.map(this.seriesHandler.bind(this));
|
||||
this.render();
|
||||
}
|
||||
|
||||
onDataError() {
|
||||
this.series = [];
|
||||
this.render();
|
||||
}
|
||||
|
||||
seriesHandler(seriesData) {
|
||||
var series = new TimeSeries({
|
||||
datapoints: seriesData.datapoints,
|
||||
alias: seriesData.target
|
||||
});
|
||||
|
||||
series.flotpairs = series.getFlotPairs(this.panel.nullPointMode);
|
||||
series.minLog = getMinLog(series);
|
||||
return series;
|
||||
}
|
||||
|
||||
parseSeries(series) {
|
||||
let min = _.min(_.map(series, s => s.stats.min));
|
||||
let minLog = _.min(_.map(series, s => s.minLog));
|
||||
let max = _.max(_.map(series, s => s.stats.max));
|
||||
|
||||
return {
|
||||
max: max,
|
||||
min: min,
|
||||
minLog: minLog
|
||||
};
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
rendering(scope, elem, attrs, ctrl);
|
||||
}
|
||||
}
|
357
public/app/plugins/panel/heatmap/heatmap_data_converter.ts
Normal file
357
public/app/plugins/panel/heatmap/heatmap_data_converter.ts
Normal file
@ -0,0 +1,357 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
let VALUE_INDEX = 0;
|
||||
let TIME_INDEX = 1;
|
||||
|
||||
/**
|
||||
* Convert set of time series into heatmap buckets
|
||||
* @return {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(series, yBucketSize, xBucketSize, logBase) {
|
||||
let seriesBuckets = _.map(series, s => {
|
||||
return seriesToHeatMap(s, yBucketSize, xBucketSize, logBase);
|
||||
});
|
||||
|
||||
let buckets = mergeBuckets(seriesBuckets);
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert buckets into linear array of "cards" - objects, represented heatmap elements.
|
||||
* @param {Object} buckets
|
||||
* @return {Array} Array of "card" objects
|
||||
*/
|
||||
function convertToCards(buckets) {
|
||||
let cards = [];
|
||||
_.forEach(buckets, xBucket => {
|
||||
_.forEach(xBucket.buckets, (yBucket, key) => {
|
||||
if (yBucket.values.length) {
|
||||
let card = {
|
||||
x: Number(xBucket.x),
|
||||
y: Number(key),
|
||||
yBounds: yBucket.bounds,
|
||||
values: yBucket.values,
|
||||
seriesStat: getSeriesStat(yBucket.points)
|
||||
};
|
||||
|
||||
cards.push(card);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return {Object} Transformed buckets
|
||||
*/
|
||||
function mergeZeroBuckets(buckets, minValue) {
|
||||
_.forEach(buckets, xBucket => {
|
||||
let yBuckets = xBucket.buckets;
|
||||
|
||||
let emptyBucket = {
|
||||
bounds: {bottom: 0, top: 0},
|
||||
values: [],
|
||||
points: []
|
||||
};
|
||||
|
||||
let nullBucket = yBuckets[0] || emptyBucket;
|
||||
let minBucket = yBuckets[minValue] || emptyBucket;
|
||||
|
||||
let newBucket = {
|
||||
y: 0,
|
||||
bounds: {bottom: minValue, top: minBucket.bounds.top || minValue},
|
||||
values: nullBucket.values.concat(minBucket.values),
|
||||
points: nullBucket.points.concat(minBucket.points)
|
||||
};
|
||||
|
||||
let newYBuckets = {};
|
||||
_.forEach(yBuckets, (bucket, bound) => {
|
||||
bound = Number(bound);
|
||||
if (bound !== 0 && bound !== minValue) {
|
||||
newYBuckets[bound] = bucket;
|
||||
}
|
||||
});
|
||||
newYBuckets[0] = newBucket;
|
||||
xBucket.buckets = newYBuckets;
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove 0 values from heatmap buckets.
|
||||
*/
|
||||
function removeZeroBuckets(buckets) {
|
||||
_.forEach(buckets, xBucket => {
|
||||
let yBuckets = xBucket.buckets;
|
||||
let newYBuckets = {};
|
||||
_.forEach(yBuckets, (bucket, bound) => {
|
||||
if (bucket.y !== 0) {
|
||||
newYBuckets[bound] = bucket;
|
||||
}
|
||||
});
|
||||
xBucket.buckets = newYBuckets;
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count values number for each timeseries in given bucket
|
||||
* @param {Array} points Bucket's datapoints with series name ([val, ts, series_name])
|
||||
* @return {Object} seriesStat: {seriesName_1: val_1, seriesName_2: val_2}
|
||||
*/
|
||||
function getSeriesStat(points) {
|
||||
return _.countBy(points, p => p[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert individual series to heatmap buckets
|
||||
*/
|
||||
function seriesToHeatMap(series, yBucketSize, xBucketSize, logBase = 1) {
|
||||
let datapoints = series.datapoints;
|
||||
let seriesName = series.label;
|
||||
let xBuckets = {};
|
||||
|
||||
// Slice series into X axis buckets
|
||||
// | | ** | | * | **|
|
||||
// | * |* *|* |** *| * |
|
||||
// |** *| | ***| |* |
|
||||
// |____|____|____|____|____|_
|
||||
//
|
||||
_.forEach(datapoints, point => {
|
||||
let bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize);
|
||||
pushToXBuckets(xBuckets, point, bucketBound, seriesName);
|
||||
});
|
||||
|
||||
// Slice X axis buckets into Y (value) buckets
|
||||
// | **| |2|,
|
||||
// | * | --\ |1|,
|
||||
// |* | --/ |1|,
|
||||
// |____| |0|
|
||||
//
|
||||
_.forEach(xBuckets, xBucket => {
|
||||
if (logBase !== 1) {
|
||||
xBucket.buckets = convertToLogScaleValueBuckets(xBucket, yBucketSize, logBase);
|
||||
} else {
|
||||
xBucket.buckets = convertToValueBuckets(xBucket, yBucketSize);
|
||||
}
|
||||
});
|
||||
return xBuckets;
|
||||
}
|
||||
|
||||
function pushToXBuckets(buckets, point, bucketNum, seriesName) {
|
||||
let value = point[VALUE_INDEX];
|
||||
if (value === null || value === undefined || isNaN(value)) { return; }
|
||||
|
||||
// Add series name to point for future identification
|
||||
point.push(seriesName);
|
||||
|
||||
if (buckets[bucketNum] && buckets[bucketNum].values) {
|
||||
buckets[bucketNum].values.push(value);
|
||||
buckets[bucketNum].points.push(point);
|
||||
} else {
|
||||
buckets[bucketNum] = {
|
||||
x: bucketNum,
|
||||
values: [value],
|
||||
points: [point]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function pushToYBuckets(buckets, bucketNum, value, point, bounds) {
|
||||
if (buckets[bucketNum]) {
|
||||
buckets[bucketNum].values.push(value);
|
||||
buckets[bucketNum].points.push(point);
|
||||
} else {
|
||||
buckets[bucketNum] = {
|
||||
y: bucketNum,
|
||||
bounds: bounds,
|
||||
values: [value],
|
||||
points: [point]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getValueBucketBound(value, yBucketSize, logBase) {
|
||||
if (logBase === 1) {
|
||||
return getBucketBound(value, yBucketSize);
|
||||
} else {
|
||||
return getLogScaleBucketBound(value, yBucketSize, logBase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find bucket for given value (for linear scale)
|
||||
*/
|
||||
function getBucketBounds(value, bucketSize) {
|
||||
let bottom, top;
|
||||
bottom = Math.floor(value / bucketSize) * bucketSize;
|
||||
top = (Math.floor(value / bucketSize) + 1) * bucketSize;
|
||||
|
||||
return {bottom, top};
|
||||
}
|
||||
|
||||
function getBucketBound(value, bucketSize) {
|
||||
let bounds = getBucketBounds(value, bucketSize);
|
||||
return bounds.bottom;
|
||||
}
|
||||
|
||||
function convertToValueBuckets(xBucket, bucketSize) {
|
||||
let values = xBucket.values;
|
||||
let points = xBucket.points;
|
||||
let buckets = {};
|
||||
_.forEach(values, (val, index) => {
|
||||
let bounds = getBucketBounds(val, bucketSize);
|
||||
let bucketNum = bounds.bottom;
|
||||
pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find bucket for given value (for log scales)
|
||||
*/
|
||||
function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) {
|
||||
let top, bottom;
|
||||
if (value === 0) {
|
||||
return {bottom: 0, top: 0};
|
||||
}
|
||||
|
||||
let value_log = logp(value, logBase);
|
||||
let pow, powTop;
|
||||
if (yBucketSplitFactor === 1 || !yBucketSplitFactor) {
|
||||
pow = Math.floor(value_log);
|
||||
powTop = pow + 1;
|
||||
} else {
|
||||
let additional_bucket_size = 1 / yBucketSplitFactor;
|
||||
let additional_log = value_log - Math.floor(value_log);
|
||||
additional_log = Math.floor(additional_log / additional_bucket_size) * additional_bucket_size;
|
||||
pow = Math.floor(value_log) + additional_log;
|
||||
powTop = pow + additional_bucket_size;
|
||||
}
|
||||
bottom = Math.pow(logBase, pow);
|
||||
top = Math.pow(logBase, powTop);
|
||||
|
||||
return {bottom, top};
|
||||
}
|
||||
|
||||
function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) {
|
||||
let bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase);
|
||||
return bounds.bottom;
|
||||
}
|
||||
|
||||
function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) {
|
||||
let values = xBucket.values;
|
||||
let points = xBucket.points;
|
||||
|
||||
let buckets = {};
|
||||
_.forEach(values, (val, index) => {
|
||||
let bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase);
|
||||
let bucketNum = bounds.bottom;
|
||||
pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge individual buckets for all series into one
|
||||
* @param {Array} seriesBuckets Array of series buckets
|
||||
* @return {Object} Merged buckets.
|
||||
*/
|
||||
function mergeBuckets(seriesBuckets) {
|
||||
let mergedBuckets = {};
|
||||
_.forEach(seriesBuckets, (seriesBucket, index) => {
|
||||
if (index === 0) {
|
||||
mergedBuckets = seriesBucket;
|
||||
} else {
|
||||
_.forEach(seriesBucket, (xBucket, xBound) => {
|
||||
if (mergedBuckets[xBound]) {
|
||||
mergedBuckets[xBound].points = xBucket.points.concat(mergedBuckets[xBound].points);
|
||||
mergedBuckets[xBound].values = xBucket.values.concat(mergedBuckets[xBound].values);
|
||||
|
||||
_.forEach(xBucket.buckets, (yBucket, yBound) => {
|
||||
let bucket = mergedBuckets[xBound].buckets[yBound];
|
||||
if (bucket && bucket.values) {
|
||||
mergedBuckets[xBound].buckets[yBound].values = bucket.values.concat(yBucket.values);
|
||||
mergedBuckets[xBound].buckets[yBound].points = bucket.points.concat(yBucket.points);
|
||||
} else {
|
||||
mergedBuckets[xBound].buckets[yBound] = yBucket;
|
||||
}
|
||||
|
||||
let points = mergedBuckets[xBound].buckets[yBound].points;
|
||||
mergedBuckets[xBound].buckets[yBound].seriesStat = getSeriesStat(points);
|
||||
});
|
||||
} else {
|
||||
mergedBuckets[xBound] = xBucket;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mergedBuckets;
|
||||
}
|
||||
|
||||
// Get minimum non zero value.
|
||||
function getMinLog(series) {
|
||||
let values = _.compact(_.map(series.datapoints, p => p[0]));
|
||||
return _.min(values);
|
||||
}
|
||||
|
||||
// Logarithm for custom base
|
||||
function logp(value, base) {
|
||||
return Math.log(value) / Math.log(base);
|
||||
}
|
||||
|
||||
export {
|
||||
convertToHeatMap,
|
||||
convertToCards,
|
||||
removeZeroBuckets,
|
||||
mergeZeroBuckets,
|
||||
getMinLog,
|
||||
getValueBucketBound
|
||||
};
|
248
public/app/plugins/panel/heatmap/heatmap_tooltip.ts
Normal file
248
public/app/plugins/panel/heatmap/heatmap_tooltip.ts
Normal file
@ -0,0 +1,248 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import d3 from 'd3';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {getValueBucketBound} from './heatmap_data_converter';
|
||||
|
||||
let TOOLTIP_PADDING_X = 30;
|
||||
let TOOLTIP_PADDING_Y = 5;
|
||||
let HISTOGRAM_WIDTH = 160;
|
||||
let HISTOGRAM_HEIGHT = 40;
|
||||
|
||||
export class HeatmapTooltip {
|
||||
tooltip: any;
|
||||
scope: any;
|
||||
dashboard: any;
|
||||
panel: any;
|
||||
heatmapPanel: any;
|
||||
mouseOverBucket: boolean;
|
||||
originalFillColor: any;
|
||||
|
||||
constructor(elem, scope) {
|
||||
this.scope = scope;
|
||||
this.dashboard = scope.ctrl.dashboard;
|
||||
this.panel = scope.ctrl.panel;
|
||||
this.heatmapPanel = elem;
|
||||
this.mouseOverBucket = false;
|
||||
this.originalFillColor = null;
|
||||
|
||||
elem.on("mouseover", this.onMouseOver.bind(this));
|
||||
elem.on("mouseleave", this.onMouseLeave.bind(this));
|
||||
}
|
||||
|
||||
onMouseOver(e) {
|
||||
if (!this.tooltip) {
|
||||
this.add();
|
||||
this.move(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (!this.panel.tooltip.show) { return; }
|
||||
|
||||
this.move(e);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("class", "heatmap-tooltip");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.tooltip) {
|
||||
this.tooltip.remove();
|
||||
}
|
||||
|
||||
this.tooltip = null;
|
||||
}
|
||||
|
||||
show(pos, data) {
|
||||
if (!this.panel.tooltip.show || !data) { return; }
|
||||
|
||||
let {xBucketIndex, yBucketIndex} = this.getBucketIndexes(pos, data);
|
||||
|
||||
if (!data.buckets[xBucketIndex] || !this.tooltip) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
let boundBottom, boundTop, valuesNumber;
|
||||
let xData = data.buckets[xBucketIndex];
|
||||
let yData = xData.buckets[yBucketIndex];
|
||||
|
||||
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat);
|
||||
let decimals = this.panel.tooltipDecimals || 5;
|
||||
let valueFormatter = this.valueFormatter(decimals);
|
||||
|
||||
let tooltipHtml = `<div><b>${time}</b></div>
|
||||
<div class="heatmap-histogram"></div>`;
|
||||
|
||||
if (yData) {
|
||||
boundBottom = valueFormatter(yData.bounds.bottom);
|
||||
boundTop = valueFormatter(yData.bounds.top);
|
||||
valuesNumber = yData.values.length;
|
||||
tooltipHtml += `<div>
|
||||
bucket: <b>${boundBottom} - ${boundTop}</b> <br>
|
||||
values: <b>${valuesNumber}</b> <br>
|
||||
</div>`;
|
||||
|
||||
if (this.panel.tooltip.seriesStat && yData.seriesStat) {
|
||||
tooltipHtml = this.addSeriesStat(tooltipHtml, yData.seriesStat);
|
||||
}
|
||||
} 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, data) {
|
||||
let xBucketIndex, yBucketIndex;
|
||||
|
||||
// if panelRelY is defined another panel wants us to show a tooltip
|
||||
if (pos.panelRelY) {
|
||||
xBucketIndex = getValueBucketBound(pos.x, data.xBucketSize, 1);
|
||||
let y = this.scope.yScale.invert(pos.panelRelY * this.scope.chartHeight);
|
||||
yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase);
|
||||
pos = this.getSharedTooltipPos(pos);
|
||||
|
||||
if (!this.tooltip) {
|
||||
// Add shared tooltip for panel
|
||||
this.add();
|
||||
}
|
||||
} else {
|
||||
xBucketIndex = this.getXBucketIndex(pos.offsetX, data);
|
||||
yBucketIndex = this.getYBucketIndex(pos.offsetY, data);
|
||||
}
|
||||
|
||||
return {xBucketIndex, yBucketIndex};
|
||||
}
|
||||
|
||||
getXBucketIndex(offsetX, data) {
|
||||
let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf();
|
||||
let xBucketIndex = getValueBucketBound(x, data.xBucketSize, 1);
|
||||
return xBucketIndex;
|
||||
}
|
||||
|
||||
getYBucketIndex(offsetY, data) {
|
||||
let y = this.scope.yScale.invert(offsetY - this.scope.chartTop);
|
||||
let yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase);
|
||||
return yBucketIndex;
|
||||
}
|
||||
|
||||
getSharedTooltipPos(pos) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
addSeriesStat(tooltipHtml, seriesStat) {
|
||||
tooltipHtml += "series: <br>";
|
||||
_.forEach(seriesStat, (values, series) => {
|
||||
tooltipHtml += ` - ${series}: <b>${values}</b><br>`;
|
||||
});
|
||||
|
||||
return tooltipHtml;
|
||||
}
|
||||
|
||||
addHistogram(data) {
|
||||
let xBucket = this.scope.ctrl.data.buckets[data.x];
|
||||
let yBucketSize = this.scope.ctrl.data.yBucketSize;
|
||||
let {min, max, ticks} = this.scope.ctrl.data.yAxis;
|
||||
let histogramData = _.map(xBucket.buckets, bucket => {
|
||||
return [bucket.y, bucket.values.length];
|
||||
});
|
||||
histogramData = _.filter(histogramData, d => {
|
||||
return d[0] >= min && d[0] <= max;
|
||||
});
|
||||
|
||||
let scale = this.scope.yScale.copy();
|
||||
let histXScale = scale
|
||||
.domain([min, max])
|
||||
.range([0, HISTOGRAM_WIDTH]);
|
||||
|
||||
let barWidth;
|
||||
if (this.panel.yAxis.logBase === 1) {
|
||||
barWidth = Math.floor(HISTOGRAM_WIDTH / (max - min) * yBucketSize * 0.9);
|
||||
} else {
|
||||
barWidth = Math.floor(HISTOGRAM_WIDTH / ticks / yBucketSize * 0.9);
|
||||
}
|
||||
barWidth = Math.max(barWidth, 1);
|
||||
|
||||
let histYScale = d3.scaleLinear()
|
||||
.domain([0, _.max(_.map(histogramData, d => d[1]))])
|
||||
.range([0, HISTOGRAM_HEIGHT]);
|
||||
|
||||
let 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 => {
|
||||
return histXScale(d[0]);
|
||||
})
|
||||
.attr("width", barWidth)
|
||||
.attr("y", d => {
|
||||
return HISTOGRAM_HEIGHT - histYScale(d[1]);
|
||||
})
|
||||
.attr("height", d => {
|
||||
return histYScale(d[1]);
|
||||
});
|
||||
}
|
||||
|
||||
move(pos) {
|
||||
if (!this.tooltip) { return; }
|
||||
|
||||
let elem = $(this.tooltip.node())[0];
|
||||
let tooltipWidth = elem.clientWidth;
|
||||
let 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");
|
||||
}
|
||||
|
||||
valueFormatter(decimals) {
|
||||
let format = this.panel.yAxis.format;
|
||||
return function(value) {
|
||||
if (_.isInteger(value)) {
|
||||
decimals = 0;
|
||||
}
|
||||
return kbn.valueFormats[format](value, decimals);
|
||||
};
|
||||
}
|
||||
}
|
195
public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg
Normal file
195
public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg
Normal file
@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="100px"
|
||||
height="100px"
|
||||
viewBox="0 0 100 100"
|
||||
style="enable-background:new 0 0 100 100;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="icn-heatmap-panel.svg"
|
||||
inkscape:version="0.92.1 unknown"><metadata
|
||||
id="metadata108"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs106" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2491"
|
||||
inkscape:window-height="1410"
|
||||
id="namedview104"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.44"
|
||||
inkscape:cx="37.431994"
|
||||
inkscape:cy="46.396264"
|
||||
inkscape:window-x="69"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><rect
|
||||
x="-0.017525015"
|
||||
y="33.438038"
|
||||
style="opacity:0.35714285;fill:#decd87;fill-opacity:1;stroke-width:0.70710677"
|
||||
width="15.8115"
|
||||
height="15.049"
|
||||
id="rect69" /><path
|
||||
style="opacity:0.42857145;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.874036,24.263391 v -7.46822 h 7.891949 7.891949 v 7.46822 7.46822 h -7.891949 -7.891949 z"
|
||||
id="path4883"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.79365079;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.69883,24.337252 v -7.46822 h 7.891949 7.891949 v 7.46822 7.46822 H 41.590779 33.69883 Z"
|
||||
id="path4885"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.80952382;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.523624,24.337251 v -7.46822 h 7.891949 7.89195 v 7.46822 7.46822 h -7.89195 -7.891949 z"
|
||||
id="path4887"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.348418,24.167764 v -7.46822 h 7.891949 7.891949 v 7.46822 7.46822 h -7.891949 -7.891949 z"
|
||||
id="path4889"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.24603176;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.173218,24.279957 v -7.46822 h 7.891947 7.891956 v 7.46822 7.46822 h -7.891956 -7.891947 z"
|
||||
id="path4891"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.38158725;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.226177,40.968612 v -7.46822 h 7.891949 7.891954 v 7.46822 7.468221 h -7.891954 -7.891949 z"
|
||||
id="path4893"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.75396824;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.377433,40.884464 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4895"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.94444442;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.528693,41.011582 v -7.46822 h 7.891949 7.89195 v 7.46822 7.468221 h -7.89195 -7.891949 z"
|
||||
id="path4897"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.53174606;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.679956,41.011587 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4899"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.64285715;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.831216,40.956187 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4901"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.58730158;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 0.04924114,57.615687 v -7.46822 H 7.8882241 15.727207 v 7.46822 7.468221 H 7.8882241 0.04924114 Z"
|
||||
id="path4905"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.884627,57.648974 v -7.46822 h 7.838984 7.838983 v 7.46822 7.468221 h -7.838983 -7.838984 z"
|
||||
id="path4907"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:1;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.390785,57.601163 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4913"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.29365079;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.226177,57.657262 v -7.46822 h 7.891947 7.891946 v 7.46822 7.468221 h -7.891946 -7.891947 z"
|
||||
id="path4915"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.73015873;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.226177,74.345913 v -7.46822 h 7.891948 7.891955 v 7.46822 7.468221 h -7.891955 -7.891948 z"
|
||||
id="path4917"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.58730158;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.380199,74.317863 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4919"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.66666667;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.534214,74.360232 v -7.46822 h 7.891949 7.89195 v 7.46822 7.468221 h -7.89195 -7.891949 z"
|
||||
id="path4921"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.84920636;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.688232,74.360242 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4923"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.70634921;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.842256,74.341769 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4925"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M -0.00372516,74.325127 V 66.856906 H 7.8882239 15.780174 v 7.468221 7.46822 H 7.8882239 -0.00372516 Z"
|
||||
id="path4927"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.13492061;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 0.04924124,91.034564 V 83.566343 H 7.8882241 15.727207 v 7.468221 7.468221 H 7.8882241 0.04924114 Z"
|
||||
id="path4929"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.26190479;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 16.88187,91.034561 V 83.56634 h 7.838983 7.838984 v 7.468221 7.468224 h -7.838983 -7.838983 z"
|
||||
id="path4931"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.58730158;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.714496,91.034569 v -7.468221 h 7.891949 7.891949 v 7.468221 7.468216 h -7.891949 -7.891949 z"
|
||||
id="path4933"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.30158727;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 50.547126,91.034561 V 83.56634 h 7.891949 7.89195 v 7.468221 7.468224 h -7.89195 -7.891949 z"
|
||||
id="path4935"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.15873018;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.379756,91.034564 v -7.468221 h 7.891949 7.891949 v 7.468221 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4937"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.11904764;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.212376,91.034568 v -7.468221 h 7.891952 7.89195 v 7.468221 7.468217 h -7.89195 -7.891952 z"
|
||||
id="path4939"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.89682539;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.555398,57.68591 v -7.46822 h 7.838983 7.838983 v 7.46822 7.468221 h -7.838983 -7.838983 z"
|
||||
id="path4941"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:1;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.720011,57.685908 v -7.46822 h 7.838983 7.838983 v 7.46822 7.468221 h -7.838983 -7.838983 z"
|
||||
id="path4943"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.16666667;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 0.04924152,24.249783 V 16.728597 H 7.8882245 15.727207 v 7.521186 7.521186 H 7.8882245 0.04924152 Z"
|
||||
id="path4976"
|
||||
inkscape:connector-curvature="0" /><rect
|
||||
x="16.900255"
|
||||
y="0.10238234"
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.70710677"
|
||||
width="15.8115"
|
||||
height="15.049"
|
||||
id="rect69-5-7-3" /><rect
|
||||
x="84.304306"
|
||||
y="0.12308588"
|
||||
style="opacity:0.11904764;fill:#decd87;fill-opacity:1;stroke-width:0.70710677"
|
||||
width="15.8115"
|
||||
height="15.049"
|
||||
id="rect69-5-2-2-6" /><path
|
||||
style="opacity:0.3174603;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 33.751268,7.6629239 V 0.19470386 h 7.891949 7.891949 V 7.6629239 15.131142 h -7.891949 -7.891949 z"
|
||||
id="path4885-1"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 50.602281,7.6629315 V 0.19471149 h 7.891949 7.891951 V 7.6629315 15.13115 H 58.49423 50.602281 Z"
|
||||
id="path4887-2"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.73015873;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.453295,7.4510673 v -7.4682202 h 7.89195 7.89195 v 7.4682202 7.4682177 h -7.89195 -7.89195 z"
|
||||
id="path4889-9"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.15873018;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M -0.02566414,7.5403525 V 0.0191665 H 7.8133188 15.652302 v 7.521186 7.5211835 H 7.8133188 -0.02566414 Z"
|
||||
id="path4976-3"
|
||||
inkscape:connector-curvature="0" /></svg>
|
After Width: | Height: | Size: 10 KiB |
7
public/app/plugins/panel/heatmap/module.html
Normal file
7
public/app/plugins/panel/heatmap/module.html
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="heatmap-wrapper">
|
||||
<div class="heatmap-canvas-wrapper">
|
||||
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
|
||||
</div>
|
||||
<!-- <div class="graph-legend-wrapper" ng-if="ctrl.panel.legend.show" heatmap-legend></div> -->
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
13
public/app/plugins/panel/heatmap/module.ts
Normal file
13
public/app/plugins/panel/heatmap/module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {loadPluginCss} from 'app/plugins/sdk';
|
||||
import {HeatmapCtrl} from './heatmap_ctrl';
|
||||
|
||||
// loadPluginCss({
|
||||
// dark: 'public/app/plugins/panel/heatmap/css/heatmap.dark.css',
|
||||
// light: 'public/app/plugins/panel/heatmap/css/heatmap.light.css'
|
||||
// });
|
||||
|
||||
export {
|
||||
HeatmapCtrl as PanelCtrl
|
||||
};
|
85
public/app/plugins/panel/heatmap/partials/axes_editor.html
Normal file
85
public/app/plugins/panel/heatmap/partials/axes_editor.html
Normal file
@ -0,0 +1,85 @@
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Y Axis</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-5"
|
||||
label="Show"
|
||||
checked="ctrl.panel.yAxis.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Unit</label>
|
||||
<div class="gf-form-dropdown-typeahead max-width-15"
|
||||
ng-model="ctrl.panel.yAxis.format"
|
||||
dropdown-typeahead2="editor.unitFormats"
|
||||
dropdown-typeahead-on-select="editor.setUnitFormat($subItem)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Scale</label>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.yAxis.logBase" ng-options="v as k for (k, v) in editor.logScales" ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-10">
|
||||
<label class="gf-form-label width-5">Y-Min</label>
|
||||
<input type="text" class="gf-form-input" placeholder="auto" empty-to-null ng-model="ctrl.panel.yAxis.min" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form max-width-10">
|
||||
<label class="gf-form-label width-5">Y-Max</label>
|
||||
<input type="text" class="gf-form-input" placeholder="auto" empty-to-null ng-model="ctrl.panel.yAxis.max" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Decimals</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Override automatic decimal precision for axis.'"
|
||||
ng-model="ctrl.panel.yAxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
<div ng-show="ctrl.panel.yAxis.logBase === 1">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Buckets</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Number of buckets for Y axis.'"
|
||||
ng-model="ctrl.panel.yBucketNumber" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Bucket Size</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Size of bucket. Has priority over Buckets option.'"
|
||||
ng-model="ctrl.panel.yBucketSize" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="ctrl.panel.yAxis.logBase !== 1">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Split Buckets</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="1" data-placement="right"
|
||||
bs-tooltip="'For log scales only. By default Y values is splitted by integer powers of log base (1, 2, 4, 8, 16, ... for log2). This option allows to split each default bucket into specified number of buckets.'"
|
||||
ng-model="ctrl.panel.yAxis.splitFactor" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Remove zero values"
|
||||
checked="ctrl.panel.yAxis.removeZeroValues" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">X Axis</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Show"
|
||||
checked="ctrl.panel.xAxis.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Buckets</label>
|
||||
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Number of buckets for X axis.'"
|
||||
ng-model="ctrl.panel.xBucketNumber" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Bucket Size</label>
|
||||
<input type="text" class="gf-form-input width-8" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Size of bucket. Number or interval (10s, 5m, 1h, etc). Supported intervals: ms, s, m, h, d, w, M, y. Has priority over Buckets option.'"
|
||||
ng-model="ctrl.panel.xBucketSize" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,93 @@
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Colors</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Color mode</label>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.mode" ng-options="s for s in ctrl.colorModes" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ctrl.panel.color.mode === 'opacity'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Card Color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.color.cardColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Opacity scale</label>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScale" ng-options="s for s in ctrl.opacityScales" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.panel.color.colorScale === 'sqrt'">
|
||||
<label class="gf-form-label width-8">Exponent</label>
|
||||
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<svg id="heatmap-opacity-legend"
|
||||
width="22.7em" height="2em">
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ctrl.panel.color.mode === 'color'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Color scheme</label>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<svg id="heatmap-color-legend"
|
||||
width="22.7em" height="2em">
|
||||
</svg>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Fill background"
|
||||
checked="ctrl.panel.color.fillBackground" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Cards</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Space</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardPadding" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Round</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Tooltip</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Show tooltip"
|
||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div ng-if="ctrl.panel.tooltip.show">
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Highlight cards"
|
||||
checked="ctrl.panel.highlightCards" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Series stats"
|
||||
checked="ctrl.panel.tooltip.seriesStat" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Histogram"
|
||||
checked="ctrl.panel.tooltip.showHistogram" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Decimals</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Max decimal precision for tooltip.'"
|
||||
ng-model="ctrl.panel.tooltipDecimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
16
public/app/plugins/panel/heatmap/plugin.json
Normal file
16
public/app/plugins/panel/heatmap/plugin.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Heatmap",
|
||||
"id": "heatmap",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-heatmap-panel.svg",
|
||||
"large": "img/icn-heatmap-panel.svg"
|
||||
}
|
||||
}
|
||||
}
|
861
public/app/plugins/panel/heatmap/rendering.ts
Normal file
861
public/app/plugins/panel/heatmap/rendering.ts
Normal file
@ -0,0 +1,861 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {appEvents} from 'app/core/core';
|
||||
import d3 from 'd3';
|
||||
import {HeatmapTooltip} from './heatmap_tooltip';
|
||||
import {convertToCards, mergeZeroBuckets, removeZeroBuckets} from './heatmap_data_converter';
|
||||
|
||||
let 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 = 50,
|
||||
X_AXIS_TICK_PADDING = 10,
|
||||
Y_AXIS_TICK_PADDING = 5,
|
||||
MIN_SELECTION_WIDTH = 2;
|
||||
|
||||
export default function link(scope, elem, attrs, ctrl) {
|
||||
let data, timeRange, panel, heatmap;
|
||||
|
||||
// $heatmap is JQuery object, but heatmap is D3
|
||||
let $heatmap = elem.find('.heatmap-panel');
|
||||
let tooltip = new HeatmapTooltip($heatmap, scope);
|
||||
|
||||
let width, height,
|
||||
yScale, xScale,
|
||||
chartWidth, chartHeight,
|
||||
chartTop, chartBottom,
|
||||
yAxisWidth, xAxisHeight,
|
||||
cardPadding, cardRound,
|
||||
cardWidth, cardHeight,
|
||||
colorScale, opacityScale,
|
||||
mouseUpHandler;
|
||||
|
||||
let selection = {
|
||||
active: false,
|
||||
x1: -1,
|
||||
x2: -1
|
||||
};
|
||||
|
||||
let padding = {left: 0, right: 0, top: 0, bottom: 0},
|
||||
margin = {left: 25, right: 15, top: 10, bottom: 20},
|
||||
dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
|
||||
|
||||
ctrl.events.on('render', () => {
|
||||
render();
|
||||
ctrl.renderingCompleted();
|
||||
});
|
||||
|
||||
function setElementHeight() {
|
||||
try {
|
||||
var height = ctrl.height || panel.height || ctrl.row.height;
|
||||
if (_.isString(height)) {
|
||||
height = parseInt(height.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
height -= 5; // padding
|
||||
height -= panel.title ? 24 : 9; // subtract panel title bar
|
||||
|
||||
$heatmap.css('height', height + 'px');
|
||||
|
||||
return true;
|
||||
} catch (e) { // IE throws errors sometimes
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getYAxisWidth(elem) {
|
||||
let axis_text = elem.selectAll(".axis-y text").nodes();
|
||||
let max_text_width = _.max(_.map(axis_text, text => {
|
||||
let el = $(text);
|
||||
// Use JQuery outerWidth() to compute full element width
|
||||
return el.outerWidth();
|
||||
}));
|
||||
|
||||
return max_text_width;
|
||||
}
|
||||
|
||||
function getXAxisHeight(elem) {
|
||||
let axis_line = elem.select(".axis-x line");
|
||||
if (!axis_line.empty()) {
|
||||
let axis_line_position = parseFloat(elem.select(".axis-x line").attr("y2"));
|
||||
let canvas_width = parseFloat(elem.attr("height"));
|
||||
return canvas_width - axis_line_position;
|
||||
} else {
|
||||
// Default height
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
function addXAxis() {
|
||||
xScale = d3.scaleTime()
|
||||
.domain([timeRange.from, timeRange.to])
|
||||
.range([0, chartWidth]);
|
||||
|
||||
let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
|
||||
let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
|
||||
|
||||
let xAxis = d3.axisBottom(xScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(d3.timeFormat(grafanaTimeFormatter))
|
||||
.tickPadding(X_AXIS_TICK_PADDING)
|
||||
.tickSize(chartHeight);
|
||||
|
||||
let posY = margin.top;
|
||||
let posX = yAxisWidth;
|
||||
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)
|
||||
heatmap.select(".axis-x").select(".domain").remove();
|
||||
}
|
||||
|
||||
function addYAxis() {
|
||||
let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
|
||||
let tick_interval = tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks);
|
||||
let {y_min, y_max} = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);
|
||||
|
||||
// Rewrite min and max if it have been set explicitly
|
||||
y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
|
||||
y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
|
||||
|
||||
// Adjust ticks after Y range widening
|
||||
tick_interval = tickStep(y_min, y_max, ticks);
|
||||
ticks = Math.ceil((y_max - y_min) / tick_interval);
|
||||
|
||||
let decimals = panel.yAxis.decimals === null ? getPrecision(tick_interval) : panel.yAxis.decimals;
|
||||
|
||||
// Set default Y min and max if no data
|
||||
if (_.isEmpty(data.buckets)) {
|
||||
y_max = 1;
|
||||
y_min = -1;
|
||||
ticks = 3;
|
||||
decimals = 1;
|
||||
}
|
||||
|
||||
data.yAxis = {
|
||||
min: y_min,
|
||||
max: y_max,
|
||||
ticks: ticks
|
||||
};
|
||||
|
||||
yScale = d3.scaleLinear()
|
||||
.domain([y_min, y_max])
|
||||
.range([chartHeight, 0]);
|
||||
|
||||
let yAxis = d3.axisLeft(yScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(tickValueFormatter(decimals))
|
||||
.tickSizeInner(0 - width)
|
||||
.tickSizeOuter(0)
|
||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||
|
||||
heatmap.append("g")
|
||||
.attr("class", "axis axis-y")
|
||||
.call(yAxis);
|
||||
|
||||
// Calculate Y axis width first, then move axis into visible area
|
||||
let posY = margin.top;
|
||||
let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||
heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")");
|
||||
|
||||
// Remove vertical line in the right of axis labels (called domain in d3)
|
||||
heatmap.select(".axis-y").select(".domain").remove();
|
||||
}
|
||||
|
||||
// Wide Y values range and anjust to bucket size
|
||||
function wideYAxisRange(min, max, tickInterval) {
|
||||
let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
|
||||
let y_min, y_max;
|
||||
|
||||
if (tickInterval === 0) {
|
||||
y_max = max * dataRangeWidingFactor;
|
||||
y_min = min - min * (dataRangeWidingFactor - 1);
|
||||
tickInterval = (y_max - y_min) / 2;
|
||||
} else {
|
||||
y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
|
||||
y_min = Math.floor((min - y_widing) / tickInterval) * tickInterval;
|
||||
}
|
||||
|
||||
// Don't wide axis below 0 if all values are positive
|
||||
if (min >= 0 && y_min < 0) {
|
||||
y_min = 0;
|
||||
}
|
||||
|
||||
return {y_min, y_max};
|
||||
}
|
||||
|
||||
function addLogYAxis() {
|
||||
let log_base = panel.yAxis.logBase;
|
||||
let {y_min, y_max} = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base);
|
||||
|
||||
y_min = panel.yAxis.min !== null ? adjustLogMin(panel.yAxis.min, log_base) : y_min;
|
||||
y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max;
|
||||
|
||||
// Set default Y min and max if no data
|
||||
if (_.isEmpty(data.buckets)) {
|
||||
y_max = Math.pow(log_base, 2);
|
||||
y_min = 1;
|
||||
}
|
||||
|
||||
yScale = d3.scaleLog()
|
||||
.base(panel.yAxis.logBase)
|
||||
.domain([y_min, y_max])
|
||||
.range([chartHeight, 0]);
|
||||
|
||||
let domain = yScale.domain();
|
||||
let tick_values = logScaleTickValues(domain, log_base);
|
||||
let decimals = panel.yAxis.decimals;
|
||||
|
||||
data.yAxis = {
|
||||
min: y_min,
|
||||
max: y_max,
|
||||
ticks: tick_values.length
|
||||
};
|
||||
|
||||
let yAxis = d3.axisLeft(yScale)
|
||||
.tickValues(tick_values)
|
||||
.tickFormat(tickValueFormatter(decimals))
|
||||
.tickSizeInner(0 - width)
|
||||
.tickSizeOuter(0)
|
||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||
|
||||
heatmap.append("g")
|
||||
.attr("class", "axis axis-y")
|
||||
.call(yAxis);
|
||||
|
||||
// Calculate Y axis width first, then move axis into visible area
|
||||
let posY = margin.top;
|
||||
let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||
heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")");
|
||||
|
||||
// Set first tick as pseudo 0
|
||||
if (y_min < 1) {
|
||||
heatmap.select(".axis-y").select(".tick text").text("0");
|
||||
}
|
||||
|
||||
// Remove vertical line in the right of axis labels (called domain in d3)
|
||||
heatmap.select(".axis-y").select(".domain").remove();
|
||||
}
|
||||
|
||||
// Adjust data range to log base
|
||||
function adjustLogRange(min, max, logBase) {
|
||||
let y_min, y_max;
|
||||
|
||||
y_min = data.heatmapStats.minLog;
|
||||
if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
|
||||
y_min = 1;
|
||||
} else {
|
||||
y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
|
||||
}
|
||||
|
||||
// Adjust max Y value to log base
|
||||
y_max = adjustLogMax(data.heatmapStats.max, logBase);
|
||||
|
||||
return {y_min, y_max};
|
||||
}
|
||||
|
||||
function adjustLogMax(max, base) {
|
||||
return Math.pow(base, Math.ceil(logp(max, base)));
|
||||
}
|
||||
|
||||
function adjustLogMin(min, base) {
|
||||
return Math.pow(base, Math.floor(logp(min, base)));
|
||||
}
|
||||
|
||||
function logScaleTickValues(domain, base) {
|
||||
let domainMin = domain[0];
|
||||
let domainMax = domain[1];
|
||||
let tickValues = [];
|
||||
|
||||
if (domainMin < 1) {
|
||||
let under_one_ticks = Math.floor(logp(domainMin, base));
|
||||
for (let i = under_one_ticks; i < 0; i++) {
|
||||
let tick_value = Math.pow(base, i);
|
||||
tickValues.push(tick_value);
|
||||
}
|
||||
}
|
||||
|
||||
let ticks = Math.ceil(logp(domainMax, base));
|
||||
for (let i = 0; i <= ticks; i++) {
|
||||
let tick_value = Math.pow(base, i);
|
||||
tickValues.push(tick_value);
|
||||
}
|
||||
|
||||
return tickValues;
|
||||
}
|
||||
|
||||
function tickValueFormatter(decimals) {
|
||||
let format = panel.yAxis.format;
|
||||
return function(value) {
|
||||
return kbn.valueFormats[format](value, decimals);
|
||||
};
|
||||
}
|
||||
|
||||
function fixYAxisTickSize() {
|
||||
heatmap.select(".axis-y")
|
||||
.selectAll(".tick line")
|
||||
.attr("x2", chartWidth);
|
||||
}
|
||||
|
||||
function addAxes() {
|
||||
chartHeight = height - margin.top - margin.bottom;
|
||||
chartTop = margin.top;
|
||||
chartBottom = chartTop + chartHeight;
|
||||
|
||||
if (panel.yAxis.logBase === 1) {
|
||||
addYAxis();
|
||||
} else {
|
||||
addLogYAxis();
|
||||
}
|
||||
|
||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||
chartWidth = width - yAxisWidth - margin.right;
|
||||
fixYAxisTickSize();
|
||||
|
||||
addXAxis();
|
||||
xAxisHeight = getXAxisHeight(heatmap);
|
||||
|
||||
if (!panel.yAxis.show) {
|
||||
heatmap.select(".axis-y").selectAll("line").style("opacity", 0);
|
||||
}
|
||||
|
||||
if (!panel.xAxis.show) {
|
||||
heatmap.select(".axis-x").selectAll("line").style("opacity", 0);
|
||||
}
|
||||
}
|
||||
|
||||
function addHeatmapCanvas() {
|
||||
let heatmap_elem = $heatmap[0];
|
||||
width = heatmap_elem.clientWidth - padding.right;
|
||||
height = heatmap_elem.clientHeight - padding.bottom;
|
||||
cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
|
||||
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
||||
|
||||
if (heatmap) {
|
||||
heatmap.remove();
|
||||
}
|
||||
|
||||
heatmap = d3.select(heatmap_elem)
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
}
|
||||
|
||||
function addHeatmap() {
|
||||
addHeatmapCanvas();
|
||||
addAxes();
|
||||
|
||||
if (panel.yAxis.logBase !== 1) {
|
||||
if (panel.yAxis.removeZeroValues) {
|
||||
data.buckets = removeZeroBuckets(data.buckets);
|
||||
} else {
|
||||
let log_base = panel.yAxis.logBase;
|
||||
let domain = yScale.domain();
|
||||
let tick_values = logScaleTickValues(domain, log_base);
|
||||
data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
|
||||
}
|
||||
}
|
||||
let cardsData = convertToCards(data.buckets);
|
||||
|
||||
let max_value = d3.max(cardsData, card => {
|
||||
return card.values.length;
|
||||
});
|
||||
|
||||
setColorScale(max_value);
|
||||
setOpacityScale(max_value);
|
||||
setCardSize();
|
||||
|
||||
if (panel.color.fillBackground && panel.color.mode === 'color') {
|
||||
fillBackground(heatmap, colorScale(0));
|
||||
}
|
||||
|
||||
let cards = heatmap.selectAll(".heatmap-card").data(cardsData);
|
||||
cards.append("title");
|
||||
cards = cards.enter().append("rect")
|
||||
.attr("x", getCardX)
|
||||
.attr("width", getCardWidth)
|
||||
.attr("y", getCardY)
|
||||
.attr("height", getCardHeight)
|
||||
.attr("rx", cardRound)
|
||||
.attr("ry", cardRound)
|
||||
.attr("class", "bordered heatmap-card")
|
||||
.style("fill", getCardColor)
|
||||
.style("stroke", getCardColor)
|
||||
.style("stroke-width", 0)
|
||||
.style("opacity", getCardOpacity);
|
||||
|
||||
let $cards = $heatmap.find(".heatmap-card");
|
||||
$cards.on("mouseenter", (event) => {
|
||||
tooltip.mouseOverBucket = true;
|
||||
highlightCard(event);
|
||||
})
|
||||
.on("mouseleave", (event) => {
|
||||
tooltip.mouseOverBucket = false;
|
||||
resetCardHighLight(event);
|
||||
});
|
||||
}
|
||||
|
||||
function highlightCard(event) {
|
||||
if (panel.highlightCards) {
|
||||
let color = d3.select(event.target).style("fill");
|
||||
let highlightColor = d3.color(color).darker(2);
|
||||
let strokeColor = d3.color(color).brighter(4);
|
||||
let current_card = d3.select(event.target);
|
||||
tooltip.originalFillColor = color;
|
||||
current_card.style("fill", highlightColor)
|
||||
.style("stroke", strokeColor)
|
||||
.style("stroke-width", 1);
|
||||
}
|
||||
}
|
||||
|
||||
function resetCardHighLight(event) {
|
||||
if (panel.highlightCards) {
|
||||
d3.select(event.target).style("fill", tooltip.originalFillColor)
|
||||
.style("stroke", tooltip.originalFillColor)
|
||||
.style("stroke-width", 0);
|
||||
}
|
||||
}
|
||||
|
||||
function setColorScale(max_value) {
|
||||
let colorInterpolator = d3[panel.color.colorScheme];
|
||||
colorScale = d3.scaleSequential(colorInterpolator).domain([0, max_value]);
|
||||
}
|
||||
|
||||
function setOpacityScale(max_value) {
|
||||
if (panel.color.colorScale === 'linear') {
|
||||
opacityScale = d3.scaleLinear()
|
||||
.domain([0, max_value])
|
||||
.range([0, 1]);
|
||||
} else if (panel.color.colorScale === 'sqrt') {
|
||||
opacityScale = d3.scalePow().exponent(panel.color.exponent)
|
||||
.domain([0, max_value])
|
||||
.range([0, 1]);
|
||||
}
|
||||
}
|
||||
|
||||
function setCardSize() {
|
||||
let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
|
||||
let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));
|
||||
|
||||
if (panel.yAxis.logBase !== 1) {
|
||||
let base = panel.yAxis.logBase;
|
||||
let splitFactor = panel.yAxis.splitFactor || 1;
|
||||
yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
|
||||
}
|
||||
|
||||
cardWidth = xGridSize - cardPadding * 2;
|
||||
cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
|
||||
}
|
||||
|
||||
function getCardX(d) {
|
||||
let x;
|
||||
if (xScale(d.x) < 0) {
|
||||
// Cut card left to prevent overlay
|
||||
x = yAxisWidth + cardPadding;
|
||||
} else {
|
||||
x = xScale(d.x) + yAxisWidth + cardPadding;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
function getCardWidth(d) {
|
||||
let w;
|
||||
if (xScale(d.x) < 0) {
|
||||
// Cut card left to prevent overlay
|
||||
let cutted_width = xScale(d.x) + cardWidth;
|
||||
w = cutted_width > 0 ? cutted_width : 0;
|
||||
} else if (xScale(d.x) + cardWidth > chartWidth) {
|
||||
// Cut card right to prevent overlay
|
||||
w = chartWidth - xScale(d.x) - cardPadding;
|
||||
} else {
|
||||
w = cardWidth;
|
||||
}
|
||||
|
||||
// Card width should be MIN_CARD_SIZE at least
|
||||
w = Math.max(w, MIN_CARD_SIZE);
|
||||
return w;
|
||||
}
|
||||
|
||||
function getCardY(d) {
|
||||
let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
|
||||
if (panel.yAxis.logBase !== 1 && d.y === 0) {
|
||||
y = chartBottom - cardHeight - cardPadding;
|
||||
} else {
|
||||
if (y < chartTop) {
|
||||
y = chartTop;
|
||||
}
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
function getCardHeight(d) {
|
||||
let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
|
||||
let h = cardHeight;
|
||||
|
||||
if (panel.yAxis.logBase !== 1 && d.y === 0) {
|
||||
return cardHeight;
|
||||
}
|
||||
|
||||
// Cut card height to prevent overlay
|
||||
if (y < chartTop) {
|
||||
h = yScale(d.y) - cardPadding;
|
||||
} else if (yScale(d.y) > chartBottom) {
|
||||
h = chartBottom - y;
|
||||
} else if (y + cardHeight > chartBottom) {
|
||||
h = chartBottom - y;
|
||||
}
|
||||
|
||||
// Height can't be more than chart height
|
||||
h = Math.min(h, chartHeight);
|
||||
// Card height should be MIN_CARD_SIZE at least
|
||||
h = Math.max(h, MIN_CARD_SIZE);
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
function getCardColor(d) {
|
||||
if (panel.color.mode === 'opacity') {
|
||||
return panel.color.cardColor;
|
||||
} else {
|
||||
return colorScale(d.values.length);
|
||||
}
|
||||
}
|
||||
|
||||
function getCardOpacity(d) {
|
||||
if (panel.color.mode === 'opacity') {
|
||||
return opacityScale(d.values.length);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function fillBackground(heatmap, color) {
|
||||
heatmap.insert("rect", "g")
|
||||
.attr("x", yAxisWidth)
|
||||
.attr("y", margin.top)
|
||||
.attr("width", chartWidth)
|
||||
.attr("height", chartHeight)
|
||||
.attr("fill", color);
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Selection and crosshair //
|
||||
/////////////////////////////
|
||||
|
||||
// Shared crosshair and tooltip
|
||||
appEvents.on('graph-hover', event => {
|
||||
drawSharedCrosshair(event.pos);
|
||||
|
||||
// Show shared tooltip
|
||||
if (ctrl.dashboard.graphTooltip === 2) {
|
||||
tooltip.show(event.pos, data);
|
||||
}
|
||||
});
|
||||
|
||||
appEvents.on('graph-hover-clear', () => {
|
||||
clearCrosshair();
|
||||
tooltip.destroy();
|
||||
});
|
||||
|
||||
function onMouseDown(event) {
|
||||
selection.active = true;
|
||||
selection.x1 = event.offsetX;
|
||||
|
||||
mouseUpHandler = function() {
|
||||
onMouseUp();
|
||||
};
|
||||
$(document).one("mouseup", mouseUpHandler);
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
$(document).unbind("mouseup", mouseUpHandler);
|
||||
mouseUpHandler = null;
|
||||
selection.active = false;
|
||||
|
||||
let selectionRange = Math.abs(selection.x2 - selection.x1);
|
||||
if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
|
||||
let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth);
|
||||
let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);
|
||||
|
||||
ctrl.timeSrv.setTime({
|
||||
from: moment.utc(timeFrom),
|
||||
to: moment.utc(timeTo)
|
||||
});
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
appEvents.emit('graph-hover-clear');
|
||||
clearCrosshair();
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
if (!heatmap) { return; }
|
||||
|
||||
if (selection.active) {
|
||||
// Clear crosshair and tooltip
|
||||
clearCrosshair();
|
||||
tooltip.destroy();
|
||||
|
||||
selection.x2 = limitSelection(event.offsetX);
|
||||
drawSelection(selection.x1, selection.x2);
|
||||
} else {
|
||||
emitGraphHoverEvet(event);
|
||||
drawCrosshair(event.offsetX);
|
||||
tooltip.show(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
function emitGraphHoverEvet(event) {
|
||||
let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
|
||||
let y = yScale.invert(event.offsetY);
|
||||
let pos = {
|
||||
pageX: event.pageX,
|
||||
pageY: event.pageY,
|
||||
x: x, x1: x,
|
||||
y: y, y1: y,
|
||||
panelRelY: null
|
||||
};
|
||||
|
||||
// Set minimum offset to prevent showing legend from another panel
|
||||
pos.panelRelY = Math.max(event.offsetY / height, 0.001);
|
||||
|
||||
// broadcast to other graph panels that we are hovering
|
||||
appEvents.emit('graph-hover', {pos: pos, panel: panel});
|
||||
}
|
||||
|
||||
function limitSelection(x2) {
|
||||
x2 = Math.max(x2, yAxisWidth);
|
||||
x2 = Math.min(x2, chartWidth + yAxisWidth);
|
||||
return x2;
|
||||
}
|
||||
|
||||
function drawSelection(posX1, posX2) {
|
||||
if (heatmap) {
|
||||
heatmap.selectAll(".heatmap-selection").remove();
|
||||
let selectionX = Math.min(posX1, posX2);
|
||||
let selectionWidth = Math.abs(posX1 - posX2);
|
||||
|
||||
if (selectionWidth > MIN_SELECTION_WIDTH) {
|
||||
heatmap.append("rect")
|
||||
.attr("class", "heatmap-selection")
|
||||
.attr("x", selectionX)
|
||||
.attr("width", selectionWidth)
|
||||
.attr("y", chartTop)
|
||||
.attr("height", chartHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selection.x1 = -1;
|
||||
selection.x2 = -1;
|
||||
|
||||
if (heatmap) {
|
||||
heatmap.selectAll(".heatmap-selection").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawCrosshair(position) {
|
||||
if (heatmap) {
|
||||
heatmap.selectAll(".heatmap-crosshair").remove();
|
||||
|
||||
let posX = position;
|
||||
posX = Math.max(posX, yAxisWidth);
|
||||
posX = Math.min(posX, chartWidth + yAxisWidth);
|
||||
|
||||
heatmap.append("g")
|
||||
.attr("class", "heatmap-crosshair")
|
||||
.attr("transform", "translate(" + posX + ",0)")
|
||||
.append("line")
|
||||
.attr("x1", 1)
|
||||
.attr("y1", chartTop)
|
||||
.attr("x2", 1)
|
||||
.attr("y2", chartBottom)
|
||||
.attr("stroke-width", 1);
|
||||
}
|
||||
}
|
||||
|
||||
function drawSharedCrosshair(pos) {
|
||||
if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
|
||||
let posX = xScale(pos.x) + yAxisWidth;
|
||||
drawCrosshair(posX);
|
||||
}
|
||||
}
|
||||
|
||||
function clearCrosshair() {
|
||||
if (heatmap) {
|
||||
heatmap.selectAll(".heatmap-crosshair").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawColorLegend() {
|
||||
d3.select("#heatmap-color-legend").selectAll("rect").remove();
|
||||
|
||||
let legend = d3.select("#heatmap-color-legend");
|
||||
let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth());
|
||||
let legendHeight = d3.select("#heatmap-color-legend").attr("height");
|
||||
|
||||
let colorInterpolator = d3[panel.color.colorScheme];
|
||||
let legendColorScale = d3.scaleSequential(colorInterpolator).domain([0, legendWidth]);
|
||||
|
||||
let rangeStep = 2;
|
||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
var 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 => {
|
||||
return legendColorScale(d);
|
||||
});
|
||||
}
|
||||
|
||||
function drawOpacityLegend() {
|
||||
d3.select("#heatmap-opacity-legend").selectAll("rect").remove();
|
||||
|
||||
let legend = d3.select("#heatmap-opacity-legend");
|
||||
let legendWidth = Math.floor($(d3.select("#heatmap-opacity-legend").node()).outerWidth());
|
||||
let legendHeight = d3.select("#heatmap-opacity-legend").attr("height");
|
||||
|
||||
let legendOpacityScale;
|
||||
if (panel.color.colorScale === 'linear') {
|
||||
legendOpacityScale = d3.scaleLinear()
|
||||
.domain([0, legendWidth])
|
||||
.range([0, 1]);
|
||||
} else if (panel.color.colorScale === 'sqrt') {
|
||||
legendOpacityScale = d3.scalePow().exponent(panel.color.exponent)
|
||||
.domain([0, legendWidth])
|
||||
.range([0, 1]);
|
||||
}
|
||||
|
||||
let rangeStep = 1;
|
||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
var 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", panel.color.cardColor)
|
||||
.style("opacity", d => {
|
||||
return legendOpacityScale(d);
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!ctrl.data || _.isEmpty(ctrl.data.buckets)) { return; }
|
||||
|
||||
data = ctrl.data;
|
||||
panel = ctrl.panel;
|
||||
timeRange = ctrl.range;
|
||||
|
||||
if (setElementHeight()) {
|
||||
addHeatmap();
|
||||
scope.yScale = yScale;
|
||||
scope.xScale = xScale;
|
||||
scope.yAxisWidth = yAxisWidth;
|
||||
scope.xAxisHeight = xAxisHeight;
|
||||
scope.chartHeight = chartHeight;
|
||||
scope.chartWidth = chartWidth;
|
||||
scope.chartTop = chartTop;
|
||||
|
||||
// Register selection listeners
|
||||
$heatmap.on("mousedown", onMouseDown);
|
||||
$heatmap.on("mousemove", onMouseMove);
|
||||
$heatmap.on("mouseleave", onMouseLeave);
|
||||
}
|
||||
|
||||
// Draw only if color editor is opened
|
||||
if (!d3.select("#heatmap-color-legend").empty()) {
|
||||
drawColorLegend();
|
||||
}
|
||||
if (!d3.select("#heatmap-opacity-legend").empty()) {
|
||||
drawOpacityLegend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function grafanaTimeFormat(ticks, min, max) {
|
||||
if (min && max && ticks) {
|
||||
let range = max - min;
|
||||
let secPerTick = (range/ticks) / 1000;
|
||||
let oneDay = 86400000;
|
||||
let oneYear = 31536000000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return "%H:%M:%S";
|
||||
}
|
||||
if (secPerTick <= 7200 || range <= oneDay) {
|
||||
return "%H:%M";
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return "%m/%d %H:%M";
|
||||
}
|
||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||
return "%m/%d";
|
||||
}
|
||||
return "%Y-%m";
|
||||
}
|
||||
|
||||
return "%H:%M";
|
||||
}
|
||||
|
||||
// Calculate tick step.
|
||||
// Implementation from d3-array (ticks.js)
|
||||
// https://github.com/d3/d3-array/blob/master/src/ticks.js
|
||||
function tickStep(start, stop, count) {
|
||||
var e10 = Math.sqrt(50),
|
||||
e5 = Math.sqrt(10),
|
||||
e2 = Math.sqrt(2);
|
||||
|
||||
var step0 = Math.abs(stop - start) / Math.max(0, count),
|
||||
step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
|
||||
error = step0 / step1;
|
||||
|
||||
if (error >= e10) {
|
||||
step1 *= 10;
|
||||
} else if (error >= e5) {
|
||||
step1 *= 5;
|
||||
} else if (error >= e2) {
|
||||
step1 *= 2;
|
||||
}
|
||||
|
||||
return stop < start ? -step1 : step1;
|
||||
}
|
||||
|
||||
function logp(value, base) {
|
||||
return Math.log(value) / Math.log(base);
|
||||
}
|
||||
|
||||
function getPrecision(num) {
|
||||
let str = num.toString();
|
||||
let dot_index = str.indexOf(".");
|
||||
if (dot_index === -1) {
|
||||
return 0;
|
||||
} else {
|
||||
return str.length - dot_index - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getTicksPrecision(values) {
|
||||
let precisions = _.map(values, getPrecision);
|
||||
return _.max(precisions);
|
||||
}
|
@ -30,7 +30,8 @@ System.config({
|
||||
"jquery.flot.time": "vendor/flot/jquery.flot.time",
|
||||
"jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair",
|
||||
"jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
|
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge"
|
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
|
||||
"d3": "vendor/d3/d3.js"
|
||||
},
|
||||
|
||||
packages: {
|
||||
|
27
public/vendor/d3/LICENSE
vendored
Normal file
27
public/vendor/d3/LICENSE
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright 2010-2016 Mike Bostock
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the author nor the names of contributors may be used to
|
||||
endorse or promote products derived from this software without specific prior
|
||||
written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
57
public/vendor/d3/README.md
vendored
Normal file
57
public/vendor/d3/README.md
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
# D3: Data-Driven Documents
|
||||
|
||||
<a href="https://d3js.org"><img src="https://d3js.org/logo.svg" align="left" hspace="10" vspace="6"></a>
|
||||
|
||||
**D3** (or **D3.js**) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data.
|
||||
|
||||
## Resources
|
||||
|
||||
* [API Reference](https://github.com/d3/d3/blob/master/API.md)
|
||||
* [Release Notes](https://github.com/d3/d3/releases)
|
||||
* [Gallery](https://github.com/d3/d3/wiki/Gallery)
|
||||
* [Examples](http://bl.ocks.org/mbostock)
|
||||
* [Wiki](https://github.com/d3/d3/wiki)
|
||||
|
||||
## Installing
|
||||
|
||||
If you use npm, `npm install d3`. Otherwise, download the [latest release](https://github.com/d3/d3/releases/latest). The released bundle supports anonymous AMD, CommonJS, and vanilla environments. You can load directly from [d3js.org](https://d3js.org), [CDNJS](https://cdnjs.com/libraries/d3), or [unpkg](https://unpkg.com/d3/). For example:
|
||||
|
||||
```html
|
||||
<script src="https://d3js.org/d3.v4.js"></script>
|
||||
```
|
||||
|
||||
For the minified version:
|
||||
|
||||
```html
|
||||
<script src="https://d3js.org/d3.v4.min.js"></script>
|
||||
```
|
||||
|
||||
You can also use the standalone D3 microlibraries. For example, [d3-selection](https://github.com/d3/d3-selection):
|
||||
|
||||
```html
|
||||
<script src="https://d3js.org/d3-selection.v1.js"></script>
|
||||
```
|
||||
|
||||
D3 is written using [ES2015 modules](http://www.2ality.com/2014/09/es6-modules-final.html). Create a [custom bundle using Rollup](http://bl.ocks.org/mbostock/bb09af4c39c79cffcde4), Webpack, or your preferred bundler. To import D3 into an ES2015 application, either import specific symbols from specific D3 modules:
|
||||
|
||||
```js
|
||||
import {scaleLinear} from "d3-scale";
|
||||
```
|
||||
|
||||
Or import everything into a namespace (here, `d3`):
|
||||
|
||||
```js
|
||||
import * as d3 from "d3";
|
||||
```
|
||||
|
||||
In Node:
|
||||
|
||||
```js
|
||||
var d3 = require("d3");
|
||||
```
|
||||
|
||||
You can also require individual modules and combine them into a `d3` object using [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign):
|
||||
|
||||
```js
|
||||
var d3 = Object.assign({}, require("d3-format"), require("d3-geo"), require("d3-geo-projection"));
|
||||
```
|
2
public/vendor/d3/d3-scale-chromatic.min.js
vendored
Normal file
2
public/vendor/d3/d3-scale-chromatic.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/vendor/d3/d3.js
vendored
Normal file
3
public/vendor/d3/d3.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
// Import main D3.js module and combine it with another
|
||||
var d3 = Object.assign({}, require('./d3.v4.min.js'), require('./d3-scale-chromatic.min.js'));
|
||||
module.exports = d3;
|
8
public/vendor/d3/d3.v4.min.js
vendored
Normal file
8
public/vendor/d3/d3.v4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user