mirror of
https://github.com/timberjoegithub/grafana-statusmap.git
synced 2026-07-22 15:53:09 +00:00
discrete color mapping
- use max from multiple values or error if no useMax check. - tooltip with mapped values for discrete color mode - tooltip with error messages - global data errors - individual bucket errors in tooltip
This commit is contained in:
@@ -38,6 +38,10 @@ mod.directive('optionsColorLegend', function() {
|
|||||||
} else if (panel.color.mode === 'opacity') {
|
} else if (panel.color.mode === 'opacity') {
|
||||||
let colorOptions = panel.color;
|
let colorOptions = panel.color;
|
||||||
drawSimpleOpacityLegend(elem, colorOptions);
|
drawSimpleOpacityLegend(elem, colorOptions);
|
||||||
|
} else if (panel.color.mode === 'discrete') {
|
||||||
|
let colorOptions = panel.color;
|
||||||
|
let colorScale = getDiscreteColorScale(colorOptions, legendWidth);
|
||||||
|
drawSimpleDiscreteColorLegend(elem, colorOptions, colorScale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +78,9 @@ mod.directive('statusHeatmapLegend', function() {
|
|||||||
} else if (panel.color.mode === 'opacity') {
|
} else if (panel.color.mode === 'opacity') {
|
||||||
let colorOptions = panel.color;
|
let colorOptions = panel.color;
|
||||||
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
|
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
|
||||||
|
} else if (panel.color.mode === 'discrete') {
|
||||||
|
let colorOptions = panel.color;
|
||||||
|
drawDiscreteColorLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +154,37 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
|
|||||||
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
|
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawDiscreteColorLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
clearLegend(elem);
|
||||||
|
|
||||||
|
let thresholds = colorOptions.thresholds;
|
||||||
|
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||||
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
let valuesNumber = thresholds.length;
|
||||||
|
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||||
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
|
|
||||||
|
let widthFactor = 1; // legendWidth / (rangeTo - rangeFrom);
|
||||||
|
|
||||||
|
let colorScale = getDiscreteColorScale(colorOptions, legendWidth);
|
||||||
|
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||||
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
|
.attr("x", d => d * widthFactor)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
|
||||||
|
.attr("height", legendHeight)
|
||||||
|
.attr("stroke-width", 0)
|
||||||
|
.attr("fill", d => colorScale(d));
|
||||||
|
|
||||||
|
drawDiscreteLegendValues(elem, colorOptions, legendWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
|
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
let legend = d3.select(legendElem.get(0));
|
let legend = d3.select(legendElem.get(0));
|
||||||
@@ -176,6 +214,62 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
|
|||||||
legend.select(".axis").select(".domain").remove();
|
legend.select(".axis").select(".domain").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
||||||
|
let thresholds = colorOptions.thresholds;
|
||||||
|
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
|
||||||
|
if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let valuesNumber = thresholds.length;
|
||||||
|
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||||
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
|
|
||||||
|
|
||||||
|
let legendValueScale = d3.scaleLinear()
|
||||||
|
.domain([0, valuesNumber])
|
||||||
|
.range([0, legendWidth]);
|
||||||
|
|
||||||
|
let thresholdValues = [];
|
||||||
|
let thresholdTooltips = [];
|
||||||
|
for (let i = 0; i < thresholds.length; i++) {
|
||||||
|
thresholdValues.push(thresholds[i].value);
|
||||||
|
thresholdTooltips.push(thresholds[i].tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
let xAxis = d3.axisBottom(legendValueScale)
|
||||||
|
.tickValues(d3.range(0, valuesNumber, 1)) //thresholdValues)
|
||||||
|
.tickSize(2)
|
||||||
|
.tickFormat((t) => {
|
||||||
|
let i = Math.floor(t);
|
||||||
|
let v = thresholdTooltips[i];
|
||||||
|
if (v != undefined) {
|
||||||
|
return ""+v;
|
||||||
|
} else {
|
||||||
|
v = thresholdValues[i];
|
||||||
|
if (v != undefined) {
|
||||||
|
return ""+v;
|
||||||
|
} else {
|
||||||
|
return "n/a";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let colorRect = legendElem.find(":first-child");
|
||||||
|
let posY = getSvgElemHeight(legendElem) + 2;
|
||||||
|
let posX = getSvgElemX(colorRect) + Math.floor(rangeStep/2);
|
||||||
|
|
||||||
|
d3.select(legendElem.get(0)).append("g")
|
||||||
|
.attr("class", "axis")
|
||||||
|
.attr("transform", "translate(" + posX + "," + posY + ")")
|
||||||
|
.call(xAxis);
|
||||||
|
|
||||||
|
legend.select(".axis").select(".domain").remove();
|
||||||
|
}
|
||||||
|
|
||||||
function drawSimpleColorLegend(elem, colorScale) {
|
function drawSimpleColorLegend(elem, colorScale) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
@@ -236,6 +330,33 @@ function drawSimpleOpacityLegend(elem, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawSimpleDiscreteColorLegend(elem, colorOptions, colorScale) {
|
||||||
|
let thresholds = colorOptions.thresholds;
|
||||||
|
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
clearLegend(elem);
|
||||||
|
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
if (legendWidth) {
|
||||||
|
let valuesNumber = thresholds.length;
|
||||||
|
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||||
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
|
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
var legendRects = legend.selectAll(".status-heatmap-discrete-legend-rect").data(valuesRange);
|
||||||
|
|
||||||
|
legendRects.enter().append("rect")
|
||||||
|
.attr("x", d => d)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
|
||||||
|
.attr("height", legendHeight)
|
||||||
|
.attr("stroke-width", 0)
|
||||||
|
.attr("fill", d => colorScale(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clearLegend(elem) {
|
function clearLegend(elem) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
legendElem.empty();
|
legendElem.empty();
|
||||||
@@ -252,6 +373,42 @@ function getColorScale(colorScheme, maxValue, minValue = 0) {
|
|||||||
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scale input range to discrete colors to draw a legend
|
||||||
|
function getDiscreteColorScale(colorOptions, maxValue, minValue = 0) {
|
||||||
|
let start = minValue;
|
||||||
|
let end = maxValue;
|
||||||
|
|
||||||
|
let thresholdValues = [];
|
||||||
|
let thresholdColors = [];
|
||||||
|
for (let i = 0; i < colorOptions.thresholds.length; i++) {
|
||||||
|
thresholdColors.push(colorOptions.thresholds[i].color);
|
||||||
|
thresholdValues.push(colorOptions.thresholds[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO sort colors by value and index?
|
||||||
|
|
||||||
|
let thresholdScaler = (d) => {
|
||||||
|
let color = thresholdColors[Math.floor(d)];
|
||||||
|
if (color != undefined) {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
return 'rgba(0,0,0,0)';
|
||||||
|
// for (let i = 0; i < thresholdValues.length; i++ ) {
|
||||||
|
// if (d == thresholdValues[i]) {
|
||||||
|
// return thresholdColors[i];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return thresholdColors[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
let inputRangeScaler = d3.scaleLinear().domain([start, end]).range([0, thresholdColors.length+1]);
|
||||||
|
|
||||||
|
// scale min-max to 0 - max-thrs-value
|
||||||
|
return function(d) {
|
||||||
|
return thresholdScaler(inputRangeScaler(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getOpacityScale(options, maxValue, minValue = 0) {
|
function getOpacityScale(options, maxValue, minValue = 0) {
|
||||||
let legendOpacityScale;
|
let legendOpacityScale;
|
||||||
if (options.colorScale === 'linear') {
|
if (options.colorScale === 'linear') {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
// Helper methods to handle discrete color mode
|
||||||
|
export class ColorModeDiscrete {
|
||||||
|
constructor(scope) {
|
||||||
|
this.scope = scope;
|
||||||
|
this.panelCtrl = scope.ctrl;
|
||||||
|
this.panel = scope.ctrl.panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get tooltip for each value ordered by thresholds priority
|
||||||
|
convertValuesToTooltips(values) {
|
||||||
|
let thresholds = this.panel.color.thresholds;
|
||||||
|
let tooltips = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < thresholds.length; i++) {
|
||||||
|
for (let j = 0; j < values.length; j++) {
|
||||||
|
if (values[j] === thresholds[i].value) {
|
||||||
|
tooltips.push(thresholds[i].tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tooltips;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getNotMatchedValues(values) {
|
||||||
|
let notMatched = [];
|
||||||
|
for (let j = 0; j < values.length; j++) {
|
||||||
|
if (!this.getMatchedThreshold(values[j])) {
|
||||||
|
notMatched.push(values[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notMatched;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiscreteColorScale() {
|
||||||
|
return (d) => {
|
||||||
|
let threshold = this.getMatchedThreshold(d);
|
||||||
|
if (!threshold) {
|
||||||
|
// Error if value not in thresholds
|
||||||
|
return 'rgba(0,0,0,1)';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return threshold.color;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCardsValuesHasColorInfo() {
|
||||||
|
let cards = this.panelCtrl.cardsData.cards;
|
||||||
|
for (let i=0; i<cards.length; i++) {
|
||||||
|
let values = cards[i].values;
|
||||||
|
for (let j=0; j<values.length; j++) {
|
||||||
|
if (!this.getMatchedThreshold(values[j])) {
|
||||||
|
cards[i].noColorDefined = true;
|
||||||
|
this.panelCtrl.cardsData.noColorDefined = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchedThreshold(value) {
|
||||||
|
let thresholds = this.panel.color.thresholds;
|
||||||
|
for (let k = 0; k < thresholds.length; k++) {
|
||||||
|
if (value === thresholds[k].value) {
|
||||||
|
return thresholds[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ $font-size-sm: 12px !default;
|
|||||||
.status-heatmap-canvas-wrapper {
|
.status-heatmap-canvas-wrapper {
|
||||||
// position: relative;
|
// position: relative;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
|
|
||||||
|
.datapoints-warning span {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-heatmap-panel {
|
.status-heatmap-panel {
|
||||||
|
|||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
<div class="status-heatmap-wrapper">
|
<div class="status-heatmap-wrapper">
|
||||||
<div class="status-heatmap-canvas-wrapper">
|
<div class="status-heatmap-canvas-wrapper">
|
||||||
|
|
||||||
<div class="datapoints-warning" ng-if="ctrl.dataWarning">
|
<div class="datapoints-warning">
|
||||||
<span class="small" bs-tooltip="ctrl.dataWarning.tip">{{ctrl.dataWarning.title}}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
|
<div class="status-heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<div ng-show="ctrl.multipleValues">
|
||||||
|
<strong>Error</strong>: data has multiple values for one target. Please change target or check "use max value".
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="ctrl.noColorDefined">
|
||||||
|
<strong>Error</strong>: data value with undefined color. Check metric values, color values or define new color.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="editor-row">
|
<div class="editor-row">
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Colors</h5>
|
<h5 class="section-heading">Colors</h5>
|
||||||
@@ -36,64 +44,104 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div ng-show="ctrl.panel.color.mode === 'discrete'">
|
||||||
|
<!-- <gf-form-switch class="gf-form" label-class="width-9"
|
||||||
|
label="Invert order"
|
||||||
|
checked="ctrl.panel.color.invertedThresholds" on-change="ctrl.render()">
|
||||||
|
</gf-form-switch> -->
|
||||||
|
<div class="gf-form">
|
||||||
|
<strong>Note:</strong> Bucket color determined by maximum for multiple values<br/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||||
|
<label class="gf-form-label width-9">If bucket value ==</label><!-- {{ $index + 1 }} -->
|
||||||
|
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
fill
|
||||||
|
</label>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
|
||||||
|
</label>
|
||||||
|
<label class="gf-form-label">set tooltip</label>
|
||||||
|
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
|
||||||
|
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||||
|
<i class="fa fa-trash"/>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||||
|
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
||||||
|
Add trafic lights
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
||||||
|
Add new status
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||||
<options-color-legend></options-color-legend>
|
<options-color-legend></options-color-legend>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||||
<h5 class="section-heading">Color scale</h5>
|
<h5 class="section-heading">Color scale</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-8">Min</label>
|
<label class="gf-form-label width-8">Min value</label>
|
||||||
<input type="number" ng-model="ctrl.panel.color.min" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" ng-model="ctrl.panel.color.min" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-8">Max</label>
|
<label class="gf-form-label width-8">Max value</label>
|
||||||
<input type="number" ng-model="ctrl.panel.color.max" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" ng-model="ctrl.panel.color.max" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Legend</h5>
|
<h5 class="section-heading">Display</h5>
|
||||||
<gf-form-switch class="gf-form" label-class="width-8"
|
<gf-form-switch class="gf-form" label-class="width-8"
|
||||||
label="Show legend"
|
label="Show legend"
|
||||||
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
|
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Buckets</h5>
|
<h5 class="section-heading">Graph</h5>
|
||||||
|
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-9"
|
||||||
|
label="Use max value for color"
|
||||||
|
checked="ctrl.panel.useMax" on-change="ctrl.render()">
|
||||||
|
</gf-form-switch>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-8">Space</label>
|
<label class="gf-form-label width-9">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">Buckets spacing</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>
|
<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>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-8">Round</label>
|
<label class="gf-form-label width-9">Bucket rounding</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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<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="Histogram"
|
|
||||||
checked="ctrl.panel.tooltip.showHistogram" on-change="ctrl.render()">
|
|
||||||
</gf-form-switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Null value</h5>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-7">Null value</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 ['null', 'null as zero']" ng-change="ctrl.render()"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,7 +21,7 @@ export function statusHeatmapOptionsEditor() {
|
|||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: true,
|
scope: true,
|
||||||
templateUrl: 'public/plugins/status-heatmap-panel/partials/options_editor.html',
|
templateUrl: 'public/plugins/status-heatmap-panel/options_editor.html',
|
||||||
controller: StatusHeatmapOptionsEditorCtrl,
|
controller: StatusHeatmapOptionsEditorCtrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-82
@@ -49,7 +49,6 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
|
|
||||||
ctrl.events.on('render', () => {
|
ctrl.events.on('render', () => {
|
||||||
render();
|
render();
|
||||||
// ctrl.renderingCompleted();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setElementHeight() {
|
function setElementHeight() {
|
||||||
@@ -124,28 +123,44 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
heatmap.select(".axis-x").select(".domain").remove();
|
heatmap.select(".axis-x").select(".domain").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addYAxis() {
|
// divide chart height by ticks for cards drawing
|
||||||
let ticks = _.map(data, d => d.target);
|
function getYScale(ticks) {
|
||||||
|
let range = [];
|
||||||
// Set default Y min and max if no data
|
let step = chartHeight / ticks.length;
|
||||||
if (_.isEmpty(data)) {
|
range.push(chartHeight);
|
||||||
ticks = [''];
|
for (let i = 1; i < ticks.length; i++) {
|
||||||
|
range.push(chartHeight - step * i);
|
||||||
}
|
}
|
||||||
|
return d3.scaleOrdinal()
|
||||||
|
.domain(ticks)
|
||||||
|
.range(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
// divide chart height by ticks with offset for ticks drawing
|
||||||
|
function getYAxisScale(ticks) {
|
||||||
let range = [];
|
let range = [];
|
||||||
let step = chartHeight / ticks.length;
|
let step = chartHeight / ticks.length;
|
||||||
range.push(chartHeight - yOffset);
|
range.push(chartHeight - yOffset);
|
||||||
for (let i = 1; i < ticks.length; i++) {
|
for (let i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i - yOffset);
|
range.push(chartHeight - step * i - yOffset);
|
||||||
}
|
}
|
||||||
|
return d3.scaleOrdinal()
|
||||||
console.log('yRange', range, yOffset);
|
|
||||||
|
|
||||||
scope.yScale = yScale = d3.scaleOrdinal()
|
|
||||||
.domain(ticks)
|
.domain(ticks)
|
||||||
.range(range);
|
.range(range);
|
||||||
|
}
|
||||||
|
|
||||||
let yAxis = d3.axisLeft(yScale)
|
function addYAxis() {
|
||||||
|
let ticks = _.uniq(_.map(data, d => d.target));
|
||||||
|
|
||||||
|
// Set default Y min and max if no data
|
||||||
|
if (_.isEmpty(data)) {
|
||||||
|
ticks = [''];
|
||||||
|
}
|
||||||
|
|
||||||
|
let yAxisScale = getYAxisScale(ticks);
|
||||||
|
scope.yScale = yScale = getYScale(ticks);
|
||||||
|
|
||||||
|
let yAxis = d3.axisLeft(yAxisScale)
|
||||||
.tickValues(ticks)
|
.tickValues(ticks)
|
||||||
.tickSizeInner(0 - width)
|
.tickSizeInner(0 - width)
|
||||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||||
@@ -164,7 +179,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
heatmap.select(".axis-y").selectAll(".tick line").remove();
|
heatmap.select(".axis-y").selectAll(".tick line").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wide Y values range and anjust to bucket size
|
// Wide Y values range and adjust to bucket size
|
||||||
function wideYAxisRange(min, max, tickInterval) {
|
function wideYAxisRange(min, max, tickInterval) {
|
||||||
let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
|
let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
|
||||||
let y_min, y_max;
|
let y_min, y_max;
|
||||||
@@ -193,19 +208,43 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixYAxisTickSize() {
|
// Create svg element, add axes and
|
||||||
heatmap.select(".axis-y")
|
// calculate sizes for cards drawing
|
||||||
.selectAll(".tick line")
|
function addHeatmapCanvas() {
|
||||||
.attr("x2", chartWidth);
|
let heatmap_elem = $heatmap[0];
|
||||||
}
|
|
||||||
|
width = Math.floor($heatmap.width()) - padding.right;
|
||||||
|
height = Math.floor($heatmap.height()) - padding.bottom;
|
||||||
|
|
||||||
|
if (heatmap) {
|
||||||
|
heatmap.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
heatmap = d3.select(heatmap_elem)
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height);
|
||||||
|
|
||||||
|
chartHeight = height - margin.top - margin.bottom;
|
||||||
|
chartTop = margin.top;
|
||||||
|
chartBottom = chartTop + chartHeight;
|
||||||
|
|
||||||
|
cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
|
||||||
|
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
||||||
|
|
||||||
|
// calculate yOffset for YAxis
|
||||||
|
let yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||||
|
cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
|
||||||
|
yOffset = cardHeight / 2;
|
||||||
|
|
||||||
function addAxes() {
|
|
||||||
addYAxis();
|
addYAxis();
|
||||||
|
|
||||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||||
chartWidth = width - yAxisWidth - margin.right;
|
chartWidth = width - yAxisWidth - margin.right;
|
||||||
// fixYAxisTickSize();
|
|
||||||
//
|
let xGridSize = Math.floor(chartWidth / cardsData.xBucketSize);
|
||||||
|
cardWidth = xGridSize - cardPadding * 2;
|
||||||
|
|
||||||
addXAxis();
|
addXAxis();
|
||||||
xAxisHeight = getXAxisHeight(heatmap);
|
xAxisHeight = getXAxisHeight(heatmap);
|
||||||
|
|
||||||
@@ -218,49 +257,25 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHeatmapCanvas() {
|
|
||||||
let heatmap_elem = $heatmap[0];
|
|
||||||
|
|
||||||
width = Math.floor($heatmap.width()) - padding.right;
|
|
||||||
height = Math.floor($heatmap.height()) - padding.bottom;
|
|
||||||
|
|
||||||
chartHeight = height - margin.top - margin.bottom - yOffset;
|
|
||||||
chartTop = margin.top;
|
|
||||||
chartBottom = chartTop + chartHeight;
|
|
||||||
|
|
||||||
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() {
|
function addHeatmap() {
|
||||||
addHeatmapCanvas();
|
addHeatmapCanvas();
|
||||||
|
|
||||||
let maxValue = panel.color.max || cardsData.maxValue;
|
let maxValue = panel.color.max || cardsData.maxValue;
|
||||||
let minValue = panel.color.min || cardsData.minValue;
|
let minValue = panel.color.min || cardsData.minValue;
|
||||||
|
|
||||||
colorScale = getColorScale(maxValue, minValue);
|
if (panel.color.mode === 'discrete') {
|
||||||
|
colorScale = ctrl.discreteHelper.getDiscreteColorScale();
|
||||||
|
} else {
|
||||||
|
colorScale = getColorScale(maxValue, minValue);
|
||||||
|
}
|
||||||
setOpacityScale(maxValue);
|
setOpacityScale(maxValue);
|
||||||
setCardSize();
|
|
||||||
|
|
||||||
addAxes();
|
|
||||||
|
|
||||||
let cards = heatmap.selectAll(".status-heatmap-card").data(cardsData.cards);
|
let cards = heatmap.selectAll(".status-heatmap-card").data(cardsData.cards);
|
||||||
cards.append("title");
|
cards.append("title");
|
||||||
cards = cards.enter().append("rect")
|
cards = cards.enter().append("rect")
|
||||||
.attr("value", c => c.value)
|
.attr("cardId", c => c.id)
|
||||||
.attr("xVal", c => c.x)
|
|
||||||
.attr("x", getCardX)
|
.attr("x", getCardX)
|
||||||
.attr("width", getCardWidth)
|
.attr("width", getCardWidth)
|
||||||
.attr("yVal", c => c.y)
|
|
||||||
.attr("y", getCardY)
|
.attr("y", getCardY)
|
||||||
.attr("height", getCardHeight)
|
.attr("height", getCardHeight)
|
||||||
.attr("rx", cardRound)
|
.attr("rx", cardRound)
|
||||||
@@ -269,14 +284,14 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
.style("fill", getCardColor)
|
.style("fill", getCardColor)
|
||||||
.style("stroke", getCardColor)
|
.style("stroke", getCardColor)
|
||||||
.style("stroke-width", 0)
|
.style("stroke-width", 0)
|
||||||
|
//.style("stroke-width", getCardStrokeWidth)
|
||||||
|
//.style("stroke-dasharray", "3,3")
|
||||||
.style("opacity", getCardOpacity);
|
.style("opacity", getCardOpacity);
|
||||||
|
|
||||||
let $cards = $heatmap.find(".status-heatmap-card");
|
let $cards = $heatmap.find(".status-heatmap-card");
|
||||||
$cards.on("mouseenter", (event) => {
|
$cards.on("mouseenter", (event) => {
|
||||||
tooltip.mouseOverBucket = true;
|
tooltip.mouseOverBucket = true;
|
||||||
highlightCard(event);
|
highlightCard(event);
|
||||||
let current_card = d3.select(event.target);
|
|
||||||
tooltip.show(event, current_card.attr('xVal'), current_card.attr('yVal'), current_card.attr('value'));
|
|
||||||
})
|
})
|
||||||
.on("mouseleave", (event) => {
|
.on("mouseleave", (event) => {
|
||||||
tooltip.mouseOverBucket = false;
|
tooltip.mouseOverBucket = false;
|
||||||
@@ -296,9 +311,11 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetCardHighLight(event) {
|
function resetCardHighLight(event) {
|
||||||
d3.select(event.target).style("fill", tooltip.originalFillColor)
|
d3
|
||||||
.style("stroke", tooltip.originalFillColor)
|
.select(event.target)
|
||||||
.style("stroke-width", 0);
|
.style("fill", tooltip.originalFillColor)
|
||||||
|
.style("stroke", tooltip.originalFillColor)
|
||||||
|
.style("stroke-width", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColorScale(maxValue, minValue = 0) {
|
function getColorScale(maxValue, minValue = 0) {
|
||||||
@@ -316,6 +333,36 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scale input range to discrete colors to draw a legend
|
||||||
|
function getDiscreteColorScale() {
|
||||||
|
let thresholds = panel.color.thresholds;
|
||||||
|
|
||||||
|
let thresholdValues = [];
|
||||||
|
let thresholdColors = [];
|
||||||
|
for (let i = 0; i < thresholds.length; i++) {
|
||||||
|
thresholdColors.push(thresholds[i].color);
|
||||||
|
thresholdValues.push(thresholds[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO sort colors by value and index?
|
||||||
|
|
||||||
|
let thresholdScaler = (d) => {
|
||||||
|
for (let i = 0; i < thresholdValues.length; i++ ) {
|
||||||
|
if (d == thresholdValues[i]) {
|
||||||
|
return thresholdColors[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Error if value not in thresholds
|
||||||
|
return 'rgba(0,0,0,1)';
|
||||||
|
};
|
||||||
|
|
||||||
|
// scale min-max to 0 - max-thrs-value
|
||||||
|
return function(d) {
|
||||||
|
return thresholdScaler(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function setOpacityScale(maxValue) {
|
function setOpacityScale(maxValue) {
|
||||||
if (panel.color.colorScale === 'linear') {
|
if (panel.color.colorScale === 'linear') {
|
||||||
opacityScale = d3.scaleLinear()
|
opacityScale = d3.scaleLinear()
|
||||||
@@ -328,16 +375,6 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCardSize() {
|
|
||||||
let xGridSize = Math.floor(chartWidth / cardsData.xBucketSize);
|
|
||||||
let yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
|
||||||
|
|
||||||
cardWidth = xGridSize - cardPadding * 2;
|
|
||||||
cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
|
|
||||||
|
|
||||||
yOffset = cardHeight / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCardX(d) {
|
function getCardX(d) {
|
||||||
let x;
|
let x;
|
||||||
if (xScale(d.x) < 0) {
|
if (xScale(d.x) < 0) {
|
||||||
@@ -369,25 +406,22 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCardY(d) {
|
function getCardY(d) {
|
||||||
let y = yScale(d.y);
|
return yScale(d.y) + chartTop - cardHeight - cardPadding;
|
||||||
|
|
||||||
y = y + chartTop - cardHeight - cardPadding + yOffset;
|
|
||||||
|
|
||||||
return y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardHeight(d) {
|
function getCardHeight(d) {
|
||||||
let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
|
let ys = yScale(d.y);
|
||||||
|
let y = ys + chartTop - cardHeight - cardPadding;
|
||||||
let h = cardHeight;
|
let h = cardHeight;
|
||||||
|
|
||||||
// Cut card height to prevent overlay
|
// Cut card height to prevent overlay
|
||||||
// if (y < chartTop) {
|
if (y < chartTop) {
|
||||||
// h = yScale(d.y) - cardPadding;
|
h = ys - cardPadding;
|
||||||
// } else if (yScale(d.y) > chartBottom) {
|
} else if (ys > chartBottom) {
|
||||||
// h = chartBottom - y;
|
h = chartBottom - y;
|
||||||
// } else if (y + cardHeight > chartBottom) {
|
} else if (y + cardHeight > chartBottom) {
|
||||||
// h = chartBottom - y;
|
h = chartBottom - y;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Height can't be more than chart height
|
// Height can't be more than chart height
|
||||||
h = Math.min(h, chartHeight);
|
h = Math.min(h, chartHeight);
|
||||||
@@ -400,13 +434,15 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function getCardColor(d) {
|
function getCardColor(d) {
|
||||||
if (panel.color.mode === 'opacity') {
|
if (panel.color.mode === 'opacity') {
|
||||||
return panel.color.cardColor;
|
return panel.color.cardColor;
|
||||||
} else {
|
} else if (panel.color.mode === 'spectrum') {
|
||||||
|
return colorScale(d.value);
|
||||||
|
} else if (panel.color.mode === 'discrete') {
|
||||||
return colorScale(d.value);
|
return colorScale(d.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardOpacity(d) {
|
function getCardOpacity(d) {
|
||||||
if (panel.nullPointMode === 'null' && d.value == null ) {
|
if (panel.nullPointMode === 'as empty' && d.value == null ) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (panel.color.mode === 'opacity') {
|
if (panel.color.mode === 'opacity') {
|
||||||
@@ -416,6 +452,13 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCardStrokeWidth(d) {
|
||||||
|
if (panel.color.mode === 'discrete') {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Selection and crosshair //
|
// Selection and crosshair //
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
@@ -477,6 +520,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
} else {
|
} else {
|
||||||
emitGraphHoverEvet(event);
|
emitGraphHoverEvet(event);
|
||||||
drawCrosshair(event.offsetX);
|
drawCrosshair(event.offsetX);
|
||||||
|
tooltip.show(event); //, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,7 +620,6 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// Draw default axes and return if no data
|
// Draw default axes and return if no data
|
||||||
if (_.isEmpty(cardsData.cards)) {
|
if (_.isEmpty(cardsData.cards)) {
|
||||||
addHeatmapCanvas();
|
addHeatmapCanvas();
|
||||||
addAxes();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+121
-31
@@ -7,8 +7,8 @@ import rendering from './rendering';
|
|||||||
// import aggregates, { aggregatesMap } from './aggregates';
|
// import aggregates, { aggregatesMap } from './aggregates';
|
||||||
// import fragments, { fragmentsMap } from './fragments';
|
// import fragments, { fragmentsMap } from './fragments';
|
||||||
// import { labelFormats } from './xAxisLabelFormats';
|
// import { labelFormats } from './xAxisLabelFormats';
|
||||||
// import canvasRendering from './canvas/rendering';
|
|
||||||
import {statusHeatmapOptionsEditor} from './options_editor';
|
import {statusHeatmapOptionsEditor} from './options_editor';
|
||||||
|
import {ColorModeDiscrete} from "./color_mode_discrete";
|
||||||
import './css/status-heatmap.css!';
|
import './css/status-heatmap.css!';
|
||||||
|
|
||||||
const CANVAS = 'CANVAS';
|
const CANVAS = 'CANVAS';
|
||||||
@@ -24,7 +24,10 @@ const panelDefaults = {
|
|||||||
cardColor: '#b4ff00',
|
cardColor: '#b4ff00',
|
||||||
colorScale: 'sqrt',
|
colorScale: 'sqrt',
|
||||||
exponent: 0.5,
|
exponent: 0.5,
|
||||||
colorScheme: 'interpolateGnYlRd'
|
colorScheme: 'interpolateGnYlRd',
|
||||||
|
// discrete mode settings
|
||||||
|
defaultColor: '#757575',
|
||||||
|
thresholds: [] // manual colors
|
||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
cardPadding: null,
|
cardPadding: null,
|
||||||
@@ -52,8 +55,9 @@ const panelDefaults = {
|
|||||||
decimals: null
|
decimals: null
|
||||||
},
|
},
|
||||||
// how null points should be handled
|
// how null points should be handled
|
||||||
nullPointMode: 'null',
|
nullPointMode: 'as empty',
|
||||||
highlightCards: true
|
highlightCards: true,
|
||||||
|
useMax: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = CANVAS;
|
const renderer = CANVAS;
|
||||||
@@ -87,7 +91,7 @@ const colorSchemes = [
|
|||||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }
|
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }
|
||||||
];
|
];
|
||||||
|
|
||||||
let colorModes = ['opacity', 'spectrum'];
|
let colorModes = ['opacity', 'spectrum', 'discrete'];
|
||||||
let opacityScales = ['linear', 'sqrt'];
|
let opacityScales = ['linear', 'sqrt'];
|
||||||
|
|
||||||
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||||
@@ -101,16 +105,34 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.colorModes = colorModes;
|
this.colorModes = colorModes;
|
||||||
this.colorSchemes = colorSchemes;
|
this.colorSchemes = colorSchemes;
|
||||||
|
|
||||||
|
this.multipleValues = false;
|
||||||
|
this.noColorDefined = false;
|
||||||
|
|
||||||
|
this.discreteHelper = new ColorModeDiscrete($scope);
|
||||||
|
|
||||||
|
this.dataWarnings = {
|
||||||
|
"noColorDefined": {
|
||||||
|
title: 'Data has value with undefined color',
|
||||||
|
tip: 'Check metric values, color values or define a new color',
|
||||||
|
},
|
||||||
|
"multipleValues": {
|
||||||
|
title: 'Data has multiple values for one target',
|
||||||
|
tip: 'Change targets definitions or set "use max value"',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.events.on('data-received', this.onDataReceived);
|
this.events.on('data-received', this.onDataReceived);
|
||||||
this.events.on('data-snapshot-load', this.onDataReceived);
|
this.events.on('data-snapshot-load', this.onDataReceived);
|
||||||
this.events.on('data-error', this.onDataError);
|
this.events.on('data-error', this.onDataError);
|
||||||
this.events.on('init-edit-mode', this.onInitEditMode);
|
this.events.on('init-edit-mode', this.onInitEditMode);
|
||||||
this.events.on('render', this.onRender);
|
this.events.on('render', this.onRender);
|
||||||
|
this.events.on('refresh', this.postRefresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDataReceived = (dataList) => {
|
onDataReceived = (dataList) => {
|
||||||
this.data = dataList;
|
this.data = dataList;
|
||||||
this.cardsData = this.convertToCards(this.data);
|
this.cardsData = this.convertToCards(this.data);
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,6 +143,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
onRender = () => {
|
onRender = () => {
|
||||||
if (!this.data) { return; }
|
if (!this.data) { return; }
|
||||||
|
|
||||||
|
this.multipleValues = false;
|
||||||
|
if (!this.panel.useMax) {
|
||||||
|
if (!_.isEmpty(this.cardsData)) {
|
||||||
|
this.multipleValues = this.cardsData.multipleValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.noColorDefined = false;
|
||||||
|
if (this.panel.color.mode === 'discrete') {
|
||||||
|
this.discreteHelper.updateCardsValuesHasColorInfo();
|
||||||
|
this.noColorDefined = this.cardsData.noColorDefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCardColorChange = (newColor) => {
|
onCardColorChange = (newColor) => {
|
||||||
@@ -133,45 +168,99 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
link = (scope, elem, attrs, ctrl) => {
|
postRefresh = () => {
|
||||||
console.log('LINK');
|
this.noColorDefined = false;
|
||||||
rendering(scope, elem, attrs, ctrl);
|
|
||||||
// switch (renderer) {
|
|
||||||
// case CANVAS: {
|
|
||||||
// canvasRendering(scope, elem, attrs, ctrl);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case SVG: {
|
|
||||||
// svgRendering(scope, elem, attrs, ctrl);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onEditorAddThreshold = () => {
|
||||||
|
this.panel.color.thresholds.push({ color: this.panel.defaultColor });
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditorRemoveThreshold = (index) => {
|
||||||
|
this.panel.color.thresholds.splice(index, 1);
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditorAddThreeLights = () => {
|
||||||
|
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
|
||||||
|
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
|
||||||
|
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
link = (scope, elem, attrs, ctrl) => {
|
||||||
|
rendering(scope, elem, attrs, ctrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
// group values into buckets by target
|
||||||
convertToCards = (data) => {
|
convertToCards = (data) => {
|
||||||
let cardsData = { cards: [], xBucketSize: 0, yBucketSize: 0, maxValue: 0, minValue: 0 };
|
let cardsData = {
|
||||||
|
cards: [],
|
||||||
|
xBucketSize: 0,
|
||||||
|
yBucketSize: 0,
|
||||||
|
maxValue: 0,
|
||||||
|
minValue: 0,
|
||||||
|
multipleValues: false,
|
||||||
|
noColorDefined: false,
|
||||||
|
};
|
||||||
|
|
||||||
if (!data || data.length == 0) { return cardsData;}
|
if (!data || data.length == 0) { return cardsData;}
|
||||||
|
|
||||||
cardsData.yBucketSize = data.length;
|
// collect uniq targets and their indexes in data array
|
||||||
|
cardsData.targetIndex = {};
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let ts = data[i];
|
||||||
|
let target = ts.target;
|
||||||
|
if (cardsData.targetIndex[target] == undefined) {
|
||||||
|
cardsData.targetIndex[target] = []
|
||||||
|
}
|
||||||
|
cardsData.targetIndex[target].push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add some logic for targets heirarchy
|
||||||
|
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||||
|
cardsData.targets.sort();
|
||||||
|
cardsData.yBucketSize = cardsData.targets.length;
|
||||||
cardsData.xBucketSize = _.min(_.map(data, d => d.datapoints.length));
|
cardsData.xBucketSize = _.min(_.map(data, d => d.datapoints.length));
|
||||||
|
|
||||||
for(let i = 0; i < cardsData.yBucketSize; i++) {
|
// Collect all values for each bucket from datapoints with similar target.
|
||||||
let s = data[i];
|
for(let i = 0; i < cardsData.targets.length; i++) {
|
||||||
|
let target = cardsData.targets[i];
|
||||||
|
|
||||||
for (let j = 0; j < cardsData.xBucketSize; j++) {
|
for (let j = 0; j < cardsData.xBucketSize; j++) {
|
||||||
let card = {};
|
let card = {
|
||||||
let v = s.datapoints[j];
|
id: i*cardsData.xBucketSize + j,
|
||||||
|
values: [],
|
||||||
|
multipleValues: false,
|
||||||
|
noColorDefined: false,
|
||||||
|
};
|
||||||
|
|
||||||
card.x = v[TIME_INDEX];
|
// collect values from all timeseries with target
|
||||||
card.y = s.target;
|
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||||
card.value = v[VALUE_INDEX];
|
let s = data[cardsData.targetIndex[target][si]];
|
||||||
|
let datapoint = s.datapoints[j];
|
||||||
|
if (card.values.length === 0) {
|
||||||
|
card.x = datapoint[TIME_INDEX];
|
||||||
|
card.y = s.target;
|
||||||
|
}
|
||||||
|
card.values.push(datapoint[VALUE_INDEX]);
|
||||||
|
}
|
||||||
|
card.minValue = _.min(card.values);
|
||||||
|
card.maxValue = _.max(card.values);
|
||||||
|
if (card.values.length > 1) {
|
||||||
|
cardsData.multipleValues = true;
|
||||||
|
card.multipleValues = true;
|
||||||
|
card.value = card.maxValue; // max value by default
|
||||||
|
} else {
|
||||||
|
card.value = card.maxValue; // max value by default
|
||||||
|
}
|
||||||
|
|
||||||
if (cardsData.maxValue < card.value)
|
if (cardsData.maxValue < card.maxValue)
|
||||||
cardsData.maxValue = card.value;
|
cardsData.maxValue = card.maxValue;
|
||||||
|
|
||||||
if (cardsData.minValue > card.value)
|
if (cardsData.minValue > card.minValue)
|
||||||
cardsData.minValue = card.value;
|
cardsData.minValue = card.minValue;
|
||||||
|
|
||||||
cardsData.cards.push(card);
|
cardsData.cards.push(card);
|
||||||
}
|
}
|
||||||
@@ -179,4 +268,5 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
return cardsData;
|
return cardsData;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-8
@@ -5,8 +5,6 @@ import kbn from 'app/core/utils/kbn';
|
|||||||
|
|
||||||
let TOOLTIP_PADDING_X = 30;
|
let TOOLTIP_PADDING_X = 30;
|
||||||
let TOOLTIP_PADDING_Y = 5;
|
let TOOLTIP_PADDING_Y = 5;
|
||||||
let HISTOGRAM_WIDTH = 160;
|
|
||||||
let HISTOGRAM_HEIGHT = 40;
|
|
||||||
|
|
||||||
export class StatusHeatmapTooltip {
|
export class StatusHeatmapTooltip {
|
||||||
constructor(elem, scope) {
|
constructor(elem, scope) {
|
||||||
@@ -55,28 +53,83 @@ export class StatusHeatmapTooltip {
|
|||||||
this.tooltip = null;
|
this.tooltip = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(pos, x, y, value) {
|
show(pos) {
|
||||||
if (!this.panel.tooltip.show) { return; }
|
if (!this.panel.tooltip.show || !this.tooltip) { return; }
|
||||||
// shared tooltip mode
|
// shared tooltip mode
|
||||||
if (pos.panelRelY) {
|
if (pos.panelRelY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let cardId = d3.select(pos.target).attr('cardId');
|
||||||
if (!x || !y || !this.tooltip) {
|
if (!cardId) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
return;
|
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 tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
||||||
|
|
||||||
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
|
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
|
||||||
<div class="status-heatmap-histogram"></div>`;
|
<div class="status-heatmap-histogram"></div>`;
|
||||||
|
|
||||||
tooltipHtml += `<div>
|
if (this.panel.color.mode === 'discrete') {
|
||||||
|
let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
||||||
|
let statusesHtml = '';
|
||||||
|
if (statuses.length > 0) {
|
||||||
|
statusesHtml = `
|
||||||
|
statuses:
|
||||||
|
<ul>
|
||||||
|
${_.join(_.map(statuses, v => `<li>${v}</li>`), "")}
|
||||||
|
</ul>`;
|
||||||
|
}
|
||||||
|
tooltipHtml += `<div>
|
||||||
|
name: <b>${y}</b> <br>
|
||||||
|
${statusesHtml}
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
if (values.length === 1) {
|
||||||
|
tooltipHtml += `<div>
|
||||||
name: <b>${y}</b> <br>
|
name: <b>${y}</b> <br>
|
||||||
value: <b>${value}</b> <br>
|
value: <b>${value}</b> <br>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
} else {
|
||||||
|
tooltipHtml += `<div>
|
||||||
|
name: <b>${y}</b> <br>
|
||||||
|
values:
|
||||||
|
<ul>
|
||||||
|
${_.join(_.map(values, v => `<li>${v}</li>`), "")}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Ambiguous bucket state: Multiple values!";
|
||||||
|
if (!this.panel.useMax && card.multipleValues) {
|
||||||
|
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.multipleValues.title}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discrete mode errors
|
||||||
|
if (this.panel.color.mode === 'discrete') {
|
||||||
|
if (card.noColorDefined) {
|
||||||
|
let badValues = this.panelCtrl.discreteHelper.getNotMatchedValues(values);
|
||||||
|
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title}
|
||||||
|
<br>bad values:
|
||||||
|
<ul>
|
||||||
|
${_.join(_.map(badValues, v => `<li>${v}</li>`), "")}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.tooltip.html(tooltipHtml);
|
this.tooltip.html(tooltipHtml);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user