Merge branch 'develop-graph-legend' into develop

This commit is contained in:
Torkel Ödegaard 2017-12-08 11:47:50 +01:00
commit 9f87d8d344
10 changed files with 523 additions and 435 deletions

View File

@ -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
};

View File

@ -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;

View File

@ -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};
}

View File

@ -20,7 +20,7 @@ var panelTemplate = `
</div>
<div class="panel-content">
<ng-transclude></ng-transclude>
<ng-transclude class="panel-height-helper"></ng-transclude>
</div>
</div>

View File

@ -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) {
$("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>").text(panel.yaxes[0].label).appendTo(elem);
@ -157,6 +115,10 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
$("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>").text(panel.yaxes[1].label).appendTo(elem);
}
if (ctrl.dataWarning) {
$(`<div class="datapoints-warning flot-temp-elem">${ctrl.dataWarning.title}</div>`).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,

View File

@ -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 = $('<section class="graph-legend"></section>');
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: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
'</series-color-picker>',
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 = '<th class="pointer" data-stat="' + statName + '">' + statName;
if (panel.legend.sort === statName) {
var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ;
html += ' <span class="' + cssClass + '"></span>';
}
return html + '</th>';
}
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 = '<tr>';
header += '<th colspan="2" style="text-align:left"></th>';
if (panel.legend.values) {
header += getTableHeaderHtml('min');
header += getTableHeaderHtml('max');
header += getTableHeaderHtml('avg');
header += getTableHeaderHtml('current');
header += getTableHeaderHtml('total');
}
header += '</tr>';
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 = '<div class="graph-legend-series';
if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
html += '" data-series-index="' + i + '">';
html += '<div class="graph-legend-icon">';
html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
html += '</div>';
html += '<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>';
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 += '<div class="graph-legend-value min">' + min + '</div>'; }
if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
}
html += '</div>';
seriesElements.push($(html));
seriesShown++;
}
if (panel.legend.alignAsTable) {
var maxHeight = ctrl.height;
if (!panel.legend.rightSide) {
maxHeight = maxHeight/2;
}
var topPadding = 6;
var tbodyElem = $('<tbody></tbody>');
tbodyElem.css("max-height", maxHeight - topPadding);
tbodyElem.append(tableHeaderElem);
tbodyElem.append(seriesElements);
$container.append(tbodyElem);
} else {
$container.append(seriesElements);
}
}
}
};
});
});

View File

@ -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: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
'</series-color-picker>',
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 = '<th class="pointer" data-stat="' + statName + '">' + statName;
if (panel.legend.sort === statName) {
var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ;
html += ' <span class="' + cssClass + '"></span>';
}
return html + '</th>';
}
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 = '<tr>';
header += '<th colspan="2" style="text-align:left"></th>';
if (panel.legend.values) {
header += getTableHeaderHtml('min');
header += getTableHeaderHtml('max');
header += getTableHeaderHtml('avg');
header += getTableHeaderHtml('current');
header += getTableHeaderHtml('total');
}
header += '</tr>';
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 = '<div class="graph-legend-series';
if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
html += '" data-series-index="' + i + '">';
html += '<div class="graph-legend-icon">';
html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
html += '</div>';
html += '<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>';
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 += '<div class="graph-legend-value min">' + min + '</div>'; }
if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
}
html += '</div>';
seriesElements.push($(html));
}
return seriesElements;
}
function renderLegendElement(tableHeaderElem) {
var seriesElements = renderSeriesLegendElements();
if (panel.legend.alignAsTable) {
var tbodyElem = $('<tbody></tbody>');
tbodyElem.append(tableHeaderElem);
tbodyElem.append(seriesElements);
elem.append(tbodyElem);
} else {
elem.append(seriesElements);
if (!legendScrollbar) {
legendScrollbar = new PerfectScrollbar(elem[0]);
} else {
legendScrollbar.update();
}
}
}
}
};
});

View File

@ -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];
}));

View File

@ -1,22 +1,10 @@
var template = `
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
<div class="graph-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.dataWarning">
<span class="small" bs-tooltip="ctrl.dataWarning.tip">{{ctrl.dataWarning.title}}</span>
</div>
<div grafana-graph class="histogram-chart" ng-dblclick="ctrl.zoomOut()">
</div>
<div class="graph-panel" ng-class="{'graph-panel--legend-right': ctrl.panel.legend.rightSide}">
<div class="graph-panel__chart" grafana-graph ng-dblclick="ctrl.zoomOut()">
</div>
<div class="graph-legend-wrapper" graph-legend></div>
</div>
<div class="clearfix"></div>
<div class="graph-legend" graph-legend></div>
</div>
`;
export default template;

View File

@ -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,