Commit 022cbdda by Torkel Ödegaard

Merge branch 'master' into panel_edit_menu_poc

parents bc9989f9 ae2523aa
# 1.9.0 (unreleased)
- [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
- [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
# 1.8.1 (unreleased)
......
{
"version": "1.8.0",
"url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.0.tar.gz"
"version": "1.8.1",
"url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.1.tar.gz"
}
......@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "1.8.0",
"version": "1.8.1",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
......
......@@ -40,6 +40,7 @@ require.config({
'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack',
'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair',
modernizr: '../vendor/modernizr-2.6.1',
......@@ -83,6 +84,7 @@ require.config({
'jquery.flot.stack': ['jquery', 'jquery.flot'],
'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
'jquery.flot.time': ['jquery', 'jquery.flot'],
'jquery.flot.crosshair':['jquery', 'jquery.flot'],
'angular-cookies': ['angular'],
'angular-dragdrop': ['jquery','jquery-ui','angular'],
'angular-loader': ['angular'],
......
......@@ -54,7 +54,7 @@ function (_, kbn) {
}
};
TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
TimeSeries.prototype.getFlotPairs = function (fillStyle) {
var result = [];
this.color = this.info.color;
......@@ -100,21 +100,21 @@ function (_, kbn) {
}
if (result.length) {
this.info.avg = (this.info.total / result.length);
this.info.current = result[result.length-1][1];
var formater = kbn.getFormatFunction(yFormats[this.yaxis - 1], 2);
this.info.avg = this.info.avg != null ? formater(this.info.avg) : null;
this.info.current = this.info.current != null ? formater(this.info.current) : null;
this.info.min = this.info.min != null ? formater(this.info.min) : null;
this.info.max = this.info.max != null ? formater(this.info.max) : null;
this.info.total = this.info.total != null ? formater(this.info.total) : null;
}
return result;
};
TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) {
this.info.avg = this.info.avg != null ? formater(this.info.avg, decimals, scaledDecimals) : null;
this.info.current = this.info.current != null ? formater(this.info.current, decimals, scaledDecimals) : null;
this.info.min = this.info.min != null ? formater(this.info.min, decimals, scaledDecimals) : null;
this.info.max = this.info.max != null ? formater(this.info.max, decimals, scaledDecimals) : null;
this.info.total = this.info.total != null ? formater(this.info.total, decimals, scaledDecimals) : null;
};
return TimeSeries;
});
......@@ -3,9 +3,10 @@ define([
'jquery',
'kbn',
'moment',
'lodash'
'lodash',
'./grafanaGraph.tooltip'
],
function (angular, $, kbn, moment, _) {
function (angular, $, kbn, moment, _, graphTooltip) {
'use strict';
var module = angular.module('grafana.directives');
......@@ -15,8 +16,8 @@ function (angular, $, kbn, moment, _) {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, annotations;
var dashboard = scope.dashboard;
var data, annotations;
var legendSideLastValue = null;
scope.$on('refresh',function() {
......@@ -80,6 +81,18 @@ function (angular, $, kbn, moment, _) {
}
}
function updateLegendValues(plot) {
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[scope.panel.y_formats[series.yaxis - 1]];
series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals);
}
}
// Function for rendering panel
function render_panel() {
if (shouldAbortRender()) {
......@@ -91,6 +104,7 @@ function (angular, $, kbn, moment, _) {
// Populate element
var options = {
hooks: { draw: [updateLegendValues] },
legend: { show: false },
series: {
stackpercent: panel.stack ? panel.percentage : false,
......@@ -113,7 +127,8 @@ function (angular, $, kbn, moment, _) {
show: panel.points,
fill: 1,
fillColor: false,
radius: panel.pointradius
radius: panel.points ? panel.pointradius : 2
// little points when highlight points
},
shadowSize: 1
},
......@@ -130,6 +145,9 @@ function (angular, $, kbn, moment, _) {
selection: {
mode: "x",
color: '#666'
},
crosshair: {
mode: panel.tooltip.shared ? "x" : null
}
};
......@@ -299,7 +317,9 @@ function (angular, $, kbn, moment, _) {
}
function configureAxisMode(axis, format) {
axis.tickFormatter = kbn.getFormatFunction(format, 1);
axis.tickFormatter = function(val, axis) {
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
};
}
function time_format(interval, ticks, min, max) {
......@@ -324,40 +344,6 @@ function (angular, $, kbn, moment, _) {
return "%H:%M";
}
var $tooltip = $('<div id="tooltip">');
elem.bind("plothover", function (event, pos, item) {
var group, value, timestamp, seriesInfo, format;
if (item) {
seriesInfo = item.series.info;
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
if (seriesInfo.alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
seriesInfo.alias +
'</small><br>';
} else {
group = kbn.query_color_dot(item.series.color, 15) + ' ';
}
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
}
else {
value = item.datapoint[1];
}
value = kbn.getFormatFunction(format, 2)(value, item.series.yaxis);
timestamp = dashboard.formatDate(item.datapoint[0]);
$tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
} else {
$tooltip.detach();
}
});
function render_panel_as_graphite_png(url) {
url += '&width=' + elem.width();
url += '&height=' + elem.css('height').replace('px', '');
......@@ -408,6 +394,8 @@ function (angular, $, kbn, moment, _) {
elem.html('<img src="' + url + '"></img>');
}
graphTooltip.register(elem, dashboard, scope);
elem.bind("plotselected", function (event, ranges) {
scope.$apply(function() {
timeSrv.setTime({
......
define([
'jquery',
'kbn',
],
function ($, kbn) {
'use strict';
function registerTooltipFeatures(elem, dashboard, scope) {
var $tooltip = $('<div id="tooltip">');
elem.mouseleave(function () {
if(scope.panel.tooltip.shared) {
var plot = elem.data().plot;
$tooltip.detach();
plot.clearCrosshair();
plot.unhighlight();
}
});
function findHoverIndex(posX, series) {
for (var j = 0; j < series.data.length; j++) {
if (series.data[j][0] > posX) {
return Math.max(j - 1, 0);
}
}
return j - 1;
}
elem.bind("plothover", function (event, pos, item) {
var plot = elem.data().plot;
var data = plot.getData();
var group, value, timestamp, seriesInfo, format, i, series, hoverIndex, seriesHtml;
if (scope.panel.tooltip.shared) {
plot.unhighlight();
//check if all series has same length if so, only one x index will
//be checked and only for exact timestamp values
var pointCount = data[0].data.length;
for (i = 1; i < data.length; i++) {
if (data[i].data.length !== pointCount) {
console.log('WARNING: tootltip shared can not be shown becouse of series points do not align, different point counts');
$tooltip.detach();
return;
}
}
seriesHtml = '';
series = data[0];
hoverIndex = findHoverIndex(pos.x, series);
//now we know the current X (j) position for X and Y values
timestamp = dashboard.formatDate(series.data[hoverIndex][0]);
var last_value = 0; //needed for stacked values
for (i = data.length-1; i >= 0; --i) {
//stacked values should be added in reverse order
series = data[i];
seriesInfo = series.info;
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
value = kbn.valueFormats[format](value, series.yaxis.tickDecimals);
if (seriesInfo.alias) {
group = '<i class="icon-minus" style="color:' + series.color +';"></i> ' + seriesInfo.alias;
} else {
group = kbn.query_color_dot(series.color, 15) + ' ';
}
//pre-pending new values
seriesHtml = group + ': <span class="graph-tooltip-value">' + value + '</span><br>' + seriesHtml;
plot.highlight(i, hoverIndex);
}
$tooltip.html('<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ timestamp + '</div> ' + seriesHtml + '</div>')
.place_tt(pos.pageX + 20, pos.pageY);
return;
}
if (item) {
seriesInfo = item.series.info;
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
if (seriesInfo.alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
seriesInfo.alias +
'</small><br>';
} else {
group = kbn.query_color_dot(item.series.color, 15) + ' ';
}
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
}
else {
value = item.datapoint[1];
}
value = kbn.valueFormats[format](value, item.series.yaxis.tickDecimals);
timestamp = dashboard.formatDate(item.datapoint[0]);
$tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
} else {
$tooltip.detach();
}
});
}
return {
register: registerTooltipFeatures
};
});
......@@ -15,7 +15,8 @@ define([
'jquery.flot.selection',
'jquery.flot.time',
'jquery.flot.stack',
'jquery.flot.stackpercent'
'jquery.flot.stackpercent',
'jquery.flot.crosshair'
],
function (angular, app, $, _, kbn, moment, TimeSeries) {
'use strict';
......@@ -160,7 +161,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
tooltip : {
value_type: 'cumulative',
query_as_alias: true
shared: false,
},
targets: [{}],
......
......@@ -51,8 +51,16 @@
<input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
</div>
</div>
<div class="section">
<h5>Tooltip</h5>
<div class="editor-option">
<label class="small">shared <tip> Show all series values on the same time in the same tooltip and a x croshair to help follow all series</tip> </label><input type="checkbox" ng-model="panel.tooltip.shared" ng-checked="panel.tooltip.shared" ng-change="render()">
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
......
......@@ -215,9 +215,9 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
if (id === title) { return; }
var self = this;
self._getDashboardInternal(id, isTemp).then(function(dashboard) {
self._getDashboardInternal(title, isTemp).then(function(dashboard) {
if (dashboard !== null) {
self.deleteDashboard(id);
self.deleteDashboard(title);
}
});
};
......
......@@ -87,7 +87,7 @@ function (Settings) {
// Example: "1m", "1h"
playlist_timespan: "1m",
// If you want to specify password before saving, please specify it bellow
// If you want to specify password before saving, please specify it below
// The purpose of this password is not security, but to stop some users from accidentally changing dashboards
admin: {
password: ''
......
......@@ -444,6 +444,18 @@ select.grafana-target-segment-input {
max-width: 800px;
max-height: 600px;
overflow: hidden;
line-height: 14px;
}
.grafana-tooltip hr {
padding: 2px;
color: #c8c8c8;
margin: 0px;
border-bottom:0px solid #c8c8c8;
/*height:0px;
background-color: rgb(58, 57, 57);*/
}
.tooltip.in {
......
......@@ -166,3 +166,19 @@
float: left;
}
}
.graph-tooltip {
.graph-tooltip-time {
text-align: center;
font-weight: bold;
position: relative;
top: -3px;
}
.graph-tooltip-value {
font-weight: bold;
float: right;
padding-left: 10px;
}
}
......@@ -27,7 +27,10 @@ define([
legend: {},
grid: {},
y_formats: [],
seriesOverrides: []
seriesOverrides: [],
tooltip: {
shared: true
}
};
scope.hiddenSeries = {};
scope.dashboard = { timezone: 'browser' };
......
define([
'jquery',
'directives/grafanaGraph.tooltip'
], function($, tooltip) {
'use strict';
describe('graph tooltip', function() {
var elem = $('<div></div>');
var dashboard = {
formatDate: sinon.stub().returns('date'),
};
var scope = {
panel: {
tooltip: {
shared: true
},
y_formats: ['ms', 'none'],
}
};
var data = [
{
data: [[10,10], [12,20]],
info: { yaxis: 1 },
yaxis: { tickDecimals: 2 },
},
{
data: [[10,10], [12,20]],
info: { yaxis: 1 },
yaxis: { tickDecimals: 2 },
}
];
var plot = {
getData: sinon.stub().returns(data),
highlight: sinon.stub(),
unhighlight: sinon.stub()
};
elem.data('plot', plot);
beforeEach(function() {
tooltip.register(elem, dashboard, scope);
elem.trigger('plothover', [{}, {x: 13}, {}]);
});
it('should add tooltip', function() {
var tooltipHtml = $(".graph-tooltip").text();
expect(tooltipHtml).to.be('date : 40.00 ms : 20.00 ms');
});
});
});
......@@ -3,76 +3,31 @@ define([
], function(kbn) {
'use strict';
describe('millisecond formating', function() {
function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
it('should translate 4378634603 as 1.67 years', function() {
var str = kbn.msFormat(4378634603, 2);
expect(str).to.be('50.68 day');
describe('value format: ' + desc, function() {
it('should translate ' + value + ' as ' + result, function() {
var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
expect(str).to.be(result);
});
});
it('should translate 3654454 as 1.02 hour', function() {
var str = kbn.msFormat(3654454, 2);
expect(str).to.be('1.02 hour');
});
it('should not downscale when value is zero', function() {
var str = kbn.msFormat(0, 2);
expect(str).to.be('0.00 ms');
});
it('should translate 365445 as 6.09 min', function() {
var str = kbn.msFormat(365445, 2);
expect(str).to.be('6.09 min');
});
});
describe('high negative exponent, issue #696', function() {
it('should ignore decimal correction if exponent', function() {
var str = kbn.getFormatFunction('')(2.75e-10, { tickDecimals: 12 });
expect(str).to.be('2.75e-10');
});
it('should format 0 correctly', function() {
var str = kbn.getFormatFunction('')(0.0, { tickDecimals: 12 });
expect(str).to.be('0');
});
});
}
describe('none format tests', function() {
it('should translate 2 as 2.0000 if axis decimals is 4', function() {
var str = kbn.getFormatFunction('')(2, { tickDecimals: 4 });
expect(str).to.be('2.0000');
});
});
describe('nanosecond formatting', function () {
it('should translate 25 to 25 ns', function () {
var str = kbn.nanosFormat(25, 2);
expect(str).to.be("25 ns");
});
it('should translate 2558 to 2.56 µs', function () {
var str = kbn.nanosFormat(2558, 2);
expect(str).to.be("2.56 µs");
});
describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms');
describeValueFormat('ms', 100, 1, 0, '100 ms');
describeValueFormat('ms', 1250, 10, 0, '1.25 s');
describeValueFormat('ms', 1250, 300, 0, '1.3 s');
describeValueFormat('ms', 65150, 10000, 0, '1.1 min');
describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour');
describeValueFormat('ms', 651500000, 150000000, 0, '8 day');
it('should translate 2558000 to 2.56 ms', function () {
var str = kbn.nanosFormat(2558000, 2);
expect(str).to.be("2.56 ms");
});
it('should translate 2019962000 to 2.02 s', function () {
var str = kbn.nanosFormat(2049962000, 2);
expect(str).to.be("2.05 s");
});
describeValueFormat('none', 2.75e-10, 0, 10, '3e-10');
describeValueFormat('none', 0, 0, 2, '0');
it('should translate 95199620000 to 1.59 m', function () {
var str = kbn.nanosFormat(95199620000, 2);
expect(str).to.be("1.59 m");
});
});
describeValueFormat('ns', 25, 1, 0, '25 ns');
describeValueFormat('ns', 2558, 50, 0, '2.56 µs');
describe('calculateInterval', function() {
it('1h 100 resultion', function() {
......
......@@ -43,6 +43,7 @@ require.config({
'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack',
'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair',
modernizr: '../vendor/modernizr-2.6.1',
},
......@@ -77,6 +78,7 @@ require.config({
'jquery.flot.stack': ['jquery', 'jquery.flot'],
'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
'jquery.flot.time': ['jquery', 'jquery.flot'],
'jquery.flot.crosshair':['jquery', 'jquery.flot'],
'angular-route': ['angular'],
'angular-cookies': ['angular'],
......@@ -127,6 +129,7 @@ require([
'specs/influxdb-datasource-specs',
'specs/graph-ctrl-specs',
'specs/grafanaGraph-specs',
'specs/graph-tooltip-specs',
'specs/seriesOverridesCtrl-specs',
'specs/timeSrv-specs',
'specs/templateSrv-specs',
......
/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin supports these options:
crosshair: {
mode: null or "x" or "y" or "xy"
color: color
lineWidth: number
}
Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
crosshair that lets you trace the values on the x axis, "y" enables a
horizontal crosshair and "xy" enables them both. "color" is the color of the
crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
the drawn lines (default is 1).
The plugin also adds four public methods:
- setCrosshair( pos )
Set the position of the crosshair. Note that this is cleared if the user
moves the mouse. "pos" is in coordinates of the plot and should be on the
form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
axes), which is coincidentally the same format as what you get from a
"plothover" event. If "pos" is null, the crosshair is cleared.
- clearCrosshair()
Clear the crosshair.
- lockCrosshair(pos)
Cause the crosshair to lock to the current location, no longer updating if
the user moves the mouse. Optionally supply a position (passed on to
setCrosshair()) to move it to.
Example usage:
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
$("#graph").bind( "plothover", function ( evt, position, item ) {
if ( item ) {
// Lock the crosshair to the data point being hovered
myFlot.lockCrosshair({
x: item.datapoint[ 0 ],
y: item.datapoint[ 1 ]
});
} else {
// Return normal crosshair operation
myFlot.unlockCrosshair();
}
});
- unlockCrosshair()
Free the crosshair to move again after locking it.
*/
(function ($) {
var options = {
crosshair: {
mode: null, // one of null, "x", "y" or "xy",
color: "rgba(170, 0, 0, 0.80)",
lineWidth: 1
}
};
function init(plot) {
// position of crosshair in pixels
var crosshair = { x: -1, y: -1, locked: false };
plot.setCrosshair = function setCrosshair(pos) {
if (!pos)
crosshair.x = -1;
else {
var o = plot.p2c(pos);
crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
}
plot.triggerRedrawOverlay();
};
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
plot.lockCrosshair = function lockCrosshair(pos) {
if (pos)
plot.setCrosshair(pos);
crosshair.locked = true;
};
plot.unlockCrosshair = function unlockCrosshair() {
crosshair.locked = false;
};
function onMouseOut(e) {
if (crosshair.locked)
return;
if (crosshair.x != -1) {
crosshair.x = -1;
plot.triggerRedrawOverlay();
}
}
function onMouseMove(e) {
if (crosshair.locked)
return;
if (plot.getSelection && plot.getSelection()) {
crosshair.x = -1; // hide the crosshair while selecting
return;
}
var offset = plot.offset();
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
plot.triggerRedrawOverlay();
}
plot.hooks.bindEvents.push(function (plot, eventHolder) {
if (!plot.getOptions().crosshair.mode)
return;
eventHolder.mouseout(onMouseOut);
eventHolder.mousemove(onMouseMove);
});
plot.hooks.drawOverlay.push(function (plot, ctx) {
var c = plot.getOptions().crosshair;
if (!c.mode)
return;
var plotOffset = plot.getPlotOffset();
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
if (crosshair.x != -1) {
var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
ctx.strokeStyle = c.color;
ctx.lineWidth = c.lineWidth;
ctx.lineJoin = "round";
ctx.beginPath();
if (c.mode.indexOf("x") != -1) {
var drawX = Math.floor(crosshair.x) + adj;
ctx.moveTo(drawX, 0);
ctx.lineTo(drawX, plot.height());
}
if (c.mode.indexOf("y") != -1) {
var drawY = Math.floor(crosshair.y) + adj;
ctx.moveTo(0, drawY);
ctx.lineTo(plot.width(), drawY);
}
ctx.stroke();
}
ctx.restore();
});
plot.hooks.shutdown.push(function (plot, eventHolder) {
eventHolder.unbind("mouseout", onMouseOut);
eventHolder.unbind("mousemove", onMouseMove);
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'crosshair',
version: '1.0'
});
})(jQuery);
......@@ -1728,6 +1728,8 @@ Licensed under the MIT license.
axis.delta = delta;
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
axis.tickSize = opts.tickSize || size;
// grafana addition
axis.scaledDecimals = axis.tickDecimals - Math.floor(Math.log(axis.tickSize) / Math.LN10);
// Time mode was moved to a plug-in in 0.8, and since so many people use it
// we'll add an especially friendly reminder to make sure they included it.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment