mirror of
https://github.com/timberjoegithub/grafana-statusmap.git
synced 2026-07-22 15:53:09 +00:00
feat: use grafana-toolkit to build a plugin
- fix linter warnings - use yarn: remove Gruntfile.js and package-lock.json ++
This commit is contained in:
+43
-27
@@ -1,4 +1,4 @@
|
||||
import d3 from 'd3';
|
||||
import * as d3 from 'd3';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -22,12 +22,14 @@ export class AnnotationTooltip {
|
||||
this.panel = scope.ctrl.panel;
|
||||
this.mouseOverAnnotationTick = false;
|
||||
|
||||
elem.on("mouseover", this.onMouseOver.bind(this));
|
||||
elem.on("mouseleave", this.onMouseLeave.bind(this));
|
||||
elem.on('mouseover', this.onMouseOver.bind(this));
|
||||
elem.on('mouseleave', this.onMouseLeave.bind(this));
|
||||
}
|
||||
|
||||
onMouseOver(e) {
|
||||
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) { return; }
|
||||
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.tooltip) {
|
||||
this.add();
|
||||
@@ -40,23 +42,29 @@ export class AnnotationTooltip {
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (!this.panel.tooltip.show) { return; }
|
||||
if (!this.panel.tooltip.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.move(e);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.tooltipBase = d3.select("body")
|
||||
.append("div")
|
||||
.attr("class", "statusmap-annotation-tooltip drop drop-popover drop-popover--annotation drop-element drop-enabled drop-target-attached-center drop-open drop-open-transitionend drop-after-open")
|
||||
.style("position", "absolute")
|
||||
this.tooltipBase = d3
|
||||
.select('body')
|
||||
.append('div')
|
||||
.attr(
|
||||
'class',
|
||||
'statusmap-annotation-tooltip drop drop-popover drop-popover--annotation drop-element drop-enabled drop-target-attached-center drop-open drop-open-transitionend drop-after-open'
|
||||
)
|
||||
.style('position', 'absolute');
|
||||
this.tooltip = this.tooltipBase
|
||||
.append("div")
|
||||
.attr("class", "drop-content")
|
||||
.append("div")
|
||||
.append("annotation-tooltip")
|
||||
.append("div")
|
||||
.attr("class", "graph-annotation");
|
||||
.append('div')
|
||||
.attr('class', 'drop-content')
|
||||
.append('div')
|
||||
.append('annotation-tooltip')
|
||||
.append('div')
|
||||
.attr('class', 'graph-annotation');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -71,11 +79,12 @@ export class AnnotationTooltip {
|
||||
}
|
||||
|
||||
this.tooltipBase = null;
|
||||
|
||||
}
|
||||
|
||||
show(pos) {
|
||||
if (!this.panel.tooltip.show || !this.tooltip) { return; }
|
||||
if (!this.panel.tooltip.show || !this.tooltip) {
|
||||
return;
|
||||
}
|
||||
// shared tooltip mode
|
||||
//if (pos.panelRelY) {
|
||||
// return;
|
||||
@@ -93,14 +102,14 @@ export class AnnotationTooltip {
|
||||
return;
|
||||
}
|
||||
|
||||
let annoTitle = "";
|
||||
let annoTitle = '';
|
||||
|
||||
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
let annoTime = this.dashboard.formatDate(anno.time, tooltipTimeFormat);
|
||||
let annoText = anno.text;
|
||||
let annoTags:any = [];
|
||||
let annoTags: any = [];
|
||||
if (anno.tags) {
|
||||
annoTags = _.map(anno.tags, t => ({"text": t, "backColor": "rgb(63, 43, 91)", "borderColor":"rgb(101, 81, 129)"}))
|
||||
annoTags = _.map(anno.tags, t => ({ text: t, backColor: 'rgb(63, 43, 91)', borderColor: 'rgb(101, 81, 129)' }));
|
||||
}
|
||||
|
||||
let tooltipHtml = `<div class="graph-annotation__header">
|
||||
@@ -108,7 +117,14 @@ export class AnnotationTooltip {
|
||||
<span class="graph-annotation__time">${annoTime}</span></div>
|
||||
<div class="graph-annotation__body">
|
||||
<div>${annoText}</div>
|
||||
${_.join(_.map(annoTags, t => `<span class="label label-tag small" style="background-color: ${t.backColor}; border-color: ${t.borderColor}">${t.text}</span>`), "")}
|
||||
${_.join(
|
||||
_.map(
|
||||
annoTags,
|
||||
t =>
|
||||
`<span class="label label-tag small" style="background-color: ${t.backColor}; border-color: ${t.borderColor}">${t.text}</span>`
|
||||
),
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<div class="statusmap-histogram"></div>`;
|
||||
|
||||
@@ -118,16 +134,18 @@ export class AnnotationTooltip {
|
||||
}
|
||||
|
||||
move(pos) {
|
||||
if (!this.tooltipBase) { return; }
|
||||
if (!this.tooltipBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elem = $(this.tooltipBase.node())[0];
|
||||
let tooltipWidth = elem.clientWidth;
|
||||
let tooltipHeight = elem.clientHeight;
|
||||
|
||||
let left = pos.pageX - tooltipWidth/2;
|
||||
let left = pos.pageX - tooltipWidth / 2;
|
||||
let top = pos.pageY + TOOLTIP_PADDING_Y;
|
||||
|
||||
if (pos.pageX + tooltipWidth/2 + 10 > window.innerWidth) {
|
||||
if (pos.pageX + tooltipWidth / 2 + 10 > window.innerWidth) {
|
||||
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
|
||||
}
|
||||
|
||||
@@ -135,8 +153,6 @@ export class AnnotationTooltip {
|
||||
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
|
||||
}
|
||||
|
||||
return this.tooltipBase
|
||||
.style("left", left + "px")
|
||||
.style("top", top + "px");
|
||||
return this.tooltipBase.style('left', left + 'px').style('top', top + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
+157
-120
@@ -1,11 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import d3 from 'd3';
|
||||
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
||||
import {contextSrv} from 'app/core/core';
|
||||
import {tickStep} from 'app/core/utils/ticks';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { StatusHeatmapCtrl } from "./module";
|
||||
import * as d3 from 'd3';
|
||||
import { d3ScaleChromatic } from './d3/d3-scale-chromatic';
|
||||
import { contextSrv } from 'grafana/app/core/core';
|
||||
import { tickStep } from 'grafana/app/core/utils/ticks';
|
||||
import coreModule from 'grafana/app/core/core_module';
|
||||
|
||||
import { StatusHeatmapCtrl } from './module';
|
||||
import { PanelEvents } from './libs/grafana/events/index';
|
||||
|
||||
const LEGEND_STEP_WIDTH = 2;
|
||||
@@ -32,7 +33,7 @@ coreModule.directive('optionsColorLegend', function() {
|
||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||
|
||||
if (panel.color.mode === 'spectrum') {
|
||||
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||
let colorScheme = _.find(ctrl.colorSchemes, { value: panel.color.colorScheme });
|
||||
let colorScale = getColorScale(colorScheme, legendWidth);
|
||||
drawSimpleColorLegend(elem, colorScale);
|
||||
} else if (panel.color.mode === 'opacity') {
|
||||
@@ -40,7 +41,7 @@ coreModule.directive('optionsColorLegend', function() {
|
||||
drawSimpleOpacityLegend(elem, colorOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -52,7 +53,7 @@ coreModule.directive('statusHeatmapLegend', function() {
|
||||
restrict: 'E',
|
||||
template: '<div class="status-heatmap-color-legend"><svg width="100px" height="6px"></svg></div>',
|
||||
link: function(scope, elem, attrs) {
|
||||
let ctrl:StatusHeatmapCtrl = scope.ctrl;
|
||||
let ctrl: StatusHeatmapCtrl = scope.ctrl;
|
||||
let panel = scope.ctrl.panel;
|
||||
|
||||
render();
|
||||
@@ -85,7 +86,7 @@ coreModule.directive('statusHeatmapLegend', function() {
|
||||
}
|
||||
|
||||
if (panel.color.mode === 'spectrum') {
|
||||
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||
let colorScheme = _.find(ctrl.colorSchemes, { value: panel.color.colorScheme });
|
||||
drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
|
||||
} else if (panel.color.mode === 'opacity') {
|
||||
let colorOptions = panel.color;
|
||||
@@ -96,37 +97,39 @@ coreModule.directive('statusHeatmapLegend', function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function drawColorLegend(elem, colorScheme, rangeFrom: number, rangeTo:number, maxValue: number, minValue:number) {
|
||||
function drawColorLegend(elem, colorScheme, rangeFrom: number, rangeTo: number, maxValue: number, minValue: number) {
|
||||
let legendElem = $(elem).find('svg');
|
||||
let legend: any = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||
let legendHeight = legendElem.attr("height");
|
||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||
let legendHeight = legendElem.attr('height');
|
||||
|
||||
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
|
||||
let rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
|
||||
// width in pixels in legend space of unit segment in range space
|
||||
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
let colorScale = getColorScale(colorScheme, maxValue, minValue);
|
||||
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||
legend
|
||||
.selectAll('.status-heatmap-color-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.enter()
|
||||
.append('rect')
|
||||
// translate from range space into pixels
|
||||
// and shift all rectangles to the right by 10
|
||||
.attr("x", d => ((d - rangeFrom) * widthFactor)+10)
|
||||
.attr("y", 0)
|
||||
.attr('x', d => (d - rangeFrom) * widthFactor + 10)
|
||||
.attr('y', 0)
|
||||
// rectangles are slightly overlaped to prevent gaps
|
||||
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", d => colorScale(d));
|
||||
.attr('width', LEGEND_STEP_WIDTH + 1)
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', d => colorScale(d));
|
||||
|
||||
drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
|
||||
}
|
||||
@@ -136,29 +139,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||
let legendHeight = legendElem.attr("height");
|
||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||
let legendHeight = legendElem.attr('height');
|
||||
|
||||
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
|
||||
let rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
|
||||
// width in pixels in legend space of unit segment in range space
|
||||
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||
legend
|
||||
.selectAll('.status-heatmap-opacity-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.enter()
|
||||
.append('rect')
|
||||
// translate from range space into pixels
|
||||
// and shift all rectangles to the right by 10
|
||||
.attr("x", d => d * widthFactor+10)
|
||||
.attr("y", 0)
|
||||
.attr('x', d => d * widthFactor + 10)
|
||||
.attr('y', 0)
|
||||
// rectangles are slightly overlaped to prevent gaps
|
||||
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", options.cardColor)
|
||||
.style("opacity", d => opacityScale(d));
|
||||
.attr('width', LEGEND_STEP_WIDTH + 1)
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', options.cardColor)
|
||||
.style('opacity', d => opacityScale(d));
|
||||
|
||||
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
|
||||
}
|
||||
@@ -173,51 +178,62 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteExtraSeries) {
|
||||
let valuesNumber = thresholds.length;
|
||||
|
||||
// graph width as a fallback
|
||||
const $heatmap = $(elem).parent().parent().parent().find('.statusmap-panel');
|
||||
const graphWidthAttr = $heatmap.find('svg').attr("width");
|
||||
let graphWidth = parseInt(graphWidthAttr);
|
||||
|
||||
const $heatmap = $(elem)
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.find('.statusmap-panel');
|
||||
const graphWidthAttr = $heatmap.find('svg').attr('width');
|
||||
let graphWidth = parseInt(graphWidthAttr, 10);
|
||||
|
||||
// calculate max width of tooltip and use it as width for each item
|
||||
let textWidth:number[] = [];
|
||||
legend.selectAll(".hidden-texts")
|
||||
let textWidth: number[] = [];
|
||||
legend
|
||||
.selectAll('.hidden-texts')
|
||||
.data(tooltips)
|
||||
.enter().append("text")
|
||||
.attr("class", "axis tick hidden-texts")
|
||||
.attr("font-family", "sans-serif")
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'axis tick hidden-texts')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.text(d => d)
|
||||
.each(function(d,i) {
|
||||
.each(function(d, i) {
|
||||
let thisWidth = this.getBBox().width;
|
||||
textWidth.push(thisWidth);
|
||||
});
|
||||
legend.selectAll(".hidden-texts").remove();
|
||||
legend.selectAll('.hidden-texts').remove();
|
||||
|
||||
let legendWidth = Math.floor(_.min([
|
||||
graphWidth - 30,
|
||||
(_.max(textWidth)! + 3) * valuesNumber,
|
||||
])!);
|
||||
legendElem.attr("width", legendWidth);
|
||||
let legendWidth = Math.floor(_.min([graphWidth - 30, (_.max(textWidth)! + 3) * valuesNumber])!);
|
||||
legendElem.attr('width', legendWidth);
|
||||
|
||||
let legendHeight = legendElem.attr("height");
|
||||
let legendHeight = legendElem.attr('height');
|
||||
|
||||
let itemWidth = Math.floor(legendWidth / valuesNumber);
|
||||
let itemWidth = Math.floor(legendWidth / valuesNumber);
|
||||
let valuesRange = d3.range(valuesNumber); // from 0 to valuesNumber-1
|
||||
|
||||
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||
legend
|
||||
.selectAll('.status-heatmap-color-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d*itemWidth)
|
||||
.attr("y", 0)
|
||||
.attr("width", itemWidth + 1) // Overlap rectangles to prevent gaps
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", d => discreteExtraSeries.getDiscreteColor(d));
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d * itemWidth)
|
||||
.attr('y', 0)
|
||||
.attr('width', itemWidth + 1) // Overlap rectangles to prevent gaps
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', d => discreteExtraSeries.getDiscreteColor(d));
|
||||
|
||||
drawDiscreteLegendValues(elem, colorOptions, legendWidth);
|
||||
}
|
||||
|
||||
|
||||
function drawLegendValues(elem, colorScale, rangeFrom: number, rangeTo: number, maxValue: number, minValue: number, legendWidth: number) {
|
||||
function drawLegendValues(
|
||||
elem,
|
||||
colorScale,
|
||||
rangeFrom: number,
|
||||
rangeTo: number,
|
||||
maxValue: number,
|
||||
minValue: number,
|
||||
legendWidth: number
|
||||
) {
|
||||
let legendElem = $(elem).find('svg');
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
|
||||
@@ -225,25 +241,31 @@ function drawLegendValues(elem, colorScale, rangeFrom: number, rangeTo: number,
|
||||
return;
|
||||
}
|
||||
|
||||
let legendValueScale = d3.scaleLinear()
|
||||
let legendValueScale = d3
|
||||
.scaleLinear()
|
||||
.domain([rangeFrom, rangeTo])
|
||||
.range([0, legendWidth]);
|
||||
|
||||
let ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
|
||||
let xAxis = d3.axisBottom(legendValueScale)
|
||||
let xAxis = d3
|
||||
.axisBottom(legendValueScale)
|
||||
.tickValues(ticks)
|
||||
.tickSize(2);
|
||||
|
||||
let colorRect = legendElem.find(":first-child");
|
||||
let colorRect = legendElem.find(':first-child');
|
||||
let posY = getSvgElemHeight(legendElem) + 2;
|
||||
let posX = getSvgElemX(colorRect);
|
||||
|
||||
d3.select(legendElem.get(0)).append("g")
|
||||
.attr("class", "axis")
|
||||
.attr("transform", "translate(" + posX + "," + posY + ")")
|
||||
d3.select(legendElem.get(0))
|
||||
.append('g')
|
||||
.attr('class', 'axis')
|
||||
.attr('transform', 'translate(' + posX + ',' + posY + ')')
|
||||
.call(xAxis);
|
||||
|
||||
legend.select(".axis").select(".domain").remove();
|
||||
legend
|
||||
.select('.axis')
|
||||
.select('.domain')
|
||||
.remove();
|
||||
}
|
||||
|
||||
function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
||||
@@ -257,10 +279,11 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
||||
}
|
||||
|
||||
let valuesNumber = thresholds.length;
|
||||
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||
//let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
|
||||
let legendValueScale = d3.scaleLinear()
|
||||
let legendValueScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, valuesNumber])
|
||||
.range([0, legendWidth]);
|
||||
|
||||
@@ -271,34 +294,39 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
||||
thresholdTooltips.push(thresholds[i].tooltip);
|
||||
}
|
||||
|
||||
let xAxis = d3.axisBottom(legendValueScale)
|
||||
let xAxis = d3
|
||||
.axisBottom(legendValueScale)
|
||||
.tickValues(d3.range(0, valuesNumber, 1)) //thresholdValues)
|
||||
.tickSize(2)
|
||||
.tickFormat((t) => {
|
||||
.tickFormat(t => {
|
||||
let i = Math.floor(t.valueOf());
|
||||
let v = thresholdTooltips[i];
|
||||
if (v != undefined) {
|
||||
return ""+v;
|
||||
if (v !== undefined) {
|
||||
return '' + v;
|
||||
} else {
|
||||
v = thresholdValues[i];
|
||||
if (v != undefined) {
|
||||
return ""+v;
|
||||
if (v !== undefined) {
|
||||
return '' + v;
|
||||
} else {
|
||||
return "n/a";
|
||||
return 'n/a';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let colorRect = legendElem.find(":first-child");
|
||||
let colorRect = legendElem.find(':first-child');
|
||||
let posY = getSvgElemHeight(legendElem) + 2;
|
||||
let posX = getSvgElemX(colorRect) + Math.floor(rangeStep/2);
|
||||
let posX = getSvgElemX(colorRect) + Math.floor(rangeStep / 2);
|
||||
|
||||
d3.select(legendElem.get(0)).append("g")
|
||||
.attr("class", "axis")
|
||||
.attr("transform", "translate(" + posX + "," + posY + ")")
|
||||
d3.select(legendElem.get(0))
|
||||
.append('g')
|
||||
.attr('class', 'axis')
|
||||
.attr('transform', 'translate(' + posX + ',' + posY + ')')
|
||||
.call(xAxis);
|
||||
|
||||
legend.select(".axis").select(".domain").remove();
|
||||
legend
|
||||
.select('.axis')
|
||||
.select('.domain')
|
||||
.remove();
|
||||
}
|
||||
|
||||
function drawSimpleColorLegend(elem, colorScale) {
|
||||
@@ -307,20 +335,22 @@ function drawSimpleColorLegend(elem, colorScale) {
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||
let legendHeight = legendElem.attr("height");
|
||||
let legendHeight = legendElem.attr('height');
|
||||
|
||||
if (legendWidth) {
|
||||
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||
|
||||
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||
legend
|
||||
.selectAll('.status-heatmap-color-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d)
|
||||
.attr("y", 0)
|
||||
.attr("width", LEGEND_STEP_WIDTH + 1) // Overlap rectangles to prevent gaps
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", d => colorScale(d));
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d)
|
||||
.attr('y', 0)
|
||||
.attr('width', LEGEND_STEP_WIDTH + 1) // Overlap rectangles to prevent gaps
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', d => colorScale(d));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,36 +360,40 @@ function drawSimpleOpacityLegend(elem, options) {
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||
let legendHeight = legendElem.attr("height");
|
||||
let legendHeight = legendElem.attr('height');
|
||||
|
||||
if (legendWidth) {
|
||||
let legendOpacityScale;
|
||||
if (options.colorScale === 'linear') {
|
||||
legendOpacityScale = d3.scaleLinear()
|
||||
.domain([0, legendWidth])
|
||||
.range([0, 1]);
|
||||
legendOpacityScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, legendWidth])
|
||||
.range([0, 1]);
|
||||
} else if (options.colorScale === 'sqrt') {
|
||||
legendOpacityScale = d3.scalePow().exponent(options.exponent)
|
||||
.domain([0, legendWidth])
|
||||
.range([0, 1]);
|
||||
legendOpacityScale = d3
|
||||
.scalePow()
|
||||
.exponent(options.exponent)
|
||||
.domain([0, legendWidth])
|
||||
.range([0, 1]);
|
||||
}
|
||||
|
||||
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||
|
||||
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||
legend
|
||||
.selectAll('.status-heatmap-opacity-legend-rect')
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d)
|
||||
.attr("y", 0)
|
||||
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", options.cardColor)
|
||||
.style("opacity", d => legendOpacityScale(d));
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d)
|
||||
.attr('y', 0)
|
||||
.attr('width', LEGEND_STEP_WIDTH + 1)
|
||||
.attr('height', legendHeight)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('fill', options.cardColor)
|
||||
.style('opacity', d => legendOpacityScale(d));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clearLegend(elem) {
|
||||
let legendElem = $(elem).find('svg');
|
||||
legendElem.empty();
|
||||
@@ -367,8 +401,8 @@ function clearLegend(elem) {
|
||||
|
||||
function getColorScale(colorScheme, maxValue, minValue = 0) {
|
||||
let colorInterpolator = d3ScaleChromatic[colorScheme.value];
|
||||
let colorScaleInverted = colorScheme.invert === 'always' ||
|
||||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
||||
let colorScaleInverted =
|
||||
colorScheme.invert === 'always' || (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
||||
|
||||
let start = colorScaleInverted ? maxValue : minValue;
|
||||
let end = colorScaleInverted ? minValue : maxValue;
|
||||
@@ -379,13 +413,16 @@ function getColorScale(colorScheme, maxValue, minValue = 0) {
|
||||
function getOpacityScale(options, maxValue, minValue = 0) {
|
||||
let legendOpacityScale;
|
||||
if (options.colorScale === 'linear') {
|
||||
legendOpacityScale = d3.scaleLinear()
|
||||
.domain([minValue, maxValue])
|
||||
.range([0, 1]);
|
||||
legendOpacityScale = d3
|
||||
.scaleLinear()
|
||||
.domain([minValue, maxValue])
|
||||
.range([0, 1]);
|
||||
} else if (options.colorScale === 'sqrt') {
|
||||
legendOpacityScale = d3.scalePow().exponent(options.exponent)
|
||||
.domain([minValue, maxValue])
|
||||
.range([0, 1]);
|
||||
legendOpacityScale = d3
|
||||
.scalePow()
|
||||
.exponent(options.exponent)
|
||||
.domain([minValue, maxValue])
|
||||
.range([0, 1]);
|
||||
}
|
||||
return legendOpacityScale;
|
||||
}
|
||||
@@ -412,7 +449,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
|
||||
let range = rangeTo - rangeFrom;
|
||||
let tickStepSize = tickStep(rangeFrom, rangeTo, 3);
|
||||
let ticksNum = Math.round(range / tickStepSize);
|
||||
let ticks:any = [];
|
||||
let ticks: any = [];
|
||||
|
||||
for (let i = 0; i < ticksNum; i++) {
|
||||
let current = tickStepSize * i + rangeFrom;
|
||||
|
||||
+46
-47
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { Bucket } from "./statusmap_data";
|
||||
import { StatusHeatmapCtrl } from "./module";
|
||||
import { Bucket } from './statusmap_data';
|
||||
import { StatusHeatmapCtrl } from './module';
|
||||
|
||||
interface Tooltip {
|
||||
tooltip: string;
|
||||
@@ -26,16 +26,16 @@ export class ColorModeDiscrete {
|
||||
}
|
||||
|
||||
// get tooltip for each value ordered by thresholds priority
|
||||
convertValuesToTooltips(values:any[]) : Tooltip[] {
|
||||
convertValuesToTooltips(values: any[]): Tooltip[] {
|
||||
let thresholds = this.panel.color.thresholds;
|
||||
let tooltips:Tooltip[] = [];
|
||||
let tooltips: Tooltip[] = [];
|
||||
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
for (let j = 0; j < values.length; j++) {
|
||||
if (values[j] == thresholds[i].value) {
|
||||
if (values[j] === thresholds[i].value) {
|
||||
tooltips.push({
|
||||
"tooltip": thresholds[i].tooltip?thresholds[i].tooltip:values[j],
|
||||
"color": thresholds[i].color
|
||||
tooltip: thresholds[i].tooltip ? thresholds[i].tooltip : values[j],
|
||||
color: thresholds[i].color,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,19 +49,19 @@ export class ColorModeDiscrete {
|
||||
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
//for (let j = 0; j < values.length; j++) {
|
||||
if (values == thresholds[i].value) {
|
||||
tooltips.push({
|
||||
"tooltip": thresholds[i].tooltip?thresholds[i].tooltip:values,
|
||||
"color": thresholds[i].color
|
||||
});
|
||||
if (values === thresholds[i].value) {
|
||||
tooltips.push({
|
||||
tooltip: thresholds[i].tooltip ? thresholds[i].tooltip : values,
|
||||
color: thresholds[i].color,
|
||||
});
|
||||
//}
|
||||
}
|
||||
}
|
||||
return tooltips;
|
||||
}
|
||||
|
||||
getNotMatchedValues(values:any[]) {
|
||||
let notMatched:any[] = [];
|
||||
getNotMatchedValues(values: any[]) {
|
||||
let notMatched: any[] = [];
|
||||
for (let j = 0; j < values.length; j++) {
|
||||
if (!this.getMatchedThreshold(values[j])) {
|
||||
notMatched.push(values[j]);
|
||||
@@ -70,11 +70,11 @@ export class ColorModeDiscrete {
|
||||
return notMatched;
|
||||
}
|
||||
|
||||
getNotColoredValues(values:any[]) {
|
||||
let notMatched:any[] = [];
|
||||
getNotColoredValues(values: any[]) {
|
||||
let notMatched: any[] = [];
|
||||
for (let j = 0; j < values.length; j++) {
|
||||
let threshold = this.getMatchedThreshold(values[j]);
|
||||
if (!threshold || !threshold.color || threshold.color == "") {
|
||||
if (!threshold || !threshold.color || threshold.color === '') {
|
||||
notMatched.push(values[j]);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export class ColorModeDiscrete {
|
||||
|
||||
getDiscreteColor(index) {
|
||||
let color = this.getThreshold(index).color;
|
||||
if (!color || color == "") {
|
||||
if (!color || color === '') {
|
||||
return 'rgba(0,0,0,1)';
|
||||
}
|
||||
return color;
|
||||
@@ -96,27 +96,27 @@ export class ColorModeDiscrete {
|
||||
return 'rgba(0,0,0,1)';
|
||||
//return this.getMatchedThreshold(null).color;
|
||||
}
|
||||
let threshold = this.getMatchedThreshold(value);
|
||||
let threshold = this.getMatchedThreshold(value);
|
||||
|
||||
if (!threshold || !threshold.color || threshold.color == "") {
|
||||
return 'rgba(0,0,0,1)';
|
||||
} else {
|
||||
return threshold.color;
|
||||
}
|
||||
if (!threshold || !threshold.color || threshold.color === '') {
|
||||
return 'rgba(0,0,0,1)';
|
||||
} else {
|
||||
return threshold.color;
|
||||
}
|
||||
}
|
||||
|
||||
// returns color from first matched thresold in order from 0 to thresholds.length
|
||||
getBucketColor(values) {
|
||||
let thresholds = this.panel.color.thresholds;
|
||||
|
||||
if (!values || values.length == 0) {
|
||||
if (!values || values.length === 0) {
|
||||
// treat as null value
|
||||
return this.getMatchedThreshold(null).color;
|
||||
}
|
||||
|
||||
if (values.length == 1) {
|
||||
if (values.length === 1) {
|
||||
let threshold = this.getMatchedThreshold(values[0]);
|
||||
if (!threshold || !threshold.color || threshold.color == "") {
|
||||
if (!threshold || !threshold.color || threshold.color === '') {
|
||||
return 'rgba(0,0,0,1)';
|
||||
} else {
|
||||
return threshold.color;
|
||||
@@ -135,7 +135,7 @@ export class ColorModeDiscrete {
|
||||
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
for (let j = 0; j < values.length; j++) {
|
||||
if (values[j] == thresholds[i].value) {
|
||||
if (values[j] === thresholds[i].value) {
|
||||
return this.getDiscreteColor(i);
|
||||
}
|
||||
}
|
||||
@@ -143,18 +143,17 @@ export class ColorModeDiscrete {
|
||||
return 'rgba(0,0,0,1)';
|
||||
}
|
||||
|
||||
|
||||
updateCardsValuesHasColorInfoSingle() {
|
||||
if (!this.panelCtrl.bucketMatrix) {
|
||||
return;
|
||||
}
|
||||
this.panelCtrl.bucketMatrix.noColorDefined = false;
|
||||
|
||||
this.panelCtrl.bucketMatrix.targets.map((target:string) => {
|
||||
this.panelCtrl.bucketMatrix.buckets[target].map((bucket:Bucket) => {
|
||||
this.panelCtrl.bucketMatrix.targets.map((target: string) => {
|
||||
this.panelCtrl.bucketMatrix.buckets[target].map((bucket: Bucket) => {
|
||||
bucket.noColorDefined = false;
|
||||
let threshold = this.getMatchedThreshold(bucket.value);
|
||||
if (!threshold || !threshold.color || threshold.color == "") {
|
||||
if (!threshold || !threshold.color || threshold.color === '') {
|
||||
bucket.noColorDefined = true;
|
||||
this.panelCtrl.bucketMatrix.noColorDefined = true;
|
||||
}
|
||||
@@ -164,16 +163,16 @@ export class ColorModeDiscrete {
|
||||
|
||||
updateCardsValuesHasColorInfo() {
|
||||
if (!this.panelCtrl.bucketMatrix) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
this.panelCtrl.bucketMatrix.noColorDefined = false;
|
||||
|
||||
this.panelCtrl.bucketMatrix.targets.map((target:string) => {
|
||||
this.panelCtrl.bucketMatrix.buckets[target].map((bucket:Bucket) => {
|
||||
this.panelCtrl.bucketMatrix.targets.map((target: string) => {
|
||||
this.panelCtrl.bucketMatrix.buckets[target].map((bucket: Bucket) => {
|
||||
bucket.noColorDefined = false;
|
||||
for (let j=0; j<bucket.values.length; j++) {
|
||||
for (let j = 0; j < bucket.values.length; j++) {
|
||||
let threshold = this.getMatchedThreshold(bucket.values[j]);
|
||||
if (!threshold || !threshold.color || threshold.color == "") {
|
||||
if (!threshold || !threshold.color || threshold.color === '') {
|
||||
bucket.noColorDefined = true;
|
||||
this.panelCtrl.bucketMatrix.noColorDefined = true;
|
||||
break;
|
||||
@@ -185,14 +184,14 @@ export class ColorModeDiscrete {
|
||||
|
||||
getMatchedThreshold(value) {
|
||||
if (value == null) {
|
||||
if (this.panel.nullPointMode == 'as empty') {
|
||||
if (this.panel.nullPointMode === 'as empty') {
|
||||
// FIXME: make this explicit for user
|
||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||
return {
|
||||
"color": "rgba(0,0,0,0)",
|
||||
"value": "null",
|
||||
"tooltip": "null",
|
||||
}
|
||||
color: 'rgba(0,0,0,0)',
|
||||
value: 'null',
|
||||
tooltip: 'null',
|
||||
};
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
@@ -200,7 +199,7 @@ export class ColorModeDiscrete {
|
||||
|
||||
let thresholds = this.panel.color.thresholds;
|
||||
for (let k = 0; k < thresholds.length; k++) {
|
||||
if (value == thresholds[k].value) {
|
||||
if (value === thresholds[k].value) {
|
||||
return thresholds[k];
|
||||
}
|
||||
}
|
||||
@@ -211,10 +210,10 @@ export class ColorModeDiscrete {
|
||||
let thresholds = this.panel.color.thresholds;
|
||||
if (index < 0 || index >= thresholds.length == null) {
|
||||
return {
|
||||
"color": "rgba(0,0,0,0)",
|
||||
"value": "null",
|
||||
"tooltip": "null",
|
||||
}
|
||||
color: 'rgba(0,0,0,0)',
|
||||
value: 'null',
|
||||
tooltip: 'null',
|
||||
};
|
||||
}
|
||||
return thresholds[index];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as d3 from 'd3';
|
||||
import * as d3ScaleChromaticOrig from 'd3-scale-chromatic';
|
||||
|
||||
let GnYlRdScheme = new Array(3)
|
||||
.concat(
|
||||
'91cf60ffffbffc8d59',
|
||||
'1a9641a6d96afdae61d7191c',
|
||||
'1a9641a6d96affffbffdae61d7191c',
|
||||
'1a985091cf60d9ef8bfee08bfc8d59d73027',
|
||||
'1a985091cf60d9ef8bffffbffee08bfc8d59d73027',
|
||||
'1a985066bd63a6d96ad9ef8bfee08bfdae61f46d43d73027',
|
||||
'1a985066bd63a6d96ad9ef8bffffbffee08bfdae61f46d43d73027',
|
||||
'0068371a985066bd63a6d96ad9ef8bfee08bfdae61f46d43d73027a50026',
|
||||
'0068371a985066bd63a6d96ad9ef8bffffbffee08bfdae61f46d43d73027a50026'
|
||||
)
|
||||
.map(function(specifier) {
|
||||
var n = (specifier.length / 6) | 0,
|
||||
colors = new Array(n),
|
||||
i = 0;
|
||||
while (i < n) {
|
||||
colors[i] = '#' + specifier.slice(i * 6, ++i * 6);
|
||||
}
|
||||
return colors;
|
||||
});
|
||||
|
||||
let GnYlRd = d3.interpolateRgbBasis(GnYlRdScheme[GnYlRdScheme.length - 1]);
|
||||
|
||||
let d3ScaleChromatic = {
|
||||
...d3ScaleChromaticOrig,
|
||||
interpolateGnYlRd: GnYlRd,
|
||||
};
|
||||
|
||||
export { d3ScaleChromatic };
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface AppEvent<T> {
|
||||
readonly name: string;
|
||||
payload?: T;
|
||||
readonly name: string;
|
||||
payload?: T;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {AppEvent} from './appEvents';
|
||||
import { AppEvent } from './appEvents';
|
||||
|
||||
export interface GraphHoverPayload {
|
||||
pos: any;
|
||||
panel: {
|
||||
id: number;
|
||||
};
|
||||
pos: any;
|
||||
panel: {
|
||||
id: number;
|
||||
};
|
||||
}
|
||||
|
||||
export var graphHover:(AppEvent<GraphHoverPayload>|string) = {name: 'graph-hover'};
|
||||
export var graphHoverClear:(AppEvent<any>|string) = {name: 'graph-hover-clear'};
|
||||
export var graphHover: AppEvent<GraphHoverPayload> | string = { name: 'graph-hover' };
|
||||
export var graphHoverClear: AppEvent<any> | string = { name: 'graph-hover-clear' };
|
||||
|
||||
export function fallbackToStringEvents() {
|
||||
graphHover = 'graph-hover';
|
||||
graphHoverClear = 'graph-hover-clear';
|
||||
graphHover = 'graph-hover';
|
||||
graphHoverClear = 'graph-hover-clear';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import * as CoreEvents from './events';
|
||||
import * as PanelEvents from './panelEvents';
|
||||
export { CoreEvents, PanelEvents }
|
||||
export { CoreEvents, PanelEvents };
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { AppEvent } from './appEvents';
|
||||
|
||||
export interface DataQueryError {
|
||||
data?: {
|
||||
message?: string;
|
||||
error?: string;
|
||||
};
|
||||
data?: {
|
||||
message?: string;
|
||||
status?: string;
|
||||
statusText?: string;
|
||||
refId?: string;
|
||||
cancelled?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
message?: string;
|
||||
status?: string;
|
||||
statusText?: string;
|
||||
refId?: string;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
export var refresh:(AppEvent<undefined>|string) = {name: 'refresh'};
|
||||
export var render:(AppEvent<any>|string) = {name: 'render'};
|
||||
export var dataError:(AppEvent<DataQueryError>|string) = {name: 'data-error'};
|
||||
export var dataReceived:(AppEvent<any[]>|string) = {name: 'data-received'};
|
||||
export var dataSnapshotLoad:(AppEvent<any[]>|string) = {name: 'data-snapshot-load'};
|
||||
export var editModeInitialized:(AppEvent<undefined>|string) = {name: 'init-edit-mode'};
|
||||
export var refresh: AppEvent<undefined> | string = { name: 'refresh' };
|
||||
export var render: AppEvent<any> | string = { name: 'render' };
|
||||
export var dataError: AppEvent<DataQueryError> | string = { name: 'data-error' };
|
||||
export var dataReceived: AppEvent<any[]> | string = { name: 'data-received' };
|
||||
export var dataSnapshotLoad: AppEvent<any[]> | string = { name: 'data-snapshot-load' };
|
||||
export var editModeInitialized: AppEvent<undefined> | string = { name: 'init-edit-mode' };
|
||||
|
||||
export function fallbackToStringEvents() {
|
||||
refresh = 'refresh';
|
||||
render = 'render';
|
||||
dataError = 'data-error';
|
||||
dataReceived = 'data-received';
|
||||
dataSnapshotLoad = 'data-snapshot-load';
|
||||
editModeInitialized = 'init-edit-mode';
|
||||
refresh = 'refresh';
|
||||
render = 'render';
|
||||
dataError = 'data-error';
|
||||
dataReceived = 'data-received';
|
||||
dataSnapshotLoad = 'data-snapshot-load';
|
||||
editModeInitialized = 'init-edit-mode';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { Emitter } from 'grafana/app/core/utils/emitter';
|
||||
|
||||
// Old Grafana releases use strings as event ids and
|
||||
// new event ids in form of object {name: "event-id"} are not
|
||||
@@ -7,17 +7,17 @@ import { Emitter } from 'app/core/utils/emitter';
|
||||
//
|
||||
// This method detects this behaviour and return true
|
||||
// only for new Grafana versions.
|
||||
export function hasAppEventCompatibleEmitter(emitter: Emitter):boolean {
|
||||
let receiveEvents = 0;
|
||||
let eventId: any = {name: "non-existed-event-id"};
|
||||
let eventId2: any = {name: "non-existed-event-id-2"};
|
||||
emitter.on(eventId, function(){
|
||||
receiveEvents++;
|
||||
});
|
||||
emitter.emit(eventId);
|
||||
emitter.emit(eventId2);
|
||||
emitter.removeAllListeners(eventId);
|
||||
export function hasAppEventCompatibleEmitter(emitter: Emitter): boolean {
|
||||
let receiveEvents = 0;
|
||||
let eventId: any = { name: 'non-existed-event-id' };
|
||||
let eventId2: any = { name: 'non-existed-event-id-2' };
|
||||
emitter.on(eventId, function() {
|
||||
receiveEvents++;
|
||||
});
|
||||
emitter.emit(eventId);
|
||||
emitter.emit(eventId2);
|
||||
emitter.removeAllListeners(eventId);
|
||||
|
||||
// New Grafana versions should receive one event.
|
||||
return receiveEvents == 1;
|
||||
// New Grafana versions should receive one event.
|
||||
return receiveEvents === 1;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import * as Polygrafill from './funcs';
|
||||
export { Polygrafill };
|
||||
export { Polygrafill };
|
||||
|
||||
+91
-83
@@ -9,22 +9,21 @@ import { tooltipEditorCtrl } from './tooltip_editor';
|
||||
import { migratePanelConfig } from './panel_config_migration';
|
||||
|
||||
// Utils
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {loadPluginCss} from 'app/plugins/sdk';
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import { loadPluginCss } from 'grafana/app/plugins/sdk';
|
||||
|
||||
// Types
|
||||
import { MetricsPanelCtrl } from 'app/plugins/sdk';
|
||||
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
|
||||
import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';
|
||||
import { AnnotationsSrv } from 'grafana/app/features/annotations/annotations_srv';
|
||||
import { CoreEvents, PanelEvents } from './libs/grafana/events/index';
|
||||
import {Bucket, BucketMatrix, BucketMatrixPager } from './statusmap_data';
|
||||
import { Bucket, BucketMatrix, BucketMatrixPager } from './statusmap_data';
|
||||
import rendering from './rendering';
|
||||
import { Polygrafill } from './libs/polygrafill/index';
|
||||
|
||||
|
||||
import {ColorModeDiscrete} from "./color_mode_discrete";
|
||||
import { ColorModeDiscrete } from './color_mode_discrete';
|
||||
|
||||
const VALUE_INDEX = 0,
|
||||
TIME_INDEX = 1;
|
||||
TIME_INDEX = 1;
|
||||
|
||||
const colorSchemes = [
|
||||
// Diverging
|
||||
@@ -52,18 +51,18 @@ const colorSchemes = [
|
||||
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
|
||||
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
|
||||
{ name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' },
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' }
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
|
||||
];
|
||||
|
||||
let colorModes = ['opacity', 'spectrum', 'discrete'];
|
||||
let opacityScales = ['linear', 'sqrt'];
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
|
||||
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
|
||||
dark: 'plugins/flant-statusmap-panel/styles/dark.css',
|
||||
light: 'plugins/flant-statusmap-panel/styles/light.css',
|
||||
});
|
||||
|
||||
export var renderComplete:any = {name:'statusmap-render-complete'};
|
||||
export var renderComplete: any = { name: 'statusmap-render-complete' };
|
||||
|
||||
class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
static templateUrl = 'module.html';
|
||||
@@ -79,7 +78,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
colorSchemes: any = [];
|
||||
unitFormats: any;
|
||||
|
||||
dataWarnings: {[warningId: string]: {title: string, tip: string}} = {};
|
||||
dataWarnings: { [warningId: string]: { title: string; tip: string } } = {};
|
||||
multipleValues: boolean;
|
||||
noColorDefined: boolean;
|
||||
noDatapoints: boolean;
|
||||
@@ -90,7 +89,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
annotationsPromise: any;
|
||||
|
||||
// TODO remove this transient variable: use ng-model-options="{ getterSetter: true }"
|
||||
pageSizeViewer: number = 15;
|
||||
pageSizeViewer = 15;
|
||||
|
||||
panelDefaults: any = {
|
||||
// datasource name, null = default datasource
|
||||
@@ -104,22 +103,22 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
colorScheme: 'interpolateGnYlRd',
|
||||
// discrete mode settings
|
||||
defaultColor: '#757575',
|
||||
thresholds: [] // manual colors
|
||||
thresholds: [], // manual colors
|
||||
},
|
||||
// buckets settings
|
||||
cards: {
|
||||
cardMinWidth: 5,
|
||||
cardVSpacing: 2,
|
||||
cardHSpacing: 2,
|
||||
cardRound: null
|
||||
cardRound: null,
|
||||
},
|
||||
xAxis: {
|
||||
show: true
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
show: true,
|
||||
minWidth: -1,
|
||||
maxWidth: -1
|
||||
maxWidth: -1,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
@@ -127,15 +126,15 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
showItems: false,
|
||||
items: [], // see tooltip_editor.ts
|
||||
showExtraInfo: false,
|
||||
extraInfo: "",
|
||||
extraInfo: '',
|
||||
},
|
||||
legend: {
|
||||
show: true
|
||||
show: true,
|
||||
},
|
||||
yLabel: {
|
||||
usingSplitLabel: false,
|
||||
delimiter: "",
|
||||
labelTemplate: "",
|
||||
delimiter: '',
|
||||
labelTemplate: '',
|
||||
},
|
||||
// how null points should be handled
|
||||
nullPointMode: 'as empty',
|
||||
@@ -147,21 +146,21 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
// Pagination options
|
||||
usingPagination: false,
|
||||
pageSize: 15
|
||||
pageSize: 15,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any, $injector: auto.IInjectorService, private annotationsSrv: AnnotationsSrv) {
|
||||
super($scope, $injector);
|
||||
|
||||
if(!Polygrafill.hasAppEventCompatibleEmitter(this.events)){
|
||||
if (!Polygrafill.hasAppEventCompatibleEmitter(this.events)) {
|
||||
CoreEvents.fallbackToStringEvents();
|
||||
PanelEvents.fallbackToStringEvents();
|
||||
renderComplete = 'statusmap-render-complete';
|
||||
}
|
||||
|
||||
// Grafana 7.2 workaround
|
||||
if (typeof kbn["intervalToMs"] === "function") {
|
||||
if (typeof kbn['intervalToMs'] === 'function') {
|
||||
kbn.interval_to_ms = kbn.intervalToMs;
|
||||
}
|
||||
|
||||
@@ -183,7 +182,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
// default graph width for discrete card width calculation
|
||||
this.graph = {
|
||||
"chartWidth" : -1
|
||||
chartWidth: -1,
|
||||
};
|
||||
|
||||
this.multipleValues = false;
|
||||
@@ -203,7 +202,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
noDatapoints: {
|
||||
title: 'No data points',
|
||||
tip: 'No datapoints returned from data query',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.annotations = [];
|
||||
@@ -221,7 +220,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
this.onCardColorChange = this.onCardColorChange.bind(this);
|
||||
}
|
||||
|
||||
onRenderComplete(data: any):void {
|
||||
onRenderComplete(data: any): void {
|
||||
this.graph.chartWidth = data.chartWidth;
|
||||
this.renderingCompleted();
|
||||
}
|
||||
@@ -256,10 +255,9 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
||||
// getChartWidth returns an approximation of chart canvas width or
|
||||
// a saved value calculated during a render.
|
||||
getChartWidth():number {
|
||||
getChartWidth(): number {
|
||||
if (this.graph.chartWidth > 0) {
|
||||
return this.graph.chartWidth;
|
||||
}
|
||||
@@ -272,10 +270,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
// - y axis ticks are not rendered yet on first data receive,
|
||||
// so choose 200 as a decent value for y legend width
|
||||
// - chartWidth can not be lower than the half of the panel width.
|
||||
const chartWidth = _.max([
|
||||
panelWidth - 200,
|
||||
panelWidth/2
|
||||
]);
|
||||
const chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
|
||||
|
||||
return chartWidth!;
|
||||
}
|
||||
@@ -283,7 +278,9 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
// Quick workaround for 6.7 and 7.0+. There is no call to
|
||||
// calculateInterval in updateTimeRange in those versions.
|
||||
// TODO ts type has no argument for this method.
|
||||
//
|
||||
updateTimeRange(datasource?: any) {
|
||||
// @ts-ignore
|
||||
let ret = super.updateTimeRange(datasource);
|
||||
this.calculateInterval();
|
||||
return ret;
|
||||
@@ -297,7 +294,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
let minCardWidth = this.panel.cards.cardMinWidth;
|
||||
let minSpacing = this.panel.cards.cardHSpacing;
|
||||
let maxCardsCount = Math.ceil((chartWidth-minCardWidth) / (minCardWidth + minSpacing));
|
||||
let maxCardsCount = Math.ceil((chartWidth - minCardWidth) / (minCardWidth + minSpacing));
|
||||
|
||||
let intervalMs;
|
||||
let rangeMs = this.range.to.valueOf() - this.range.from.valueOf();
|
||||
@@ -307,7 +304,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
// Calculate low limit of interval
|
||||
let lowLimitMs = 1; // 1 millisecond default low limit
|
||||
|
||||
|
||||
let intervalOverride = this.panel.interval;
|
||||
|
||||
// if no panel interval check datasource
|
||||
@@ -351,7 +348,9 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
* issue 11806.
|
||||
*/
|
||||
// 5.x before 5.4 doesn't have datasourcePromises.
|
||||
if ("undefined" !== typeof(this.annotationsSrv.datasourcePromises)) {
|
||||
// @ts-ignore
|
||||
if ('undefined' !== typeof this.annotationsSrv.datasourcePromises) {
|
||||
// @ts-ignore
|
||||
return this.annotationsSrv.datasourcePromises.then(r => {
|
||||
return this.issueQueriesWithInterval(datasource, this.interval);
|
||||
});
|
||||
@@ -372,14 +371,20 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
onDataReceived(dataList: any) {
|
||||
this.data = dataList;
|
||||
this.data = dataList;
|
||||
// Quick workaround for 7.0+. There is no call to
|
||||
// calculateInterval when enter Edit mode.
|
||||
if (!this.intervalMs) {
|
||||
this.calculateInterval();
|
||||
}
|
||||
|
||||
let newBucketMatrix = this.convertDataToBuckets(dataList, this.range.from.valueOf(), this.range.to.valueOf(), this.intervalMs, true);
|
||||
let newBucketMatrix = this.convertDataToBuckets(
|
||||
dataList,
|
||||
this.range.from.valueOf(),
|
||||
this.range.to.valueOf(),
|
||||
this.intervalMs,
|
||||
true
|
||||
);
|
||||
|
||||
this.bucketMatrix = newBucketMatrix;
|
||||
this.bucketMatrixPager.bucketMatrix = newBucketMatrix;
|
||||
@@ -437,7 +442,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
this.noColorDefined = false;
|
||||
if (this.panel.color.mode === 'discrete') {
|
||||
if (this.panel.seriesFilterIndex == -1) {
|
||||
if (this.panel.seriesFilterIndex === -1) {
|
||||
this.discreteExtraSeries.updateCardsValuesHasColorInfo();
|
||||
} else {
|
||||
this.discreteExtraSeries.updateCardsValuesHasColorInfoSingle();
|
||||
@@ -514,26 +519,30 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
to and from — a time range of the panel.
|
||||
intervalMs — a calculated interval. It is used to split a time range.
|
||||
*/
|
||||
convertDataToBuckets(data:any, from:number, to:number, intervalMs: number, mostRecentBucket: boolean):BucketMatrix {
|
||||
convertDataToBuckets(
|
||||
data: any,
|
||||
from: number,
|
||||
to: number,
|
||||
intervalMs: number,
|
||||
mostRecentBucket: boolean
|
||||
): BucketMatrix {
|
||||
let bucketMatrix = new BucketMatrix();
|
||||
bucketMatrix.rangeMs = to - from;
|
||||
bucketMatrix.intervalMs = intervalMs;
|
||||
|
||||
if (!data || data.length == 0) {
|
||||
if (!data || data.length === 0) {
|
||||
// Mimic heatmap and graph 'no data' labels.
|
||||
bucketMatrix.targets = [
|
||||
"1.0", "0.0", "-1.0"
|
||||
];
|
||||
bucketMatrix.buckets["1.0"] = [];
|
||||
bucketMatrix.buckets["0.0"] = [];
|
||||
bucketMatrix.buckets["-1.0"] = [];
|
||||
bucketMatrix.targets = ['1.0', '0.0', '-1.0'];
|
||||
bucketMatrix.buckets['1.0'] = [];
|
||||
bucketMatrix.buckets['0.0'] = [];
|
||||
bucketMatrix.buckets['-1.0'] = [];
|
||||
bucketMatrix.xBucketSize = 42;
|
||||
bucketMatrix.noDatapoints = true;
|
||||
return bucketMatrix;
|
||||
}
|
||||
|
||||
let targetIndex: {[target: string]: number[]} = {};
|
||||
let targetPartials: {[target: string]: string[]} = {};
|
||||
let targetIndex: { [target: string]: number[] } = {};
|
||||
let targetPartials: { [target: string]: string[] } = {};
|
||||
|
||||
// Group indicies of elements in data by target (y label).
|
||||
|
||||
@@ -547,22 +556,22 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
let yLabel = queryResult.target;
|
||||
|
||||
// Check if there is some labelTemplate configured
|
||||
if (this.panel.yLabel.usingSplitLabel && this.panel.yLabel.delimiter != "" ) {
|
||||
if (this.panel.yLabel.usingSplitLabel && this.panel.yLabel.delimiter !== '') {
|
||||
let pLabels = queryResult.target.split(this.panel.yLabel.delimiter);
|
||||
|
||||
// Load all possible values as scoped vars and load them into targetPartials
|
||||
// to be used on different components as Bucket and BucketMatrix props
|
||||
let scopedVars = []
|
||||
scopedVars[`__y_label`] = {value: yLabel}
|
||||
let scopedVars = [];
|
||||
scopedVars[`__y_label`] = { value: yLabel };
|
||||
for (let i in pLabels) {
|
||||
scopedVars[`__y_label_${i}`] = {value: pLabels[i]};
|
||||
scopedVars[`__y_label_${i}`] = { value: pLabels[i] };
|
||||
}
|
||||
|
||||
if (this.panel.yLabel.labelTemplate != "") {
|
||||
yLabel = this.templateSrv.replace(this.panel.yLabel.labelTemplate, scopedVars)
|
||||
if (this.panel.yLabel.labelTemplate !== '') {
|
||||
yLabel = this.templateSrv.replace(this.panel.yLabel.labelTemplate, scopedVars);
|
||||
}
|
||||
|
||||
targetPartials[yLabel] = pLabels
|
||||
targetPartials[yLabel] = pLabels;
|
||||
}
|
||||
|
||||
//reset if it already exists
|
||||
@@ -576,18 +585,18 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
//console.log ("targetIndex: ", targetIndex, "targetKeys: ", targetKeys);
|
||||
|
||||
let targetTimestampRanges: {[target: string]: {[timestamp: number]: number[]}} = {};
|
||||
let targetTimestampRanges: { [target: string]: { [timestamp: number]: number[] } } = {};
|
||||
|
||||
// Collect all timestamps for each target.
|
||||
// Make map timestamp => [from, to]. from == previous ts, to == ts from datapoint.
|
||||
targetKeys.map((target) => {
|
||||
targetKeys.map(target => {
|
||||
let targetTimestamps: any[] = [];
|
||||
|
||||
for (let si = 0; si < targetIndex[target].length; si++) {
|
||||
let s = data[targetIndex[target][si]];
|
||||
_.map(s.datapoints, (datapoint, idx) => {
|
||||
targetTimestamps.push(datapoint[TIME_INDEX]-from);
|
||||
})
|
||||
targetTimestamps.push(datapoint[TIME_INDEX] - from);
|
||||
});
|
||||
}
|
||||
|
||||
//console.log("timestamps['"+target+"'] = ", targetTimestamps);
|
||||
@@ -597,15 +606,15 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
//console.log("uniq timestamps['"+target+"'] = ", targetTimestamps);
|
||||
|
||||
targetTimestampRanges[target] = [];
|
||||
for (let i = targetTimestamps.length-1 ; i>=0; i-- ) {
|
||||
for (let i = targetTimestamps.length - 1; i >= 0; i--) {
|
||||
let tsTo = targetTimestamps[i];
|
||||
let tsFrom = 0;
|
||||
if (tsTo < 0) {
|
||||
tsFrom = tsTo - intervalMs;
|
||||
} else {
|
||||
if (i-1 >= 0) {
|
||||
if (i - 1 >= 0) {
|
||||
// Set from to previous timestamp + 1ms;
|
||||
tsFrom = targetTimestamps[i-1]+1;
|
||||
tsFrom = targetTimestamps[i - 1] + 1;
|
||||
// tfTo - tfFrom should not be more than intervalMs
|
||||
let minFrom = tsTo - intervalMs;
|
||||
if (tsFrom < minFrom) {
|
||||
@@ -622,10 +631,10 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
// Create empty buckets using intervalMs to calculate ranges.
|
||||
// If mostRecentBucket is set, create a bucket with a range "to":"to"
|
||||
// to store most recent values.
|
||||
targetKeys.map((target) => {
|
||||
targetKeys.map(target => {
|
||||
let targetEmptyBuckets: any[] = [];
|
||||
|
||||
let lastTs = to-from;
|
||||
let lastTs = to - from;
|
||||
|
||||
if (mostRecentBucket) {
|
||||
let topBucket = new Bucket();
|
||||
@@ -638,21 +647,21 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
topBucket.relFrom = targetTimestampRanges[target][lastTs][0];
|
||||
lastTs = topBucket.relFrom;
|
||||
}
|
||||
topBucket.to = topBucket.relTo+from;
|
||||
topBucket.from = topBucket.relFrom+from;
|
||||
topBucket.to = topBucket.relTo + from;
|
||||
topBucket.from = topBucket.relFrom + from;
|
||||
targetEmptyBuckets.push(topBucket);
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
let bucketFrom: number = 0;
|
||||
let bucketFrom = 0;
|
||||
while (bucketFrom >= 0) {
|
||||
let b = new Bucket();
|
||||
b.yLabel = target;
|
||||
b.pLabels = targetPartials[target]
|
||||
b.relTo = lastTs - idx*intervalMs;
|
||||
b.relFrom = lastTs - ((idx+1) * intervalMs);
|
||||
b.to = b.relTo+from;
|
||||
b.from = b.relFrom+from;
|
||||
b.pLabels = targetPartials[target];
|
||||
b.relTo = lastTs - idx * intervalMs;
|
||||
b.relFrom = lastTs - (idx + 1) * intervalMs;
|
||||
b.to = b.relTo + from;
|
||||
b.from = b.relFrom + from;
|
||||
b.values = [];
|
||||
bucketFrom = b.relFrom;
|
||||
targetEmptyBuckets.push(b);
|
||||
@@ -671,8 +680,8 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
// Put values into buckets.
|
||||
bucketMatrix.minValue = Number.MAX_VALUE;
|
||||
bucketMatrix.maxValue = Number.MIN_SAFE_INTEGER;
|
||||
targetKeys.map((target) => {
|
||||
targetIndex[target].map((dataIndex) => {
|
||||
targetKeys.map(target => {
|
||||
targetIndex[target].map(dataIndex => {
|
||||
let s = data[dataIndex];
|
||||
s.datapoints.map((dp: any) => {
|
||||
for (let i = 0; i < bucketMatrix.buckets[target].length; i++) {
|
||||
@@ -682,7 +691,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
});
|
||||
});
|
||||
bucketMatrix.buckets[target].map((bucket) => {
|
||||
bucketMatrix.buckets[target].map(bucket => {
|
||||
bucket.minValue = _.min(bucket.values);
|
||||
bucket.maxValue = _.max(bucket.values);
|
||||
if (bucket.minValue < bucketMatrix.minValue) {
|
||||
@@ -696,13 +705,14 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
bucketMatrix.multipleValues = true;
|
||||
bucket.multipleValues = true;
|
||||
|
||||
bucket.value = this.panel.seriesFilterIndex != -1 ? bucket.values[this.panel.seriesFilterIndex] : bucket.maxValue;
|
||||
bucket.value =
|
||||
this.panel.seriesFilterIndex !== -1 ? bucket.values[this.panel.seriesFilterIndex] : bucket.maxValue;
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
bucketMatrix.xBucketSize = Number.MIN_SAFE_INTEGER;
|
||||
targetKeys.map((target) => {
|
||||
targetKeys.map(target => {
|
||||
let bucketsLen: number = bucketMatrix.buckets[target].length;
|
||||
if (bucketsLen > bucketMatrix.xBucketSize) {
|
||||
bucketMatrix.xBucketSize = bucketsLen;
|
||||
@@ -717,6 +727,4 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
StatusHeatmapCtrl, StatusHeatmapCtrl as PanelCtrl
|
||||
};
|
||||
export { StatusHeatmapCtrl, StatusHeatmapCtrl as PanelCtrl };
|
||||
|
||||
+17
-18
@@ -1,4 +1,4 @@
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import { StatusHeatmapCtrl } from './module';
|
||||
|
||||
export class StatusHeatmapOptionsEditorCtrl {
|
||||
@@ -29,7 +29,7 @@ export class StatusHeatmapOptionsEditorCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
onRemoveThreshold(index:number) {
|
||||
onRemoveThreshold(index: number) {
|
||||
this.panel.color.thresholds.splice(index, 1);
|
||||
this.render();
|
||||
}
|
||||
@@ -40,25 +40,24 @@ export class StatusHeatmapOptionsEditorCtrl {
|
||||
}
|
||||
|
||||
onAddThreeLights() {
|
||||
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();
|
||||
}
|
||||
|
||||
/* https://ethanschoonover.com/solarized/ */
|
||||
onAddSolarized() {
|
||||
this.panel.color.thresholds.push({color: "#b58900", value: 0, tooltip: "yellow" });
|
||||
this.panel.color.thresholds.push({color: "#cb4b16", value: 1, tooltip: "orange" });
|
||||
this.panel.color.thresholds.push({color: "#dc322f", value: 2, tooltip: "red" });
|
||||
this.panel.color.thresholds.push({color: "#d33682", value: 3, tooltip: "magenta" });
|
||||
this.panel.color.thresholds.push({color: "#6c71c4", value: 4, tooltip: "violet" });
|
||||
this.panel.color.thresholds.push({color: "#268bd2", value: 5, tooltip: "blue" });
|
||||
this.panel.color.thresholds.push({color: "#2aa198", value: 6, tooltip: "cyan" });
|
||||
this.panel.color.thresholds.push({color: "#859900", value: 7, tooltip: "green" });
|
||||
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();
|
||||
}
|
||||
|
||||
/* https://ethanschoonover.com/solarized/ */
|
||||
onAddSolarized() {
|
||||
this.panel.color.thresholds.push({ color: '#b58900', value: 0, tooltip: 'yellow' });
|
||||
this.panel.color.thresholds.push({ color: '#cb4b16', value: 1, tooltip: 'orange' });
|
||||
this.panel.color.thresholds.push({ color: '#dc322f', value: 2, tooltip: 'red' });
|
||||
this.panel.color.thresholds.push({ color: '#d33682', value: 3, tooltip: 'magenta' });
|
||||
this.panel.color.thresholds.push({ color: '#6c71c4', value: 4, tooltip: 'violet' });
|
||||
this.panel.color.thresholds.push({ color: '#268bd2', value: 5, tooltip: 'blue' });
|
||||
this.panel.color.thresholds.push({ color: '#2aa198', value: 6, tooltip: 'cyan' });
|
||||
this.panel.color.thresholds.push({ color: '#859900', value: 7, tooltip: 'green' });
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
|
||||
@@ -2,31 +2,31 @@ import _ from 'lodash';
|
||||
|
||||
function migrate_V0_V1(panel: any) {
|
||||
// Remove unused fields.
|
||||
if (_.has(panel, "xAxis.labelFormat")) {
|
||||
if (_.has(panel, 'xAxis.labelFormat')) {
|
||||
delete panel.xAxis.labelFormat;
|
||||
}
|
||||
if (_.has(panel, "xAxis.minBucketWidthToShowWeekends")) {
|
||||
if (_.has(panel, 'xAxis.minBucketWidthToShowWeekends')) {
|
||||
delete panel.xAxis.minBucketWidthToShowWeekends;
|
||||
}
|
||||
if (_.has(panel, "xAxis.showCrosshair")) {
|
||||
if (_.has(panel, 'xAxis.showCrosshair')) {
|
||||
delete panel.xAxis.showCrosshair;
|
||||
}
|
||||
if (_.has(panel, "xAxis.showWeekends")) {
|
||||
if (_.has(panel, 'xAxis.showWeekends')) {
|
||||
delete panel.xAxis.showWeekends;
|
||||
}
|
||||
if (_.has(panel, "yAxis.showCrosshair")) {
|
||||
if (_.has(panel, 'yAxis.showCrosshair')) {
|
||||
delete panel.yAxis.showCrosshair;
|
||||
}
|
||||
if (_.has(panel, "data.unitFormat")) {
|
||||
if (_.has(panel, 'data.unitFormat')) {
|
||||
delete panel.data;
|
||||
}
|
||||
|
||||
// Migrate cardSpacing value. Seems rare (update from version 0.0.2).
|
||||
if (_.has(panel, "cards.cardSpacing")) {
|
||||
if (!_.has(panel, "cards.cardVSpacing")) {
|
||||
if (_.has(panel, 'cards.cardSpacing')) {
|
||||
if (!_.has(panel, 'cards.cardVSpacing')) {
|
||||
if (panel.cards.cardSpacing) {
|
||||
panel.cards.cardVSpacing = panel.cards.cardSpacing
|
||||
panel.cards.cardHSpacing = panel.cards.cardSpacing
|
||||
panel.cards.cardVSpacing = panel.cards.cardSpacing;
|
||||
panel.cards.cardHSpacing = panel.cards.cardSpacing;
|
||||
}
|
||||
}
|
||||
delete panel.cards.cardSpacing;
|
||||
@@ -34,22 +34,22 @@ function migrate_V0_V1(panel: any) {
|
||||
|
||||
// Migrate initial config for urls in tooltip (pull/86).
|
||||
// 'usingUrl' was used to show tooltip with urls on click or not.
|
||||
if (_.has(panel, "usingUrl")) {
|
||||
if (!_.has(panel, "tooltip.freezeOnClick")) {
|
||||
if (_.has(panel, 'usingUrl')) {
|
||||
if (!_.has(panel, 'tooltip.freezeOnClick')) {
|
||||
panel.tooltip.freezeOnClick = panel.usingUrl;
|
||||
}
|
||||
delete panel.usingUrl;
|
||||
}
|
||||
|
||||
// 'urls' array is now tooltip.items array. Also items are changed.
|
||||
if (_.has(panel, "urls")) {
|
||||
if (!_.has(panel, "tooltip.items")) {
|
||||
if (_.has(panel, 'urls')) {
|
||||
if (!_.has(panel, 'tooltip.items')) {
|
||||
panel.tooltip.items = [];
|
||||
let hasRealItems = true;
|
||||
if (panel.urls.length == 0) {
|
||||
if (panel.urls.length === 0) {
|
||||
hasRealItems = false;
|
||||
}
|
||||
if (panel.urls.length == 1) {
|
||||
if (panel.urls.length === 1) {
|
||||
let url = panel.urls[0];
|
||||
if (url.base_url === '' && url.label === '') {
|
||||
hasRealItems = false;
|
||||
@@ -64,19 +64,19 @@ function migrate_V0_V1(panel: any) {
|
||||
urlIcon: _.toString(url.icon_fa),
|
||||
urlToLowerCase: url.forcelowercase,
|
||||
valueDateFormat: '',
|
||||
}
|
||||
};
|
||||
// replace $vars with new ${__vars} if url template is not empty
|
||||
if (item.urlTemplate !== "") {
|
||||
if (item.urlTemplate !== '') {
|
||||
// $time was a graph time with prepended &
|
||||
item.urlTemplate = _.replace(url.base_url, /\$time/g, "&${__url_time_range}");
|
||||
item.urlTemplate = _.replace(url.base_url, /\$time/g, '&${__url_time_range}');
|
||||
// $series_label was a y axis label
|
||||
item.urlTemplate = _.replace(item.urlTemplate, /\$series_label/, "${__y_label}");
|
||||
item.urlTemplate = _.replace(item.urlTemplate, /\$series_label/, '${__y_label}');
|
||||
// $series_extra was a value from bucket. This value has format options and index.
|
||||
let valueVar = "__value";
|
||||
let valueVar = '__value';
|
||||
if (url.useExtraSeries === true) {
|
||||
// index?
|
||||
if (url.extraSeries.index > -1) {
|
||||
valueVar += "_" + url.extraSeries.index;
|
||||
valueVar += '_' + url.extraSeries.index;
|
||||
}
|
||||
|
||||
let format = _.toString(url.extraSeries.format);
|
||||
@@ -97,14 +97,13 @@ function migrate_V0_V1(panel: any) {
|
||||
|
||||
// create statusmap metadata
|
||||
panel.statusmap = {
|
||||
"ConfigVersion": "v1",
|
||||
}
|
||||
ConfigVersion: 'v1',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function migratePanelConfig(panel: any) {
|
||||
if (_.has(panel, "statusmap")) {
|
||||
if (panel.statusmap.ConfigVersion == "v1") {
|
||||
if (_.has(panel, 'statusmap')) {
|
||||
if (panel.statusmap.ConfigVersion === 'v1') {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -112,4 +111,3 @@ export function migratePanelConfig(panel: any) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<div class="section gf-form-group">
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Show tooltip"
|
||||
tooltip="'Show tooltip when mouse is over the card'"
|
||||
tooltip="Show tooltip when mouse is over the card"
|
||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Freeze on click"
|
||||
tooltip="'Freeze tooltip copy when card is clicked'"
|
||||
tooltip="Freeze tooltip copy when card is clicked"
|
||||
checked="ctrl.panel.tooltip.freezeOnClick" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Show items"
|
||||
tooltip="'Show items (urls) in tooltip'"
|
||||
tooltip="Show items (urls) in tooltip"
|
||||
checked="ctrl.panel.tooltip.showItems" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
@@ -63,6 +63,7 @@
|
||||
<label class="gf-form-label width-2">0</label>
|
||||
<label class="gf-form-input width-30">No items defined.</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div ng-repeat="item in ctrl.panel.tooltip.items">
|
||||
<div class="gf-form gf-form--grow">
|
||||
@@ -159,4 +160,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+287
-233
@@ -1,64 +1,68 @@
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {appEvents, contextSrv} from 'app/core/core';
|
||||
|
||||
/* eslint-disable id-blacklist, no-restricted-imports, @typescript-eslint/ban-types */
|
||||
// import moment from 'moment';
|
||||
import { toUtc } from '@grafana/data';
|
||||
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import { appEvents, contextSrv } from 'grafana/app/core/core';
|
||||
import * as d3 from 'd3';
|
||||
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
||||
import {StatusmapTooltip} from './tooltip';
|
||||
import {AnnotationTooltip} from './annotations';
|
||||
import { d3ScaleChromatic } from './d3/d3-scale-chromatic';
|
||||
import { StatusmapTooltip } from './tooltip';
|
||||
import { AnnotationTooltip } from './annotations';
|
||||
import { Bucket, BucketMatrix, BucketMatrixPager } from './statusmap_data';
|
||||
import { StatusHeatmapCtrl, renderComplete } from './module';
|
||||
import { CoreEvents, PanelEvents } from './libs/grafana/events/index';
|
||||
|
||||
let MIN_CARD_SIZE = 5,
|
||||
CARD_H_SPACING = 2,
|
||||
CARD_V_SPACING = 2,
|
||||
CARD_ROUND = 0,
|
||||
DATA_RANGE_WIDING_FACTOR = 1.2,
|
||||
DEFAULT_X_TICK_SIZE_PX = 100,
|
||||
DEFAULT_Y_TICK_SIZE_PX = 50,
|
||||
X_AXIS_TICK_PADDING = 10,
|
||||
Y_AXIS_TICK_PADDING = 5,
|
||||
MIN_SELECTION_WIDTH = 2;
|
||||
CARD_H_SPACING = 2,
|
||||
CARD_V_SPACING = 2,
|
||||
CARD_ROUND = 0,
|
||||
DATA_RANGE_WIDING_FACTOR = 1.2,
|
||||
DEFAULT_X_TICK_SIZE_PX = 100,
|
||||
// @ts-ignore
|
||||
DEFAULT_Y_TICK_SIZE_PX = 50,
|
||||
X_AXIS_TICK_PADDING = 10,
|
||||
Y_AXIS_TICK_PADDING = 5,
|
||||
MIN_SELECTION_WIDTH = 2;
|
||||
|
||||
export default function rendering(scope: any, elem: any, attrs: any, ctrl: any) {
|
||||
return new StatusmapRenderer(scope, elem, attrs, ctrl);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
class Statusmap {
|
||||
$svg: any;
|
||||
svg: any;
|
||||
bucketMatrix: BucketMatrix;
|
||||
|
||||
timeRange: {from: number, to: number} = {from:0, to:0};
|
||||
timeRange: { from: number; to: number } = { from: 0, to: 0 };
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export class StatusmapRenderer {
|
||||
width: number = 0;
|
||||
height: number = 0;
|
||||
width = 0;
|
||||
height = 0;
|
||||
yScale: any;
|
||||
xScale: any;
|
||||
chartWidth: number = 0;
|
||||
chartHeight: number = 0;
|
||||
chartTop: number = 0;
|
||||
chartBottom: number = 0;
|
||||
yAxisWidth: number = 0;
|
||||
xAxisHeight: number = 0;
|
||||
cardVSpacing: number = 0;
|
||||
cardHSpacing: number = 0;
|
||||
cardRound: number = 0;
|
||||
cardWidth: number = 0;
|
||||
cardHeight: number = 0;
|
||||
chartWidth = 0;
|
||||
chartHeight = 0;
|
||||
chartTop = 0;
|
||||
chartBottom = 0;
|
||||
yAxisWidth = 0;
|
||||
xAxisHeight = 0;
|
||||
cardVSpacing = 0;
|
||||
cardHSpacing = 0;
|
||||
cardRound = 0;
|
||||
cardWidth = 0;
|
||||
cardHeight = 0;
|
||||
colorScale: any;
|
||||
opacityScale: any;
|
||||
mouseUpHandler: any;
|
||||
xGridSize: number = 0;
|
||||
yGridSize: number = 0;
|
||||
xGridSize = 0;
|
||||
yGridSize = 0;
|
||||
|
||||
bucketMatrix: BucketMatrix;
|
||||
bucketMatrixPager: BucketMatrixPager;
|
||||
@@ -94,6 +98,7 @@ export class StatusmapRenderer {
|
||||
|
||||
this.ctrl.events.on(PanelEvents.render, this.onRender.bind(this));
|
||||
|
||||
// @ts-ignore
|
||||
this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
|
||||
|
||||
/////////////////////////////
|
||||
@@ -102,9 +107,9 @@ export class StatusmapRenderer {
|
||||
|
||||
// Shared crosshair and tooltip this.empty = true;
|
||||
|
||||
appEvents.on( CoreEvents.graphHover, this.onGraphHover.bind(this), this.scope);
|
||||
appEvents.on(CoreEvents.graphHover, this.onGraphHover.bind(this), this.scope);
|
||||
|
||||
appEvents.on( CoreEvents.graphHoverClear, this.onGraphHoverClear.bind(this), this.scope);
|
||||
appEvents.on(CoreEvents.graphHoverClear, this.onGraphHoverClear.bind(this), this.scope);
|
||||
|
||||
// Register selection listeners
|
||||
this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
|
||||
@@ -126,7 +131,6 @@ export class StatusmapRenderer {
|
||||
this.ctrl.renderingCompleted();
|
||||
}
|
||||
|
||||
|
||||
setElementHeight(): boolean {
|
||||
try {
|
||||
var height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
|
||||
@@ -146,26 +150,29 @@ export class StatusmapRenderer {
|
||||
this.$heatmap.css('height', height + 'px');
|
||||
|
||||
return true;
|
||||
} catch (e) { // IE throws errors sometimes
|
||||
} catch (e) {
|
||||
// IE throws errors sometimes
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getYAxisWidth(elem: any): number {
|
||||
const axisText = elem.selectAll(".axis-y text").nodes();
|
||||
const maxTextWidth = _.max(_.map(axisText, text => {
|
||||
// Use SVG getBBox method
|
||||
return text.getBBox().width;
|
||||
}));
|
||||
const axisText = elem.selectAll('.axis-y text').nodes();
|
||||
const maxTextWidth = _.max(
|
||||
_.map(axisText, text => {
|
||||
// Use SVG getBBox method
|
||||
return text.getBBox().width;
|
||||
})
|
||||
);
|
||||
|
||||
return Math.ceil(maxTextWidth);
|
||||
}
|
||||
|
||||
getXAxisHeight(elem: any): number {
|
||||
let axisLine = elem.select(".axis-x line");
|
||||
let axisLine = elem.select('.axis-x line');
|
||||
if (!axisLine.empty()) {
|
||||
let axisLinePosition = parseFloat(elem.select(".axis-x line").attr("y2"));
|
||||
let canvasWidth = parseFloat(elem.attr("height"));
|
||||
let axisLinePosition = parseFloat(elem.select('.axis-x line').attr('y2'));
|
||||
let canvasWidth = parseFloat(elem.attr('height'));
|
||||
return canvasWidth - axisLinePosition;
|
||||
} else {
|
||||
// Default height
|
||||
@@ -179,9 +186,10 @@ export class StatusmapRenderer {
|
||||
// .domain([this.timeRange.from, this.timeRange.to])
|
||||
// .range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
|
||||
// Buckets without the most recent
|
||||
this.scope.xScale = this.xScale = d3.scaleTime()
|
||||
.domain([this.timeRange.from, this.timeRange.to])
|
||||
.range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
|
||||
this.scope.xScale = this.xScale = d3
|
||||
.scaleTime()
|
||||
.domain([this.timeRange.from, this.timeRange.to])
|
||||
.range([this.xGridSize / 2, this.chartWidth - this.xGridSize / 2]);
|
||||
|
||||
let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
|
||||
let grafanaTimeFormatter = grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
|
||||
@@ -194,87 +202,97 @@ export class StatusmapRenderer {
|
||||
}
|
||||
|
||||
let xAxis = d3
|
||||
.axisBottom(this.xScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(timeFormat)
|
||||
.tickPadding(X_AXIS_TICK_PADDING)
|
||||
.tickSize(this.chartHeight);
|
||||
.axisBottom(this.xScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(timeFormat)
|
||||
.tickPadding(X_AXIS_TICK_PADDING)
|
||||
.tickSize(this.chartHeight);
|
||||
|
||||
let posY = this.chartTop; // this.margin.top !
|
||||
let posX = this.yAxisWidth;
|
||||
|
||||
this.heatmap.append("g")
|
||||
.attr("class", "axis axis-x")
|
||||
.attr("transform", "translate(" + posX + "," + posY + ")")
|
||||
.call(xAxis);
|
||||
this.heatmap
|
||||
.append('g')
|
||||
.attr('class', 'axis axis-x')
|
||||
.attr('transform', 'translate(' + posX + ',' + posY + ')')
|
||||
.call(xAxis);
|
||||
|
||||
// Remove horizontal line in the top of axis labels (called domain in d3)
|
||||
this.heatmap
|
||||
.select(".axis-x")
|
||||
.select(".domain")
|
||||
.remove();
|
||||
.select('.axis-x')
|
||||
.select('.domain')
|
||||
.remove();
|
||||
}
|
||||
|
||||
// divide chart height by ticks for cards drawing
|
||||
getYScale(ticks: any[]) {
|
||||
let range:any[] = [];
|
||||
let range: any[] = [];
|
||||
let step = this.chartHeight / ticks.length;
|
||||
// svg has y=0 on the top, so top card should have a minimal value in range
|
||||
range.push(step);
|
||||
for (let i = 1; i < ticks.length; i++) {
|
||||
range.push(step * (i+1));
|
||||
range.push(step * (i + 1));
|
||||
}
|
||||
return d3.scaleOrdinal()
|
||||
.domain(ticks)
|
||||
.range(range);
|
||||
return d3
|
||||
.scaleOrdinal()
|
||||
.domain(ticks)
|
||||
.range(range);
|
||||
}
|
||||
|
||||
// divide chart height by ticks with offset for ticks drawing
|
||||
getYAxisScale(ticks: any[]) {
|
||||
let range:any[] = [];
|
||||
let range: any[] = [];
|
||||
let step = this.chartHeight / ticks.length;
|
||||
// svg has y=0 on the top, so top tick should have a minimal value in range
|
||||
range.push(this.yOffset);
|
||||
for (let i = 1; i < ticks.length; i++) {
|
||||
range.push(step * i + this.yOffset);
|
||||
}
|
||||
return d3.scaleOrdinal()
|
||||
.domain(ticks)
|
||||
.range(range);
|
||||
return d3
|
||||
.scaleOrdinal()
|
||||
.domain(ticks)
|
||||
.range(range);
|
||||
}
|
||||
|
||||
addYAxis() {
|
||||
let ticks = this.bucketMatrixPager.targets();
|
||||
|
||||
// TODO move sorting into bucketMatrixPager.
|
||||
if (this.panel.yAxisSort == 'a → z') {
|
||||
ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
|
||||
} else if (this.panel.yAxisSort == 'z → a') {
|
||||
ticks.sort((b, a) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
|
||||
if (this.panel.yAxisSort === 'a → z') {
|
||||
ticks.sort((a, b) => a.localeCompare(b, 'en', { ignorePunctuation: false, numeric: true }));
|
||||
} else if (this.panel.yAxisSort === 'z → a') {
|
||||
ticks.sort((b, a) => a.localeCompare(b, 'en', { ignorePunctuation: false, numeric: true }));
|
||||
}
|
||||
|
||||
let yAxisScale = this.getYAxisScale(ticks);
|
||||
this.scope.yScale = this.yScale = this.getYScale(ticks);
|
||||
|
||||
let yAxis = d3
|
||||
.axisLeft(yAxisScale)
|
||||
.tickValues(ticks)
|
||||
.tickSizeInner(0 - this.width)
|
||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||
// @ts-ignore
|
||||
.axisLeft(yAxisScale)
|
||||
.tickValues(ticks)
|
||||
.tickSizeInner(0 - this.width)
|
||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||
|
||||
this.heatmap
|
||||
.append("g")
|
||||
.attr("class", "axis axis-y")
|
||||
.call(yAxis);
|
||||
.append('g')
|
||||
.attr('class', 'axis axis-y')
|
||||
.call(yAxis);
|
||||
|
||||
// Calculate Y axis width first, then move axis into visible area
|
||||
let posY = this.margin.top;
|
||||
let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
|
||||
this.heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")");
|
||||
this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
|
||||
|
||||
// Remove vertical line in the right of axis labels (called domain in d3)
|
||||
this.heatmap.select(".axis-y").select(".domain").remove();
|
||||
this.heatmap.select(".axis-y").selectAll(".tick line").remove();
|
||||
this.heatmap
|
||||
.select('.axis-y')
|
||||
.select('.domain')
|
||||
.remove();
|
||||
this.heatmap
|
||||
.select('.axis-y')
|
||||
.selectAll('.tick line')
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Wide Y values range and adjust to bucket size
|
||||
@@ -296,7 +314,7 @@ export class StatusmapRenderer {
|
||||
y_min = 0;
|
||||
}
|
||||
|
||||
return {y_min, y_max};
|
||||
return { y_min, y_max };
|
||||
}
|
||||
|
||||
tickValueFormatter(decimals, scaledDecimals = null) {
|
||||
@@ -320,10 +338,11 @@ export class StatusmapRenderer {
|
||||
|
||||
// Insert svg as a first child into heatmap_elem
|
||||
// as the frozen tooltip moving logiс assumes that tooltip is the last child.
|
||||
this.heatmap = d3.select(heatmap_elem)
|
||||
.insert("svg",":first-child")
|
||||
.attr("width", this.width)
|
||||
.attr("height", this.height);
|
||||
this.heatmap = d3
|
||||
.select(heatmap_elem)
|
||||
.insert('svg', ':first-child')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height);
|
||||
|
||||
this.chartHeight = this.height - this.margin.top - this.margin.bottom;
|
||||
this.chartTop = this.margin.top;
|
||||
@@ -348,22 +367,28 @@ export class StatusmapRenderer {
|
||||
|
||||
// TODO allow per-y cardWidth!
|
||||
// we need to fill chartWidth with xBucketSize cards.
|
||||
this.xGridSize = this.chartWidth / (this.bucketMatrix.xBucketSize+1);
|
||||
this.xGridSize = this.chartWidth / (this.bucketMatrix.xBucketSize + 1);
|
||||
this.cardWidth = this.xGridSize - this.cardHSpacing;
|
||||
|
||||
this.addXAxis();
|
||||
this.xAxisHeight = this.getXAxisHeight(this.heatmap);
|
||||
|
||||
if (!this.panel.yAxis.show) {
|
||||
this.heatmap.select(".axis-y").selectAll("line").style("opacity", 0);
|
||||
this.heatmap
|
||||
.select('.axis-y')
|
||||
.selectAll('line')
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
if (!this.panel.xAxis.show) {
|
||||
this.heatmap.select(".axis-x").selectAll("line").style("opacity", 0);
|
||||
this.heatmap
|
||||
.select('.axis-x')
|
||||
.selectAll('line')
|
||||
.style('opacity', 0);
|
||||
}
|
||||
}
|
||||
|
||||
addStatusmap():void {
|
||||
addStatusmap(): void {
|
||||
let maxValue = this.panel.color.max != null ? this.panel.color.max : this.bucketMatrix.maxValue;
|
||||
let minValue = this.panel.color.min != null ? this.panel.color.min : this.bucketMatrix.minValue;
|
||||
|
||||
@@ -373,37 +398,39 @@ export class StatusmapRenderer {
|
||||
this.setOpacityScale(maxValue);
|
||||
|
||||
// Draw cards from buckets.
|
||||
this.heatmap.selectAll(".statusmap-cards-row").data(this.bucketMatrixPager.targets())
|
||||
this.heatmap
|
||||
.selectAll('.statusmap-cards-row')
|
||||
.data(this.bucketMatrixPager.targets())
|
||||
.enter()
|
||||
.selectAll(".statustmap-card")
|
||||
.data((target:string) => this.bucketMatrix.buckets[target])
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("cardId", (b:Bucket) => b.id)
|
||||
.attr("xid", (b:Bucket) => b.xid)
|
||||
.attr("yid", (b:Bucket) => b.yLabel)
|
||||
.attr("x", this.getCardX.bind(this))
|
||||
.attr("width", this.getCardWidth.bind(this))
|
||||
.attr("y", this.getCardY.bind(this))
|
||||
.attr("height", this.getCardHeight.bind(this))
|
||||
.attr("rx", this.cardRound)
|
||||
.attr("ry", this.cardRound)
|
||||
.attr("class", (b:Bucket) => b.isEmpty() ? "empty-card" : "bordered statusmap-card")
|
||||
.style("fill", this.getCardColor.bind(this))
|
||||
.style("stroke", this.getCardColor.bind(this))
|
||||
.style("stroke-width", 0)
|
||||
//.style("stroke-width", getCardStrokeWidth)
|
||||
//.style("stroke-dasharray", "3,3")
|
||||
.style("opacity", this.getCardOpacity.bind(this));
|
||||
.selectAll('.statustmap-card')
|
||||
.data((target: string) => this.bucketMatrix.buckets[target])
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('cardId', (b: Bucket) => b.id)
|
||||
.attr('xid', (b: Bucket) => b.xid)
|
||||
.attr('yid', (b: Bucket) => b.yLabel)
|
||||
.attr('x', this.getCardX.bind(this))
|
||||
.attr('width', this.getCardWidth.bind(this))
|
||||
.attr('y', this.getCardY.bind(this))
|
||||
.attr('height', this.getCardHeight.bind(this))
|
||||
.attr('rx', this.cardRound)
|
||||
.attr('ry', this.cardRound)
|
||||
.attr('class', (b: Bucket) => (b.isEmpty() ? 'empty-card' : 'bordered statusmap-card'))
|
||||
.style('fill', this.getCardColor.bind(this))
|
||||
.style('stroke', this.getCardColor.bind(this))
|
||||
.style('stroke-width', 0)
|
||||
//.style('stroke-width', getCardStrokeWidth)
|
||||
//.style('stroke-dasharray', '3,3')
|
||||
.style('opacity', this.getCardOpacity.bind(this));
|
||||
|
||||
// Set mouse events on cards.
|
||||
let $cards = this.$heatmap.find(".statusmap-card + .bordered");
|
||||
let $cards = this.$heatmap.find('.statusmap-card + .bordered');
|
||||
$cards
|
||||
.on("mouseenter", (event) => {
|
||||
.on('mouseenter', event => {
|
||||
this.tooltip.mouseOverBucket = true;
|
||||
this.highlightCard(event);
|
||||
})
|
||||
.on("mouseleave", (event) => {
|
||||
.on('mouseleave', event => {
|
||||
this.tooltip.mouseOverBucket = false;
|
||||
this.resetCardHighLight(event);
|
||||
});
|
||||
@@ -411,37 +438,40 @@ export class StatusmapRenderer {
|
||||
this._renderAnnotations();
|
||||
|
||||
this.ctrl.events.emit(renderComplete, {
|
||||
"chartWidth": this.chartWidth
|
||||
chartWidth: this.chartWidth,
|
||||
});
|
||||
}
|
||||
|
||||
highlightCard(event) {
|
||||
const color = d3.select(event.target).style("fill");
|
||||
const color = d3.select(event.target).style('fill');
|
||||
const highlightColor = d3.color(color).darker(2);
|
||||
const strokeColor = d3.color(color).brighter(4);
|
||||
const current_card = d3.select(event.target);
|
||||
this.tooltip.originalFillColor = color;
|
||||
current_card
|
||||
.style("fill", highlightColor.toString())
|
||||
.style("stroke", strokeColor.toString())
|
||||
.style("stroke-width", 1);
|
||||
.style('fill', highlightColor.toString())
|
||||
.style('stroke', strokeColor.toString())
|
||||
.style('stroke-width', 1);
|
||||
}
|
||||
|
||||
resetCardHighLight(event) {
|
||||
d3.select(event.target)
|
||||
.style("fill", this.tooltip.originalFillColor)
|
||||
.style("stroke", this.tooltip.originalFillColor)
|
||||
.style("stroke-width", 0);
|
||||
.style('fill', this.tooltip.originalFillColor)
|
||||
.style('stroke', this.tooltip.originalFillColor)
|
||||
.style('stroke-width', 0);
|
||||
}
|
||||
|
||||
getColorScale(maxValue, minValue = 0) {
|
||||
let colorScheme = _.find(this.ctrl.colorSchemes, {value: this.panel.color.colorScheme});
|
||||
let colorScheme = _.find(this.ctrl.colorSchemes, { value: this.panel.color.colorScheme });
|
||||
// @ts-ignore
|
||||
let colorInterpolator = d3ScaleChromatic[colorScheme.value];
|
||||
let colorScaleInverted = colorScheme.invert === 'always' ||
|
||||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
||||
let colorScaleInverted =
|
||||
// @ts-ignore
|
||||
colorScheme.invert === 'always' || (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
||||
|
||||
if (maxValue == minValue)
|
||||
if (maxValue === minValue) {
|
||||
maxValue = minValue + 1;
|
||||
}
|
||||
|
||||
let start = colorScaleInverted ? maxValue : minValue;
|
||||
let end = colorScaleInverted ? minValue : maxValue;
|
||||
@@ -451,13 +481,16 @@ export class StatusmapRenderer {
|
||||
|
||||
setOpacityScale(maxValue) {
|
||||
if (this.panel.color.colorScale === 'linear') {
|
||||
this.opacityScale = d3.scaleLinear()
|
||||
.domain([0, maxValue])
|
||||
.range([0, 1]);
|
||||
this.opacityScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, maxValue])
|
||||
.range([0, 1]);
|
||||
} else if (this.panel.color.colorScale === 'sqrt') {
|
||||
this.opacityScale = d3.scalePow().exponent(this.panel.color.exponent)
|
||||
.domain([0, maxValue])
|
||||
.range([0, 1]);
|
||||
this.opacityScale = d3
|
||||
.scalePow()
|
||||
.exponent(this.panel.color.exponent)
|
||||
.domain([0, maxValue])
|
||||
.range([0, 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,12 +499,12 @@ export class StatusmapRenderer {
|
||||
// cx is the center of the card. Card should be placed to the left.
|
||||
//let cx = this.xScale(d.x);
|
||||
let rightX = (b.relTo / this.bucketMatrix.rangeMs) * this.chartWidth;
|
||||
let cx = rightX - this.cardWidth/2;
|
||||
let cx = rightX - this.cardWidth / 2;
|
||||
|
||||
if (cx - this.cardWidth/2 < 0) {
|
||||
x = this.yAxisWidth + this.cardHSpacing/2;
|
||||
if (cx - this.cardWidth / 2 < 0) {
|
||||
x = this.yAxisWidth + this.cardHSpacing / 2;
|
||||
} else {
|
||||
x = this.yAxisWidth + cx - this.cardWidth/2;
|
||||
x = this.yAxisWidth + cx - this.cardWidth / 2;
|
||||
}
|
||||
|
||||
return x;
|
||||
@@ -483,17 +516,17 @@ export class StatusmapRenderer {
|
||||
let w;
|
||||
|
||||
let rightX = (b.relTo / this.bucketMatrix.rangeMs) * this.chartWidth;
|
||||
let cx = rightX - this.cardWidth/2;
|
||||
let cx = rightX - this.cardWidth / 2;
|
||||
//let cx = this.xScale(d.x);
|
||||
|
||||
if (cx < this.cardWidth/2) {
|
||||
if (cx < this.cardWidth / 2) {
|
||||
// Center should not exceed half of card.
|
||||
// Cut card to the left to prevent overlay of y axis.
|
||||
let cutted_width = (cx - this.cardHSpacing/2) + this.cardWidth/2;
|
||||
let cutted_width = cx - this.cardHSpacing / 2 + this.cardWidth / 2;
|
||||
w = cutted_width > 0 ? cutted_width : 0;
|
||||
} else if (this.chartWidth - cx < this.cardWidth/2) {
|
||||
} else if (this.chartWidth - cx < this.cardWidth / 2) {
|
||||
// Cut card to the right to prevent overlay of right graph edge.
|
||||
w = this.cardWidth/2 + (this.chartWidth - cx - this.cardHSpacing/2);
|
||||
w = this.cardWidth / 2 + (this.chartWidth - cx - this.cardHSpacing / 2);
|
||||
} else {
|
||||
w = this.cardWidth;
|
||||
}
|
||||
@@ -501,8 +534,8 @@ export class StatusmapRenderer {
|
||||
// Card width should be MIN_CARD_SIZE at least
|
||||
w = Math.max(w, MIN_CARD_SIZE);
|
||||
|
||||
if (this.cardHSpacing == 0) {
|
||||
w = w+1;
|
||||
if (this.cardHSpacing === 0) {
|
||||
w = w + 1;
|
||||
}
|
||||
|
||||
return w;
|
||||
@@ -511,18 +544,18 @@ export class StatusmapRenderer {
|
||||
// Top y for card.
|
||||
// yScale gives ???
|
||||
getCardY(b: Bucket) {
|
||||
return this.yScale(b.yLabel) + this.chartTop - this.cardHeight - this.cardVSpacing/2;
|
||||
return this.yScale(b.yLabel) + this.chartTop - this.cardHeight - this.cardVSpacing / 2;
|
||||
}
|
||||
|
||||
getCardHeight(b: Bucket) {
|
||||
//return 20;
|
||||
let ys = this.yScale(b.yLabel);
|
||||
let y = ys + this.chartTop - this.cardHeight - this.cardVSpacing/2;
|
||||
let y = ys + this.chartTop - this.cardHeight - this.cardVSpacing / 2;
|
||||
let h = this.cardHeight;
|
||||
|
||||
// Cut card height to prevent overlay
|
||||
if (y < this.chartTop) {
|
||||
h = ys - this.cardVSpacing/2;
|
||||
h = ys - this.cardVSpacing / 2;
|
||||
} else if (ys > this.chartBottom) {
|
||||
h = this.chartBottom - y;
|
||||
} else if (y + this.cardHeight > this.chartBottom) {
|
||||
@@ -534,8 +567,8 @@ export class StatusmapRenderer {
|
||||
// Card height should be MIN_CARD_SIZE at least
|
||||
h = Math.max(h, MIN_CARD_SIZE);
|
||||
|
||||
if (this.cardVSpacing == 0) {
|
||||
h = h+1
|
||||
if (this.cardVSpacing === 0) {
|
||||
h = h + 1;
|
||||
}
|
||||
|
||||
return h;
|
||||
@@ -547,7 +580,7 @@ export class StatusmapRenderer {
|
||||
} else if (this.panel.color.mode === 'spectrum') {
|
||||
return this.colorScale(bucket.value);
|
||||
} else if (this.panel.color.mode === 'discrete') {
|
||||
if (this.panel.seriesFilterIndex != null && this.panel.seriesFilterIndex != -1) {
|
||||
if (this.panel.seriesFilterIndex !== null && this.panel.seriesFilterIndex !== -1) {
|
||||
return this.ctrl.discreteExtraSeries.getBucketColorSingle(bucket.values[this.panel.seriesFilterIndex]);
|
||||
} else {
|
||||
return this.ctrl.discreteExtraSeries.getBucketColor(bucket.values);
|
||||
@@ -556,7 +589,7 @@ export class StatusmapRenderer {
|
||||
}
|
||||
|
||||
getCardOpacity(bucket: Bucket) {
|
||||
if (this.panel.nullPointMode === 'as empty' && bucket.value == null ) {
|
||||
if (this.panel.nullPointMode === 'as empty' && bucket.value == null) {
|
||||
return 0;
|
||||
}
|
||||
if (this.panel.color.mode === 'opacity') {
|
||||
@@ -573,7 +606,6 @@ export class StatusmapRenderer {
|
||||
return '0';
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
// Selection and crosshair //
|
||||
/////////////////////////////
|
||||
@@ -594,22 +626,26 @@ export class StatusmapRenderer {
|
||||
this.onMouseUp();
|
||||
};
|
||||
|
||||
$(document).one("mouseup", this.mouseUpHandler.bind(this));
|
||||
$(document).one('mouseup', this.mouseUpHandler.bind(this));
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
$(document).unbind("mouseup", this.mouseUpHandler.bind(this));
|
||||
$(document).unbind('mouseup', this.mouseUpHandler.bind(this));
|
||||
this.mouseUpHandler = null;
|
||||
this.selection.active = false;
|
||||
|
||||
let selectionRange = Math.abs(this.selection.x2 - this.selection.x1);
|
||||
if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
|
||||
let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth - this.xGridSize/2);
|
||||
let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth - this.xGridSize/2);
|
||||
let timeFrom = this.xScale.invert(
|
||||
Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth - this.xGridSize / 2
|
||||
);
|
||||
let timeTo = this.xScale.invert(
|
||||
Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth - this.xGridSize / 2
|
||||
);
|
||||
|
||||
this.ctrl.timeSrv.setTime({
|
||||
from: moment.utc(timeFrom),
|
||||
to: moment.utc(timeTo)
|
||||
from: toUtc(timeFrom),
|
||||
to: toUtc(timeTo),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -623,7 +659,9 @@ export class StatusmapRenderer {
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if (!this.heatmap) { return; }
|
||||
if (!this.heatmap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = this.getEventOffset(event);
|
||||
if (this.selection.active) {
|
||||
@@ -644,7 +682,7 @@ export class StatusmapRenderer {
|
||||
}
|
||||
|
||||
// TODO emit an event and move logic to panelCtrl
|
||||
public onMouseClick(e: MouseEvent) {
|
||||
onMouseClick(e: MouseEvent) {
|
||||
if (this.ctrl.panel.tooltip.freezeOnClick) {
|
||||
this.tooltip.showFrozen(e);
|
||||
this.tooltip.destroy();
|
||||
@@ -669,21 +707,23 @@ export class StatusmapRenderer {
|
||||
}
|
||||
|
||||
emitGraphHoverEvent(event) {
|
||||
let x = this.xScale.invert(event.offsetX - this.yAxisWidth - this.xGridSize/2).valueOf();
|
||||
let x = this.xScale.invert(event.offsetX - this.yAxisWidth - this.xGridSize / 2).valueOf();
|
||||
let y = this.yScale(event.offsetY);
|
||||
let pos = {
|
||||
pageX: event.pageX,
|
||||
pageY: event.pageY,
|
||||
x: x, x1: x,
|
||||
y: y, y1: y,
|
||||
panelRelY: 0
|
||||
x: x,
|
||||
x1: x,
|
||||
y: y,
|
||||
y1: y,
|
||||
panelRelY: 0,
|
||||
};
|
||||
|
||||
// Set minimum offset to prevent showing legend from another panel
|
||||
pos.panelRelY = Math.max(event.offsetY / this.height, 0.001);
|
||||
|
||||
// broadcast to other graph panels that we are hovering
|
||||
appEvents.emit(CoreEvents.graphHover, {pos: pos, panel: this.panel});
|
||||
appEvents.emit(CoreEvents.graphHover, { pos: pos, panel: this.panel });
|
||||
}
|
||||
|
||||
limitSelection(x2) {
|
||||
@@ -694,17 +734,18 @@ export class StatusmapRenderer {
|
||||
|
||||
drawSelection(posX1, posX2) {
|
||||
if (this.heatmap) {
|
||||
this.heatmap.selectAll(".status-heatmap-selection").remove();
|
||||
this.heatmap.selectAll('.status-heatmap-selection').remove();
|
||||
let selectionX = Math.min(posX1, posX2);
|
||||
let selectionWidth = Math.abs(posX1 - posX2);
|
||||
|
||||
if (selectionWidth > MIN_SELECTION_WIDTH) {
|
||||
this.heatmap.append("rect")
|
||||
.attr("class", "status-heatmap-selection")
|
||||
.attr("x", selectionX)
|
||||
.attr("width", selectionWidth)
|
||||
.attr("y", this.chartTop)
|
||||
.attr("height", this.chartHeight);
|
||||
this.heatmap
|
||||
.append('rect')
|
||||
.attr('class', 'status-heatmap-selection')
|
||||
.attr('x', selectionX)
|
||||
.attr('width', selectionWidth)
|
||||
.attr('y', this.chartTop)
|
||||
.attr('height', this.chartHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -714,28 +755,28 @@ export class StatusmapRenderer {
|
||||
this.selection.x2 = -1;
|
||||
|
||||
if (this.heatmap) {
|
||||
this.heatmap.selectAll(".status-heatmap-selection").remove();
|
||||
this.heatmap.selectAll('.status-heatmap-selection').remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
drawCrosshair(position) {
|
||||
if (this.heatmap) {
|
||||
this.heatmap.selectAll(".status-heatmap-crosshair").remove();
|
||||
this.heatmap.selectAll('.status-heatmap-crosshair').remove();
|
||||
|
||||
let posX = position;
|
||||
posX = Math.max(posX, this.yAxisWidth);
|
||||
posX = Math.min(posX, this.chartWidth + this.yAxisWidth);
|
||||
|
||||
this.heatmap.append("g")
|
||||
.attr("class", "status-heatmap-crosshair")
|
||||
.attr("transform", "translate(" + posX + ",0)")
|
||||
.append("line")
|
||||
.attr("x1", 1)
|
||||
.attr("y1", this.chartTop)
|
||||
.attr("x2", 1)
|
||||
.attr("y2", this.chartBottom)
|
||||
.attr("stroke-width", 1);
|
||||
this.heatmap
|
||||
.append('g')
|
||||
.attr('class', 'status-heatmap-crosshair')
|
||||
.attr('transform', 'translate(' + posX + ',0)')
|
||||
.append('line')
|
||||
.attr('x1', 1)
|
||||
.attr('y1', this.chartTop)
|
||||
.attr('x2', 1)
|
||||
.attr('y2', this.chartBottom)
|
||||
.attr('stroke-width', 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,7 +790,7 @@ export class StatusmapRenderer {
|
||||
|
||||
clearCrosshair() {
|
||||
if (this.heatmap) {
|
||||
this.heatmap.selectAll(".status-heatmap-crosshair").remove();
|
||||
this.heatmap.selectAll('.status-heatmap-crosshair').remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,7 +819,7 @@ export class StatusmapRenderer {
|
||||
}
|
||||
|
||||
_renderAnnotations() {
|
||||
if (!this.ctrl.annotations || this.ctrl.annotations.length == 0) {
|
||||
if (!this.ctrl.annotations || this.ctrl.annotations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -786,52 +827,65 @@ export class StatusmapRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let annoData = _.map(this.ctrl.annotations, (d,i) => ({"x": Math.floor(this.yAxisWidth + this.xScale(d.time)), "id":i, "anno": d.source}));
|
||||
let annoData = _.map(this.ctrl.annotations, (d, i) => ({
|
||||
// @ts-ignore
|
||||
x: Math.floor(this.yAxisWidth + this.xScale(d.time)),
|
||||
id: i,
|
||||
// @ts-ignore
|
||||
anno: d.source,
|
||||
}));
|
||||
|
||||
//({"ctrl_annotations": this.ctrl.annotations, "annoData": annoData});
|
||||
|
||||
let anno = this.heatmap
|
||||
.append("g")
|
||||
.attr("class", "statusmap-annotations")
|
||||
.attr("transform", "translate(0.5,0)")
|
||||
.selectAll(".statusmap-annotations")
|
||||
.data(annoData)
|
||||
.enter().append("g")
|
||||
;
|
||||
anno.append("line")
|
||||
//.attr("class", "statusmap-annotation-tick")
|
||||
.attr("x1", d => d.x)
|
||||
.attr("y1", this.chartTop)
|
||||
.attr("x2", d => d.x)
|
||||
.attr("y2", this.chartBottom)
|
||||
.style("stroke", d => d.anno.iconColor)
|
||||
.style("stroke-width", 1)
|
||||
.style("stroke-dasharray", "3,3")
|
||||
;
|
||||
anno.append("polygon")
|
||||
.attr("points", d => [[d.x, this.chartBottom+1], [d.x-5, this.chartBottom+6], [d.x+5, this.chartBottom+6]].join(" "))
|
||||
.style("stroke-width", 0)
|
||||
.style("fill", d => d.anno.iconColor)
|
||||
;
|
||||
// Polygons didn't fire mouseevents
|
||||
anno.append("rect")
|
||||
.attr("x", d => d.x-5)
|
||||
.attr("width", 10)
|
||||
.attr("y", this.chartBottom+1)
|
||||
.attr("height", 5)
|
||||
.attr("class", "statusmap-annotation-tick")
|
||||
.attr("annoId", d => d.id)
|
||||
.style("opacity", 0)
|
||||
;
|
||||
.append('g')
|
||||
.attr('class', 'statusmap-annotations')
|
||||
.attr('transform', 'translate(0.5,0)')
|
||||
.selectAll('.statusmap-annotations')
|
||||
.data(annoData)
|
||||
.enter()
|
||||
.append('g');
|
||||
|
||||
let $ticks = this.$heatmap.find(".statusmap-annotation-tick");
|
||||
anno
|
||||
.append('line')
|
||||
//.attr("class", "statusmap-annotation-tick")
|
||||
.attr('x1', d => d.x)
|
||||
.attr('y1', this.chartTop)
|
||||
.attr('x2', d => d.x)
|
||||
.attr('y2', this.chartBottom)
|
||||
.style('stroke', d => d.anno.iconColor)
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '3,3');
|
||||
|
||||
anno
|
||||
.append('polygon')
|
||||
.attr('points', d =>
|
||||
[
|
||||
[d.x, this.chartBottom + 1],
|
||||
[d.x - 5, this.chartBottom + 6],
|
||||
[d.x + 5, this.chartBottom + 6],
|
||||
].join(' ')
|
||||
)
|
||||
.style('stroke-width', 0)
|
||||
.style('fill', d => d.anno.iconColor);
|
||||
|
||||
// Polygons didn't fire mouseevents
|
||||
anno
|
||||
.append('rect')
|
||||
.attr('x', d => d.x - 5)
|
||||
.attr('width', 10)
|
||||
.attr('y', this.chartBottom + 1)
|
||||
.attr('height', 5)
|
||||
.attr('class', 'statusmap-annotation-tick')
|
||||
.attr('annoId', d => d.id)
|
||||
.style('opacity', 0);
|
||||
|
||||
let $ticks = this.$heatmap.find('.statusmap-annotation-tick');
|
||||
$ticks
|
||||
.on("mouseenter", (event) => {
|
||||
.on('mouseenter', event => {
|
||||
this.annotationTooltip.mouseOverAnnotationTick = true;
|
||||
})
|
||||
.on("mouseleave", (event) => {
|
||||
.on('mouseleave', event => {
|
||||
this.annotationTooltip.mouseOverAnnotationTick = false;
|
||||
});
|
||||
}
|
||||
@@ -840,24 +894,24 @@ export class StatusmapRenderer {
|
||||
function grafanaTimeFormat(ticks, min, max) {
|
||||
if (min && max && ticks) {
|
||||
let range = max - min;
|
||||
let secPerTick = (range/ticks) / 1000;
|
||||
let secPerTick = range / ticks / 1000;
|
||||
let oneDay = 86400000;
|
||||
let oneYear = 31536000000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return "%H:%M:%S";
|
||||
return '%H:%M:%S';
|
||||
}
|
||||
if (secPerTick <= 7200 || range <= oneDay) {
|
||||
return "%H:%M";
|
||||
return '%H:%M';
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return "%m/%d %H:%M";
|
||||
return '%m/%d %H:%M';
|
||||
}
|
||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||
return "%m/%d";
|
||||
return '%m/%d';
|
||||
}
|
||||
return "%Y-%m";
|
||||
return '%Y-%m';
|
||||
}
|
||||
|
||||
return "%H:%M";
|
||||
return '%H:%M';
|
||||
}
|
||||
|
||||
@@ -5,91 +5,91 @@ describe('when migrate from config with urls', () => {
|
||||
|
||||
describe('given full panel configuration', () => {
|
||||
const panelConfig = {
|
||||
"cards": {
|
||||
"cardHSpacing": 2,
|
||||
"cardMinWidth": 5,
|
||||
"cardRound": null,
|
||||
"cardVSpacing": 2
|
||||
cards: {
|
||||
cardHSpacing: 2,
|
||||
cardMinWidth: 5,
|
||||
cardRound: null,
|
||||
cardVSpacing: 2,
|
||||
},
|
||||
"color": {
|
||||
"cardColor": "#b4ff00",
|
||||
"colorScale": "sqrt",
|
||||
"colorScheme": "interpolateGnYlRd",
|
||||
"defaultColor": "#757575",
|
||||
"exponent": 0.5,
|
||||
"mode": "opacity",
|
||||
"thresholds": []
|
||||
color: {
|
||||
cardColor: '#b4ff00',
|
||||
colorScale: 'sqrt',
|
||||
colorScheme: 'interpolateGnYlRd',
|
||||
defaultColor: '#757575',
|
||||
exponent: 0.5,
|
||||
mode: 'opacity',
|
||||
thresholds: [],
|
||||
},
|
||||
"data": {
|
||||
"decimals": null,
|
||||
"unitFormat": "short"
|
||||
data: {
|
||||
decimals: null,
|
||||
unitFormat: 'short',
|
||||
},
|
||||
"datasource": "TestData DB",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
datasource: 'TestData DB',
|
||||
gridPos: {
|
||||
h: 8,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
"highlightCards": true,
|
||||
"id": 4,
|
||||
"legend": {
|
||||
"show": false
|
||||
highlightCards: true,
|
||||
id: 4,
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
"nullPointMode": "as empty",
|
||||
"seriesFilterIndex": -1,
|
||||
"targets": [
|
||||
nullPointMode: 'as empty',
|
||||
seriesFilterIndex: -1,
|
||||
targets: [
|
||||
{
|
||||
"aggregation": "Last",
|
||||
"csvWave": {
|
||||
"timeStep": 60,
|
||||
"valuesCSV": "0,0,2,2,1,1,3,3"
|
||||
aggregation: 'Last',
|
||||
csvWave: {
|
||||
timeStep: 60,
|
||||
valuesCSV: '0,0,2,2,1,1,3,3',
|
||||
},
|
||||
"decimals": 2,
|
||||
"displayAliasType": "Warning / Critical",
|
||||
"displayType": "Regular",
|
||||
"displayValueWithAlias": "Never",
|
||||
"refId": "A",
|
||||
"scenarioId": "predictable_csv_wave",
|
||||
"stringInput": "",
|
||||
"units": "none",
|
||||
"valueHandler": "Number Threshold"
|
||||
}
|
||||
decimals: 2,
|
||||
displayAliasType: 'Warning / Critical',
|
||||
displayType: 'Regular',
|
||||
displayValueWithAlias: 'Never',
|
||||
refId: 'A',
|
||||
scenarioId: 'predictable_csv_wave',
|
||||
stringInput: '',
|
||||
units: 'none',
|
||||
valueHandler: 'Number Threshold',
|
||||
},
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"show": true
|
||||
timeFrom: null,
|
||||
timeShift: null,
|
||||
title: 'Panel Title',
|
||||
tooltip: {
|
||||
show: true,
|
||||
},
|
||||
"type": "flant-statusmap-panel",
|
||||
"urls": [
|
||||
type: 'flant-statusmap-panel',
|
||||
urls: [
|
||||
{
|
||||
"base_url": "",
|
||||
"extraSeries": {
|
||||
"index": -1
|
||||
base_url: '',
|
||||
extraSeries: {
|
||||
index: -1,
|
||||
},
|
||||
"forcelowercase": true,
|
||||
"icon_fa": "external-link",
|
||||
"label": "",
|
||||
"tooltip": "",
|
||||
"useExtraSeries": false,
|
||||
"useseriesname": true
|
||||
}
|
||||
forcelowercase: true,
|
||||
icon_fa: 'external-link',
|
||||
label: '',
|
||||
tooltip: '',
|
||||
useExtraSeries: false,
|
||||
useseriesname: true,
|
||||
},
|
||||
],
|
||||
"useMax": true,
|
||||
"usingUrl": true,
|
||||
"xAxis": {
|
||||
"labelFormat": "%a %m/%d",
|
||||
"show": true
|
||||
useMax: true,
|
||||
usingUrl: true,
|
||||
xAxis: {
|
||||
labelFormat: '%a %m/%d',
|
||||
show: true,
|
||||
},
|
||||
"yAxis": {
|
||||
"maxWidth": -1,
|
||||
"minWidth": -1,
|
||||
"show": true
|
||||
yAxis: {
|
||||
maxWidth: -1,
|
||||
minWidth: -1,
|
||||
show: true,
|
||||
},
|
||||
"yAxisSort": "metrics"
|
||||
}
|
||||
yAxisSort: 'metrics',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
panel = Object.assign({}, panelConfig);
|
||||
@@ -98,7 +98,7 @@ describe('when migrate from config with urls', () => {
|
||||
|
||||
it('should migrate usingUrls to tooltip.freezeOnClick', () => {
|
||||
expect(panel.usingUrl).toBeUndefined();
|
||||
expect(panel.tooltip).toHaveProperty("freezeOnClick", true);
|
||||
expect(panel.tooltip).toHaveProperty('freezeOnClick', true);
|
||||
expect(panel.tooltip.freezeOnClick).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -107,61 +107,60 @@ describe('when migrate from config with urls', () => {
|
||||
});
|
||||
|
||||
it('should have empty items in tooltip', () => {
|
||||
expect(panel.tooltip).toHaveProperty("items", []);
|
||||
expect(panel.tooltip).toHaveProperty('items', []);
|
||||
expect(panel.tooltip.items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given configuration with non-empty base_url", () => {
|
||||
|
||||
describe("with variables", () => {
|
||||
describe('given configuration with non-empty base_url', () => {
|
||||
describe('with variables', () => {
|
||||
const panelConfig = {
|
||||
"tooltip": {
|
||||
"show": true
|
||||
tooltip: {
|
||||
show: true,
|
||||
},
|
||||
"urls": [
|
||||
urls: [
|
||||
{
|
||||
"base_url": "https://google.com/$series_label$time",
|
||||
"extraSeries": {
|
||||
"index": -1
|
||||
base_url: 'https://google.com/$series_label$time',
|
||||
extraSeries: {
|
||||
index: -1,
|
||||
},
|
||||
"forcelowercase": true,
|
||||
"icon_fa": "external-link",
|
||||
"label": "google",
|
||||
"tooltip": "",
|
||||
"useExtraSeries": false,
|
||||
"useseriesname": true,
|
||||
forcelowercase: true,
|
||||
icon_fa: 'external-link',
|
||||
label: 'google',
|
||||
tooltip: '',
|
||||
useExtraSeries: false,
|
||||
useseriesname: true,
|
||||
},
|
||||
{
|
||||
"base_url": "example.com/$series_extra",
|
||||
"extraSeries": {
|
||||
"format": "YYYY/MM/DD/HH_mm_ss",
|
||||
"index": -1
|
||||
base_url: 'example.com/$series_extra',
|
||||
extraSeries: {
|
||||
format: 'YYYY/MM/DD/HH_mm_ss',
|
||||
index: -1,
|
||||
},
|
||||
"forcelowercase": true,
|
||||
"icon_fa": "external-link",
|
||||
"label": "DateLink",
|
||||
"useExtraSeries": true,
|
||||
}
|
||||
]
|
||||
forcelowercase: true,
|
||||
icon_fa: 'external-link',
|
||||
label: 'DateLink',
|
||||
useExtraSeries: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
beforeEach(() => {
|
||||
panel = Object.assign({}, panelConfig);
|
||||
migratePanelConfig(panel);
|
||||
});
|
||||
|
||||
it("should have equal size of tooltip.items", () => {
|
||||
expect(panel.tooltip).toHaveProperty("items");
|
||||
it('should have equal size of tooltip.items', () => {
|
||||
expect(panel.tooltip).toHaveProperty('items');
|
||||
expect(panel.tooltip.items).toHaveLength(panelConfig.urls.length);
|
||||
});
|
||||
it("should replace time variable with __url_time_range", () => {
|
||||
expect(panel.tooltip.items[0].urlTemplate).toContain("${__url_time_range}");
|
||||
it('should replace time variable with __url_time_range', () => {
|
||||
expect(panel.tooltip.items[0].urlTemplate).toContain('${__url_time_range}');
|
||||
});
|
||||
it("should replace series_label variable with __y_label", () => {
|
||||
expect(panel.tooltip.items[0].urlTemplate).toContain("${__y_label}");
|
||||
it('should replace series_label variable with __y_label', () => {
|
||||
expect(panel.tooltip.items[0].urlTemplate).toContain('${__y_label}');
|
||||
});
|
||||
it("should replace series_extra variable with __value_date", () => {
|
||||
expect(panel.tooltip.items[1].urlTemplate).toContain("${__value_date}");
|
||||
it('should replace series_extra variable with __value_date', () => {
|
||||
expect(panel.tooltip.items[1].urlTemplate).toContain('${__value_date}');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+42
-47
@@ -1,39 +1,38 @@
|
||||
// A holder of a group of values
|
||||
class Bucket {
|
||||
// uniq id
|
||||
id: number = 0;
|
||||
id = 0;
|
||||
// Array of values in this bucket
|
||||
values: any[] = [];
|
||||
columns: any[] = []; // From pr/86
|
||||
// a bucket has multiple values
|
||||
multipleValues: boolean = false;
|
||||
multipleValues = false;
|
||||
// a bucket has values that has no mapped color
|
||||
noColorDefined: boolean = false;
|
||||
noColorDefined = false;
|
||||
// y label
|
||||
y: string = "";
|
||||
yLabel: string = "";
|
||||
y = '';
|
||||
yLabel = '';
|
||||
pLabels: string[] = [];
|
||||
|
||||
// This value can be used to calculate a x coordinate on a graph
|
||||
x: number = 0;
|
||||
xid: number = 0;
|
||||
x = 0;
|
||||
xid = 0;
|
||||
// a time range of this bucket
|
||||
from: number = 0;
|
||||
to: number = 0;
|
||||
from = 0;
|
||||
to = 0;
|
||||
// to and from relative to real "from"
|
||||
relFrom: number = 0;
|
||||
relTo: number = 0;
|
||||
relFrom = 0;
|
||||
relTo = 0;
|
||||
|
||||
mostRecent: boolean = false;
|
||||
mostRecent = false;
|
||||
|
||||
// Saved minimum and maximum of values in this bucket
|
||||
minValue: number = 0;
|
||||
maxValue: number = 0;
|
||||
minValue = 0;
|
||||
maxValue = 0;
|
||||
// A value if multiple values is not allowed
|
||||
value: number = 0;
|
||||
value = 0;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
belong(ts: number): boolean {
|
||||
return ts >= this.from && ts <= this.to;
|
||||
@@ -48,32 +47,29 @@ class Bucket {
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.values.length == 0;
|
||||
return this.values.length === 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class BucketMatrix {
|
||||
// buckets for each y label
|
||||
buckets: {[yLabel: string]: Bucket[]} = {};
|
||||
maxValue: number = 0;
|
||||
minValue: number = 0;
|
||||
multipleValues: boolean = false;
|
||||
noColorDefined: boolean = false;
|
||||
buckets: { [yLabel: string]: Bucket[] } = {};
|
||||
maxValue = 0;
|
||||
minValue = 0;
|
||||
multipleValues = false;
|
||||
noColorDefined = false;
|
||||
// a flag that indicate that buckets has stub values
|
||||
noDatapoints: boolean = false;
|
||||
noDatapoints = false;
|
||||
|
||||
// An array of row labels
|
||||
targets: string[] = [];
|
||||
pLabels: {[target: string]: string[]} = {};
|
||||
rangeMs: number = 0;
|
||||
intervalMs: number = 0;
|
||||
pLabels: { [target: string]: string[] } = {};
|
||||
rangeMs = 0;
|
||||
intervalMs = 0;
|
||||
|
||||
xBucketSize: number = 0; // TODO remove: a transition from CardsData
|
||||
xBucketSize = 0; // TODO remove: a transition from CardsData
|
||||
|
||||
constructor() {
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
get(yid: string, xid: number): Bucket {
|
||||
if (yid in this.buckets) {
|
||||
@@ -87,7 +83,7 @@ class BucketMatrix {
|
||||
hasData(): boolean {
|
||||
let hasData = false;
|
||||
if (this.targets.length > 0) {
|
||||
this.targets.map((target:string) => {
|
||||
this.targets.map((target: string) => {
|
||||
if (this.buckets[target].length > 0) {
|
||||
hasData = true;
|
||||
}
|
||||
@@ -97,14 +93,14 @@ class BucketMatrix {
|
||||
}
|
||||
}
|
||||
|
||||
export var pagerChanged:any = {name:'statusmap-pager-changed'};
|
||||
export var pagerChanged: any = { name: 'statusmap-pager-changed' };
|
||||
|
||||
class BucketMatrixPager {
|
||||
bucketMatrix: BucketMatrix;
|
||||
enable: boolean;
|
||||
defaultPageSize: number = -1;
|
||||
pageSize: number = -1;
|
||||
currentPage: number = 0;
|
||||
defaultPageSize = -1;
|
||||
pageSize = -1;
|
||||
currentPage = 0;
|
||||
|
||||
constructor() {
|
||||
let m = new BucketMatrix();
|
||||
@@ -118,20 +114,20 @@ class BucketMatrixPager {
|
||||
return this.bucketMatrix.targets;
|
||||
}
|
||||
|
||||
return this.bucketMatrix.targets.slice(this.pageSize * this.currentPage, this.pageSize * (this.currentPage+1) );
|
||||
return this.bucketMatrix.targets.slice(this.pageSize * this.currentPage, this.pageSize * (this.currentPage + 1));
|
||||
}
|
||||
|
||||
buckets(): {[yLabel: string]: Bucket[]} {
|
||||
buckets(): { [yLabel: string]: Bucket[] } {
|
||||
if (!this.enable) {
|
||||
return this.bucketMatrix.buckets;
|
||||
}
|
||||
|
||||
let buckets: {[yLabel: string]: Bucket[]} = {}
|
||||
let buckets: { [yLabel: string]: Bucket[] } = {};
|
||||
let me = this;
|
||||
|
||||
this.targets().map(function (rowLabel) {
|
||||
this.targets().map(function(rowLabel) {
|
||||
buckets[rowLabel] = me.bucketMatrix.buckets[rowLabel];
|
||||
})
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
@@ -164,7 +160,7 @@ class BucketMatrixPager {
|
||||
if (!this.enable) {
|
||||
return 1;
|
||||
}
|
||||
return (this.pageSize * this.currentPage) + 1;
|
||||
return this.pageSize * this.currentPage + 1;
|
||||
}
|
||||
|
||||
pageEndRow(): number {
|
||||
@@ -172,7 +168,7 @@ class BucketMatrixPager {
|
||||
return this.totalRows();
|
||||
}
|
||||
|
||||
let last = this.pageSize * (this.currentPage+1);
|
||||
let last = this.pageSize * (this.currentPage + 1);
|
||||
if (last > this.totalRows()) {
|
||||
return this.totalRows();
|
||||
}
|
||||
@@ -185,7 +181,7 @@ class BucketMatrixPager {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((this.currentPage+1)*this.pageSize >= this.totalRows()) {
|
||||
if ((this.currentPage + 1) * this.pageSize >= this.totalRows()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -212,7 +208,6 @@ class BucketMatrixPager {
|
||||
this.currentPage = this.currentPage - 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export {Bucket, BucketMatrix, BucketMatrixPager };
|
||||
export { Bucket, BucketMatrix, BucketMatrixPager };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import "variables";
|
||||
@import "variables.dark";
|
||||
@import "statusmap"
|
||||
@import "statusmap";
|
||||
@@ -1,3 +1,3 @@
|
||||
@import "variables";
|
||||
@import "variables.light";
|
||||
@import "statusmap"
|
||||
@import "statusmap";
|
||||
+90
-56
@@ -1,4 +1,4 @@
|
||||
import d3 from 'd3';
|
||||
import * as d3 from 'd3';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -33,12 +33,14 @@ export class StatusmapTooltip {
|
||||
this.mouseOverBucket = false;
|
||||
this.originalFillColor = null;
|
||||
|
||||
elem.on("mouseover", this.onMouseOver.bind(this));
|
||||
elem.on("mouseleave", this.onMouseLeave.bind(this));
|
||||
elem.on('mouseover', this.onMouseOver.bind(this));
|
||||
elem.on('mouseleave', this.onMouseLeave.bind(this));
|
||||
}
|
||||
|
||||
onMouseOver(e) {
|
||||
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) { return; }
|
||||
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.tooltip) {
|
||||
this.add();
|
||||
@@ -51,15 +53,18 @@ export class StatusmapTooltip {
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (!this.panel.tooltip.show) { return; }
|
||||
if (!this.panel.tooltip.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.move(e, this.tooltip);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("class", "graph-tooltip statusmap-tooltip");
|
||||
this.tooltip = d3
|
||||
.select('body')
|
||||
.append('div')
|
||||
.attr('class', 'graph-tooltip statusmap-tooltip');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -73,21 +78,24 @@ export class StatusmapTooltip {
|
||||
removeFrozen() {
|
||||
if (this.tooltipFrozen) {
|
||||
this.tooltipFrozen.remove();
|
||||
this.tooltipFrozen = null
|
||||
this.tooltipFrozen = null;
|
||||
}
|
||||
}
|
||||
|
||||
showFrozen(pos: any) {
|
||||
this.removeFrozen();
|
||||
this.tooltipFrozen = d3.select(this.panelElem[0])
|
||||
.append("div")
|
||||
.attr("class", "graph-tooltip statusmap-tooltip statusmap-tooltip-frozen");
|
||||
this.tooltipFrozen = d3
|
||||
.select(this.panelElem[0])
|
||||
.append('div')
|
||||
.attr('class', 'graph-tooltip statusmap-tooltip statusmap-tooltip-frozen');
|
||||
this.displayTooltip(pos, this.tooltipFrozen, true);
|
||||
this.moveRelative(pos, this.tooltipFrozen);
|
||||
}
|
||||
|
||||
show(pos: any) {
|
||||
if (!this.panel.tooltip.show || !this.tooltip) { return; }
|
||||
if (!this.panel.tooltip.show || !this.tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO support for shared tooltip mode
|
||||
if (pos.panelRelY) {
|
||||
@@ -104,6 +112,7 @@ export class StatusmapTooltip {
|
||||
let cardEl = d3.select(pos.target);
|
||||
let yid = cardEl.attr('yid');
|
||||
let xid = cardEl.attr('xid');
|
||||
// @ts-ignore
|
||||
let bucket = this.panelCtrl.bucketMatrix.get(yid, xid); // TODO string-to-number conversion for xid
|
||||
if (!bucket || bucket.isEmpty()) {
|
||||
this.destroy();
|
||||
@@ -116,11 +125,11 @@ export class StatusmapTooltip {
|
||||
let value = bucket.value;
|
||||
let values = bucket.values;
|
||||
// TODO create option for this formatting.
|
||||
let tooltipTimeFormat:string = 'YYYY-MM-DD HH:mm:ss';
|
||||
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
let time: Date = this.dashboard.formatDate(+timestamp, tooltipTimeFormat);
|
||||
|
||||
// Close button for the frozen tooltip.
|
||||
let tooltipClose:string = ``;
|
||||
let tooltipClose = ``;
|
||||
if (frozen) {
|
||||
tooltipClose = `
|
||||
<a class="pointer pull-right small tooltip-close">
|
||||
@@ -129,7 +138,7 @@ export class StatusmapTooltip {
|
||||
`;
|
||||
}
|
||||
|
||||
let tooltipHtml:string = `<div class="graph-tooltip-time">${time}${tooltipClose}</div>`;
|
||||
let tooltipHtml = `<div class="graph-tooltip-time">${time}${tooltipClose}</div>`;
|
||||
|
||||
if (this.panel.color.mode === 'discrete') {
|
||||
let statuses;
|
||||
@@ -138,10 +147,10 @@ export class StatusmapTooltip {
|
||||
} else {
|
||||
statuses = this.panelCtrl.discreteExtraSeries.convertValuesToTooltips(values);
|
||||
}
|
||||
|
||||
let statusTitle:string = "status:";
|
||||
|
||||
let statusTitle = 'status:';
|
||||
if (statuses.length > 1) {
|
||||
statusTitle = "statuses:";
|
||||
statusTitle = 'statuses:';
|
||||
}
|
||||
tooltipHtml += `
|
||||
<div>
|
||||
@@ -149,12 +158,18 @@ export class StatusmapTooltip {
|
||||
<br>
|
||||
<span>${statusTitle}</span>
|
||||
<ul>
|
||||
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}; text-align:center" class="discrete-item">${v.tooltip}</li>`), "")}
|
||||
${_.join(
|
||||
_.map(
|
||||
statuses,
|
||||
v => `<li style="background-color: ${v.color}; text-align:center" class="discrete-item">${v.tooltip}</li>`
|
||||
),
|
||||
''
|
||||
)}
|
||||
</ul>
|
||||
</div>`;
|
||||
} else {
|
||||
if (values.length === 1) {
|
||||
tooltipHtml += `<div>
|
||||
tooltipHtml += `<div>
|
||||
name: <b>${yLabel}</b> <br>
|
||||
value: <b>${value}</b> <br>
|
||||
</div>`;
|
||||
@@ -163,7 +178,10 @@ export class StatusmapTooltip {
|
||||
name: <b>${yLabel}</b> <br>
|
||||
values:
|
||||
<ul>
|
||||
${_.join(_.map(values, v => `<li>${v}</li>`), "")}
|
||||
${_.join(
|
||||
_.map(values, v => `<li>${v}</li>`),
|
||||
''
|
||||
)}
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
@@ -181,35 +199,35 @@ export class StatusmapTooltip {
|
||||
let valueVar;
|
||||
for (let i = 0; i < bucket.values.length; i++) {
|
||||
valueVar = `__value_${i}`;
|
||||
scopedVars[valueVar] = {value: bucket.values[i] };
|
||||
scopedVars[valueVar] = { value: bucket.values[i] };
|
||||
}
|
||||
scopedVars[`__value`] = {value: bucket.value};
|
||||
scopedVars[`__y_label`] = {value: yLabel};
|
||||
scopedVars[`__y_label_trim`] = {value: yLabel.trim()};
|
||||
scopedVars['__value'] = { value: bucket.value };
|
||||
scopedVars['__y_label'] = { value: yLabel };
|
||||
scopedVars['__y_label_trim'] = { value: yLabel.trim() };
|
||||
// Grafana 7.0 compatible
|
||||
scopedVars[`__url_time_range`] = {value: this.panelCtrl.retrieveTimeVar()};
|
||||
scopedVars['__url_time_range'] = { value: this.panelCtrl.retrieveTimeVar() };
|
||||
|
||||
//New vars based on partialLabels:
|
||||
for (let i in pLabels) {
|
||||
scopedVars[`__y_label_${i}`] = {value: pLabels[i]};
|
||||
scopedVars[`__y_label_${i}`] = { value: pLabels[i] };
|
||||
}
|
||||
|
||||
for (let item of items) {
|
||||
if (_.isEmpty(item.urlTemplate)) {
|
||||
item.link = "#";
|
||||
item.link = '#';
|
||||
} else {
|
||||
let dateFormat = item.valueDateFormat;
|
||||
if (dateFormat == '') {
|
||||
if (dateFormat === '') {
|
||||
dateFormat = DefaultValueDateFormat;
|
||||
}
|
||||
let valueDateVar;
|
||||
for (let i = 0; i < bucket.values.length; i++) {
|
||||
valueDateVar = `__value_${i}_date`;
|
||||
scopedVars[valueDateVar] = {value: this.dashboard.formatDate(+bucket.values[i], dateFormat)};
|
||||
scopedVars[valueDateVar] = { value: this.dashboard.formatDate(+bucket.values[i], dateFormat) };
|
||||
}
|
||||
scopedVars[`__value_date`] = {value: this.dashboard.formatDate(+bucket.value, dateFormat)};
|
||||
scopedVars['__value_date'] = { value: this.dashboard.formatDate(+bucket.value, dateFormat) };
|
||||
|
||||
item.link = this.panelCtrl.templateSrv.replace(item.urlTemplate, scopedVars)
|
||||
item.link = this.panelCtrl.templateSrv.replace(item.urlTemplate, scopedVars);
|
||||
|
||||
// Force lowercase for link
|
||||
if (item.urlToLowerCase) {
|
||||
@@ -219,24 +237,29 @@ export class StatusmapTooltip {
|
||||
|
||||
item.label = item.urlText;
|
||||
if (_.isEmpty(item.label)) {
|
||||
item.label = _.isEmpty(item.urlTemplate) ? "Empty URL" : _.truncate(item.link);
|
||||
item.label = _.isEmpty(item.urlTemplate) ? 'Empty URL' : _.truncate(item.link);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.panel.tooltip.showCustomContent) {
|
||||
let customContent: string = this.panelCtrl.templateSrv.replace(this.panel.tooltip.customContent, scopedVars)
|
||||
tooltipHtml += `<div>${customContent}</div>`
|
||||
let customContent: string = this.panelCtrl.templateSrv.replace(this.panel.tooltip.customContent, scopedVars);
|
||||
tooltipHtml += `<div>${customContent}</div>`;
|
||||
}
|
||||
|
||||
tooltipHtml += _.join(_.map(items, v => `
|
||||
tooltipHtml += _.join(
|
||||
_.map(
|
||||
items,
|
||||
v => `
|
||||
<div>
|
||||
<a href="${v.link}" target="_blank">
|
||||
<div class="dashlist-item">
|
||||
<p class="dashlist-link dashlist-link-dash-db">
|
||||
<span style="word-wrap: break-word;" class="dash-title">${v.label}</span><span class="dashlist-star">
|
||||
<i class="fa fa-${v.urlIcon}"></i>
|
||||
</span></p> </div></a><div>`), "\n");
|
||||
|
||||
</span></p> </div></a><div>`
|
||||
),
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
// Ambiguous state: there multiple values in bucket!
|
||||
@@ -252,7 +275,10 @@ export class StatusmapTooltip {
|
||||
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title}
|
||||
<br>not colored values:
|
||||
<ul>
|
||||
${_.join(_.map(badValues, v => `<li>${v}</li>`), "")}
|
||||
${_.join(
|
||||
_.map(badValues, v => `<li>${v}</li>`),
|
||||
''
|
||||
)}
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
@@ -264,19 +290,29 @@ export class StatusmapTooltip {
|
||||
if (frozen) {
|
||||
// Stop propagation mouse events up to parents to allow interaction with frozen tooltip’s elements.
|
||||
tooltip
|
||||
.on("click", function(){d3.event.stopPropagation(); })
|
||||
.on("mousedown", function(){d3.event.stopPropagation(); })
|
||||
.on("mouseup", function(){d3.event.stopPropagation(); });
|
||||
.on('click', function() {
|
||||
// @ts-ignore
|
||||
d3.event.stopPropagation();
|
||||
})
|
||||
.on('mousedown', function() {
|
||||
// @ts-ignore
|
||||
d3.event.stopPropagation();
|
||||
})
|
||||
.on('mouseup', function() {
|
||||
// @ts-ignore
|
||||
d3.event.stopPropagation();
|
||||
});
|
||||
|
||||
// Activate close button
|
||||
tooltip.select("a.tooltip-close")
|
||||
.on("click", this.removeFrozen.bind(this));
|
||||
tooltip.select('a.tooltip-close').on('click', this.removeFrozen.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
// Move tooltip as absolute positioned element.
|
||||
move(pos, tooltip) {
|
||||
if (!tooltip) { return; }
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elem = $(tooltip.node())[0];
|
||||
let tooltipWidth = elem.clientWidth;
|
||||
@@ -294,14 +330,14 @@ export class StatusmapTooltip {
|
||||
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
|
||||
}
|
||||
|
||||
return tooltip
|
||||
.style("left", left + "px")
|
||||
.style("top", top + "px");
|
||||
return tooltip.style('left', left + 'px').style('top', top + 'px');
|
||||
}
|
||||
|
||||
// Move tooltip relative to svg element of panel.
|
||||
moveRelative(pos, tooltip) {
|
||||
if (!tooltip) { return; }
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
let panelX = pos.pageX - this.panelElem.offset().left;
|
||||
let panelY = pos.pageY - this.panelElem.offset().top;
|
||||
@@ -317,7 +353,7 @@ export class StatusmapTooltip {
|
||||
if (tooltipLeft + tooltipWidth > panelWidth) {
|
||||
tooltipLeft = panelWidth - tooltipWidth;
|
||||
}
|
||||
if (tooltipLeft < 0 ) {
|
||||
if (tooltipLeft < 0) {
|
||||
tooltipLeft = 0;
|
||||
}
|
||||
|
||||
@@ -327,10 +363,8 @@ export class StatusmapTooltip {
|
||||
let tooltipTop = -(panelHeight - panelY + TOOLTIP_PADDING_Y);
|
||||
|
||||
return tooltip
|
||||
.style("left", tooltipLeft + "px")
|
||||
.style("top", tooltipTop + "px")
|
||||
.style("width", tooltipWidth + "px");
|
||||
.style('left', tooltipLeft + 'px')
|
||||
.style('top', tooltipTop + 'px')
|
||||
.style('width', tooltipWidth + 'px');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ let emptyTooltipItem = {
|
||||
urlTemplate: '',
|
||||
urlIcon: 'external-link',
|
||||
urlToLowerCase: true,
|
||||
valueDateFormat: ''
|
||||
valueDateFormat: '',
|
||||
};
|
||||
|
||||
export class TooltipEditorCtrl {
|
||||
panel: any;
|
||||
panelCtrl: StatusHeatmapCtrl;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any) {
|
||||
$scope.editor = this;
|
||||
this.panelCtrl = $scope.ctrl as StatusHeatmapCtrl;
|
||||
@@ -42,11 +43,7 @@ export class TooltipEditorCtrl {
|
||||
}
|
||||
|
||||
getValueDateFormats() {
|
||||
return [
|
||||
'YYYY/MM/DD/HH_mm_ss',
|
||||
'YYYYMMDDHHmmss',
|
||||
'YYYY-MM-DD-HH-mm-ss'
|
||||
];
|
||||
return ['YYYY/MM/DD/HH_mm_ss', 'YYYYMMDDHHmmss', 'YYYY-MM-DD-HH-mm-ss'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user