From ef9dd014c783548c0b3df3e2460f63f75ceac2a6 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 31 Jul 2017 14:03:03 +0300 Subject: [PATCH 01/37] heatmap: add color scale options, issue #8539 --- .../panel/heatmap/partials/display_editor.html | 12 ++++++++++++ public/app/plugins/panel/heatmap/rendering.ts | 12 +++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/heatmap/partials/display_editor.html b/public/app/plugins/panel/heatmap/partials/display_editor.html index 863fcc49d07..2c66b90b949 100644 --- a/public/app/plugins/panel/heatmap/partials/display_editor.html +++ b/public/app/plugins/panel/heatmap/partials/display_editor.html @@ -43,6 +43,18 @@ +
+
Color scale
+
+ + +
+
+ + +
+
+
Buckets
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index de2c2fde719..c6f898fcb28 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -385,9 +385,11 @@ export default function link(scope, elem, attrs, ctrl) { } let cardsData = convertToCards(data.buckets); - let maxValue = d3.max(cardsData, card => card.count); + let maxValueAuto = d3.max(cardsData, card => card.count); + let maxValue = panel.color.max || maxValueAuto; + let minValue = panel.color.min || 0; - colorScale = getColorScale(maxValue); + colorScale = getColorScale(maxValue, minValue); setOpacityScale(maxValue); setCardSize(); @@ -434,14 +436,14 @@ export default function link(scope, elem, attrs, ctrl) { .style("stroke-width", 0); } - function getColorScale(maxValue) { + function getColorScale(maxValue, minValue = 0) { let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); let colorInterpolator = d3[colorScheme.value]; let colorScaleInverted = colorScheme.invert === 'always' || (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme); - let start = colorScaleInverted ? maxValue : 0; - let end = colorScaleInverted ? 0 : maxValue; + let start = colorScaleInverted ? maxValue : minValue; + let end = colorScaleInverted ? minValue : maxValue; return d3.scaleSequential(colorInterpolator).domain([start, end]); } From 55b24be11585737911fc990c7bc2e46ebae13bc6 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 31 Jul 2017 17:45:05 +0300 Subject: [PATCH 02/37] heatmap: refactor, directive for color legend --- .../app/plugins/panel/heatmap/color_legend.ts | 136 ++++++++++++++++++ .../plugins/panel/heatmap/display_editor.ts | 6 + public/app/plugins/panel/heatmap/module.ts | 2 +- .../heatmap/partials/display_editor.html | 10 +- public/app/plugins/panel/heatmap/rendering.ts | 67 --------- 5 files changed, 147 insertions(+), 74 deletions(-) create mode 100644 public/app/plugins/panel/heatmap/color_legend.ts diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts new file mode 100644 index 00000000000..572d26c7330 --- /dev/null +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -0,0 +1,136 @@ +/// +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import d3 from 'd3'; +import {contextSrv} from 'app/core/core'; + +let module = angular.module('grafana.directives'); +module.directive('colorLegend', function() { + return { + restrict: 'E', + template: '
', + link: function(scope, elem, attrs) { + let ctrl = scope.ctrl; + let panel = scope.ctrl.panel; + + render(); + + ctrl.events.on('render', function() { + render(); + }); + + function render() { + let legendElem = $(elem).find('svg'); + let legendWidth = Math.floor(legendElem.outerWidth()); + + if (panel.color.mode === 'spectrum') { + let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); + let colorScale = getColorScale(colorScheme, legendWidth); + drawColorLegend(elem, colorScale); + } else if (panel.color.mode === 'opacity') { + let colorOptions = panel.color; + drawOpacityLegend(elem, colorOptions); + } + } + } + }; +}); + +module.directive('heatmapLegend', function() { + return { + restrict: 'E', + template: '
', + link: function(scope, elem, attrs) { + let ctrl = scope.ctrl; + let panel = scope.ctrl.panel; + + ctrl.events.on('render', function() { + if (!_.isEmpty(ctrl.data)) { + let legendElem = $(elem).find('svg'); + let legendWidth = Math.floor(legendElem.outerWidth()); + + if (panel.color.mode === 'spectrum') { + let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); + let colorScale = getColorScale(colorScheme, legendWidth); + drawColorLegend(elem, colorScale); + } else if (panel.color.mode === 'opacity') { + let colorOptions = panel.color; + drawOpacityLegend(elem, colorOptions); + } + } + }); + } + }; +}); + +function drawColorLegend(elem, colorScale) { + let legendElem = $(elem).find('svg'); + legendElem.find("rect").remove(); + + let legendWidth = Math.floor(legendElem.outerWidth()); + let legendHeight = legendElem.attr("height"); + + let rangeStep = 2; + let valuesRange = d3.range(0, legendWidth, rangeStep); + + let legend = d3.select(legendElem.get(0)); + var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", d => colorScale(d)); +} + +function clearLegend(elem) { + let legendElem = $(elem).find('svg'); + legendElem.find("rect").remove(); +} + +function drawOpacityLegend(elem, options) { + let legendElem = $(elem).find('svg'); + clearLegend(elem); + + let legend = d3.select(legendElem.get(0)); + let legendWidth = Math.floor(legendElem.outerWidth()); + let legendHeight = legendElem.attr("height"); + + let legendOpacityScale; + if (options.colorScale === 'linear') { + 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]); + } + + let rangeStep = 1; + let valuesRange = d3.range(0, legendWidth, rangeStep); + var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep) + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", options.cardColor) + .style("opacity", d => legendOpacityScale(d)); +} + +function getColorScale(colorScheme, maxValue, minValue = 0) { + let colorInterpolator = d3[colorScheme.value]; + let colorScaleInverted = colorScheme.invert === 'always' || + (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme); + + let start = colorScaleInverted ? maxValue : minValue; + let end = colorScaleInverted ? minValue : maxValue; + + return d3.scaleSequential(colorInterpolator).domain([start, end]); +} diff --git a/public/app/plugins/panel/heatmap/display_editor.ts b/public/app/plugins/panel/heatmap/display_editor.ts index 775017b00da..1cea9505567 100644 --- a/public/app/plugins/panel/heatmap/display_editor.ts +++ b/public/app/plugins/panel/heatmap/display_editor.ts @@ -1,4 +1,10 @@ /// +import _ from 'lodash'; +import $ from 'jquery'; +import d3 from 'd3'; +import {contextSrv} from 'app/core/core'; + +const COLOR_LEGEND_SELECTOR = '.heatmap-color-legend'; export class HeatmapDisplayEditorCtrl { panel: any; diff --git a/public/app/plugins/panel/heatmap/module.ts b/public/app/plugins/panel/heatmap/module.ts index d6926455563..d5ef7291308 100644 --- a/public/app/plugins/panel/heatmap/module.ts +++ b/public/app/plugins/panel/heatmap/module.ts @@ -1,5 +1,5 @@ /// - +import './color_legend'; import {HeatmapCtrl} from './heatmap_ctrl'; export { diff --git a/public/app/plugins/panel/heatmap/partials/display_editor.html b/public/app/plugins/panel/heatmap/partials/display_editor.html index 2c66b90b949..4f215f3fcce 100644 --- a/public/app/plugins/panel/heatmap/partials/display_editor.html +++ b/public/app/plugins/panel/heatmap/partials/display_editor.html @@ -25,9 +25,6 @@
-
- -
@@ -37,9 +34,10 @@
-
- -
+ + +
+
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index c6f898fcb28..74b433653f1 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -706,78 +706,11 @@ export default function link(scope, elem, attrs, ctrl) { } } - function drawColorLegend() { - d3.select("#heatmap-color-legend").selectAll("rect").remove(); - - let legend = d3.select("#heatmap-color-legend"); - let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth()); - let legendHeight = d3.select("#heatmap-color-legend").attr("height"); - - let legendColorScale = getColorScale(legendWidth); - - let rangeStep = 2; - let valuesRange = d3.range(0, legendWidth, rangeStep); - var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange); - - legendRects.enter().append("rect") - .attr("x", d => d) - .attr("y", 0) - .attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps - .attr("height", legendHeight) - .attr("stroke-width", 0) - .attr("fill", d => { - return legendColorScale(d); - }); - } - - function drawOpacityLegend() { - d3.select("#heatmap-opacity-legend").selectAll("rect").remove(); - - let legend = d3.select("#heatmap-opacity-legend"); - let legendWidth = Math.floor($(d3.select("#heatmap-opacity-legend").node()).outerWidth()); - let legendHeight = d3.select("#heatmap-opacity-legend").attr("height"); - - let legendOpacityScale; - if (panel.color.colorScale === 'linear') { - legendOpacityScale = d3.scaleLinear() - .domain([0, legendWidth]) - .range([0, 1]); - } else if (panel.color.colorScale === 'sqrt') { - legendOpacityScale = d3.scalePow().exponent(panel.color.exponent) - .domain([0, legendWidth]) - .range([0, 1]); - } - - let rangeStep = 1; - let valuesRange = d3.range(0, legendWidth, rangeStep); - var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); - - legendRects.enter().append("rect") - .attr("x", d => d) - .attr("y", 0) - .attr("width", rangeStep) - .attr("height", legendHeight) - .attr("stroke-width", 0) - .attr("fill", panel.color.cardColor) - .style("opacity", d => { - return legendOpacityScale(d); - }); - } - function render() { data = ctrl.data; panel = ctrl.panel; timeRange = ctrl.range; - // Draw only if color editor is opened - if (!d3.select("#heatmap-color-legend").empty()) { - drawColorLegend(); - } - - if (!d3.select("#heatmap-opacity-legend").empty()) { - drawOpacityLegend(); - } - if (!setElementHeight() || !data) { return; } From c79a68dcd173395859b56b29ca35eba46a6f6037 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 31 Jul 2017 18:15:03 +0300 Subject: [PATCH 03/37] heatmap: refactor, build cards before rendering --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 14 ++++++++++++-- public/app/plugins/panel/heatmap/rendering.ts | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 37aa9410ea8..03bb46a104f 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -7,7 +7,7 @@ import TimeSeries from 'app/core/time_series'; import {axesEditor} from './axes_editor'; import {heatmapDisplayEditor} from './display_editor'; import rendering from './rendering'; -import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter'; +import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter'; let X_BUCKET_NUMBER_DEFAULT = 30; let Y_BUCKET_NUMBER_DEFAULT = 10; @@ -188,11 +188,21 @@ export class HeatmapCtrl extends MetricsPanelCtrl { yBucketSize = 1; } + let cardsData = convertToCards(bucketsData); + let maxCardsValue = _.max(_.map(cardsData, 'count')); + let minCardsValue = _.min(_.map(cardsData, 'count')); + let cardStats = { + max: maxCardsValue, + min: minCardsValue + }; + this.data = { buckets: bucketsData, heatmapStats: heatmapStats, xBucketSize: xBucketSize, - yBucketSize: yBucketSize + yBucketSize: yBucketSize, + cards: cardsData, + cardStats: cardStats }; } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 74b433653f1..8d85186b78e 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -8,7 +8,7 @@ import {appEvents, contextSrv} from 'app/core/core'; import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks'; import d3 from 'd3'; import {HeatmapTooltip} from './heatmap_tooltip'; -import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter'; +import {mergeZeroBuckets} from './heatmap_data_converter'; let MIN_CARD_SIZE = 1, CARD_PADDING = 1, @@ -384,8 +384,8 @@ export default function link(scope, elem, attrs, ctrl) { data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values)); } - let cardsData = convertToCards(data.buckets); - let maxValueAuto = d3.max(cardsData, card => card.count); + let cardsData = data.cards; + let maxValueAuto = data.cardStats.max; let maxValue = panel.color.max || maxValueAuto; let minValue = panel.color.min || 0; From 2aa26c98b6ba41bebb0671cb0c57c356d8040ae4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 31 Jul 2017 20:11:55 +0300 Subject: [PATCH 04/37] heatmap: initial legend --- .../app/plugins/panel/heatmap/color_legend.ts | 229 ++++++++++++++---- public/app/plugins/panel/heatmap/module.html | 3 + public/sass/components/_panel_heatmap.scss | 37 +++ 3 files changed, 227 insertions(+), 42 deletions(-) diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 572d26c7330..0b23a10e000 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -4,12 +4,13 @@ import _ from 'lodash'; import $ from 'jquery'; import d3 from 'd3'; import {contextSrv} from 'app/core/core'; +import {tickStep} from 'app/core/utils/ticks'; let module = angular.module('grafana.directives'); module.directive('colorLegend', function() { return { restrict: 'E', - template: '
', + template: '
', link: function(scope, elem, attrs) { let ctrl = scope.ctrl; let panel = scope.ctrl.panel; @@ -27,10 +28,10 @@ module.directive('colorLegend', function() { if (panel.color.mode === 'spectrum') { let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); let colorScale = getColorScale(colorScheme, legendWidth); - drawColorLegend(elem, colorScale); + drawSimpleColorLegend(elem, colorScale); } else if (panel.color.mode === 'opacity') { let colorOptions = panel.color; - drawOpacityLegend(elem, colorOptions); + drawSimpleOpacityLegend(elem, colorOptions); } } } @@ -40,7 +41,7 @@ module.directive('colorLegend', function() { module.directive('heatmapLegend', function() { return { restrict: 'E', - template: '
', + template: '
', link: function(scope, elem, attrs) { let ctrl = scope.ctrl; let panel = scope.ctrl.panel; @@ -50,13 +51,19 @@ module.directive('heatmapLegend', function() { let legendElem = $(elem).find('svg'); let legendWidth = Math.floor(legendElem.outerWidth()); + // let maxValue = ctrl.data.cardStats.max || legendWidth; + let rangeFrom = ctrl.data.cardStats.min; + let rangeTo = ctrl.data.cardStats.max; + let maxValue = panel.color.max || rangeTo; + let minValue = panel.color.min || 0; + if (panel.color.mode === 'spectrum') { let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); - let colorScale = getColorScale(colorScheme, legendWidth); - drawColorLegend(elem, colorScale); + let colorScale = getColorScale(colorScheme, maxValue, minValue); + drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue); } else if (panel.color.mode === 'opacity') { let colorOptions = panel.color; - drawOpacityLegend(elem, colorOptions); + drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); } } }); @@ -64,34 +71,116 @@ module.directive('heatmapLegend', function() { }; }); -function drawColorLegend(elem, colorScale) { +function drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue) { let legendElem = $(elem).find('svg'); - legendElem.find("rect").remove(); + clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()); + let legendWidth = Math.floor(legendElem.outerWidth()) - 30; let legendHeight = legendElem.attr("height"); - let rangeStep = 2; - let valuesRange = d3.range(0, legendWidth, rangeStep); + let rangeStep = 1; + if (rangeTo - rangeFrom > legendWidth) { + rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth); + } + let widthFactor = legendWidth / (rangeTo - rangeFrom); + let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); let legend = d3.select(legendElem.get(0)); var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange); legendRects.enter().append("rect") - .attr("x", d => d) - .attr("y", 0) - .attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps - .attr("height", legendHeight) - .attr("stroke-width", 0) - .attr("fill", d => colorScale(d)); + .attr("x", d => d * widthFactor) + .attr("y", 0) + .attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", d => colorScale(d)); + + drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); +} + +function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) { + let legendElem = $(elem).find('svg'); + clearLegend(elem); + + let legendWidth = Math.floor(legendElem.outerWidth()) - 30; + let legendHeight = legendElem.attr("height"); + + let rangeStep = 10; + let widthFactor = legendWidth / (rangeTo - rangeFrom); + let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + let legend = d3.select(legendElem.get(0)); + var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); + + let legendOpacityScale = getOpacityScale(options, maxValue, minValue); + legendRects.enter().append("rect") + .attr("x", d => d * widthFactor) + .attr("y", 0) + .attr("width", rangeStep * widthFactor) + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", options.cardColor) + .style("opacity", d => legendOpacityScale(d)); + + drawLegendValues(elem, legendOpacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); +} + +function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { + let legendElem = $(elem).find('svg'); + let legend = d3.select(legendElem.get(0)); + + let legendValueDomain = _.sortBy(colorScale.domain()); + let legendValueScale = d3.scaleLinear() + .domain([0, rangeTo]) + .range([0, legendWidth]); + + let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); + let xAxis = d3.axisBottom(legendValueScale) + .tickValues(ticks) + .tickSize(3); + + let legendElemHeight = legendElem.height(); + let posY = legendElemHeight - 23; + let posX = getSvgElemX(legendElem.find(":first-child")); + d3.select(legendElem.get(0)).append("g") + .attr("class", "axis") + .attr("transform", "translate(" + posX + "," + posY + ")") + .call(xAxis); + + legend.select(".axis").select(".domain").remove(); +} + +function drawSimpleColorLegend(elem, colorScale) { + let legendElem = $(elem).find('svg'); + clearLegend(elem); + + let legendWidth = Math.floor(legendElem.outerWidth()); + let legendHeight = legendElem.attr("height"); + + if (legendWidth) { + let valuesNumber = Math.floor(legendWidth / 2); + let rangeStep = Math.floor(legendWidth / valuesNumber); + let valuesRange = d3.range(0, legendWidth, rangeStep); + + let legend = d3.select(legendElem.get(0)); + var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", d => colorScale(d)); + } } function clearLegend(elem) { let legendElem = $(elem).find('svg'); - legendElem.find("rect").remove(); + legendElem.empty(); } -function drawOpacityLegend(elem, options) { +function drawSimpleOpacityLegend(elem, options) { let legendElem = $(elem).find('svg'); clearLegend(elem); @@ -99,29 +188,31 @@ function drawOpacityLegend(elem, options) { let legendWidth = Math.floor(legendElem.outerWidth()); let legendHeight = legendElem.attr("height"); - let legendOpacityScale; - if (options.colorScale === 'linear') { - 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]); + if (legendWidth) { + let legendOpacityScale; + if (options.colorScale === 'linear') { + 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]); + } + + let rangeStep = 10; + let valuesRange = d3.range(0, legendWidth, rangeStep); + var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep) + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", options.cardColor) + .style("opacity", d => legendOpacityScale(d)); } - - let rangeStep = 1; - let valuesRange = d3.range(0, legendWidth, rangeStep); - var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); - - legendRects.enter().append("rect") - .attr("x", d => d) - .attr("y", 0) - .attr("width", rangeStep) - .attr("height", legendHeight) - .attr("stroke-width", 0) - .attr("fill", options.cardColor) - .style("opacity", d => legendOpacityScale(d)); } function getColorScale(colorScheme, maxValue, minValue = 0) { @@ -134,3 +225,57 @@ function getColorScale(colorScheme, maxValue, minValue = 0) { return d3.scaleSequential(colorInterpolator).domain([start, end]); } + +function getOpacityScale(options, maxValue, minValue = 0) { + let legendOpacityScale; + if (options.colorScale === 'linear') { + 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]); + } + return legendOpacityScale; +} + +function getSvgElemX(elem) { + return elem.get(0).x.baseVal.value; +} + +function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { + let range = rangeTo - rangeFrom; + let tickStepSize = tickStep(rangeFrom, rangeTo, 3); + let ticksNum = Math.floor(range / tickStepSize); + let ticks = []; + + for (let i = 0; i < ticksNum; i++) { + let current = tickStepSize * i; + // Add user-defined min and max if it had been set + if (isValueCloseTo(minValue, current, tickStepSize)) { + ticks.push(minValue); + continue; + } else if (minValue < current) { + ticks.push(minValue); + } + if (isValueCloseTo(maxValue, current, tickStepSize)) { + ticks.push(maxValue); + continue; + } else if (maxValue < current) { + ticks.push(maxValue); + } + ticks.push(tickStepSize * i); + } + if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) { + ticks.push(maxValue); + } + ticks.push(rangeTo); + ticks = _.sortBy(_.uniq(ticks)); + return ticks; +} + +function isValueCloseTo(val, valueTo, step) { + let diff = Math.abs(val - valueTo); + return diff < step * 0.3; +} diff --git a/public/app/plugins/panel/heatmap/module.html b/public/app/plugins/panel/heatmap/module.html index 6cb89f2e1f2..ac9652a5afc 100644 --- a/public/app/plugins/panel/heatmap/module.html +++ b/public/app/plugins/panel/heatmap/module.html @@ -7,5 +7,8 @@
+
+ +
diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index 9d07e6a363c..9ca7b6f4bba 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -46,3 +46,40 @@ stroke-width: 1; } } + +.heatmap-legend-wrapper { + @include clearfix(); + margin: 0 $spacer; + padding-top: 10px; + + svg { + width: 100%; + max-width: 300px; + height: 38px; + float: left; + white-space: nowrap; + padding-left: 10px; + } + + .heatmap-legend-values { + display: inline-block; + } + + .axis .tick { + text { + fill: $text-color; + color: $text-color; + font-size: $font-size-sm; + } + + line { + opacity: 0.4; + stroke: $text-color-weak; + } + + .domain { + opacity: 0.4; + stroke: $text-color-weak; + } + } +} From e72baca4a714076ffb375eb8cd6e6c638d08bbda Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Aug 2017 13:54:09 +0300 Subject: [PATCH 05/37] heatmap: fix black selection area --- public/sass/components/_panel_heatmap.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index 9ca7b6f4bba..e593f6e2bf8 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -47,6 +47,13 @@ } } +.heatmap-selection { + stroke-width: 1; + opacity: 0.3; + fill: #828282; + stroke: darken($red,15%); +} + .heatmap-legend-wrapper { @include clearfix(); margin: 0 $spacer; From 91a921e12d51f1d75275a1ea556102cedabeb37b Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Aug 2017 14:07:52 +0300 Subject: [PATCH 06/37] heatmap: add show legend option --- public/app/plugins/panel/heatmap/color_legend.ts | 14 ++++++++++++-- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 3 +++ public/app/plugins/panel/heatmap/module.html | 2 +- .../panel/heatmap/partials/display_editor.html | 8 ++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 0b23a10e000..2d52acb8ddb 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -46,7 +46,12 @@ module.directive('heatmapLegend', function() { let ctrl = scope.ctrl; let panel = scope.ctrl.panel; + render(); ctrl.events.on('render', function() { + render(); + }); + + function render() { if (!_.isEmpty(ctrl.data)) { let legendElem = $(elem).find('svg'); let legendWidth = Math.floor(legendElem.outerWidth()); @@ -66,7 +71,7 @@ module.directive('heatmapLegend', function() { drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); } } - }); + } } }; }); @@ -241,7 +246,12 @@ function getOpacityScale(options, maxValue, minValue = 0) { } function getSvgElemX(elem) { - return elem.get(0).x.baseVal.value; + let svgElem = elem.get(0); + if (svgElem && svgElem.x && svgElem.x.baseVal) { + return elem.get(0).x.baseVal.value; + } else { + return 0; + } } function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 03bb46a104f..3b8d1fa9d41 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -26,6 +26,9 @@ let panelDefaults = { exponent: 0.5, colorScheme: 'interpolateOranges', }, + legend: { + show: false + }, dataFormat: 'timeseries', xAxis: { show: true, diff --git a/public/app/plugins/panel/heatmap/module.html b/public/app/plugins/panel/heatmap/module.html index ac9652a5afc..272aac99772 100644 --- a/public/app/plugins/panel/heatmap/module.html +++ b/public/app/plugins/panel/heatmap/module.html @@ -8,7 +8,7 @@
- +
diff --git a/public/app/plugins/panel/heatmap/partials/display_editor.html b/public/app/plugins/panel/heatmap/partials/display_editor.html index 4f215f3fcce..f161bf6cab4 100644 --- a/public/app/plugins/panel/heatmap/partials/display_editor.html +++ b/public/app/plugins/panel/heatmap/partials/display_editor.html @@ -53,6 +53,14 @@ +
+
Legend
+ + +
+
Buckets
From f64ebf538c11672bc389ebad59cca0a8ea0b26b8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Aug 2017 15:25:52 +0300 Subject: [PATCH 07/37] heatmap: minor legend fixes --- .../app/plugins/panel/heatmap/color_legend.ts | 17 +++++++++++------ public/sass/components/_panel_heatmap.scss | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 2d52acb8ddb..0cbbddd0317 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -52,7 +52,8 @@ module.directive('heatmapLegend', function() { }); function render() { - if (!_.isEmpty(ctrl.data)) { + clearLegend(elem); + if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) { let legendElem = $(elem).find('svg'); let legendWidth = Math.floor(legendElem.outerWidth()); @@ -131,6 +132,10 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue } function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { + if (legendWidth <= 0) { + return; + } + let legendElem = $(elem).find('svg'); let legend = d3.select(legendElem.get(0)); @@ -142,11 +147,11 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); let xAxis = d3.axisBottom(legendValueScale) .tickValues(ticks) - .tickSize(3); + .tickSize(2); - let legendElemHeight = legendElem.height(); - let posY = legendElemHeight - 23; - let posX = getSvgElemX(legendElem.find(":first-child")); + let colorRect = legendElem.find(":first-child"); + let posY = colorRect.height() + 2; + let posX = getSvgElemX(colorRect); d3.select(legendElem.get(0)).append("g") .attr("class", "axis") .attr("transform", "translate(" + posX + "," + posY + ")") @@ -257,7 +262,7 @@ function getSvgElemX(elem) { function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { let range = rangeTo - rangeFrom; let tickStepSize = tickStep(rangeFrom, rangeTo, 3); - let ticksNum = Math.floor(range / tickStepSize); + let ticksNum = Math.round(range / tickStepSize); let ticks = []; for (let i = 0; i < ticksNum; i++) { diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index e593f6e2bf8..3fa2e331c7b 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -62,7 +62,7 @@ svg { width: 100%; max-width: 300px; - height: 38px; + height: 33px; float: left; white-space: nowrap; padding-left: 10px; From b01a4e65839878b99c889bee37171d8366d5ca77 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Aug 2017 16:21:40 +0300 Subject: [PATCH 08/37] heatmap: use the same color for selection as for graph --- public/sass/components/_panel_heatmap.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index 3fa2e331c7b..134186f25a2 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -49,9 +49,8 @@ .heatmap-selection { stroke-width: 1; - opacity: 0.3; - fill: #828282; - stroke: darken($red,15%); + fill: rgba(102, 102, 102, 0.4); + stroke: rgba(102, 102, 102, 0.8); } .heatmap-legend-wrapper { From 663a3293eed813c746617dfeedac37ad55fcbde4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Aug 2017 16:23:01 +0300 Subject: [PATCH 09/37] heatmap: some legend fixes --- .../app/plugins/panel/heatmap/color_legend.ts | 58 ++++++++++--------- public/app/plugins/panel/heatmap/module.html | 4 +- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 0cbbddd0317..60bbe38d689 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -7,6 +7,10 @@ import {contextSrv} from 'app/core/core'; import {tickStep} from 'app/core/utils/ticks'; let module = angular.module('grafana.directives'); + +/** + * Color legend for heatmap editor. + */ module.directive('colorLegend', function() { return { restrict: 'E', @@ -38,6 +42,9 @@ module.directive('colorLegend', function() { }; }); +/** + * Heatmap legend with scale values. + */ module.directive('heatmapLegend', function() { return { restrict: 'E', @@ -54,19 +61,14 @@ module.directive('heatmapLegend', function() { function render() { clearLegend(elem); if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) { - let legendElem = $(elem).find('svg'); - let legendWidth = Math.floor(legendElem.outerWidth()); - - // let maxValue = ctrl.data.cardStats.max || legendWidth; - let rangeFrom = ctrl.data.cardStats.min; + let rangeFrom = 0; let rangeTo = ctrl.data.cardStats.max; let maxValue = panel.color.max || rangeTo; let minValue = panel.color.min || 0; if (panel.color.mode === 'spectrum') { let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); - let colorScale = getColorScale(colorScheme, maxValue, minValue); - drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue); + drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue); } else if (panel.color.mode === 'opacity') { let colorOptions = panel.color; drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); @@ -77,8 +79,9 @@ module.directive('heatmapLegend', function() { }; }); -function drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue) { +function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { let legendElem = $(elem).find('svg'); + let legend = d3.select(legendElem.get(0)); clearLegend(elem); let legendWidth = Math.floor(legendElem.outerWidth()) - 30; @@ -91,10 +94,10 @@ function drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValu let widthFactor = legendWidth / (rangeTo - rangeFrom); let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); - let legend = d3.select(legendElem.get(0)); - var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange); - - legendRects.enter().append("rect") + let colorScale = getColorScale(colorScheme, maxValue, minValue); + legend.selectAll(".heatmap-color-legend-rect") + .data(valuesRange) + .enter().append("rect") .attr("x", d => d * widthFactor) .attr("y", 0) .attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps @@ -107,6 +110,7 @@ function drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValu function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) { let legendElem = $(elem).find('svg'); + let legend = d3.select(legendElem.get(0)); clearLegend(elem); let legendWidth = Math.floor(legendElem.outerWidth()) - 30; @@ -115,30 +119,30 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue let rangeStep = 10; let widthFactor = legendWidth / (rangeTo - rangeFrom); let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); - let legend = d3.select(legendElem.get(0)); - var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); - let legendOpacityScale = getOpacityScale(options, maxValue, minValue); - legendRects.enter().append("rect") + let opacityScale = getOpacityScale(options, maxValue, minValue); + legend.selectAll(".heatmap-opacity-legend-rect") + .data(valuesRange) + .enter().append("rect") .attr("x", d => d * widthFactor) .attr("y", 0) .attr("width", rangeStep * widthFactor) .attr("height", legendHeight) .attr("stroke-width", 0) .attr("fill", options.cardColor) - .style("opacity", d => legendOpacityScale(d)); + .style("opacity", d => opacityScale(d)); - drawLegendValues(elem, legendOpacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); + drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); } function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { - if (legendWidth <= 0) { - return; - } - let legendElem = $(elem).find('svg'); let legend = d3.select(legendElem.get(0)); + if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) { + return; + } + let legendValueDomain = _.sortBy(colorScale.domain()); let legendValueScale = d3.scaleLinear() .domain([0, rangeTo]) @@ -185,11 +189,6 @@ function drawSimpleColorLegend(elem, colorScale) { } } -function clearLegend(elem) { - let legendElem = $(elem).find('svg'); - legendElem.empty(); -} - function drawSimpleOpacityLegend(elem, options) { let legendElem = $(elem).find('svg'); clearLegend(elem); @@ -225,6 +224,11 @@ function drawSimpleOpacityLegend(elem, options) { } } +function clearLegend(elem) { + let legendElem = $(elem).find('svg'); + legendElem.empty(); +} + function getColorScale(colorScheme, maxValue, minValue = 0) { let colorInterpolator = d3[colorScheme.value]; let colorScaleInverted = colorScheme.invert === 'always' || diff --git a/public/app/plugins/panel/heatmap/module.html b/public/app/plugins/panel/heatmap/module.html index 272aac99772..5b5c5296ca1 100644 --- a/public/app/plugins/panel/heatmap/module.html +++ b/public/app/plugins/panel/heatmap/module.html @@ -7,8 +7,8 @@
-
- +
+
From f7ea08dba710cfe950d94595a3bc4d4ece89b300 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 1 Aug 2017 16:51:55 +0300 Subject: [PATCH 10/37] heatmap: fix rendering tests --- .../plugins/panel/heatmap/specs/renderer_specs.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 5d3eb665e55..884ee1e81f4 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -11,8 +11,7 @@ import TimeSeries from 'app/core/time_series2'; import moment from 'moment'; import { Emitter } from 'app/core/core'; import rendering from '../rendering'; -import { convertToHeatMap } from '../heatmap_data_converter'; -// import d3 from 'd3'; +import {convertToHeatMap, convertToCards} from '../heatmap_data_converter'; describe('grafanaHeatmap', function () { @@ -115,8 +114,15 @@ describe('grafanaHeatmap', function () { let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); ctx.data.buckets = bucketsData; - // console.log("bucketsData", bucketsData); - // console.log("series", ctrl.panel.yAxis.logBase, ctx.series.length); + let cardsData = convertToCards(bucketsData); + let maxCardsValue = _.max(_.map(cardsData, 'count')); + let minCardsValue = _.min(_.map(cardsData, 'count')); + let cardStats = { + max: maxCardsValue, + min: minCardsValue + }; + ctx.data.cards = cardsData; + ctx.data.cardStats = cardStats; let elemHtml = `
From 6224a25e42edb9618bdbf5c69c5ec249adaa5342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Aug 2017 11:16:37 +0200 Subject: [PATCH 11/37] feat: Elasticsearch filtering wip, moved to typescript --- .../datasource/elasticsearch/datasource.js | 2 + .../elasticsearch/elastic_response.d.ts | 2 - .../elasticsearch/elastic_response.js | 350 ------------------ .../elasticsearch/elastic_response.ts | 346 +++++++++++++++++ .../specs/elastic_response_specs.ts | 2 +- 5 files changed, 349 insertions(+), 353 deletions(-) delete mode 100644 public/app/plugins/datasource/elasticsearch/elastic_response.d.ts delete mode 100644 public/app/plugins/datasource/elasticsearch/elastic_response.js create mode 100644 public/app/plugins/datasource/elasticsearch/elastic_response.ts diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 49398a894e7..5f3ce0fa361 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -11,6 +11,8 @@ define([ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) { 'use strict'; + ElasticResponse = ElasticResponse.ElasticResponse; + /** @ngInject */ function ElasticDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) { this.basicAuth = instanceSettings.basicAuth; diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.d.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.d.ts deleted file mode 100644 index c3318b8e133..00000000000 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare var test: any; -export default test; diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.js b/public/app/plugins/datasource/elasticsearch/elastic_response.js deleted file mode 100644 index 9e944774dc9..00000000000 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.js +++ /dev/null @@ -1,350 +0,0 @@ -define([ - "lodash", - "./query_def" -], -function (_, queryDef) { - 'use strict'; - - function ElasticResponse(targets, response) { - this.targets = targets; - this.response = response; - } - - ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) { - var metric, y, i, newSeries, bucket, value; - - for (y = 0; y < target.metrics.length; y++) { - metric = target.metrics[y]; - if (metric.hide) { - continue; - } - - switch(metric.type) { - case 'count': { - newSeries = { datapoints: [], metric: 'count', props: props}; - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - value = bucket.doc_count; - newSeries.datapoints.push([value, bucket.key]); - } - seriesList.push(newSeries); - break; - } - case 'percentiles': { - if (esAgg.buckets.length === 0) { - break; - } - - var firstBucket = esAgg.buckets[0]; - var percentiles = firstBucket[metric.id].values; - - for (var percentileName in percentiles) { - newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field}; - - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - var values = bucket[metric.id].values; - newSeries.datapoints.push([values[percentileName], bucket.key]); - } - seriesList.push(newSeries); - } - - break; - } - case 'extended_stats': { - for (var statName in metric.meta) { - if (!metric.meta[statName]) { - continue; - } - - newSeries = {datapoints: [], metric: statName, props: props, field: metric.field}; - - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - var stats = bucket[metric.id]; - - // add stats that are in nested obj to top level obj - stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; - stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; - - newSeries.datapoints.push([stats[statName], bucket.key]); - } - - seriesList.push(newSeries); - } - - break; - } - default: { - newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props}; - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - - value = bucket[metric.id]; - if (value !== undefined) { - if (value.normalized_value) { - newSeries.datapoints.push([value.normalized_value, bucket.key]); - } else { - newSeries.datapoints.push([value.value, bucket.key]); - } - } - - } - seriesList.push(newSeries); - break; - } - } - } - }; - - ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, docs, props) { - var metric, y, i, bucket, metricName, doc; - - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - doc = _.defaults({}, props); - doc[aggDef.field] = bucket.key; - - for (y = 0; y < target.metrics.length; y++) { - metric = target.metrics[y]; - - switch(metric.type) { - case "count": { - metricName = this._getMetricName(metric.type); - doc[metricName] = bucket.doc_count; - break; - } - case 'extended_stats': { - for (var statName in metric.meta) { - if (!metric.meta[statName]) { - continue; - } - - var stats = bucket[metric.id]; - // add stats that are in nested obj to top level obj - stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; - stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; - - metricName = this._getMetricName(statName); - doc[metricName] = stats[statName]; - } - break; - } - default: { - metricName = this._getMetricName(metric.type); - var otherMetrics = _.filter(target.metrics, {type: metric.type}); - - // if more of the same metric type include field field name in property - if (otherMetrics.length > 1) { - metricName += ' ' + metric.field; - } - - doc[metricName] = bucket[metric.id].value; - break; - } - } - } - - docs.push(doc); - } - }; - - // This is quite complex - // neeed to recurise down the nested buckets to build series - ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, docs, props, depth) { - var bucket, aggDef, esAgg, aggId; - var maxDepth = target.bucketAggs.length-1; - - for (aggId in aggs) { - aggDef = _.find(target.bucketAggs, {id: aggId}); - esAgg = aggs[aggId]; - - if (!aggDef) { - continue; - } - - if (depth === maxDepth) { - if (aggDef.type === 'date_histogram') { - this.processMetrics(esAgg, target, seriesList, props); - } else { - this.processAggregationDocs(esAgg, aggDef, target, docs, props); - } - } else { - for (var nameIndex in esAgg.buckets) { - bucket = esAgg.buckets[nameIndex]; - props = _.clone(props); - if (bucket.key !== void 0) { - props[aggDef.field] = bucket.key; - } else { - props["filter"] = nameIndex; - } - if (bucket.key_as_string) { - props[aggDef.field] = bucket.key_as_string; - } - this.processBuckets(bucket, target, seriesList, docs, props, depth+1); - } - } - } - }; - - ElasticResponse.prototype._getMetricName = function(metric) { - var metricDef = _.find(queryDef.metricAggTypes, {value: metric}); - if (!metricDef) { - metricDef = _.find(queryDef.extendedStats, {value: metric}); - } - - return metricDef ? metricDef.text : metric; - }; - - ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) { - var metricName = this._getMetricName(series.metric); - - if (target.alias) { - var regex = /\{\{([\s\S]+?)\}\}/g; - - return target.alias.replace(regex, function(match, g1, g2) { - var group = g1 || g2; - - if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; } - if (series.props[group] !== void 0) { return series.props[group]; } - if (group === 'metric') { return metricName; } - if (group === 'field') { return series.field; } - - return match; - }); - } - - if (series.field && queryDef.isPipelineAgg(series.metric)) { - var appliedAgg = _.find(target.metrics, { id: series.field }); - if (appliedAgg) { - metricName += ' ' + queryDef.describeMetric(appliedAgg); - } else { - metricName = 'Unset'; - } - } else if (series.field) { - metricName += ' ' + series.field; - } - - var propKeys = _.keys(series.props); - if (propKeys.length === 0) { - return metricName; - } - - var name = ''; - for (var propName in series.props) { - name += series.props[propName] + ' '; - } - - if (metricTypeCount === 1) { - return name.trim(); - } - - return name.trim() + ' ' + metricName; - }; - - ElasticResponse.prototype.nameSeries = function(seriesList, target) { - var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length; - var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length; - - for (var i = 0; i < seriesList.length; i++) { - var series = seriesList[i]; - series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount); - } - }; - - ElasticResponse.prototype.processHits = function(hits, seriesList) { - var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total}; - var propName, hit, doc, i; - - for (i = 0; i < hits.hits.length; i++) { - hit = hits.hits[i]; - doc = { - _id: hit._id, - _type: hit._type, - _index: hit._index - }; - - if (hit._source) { - for (propName in hit._source) { - doc[propName] = hit._source[propName]; - } - } - - for (propName in hit.fields) { - doc[propName] = hit.fields[propName]; - } - series.datapoints.push(doc); - } - - seriesList.push(series); - }; - - ElasticResponse.prototype.trimDatapoints = function(aggregations, target) { - var histogram = _.find(target.bucketAggs, { type: 'date_histogram'}); - - var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges; - if (shouldDropFirstAndLast) { - var trim = histogram.settings.trimEdges; - for(var prop in aggregations) { - var points = aggregations[prop]; - if (points.datapoints.length > trim * 2) { - points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim); - } - } - } - }; - - ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) { - var result = {}; - result.data = JSON.stringify(err, null, 4); - if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) { - result.message = err.root_cause[0].reason; - } else { - result.message = err.reason || 'Unkown elatic error response'; - } - - if (response.$$config) { - result.config = response.$$config; - } - - return result; - }; - - ElasticResponse.prototype.getTimeSeries = function() { - var seriesList = []; - - for (var i = 0; i < this.response.responses.length; i++) { - var response = this.response.responses[i]; - if (response.error) { - throw this.getErrorFromElasticResponse(this.response, response.error); - } - - if (response.hits && response.hits.hits.length > 0) { - this.processHits(response.hits, seriesList); - } - - if (response.aggregations) { - var aggregations = response.aggregations; - var target = this.targets[i]; - var tmpSeriesList = []; - var docs = []; - - this.processBuckets(aggregations, target, tmpSeriesList, docs, {}, 0); - this.trimDatapoints(tmpSeriesList, target); - this.nameSeries(tmpSeriesList, target); - - for (var y = 0; y < tmpSeriesList.length; y++) { - seriesList.push(tmpSeriesList[y]); - } - - if (seriesList.length === 0 && docs.length > 0) { - seriesList.push({target: 'docs', type: 'docs', datapoints: docs}); - } - } - } - - return { data: seriesList }; - }; - - return ElasticResponse; -}); diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts new file mode 100644 index 00000000000..04cfe20a9c1 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -0,0 +1,346 @@ +/// + +import _ from 'lodash'; +import queryDef from "./query_def"; + +export function ElasticResponse(targets, response) { + this.targets = targets; + this.response = response; +} + +ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) { + var metric, y, i, newSeries, bucket, value; + + for (y = 0; y < target.metrics.length; y++) { + metric = target.metrics[y]; + if (metric.hide) { + continue; + } + + switch (metric.type) { + case 'count': { + newSeries = { datapoints: [], metric: 'count', props: props}; + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + value = bucket.doc_count; + newSeries.datapoints.push([value, bucket.key]); + } + seriesList.push(newSeries); + break; + } + case 'percentiles': { + if (esAgg.buckets.length === 0) { + break; + } + + var firstBucket = esAgg.buckets[0]; + var percentiles = firstBucket[metric.id].values; + + for (var percentileName in percentiles) { + newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field}; + + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + var values = bucket[metric.id].values; + newSeries.datapoints.push([values[percentileName], bucket.key]); + } + seriesList.push(newSeries); + } + + break; + } + case 'extended_stats': { + for (var statName in metric.meta) { + if (!metric.meta[statName]) { + continue; + } + + newSeries = {datapoints: [], metric: statName, props: props, field: metric.field}; + + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + var stats = bucket[metric.id]; + + // add stats that are in nested obj to top level obj + stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; + stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; + + newSeries.datapoints.push([stats[statName], bucket.key]); + } + + seriesList.push(newSeries); + } + + break; + } + default: { + newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props}; + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + + value = bucket[metric.id]; + if (value !== undefined) { + if (value.normalized_value) { + newSeries.datapoints.push([value.normalized_value, bucket.key]); + } else { + newSeries.datapoints.push([value.value, bucket.key]); + } + } + + } + seriesList.push(newSeries); + break; + } + } + } +}; + +ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, docs, props) { + var metric, y, i, bucket, metricName, doc; + + for (i = 0; i < esAgg.buckets.length; i++) { + bucket = esAgg.buckets[i]; + doc = _.defaults({}, props); + doc[aggDef.field] = bucket.key; + + for (y = 0; y < target.metrics.length; y++) { + metric = target.metrics[y]; + + switch (metric.type) { + case "count": { + metricName = this._getMetricName(metric.type); + doc[metricName] = bucket.doc_count; + break; + } + case 'extended_stats': { + for (var statName in metric.meta) { + if (!metric.meta[statName]) { + continue; + } + + var stats = bucket[metric.id]; + // add stats that are in nested obj to top level obj + stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; + stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; + + metricName = this._getMetricName(statName); + doc[metricName] = stats[statName]; + } + break; + } + default: { + metricName = this._getMetricName(metric.type); + var otherMetrics = _.filter(target.metrics, {type: metric.type}); + + // if more of the same metric type include field field name in property + if (otherMetrics.length > 1) { + metricName += ' ' + metric.field; + } + + doc[metricName] = bucket[metric.id].value; + break; + } + } + } + + docs.push(doc); + } +}; + +// This is quite complex +// neeed to recurise down the nested buckets to build series +ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, docs, props, depth) { + var bucket, aggDef, esAgg, aggId; + var maxDepth = target.bucketAggs.length-1; + + for (aggId in aggs) { + aggDef = _.find(target.bucketAggs, {id: aggId}); + esAgg = aggs[aggId]; + + if (!aggDef) { + continue; + } + + if (depth === maxDepth) { + if (aggDef.type === 'date_histogram') { + this.processMetrics(esAgg, target, seriesList, props); + } else { + this.processAggregationDocs(esAgg, aggDef, target, docs, props); + } + } else { + for (var nameIndex in esAgg.buckets) { + bucket = esAgg.buckets[nameIndex]; + props = _.clone(props); + if (bucket.key !== void 0) { + props[aggDef.field] = bucket.key; + } else { + props["filter"] = nameIndex; + } + if (bucket.key_as_string) { + props[aggDef.field] = bucket.key_as_string; + } + this.processBuckets(bucket, target, seriesList, docs, props, depth+1); + } + } + } +}; + +ElasticResponse.prototype._getMetricName = function(metric) { + var metricDef = _.find(queryDef.metricAggTypes, {value: metric}); + if (!metricDef) { + metricDef = _.find(queryDef.extendedStats, {value: metric}); + } + + return metricDef ? metricDef.text : metric; +}; + +ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) { + var metricName = this._getMetricName(series.metric); + + if (target.alias) { + var regex = /\{\{([\s\S]+?)\}\}/g; + + return target.alias.replace(regex, function(match, g1, g2) { + var group = g1 || g2; + + if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; } + if (series.props[group] !== void 0) { return series.props[group]; } + if (group === 'metric') { return metricName; } + if (group === 'field') { return series.field; } + + return match; + }); + } + + if (series.field && queryDef.isPipelineAgg(series.metric)) { + var appliedAgg = _.find(target.metrics, { id: series.field }); + if (appliedAgg) { + metricName += ' ' + queryDef.describeMetric(appliedAgg); + } else { + metricName = 'Unset'; + } + } else if (series.field) { + metricName += ' ' + series.field; + } + + var propKeys = _.keys(series.props); + if (propKeys.length === 0) { + return metricName; + } + + var name = ''; + for (var propName in series.props) { + name += series.props[propName] + ' '; + } + + if (metricTypeCount === 1) { + return name.trim(); + } + + return name.trim() + ' ' + metricName; +}; + +ElasticResponse.prototype.nameSeries = function(seriesList, target) { + var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length; + var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length; + + for (var i = 0; i < seriesList.length; i++) { + var series = seriesList[i]; + series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount); + } +}; + +ElasticResponse.prototype.processHits = function(hits, seriesList) { + var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total}; + var propName, hit, doc, i; + + for (i = 0; i < hits.hits.length; i++) { + hit = hits.hits[i]; + doc = { + _id: hit._id, + _type: hit._type, + _index: hit._index + }; + + if (hit._source) { + for (propName in hit._source) { + doc[propName] = hit._source[propName]; + } + } + + for (propName in hit.fields) { + doc[propName] = hit.fields[propName]; + } + series.datapoints.push(doc); + } + + seriesList.push(series); +}; + +ElasticResponse.prototype.trimDatapoints = function(aggregations, target) { + var histogram = _.find(target.bucketAggs, { type: 'date_histogram'}); + + var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges; + if (shouldDropFirstAndLast) { + var trim = histogram.settings.trimEdges; + for (var prop in aggregations) { + var points = aggregations[prop]; + if (points.datapoints.length > trim * 2) { + points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim); + } + } + } +}; + +ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) { + var result: any = {}; + result.data = JSON.stringify(err, null, 4); + if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) { + result.message = err.root_cause[0].reason; + } else { + result.message = err.reason || 'Unkown elatic error response'; + } + + if (response.$$config) { + result.config = response.$$config; + } + + return result; +}; + +ElasticResponse.prototype.getTimeSeries = function() { + var seriesList = []; + + for (var i = 0; i < this.response.responses.length; i++) { + var response = this.response.responses[i]; + if (response.error) { + throw this.getErrorFromElasticResponse(this.response, response.error); + } + + if (response.hits && response.hits.hits.length > 0) { + this.processHits(response.hits, seriesList); + } + + if (response.aggregations) { + var aggregations = response.aggregations; + var target = this.targets[i]; + var tmpSeriesList = []; + var docs = []; + + this.processBuckets(aggregations, target, tmpSeriesList, docs, {}, 0); + this.trimDatapoints(tmpSeriesList, target); + this.nameSeries(tmpSeriesList, target); + + for (var y = 0; y < tmpSeriesList.length; y++) { + seriesList.push(tmpSeriesList[y]); + } + + if (seriesList.length === 0 && docs.length > 0) { + seriesList.push({target: 'docs', type: 'docs', datapoints: docs}); + } + } + } + + return { data: seriesList }; +}; + diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts index bd89055c3b4..2e70cf18e9a 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts @@ -1,6 +1,6 @@ import {describe, beforeEach, it, expect} from 'test/lib/common'; -import ElasticResponse from '../elastic_response'; +import {ElasticResponse} from '../elastic_response'; describe('ElasticResponse', function() { var targets; From ede827f5c05cd6f8d19686ed251d71a847dc893a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Aug 2017 12:15:39 +0200 Subject: [PATCH 12/37] feat: Elasticsearch change to how queries without date histogram are transformed into Grafana data stucture, now it is processed into a table structure instead of json structure --- public/app/core/table_model.ts | 9 +++ .../elasticsearch/elastic_response.ts | 60 ++++++++++++------- .../specs/elastic_response_specs.ts | 28 ++++----- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index f3d0b81998f..6b02e906583 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -3,9 +3,11 @@ export default class TableModel { columns: any[]; rows: any[]; type: string; + columnMap: any; constructor() { this.columns = []; + this.columnMap = {}; this.rows = []; this.type = 'table'; } @@ -36,4 +38,11 @@ export default class TableModel { this.columns[options.col].desc = false; } } + + addColumn(col) { + if (!this.columnMap[col.text]) { + this.columns.push(col); + this.columnMap[col.text] = col; + } + } } diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts index 04cfe20a9c1..a0416e8d91c 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import queryDef from "./query_def"; +import TableModel from 'app/core/table_model'; export function ElasticResponse(targets, response) { this.targets = targets; @@ -95,21 +96,35 @@ ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, p } }; -ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, docs, props) { - var metric, y, i, bucket, metricName, doc; +ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, table, props) { + // add columns + if (table.columns.length === 0) { + for (let propKey of _.keys(props)) { + table.addColumn({text: propKey}); + } + table.addColumn({text: aggDef.field}); + } - for (i = 0; i < esAgg.buckets.length; i++) { - bucket = esAgg.buckets[i]; - doc = _.defaults({}, props); - doc[aggDef.field] = bucket.key; + // helper func to add values to value array + let addMetricValue = (values, metricName, value) => { + table.addColumn({text: metricName}); + values.push(value); + }; - for (y = 0; y < target.metrics.length; y++) { - metric = target.metrics[y]; + for (let bucket of esAgg.buckets) { + let values = []; + for (let propValues of _.values(props)) { + values.push(propValues); + } + + // add bucket key (value) + values.push(bucket.key); + + for (let metric of target.metrics) { switch (metric.type) { case "count": { - metricName = this._getMetricName(metric.type); - doc[metricName] = bucket.doc_count; + addMetricValue(values, this._getMetricName(metric.type), bucket.doc_count); break; } case 'extended_stats': { @@ -123,33 +138,32 @@ ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, targe stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper; stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower; - metricName = this._getMetricName(statName); - doc[metricName] = stats[statName]; + addMetricValue(values, this._getMetricName(statName), stats[statName]); } break; } default: { - metricName = this._getMetricName(metric.type); - var otherMetrics = _.filter(target.metrics, {type: metric.type}); + let metricName = this._getMetricName(metric.type); + let otherMetrics = _.filter(target.metrics, {type: metric.type}); // if more of the same metric type include field field name in property if (otherMetrics.length > 1) { metricName += ' ' + metric.field; } - doc[metricName] = bucket[metric.id].value; + addMetricValue(values, metricName, bucket[metric.id].value); break; } } } - docs.push(doc); + table.rows.push(values); } }; // This is quite complex // neeed to recurise down the nested buckets to build series -ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, docs, props, depth) { +ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, table, props, depth) { var bucket, aggDef, esAgg, aggId; var maxDepth = target.bucketAggs.length-1; @@ -165,7 +179,7 @@ ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, do if (aggDef.type === 'date_histogram') { this.processMetrics(esAgg, target, seriesList, props); } else { - this.processAggregationDocs(esAgg, aggDef, target, docs, props); + this.processAggregationDocs(esAgg, aggDef, target, table, props); } } else { for (var nameIndex in esAgg.buckets) { @@ -179,7 +193,7 @@ ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, do if (bucket.key_as_string) { props[aggDef.field] = bucket.key_as_string; } - this.processBuckets(bucket, target, seriesList, docs, props, depth+1); + this.processBuckets(bucket, target, seriesList, table, props, depth+1); } } } @@ -325,9 +339,9 @@ ElasticResponse.prototype.getTimeSeries = function() { var aggregations = response.aggregations; var target = this.targets[i]; var tmpSeriesList = []; - var docs = []; + var table = new TableModel(); - this.processBuckets(aggregations, target, tmpSeriesList, docs, {}, 0); + this.processBuckets(aggregations, target, tmpSeriesList, table, {}, 0); this.trimDatapoints(tmpSeriesList, target); this.nameSeries(tmpSeriesList, target); @@ -335,8 +349,8 @@ ElasticResponse.prototype.getTimeSeries = function() { seriesList.push(tmpSeriesList[y]); } - if (seriesList.length === 0 && docs.length > 0) { - seriesList.push({target: 'docs', type: 'docs', datapoints: docs}); + if (table.rows.length > 0) { + seriesList.push(table); } } } diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts index 2e70cf18e9a..ff78a99e10e 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts @@ -387,10 +387,9 @@ describe('ElasticResponse', function() { result = new ElasticResponse(targets, response).getTimeSeries(); }); - it('should return docs with byte and count', function() { - expect(result.data[0].datapoints.length).to.be(3); - expect(result.data[0].datapoints[0].Count).to.be(1); - expect(result.data[0].datapoints[0].bytes).to.be(1000); + it('should return table with byte and count', function() { + expect(result.data[0].rows.length).to.be(3); + expect(result.data[0].columns).to.eql([{text: 'bytes'}, {text: 'Count'}]); }); }); @@ -530,14 +529,14 @@ describe('ElasticResponse', function() { it('should return table', function() { expect(result.data.length).to.be(1); - expect(result.data[0].type).to.be('docs'); - expect(result.data[0].datapoints.length).to.be(2); - expect(result.data[0].datapoints[0].host).to.be("server-1"); - expect(result.data[0].datapoints[0].Average).to.be(1000); - expect(result.data[0].datapoints[0].Count).to.be(369); + expect(result.data[0].type).to.be('table'); + expect(result.data[0].rows.length).to.be(2); + expect(result.data[0].rows[0][0]).to.be("server-1"); + expect(result.data[0].rows[0][1]).to.be(1000); + expect(result.data[0].rows[0][2]).to.be(369); - expect(result.data[0].datapoints[1].host).to.be("server-2"); - expect(result.data[0].datapoints[1].Average).to.be(2000); + expect(result.data[0].rows[1][0]).to.be("server-2"); + expect(result.data[0].rows[1][1]).to.be(2000); }); }); @@ -573,10 +572,9 @@ describe('ElasticResponse', function() { }); it('should include field in metric name', function() { - expect(result.data[0].type).to.be('docs'); - expect(result.data[0].datapoints[0].Average).to.be(undefined); - expect(result.data[0].datapoints[0]['Average test']).to.be(1000); - expect(result.data[0].datapoints[0]['Average test2']).to.be(3000); + expect(result.data[0].type).to.be('table'); + expect(result.data[0].rows[0][1]).to.be(1000); + expect(result.data[0].rows[0][2]).to.be(3000); }); }); From c17b5d13066445f9ef33529554d6e3e57bb32c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Aug 2017 12:34:36 +0200 Subject: [PATCH 13/37] table: minor table options improvement --- public/app/plugins/panel/table/editor.html | 10 ++++++++- public/app/plugins/panel/table/editor.ts | 24 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/table/editor.html b/public/app/plugins/panel/table/editor.html index 9854ac26dc3..36d78b09dfa 100644 --- a/public/app/plugins/panel/table/editor.html +++ b/public/app/plugins/panel/table/editor.html @@ -17,9 +17,17 @@ {{column.text}}
-
+
+
+ +
diff --git a/public/app/plugins/panel/table/editor.ts b/public/app/plugins/panel/table/editor.ts index 9850d60c9ad..a4fd72ed6f2 100644 --- a/public/app/plugins/panel/table/editor.ts +++ b/public/app/plugins/panel/table/editor.ts @@ -16,6 +16,8 @@ export class TablePanelEditorCtrl { fontSizes: any; addColumnSegment: any; getColumnNames: any; + canSetColumns: boolean; + columnsHelpMessage: string; /** @ngInject */ constructor($scope, private $q, private uiSegmentSrv) { @@ -24,8 +26,27 @@ export class TablePanelEditorCtrl { this.panel = this.panelCtrl.panel; this.transformers = transformers; this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%']; - this.addColumnSegment = uiSegmentSrv.newPlusButton(); + this.updateTransformHints(); + } + + updateTransformHints() { + this.canSetColumns = false; + this.columnsHelpMessage = ''; + + switch (this.panel.transform) { + case "timeseries_aggregations": { + this.canSetColumns = true; + break; + } + case "json": { + this.canSetColumns = true; + break; + } + case "table": { + this.columnsHelpMessage = "Columns and their order are determined by the data query"; + } + } } getColumnOptions() { @@ -57,6 +78,7 @@ export class TablePanelEditorCtrl { this.panel.columns.push({text: 'Avg', value: 'avg'}); } + this.updateTransformHints(); this.render(); } From a5d5f3d82fc96eabbe4937117827568c1e826ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Aug 2017 16:15:22 +0200 Subject: [PATCH 14/37] feat: add ad hoc filters directly from table panel cells, kibana 3 style, #8052 --- .../app/features/dashboard/ad_hoc_filters.ts | 6 +-- .../app/features/dashboard/submenu/submenu.ts | 5 +-- public/app/features/templating/editor_ctrl.ts | 10 ++--- .../templating/specs/variable_srv_specs.ts | 5 ++- .../app/features/templating/variable_srv.ts | 44 ++++++++++++++++--- .../elasticsearch/elastic_response.ts | 6 +-- .../specs/elastic_response_specs.ts | 2 +- public/app/plugins/panel/table/module.ts | 18 +++++++- public/app/plugins/panel/table/renderer.ts | 33 ++++++++++---- .../app/plugins/panel/table/transformers.ts | 12 ++++- public/sass/components/_panel_table.scss | 14 ++++++ 11 files changed, 119 insertions(+), 36 deletions(-) diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index f962f8ca2f4..47babfdd5dd 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -10,9 +10,10 @@ export class AdHocFiltersCtrl { removeTagFilterSegment: any; /** @ngInject */ - constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) { + constructor(private uiSegmentSrv, private datasourceSrv, private $q, private variableSrv, private $scope, private $rootScope) { this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'}); this.buildSegmentModel(); + this.$rootScope.onAppEvent('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope); } buildSegmentModel() { @@ -141,8 +142,7 @@ export class AdHocFiltersCtrl { } this.variable.setFilters(filters); - this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh'); + this.variableSrv.variableUpdated(this.variable, true); } } diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/submenu/submenu.ts index a925ba97f0d..0082f120b5f 100644 --- a/public/app/features/dashboard/submenu/submenu.ts +++ b/public/app/features/dashboard/submenu/submenu.ts @@ -22,10 +22,7 @@ export class SubmenuCtrl { } variableUpdated(variable) { - this.variableSrv.variableUpdated(variable).then(() => { - this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh'); - }); + this.variableSrv.variableUpdated(variable, true); } openEditView(editview) { diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 97274cb30d0..99c235ef24c 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -55,9 +55,8 @@ export class VariableEditorCtrl { $scope.add = function() { if ($scope.isValid()) { - $scope.variables.push($scope.current); + variableSrv.addVariable($scope.current); $scope.update(); - $scope.dashboard.updateSubmenuVisibility(); } }; @@ -114,9 +113,8 @@ export class VariableEditorCtrl { $scope.duplicate = function(variable) { var clone = _.cloneDeep(variable.getSaveModel()); $scope.current = variableSrv.createVariableFromModel(clone); - $scope.variables.push($scope.current); $scope.current.name = 'copy_of_'+variable.name; - $scope.dashboard.updateSubmenuVisibility(); + $scope.variableSrv.addVariable($scope.current); }; $scope.update = function() { @@ -150,9 +148,7 @@ export class VariableEditorCtrl { }; $scope.removeVariable = function(variable) { - var index = _.indexOf($scope.variables, variable); - $scope.variables.splice(index, 1); - $scope.dashboard.updateSubmenuVisibility(); + variableSrv.removeVariable(variable); }; } } diff --git a/public/app/features/templating/specs/variable_srv_specs.ts b/public/app/features/templating/specs/variable_srv_specs.ts index 85bca8d6068..9afb38056bd 100644 --- a/public/app/features/templating/specs/variable_srv_specs.ts +++ b/public/app/features/templating/specs/variable_srv_specs.ts @@ -22,6 +22,7 @@ describe('VariableSrv', function() { ctx.variableSrv.init({ templating: {list: []}, events: new Emitter(), + updateSubmenuVisibility: sinon.stub(), }); ctx.$rootScope.$digest(); })); @@ -41,7 +42,9 @@ describe('VariableSrv', function() { ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources); - scenario.variable = ctx.variableSrv.addVariable(scenario.variableModel); + scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); + ctx.variableSrv.addVariable(scenario.variable); + ctx.variableSrv.updateOptions(scenario.variable); ctx.$rootScope.$digest(); }); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 022203b9eb9..c147c5b6685 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -90,17 +90,24 @@ export class VariableSrv { return variable; } - addVariable(model) { - var variable = this.createVariableFromModel(model); + addVariable(variable) { this.variables.push(variable); - return variable; + this.templateSrv.updateTemplateData(); + this.dashboard.updateSubmenuVisibility(); + } + + removeVariable(variable) { + var index = _.indexOf(this.variables, variable); + this.variables.splice(index, 1); + this.templateSrv.updateTemplateData(); + this.dashboard.updateSubmenuVisibility(); } updateOptions(variable) { return variable.updateOptions(); } - variableUpdated(variable) { + variableUpdated(variable, emitChangeEvents?) { // if there is a variable lock ignore cascading update because we are in a boot up scenario if (variable.initLock) { return this.$q.when(); @@ -117,7 +124,12 @@ export class VariableSrv { } }); - return this.$q.all(promises); + return this.$q.all(promises).then(() => { + if (emitChangeEvents) { + this.$rootScope.$emit('template-variable-value-updated'); + this.$rootScope.$broadcast('refresh'); + } + }); } selectOptionsForCurrentValue(variable) { @@ -218,6 +230,28 @@ export class VariableSrv { // update url this.$location.search(params); } + + setAdhocFilter(options) { + var variable = _.find(this.variables, {type: 'adhoc', datasource: options.datasource}); + if (!variable) { + variable = this.createVariableFromModel({name: 'Filters', type: 'adhoc', datasource: options.datasource}); + this.addVariable(variable); + } + + let filters = variable.filters; + let filter = _.find(filters, {key: options.key, value: options.value}); + + if (!filter) { + filter = {key: options.key, value: options.value}; + filters.push(filter); + } + + filter.operator = options.operator; + + variable.setFilters(filters); + this.variableUpdated(variable, true); + } + } coreModule.service('variableSrv', VariableSrv); diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts index a0416e8d91c..47fa14a99a6 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -100,9 +100,9 @@ ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, targe // add columns if (table.columns.length === 0) { for (let propKey of _.keys(props)) { - table.addColumn({text: propKey}); + table.addColumn({text: propKey, filterable: true}); } - table.addColumn({text: aggDef.field}); + table.addColumn({text: aggDef.field, filterable: true}); } // helper func to add values to value array @@ -265,7 +265,7 @@ ElasticResponse.prototype.nameSeries = function(seriesList, target) { }; ElasticResponse.prototype.processHits = function(hits, seriesList) { - var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total}; + var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total, filterable: true}; var propName, hit, doc, i; for (i = 0; i < hits.hits.length; i++) { diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts index ff78a99e10e..fde876af4f7 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts @@ -389,7 +389,7 @@ describe('ElasticResponse', function() { it('should return table with byte and count', function() { expect(result.data[0].rows.length).to.be(3); - expect(result.data[0].columns).to.eql([{text: 'bytes'}, {text: 'Count'}]); + expect(result.data[0].columns).to.eql([{text: 'bytes', filterable: true}, {text: 'Count'}]); }); }); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 375d3a0a0ae..ba26e853a29 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -50,8 +50,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize) { + constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize, private variableSrv) { super($scope, $injector); + this.pageIndex = 0; if (this.panel.styles === void 0) { @@ -223,10 +224,25 @@ class TablePanelCtrl extends MetricsPanelCtrl { selector: '[data-link-tooltip]' }); + function addFilterClicked(e) { + let filterData = $(e.currentTarget).data(); + var options = { + datasource: panel.datasource, + key: data.columns[filterData.column].text, + value: data.rows[filterData.row][filterData.column], + operator: filterData.operator, + }; + + ctrl.variableSrv.setAdhocFilter(options); + console.log('clicked', options); + } + elem.on('click', '.table-panel-page-link', switchPage); + elem.on('click', '.table-panel-filter-link', addFilterClicked); var unbindDestroy = scope.$on('$destroy', function() { elem.off('click', '.table-panel-page-link'); + elem.off('click', '.table-panel-filter-link'); unbindDestroy(); }); diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index ef53b1d489b..02f0ceca72c 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -140,9 +140,12 @@ export class TableRenderer { renderCell(columnIndex, rowIndex, value, addWidthHack = false) { value = this.formatColumnValue(columnIndex, value); + + var column = this.table.columns[columnIndex]; var style = ''; var cellClasses = []; var cellClass = ''; + if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; this.colorState.cell = null; @@ -161,26 +164,25 @@ export class TableRenderer { if (value === undefined) { style = ' style="display:none;"'; - this.table.columns[columnIndex].hidden = true; + column.hidden = true; } else { - this.table.columns[columnIndex].hidden = false; + column.hidden = false; } - var columnStyle = this.table.columns[columnIndex].style; - if (columnStyle && columnStyle.preserveFormat) { + if (column.style && column.style.preserveFormat) { cellClasses.push("table-panel-cell-pre"); } - var columnHtml = value + widthHack; + var columnHtml = widthHack + value; - if (columnStyle && columnStyle.link) { + if (column.style && column.style.link) { // Render cell as link var scopedVars = this.renderRowVariables(rowIndex); scopedVars['__cell'] = { value: value }; - var cellLink = this.templateSrv.replace(columnStyle.linkUrl, scopedVars); - var cellLinkTooltip = this.templateSrv.replace(columnStyle.linkTooltip, scopedVars); - var cellTarget = columnStyle.linkTargetBlank ? '_blank' : ''; + var cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars); + var cellLinkTooltip = this.templateSrv.replace(column.style.linkTooltip, scopedVars); + var cellTarget = column.style.linkTargetBlank ? '_blank' : ''; cellClasses.push("table-panel-cell-link"); columnHtml = ` @@ -190,6 +192,19 @@ export class TableRenderer { `; } + if (column.filterable) { + cellClasses.push("table-panel-cell-filterable"); + columnHtml += ` + + + + + + `; + } + if (cellClasses.length) { cellClass = ' class="' + cellClasses.join(' ') + '"'; } diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 07d183eafa8..cb6d30e627d 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -185,8 +185,16 @@ transformers['json'] = { }, transform: function(data, panel, model) { var i, y, z; - for (i = 0; i < panel.columns.length; i++) { - model.columns.push({text: panel.columns[i].text}); + + for (let column of panel.columns) { + var tableCol: any = {text: column.text}; + + // if filterable data then set columns to filterable + if (data.length > 0 && data[0].filterable) { + tableCol.filterable = true; + } + + model.columns.push(tableCol); } if (model.columns.length === 0) { diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 073d8be4abf..cf81dca4465 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -91,9 +91,23 @@ &.cell-highlighted:hover { background-color: $tight-form-func-bg; } + + &:hover { + .table-panel-filter-link { + visibility: visible; + } + } } } +.table-panel-filter-link { + visibility: hidden; + color: $text-color-weak; + float: right; + display: block; + padding: 0 5px; +} + .table-panel-header-bg { background: $grafanaListAccent; border-top: 2px solid $body-bg; From a5bdfec0de702fe593c76bfb5559e5b57a712f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 Aug 2017 16:52:43 +0200 Subject: [PATCH 15/37] ES: return .raw fields in field lookups, closes #8975 --- .../plugins/datasource/elasticsearch/datasource.js | 11 +++++++++-- .../elasticsearch/specs/datasource_specs.ts | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 5f3ce0fa361..9ea679d5cc5 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -272,10 +272,17 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes var subObj = obj[key]; // Check mapping field for nested fields - if (subObj.hasOwnProperty('properties')) { + if (_.isObject(subObj.properties)) { fieldNameParts.push(key); getFieldsRecursively(subObj.properties); - } else { + } + + if (_.isObject(subObj.fields)) { + fieldNameParts.push(key); + getFieldsRecursively(subObj.fields); + } + + if (_.isString(subObj.type)) { var fieldName = fieldNameParts.concat(key).join('.'); // Hide meta-fields and check field type diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index d0133256982..0838dc33654 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts @@ -129,7 +129,10 @@ describe('ElasticDatasource', function() { '@timestamp': {type: 'date'}, beat: { properties: { - name: {type: 'string'}, + name: { + fields: {raw: {type: 'keyword'}}, + type: 'string' + }, hostname: {type: 'string'}, } }, @@ -169,6 +172,7 @@ describe('ElasticDatasource', function() { var fields = _.map(fieldObjects, 'text'); expect(fields).to.eql([ '@timestamp', + 'beat.name.raw', 'beat.name', 'beat.hostname', 'system.cpu.system', From c7e8b98d144ee492b10b2161c65d3bd09cf3e161 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 2 Aug 2017 21:00:05 +0300 Subject: [PATCH 16/37] heatmap: minor refactor, don't repeat cards stats calculation --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 10 ++-------- .../plugins/panel/heatmap/heatmap_data_converter.ts | 12 +++++++++++- .../plugins/panel/heatmap/specs/renderer_specs.ts | 10 ++-------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 3b8d1fa9d41..b564339673f 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -191,20 +191,14 @@ export class HeatmapCtrl extends MetricsPanelCtrl { yBucketSize = 1; } - let cardsData = convertToCards(bucketsData); - let maxCardsValue = _.max(_.map(cardsData, 'count')); - let minCardsValue = _.min(_.map(cardsData, 'count')); - let cardStats = { - max: maxCardsValue, - min: minCardsValue - }; + let {cards, cardStats} = convertToCards(bucketsData); this.data = { buckets: bucketsData, heatmapStats: heatmapStats, xBucketSize: xBucketSize, yBucketSize: yBucketSize, - cards: cardsData, + cards: cards, cardStats: cardStats }; } diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index ef405ea5508..993c32c0fca 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -51,6 +51,7 @@ function elasticHistogramToHeatmap(seriesList) { * @return {Array} Array of "card" objects */ function convertToCards(buckets) { + let min = 0, max = 0; let cards = []; _.forEach(buckets, xBucket => { _.forEach(xBucket.buckets, yBucket=> { @@ -62,10 +63,19 @@ function convertToCards(buckets) { count: yBucket.count, }; cards.push(card); + + if (cards.length === 1) { + min = yBucket.count; + max = yBucket.count; + } + + min = yBucket.count < min ? yBucket.count : min; + max = yBucket.count > max ? yBucket.count : max; }); }); - return cards; + let cardStats = {min, max}; + return {cards, cardStats}; } /** diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 884ee1e81f4..01d09a84228 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -114,14 +114,8 @@ describe('grafanaHeatmap', function () { let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); ctx.data.buckets = bucketsData; - let cardsData = convertToCards(bucketsData); - let maxCardsValue = _.max(_.map(cardsData, 'count')); - let minCardsValue = _.min(_.map(cardsData, 'count')); - let cardStats = { - max: maxCardsValue, - min: minCardsValue - }; - ctx.data.cards = cardsData; + let {cards, cardStats} = convertToCards(bucketsData); + ctx.data.cards = cards; ctx.data.cardStats = cardStats; let elemHtml = ` From 77b7f4b3767a447295778b9a0f84401a76e5b62c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 2 Aug 2017 21:27:10 +0300 Subject: [PATCH 17/37] heatmap: add unit tests for convertToCards() --- .../specs/heatmap_data_converter_specs.ts | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts index 03adf1f9fa3..a898a14ff10 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts @@ -3,7 +3,8 @@ import _ from 'lodash'; import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; import TimeSeries from 'app/core/time_series2'; -import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, isHeatmapDataEqual } from '../heatmap_data_converter'; +import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, + calculateBucketSize, isHeatmapDataEqual} from '../heatmap_data_converter'; describe('isHeatmapDataEqual', () => { let ctx: any = {}; @@ -244,6 +245,47 @@ describe('ES Histogram converter', () => { }); }); +describe('convertToCards', () => { + let buckets = {}; + + beforeEach(() => { + buckets = { + '1422774000000': { + x: 1422774000000, + buckets: { + '1': { y: 1, values: [1], count: 1, bounds: {} }, + '2': { y: 2, values: [2], count: 1, bounds: {} } + } + }, + '1422774060000': { + x: 1422774060000, + buckets: { + '2': { y: 2, values: [2, 3], count: 2, bounds: {} } + } + }, + }; + }); + + it('should build proper cards data', () => { + let expectedCards = [ + {x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {}}, + {x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {}}, + {x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {}} + ]; + let {cards, cardStats} = convertToCards(buckets); + expect(cards).to.eql(expectedCards); + }); + + it('should build proper cards stats', () => { + let expectedStats = { + min: 1, + max: 2 + }; + let {cards, cardStats} = convertToCards(buckets); + expect(cardStats).to.eql(expectedStats); + }); +}); + /** * Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss. * @param a From 54c79c564870bf4b36ce630e93513f485e598ab8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 7 Aug 2017 10:50:18 +0200 Subject: [PATCH 18/37] updated version to v4.4.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52de1007d0b..423f8df3349 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "4.4.2", + "version": "4.4.3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From def45f55da0e34775a31c6ec2951741f8f30e2d9 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 7 Aug 2017 13:11:57 +0200 Subject: [PATCH 19/37] docs: update installation for version 4.4.3 --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 10 +++++----- docs/sources/installation/windows.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index fb21f76b599..193fe4d596c 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,7 +15,7 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_4.4.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.0_amd64.deb) +Stable for Debian-based Linux | [grafana_4.4.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.3_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -23,9 +23,9 @@ installation. ## Install Stable ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.0_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.3_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_4.4.0_amd64.deb +sudo dpkg -i grafana_4.4.3_amd64.deb ```