mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'develop-graph-legend' into develop
This commit is contained in:
commit
9f87d8d344
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
233
public/app/plugins/panel/graph/legend.ts
Normal file
233
public/app/plugins/panel/graph/legend.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
@ -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];
|
||||
}));
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user