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:
Ivan Mikheykin
2018-08-29 17:26:24 +03:00
parent 06afdc1ee8
commit 0d7289d3d7
9 changed files with 620 additions and 150 deletions
+157
View File
@@ -38,6 +38,10 @@ mod.directive('optionsColorLegend', function() {
} else if (panel.color.mode === 'opacity') {
let colorOptions = panel.color;
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') {
let colorOptions = panel.color;
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);
}
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) {
let legendElem = $(elem).find('svg');
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();
}
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) {
let legendElem = $(elem).find('svg');
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) {
let legendElem = $(elem).find('svg');
legendElem.empty();
@@ -252,6 +373,42 @@ function getColorScale(colorScheme, maxValue, minValue = 0) {
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) {
let legendOpacityScale;
if (options.colorScale === 'linear') {
+73
View File
@@ -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;
}
}
+5
View File
@@ -4,6 +4,11 @@ $font-size-sm: 12px !default;
.status-heatmap-canvas-wrapper {
// position: relative;
cursor: crosshair;
.datapoints-warning span {
background-color: white;
padding: 1px;
}
}
.status-heatmap-panel {
+3 -2
View File
@@ -1,8 +1,9 @@
<div class="status-heatmap-wrapper">
<div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.dataWarning">
<span class="small" bs-tooltip="ctrl.dataWarning.tip">{{ctrl.dataWarning.title}}</span>
<div class="datapoints-warning">
<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 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="section gf-form-group">
<h5 class="section-heading">Colors</h5>
@@ -36,64 +44,104 @@
</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>
</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>
<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>
</div>
<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>
</div>
</div>
<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"
label="Show legend"
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</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 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">
<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>
</div>
<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>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Tooltip</h5>
<gf-form-switch class="gf-form" label-class="width-8"
label="Show tooltip"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
<div ng-if="ctrl.panel.tooltip.show">
<gf-form-switch class="gf-form" label-class="width-8"
label="Histogram"
checked="ctrl.panel.tooltip.showHistogram" on-change="ctrl.render()">
</gf-form-switch>
</div>
</div>
<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>
+1 -1
View File
@@ -21,7 +21,7 @@ export function statusHeatmapOptionsEditor() {
return {
restrict: 'E',
scope: true,
templateUrl: 'public/plugins/status-heatmap-panel/partials/options_editor.html',
templateUrl: 'public/plugins/status-heatmap-panel/options_editor.html',
controller: StatusHeatmapOptionsEditorCtrl,
};
}
+125 -82
View File
@@ -49,7 +49,6 @@ export default function link(scope, elem, attrs, ctrl) {
ctrl.events.on('render', () => {
render();
// ctrl.renderingCompleted();
});
function setElementHeight() {
@@ -124,28 +123,44 @@ export default function link(scope, elem, attrs, ctrl) {
heatmap.select(".axis-x").select(".domain").remove();
}
function addYAxis() {
let ticks = _.map(data, d => d.target);
// Set default Y min and max if no data
if (_.isEmpty(data)) {
ticks = [''];
// divide chart height by ticks for cards drawing
function getYScale(ticks) {
let range = [];
let step = chartHeight / ticks.length;
range.push(chartHeight);
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 step = chartHeight / ticks.length;
range.push(chartHeight - yOffset);
for (let i = 1; i < ticks.length; i++) {
range.push(chartHeight - step * i - yOffset);
}
console.log('yRange', range, yOffset);
scope.yScale = yScale = d3.scaleOrdinal()
return d3.scaleOrdinal()
.domain(ticks)
.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)
.tickSizeInner(0 - width)
.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();
}
// Wide Y values range and anjust to bucket size
// Wide Y values range and adjust to bucket size
function wideYAxisRange(min, max, tickInterval) {
let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
let y_min, y_max;
@@ -193,19 +208,43 @@ export default function link(scope, elem, attrs, ctrl) {
};
}
function fixYAxisTickSize() {
heatmap.select(".axis-y")
.selectAll(".tick line")
.attr("x2", chartWidth);
}
// Create svg element, add axes and
// calculate sizes for cards drawing
function addHeatmapCanvas() {
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();
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
chartWidth = width - yAxisWidth - margin.right;
// fixYAxisTickSize();
//
let xGridSize = Math.floor(chartWidth / cardsData.xBucketSize);
cardWidth = xGridSize - cardPadding * 2;
addXAxis();
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() {
addHeatmapCanvas();
let maxValue = panel.color.max || cardsData.maxValue;
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);
setCardSize();
addAxes();
let cards = heatmap.selectAll(".status-heatmap-card").data(cardsData.cards);
cards.append("title");
cards = cards.enter().append("rect")
.attr("value", c => c.value)
.attr("xVal", c => c.x)
.attr("cardId", c => c.id)
.attr("x", getCardX)
.attr("width", getCardWidth)
.attr("yVal", c => c.y)
.attr("y", getCardY)
.attr("height", getCardHeight)
.attr("rx", cardRound)
@@ -269,14 +284,14 @@ export default function link(scope, elem, attrs, ctrl) {
.style("fill", getCardColor)
.style("stroke", getCardColor)
.style("stroke-width", 0)
//.style("stroke-width", getCardStrokeWidth)
//.style("stroke-dasharray", "3,3")
.style("opacity", getCardOpacity);
let $cards = $heatmap.find(".status-heatmap-card");
$cards.on("mouseenter", (event) => {
tooltip.mouseOverBucket = true;
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) => {
tooltip.mouseOverBucket = false;
@@ -296,9 +311,11 @@ export default function link(scope, elem, attrs, ctrl) {
}
function resetCardHighLight(event) {
d3.select(event.target).style("fill", tooltip.originalFillColor)
.style("stroke", tooltip.originalFillColor)
.style("stroke-width", 0);
d3
.select(event.target)
.style("fill", tooltip.originalFillColor)
.style("stroke", tooltip.originalFillColor)
.style("stroke-width", 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]);
}
// 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) {
if (panel.color.colorScale === 'linear') {
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) {
let x;
if (xScale(d.x) < 0) {
@@ -369,25 +406,22 @@ export default function link(scope, elem, attrs, ctrl) {
}
function getCardY(d) {
let y = yScale(d.y);
y = y + chartTop - cardHeight - cardPadding + yOffset;
return y;
return yScale(d.y) + chartTop - cardHeight - cardPadding;
}
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;
// Cut card height to prevent overlay
// if (y < chartTop) {
// h = yScale(d.y) - cardPadding;
// } else if (yScale(d.y) > chartBottom) {
// h = chartBottom - y;
// } else if (y + cardHeight > chartBottom) {
// h = chartBottom - y;
// }
if (y < chartTop) {
h = ys - cardPadding;
} else if (ys > chartBottom) {
h = chartBottom - y;
} else if (y + cardHeight > chartBottom) {
h = chartBottom - y;
}
// Height can't be more than chart height
h = Math.min(h, chartHeight);
@@ -400,13 +434,15 @@ export default function link(scope, elem, attrs, ctrl) {
function getCardColor(d) {
if (panel.color.mode === 'opacity') {
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);
}
}
function getCardOpacity(d) {
if (panel.nullPointMode === 'null' && d.value == null ) {
if (panel.nullPointMode === 'as empty' && d.value == null ) {
return 0;
}
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 //
/////////////////////////////
@@ -477,6 +520,7 @@ export default function link(scope, elem, attrs, ctrl) {
} else {
emitGraphHoverEvet(event);
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
if (_.isEmpty(cardsData.cards)) {
addHeatmapCanvas();
addAxes();
return;
}
+121 -31
View File
@@ -7,8 +7,8 @@ import rendering from './rendering';
// import aggregates, { aggregatesMap } from './aggregates';
// import fragments, { fragmentsMap } from './fragments';
// import { labelFormats } from './xAxisLabelFormats';
// import canvasRendering from './canvas/rendering';
import {statusHeatmapOptionsEditor} from './options_editor';
import {ColorModeDiscrete} from "./color_mode_discrete";
import './css/status-heatmap.css!';
const CANVAS = 'CANVAS';
@@ -24,7 +24,10 @@ const panelDefaults = {
cardColor: '#b4ff00',
colorScale: 'sqrt',
exponent: 0.5,
colorScheme: 'interpolateGnYlRd'
colorScheme: 'interpolateGnYlRd',
// discrete mode settings
defaultColor: '#757575',
thresholds: [] // manual colors
},
cards: {
cardPadding: null,
@@ -52,8 +55,9 @@ const panelDefaults = {
decimals: null
},
// how null points should be handled
nullPointMode: 'null',
highlightCards: true
nullPointMode: 'as empty',
highlightCards: true,
useMax: true
};
const renderer = CANVAS;
@@ -87,7 +91,7 @@ const colorSchemes = [
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }
];
let colorModes = ['opacity', 'spectrum'];
let colorModes = ['opacity', 'spectrum', 'discrete'];
let opacityScales = ['linear', 'sqrt'];
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
@@ -101,16 +105,34 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.colorModes = colorModes;
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-snapshot-load', this.onDataReceived);
this.events.on('data-error', this.onDataError);
this.events.on('init-edit-mode', this.onInitEditMode);
this.events.on('render', this.onRender);
this.events.on('refresh', this.postRefresh);
}
onDataReceived = (dataList) => {
this.data = dataList;
this.cardsData = this.convertToCards(this.data);
this.render();
};
@@ -121,6 +143,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
onRender = () => {
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) => {
@@ -133,45 +168,99 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.render();
};
link = (scope, elem, attrs, ctrl) => {
console.log('LINK');
rendering(scope, elem, attrs, ctrl);
// switch (renderer) {
// case CANVAS: {
// canvasRendering(scope, elem, attrs, ctrl);
// break;
// }
// case SVG: {
// svgRendering(scope, elem, attrs, ctrl);
// break;
// }
// }
postRefresh = () => {
this.noColorDefined = false;
};
onEditorAddThreshold = () => {
this.panel.color.thresholds.push({ color: this.panel.defaultColor });
this.render();
};
onEditorRemoveThreshold = (index) => {
this.panel.color.thresholds.splice(index, 1);
this.render();
};
onEditorAddThreeLights = () => {
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
this.render();
};
link = (scope, elem, attrs, ctrl) => {
rendering(scope, elem, attrs, ctrl);
};
// group values into buckets by target
convertToCards = (data) => {
let cardsData = { cards: [], xBucketSize: 0, yBucketSize: 0, maxValue: 0, minValue: 0 };
let cardsData = {
cards: [],
xBucketSize: 0,
yBucketSize: 0,
maxValue: 0,
minValue: 0,
multipleValues: false,
noColorDefined: false,
};
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));
for(let i = 0; i < cardsData.yBucketSize; i++) {
let s = data[i];
// Collect all values for each bucket from datapoints with similar target.
for(let i = 0; i < cardsData.targets.length; i++) {
let target = cardsData.targets[i];
for (let j = 0; j < cardsData.xBucketSize; j++) {
let card = {};
let v = s.datapoints[j];
let card = {
id: i*cardsData.xBucketSize + j,
values: [],
multipleValues: false,
noColorDefined: false,
};
card.x = v[TIME_INDEX];
card.y = s.target;
card.value = v[VALUE_INDEX];
// collect values from all timeseries with target
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
let s = data[cardsData.targetIndex[target][si]];
let datapoint = s.datapoints[j];
if (card.values.length === 0) {
card.x = datapoint[TIME_INDEX];
card.y = s.target;
}
card.values.push(datapoint[VALUE_INDEX]);
}
card.minValue = _.min(card.values);
card.maxValue = _.max(card.values);
if (card.values.length > 1) {
cardsData.multipleValues = true;
card.multipleValues = true;
card.value = card.maxValue; // max value by default
} else {
card.value = card.maxValue; // max value by default
}
if (cardsData.maxValue < card.value)
cardsData.maxValue = card.value;
if (cardsData.maxValue < card.maxValue)
cardsData.maxValue = card.maxValue;
if (cardsData.minValue > card.value)
cardsData.minValue = card.value;
if (cardsData.minValue > card.minValue)
cardsData.minValue = card.minValue;
cardsData.cards.push(card);
}
@@ -179,4 +268,5 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
return cardsData;
};
}
+61 -8
View File
@@ -5,8 +5,6 @@ import kbn from 'app/core/utils/kbn';
let TOOLTIP_PADDING_X = 30;
let TOOLTIP_PADDING_Y = 5;
let HISTOGRAM_WIDTH = 160;
let HISTOGRAM_HEIGHT = 40;
export class StatusHeatmapTooltip {
constructor(elem, scope) {
@@ -55,28 +53,83 @@ export class StatusHeatmapTooltip {
this.tooltip = null;
}
show(pos, x, y, value) {
if (!this.panel.tooltip.show) { return; }
show(pos) {
if (!this.panel.tooltip.show || !this.tooltip) { return; }
// shared tooltip mode
if (pos.panelRelY) {
return;
}
if (!x || !y || !this.tooltip) {
let cardId = d3.select(pos.target).attr('cardId');
if (!cardId) {
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 tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
let tooltipHtml = `<div class="graph-tooltip-time">${time}</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>
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);