feat: use timestamp to put values in buckets

- fix rendering for spreaded values
- fix legend for spectrum and opacity
This commit is contained in:
Ivan Mikheykin
2020-03-04 17:17:05 +03:00
parent a3f1495680
commit 9463913189
30 changed files with 1448 additions and 596 deletions
+34 -12
View File
@@ -5,6 +5,7 @@ import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
import {contextSrv} from 'app/core/core';
import {tickStep} from 'app/core/utils/ticks';
import coreModule from 'app/core/core_module';
import { BucketMatrix } from './statusmap_data';
const LEGEND_STEP_WIDTH = 2;
@@ -61,14 +62,35 @@ coreModule.directive('statusHeatmapLegend', function() {
function render() {
clearLegend(elem);
if (!ctrl.panel.legend.show) {
return
return;
}
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
let rangeFrom = ctrl.cardsData.minValue;
let rangeTo = ctrl.cardsData.maxValue;
if (ctrl.bucketMatrix) {
let rangeFrom = ctrl.bucketMatrix.minValue;
let rangeTo = ctrl.bucketMatrix.maxValue;
let maxValue = panel.color.max || rangeTo;
let minValue = panel.color.min || rangeFrom;
if (ctrl.bucketMatrix.noDatapoints) {
if (!panel.color.max) {
rangeTo = maxValue = 100;
} else {
rangeTo = 100;
}
if (!panel.color.min) {
rangeFrom = minValue = 0;
} else {
rangeFrom = 0;
}
}
console.log("legent state:", {
rangeFrom: rangeFrom,
rangeTo: rangeTo,
maxValue: maxValue,
minValue: minValue,
colorMode: panel.color.mode
});
if (panel.color.mode === 'spectrum') {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
@@ -85,7 +107,7 @@ coreModule.directive('statusHeatmapLegend', function() {
};
});
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
function drawColorLegend(elem, colorScheme, rangeFrom: number, rangeTo:number, maxValue: number, minValue:number) {
let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
@@ -105,7 +127,7 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
.enter().append("rect")
// translate from range space into pixels
// and shift all rectangles to the right by 10
.attr("x", d => d * widthFactor+10)
.attr("x", d => ((d - rangeFrom) * widthFactor)+10)
.attr("y", 0)
// rectangles are slightly overlaped to prevent gaps
.attr("width", LEGEND_STEP_WIDTH+1)
@@ -158,7 +180,7 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteExtraSeries) {
let valuesNumber = thresholds.length;
// graph width as a fallback
const $heatmap = $(elem).parent().parent().parent().find('.status-heatmap-panel');
const $heatmap = $(elem).parent().parent().parent().find('.statusmap-panel');
const graphWidthAttr = $heatmap.find('svg').attr("width");
let graphWidth = parseInt(graphWidthAttr);
@@ -202,7 +224,7 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteExtraSeries) {
}
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
function drawLegendValues(elem, colorScale, rangeFrom: number, rangeTo: number, maxValue: number, minValue: number, legendWidth: number) {
let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0));
@@ -211,10 +233,10 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
}
let legendValueScale = d3.scaleLinear()
.domain([0, rangeTo])
.domain([rangeFrom, rangeTo])
.range([0, legendWidth]);
let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue);
let ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
let xAxis = d3.axisBottom(legendValueScale)
.tickValues(ticks)
.tickSize(2);
@@ -401,7 +423,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
let ticks:any = [];
for (let i = 0; i < ticksNum; i++) {
let current = tickStepSize * i;
let current = tickStepSize * i + rangeFrom;
// Add user-defined min and max if it had been set
if (isValueCloseTo(minValue, current, tickStepSize)) {
ticks.push(minValue);
@@ -415,7 +437,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
} else if (maxValue < current) {
ticks.push(maxValue);
}
ticks.push(tickStepSize * i);
ticks.push(current);
}
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
ticks.push(maxValue);
+30 -27
View File
@@ -1,5 +1,6 @@
import _ from 'lodash';
import {StatusHeatmapCtrl} from "./status_heatmap_ctrl";
import { Bucket } from "./statusmap_data";
import { StatusHeatmapCtrl } from "./module";
interface Tooltip {
tooltip: string;
@@ -144,40 +145,42 @@ export class ColorModeDiscrete {
updateCardsValuesHasColorInfoSingle() {
if (!this.panelCtrl.cardsData) {
if (!this.panelCtrl.bucketMatrix) {
return;
}
this.panelCtrl.cardsData.noColorDefined = false;
var cards = this.panelCtrl.cardsData.cards;
for (var i = 0; i < cards.length; i++) {
cards[i].noColorDefined = false;
var values = cards[i].value;
var threshold = this.getMatchedThreshold(values);
if (!threshold || !threshold.color || threshold.color == "") {
cards[i].noColorDefined = true;
this.panelCtrl.cardsData.noColorDefined = true;
}
}
this.panelCtrl.bucketMatrix.noColorDefined = false;
this.panelCtrl.bucketMatrix.targets.map((target:string) => {
this.panelCtrl.bucketMatrix.buckets[target].map((bucket:Bucket) => {
bucket.noColorDefined = false;
let threshold = this.getMatchedThreshold(bucket.value);
if (!threshold || !threshold.color || threshold.color == "") {
bucket.noColorDefined = true;
this.panelCtrl.bucketMatrix.noColorDefined = true;
}
});
});
}
updateCardsValuesHasColorInfo() {
if (!this.panelCtrl.cardsData) {
if (!this.panelCtrl.bucketMatrix) {
return
}
this.panelCtrl.cardsData.noColorDefined = false;
let cards = this.panelCtrl.cardsData.cards;
for (let i=0; i<cards.length; i++) {
cards[i].noColorDefined = false;
let values = cards[i].values;
for (let j=0; j<values.length; j++) {
let threshold = this.getMatchedThreshold(values[j]);
if (!threshold || !threshold.color || threshold.color == "") {
cards[i].noColorDefined = true;
this.panelCtrl.cardsData.noColorDefined = true;
break;
this.panelCtrl.bucketMatrix.noColorDefined = false;
this.panelCtrl.bucketMatrix.targets.map((target:string) => {
this.panelCtrl.bucketMatrix.buckets[target].map((bucket:Bucket) => {
bucket.noColorDefined = false;
for (let j=0; j<bucket.values.length; j++) {
let threshold = this.getMatchedThreshold(bucket.values[j]);
if (!threshold || !threshold.color || threshold.color == "") {
bucket.noColorDefined = true;
this.panelCtrl.bucketMatrix.noColorDefined = true;
break;
}
}
}
}
});
});
}
getMatchedThreshold(value) {
+1 -1
View File
@@ -14,7 +14,7 @@
}
}
.status-heatmap-panel {
.statusmap-panel {
position: relative;
.axis .tick {
+4 -2
View File
@@ -1,12 +1,14 @@
<div class="status-heatmap-wrapper">
<div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined || ctrl.noDatapoints">
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
<span class="small" ng-if="ctrl.noDatapoints" bs-tooltip="'{{ctrl.dataWarnings.noDatapoints.tip}}'">{{ctrl.dataWarnings.noDatapoints.title}}</span>
</div>
<div class="status-heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
<div class="statusmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div>
<div class="status-heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
<status-heatmap-legend></status-heatmap-legend>
+409 -119
View File
@@ -12,7 +12,7 @@ import {loadPluginCss} from 'app/plugins/sdk';
// Types
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
import {CardsStorage, Card} from './statusmap_data';
import { Bucket, BucketMatrix } from './statusmap_data';
import rendering from './rendering';
// import aggregates, { aggregatesMap } from './aggregates';
// import fragments, { fragmentsMap } from './fragments';
@@ -21,13 +21,9 @@ import {statusHeatmapOptionsEditor} from './options_editor';
import {ColorModeDiscrete} from "./color_mode_discrete";
import { ExtraSeriesFormat, ExtraSeriesFormatValue } from './extra_series_format';
const CANVAS = 'CANVAS';
const SVG = 'SVG';
const VALUE_INDEX = 0,
TIME_INDEX = 1;
const renderer = CANVAS;
const colorSchemes = [
// Diverging
{ name: 'Spectral', value: 'interpolateSpectral', invert: 'always' },
@@ -60,22 +56,6 @@ const colorSchemes = [
let colorModes = ['opacity', 'spectrum', 'discrete'];
let opacityScales = ['linear', 'sqrt'];
interface DataWarning {
title: string;
tip: string;
}
interface DataWarnings {
noColorDefined: DataWarning;
multipleValues: DataWarning;
}
interface ColorThreshold {
}
loadPluginCss({
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
@@ -85,17 +65,22 @@ loadPluginCss({
class StatusHeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
data: any;
bucketMatrix: BucketMatrix;
graph: any;
discreteHelper: ColorModeDiscrete;
opacityScales: any = [];
colorModes: any = [];
colorSchemes: any = [];
unitFormats: any;
data: any;
cardsData: any;
graph: any;
dataWarnings: {[warningId: string]: {title: string, tip: string}} = {};
multipleValues: boolean;
noColorDefined: boolean;
noDatapoints: boolean;
discreteExtraSeries: ColorModeDiscrete;
dataWarnings: DataWarnings;
extraSeriesFormats: any = [];
annotations: object[] = [];
@@ -124,14 +109,12 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
},
xAxis: {
show: true,
showWeekends: true,
minBucketWidthToShowWeekends: 4,
showCrosshair: true,
labelFormat: '%a %m/%d'
},
yAxis: {
show: true,
showCrosshair: false
minWidth: -1,
maxWidth: -1,
},
tooltip: {
show: true
@@ -199,13 +182,17 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.discreteExtraSeries = new ColorModeDiscrete($scope);
this.dataWarnings = {
"noColorDefined": {
noColorDefined: {
title: 'Data has value with undefined color',
tip: 'Check metric values, color values or define a new color',
},
"multipleValues": {
multipleValues: {
title: 'Data has multiple values for one target',
tip: 'Change targets definitions or set "use max value"',
},
noDatapoints: {
title: 'No data points',
tip: 'No datapoints returned from data query',
}
};
@@ -225,7 +212,15 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.events.on('onChangeType', this.onChangeType.bind(this));
}
onRenderComplete(data):void {
onRenderComplete(data: any):void {
// console.log({
// data: this.data,
// bucketMatrix: this.bucketMatrix,
// chartWidth: data.chartWidth,
// from: this.range.from.valueOf(),
// to: this.range.to.valueOf()
// })
this.graph.chartWidth = data.chartWidth;
this.renderingCompleted();
}
@@ -244,12 +239,21 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
}
}
// getChartWidth returns an approximation of chart canvas width or
// a saved value calculated during a render.
getChartWidth():number {
if (this.graph.chartWidth > 0) {
return this.graph.chartWidth;
}
const wndWidth = $(window).width();
// gripPos.w is a width in grid's measurements. Grid size in Grafana is 24.
const panelWidthFactor = this.panel.gridPos.w / 24;
const panelWidth = Math.ceil(wndWidth * panelWidthFactor);
// approximate chartWidth because y axis ticks not rendered yet on first data receive.
// approximate width of the chart draw canvas:
// - y axis ticks are not rendered yet on first data receive,
// so choose 200 as a decent value for y legend width
// - chartWidth can not be lower than the half of the panel width.
const chartWidth = _.max([
panelWidth - 200,
panelWidth/2
@@ -258,7 +262,9 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
return chartWidth!;
}
// override calculateInterval for discrete color mode
// calculateInterval is called on 'refresh' to calculate an interval
// for datasource.
// It is override of calculateInterval from MetricsPanelCtrl.
calculateInterval() {
let chartWidth = this.getChartWidth();
@@ -298,6 +304,19 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.intervalMs = intervalMs;
this.interval = interval;
// Get final buckets count after interval is adjusted
//this.xBucketsCount = Math.floor(rangeMs / intervalMs);
// console.log("calculateInterval: ", {
// interval: this.interval,
// intervalMs: this.intervalMs,
// rangeMs: rangeMs,
// from: this.range.from.valueOf(),
// to: this.range.to.valueOf(),
// numIntervals: rangeMs/this.intervalMs,
// maxCardsCount: maxCardsCount,
// });
}
issueQueries(datasource: any) {
@@ -329,7 +348,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
// so we need to save-restore this.panel.interval.
issueQueriesWithInterval(datasource: any, interval: any) {
var origInterval = this.panel.interval;
this.panel.interval = this.interval;
this.panel.interval = interval;
var res = super.issueQueries(datasource);
this.panel.interval = origInterval;
return res;
@@ -337,8 +356,9 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
onDataReceived(dataList: any) {
this.data = dataList;
this.cardsData = this.convertToCards(this.data);
this.data = dataList;
this.bucketMatrix = this.convertDataToBuckets(dataList, this.range.from.valueOf(), this.range.to.valueOf(), this.intervalMs, true);
this.noDatapoints = this.bucketMatrix.noDatapoints;
this.annotationsPromise.then(
(result: { alertState: any; annotations: any }) => {
@@ -357,8 +377,6 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.render();
}
);
//this.render();
}
onInitEditMode() {
@@ -366,13 +384,19 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.unitFormats = kbn.getUnitFormats();
}
// onRender will be called before StatusmapRenderer.onRender.
// Decide if warning should be displayed over cards.
onRender() {
if (!this.range || !this.data) { return; }
//console.log('OnRender');
if (!this.range || !this.data) {
this.noDatapoints = true;
return;
}
this.multipleValues = false;
if (!this.panel.useMax) {
if (this.cardsData) {
this.multipleValues = this.cardsData.multipleValues;
if (this.bucketMatrix) {
this.multipleValues = this.bucketMatrix.multipleValues;
}
}
@@ -383,10 +407,17 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
} else {
this.discreteExtraSeries.updateCardsValuesHasColorInfoSingle();
}
if (this.cardsData) {
this.noColorDefined = this.cardsData.noColorDefined;
if (this.bucketMatrix) {
this.noColorDefined = this.bucketMatrix.noColorDefined;
}
}
this.noDatapoints = false;
if (this.bucketMatrix) {
this.noDatapoints = this.bucketMatrix.noDatapoints;
}
//console.log(this);
}
onCardColorChange(newColor) {
@@ -475,85 +506,344 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
return var_time;
}
// group values into buckets by target
convertToCards(data) {
let cardsData = <CardsStorage> {
cards: [],
xBucketSize: 0,
yBucketSize: 0,
maxValue: 0,
minValue: 0,
multipleValues: false,
noColorDefined: false,
targets: [], // array of available unique targets
targetIndex: {} // indices in data array for each of available unique targets
};
// convertToBuckets groups values in data into buckets by target and timestamp.
//
// data is a result from datasource. It is an array of timeseries and tables:
/* [
// timeseries
{
target: "query alias",
refId: "A",
datapoints: [
[0, 1582681239911],
[3, 158....],
...
],
tags?:{key: value,...}
},
// table
{
name: "table name",
refId: "B",
columns: [
{text: "id"},
{text: "info"},
...
],
rows: [
[1, "123"],
[2, "44411"],
...
]
},
...
]
if (!data || data.length == 0) { return cardsData;}
to and from — a time range of the panel.
intervalMs — a calculated interval. It is used to split a time range.
*/
convertDataToBuckets(data:any, from:number, to:number, intervalMs: number, mostRecentBucket: boolean):BucketMatrix {
let bucketMatrix = new BucketMatrix();
bucketMatrix.rangeMs = to - from;
bucketMatrix.intervalMs = intervalMs;
// Collect uniq timestamps from data and spread over targets and timestamps
// collect uniq targets and their indices
_.map(data, (d, i) => {
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
});
// TODO add some logic for targets heirarchy
cardsData.targets = _.keys(cardsData.targetIndex);
cardsData.yBucketSize = cardsData.targets.length;
// Maximum number of buckets over x axis
cardsData.xBucketSize = _.max(_.map(data, d => d.datapoints.length));
// Collect all values for each bucket from datapoints with similar target.
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
for(let i = 0; i < cardsData.targets.length; i++) {
let target = cardsData.targets[i];
for (let j = 0; j < cardsData.xBucketSize; j++) {
let card = new Card();
card.id = i*cardsData.xBucketSize + j;
card.values = [];
card.columns = [];
card.multipleValues = false;
card.noColorDefined = false;
card.y = target;
card.x = -1;
// collect values from all timeseries with target
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
let s = data[cardsData.targetIndex[target][si]];
if (s.datapoints.length <= j) {
continue;
}
let datapoint = s.datapoints[j];
if (card.values.length === 0) {
card.x = datapoint[TIME_INDEX];
}
card.values.push(datapoint[VALUE_INDEX]);
}
card.minValue = _.min(card.values);
card.maxValue = _.max(card.values);
if (card.values.length > 1) {
cardsData.multipleValues = true;
card.multipleValues = true;
card.value = this.panel.seriesFilterIndex != -1 ? card.values[this.panel.seriesFilterIndex] : card.maxValue;
} else {
card.value = card.maxValue; // max value by default
}
if (cardsData.maxValue < card.maxValue)
cardsData.maxValue = card.maxValue;
if (cardsData.minValue > card.minValue)
cardsData.minValue = card.minValue;
if (card.x != -1) {
cardsData.cards.push(card);
}
}
if (!data || data.length == 0) {
// Mimic heatmap and graph 'no data' labels.
bucketMatrix.targets = [
"1.0", "0.0", "-1.0"
];
bucketMatrix.buckets["1.0"] = [];
bucketMatrix.buckets["0.0"] = [];
bucketMatrix.buckets["-1.0"] = [];
bucketMatrix.xBucketSize = 42;
bucketMatrix.noDatapoints = true;
return bucketMatrix;
}
return cardsData;
let targetIndex: {[target: string]: number[]} = {};
// Group indicies of elements in data by target (y label).
// lodash version:
//_.map(data, (d, i) => {
// targetIndex[d.target] = _.concat(_.toArray(targetIndex[d.target]), i);
//});
data.map((queryResult: any, i: number) => {
let yLabel = queryResult.target;
if (!targetIndex.hasOwnProperty(yLabel)) {
targetIndex[yLabel] = [];
}
targetIndex[yLabel].push(i);
});
let targetKeys = _.keys(targetIndex);
//console.log ("targetIndex: ", targetIndex, "targetKeys: ", targetKeys);
let targetTimestampRanges: {[target: string]: {[timestamp: number]: number[]}} = {};
// Collect all timestamps for each target.
// Make map timestamp => [from, to]. from == previous ts, to == ts from datapoint.
targetKeys.map((target) => {
let targetTimestamps: any[] = [];
for (let si = 0; si < targetIndex[target].length; si++) {
let s = data[targetIndex[target][si]];
_.map(s.datapoints, (datapoint, idx) => {
targetTimestamps.push(datapoint[TIME_INDEX]-from);
})
}
//console.log("timestamps['"+target+"'] = ", targetTimestamps);
targetTimestamps = _.uniq(targetTimestamps);
//console.log("uniq timestamps['"+target+"'] = ", targetTimestamps);
targetTimestampRanges[target] = [];
for (let i = targetTimestamps.length-1 ; i>=0; i-- ) {
let tsTo = targetTimestamps[i];
let tsFrom = 0;
if (tsTo < 0) {
tsFrom = tsTo - intervalMs;
} else {
if (i-1 >= 0) {
// Set from to previous timestamp + 1ms;
tsFrom = targetTimestamps[i-1]+1;
// tfTo - tfFrom should not be more than intervalMs
let minFrom = tsTo - intervalMs;
if (tsFrom < minFrom) {
tsFrom = minFrom;
}
}
}
targetTimestampRanges[target][tsTo] = [tsFrom, tsTo];
}
});
// console.log ("targetTimestampRanges: ", targetTimestampRanges);
// Create empty buckets using intervalMs to calculate ranges.
// If mostRecentBucket is set, create a bucket with a range "to":"to"
// to store most recent values.
targetKeys.map((target) => {
let targetEmptyBuckets: any[] = [];
let lastTs = to-from;
if (mostRecentBucket) {
let topBucket = new Bucket();
topBucket.yLabel = target;
topBucket.relTo = lastTs;
topBucket.relFrom = lastTs;
topBucket.values = [];
topBucket.mostRecent = true;
if (targetTimestampRanges[target].hasOwnProperty(lastTs)) {
topBucket.relFrom = targetTimestampRanges[target][lastTs][0];
lastTs = topBucket.relFrom;
}
topBucket.to = topBucket.relTo+from;
topBucket.from = topBucket.relFrom+from;
targetEmptyBuckets.push(topBucket);
}
let idx = 0;
let bucketFrom: number = 0;
while (bucketFrom >= 0) {
let b = new Bucket();
b.yLabel = target;
b.relTo = lastTs - idx*intervalMs;
b.relFrom = lastTs - ((idx+1) * intervalMs);
b.to = b.relTo+from;
b.from = b.relFrom+from;
b.values = [];
bucketFrom = b.relFrom;
targetEmptyBuckets.push(b);
idx++;
}
targetEmptyBuckets.map((bucket, i) => {
bucket.xid = i;
});
bucketMatrix.buckets[target] = targetEmptyBuckets;
});
//console.log ("bucketMatrix: ", bucketMatrix);
// Put values into buckets.
bucketMatrix.minValue = Number.MAX_VALUE;
bucketMatrix.maxValue = Number.MIN_SAFE_INTEGER;
targetKeys.map((target) => {
targetIndex[target].map((dataIndex) => {
let s = data[dataIndex];
s.datapoints.map((dp: any) => {
for (let i = 0; i < bucketMatrix.buckets[target].length; i++) {
if (bucketMatrix.buckets[target][i].belong(dp[TIME_INDEX])) {
bucketMatrix.buckets[target][i].put(dp[VALUE_INDEX]);
}
}
});
});
bucketMatrix.buckets[target].map((bucket) => {
bucket.minValue = _.min(bucket.values);
bucket.maxValue = _.max(bucket.values);
if (bucket.minValue < bucketMatrix.minValue) {
bucketMatrix.minValue = bucket.minValue;
}
if (bucket.maxValue > bucketMatrix.maxValue) {
bucketMatrix.maxValue = bucket.maxValue;
}
bucket.value = bucket.maxValue;
if (bucket.values.length > 1) {
bucketMatrix.multipleValues = true;
bucket.multipleValues = true;
bucket.value = this.panel.seriesFilterIndex != -1 ? bucket.values[this.panel.seriesFilterIndex] : bucket.maxValue;
}
})
});
bucketMatrix.xBucketSize = Number.MIN_SAFE_INTEGER;
targetKeys.map((target) => {
let bucketsLen: number = bucketMatrix.buckets[target].length;
if (bucketsLen > bucketMatrix.xBucketSize) {
bucketMatrix.xBucketSize = bucketsLen;
}
});
//console.log ("bucketMatrix with values: ", bucketMatrix);
bucketMatrix.targets = targetKeys;
return bucketMatrix;
this.bucketMatrix = bucketMatrix;
// Collect all values for each bucket from datapoints with similar target.
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
// for(let i = 0; i < cardsData.targets.length; i++) {
// let target = cardsData.targets[i];
// for (let j = 0; j < cardsData.xBucketSize; j++) {
// let card = new Card();
// card.id = i*cardsData.xBucketSize + j;
// card.values = [];
// card.y = target;
// card.x = -1;
// // collect values from all timeseries with target
// for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
// let s = data[cardsData.targetIndex[target][si]];
// if (s.datapoints.length <= j) {
// continue;
// }
// let datapoint = s.datapoints[j];
// if (card.values.length === 0) {
// card.x = datapoint[TIME_INDEX];
// }
// card.values.push(datapoint[VALUE_INDEX]);
// }
// card.minValue = _.min(card.values);
// card.maxValue = _.max(card.values);
// if (card.values.length > 1) {
// cardsData.multipleValues = true;
// card.multipleValues = true;
// card.value = card.maxValue; // max value by default
// } else {
// card.value = card.maxValue; // max value by default
// }
// if (cardsData.maxValue < card.maxValue)
// cardsData.maxValue = card.maxValue;
// if (cardsData.minValue > card.minValue)
// cardsData.minValue = card.minValue;
// if (card.x != -1) {
// cardsData.cards.push(card);
// }
// }
// }
// let cardsData = <CardsStorage> {
// cards: [],
// xBucketSize: 0,
// yBucketSize: 0,
// maxValue: 0,
// minValue: 0,
// multipleValues: false,
// noColorDefined: false,
// targets: [], // array of available unique targets
// targetIndex: {} // indices in data array for each of available unique targets
// };
// if (!data || data.length == 0) { return cardsData;}
// // Collect uniq timestamps from data and spread over targets and timestamps
// // collect uniq targets and their indices
// _.map(data, (d, i) => {
// cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
// });
// // TODO add some logic for targets heirarchy
// cardsData.targets = _.keys(cardsData.targetIndex);
// cardsData.yBucketSize = cardsData.targets.length;
// // Maximum number of buckets over x axis
// cardsData.xBucketSize = _.max(_.map(data, d => d.datapoints.length));
// // Collect all values for each bucket from datapoints with similar target.
// // TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
// for(let i = 0; i < cardsData.targets.length; i++) {
// let target = cardsData.targets[i];
// for (let j = 0; j < cardsData.xBucketSize; j++) {
// let card = new Card();
// card.id = i*cardsData.xBucketSize + j;
// card.values = [];
// card.y = target;
// card.x = -1;
// // collect values from all timeseries with target
// for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
// let s = data[cardsData.targetIndex[target][si]];
// if (s.datapoints.length <= j) {
// continue;
// }
// let datapoint = s.datapoints[j];
// if (card.values.length === 0) {
// card.x = datapoint[TIME_INDEX];
// }
// card.values.push(datapoint[VALUE_INDEX]);
// }
// card.minValue = _.min(card.values);
// card.maxValue = _.max(card.values);
// if (card.values.length > 1) {
// cardsData.multipleValues = true;
// card.multipleValues = true;
// card.value = card.maxValue; // max value by default
// } else {
// card.value = card.maxValue; // max value by default
// }
// if (cardsData.maxValue < card.maxValue)
// cardsData.maxValue = card.maxValue;
// if (cardsData.minValue > card.minValue)
// cardsData.minValue = card.minValue;
// if (card.x != -1) {
// cardsData.cards.push(card);
// }
// }
// }
// return cardsData;
}
}
+30 -12
View File
@@ -148,8 +148,30 @@
label="Show tooltip"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-8">Y axis sort</label>
<gf-form-switch class="gf-form" label-class="width-8"
label="Show X axis"
checked="ctrl.panel.xAxis.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-8"
label="Show Y axis"
checked="ctrl.panel.yAxis.show" on-change="ctrl.render()">
</gf-form-switch>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Min width</label>
<input type="number" ng-model="ctrl.panel.xAxis.minWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
</div>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Max width</label>
<input type="number" ng-model="ctrl.panel.xAxis.maxWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Buckets</h5>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Rows sort</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-8"
ng-model="ctrl.panel.yAxisSort"
@@ -157,12 +179,8 @@
ng-change="ctrl.render()"></select>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Bucket</h5>
<gf-form-switch class="gf-form" label-class="width-9"
<gf-form-switch class="gf-form" label-class="width-8"
label="Multiple values"
checked="ctrl.panel.useMax" on-change="ctrl.render()">
</gf-form-switch>
@@ -171,25 +189,25 @@
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Display nulls</label>
<label class="gf-form-label width-8">Display nulls</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-9" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['as empty', 'as zero']" ng-change="ctrl.render()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Min width for 1/1</label>
<label class="gf-form-label width-8">Min width</label>
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">V spacing</label>
<label class="gf-form-label width-8">V spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards vertical spacing in pixels'" ng-model="ctrl.panel.cards.cardVSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">H spacing</label>
<label class="gf-form-label width-8">H spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Rounding</label>
<label class="gf-form-label width-8">Rounding</label>
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
</div>
+97 -66
View File
@@ -9,6 +9,8 @@ import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
import {StatusmapTooltip} from './tooltip';
import {StatusHeatmapTooltipExtraSeries} from './tooltipextraseries';
import {AnnotationTooltip} from './annotations';
import { Bucket, BucketMatrix } from './statusmap_data';
import { StatusHeatmapCtrl } from './module';
let MIN_CARD_SIZE = 5,
CARD_H_SPACING = 2,
@@ -25,6 +27,18 @@ export default function rendering(scope: any, elem: any, attrs: any, ctrl: any)
return new StatusmapRenderer(scope, elem, attrs, ctrl);
}
class Statusmap {
$svg: any;
svg: any;
bucketMatrix: BucketMatrix;
timeRange: {from: number, to: number} = {from:0, to:0};
constructor() {
}
}
export class StatusmapRenderer {
width: number = 0;
height: number = 0;
@@ -46,8 +60,8 @@ export class StatusmapRenderer {
mouseUpHandler: any;
xGridSize: number = 0;
yGridSize: number = 0;
data: any;
cardsData: any;
bucketMatrix: BucketMatrix;
panel: any;
$heatmap: any;
tooltip: StatusmapTooltip;
@@ -62,9 +76,9 @@ export class StatusmapRenderer {
margin: any;
dataRangeWidingFactor: number = DATA_RANGE_WIDING_FACTOR;
constructor(private scope: any, private elem: any, attrs: any, private ctrl: any) {
constructor(private scope: any, private elem: any, attrs: any, private ctrl: StatusHeatmapCtrl) {
// $heatmap is JQuery object, but heatmap is D3
this.$heatmap = this.elem.find('.status-heatmap-panel');
this.$heatmap = this.elem.find('.statusmap-panel');
this.tooltip = new StatusmapTooltip(this.$heatmap, this.scope);
this.tooltipExtraSeries = new StatusHeatmapTooltipExtraSeries(this.$heatmap, this.scope);
this.annotationTooltip = new AnnotationTooltip(this.$heatmap, this.scope);
@@ -88,7 +102,8 @@ export class StatusmapRenderer {
// Selection and crosshair //
/////////////////////////////
// Shared crosshair and tooltip
// Shared crosshair and tooltip this.empty = true;
appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope);
appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope);
@@ -114,7 +129,7 @@ export class StatusmapRenderer {
}
setElementHeight() {
setElementHeight(): boolean {
try {
var height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
if (_.isString(height)) {
@@ -131,7 +146,7 @@ export class StatusmapRenderer {
}
}
getYAxisWidth(elem: any) {
getYAxisWidth(elem: any): number {
const axisText = elem.selectAll(".axis-y text").nodes();
const maxTextWidth = _.max(_.map(axisText, text => {
// Use SVG getBBox method
@@ -141,7 +156,7 @@ export class StatusmapRenderer {
return Math.ceil(maxTextWidth);
}
getXAxisHeight(elem: any) {
getXAxisHeight(elem: any): number {
let axisLine = elem.select(".axis-x line");
if (!axisLine.empty()) {
let axisLinePosition = parseFloat(elem.select(".axis-x line").attr("y2"));
@@ -155,6 +170,10 @@ export class StatusmapRenderer {
addXAxis() {
// Scale timestamps to cards centers
//this.scope.xScale = this.xScale = d3.scaleTime()
// .domain([this.timeRange.from, this.timeRange.to])
// .range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
// Buckets without the most recent
this.scope.xScale = this.xScale = d3.scaleTime()
.domain([this.timeRange.from, this.timeRange.to])
.range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
@@ -192,7 +211,7 @@ export class StatusmapRenderer {
}
// divide chart height by ticks for cards drawing
getYScale(ticks) {
getYScale(ticks: any[]) {
let range:any[] = [];
let step = this.chartHeight / ticks.length;
// svg has y=0 on the top, so top card should have a minimal value in range
@@ -206,7 +225,7 @@ export class StatusmapRenderer {
}
// divide chart height by ticks with offset for ticks drawing
getYAxisScale(ticks) {
getYAxisScale(ticks: any[]) {
let range:any[] = [];
let step = this.chartHeight / ticks.length;
// svg has y=0 on the top, so top tick should have a minimal value in range
@@ -220,12 +239,7 @@ export class StatusmapRenderer {
}
addYAxis() {
let ticks = _.uniq(_.map(this.data, d => d.target));
// Set default Y min and max if no data
if (_.isEmpty(this.data)) {
ticks = [''];
}
let ticks = this.bucketMatrix.targets;
if (this.panel.yAxisSort == 'a → z') {
ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
@@ -258,7 +272,7 @@ export class StatusmapRenderer {
}
// Wide Y values range and adjust to bucket size
wideYAxisRange(min, max, tickInterval) {
wideYAxisRange(min: number, max: number, tickInterval: number) {
let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2;
let y_min, y_max;
@@ -288,7 +302,7 @@ export class StatusmapRenderer {
// Create svg element, add axes and
// calculate sizes for cards drawing
addHeatmapCanvas() {
addStatusmapCanvas() {
let heatmap_elem = this.$heatmap[0];
this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
@@ -312,7 +326,10 @@ export class StatusmapRenderer {
this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND;
// calculate yOffset for YAxis
this.yGridSize = Math.floor(this.chartHeight / this.cardsData.yBucketSize);
this.yGridSize = this.chartHeight;
if (this.bucketMatrix.targets.length > 0) {
this.yGridSize = Math.floor(this.chartHeight / this.bucketMatrix.targets.length);
}
this.cardHeight = this.yGridSize ? this.yGridSize - this.cardVSpacing : 0;
this.yOffset = this.cardHeight / 2;
@@ -323,7 +340,7 @@ export class StatusmapRenderer {
// TODO allow per-y cardWidth!
// we need to fill chartWidth with xBucketSize cards.
this.xGridSize = this.chartWidth / (this.cardsData.xBucketSize+1);
this.xGridSize = this.chartWidth / (this.bucketMatrix.xBucketSize+1);
this.cardWidth = this.xGridSize - this.cardHSpacing;
this.addXAxis();
@@ -338,36 +355,41 @@ export class StatusmapRenderer {
}
}
addHeatmap() {
this.addHeatmapCanvas();
let maxValue = this.panel.color.max || this.cardsData.maxValue;
let minValue = this.panel.color.min || this.cardsData.minValue;
addStatusmap():void {
let maxValue = this.panel.color.max || this.bucketMatrix.maxValue;
let minValue = this.panel.color.min || this.bucketMatrix.minValue;
if (this.panel.color.mode !== 'discrete') {
this.colorScale = this.getColorScale(maxValue, minValue);
}
this.setOpacityScale(maxValue);
let cards = this.heatmap.selectAll(".status-heatmap-card").data(this.cardsData.cards);
cards.append("title");
cards = cards.enter().append("rect")
.attr("cardId", c => c.id)
.attr("x", this.getCardX.bind(this))
.attr("width", this.getCardWidth.bind(this))
.attr("y", this.getCardY.bind(this))
.attr("height", this.getCardHeight.bind(this))
.attr("rx", this.cardRound)
.attr("ry", this.cardRound)
.attr("class", "bordered status-heatmap-card")
.style("fill", this.getCardColor.bind(this))
.style("stroke", this.getCardColor.bind(this))
.style("stroke-width", 0)
//.style("stroke-width", getCardStrokeWidth)
//.style("stroke-dasharray", "3,3")
.style("opacity", this.getCardOpacity.bind(this));
// Draw cards from buckets.
this.heatmap.selectAll(".statusmap-cards-row").data(this.bucketMatrix.targets)
.enter()
.selectAll(".statustmap-card")
.data((target:string) => this.bucketMatrix.buckets[target])
.enter()
.append("rect")
.attr("cardId", (b:Bucket) => b.id)
.attr("xid", (b:Bucket) => b.xid)
.attr("yid", (b:Bucket) => b.yLabel)
.attr("x", this.getCardX.bind(this))
.attr("width", this.getCardWidth.bind(this))
.attr("y", this.getCardY.bind(this))
.attr("height", this.getCardHeight.bind(this))
.attr("rx", this.cardRound)
.attr("ry", this.cardRound)
.attr("class", (b:Bucket) => b.isEmpty() ? "empty-card" : "bordered statusmap-card")
.style("fill", this.getCardColor.bind(this))
.style("stroke", this.getCardColor.bind(this))
.style("stroke-width", 0)
//.style("stroke-width", getCardStrokeWidth)
//.style("stroke-dasharray", "3,3")
.style("opacity", this.getCardOpacity.bind(this));
let $cards = this.$heatmap.find(".status-heatmap-card");
// Set mouse events on cards.
let $cards = this.$heatmap.find(".statusmap-card + .bordered");
$cards
.on("mouseenter", (event) => {
this.tooltip.mouseOverBucket = true;
@@ -434,10 +456,12 @@ export class StatusmapRenderer {
}
}
getCardX(d) {
getCardX(b: Bucket) {
let x;
// cx is the center of the card. Card should be placed to the left.
let cx = this.xScale(d.x);
//let cx = this.xScale(d.x);
let rightX = (b.relTo / this.bucketMatrix.rangeMs) * this.chartWidth;
let cx = rightX - this.cardWidth/2;
if (cx - this.cardWidth/2 < 0) {
x = this.yAxisWidth + this.cardHSpacing/2;
@@ -449,9 +473,13 @@ export class StatusmapRenderer {
}
// xScale returns card center. Adjust cardWidth in case of overlaping.
getCardWidth(d) {
getCardWidth(b: Bucket) {
//return 20;
let w;
let cx = this.xScale(d.x);
let rightX = (b.relTo / this.bucketMatrix.rangeMs) * this.chartWidth;
let cx = rightX - this.cardWidth/2;
//let cx = this.xScale(d.x);
if (cx < this.cardWidth/2) {
// Center should not exceed half of card.
@@ -475,12 +503,16 @@ export class StatusmapRenderer {
return w;
}
getCardY(d) {
return this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardVSpacing/2;
// Top y for card.
// yScale gives ???
//
getCardY(b: Bucket) {
return this.yScale(b.yLabel) + this.chartTop - this.cardHeight - this.cardVSpacing/2;
}
getCardHeight(d) {
let ys = this.yScale(d.y);
getCardHeight(b: Bucket) {
//return 20;
let ys = this.yScale(b.yLabel);
let y = ys + this.chartTop - this.cardHeight - this.cardVSpacing/2;
let h = this.cardHeight;
@@ -505,32 +537,32 @@ export class StatusmapRenderer {
return h;
}
getCardColor(d) {
getCardColor(b: Bucket) {
if (this.panel.color.mode === 'opacity') {
return this.panel.color.cardColor;
} else if (this.panel.color.mode === 'spectrum') {
return this.colorScale(d.value);
return this.colorScale(b.value);
} else if (this.panel.color.mode === 'discrete') {
if (this.panel.seriesFilterIndex != -1 || this.panel.seriesFilterIndex != null) {
return this.ctrl.discreteExtraSeries.getBucketColorSingle(d.values[this.panel.seriesFilterIndex]);
return this.ctrl.discreteExtraSeries.getBucketColorSingle(b.values[this.panel.seriesFilterIndex]);
} else {
return this.ctrl.discreteExtraSeries.getBucketColor(d.values);
return this.ctrl.discreteExtraSeries.getBucketColor(b.values);
}
}
}
getCardOpacity(d) {
if (this.panel.nullPointMode === 'as empty' && d.value == null ) {
getCardOpacity(b: Bucket) {
if (this.panel.nullPointMode === 'as empty' && b.value == null ) {
return 0;
}
if (this.panel.color.mode === 'opacity') {
return this.opacityScale(d.value);
return this.opacityScale(b.value);
} else {
return 1;
}
}
getCardStrokeWidth(d) {
getCardStrokeWidth(b: Bucket) {
if (this.panel.color.mode === 'discrete') {
return '1';
}
@@ -609,7 +641,7 @@ export class StatusmapRenderer {
//const pos = this.getEventPos(event, offset);
this.emitGraphHoverEvent(event);
this.drawCrosshair(offset.x);
this.tooltip.show(event); //, data); // pos, this.data
this.tooltip.show(event);
this.annotationTooltip.show(event);
}
}
@@ -725,22 +757,21 @@ export class StatusmapRenderer {
render() {
this.data = this.ctrl.data;
this.panel = this.ctrl.panel;
this.timeRange = this.ctrl.range;
this.cardsData = this.ctrl.cardsData;
this.bucketMatrix = this.ctrl.bucketMatrix;
if (!this.data || !this.cardsData || !this.setElementHeight()) {
if (!this.bucketMatrix || !this.setElementHeight()) {
return;
}
// Draw default axes and return if no data
if (_.isEmpty(this.cardsData.cards)) {
this.addHeatmapCanvas();
this.addStatusmapCanvas();
if (this.bucketMatrix.noDatapoints) {
return;
}
this.addHeatmap();
this.addStatusmap();
this.scope.yAxisWidth = this.yAxisWidth;
this.scope.xAxisHeight = this.xAxisHeight;
this.scope.chartHeight = this.chartHeight;
+91 -41
View File
@@ -1,46 +1,96 @@
// A holder of a group of values
class Bucket {
// uniq id
id: number = 0;
// Array of values in this bucket
values: any[] = [];
columns: any[] = []; // From pr/86
// a bucket has multiple values
multipleValues: boolean = false;
// a bucket has values that has no mapped color
noColorDefined: boolean = false;
// y label
y: string = "";
yLabel: string = "";
// This value can be used to calculate a x coordinate on a graph
x: number = 0;
xid: number = 0;
// a time range of this bucket
from: number = 0;
to: number = 0;
// to and from relative to real "from"
relFrom: number = 0;
relTo: number = 0;
mostRecent: boolean = false;
// A holder of values
class Card {
// uniq
id: number = 0;
// Array of values in this bucket
values: any[] = [];
columns: any[] = [];
// card has multiple values
multipleValues: boolean = false;
// card has values that has no color
noColorDefined: boolean = false;
//
y: string = "";
//
x: number = 0;
//
minValue: number = 0;
maxValue: number = 0;
value: number = 0;
constructor() {
// Saved minimum and maximum of values in this bucket
minValue: number = 0;
maxValue: number = 0;
// A value if multiple values is not allowed
value: number = 0;
}
}
class CardsStorage {
cards: Card[] = [];
xBucketSize: number = 0;
yBucketSize: number = 0;
maxValue: number;
minValue: number;
multipleValues: boolean = false;
noColorDefined: boolean = false;
targets: string[] = [];
targetIndex: any;
constructor() {
this.maxValue = 0;
this.minValue = 0;
}
constructor() {
}
export {CardsStorage, Card};
belong(ts: number): boolean {
return ts >= this.from && ts <= this.to;
}
put(value: any) {
this.values.push(value);
}
done() {
// calculate min, max, value
}
isEmpty(): boolean {
return this.values.length == 0;
}
}
class BucketMatrix {
// buckets for each y label
buckets: {[yLabel: string]: Bucket[]} = {};
maxValue: number = 0;
minValue: number = 0;
multipleValues: boolean = false;
noColorDefined: boolean = false;
// a flag that indicate that buckets has stub values
noDatapoints: boolean = false;
targets: string[] = [];
rangeMs: number = 0;
intervalMs: number = 0;
xBucketSize: number = 0; // TODO remove: a transition from CardsData
constructor() {
}
get(yid: string, xid: number): Bucket {
if (yid in this.buckets) {
if (xid in this.buckets[yid]) {
return this.buckets[yid][xid];
}
}
return new Bucket();
}
hasData(): boolean {
let hasData = false;
if (this.targets.length > 0) {
this.targets.map((target:string) => {
if (this.buckets[target].length > 0) {
hasData = true;
}
});
}
return hasData;
}
}
export {Bucket, BucketMatrix };
+22 -21
View File
@@ -2,6 +2,8 @@ import d3 from 'd3';
import $ from 'jquery';
import _ from 'lodash';
import { StatusHeatmapCtrl } from './module';
let TOOLTIP_PADDING_X = 30;
let TOOLTIP_PADDING_Y = 5;
@@ -9,7 +11,7 @@ export class StatusmapTooltip {
tooltip: any;
scope: any;
dashboard: any;
panelCtrl: any;
panelCtrl: StatusHeatmapCtrl;
panel: any;
heatmapPanel: any;
mouseOverBucket: any;
@@ -63,28 +65,26 @@ export class StatusmapTooltip {
show(pos) {
if (!this.panel.tooltip.show || !this.tooltip) { return; }
// shared tooltip mode
// TODO support for shared tooltip mode
if (pos.panelRelY) {
return;
}
let cardId = d3.select(pos.target).attr('cardId');
if (!cardId) {
let cardEl = d3.select(pos.target);
let yid = cardEl.attr('yid');
let xid = cardEl.attr('xid');
let bucket = this.panelCtrl.bucketMatrix.get(yid, xid); // TODO string-to-number conversion for xid
if (!bucket || bucket.isEmpty()) {
this.destroy();
return;
}
let card = this.panelCtrl.cardsData.cards[cardId];
if (!card) {
this.destroy();
return;
}
let x = card.x;
let y = card.y;
let value = card.value;
let values = card.values;
let timestamp = bucket.to;
let name = bucket.yLabel;
let value = bucket.value;
let values = bucket.values;
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
let time = this.dashboard.formatDate(+timestamp, tooltipTimeFormat);
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
<div class="statusmap-histogram"></div>`;
@@ -92,7 +92,7 @@ export class StatusmapTooltip {
let statuses;
if (this.panel.color.mode === 'discrete') {
if (this.panel.seriesFilterIndex > 0) {
if (this.panel.seriesFilterIndex >= 0) {
statuses = this.panelCtrl.discreteExtraSeries.convertValueToTooltips(value);
} else {
statuses = this.panelCtrl.discreteExtraSeries.convertValuesToTooltips(values);
@@ -106,7 +106,7 @@ export class StatusmapTooltip {
}
tooltipHtml += `
<div>
name: <b>${y}</b> <br>
name: <b>${name}</b> <br>
${statusesHtml}
<ul>
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}" class="discrete-item">${v.tooltip}</li>`), "")}
@@ -115,12 +115,12 @@ export class StatusmapTooltip {
} else {
if (values.length === 1) {
tooltipHtml += `<div>
name: <b>${y}</b> <br>
name: <b>${name}</b> <br>
value: <b>${value}</b> <br>
</div>`;
} else {
tooltipHtml += `<div>
name: <b>${y}</b> <br>
name: <b>${name}}</b> <br>
values:
<ul>
${_.join(_.map(values, v => `<li>${v}</li>`), "")}
@@ -130,13 +130,14 @@ export class StatusmapTooltip {
}
// "Ambiguous bucket state: Multiple values!";
if (!this.panel.useMax && card.multipleValues) {
if (!this.panel.useMax && bucket.multipleValues) {
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.multipleValues.title}</div>`;
}
// Discrete mode errors
if (this.panel.color.mode === 'discrete') {
if (card.noColorDefined) {
if (bucket.noColorDefined) {
let badValues = this.panelCtrl.discreteExtraSeries.getNotColoredValues(values);
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title}
<br>not colored values: