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: '