diff --git a/public/app/core/core.ts b/public/app/core/core.ts
index b32651f418a..2c5c5653fb2 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';
import {manageDashboardsDirective} from './components/manage_dashboards/manage_dashboards';
@@ -86,6 +88,8 @@ export {
geminiScrollbar,
gfPageDirective,
orgSwitcher,
- searchResultsDirective,
- manageDashboardsDirective
+ manageDashboardsDirective,
+ TimeSeries,
+ updateLegendValues,
+ searchResultsDirective
};
diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts
index 0f3dcbc1171..5a242a5abc1 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,48 @@ 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
+ */
+export function updateLegendValues(data: TimeSeries[], panel) {
+ 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);
+ 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[]) {
+ let datamin = null;
+ let datamax = null;
+
+ for (let series of data) {
+ if (datamax === null || datamax < series.stats.max) {
+ datamax = series.stats.max;
+ }
+ if (datamin === null || datamin > series.stats.min) {
+ datamin = series.stats.min;
+ }
+ }
+
+ 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..8b16b528bdd 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,91 @@ 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};
+}
+
+/**
+ * Calculate tick decimals.
+ * Implementation from Flot.
+ */
+export function getFlotTickDecimals(data, axis) {
+ let {datamin, datamax} = getDataMinMax(data);
+ let {min, max} = getFlotRange(axis.min, axis.max, datamin, datamax);
+ let noTicks = 3;
+ 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/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts
index 0577c6fbfcc..323cd4e6f6b 100644
--- a/public/app/features/panel/panel_directive.ts
+++ b/public/app/features/panel/panel_directive.ts
@@ -20,7 +20,7 @@ var panelTemplate = `
-
+
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index f8567d02fad..9a67f24890e 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,47 +86,17 @@ 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) {
- 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;
- }
- }
-
- function setElementHeight() {
- try {
- var height = ctrl.height - getLegendHeight(ctrl.height);
- elem.css('height', height + 'px');
-
- return true;
- } catch (e) { // IE throws errors sometimes
- console.log(e);
- return false;
- }
- }
-
function shouldAbortRender() {
if (!data) {
return true;
}
- if (!setElementHeight()) { return true; }
-
if (panelWidth === 0) {
return true;
}
@@ -126,27 +105,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);
@@ -157,6 +115,10 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
$("").text(panel.yaxes[1].label).appendTo(elem);
}
+ if (ctrl.dataWarning) {
+ $(`${ctrl.dataWarning.title}
`).appendTo(elem);
+ }
+
thresholdManager.draw(plot);
}
@@ -207,7 +169,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
// Function for rendering panel
function render_panel() {
panelWidth = elem.width();
-
if (shouldAbortRender()) {
return;
}
@@ -218,10 +179,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 +328,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 +371,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 +470,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..e7589170301
--- /dev/null
+++ b/public/app/plugins/panel/graph/legend.ts
@@ -0,0 +1,233 @@
+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 firstRender = true;
+ var ctrl = scope.ctrl;
+ var panel = ctrl.panel;
+ var data;
+ var seriesList;
+ var i;
+ var legendScrollbar;
+
+ scope.$on("$destroy", function() {
+ if (!legendScrollbar) {
+ legendScrollbar.destroy();
+ }
+ });
+
+ ctrl.events.on('render-legend', () => {
+ data = ctrl.seriesList;
+ if (data) {
+ render();
+ }
+ ctrl.events.emit('legend-rendering-complete');
+ });
+
+ function updateLegendDecimals() {
+ updateLegendValues(data, panel);
+ }
+
+ 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 = $(elem.children('tbody')).scrollTop();
+ ctrl.toggleSeries(seriesInfo, e);
+ $(elem.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.on('click', '.graph-legend-icon', openColorSelector);
+ elem.on('click', '.graph-legend-alias', toggleSeries);
+ elem.on('click', 'th', sortLegend);
+ firstRender = false;
+ }
+
+ seriesList = data;
+
+ elem.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" : "";
+ elem.css("min-width", width);
+
+ elem.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);
+ updateLegendDecimals();
+ elem.empty();
+ } else {
+ updateLegendDecimals();
+ }
+
+ 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 tbodyElem = $('');
+ tbodyElem.append(tableHeaderElem);
+ tbodyElem.append(seriesElements);
+ elem.append(tbodyElem);
+ } else {
+ elem.append(seriesElements);
+
+ if (!legendScrollbar) {
+ legendScrollbar = new PerfectScrollbar(elem[0]);
+ } else {
+ 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/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts
index 6ded97ce33a..0b9eb8227df 100644
--- a/public/app/plugins/panel/graph/template.ts
+++ b/public/app/plugins/panel/graph/template.ts
@@ -1,22 +1,10 @@
var template = `
-
-
-
-
- {{ctrl.dataWarning.title}}
-
-
-
-
-
+
-
-
+
+
`;
export default template;
-
-
diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss
index b5c0ec2c66a..c28a608052d 100644
--- a/public/sass/components/_panel_graph.scss
+++ b/public/sass/components/_panel_graph.scss
@@ -1,10 +1,31 @@
-.graph-canvas-wrapper {
- position: relative;
- cursor: crosshair;
+.graph-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ &--legend-right {
+ flex-direction: row;
+
+ .graph-legend {
+ flex: 0 1 10px;
+ max-height: 100%;
+ }
+
+ .graph-legend-series {
+ display: block;
+ padding-left: 0px;
+ }
+
+ .graph-legend-table .graph-legend-series {
+ display: table-row;
+ }
+ }
}
-.histogram-chart {
+.graph-panel__chart {
position: relative;
+ cursor: crosshair;
+ flex-grow: 1;
}
.datapoints-warning {
@@ -22,11 +43,12 @@
}
.graph-legend {
- @include clearfix();
+ flex: 0 1 auto;
+ max-height: 30%;
margin: 0 $spacer;
text-align: center;
- width: calc(100% - $spacer);
padding-top: 6px;
+ position: relative;
.popover-content {
padding: 0;
@@ -89,7 +111,9 @@
display: block;
overflow-y: auto;
overflow-x: hidden;
+ height: 100%;
padding-bottom: 1px;
+ padding-right: 5px;
}
.graph-legend-series {
@@ -160,39 +184,6 @@
}
}
-.graph-legend-rightside {
-
- &.graph-wrapper {
- display: table;
- width: 100%;
- }
-
- .graph-canvas-wrapper {
- display: table-cell;
- width: 100%;
- position: relative;
- }
-
- .graph-legend-wrapper {
- display: table-cell;
- vertical-align: top;
- position: relative;
- left: 4px;
- }
-
- .graph-legend {
- margin: 0 0 0 1rem;
- }
-
- .graph-legend-series {
- display: block;
- padding-left: 0px;
- }
-
- .graph-legend-table .graph-legend-series {
- display: table-row;
- }
-}
.graph-legend-series-hidden {
.graph-legend-value,