diff --git a/CHANGELOG.md b/CHANGELOG.md index 793a0740e4e..0c6d39929f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible - [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode. - [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger +- [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc) **Fixes** - [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: Fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13) diff --git a/package.json b/package.json index 31c552fe6c2..eecf826776e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "grunt-string-replace": "~0.2.4", "grunt-usemin": "^2.1.1", "jshint-stylish": "~0.1.5", - "karma": "~0.12.16", + "karma": "~0.12.21", "karma-chrome-launcher": "~0.1.4", "karma-coffee-preprocessor": "~0.1.2", "karma-coverage": "^0.2.5", diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index 71fe1b4b75a..46a1b8dfa79 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -231,36 +231,36 @@ function($, _, moment) { if (type === 0) { roundUp ? dateTime.endOf('year') : dateTime.startOf('year'); } else if (type === 1) { - dateTime.add('years',num); + dateTime.add(num, 'years'); } else if (type === 2) { - dateTime.subtract('years',num); + dateTime.subtract(num, 'years'); } break; case 'M': if (type === 0) { roundUp ? dateTime.endOf('month') : dateTime.startOf('month'); } else if (type === 1) { - dateTime.add('months',num); + dateTime.add(num, 'months'); } else if (type === 2) { - dateTime.subtract('months',num); + dateTime.subtract(num, 'months'); } break; case 'w': if (type === 0) { roundUp ? dateTime.endOf('week') : dateTime.startOf('week'); } else if (type === 1) { - dateTime.add('weeks',num); + dateTime.add(num, 'weeks'); } else if (type === 2) { - dateTime.subtract('weeks',num); + dateTime.subtract(num, 'weeks'); } break; case 'd': if (type === 0) { roundUp ? dateTime.endOf('day') : dateTime.startOf('day'); } else if (type === 1) { - dateTime.add('days',num); + dateTime.add(num, 'days'); } else if (type === 2) { - dateTime.subtract('days',num); + dateTime.subtract(num, 'days'); } break; case 'h': @@ -268,27 +268,27 @@ function($, _, moment) { if (type === 0) { roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour'); } else if (type === 1) { - dateTime.add('hours',num); + dateTime.add(num, 'hours'); } else if (type === 2) { - dateTime.subtract('hours',num); + dateTime.subtract(num,'hours'); } break; case 'm': if (type === 0) { roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute'); } else if (type === 1) { - dateTime.add('minutes',num); + dateTime.add(num, 'minutes'); } else if (type === 2) { - dateTime.subtract('minutes',num); + dateTime.subtract(num, 'minutes'); } break; case 's': if (type === 0) { roundUp ? dateTime.endOf('second') : dateTime.startOf('second'); } else if (type === 1) { - dateTime.add('seconds',num); + dateTime.add(num, 'seconds'); } else if (type === 2) { - dateTime.subtract('seconds',num); + dateTime.subtract(num, 'seconds'); } break; default: diff --git a/src/app/components/settings.js b/src/app/components/settings.js index 3edafc5a407..b43237224b4 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -19,7 +19,7 @@ function (_, crypto) { default_route : '/dashboard/file/default.json', playlist_timespan : "1m", unsaved_changes_warning : true, - search : { max_results: 20 }, + search : { max_results: 16 }, admin : {} }; diff --git a/src/app/panels/graph/timeSeries.js b/src/app/components/timeSeries.js similarity index 51% rename from src/app/panels/graph/timeSeries.js rename to src/app/components/timeSeries.js index f1794cd6a30..85c27f1da88 100644 --- a/src/app/panels/graph/timeSeries.js +++ b/src/app/components/timeSeries.js @@ -5,15 +5,57 @@ define([ function (_, kbn) { 'use strict'; - var ts = {}; - - ts.ZeroFilled = function (opts) { + function TimeSeries(opts) { this.datapoints = opts.datapoints; this.info = opts.info; this.label = opts.info.alias; + } + + function matchSeriesOverride(aliasOrRegex, seriesAlias) { + if (!aliasOrRegex) { return false; } + + if (aliasOrRegex[0] === '/') { + var match = aliasOrRegex.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + var regex = new RegExp(match[1], match[2]); + return seriesAlias.match(regex) != null; + } + + return aliasOrRegex === seriesAlias; + } + + function translateFillOption(fill) { + return fill === 0 ? 0.001 : fill/10; + } + + TimeSeries.prototype.applySeriesOverrides = function(overrides) { + this.lines = {}; + this.points = {}; + this.bars = {}; + this.info.yaxis = 1; + this.zindex = 0; + delete this.stack; + + for (var i = 0; i < overrides.length; i++) { + var override = overrides[i]; + if (!matchSeriesOverride(override.alias, this.info.alias)) { + continue; + } + if (override.lines !== void 0) { this.lines.show = override.lines; } + if (override.points !== void 0) { this.points.show = override.points; } + if (override.bars !== void 0) { this.bars.show = override.bars; } + if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); } + if (override.stack !== void 0) { this.stack = override.stack; } + if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; } + if (override.pointradius !== void 0) { this.points.radius = override.pointradius; } + if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; } + if (override.zindex !== void 0) { this.zindex = override.zindex; } + if (override.yaxis !== void 0) { + this.info.yaxis = override.yaxis; + } + } }; - ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle, yFormats) { + TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) { var result = []; this.color = this.info.color; @@ -74,5 +116,6 @@ function (_, kbn) { return result; }; - return ts; -}); \ No newline at end of file + return TimeSeries; + +}); diff --git a/src/app/controllers/dashboardCtrl.js b/src/app/controllers/dashboardCtrl.js index 5620b7305a2..7132ca8a571 100644 --- a/src/app/controllers/dashboardCtrl.js +++ b/src/app/controllers/dashboardCtrl.js @@ -13,7 +13,7 @@ function (angular, $, config, _) { module.controller('DashboardCtrl', function( $scope, $rootScope, dashboardKeybindings, filterSrv, dashboardSrv, dashboardViewStateSrv, - panelMoveSrv, timer) { + panelMoveSrv, timer, $timeout) { $scope.editor = { index: 0 }; $scope.panelNames = config.panels; @@ -21,6 +21,13 @@ function (angular, $, config, _) { $scope.init = function() { $scope.availablePanels = config.panels; $scope.onAppEvent('setup-dashboard', $scope.setupDashboard); + + angular.element(window).bind('resize', function() { + $timeout(function() { + $scope.$broadcast('render'); + }); + }); + }; $scope.setupDashboard = function(event, dashboardData) { diff --git a/src/app/directives/addGraphiteFunc.js b/src/app/directives/addGraphiteFunc.js index ca5943da508..6898b838845 100644 --- a/src/app/directives/addGraphiteFunc.js +++ b/src/app/directives/addGraphiteFunc.js @@ -97,4 +97,4 @@ function (angular, app, _, $, gfunc) { }; }); } -}); \ No newline at end of file +}); diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index a91967b967b..222bde78b26 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -46,11 +46,6 @@ function (angular, $, kbn, moment, _) { render_panel(); }); - // Re-render if the window is resized - angular.element(window).bind('resize', function() { - render_panel(); - }); - function setElementHeight() { try { var height = scope.height || scope.panel.height || scope.row.height; @@ -118,7 +113,7 @@ function (angular, $, kbn, moment, _) { lines: { show: panel.lines, zero: false, - fill: panel.fill === 0 ? 0.001 : panel.fill/10, + fill: translateFillOption(panel.fill), lineWidth: panel.linewidth, steps: panel.steppedLine }, @@ -154,11 +149,12 @@ function (angular, $, kbn, moment, _) { }; for (var i = 0; i < data.length; i++) { - var _d = data[i].getFlotPairs(panel.nullPointMode, panel.y_formats); - data[i].data = _d; + var series = data[i]; + series.applySeriesOverrides(panel.seriesOverrides); + series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats); } - if (panel.bars && data.length && data[0].info.timeStep) { + if (data.length && data[0].info.timeStep) { options.series.bars.barWidth = data[0].info.timeStep / 1.5; } @@ -167,21 +163,27 @@ function (angular, $, kbn, moment, _) { addAnnotations(options); configureAxisOptions(data, options); + var sortedSeries = _.sortBy(data, function(series) { return series.zindex; }); + // if legend is to the right delay plot draw a few milliseconds // so the legend width calculation can be done if (shouldDelayDraw(panel)) { legendSideLastValue = panel.legend.rightSide; setTimeout(function() { - plot = $.plot(elem, data, options); + plot = $.plot(elem, sortedSeries, options); addAxisLabels(); }, 50); } else { - plot = $.plot(elem, data, options); + plot = $.plot(elem, sortedSeries, options); addAxisLabels(); } } + function translateFillOption(fill) { + return fill === 0 ? 0.001 : fill/10; + } + function shouldDelayDraw(panel) { if (panel.legend.rightSide) { return true; diff --git a/src/app/panels/graph/legend.html b/src/app/panels/graph/legend.html index 5524a8047fe..0e5edc459ce 100755 --- a/src/app/panels/graph/legend.html +++ b/src/app/panels/graph/legend.html @@ -34,12 +34,12 @@
- -
-
+
diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 647e52c257e..6897cc090b0 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -1,16 +1,3 @@ -/** @scratch /panels/5 - * include::panels/histogram.asciidoc[] - */ - -/** @scratch /panels/histogram/0 - * == Histogram - * Status: *Stable* - * - * The histogram panel allow for the display of time charts. It includes several modes and tranformations - * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter - * fields. - * - */ define([ 'angular', 'app', @@ -18,7 +5,8 @@ define([ 'lodash', 'kbn', 'moment', - './timeSeries', + 'components/timeSeries', + './seriesOverridesCtrl', 'services/panelSrv', 'services/annotationsSrv', 'services/datasourceSrv', @@ -29,11 +17,10 @@ define([ 'jquery.flot.stack', 'jquery.flot.stackpercent' ], -function (angular, app, $, _, kbn, moment, timeSeries) { - +function (angular, app, $, _, kbn, moment, TimeSeries) { 'use strict'; - var module = angular.module('grafana.panels.graph', []); + var module = angular.module('grafana.panels.graph'); app.useModule(module); module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) { @@ -179,7 +166,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) { targets: [{}], aliasColors: {}, - aliasYAxis: {}, + + seriesOverrides: [], }; _.defaults($scope.panel,_d); @@ -258,18 +246,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) { var datapoints = seriesData.datapoints; var alias = seriesData.target; var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index]; - var yaxis = $scope.panel.aliasYAxis[alias] || 1; var seriesInfo = { alias: alias, color: color, - enable: true, - yaxis: yaxis }; $scope.legend.push(seriesInfo); - var series = new timeSeries.ZeroFilled({ + var series = new TimeSeries({ datapoints: datapoints, info: seriesInfo, }); @@ -347,8 +332,12 @@ function (angular, app, $, _, kbn, moment, timeSeries) { }; $scope.toggleYAxis = function(info) { - info.yaxis = info.yaxis === 2 ? 1 : 2; - $scope.panel.aliasYAxis[info.alias] = info.yaxis; + var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias }); + if (!override) { + override = { alias: info.alias }; + $scope.panel.seriesOverrides.push(override); + } + override.yaxis = info.yaxis === 2 ? 1 : 2; $scope.render(); }; @@ -357,6 +346,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) { $scope.render(); }; + $scope.addSeriesOverride = function() { + $scope.panel.seriesOverrides.push({}); + }; + + $scope.removeSeriesOverride = function(override) { + $scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override); + $scope.render(); + }; + panelSrv.init($scope); }); diff --git a/src/app/panels/graph/seriesOverridesCtrl.js b/src/app/panels/graph/seriesOverridesCtrl.js new file mode 100644 index 00000000000..4f54c6d0e6f --- /dev/null +++ b/src/app/panels/graph/seriesOverridesCtrl.js @@ -0,0 +1,80 @@ +define([ + 'angular', + 'app', + 'lodash', +], function(angular, app, _) { + 'use strict'; + + var module = angular.module('grafana.panels.graph', []); + app.useModule(module); + + module.controller('SeriesOverridesCtrl', function($scope) { + $scope.overrideMenu = []; + $scope.currentOverrides = []; + $scope.override = $scope.override || {}; + + $scope.addOverrideOption = function(name, propertyName, values) { + var option = {}; + option.text = name; + option.propertyName = propertyName; + option.index = $scope.overrideMenu.length; + option.values = values; + + option.submenu = _.map(values, function(value, index) { + return { + text: String(value), + click: 'setOverride(' + option.index + ',' + index + ')' + }; + }); + + $scope.overrideMenu.push(option); + }; + + $scope.setOverride = function(optionIndex, valueIndex) { + var option = $scope.overrideMenu[optionIndex]; + var value = option.values[valueIndex]; + $scope.override[option.propertyName] = value; + $scope.updateCurrentOverrides(); + $scope.render(); + }; + + $scope.removeOverride = function(option) { + delete $scope.override[option.propertyName]; + $scope.updateCurrentOverrides(); + $scope.render(); + }; + + $scope.getSeriesNames = function() { + return _.map($scope.legend, function(info) { + return info.alias; + }); + }; + + $scope.updateCurrentOverrides = function() { + $scope.currentOverrides = []; + _.each($scope.overrideMenu, function(option) { + var value = $scope.override[option.propertyName]; + if (_.isUndefined(value)) { return; } + $scope.currentOverrides.push({ + name: option.text, + propertyName: option.propertyName, + value: String(value) + }); + }); + }; + + $scope.addOverrideOption('Bars', 'bars', [true, false]); + $scope.addOverrideOption('Lines', 'lines', [true, false]); + $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]); + $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]); + $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); + $scope.addOverrideOption('Points', 'points', [true, false]); + $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]); + $scope.addOverrideOption('Stack', 'stack', [true, false]); + $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); + $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]); + $scope.updateCurrentOverrides(); + + }); + +}); diff --git a/src/app/panels/graph/styleEditor.html b/src/app/panels/graph/styleEditor.html index ec18121cba7..de7318d4500 100644 --- a/src/app/panels/graph/styleEditor.html +++ b/src/app/panels/graph/styleEditor.html @@ -1,5 +1,3 @@ - -
Chart Options
@@ -64,3 +62,49 @@
+ +
+
+
Series specific overrides Regex match example: /server[0-3]/i
+
+
+
+
+ +
    +
  • + +
  • +
+ +
    +
  • + alias or regex +
  • +
  • + +
  • +
  • + + {{option.name}}: {{option.value}} +
  • + +
+
+
+
+
+
+ + +
+
diff --git a/src/app/panels/timepicker/module.js b/src/app/panels/timepicker/module.js index e080096ae60..f00ad5f7a87 100644 --- a/src/app/panels/timepicker/module.js +++ b/src/app/panels/timepicker/module.js @@ -193,8 +193,8 @@ function (angular, app, _, moment, kbn) { moment(model.to.date).fromNow(); } else { - model.rangeString = $scope.dashboard.formatDate(model.from.date, 'MMM D, YYYY hh:mm:ss') + ' to ' + - $scope.dashboard.formatDate(model.to.date, 'MMM D, YYYY hh:mm:ss'); + model.rangeString = $scope.dashboard.formatDate(model.from.date, 'MMM D, YYYY HH:mm:ss') + ' to ' + + $scope.dashboard.formatDate(model.to.date, 'MMM D, YYYY HH:mm:ss'); } } diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index b49e528ffff..271b71cf5a4 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -22,7 +22,7 @@