diff --git a/src/color_legend.js b/src/color_legend.js index ed77896..49d3296 100644 --- a/src/color_legend.js +++ b/src/color_legend.js @@ -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') { diff --git a/src/color_mode_discrete.js b/src/color_mode_discrete.js new file mode 100644 index 0000000..6e7aa4e --- /dev/null +++ b/src/color_mode_discrete.js @@ -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
-
- {{ctrl.dataWarning.title}} +
+ {{ctrl.dataWarnings.multipleValues.title}} + {{ctrl.dataWarnings.noColorDefined.title}}
diff --git a/src/partials/options_editor.html b/src/options_editor.html similarity index 54% rename from src/partials/options_editor.html rename to src/options_editor.html index b9ea9e3..20bff1a 100644 --- a/src/partials/options_editor.html +++ b/src/options_editor.html @@ -1,3 +1,11 @@ +
+ Error: data has multiple values for one target. Please change target or check "use max value". +
+ +
+ Error: data value with undefined color. Check metric values, color values or define new color. +
+
Colors
@@ -36,64 +44,104 @@
-
+
+ +
+ Note: Bucket color determined by maximum for multiple values
+
+ +
+ + + + + + + + +
+ + +
+ +
-
+
Color scale
- +
- +
-
Legend
+
Display
+ +
-
Buckets
+
Graph
+ + + +
- + +
+ +
+
+
+
- +
-
Tooltip
- - -
- - -
-
Null value
-
- -
- -
-
diff --git a/src/options_editor.js b/src/options_editor.js index c3d6128..e020298 100644 --- a/src/options_editor.js +++ b/src/options_editor.js @@ -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, }; } diff --git a/src/rendering.js b/src/rendering.js index bbb8d8b..3b819f5 100644 --- a/src/rendering.js +++ b/src/rendering.js @@ -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; } diff --git a/src/status_heatmap_ctrl.js b/src/status_heatmap_ctrl.js index cf93aed..55a715c 100644 --- a/src/status_heatmap_ctrl.js +++ b/src/status_heatmap_ctrl.js @@ -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; }; + } diff --git a/src/tooltip.js b/src/tooltip.js index ccb853b..21b205f 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -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 = `
${time}
`; - tooltipHtml += `
+ if (this.panel.color.mode === 'discrete') { + let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values); + let statusesHtml = ''; + if (statuses.length > 0) { + statusesHtml = ` + statuses: +
    + ${_.join(_.map(statuses, v => `
  • ${v}
  • `), "")} +
`; + } + tooltipHtml += `
+ name: ${y}
+ ${statusesHtml} +
`; + } else { + if (values.length === 1) { + tooltipHtml += `
name: ${y}
value: ${value}
-
`; +
`; + } else { + tooltipHtml += `
+ name: ${y}
+ values: +
    + ${_.join(_.map(values, v => `
  • ${v}
  • `), "")} +
+
`; + } + } + + // "Ambiguous bucket state: Multiple values!"; + if (!this.panel.useMax && card.multipleValues) { + tooltipHtml += `
Error: ${this.panelCtrl.dataWarnings.multipleValues.title}
`; + } + + // Discrete mode errors + if (this.panel.color.mode === 'discrete') { + if (card.noColorDefined) { + let badValues = this.panelCtrl.discreteHelper.getNotMatchedValues(values); + tooltipHtml += `
Error: ${this.panelCtrl.dataWarnings.noColorDefined.title} +
bad values: +
    + ${_.join(_.map(badValues, v => `
  • ${v}
  • `), "")} +
+
`; + + } + } this.tooltip.html(tooltipHtml);