From 1a3bc60e69c48e741202c60474574d9905d21a6c Mon Sep 17 00:00:00 2001 From: smalik Date: Fri, 1 Jul 2016 13:16:49 -0400 Subject: [PATCH] feat(dashed lines): Implementing dashed lines Adding support for dashed lines using jquery.flot.dashes.js --- public/app/core/time_series2.ts | 15 +- public/app/plugins/panel/graph/graph.ts | 11 +- public/app/plugins/panel/graph/module.ts | 6 + .../panel/graph/series_overrides_ctrl.js | 3 + .../plugins/panel/graph/specs/graph_specs.ts | 14 ++ .../app/plugins/panel/graph/tab_display.html | 18 +- public/app/system.conf.js | 3 +- public/dashboards/default.json | 3 + public/dashboards/template_vars.json | 3 + public/test/test-main.js | 1 + public/vendor/flot/jquery.flot.dashes.js | 236 ++++++++++++++++++ 11 files changed, 309 insertions(+), 4 deletions(-) create mode 100644 public/vendor/flot/jquery.flot.dashes.js diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index daf03f8f827..63b3dd3837b 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -35,6 +35,7 @@ export default class TimeSeries { isOutsideRange: boolean; lines: any; + dashes: any; bars: any; points: any; yaxis: any; @@ -61,6 +62,9 @@ export default class TimeSeries { applySeriesOverrides(overrides) { this.lines = {}; + this.dashes = { + dashLength: [] + }; this.points = {}; this.bars = {}; this.yaxis = 1; @@ -74,11 +78,20 @@ export default class TimeSeries { continue; } if (override.lines !== void 0) { this.lines.show = override.lines; } + if (override.dashes !== void 0) { + this.dashes.show = override.dashes; + this.lines.lineWidth = 0; + } 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.linewidth !== void 0) { + this.lines.lineWidth = override.linewidth; + this.dashes.lineWidth = override.linewidth; + } + if (override.dashLength !== void 0) { this.dashes.dashLength[0] = override.dashLength; } + if (override.spaceLength !== void 0) { this.dashes.dashLength[1] = override.spaceLength; } if (override.nullPointMode !== void 0) { this.nullPointMode = override.nullPointMode; } if (override.pointradius !== void 0) { this.points.radius = override.pointradius; } if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ca1b5021ab0..3d4fd4c7edb 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -7,6 +7,7 @@ import 'jquery.flot.stack'; import 'jquery.flot.stackpercent'; import 'jquery.flot.fillbelow'; import 'jquery.flot.crosshair'; +import 'jquery.flot.dashes'; import './jquery.flot.events'; import $ from 'jquery'; @@ -215,6 +216,9 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { // give space to alert editing thresholdManager.prepare(elem, data); + // un-check dashes if lines are unchecked + panel.dashes = panel.lines ? panel.dashes : false; + var stack = panel.stack ? true : null; // Populate element @@ -231,9 +235,14 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { show: panel.lines, zero: false, fill: translateFillOption(panel.fill), - lineWidth: panel.linewidth, + lineWidth: panel.dashes ? 0 : panel.linewidth, steps: panel.steppedLine }, + dashes: { + show: panel.dashes, + lineWidth: panel.linewidth, + dashLength: [panel.dashLength, panel.spaceLength] + }, bars: { show: panel.bars, fill: 1, diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 0a55fe0a9df..e2dce12168d 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -67,6 +67,12 @@ class GraphCtrl extends MetricsPanelCtrl { fill : 1, // line width in pixels linewidth : 1, + // show/hide dashed line + dashes : false, + // length of a dash + dashLength : 10, + // length of space between two dashes + spaceLength : 10, // show hide points points : false, // point radius in pixels diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.js b/public/app/plugins/panel/graph/series_overrides_ctrl.js index f5dae8e3c7e..945f8340b33 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.js +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.js @@ -100,6 +100,9 @@ define([ $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']); $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames()); $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); + $scope.addOverrideOption('Dashes', 'dashes', [true, false]); + $scope.addOverrideOption('Dash Length', 'dashLength', [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]); + $scope.addOverrideOption('Dash Space', 'spaceLength', [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]); $scope.addOverrideOption('Points', 'points', [true, false]); $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]); $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']); diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 0d8c088f704..161aca11e42 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -153,6 +153,20 @@ describe('grafanaGraph', function() { }); }); + graphScenario('dashed lines options', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.lines = true; + ctrl.panel.linewidth = 2; + ctrl.panel.dashes = true; + }); + + it('should configure dashed plot with correct options', function() { + expect(ctx.plotOptions.series.lines.show).to.be(true); + expect(ctx.plotOptions.series.dashes.lineWidth).to.be(2); + expect(ctx.plotOptions.series.dashes.show).to.be(true); + }); + }); + graphScenario('should use timeStep for barWidth', function(ctx) { ctx.setup(function(ctrl, data) { ctrl.panel.bars = true; diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index 8ace16c6078..9f8a90829ae 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -32,12 +32,28 @@ -
+
+ + +
+ +
+ +
+
+
+ +
+ +
+
diff --git a/public/app/system.conf.js b/public/app/system.conf.js index f7ed0c5b2d2..ae8e93a726a 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -31,7 +31,8 @@ System.config({ "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", - "d3": "vendor/d3/d3.js" + "d3": "vendor/d3/d3.js", + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes" }, packages: { diff --git a/public/dashboards/default.json b/public/dashboards/default.json index 931d4c6f802..b16b5d710d6 100644 --- a/public/dashboards/default.json +++ b/public/dashboards/default.json @@ -82,6 +82,9 @@ "lines": true, "fill": 1, "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, "points": false, "pointradius": 5, "bars": false, diff --git a/public/dashboards/template_vars.json b/public/dashboards/template_vars.json index a3c25d00371..f9ce493300c 100644 --- a/public/dashboards/template_vars.json +++ b/public/dashboards/template_vars.json @@ -50,6 +50,9 @@ "lines": true, "fill": 1, "linewidth": 1, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, "points": false, "pointradius": 5, "bars": false, diff --git a/public/test/test-main.js b/public/test/test-main.js index 6fba92cc109..50553b7c8b3 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -40,6 +40,7 @@ "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", "d3": "vendor/d3/d3.js", + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes" }, packages: { diff --git a/public/vendor/flot/jquery.flot.dashes.js b/public/vendor/flot/jquery.flot.dashes.js new file mode 100644 index 00000000000..5b204a5567b --- /dev/null +++ b/public/vendor/flot/jquery.flot.dashes.js @@ -0,0 +1,236 @@ +/* + * jQuery.flot.dashes + * + * options = { + * series: { + * dashes: { + * + * // show + * // default: false + * // Whether to show dashes for the series. + * show: , + * + * // lineWidth + * // default: 2 + * // The width of the dashed line in pixels. + * lineWidth: , + * + * // dashLength + * // default: 10 + * // Controls the length of the individual dashes and the amount of + * // space between them. + * // If this is a number, the dashes and spaces will have that length. + * // If this is an array, it is read as [ dashLength, spaceLength ] + * dashLength: or + * } + * } + * } + */ +(function($){ + + function init(plot) { + + plot.hooks.processDatapoints.push(function(plot, series, datapoints) { + + if (!series.dashes.show) return; + + plot.hooks.draw.push(function(plot, ctx) { + + var plotOffset = plot.getPlotOffset(), + axisx = series.xaxis, + axisy = series.yaxis; + + function plotDashes(xoffset, yoffset) { + + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, + prevy = null, + dashRemainder = 0, + dashOn = true, + dashOnLength, + dashOffLength; + + if (series.dashes.dashLength[0]) { + dashOnLength = series.dashes.dashLength[0]; + if (series.dashes.dashLength[1]) { + dashOffLength = series.dashes.dashLength[1]; + } else { + dashOffLength = dashOnLength; + } + } else { + dashOffLength = dashOnLength = series.dashes.dashLength; + } + + ctx.beginPath(); + + for (var i = ps; i < points.length; i += ps) { + + var x1 = points[i - ps], + y1 = points[i - ps + 1], + x2 = points[i], + y2 = points[i + 1]; + + if (x1 == null || x2 == null) continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) { + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + } + + var ax1 = axisx.p2c(x1) + xoffset, + ay1 = axisy.p2c(y1) + yoffset, + ax2 = axisx.p2c(x2) + xoffset, + ay2 = axisy.p2c(y2) + yoffset, + dashOffset; + + function lineSegmentOffset(segmentLength) { + + var c = Math.sqrt(Math.pow(ax2 - ax1, 2) + Math.pow(ay2 - ay1, 2)); + + if (c <= segmentLength) { + return { + deltaX: ax2 - ax1, + deltaY: ay2 - ay1, + distance: c, + remainder: segmentLength - c + } + } else { + var xsign = ax2 > ax1 ? 1 : -1, + ysign = ay2 > ay1 ? 1 : -1; + return { + deltaX: xsign * Math.sqrt(Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))), + deltaY: ysign * Math.sqrt(Math.pow(segmentLength, 2) - Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))), + distance: segmentLength, + remainder: 0 + }; + } + } + //-end lineSegmentOffset + + do { + + dashOffset = lineSegmentOffset( + dashRemainder > 0 ? dashRemainder : + dashOn ? dashOnLength : dashOffLength); + + if (dashOffset.deltaX != 0 || dashOffset.deltaY != 0) { + if (dashOn) { + ctx.lineTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY); + } else { + ctx.moveTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY); + } + } + + dashOn = !dashOn; + dashRemainder = dashOffset.remainder; + ax1 += dashOffset.deltaX; + ay1 += dashOffset.deltaY; + + } while (dashOffset.distance > 0); + + prevx = x2; + prevy = y2; + } + + ctx.stroke(); + } + //-end plotDashes + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = 'round'; + + var lw = series.dashes.lineWidth, + sw = series.shadowSize; + + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotDashes(Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2)); + ctx.lineWidth = sw/2; + plotDashes(Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4)); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + + if (lw > 0) { + plotDashes(0, 0); + } + + ctx.restore(); + + }); + //-end draw hook + + }); + //-end processDatapoints hook + + } + //-end init + + $.plot.plugins.push({ + init: init, + options: { + series: { + dashes: { + show: false, + lineWidth: 2, + dashLength: 10 + } + } + }, + name: 'dashes', + version: '0.1' + }); + +})(jQuery)