diff --git a/public/app/core/core.ts b/public/app/core/core.ts index a1a93c3cdcc..c88e1f21136 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -52,6 +52,8 @@ import {gfPageDirective} from './components/gf_page'; import {orgSwitcher} from './components/org_switcher'; import {profiler} from './profiler'; import {registerAngularDirectives} from './angular_wrappers'; +import {updateLegendValues} from './time_series2'; +import TimeSeries from './time_series2'; import {searchResultsDirective} from './components/search/search_results'; export { @@ -85,5 +87,7 @@ export { geminiScrollbar, gfPageDirective, orgSwitcher, + TimeSeries, + updateLegendValues, searchResultsDirective }; diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 0f3dcbc1171..bf8a38761d3 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -1,4 +1,5 @@ import kbn from 'app/core/utils/kbn'; +import {getFlotTickDecimals} from 'app/core/utils/ticks'; import _ from 'lodash'; function matchSeriesOverride(aliasOrRegex, seriesAlias) { @@ -16,6 +17,43 @@ function translateFillOption(fill) { return fill === 0 ? 0.001 : fill/10; } +/** + * Calculate decimals for legend and update values for each series. + * @param data series data + * @param panel + * @param height Graph height + */ +export function updateLegendValues(data: TimeSeries[], panel, height) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + let yaxes = panel.yaxes; + let axis = yaxes[series.yaxis - 1]; + let {tickDecimals, scaledDecimals} = getFlotTickDecimals(data, axis, height); + let formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; + + // decimal override + if (_.isNumber(panel.decimals)) { + series.updateLegendValues(formater, panel.decimals, null); + } else { + // auto decimals + // legend and tooltip gets one more decimal precision + // than graph legend ticks + tickDecimals = (tickDecimals || -1) + 1; + series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); + } + } +} + +export function getDataMinMax(data: TimeSeries[]) { + const datamin = _.minBy(data, (series) => { + return series.stats.min; + }).stats.min; + const datamax = _.maxBy(data, (series: TimeSeries) => { + return series.stats.max; + }).stats.max; + return {datamin, datamax}; +} + export default class TimeSeries { datapoints: any; id: string; diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index b033e9247a1..334090c056f 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -1,3 +1,5 @@ +import {getDataMinMax} from 'app/core/time_series2'; + /** * Calculate tick step. * Implementation from d3-array (ticks.js) @@ -32,6 +34,7 @@ export function getScaledDecimals(decimals, tick_size) { /** * Calculate tick size based on min and max values, number of ticks and precision. + * Implementation from Flot. * @param min Axis minimum * @param max Axis maximum * @param noTicks Number of ticks @@ -65,3 +68,107 @@ export function getFlotTickSize(min: number, max: number, noTicks: number, tickD return size; } + +/** + * Calculate axis range (min and max). + * Implementation from Flot. + */ +export function getFlotRange(panelMin, panelMax, datamin, datamax) { + const autoscaleMargin = 0.02; + + let min = +(panelMin != null ? panelMin : datamin); + let max = +(panelMax != null ? panelMax : datamax); + let delta = max - min; + + if (delta === 0.0) { + // Grafana fix: wide Y min and max using increased wideFactor + // when all series values are the same + var wideFactor = 0.25; + var widen = Math.abs(max === 0 ? 1 : max * wideFactor); + + if (panelMin === null) { + min -= widen; + } + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (panelMax == null || panelMin != null) { + max += widen; + } + } else { + // consider autoscaling + var margin = autoscaleMargin; + if (margin != null) { + if (panelMin == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && datamin != null && datamin >= 0) { + min = 0; + } + } + if (panelMax == null) { + max += delta * margin; + if (max > 0 && datamax != null && datamax <= 0) { + max = 0; + } + } + } + } + return {min, max}; +} + +/** + * Estimate number of ticks for Y axis. + * Implementation from Flot. + */ +export function getFlotNumberOfTicks(height, ticks?) { + let noTicks; + if (typeof ticks === "number" && ticks > 0) { + noTicks = ticks; + } else { + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(height); + } + return noTicks; +} + +/** + * Calculate tick decimals. + * Implementation from Flot. + */ +export function getFlotTickDecimals(data, axis, height) { + let {datamin, datamax} = getDataMinMax(data); + let {min, max} = getFlotRange(axis.min, axis.max, datamin, datamax); + let noTicks = getFlotNumberOfTicks(height); + let tickDecimals, maxDec; + let delta = (max - min) / noTicks; + let dec = -Math.floor(Math.log(delta) / Math.LN10); + + let magn = Math.pow(10, -dec); + // norm is between 1.0 and 10.0 + let norm = delta / magn; + let size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + // grafana addition + const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); + return {tickDecimals, scaledDecimals}; +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index f8567d02fad..30761238d82 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import {EventManager} from 'app/features/annotations/all'; import {convertValuesToHistogram, getSeriesValues} from './histogram'; /** @ngInject **/ -function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { +function graphDirective(timeSrv, popoverSrv, contextSrv) { return { restrict: 'A', template: '', @@ -34,8 +34,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { var data; var plot; var sortedSeries; - var legendSideLastValue = null; - var rootScope = scope.$root; var panelWidth = 0; var eventManager = new EventManager(ctrl); var thresholdManager = new ThresholdManager(ctrl); @@ -53,17 +51,28 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { } }); - ctrl.events.on('render', function(renderData) { + /** + * Split graph rendering into two parts. + * First, calculate series stats in buildFlotPairs() function. Then legend rendering started + * (see ctrl.events.on('render') in legend.ts). + * When legend is rendered it emits 'legend-rendering-complete' and graph rendered. + */ + ctrl.events.on('render', (renderData) => { data = renderData || data; if (!data) { return; } annotations = ctrl.annotations || []; + buildFlotPairs(data); + ctrl.events.emit('render-legend'); + }); + + ctrl.events.on('legend-rendering-complete', () => { render_panel(); }); // global events - appEvents.on('graph-hover', function(evt) { + appEvents.on('graph-hover', (evt) => { // ignore other graph hover events if shared tooltip is disabled if (!dashboard.sharedTooltipModeEnabled()) { return; @@ -77,25 +86,32 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { tooltip.show(evt.pos); }, scope); - appEvents.on('graph-hover-clear', function(event, info) { + appEvents.on('graph-hover-clear', (event, info) => { if (plot) { tooltip.clear(plot); } }, scope); function getLegendHeight(panelHeight) { + const LEGEND_PADDING = 23; + if (!panel.legend.show || panel.legend.rightSide) { return 0; } - if (panel.legend.alignAsTable) { - var legendSeries = _.filter(data, function(series) { - return series.hideFromLegend(panel.legend) === false; - }); - var total = 23 + (21 * legendSeries.length); - return Math.min(total, Math.floor(panelHeight/2)); - } else { - return 26; + let legendHeight = getLegendContainerHeight() + LEGEND_PADDING; + return Math.min(legendHeight, Math.floor(panelHeight/2)); + } + + function getLegendContainerHeight() { + try { + let graphWrapperElem = elem.parent().parent(); + let legendElem = graphWrapperElem.find('.graph-legend-wrapper'); + let legendHeight = legendElem.height(); + return legendHeight; + } catch (e) { + console.log(e); + return 0; } } @@ -126,27 +142,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { } function drawHook(plot) { - // Update legend values - var yaxis = plot.getYAxes(); - for (var i = 0; i < data.length; i++) { - var series = data[i]; - var axis = yaxis[series.yaxis - 1]; - var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; - - // decimal override - if (_.isNumber(panel.decimals)) { - series.updateLegendValues(formater, panel.decimals, null); - } else { - // auto decimals - // legend and tooltip gets one more decimal precision - // than graph legend ticks - var tickDecimals = (axis.tickDecimals || -1) + 1; - series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2); - } - - if (!rootScope.$$phase) { scope.$digest(); } - } - // add left axis labels if (panel.yaxes[0].label && panel.yaxes[0].show) { $("
").text(panel.yaxes[0].label).appendTo(elem); @@ -207,7 +202,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { // Function for rendering panel function render_panel() { panelWidth = elem.width(); - if (shouldAbortRender()) { return; } @@ -218,10 +212,99 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { // un-check dashes if lines are unchecked panel.dashes = panel.lines ? panel.dashes : false; - var stack = panel.stack ? true : null; - // Populate element - var options: any = { + let options: any = buildFlotOptions(panel); + prepareXAxis(options, panel); + configureYAxisOptions(data, options); + thresholdManager.addFlotOptions(options, panel); + eventManager.addFlotEvents(annotations, options); + + sortedSeries = sortSeries(data, panel); + callPlot(options, true); + } + + function buildFlotPairs(data) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); + + // if hidden remove points and disable stack + if (ctrl.hiddenSeries[series.alias]) { + series.data = []; + series.stack = false; + } + } + } + + function prepareXAxis(options, panel) { + switch (panel.xaxis.mode) { + case 'series': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + + for (let i = 0; i < data.length; i++) { + let series = data[i]; + series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; + } + + addXSeriesAxis(options); + break; + } + case 'histogram': { + let bucketSize: number; + let values = getSeriesValues(data); + + if (data.length && values.length) { + let histMin = _.min(_.map(data, s => s.stats.min)); + let histMax = _.max(_.map(data, s => s.stats.max)); + let ticks = panel.xaxis.buckets || panelWidth / 50; + bucketSize = tickStep(histMin, histMax, ticks); + let histogram = convertValuesToHistogram(values, bucketSize); + data[0].data = histogram; + options.series.bars.barWidth = bucketSize * 0.8; + } else { + bucketSize = 0; + } + + addXHistogramAxis(options, bucketSize); + break; + } + case 'table': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + addXTableAxis(options); + break; + } + default: { + options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; + addTimeAxis(options); + break; + } + } + } + + function callPlot(options, incrementRenderCounter) { + try { + plot = $.plot(elem, sortedSeries, options); + if (ctrl.renderError) { + delete ctrl.error; + delete ctrl.inspector; + } + } catch (e) { + console.log('flotcharts error', e); + ctrl.error = e.message || "Render Error"; + ctrl.renderError = true; + ctrl.inspector = {error: e}; + } + + if (incrementRenderCounter) { + ctrl.renderingCompleted(); + } + } + + function buildFlotOptions(panel) { + const stack = panel.stack ? true : null; + let options = { hooks: { draw: [drawHook], processOffset: [processOffsetHook], @@ -278,96 +361,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { mode: 'x' } }; - - for (let i = 0; i < data.length; i++) { - let series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - - // if hidden remove points and disable stack - if (ctrl.hiddenSeries[series.alias]) { - series.data = []; - series.stack = false; - } - } - - switch (panel.xaxis.mode) { - case 'series': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - - for (let i = 0; i < data.length; i++) { - let series = data[i]; - series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; - } - - addXSeriesAxis(options); - break; - } - case 'histogram': { - let bucketSize: number; - let values = getSeriesValues(data); - - if (data.length && values.length) { - let histMin = _.min(_.map(data, s => s.stats.min)); - let histMax = _.max(_.map(data, s => s.stats.max)); - let ticks = panel.xaxis.buckets || panelWidth / 50; - bucketSize = tickStep(histMin, histMax, ticks); - let histogram = convertValuesToHistogram(values, bucketSize); - data[0].data = histogram; - options.series.bars.barWidth = bucketSize * 0.8; - } else { - bucketSize = 0; - } - - addXHistogramAxis(options, bucketSize); - break; - } - case 'table': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - addXTableAxis(options); - break; - } - default: { - options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; - addTimeAxis(options); - break; - } - } - - thresholdManager.addFlotOptions(options, panel); - eventManager.addFlotEvents(annotations, options); - configureAxisOptions(data, options); - - sortedSeries = sortSeries(data, ctrl.panel); - - function callPlot(incrementRenderCounter) { - try { - plot = $.plot(elem, sortedSeries, options); - if (ctrl.renderError) { - delete ctrl.error; - delete ctrl.inspector; - } - } catch (e) { - console.log('flotcharts error', e); - ctrl.error = e.message || "Render Error"; - ctrl.renderError = true; - ctrl.inspector = {error: e}; - } - - if (incrementRenderCounter) { - ctrl.renderingCompleted(); - } - } - - if (shouldDelayDraw(panel)) { - // temp fix for legends on the side, need to render twice to get dimensions right - callPlot(false); - setTimeout(function() { callPlot(true); }, 50); - legendSideLastValue = panel.legend.rightSide; - } else { - callPlot(true); - } + return options; } function sortSeries(series, panel) { @@ -410,16 +404,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { } } - function shouldDelayDraw(panel) { - if (panel.legend.rightSide) { - return true; - } - if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) { - return true; - } - return false; - } - function addTimeAxis(options) { var ticks = panelWidth / 100; var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf(); @@ -519,7 +503,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { }; } - function configureAxisOptions(data, options) { + function configureYAxisOptions(data, options) { var defaults = { position: 'left', show: panel.yaxes[0].show, diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js deleted file mode 100644 index 265efa05748..00000000000 --- a/public/app/plugins/panel/graph/legend.js +++ /dev/null @@ -1,215 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', -], -function (angular, _, $) { - 'use strict'; - - var module = angular.module('grafana.directives'); - - module.directive('graphLegend', function(popoverSrv, $timeout) { - - return { - link: function(scope, elem) { - var $container = $('
'); - var firstRender = true; - var ctrl = scope.ctrl; - var panel = ctrl.panel; - var data; - var seriesList; - var i; - - ctrl.events.on('render', function() { - data = ctrl.seriesList; - if (data) { - render(); - } - }); - - function getSeriesIndexForElement(el) { - return el.parents('[data-series-index]').data('series-index'); - } - - function openColorSelector(e) { - // if we clicked inside poup container ignore click - if ($(e.target).parents('.popover').length) { - return; - } - - var el = $(e.currentTarget).find('.fa-minus'); - var index = getSeriesIndexForElement(el); - var series = seriesList[index]; - - $timeout(function() { - popoverSrv.show({ - element: el[0], - position: 'bottom center', - template: '' + - '', - openOn: 'hover', - model: { - series: series, - toggleAxis: function() { - ctrl.toggleAxis(series); - }, - colorSelected: function(color) { - ctrl.changeSeriesColor(series, color); - } - }, - }); - }); - } - - function toggleSeries(e) { - var el = $(e.currentTarget); - var index = getSeriesIndexForElement(el); - var seriesInfo = seriesList[index]; - var scrollPosition = $($container.children('tbody')).scrollTop(); - ctrl.toggleSeries(seriesInfo, e); - $($container.children('tbody')).scrollTop(scrollPosition); - } - - function sortLegend(e) { - var el = $(e.currentTarget); - var stat = el.data('stat'); - - if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; } - - // if already sort ascending, disable sorting - if (panel.legend.sortDesc === false) { - panel.legend.sort = null; - panel.legend.sortDesc = null; - ctrl.render(); - return; - } - - panel.legend.sortDesc = !panel.legend.sortDesc; - panel.legend.sort = stat; - ctrl.render(); - } - - function getTableHeaderHtml(statName) { - if (!panel.legend[statName]) { return ""; } - var html = '' + statName; - - if (panel.legend.sort === statName) { - var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ; - html += ' '; - } - - return html + ''; - } - - function render() { - if (!ctrl.panel.legend.show) { - elem.empty(); - firstRender = true; - return; - } - - if (firstRender) { - elem.append($container); - $container.on('click', '.graph-legend-icon', openColorSelector); - $container.on('click', '.graph-legend-alias', toggleSeries); - $container.on('click', 'th', sortLegend); - firstRender = false; - } - - seriesList = data; - - $container.empty(); - - // Set min-width if side style and there is a value, otherwise remove the CSS propery - var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : ""; - $container.css("min-width", width); - - $container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); - - var tableHeaderElem; - if (panel.legend.alignAsTable) { - var header = ''; - header += ''; - if (panel.legend.values) { - header += getTableHeaderHtml('min'); - header += getTableHeaderHtml('max'); - header += getTableHeaderHtml('avg'); - header += getTableHeaderHtml('current'); - header += getTableHeaderHtml('total'); - } - header += ''; - tableHeaderElem = $(header); - } - - if (panel.legend.sort) { - seriesList = _.sortBy(seriesList, function(series) { - return series.stats[panel.legend.sort]; - }); - if (panel.legend.sortDesc) { - seriesList = seriesList.reverse(); - } - } - - var seriesShown = 0; - var seriesElements = []; - - for (i = 0; i < seriesList.length; i++) { - var series = seriesList[i]; - - if (series.hideFromLegend(panel.legend)) { - continue; - } - - var html = '
'; - html += '
'; - html += ''; - html += '
'; - - html += '' + series.aliasEscaped + ''; - - if (panel.legend.values) { - var avg = series.formatValue(series.stats.avg); - var current = series.formatValue(series.stats.current); - var min = series.formatValue(series.stats.min); - var max = series.formatValue(series.stats.max); - var total = series.formatValue(series.stats.total); - - if (panel.legend.min) { html += '
' + min + '
'; } - if (panel.legend.max) { html += '
' + max + '
'; } - if (panel.legend.avg) { html += '
' + avg + '
'; } - if (panel.legend.current) { html += '
' + current + '
'; } - if (panel.legend.total) { html += '
' + total + '
'; } - } - - html += '
'; - seriesElements.push($(html)); - - seriesShown++; - } - - if (panel.legend.alignAsTable) { - var maxHeight = ctrl.height; - - if (!panel.legend.rightSide) { - maxHeight = maxHeight/2; - } - - var topPadding = 6; - var tbodyElem = $(''); - tbodyElem.css("max-height", maxHeight - topPadding); - tbodyElem.append(tableHeaderElem); - tbodyElem.append(seriesElements); - $container.append(tbodyElem); - } else { - $container.append(seriesElements); - } - } - } - }; - }); - -}); diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts new file mode 100644 index 00000000000..7983910ffb3 --- /dev/null +++ b/public/app/plugins/panel/graph/legend.ts @@ -0,0 +1,238 @@ +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import PerfectScrollbar from 'perfect-scrollbar'; +import {updateLegendValues} from 'app/core/core'; + +var module = angular.module('grafana.directives'); + +module.directive('graphLegend', function(popoverSrv, $timeout) { + + return { + link: function(scope, elem) { + var $container = $('
'); + var firstRender = true; + var ctrl = scope.ctrl; + var panel = ctrl.panel; + var data; + var seriesList; + var i; + var legendScrollbar; + + ctrl.events.on('render-legend', () => { + data = ctrl.seriesList; + if (data) { + render(); + } + ctrl.events.emit('legend-rendering-complete'); + }); + + function updateLegendDecimals(graphHeight) { + updateLegendValues(data, panel, graphHeight); + } + + function getSeriesIndexForElement(el) { + return el.parents('[data-series-index]').data('series-index'); + } + + function openColorSelector(e) { + // if we clicked inside poup container ignore click + if ($(e.target).parents('.popover').length) { + return; + } + + var el = $(e.currentTarget).find('.fa-minus'); + var index = getSeriesIndexForElement(el); + var series = seriesList[index]; + + $timeout(function() { + popoverSrv.show({ + element: el[0], + position: 'bottom center', + template: '' + + '', + openOn: 'hover', + model: { + series: series, + toggleAxis: function() { + ctrl.toggleAxis(series); + }, + colorSelected: function(color) { + ctrl.changeSeriesColor(series, color); + } + }, + }); + }); + } + + function toggleSeries(e) { + var el = $(e.currentTarget); + var index = getSeriesIndexForElement(el); + var seriesInfo = seriesList[index]; + var scrollPosition = $($container.children('tbody')).scrollTop(); + ctrl.toggleSeries(seriesInfo, e); + $($container.children('tbody')).scrollTop(scrollPosition); + } + + function sortLegend(e) { + var el = $(e.currentTarget); + var stat = el.data('stat'); + + if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; } + + // if already sort ascending, disable sorting + if (panel.legend.sortDesc === false) { + panel.legend.sort = null; + panel.legend.sortDesc = null; + ctrl.render(); + return; + } + + panel.legend.sortDesc = !panel.legend.sortDesc; + panel.legend.sort = stat; + ctrl.render(); + } + + function getTableHeaderHtml(statName) { + if (!panel.legend[statName]) { return ""; } + var html = '' + statName; + + if (panel.legend.sort === statName) { + var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ; + html += ' '; + } + + return html + ''; + } + + function render() { + if (!ctrl.panel.legend.show) { + elem.empty(); + firstRender = true; + return; + } + + if (firstRender) { + elem.append($container); + $container.on('click', '.graph-legend-icon', openColorSelector); + $container.on('click', '.graph-legend-alias', toggleSeries); + $container.on('click', 'th', sortLegend); + firstRender = false; + } + + seriesList = data; + + $container.empty(); + + // Set min-width if side style and there is a value, otherwise remove the CSS propery + var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : ""; + $container.css("min-width", width); + + $container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); + + var tableHeaderElem; + if (panel.legend.alignAsTable) { + var header = ''; + header += ''; + if (panel.legend.values) { + header += getTableHeaderHtml('min'); + header += getTableHeaderHtml('max'); + header += getTableHeaderHtml('avg'); + header += getTableHeaderHtml('current'); + header += getTableHeaderHtml('total'); + } + header += ''; + tableHeaderElem = $(header); + } + + if (panel.legend.sort) { + seriesList = _.sortBy(seriesList, function(series) { + return series.stats[panel.legend.sort]; + }); + if (panel.legend.sortDesc) { + seriesList = seriesList.reverse(); + } + } + + // render first time for getting proper legend height + if (!panel.legend.rightSide) { + renderLegendElement(tableHeaderElem); + let graphHeight = ctrl.height - $container.height() - 23; + updateLegendDecimals(graphHeight); + $container.empty(); + } else { + updateLegendDecimals(ctrl.height); + } + + renderLegendElement(tableHeaderElem); + } + + function renderSeriesLegendElements() { + let seriesElements = []; + for (i = 0; i < seriesList.length; i++) { + var series = seriesList[i]; + + if (series.hideFromLegend(panel.legend)) { + continue; + } + + var html = '
'; + html += '
'; + html += ''; + html += '
'; + + html += '' + series.aliasEscaped + ''; + + if (panel.legend.values) { + var avg = series.formatValue(series.stats.avg); + var current = series.formatValue(series.stats.current); + var min = series.formatValue(series.stats.min); + var max = series.formatValue(series.stats.max); + var total = series.formatValue(series.stats.total); + + if (panel.legend.min) { html += '
' + min + '
'; } + if (panel.legend.max) { html += '
' + max + '
'; } + if (panel.legend.avg) { html += '
' + avg + '
'; } + if (panel.legend.current) { html += '
' + current + '
'; } + if (panel.legend.total) { html += '
' + total + '
'; } + } + + html += '
'; + seriesElements.push($(html)); + } + return seriesElements; + } + + function renderLegendElement(tableHeaderElem) { + var seriesElements = renderSeriesLegendElements(); + + if (panel.legend.alignAsTable) { + var maxHeight = ctrl.height; + + if (!panel.legend.rightSide) { + maxHeight = maxHeight/2; + } + + var topPadding = 6; + var tbodyElem = $(''); + tbodyElem.css("max-height", maxHeight - topPadding); + tbodyElem.append(tableHeaderElem); + tbodyElem.append(seriesElements); + $container.append(tbodyElem); + } else { + var maxLegendHeight = ctrl.height / 2; + $container.css("max-height", maxLegendHeight - 6); + $container.append(seriesElements); + if (!legendScrollbar) { + legendScrollbar = new PerfectScrollbar($container[0]); + } + legendScrollbar.update(); + } + } + } + }; +}); diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 17445dd3002..7d310b078c0 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -87,6 +87,8 @@ describe('grafanaGraph', function() { $.plot = ctx.plotSpy = sinon.spy(); ctrl.events.emit('render', ctx.data); + ctrl.events.emit('render-legend'); + ctrl.events.emit('legend-rendering-complete'); ctx.plotData = ctx.plotSpy.getCall(0).args[1]; ctx.plotOptions = ctx.plotSpy.getCall(0).args[2]; })); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index b5c0ec2c66a..fcf95d6d702 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -27,6 +27,7 @@ text-align: center; width: calc(100% - $spacer); padding-top: 6px; + position: relative; .popover-content { padding: 0;