Parcourir la source

Merge pull request #10994 from ilgizar/1271_share_zero

Marcus Efraimsson il y a 7 ans
Parent
commit
1ee0d1c296

+ 154 - 0
public/app/plugins/panel/graph/align_yaxes.ts

@@ -0,0 +1,154 @@
+import _ from 'lodash';
+
+/**
+ * To align two Y axes by Y level
+ * @param yAxes data [{min: min_y1, min: max_y1}, {min: min_y2, max: max_y2}]
+ * @param level Y level
+ */
+export function alignYLevel(yAxes, level) {
+  if (isNaN(level) || !checkCorrectAxis(yAxes)) {
+    return;
+  }
+
+  var [yLeft, yRight] = yAxes;
+  moveLevelToZero(yLeft, yRight, level);
+
+  expandStuckValues(yLeft, yRight);
+
+  // one of graphs on zero
+  var zero = yLeft.min === 0 || yRight.min === 0 || yLeft.max === 0 || yRight.max === 0;
+
+  var oneSide = checkOneSide(yLeft, yRight);
+
+  if (zero && oneSide) {
+    yLeft.min = yLeft.max > 0 ? 0 : yLeft.min;
+    yLeft.max = yLeft.max > 0 ? yLeft.max : 0;
+    yRight.min = yRight.max > 0 ? 0 : yRight.min;
+    yRight.max = yRight.max > 0 ? yRight.max : 0;
+  } else {
+    if (checkOppositeSides(yLeft, yRight)) {
+      if (yLeft.min >= 0) {
+        yLeft.min = -yLeft.max;
+        yRight.max = -yRight.min;
+      } else {
+        yLeft.max = -yLeft.min;
+        yRight.min = -yRight.max;
+      }
+    } else {
+      var rate = getRate(yLeft, yRight);
+
+      if (oneSide) {
+        // all graphs above the Y level
+        if (yLeft.min > 0) {
+          yLeft.min = yLeft.max / rate;
+          yRight.min = yRight.max / rate;
+        } else {
+          yLeft.max = yLeft.min / rate;
+          yRight.max = yRight.min / rate;
+        }
+      } else {
+        if (checkTwoCross(yLeft, yRight)) {
+          yLeft.min = yRight.min ? yRight.min * rate : yLeft.min;
+          yRight.min = yLeft.min ? yLeft.min / rate : yRight.min;
+          yLeft.max = yRight.max ? yRight.max * rate : yLeft.max;
+          yRight.max = yLeft.max ? yLeft.max / rate : yRight.max;
+        } else {
+          yLeft.min = yLeft.min > 0 ? yRight.min * rate : yLeft.min;
+          yRight.min = yRight.min > 0 ? yLeft.min / rate : yRight.min;
+          yLeft.max = yLeft.max < 0 ? yRight.max * rate : yLeft.max;
+          yRight.max = yRight.max < 0 ? yLeft.max / rate : yRight.max;
+        }
+      }
+    }
+  }
+
+  restoreLevelFromZero(yLeft, yRight, level);
+}
+
+function expandStuckValues(yLeft, yRight) {
+  // wide Y min and max using increased wideFactor
+  var wideFactor = 0.25;
+  if (yLeft.max === yLeft.min) {
+    yLeft.min -= wideFactor;
+    yLeft.max += wideFactor;
+  }
+  if (yRight.max === yRight.min) {
+    yRight.min -= wideFactor;
+    yRight.max += wideFactor;
+  }
+}
+
+function moveLevelToZero(yLeft, yRight, level) {
+  if (level !== 0) {
+    yLeft.min -= level;
+    yLeft.max -= level;
+    yRight.min -= level;
+    yRight.max -= level;
+  }
+}
+
+function restoreLevelFromZero(yLeft, yRight, level) {
+  if (level !== 0) {
+    yLeft.min += level;
+    yLeft.max += level;
+    yRight.min += level;
+    yRight.max += level;
+  }
+}
+
+function checkCorrectAxis(axis) {
+  return axis.length === 2 && checkCorrectAxes(axis[0]) && checkCorrectAxes(axis[1]);
+}
+
+function checkCorrectAxes(axes) {
+  return 'min' in axes && 'max' in axes;
+}
+
+function checkOneSide(yLeft, yRight) {
+  // on the one hand with respect to zero
+  return (yLeft.min >= 0 && yRight.min >= 0) || (yLeft.max <= 0 && yRight.max <= 0);
+}
+
+function checkTwoCross(yLeft, yRight) {
+  // both across zero
+  return yLeft.min <= 0 && yLeft.max >= 0 && yRight.min <= 0 && yRight.max >= 0;
+}
+
+function checkOppositeSides(yLeft, yRight) {
+  // on the opposite sides with respect to zero
+  return (yLeft.min >= 0 && yRight.max <= 0) || (yLeft.max <= 0 && yRight.min >= 0);
+}
+
+function getRate(yLeft, yRight) {
+  var rateLeft, rateRight, rate;
+  if (checkTwoCross(yLeft, yRight)) {
+    rateLeft = yRight.min ? yLeft.min / yRight.min : 0;
+    rateRight = yRight.max ? yLeft.max / yRight.max : 0;
+  } else {
+    if (checkOneSide(yLeft, yRight)) {
+      var absLeftMin = Math.abs(yLeft.min);
+      var absLeftMax = Math.abs(yLeft.max);
+      var absRightMin = Math.abs(yRight.min);
+      var absRightMax = Math.abs(yRight.max);
+      var upLeft = _.max([absLeftMin, absLeftMax]);
+      var downLeft = _.min([absLeftMin, absLeftMax]);
+      var upRight = _.max([absRightMin, absRightMax]);
+      var downRight = _.min([absRightMin, absRightMax]);
+
+      rateLeft = downLeft ? upLeft / downLeft : upLeft;
+      rateRight = downRight ? upRight / downRight : upRight;
+    } else {
+      if (yLeft.min > 0 || yRight.min > 0) {
+        rateLeft = yLeft.max / yRight.max;
+        rateRight = 0;
+      } else {
+        rateLeft = 0;
+        rateRight = yLeft.min / yRight.min;
+      }
+    }
+  }
+
+  rate = rateLeft > rateRight ? rateLeft : rateRight;
+
+  return rate;
+}

+ 17 - 2
public/app/plugins/panel/graph/axes_editor.html

@@ -11,6 +11,7 @@
 				<label class="gf-form-label width-6">Unit</label>
 				<div class="gf-form-dropdown-typeahead max-width-20" ng-model="yaxis.format" dropdown-typeahead2="ctrl.unitFormats" dropdown-typeahead-on-select="ctrl.setUnitFormat(yaxis, $subItem)"></div>
 			</div>
+		</div>
 
 			<div class="gf-form">
 				<label class="gf-form-label width-6">Scale</label>
@@ -28,8 +29,10 @@
 					<label class="gf-form-label width-6">Y-Max</label>
 					<input type="text" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="yaxis.max" ng-change="ctrl.render()" ng-model-onblur>
 				</div>
-      </div>
-      <div class="gf-form">
+			</div>
+
+		<div ng-if="yaxis.show">
+			<div class="gf-form">
 				<label class="gf-form-label width-6">Decimals</label>
 				<input type="number" class="gf-form-input max-width-20" placeholder="auto" empty-to-null bs-tooltip="'Override automatic decimal precision for y-axis'" data-placement="right" ng-model="yaxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
 			</div>
@@ -64,6 +67,18 @@
 			<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Number of buckets'" data-placement="right">
 		</div>
 
+		<div>
+			<br/>
+			<h5 class="section-heading">Y-Axes</h5>
+			<gf-form-switch class="gf-form" label="Align" tooltip="Align left and right Y-axes" label-class="width-6" switch-class="width-5" checked="ctrl.panel.yaxis.align" on-change="ctrl.render()"></gf-form-switch>
+			<div class="gf-form" ng-show="ctrl.panel.yaxis.align">
+				<label class="gf-form-label width-6">
+					Level
+				</label>
+				<input type="number" class="gf-form-input width-5" placeholder="0" ng-model="ctrl.panel.yaxis.alignLevel" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Alignment of Y-axes are based on this value, starting from Y=0'" data-placement="right">
+			</div>
+		</div>
+
 	</div>
 
 </div>

+ 12 - 0
public/app/plugins/panel/graph/graph.ts

@@ -18,6 +18,7 @@ import GraphTooltip from './graph_tooltip';
 import { ThresholdManager } from './threshold_manager';
 import { EventManager } from 'app/features/annotations/all';
 import { convertToHistogramData } from './histogram';
+import { alignYLevel } from './align_yaxes';
 import config from 'app/core/config';
 
 /** @ngInject **/
@@ -155,6 +156,16 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
         }
       }
 
+      function processRangeHook(plot) {
+        var yAxes = plot.getYAxes();
+        const align = panel.yaxis.align || false;
+
+        if (yAxes.length > 1 && align === true) {
+          const level = panel.yaxis.alignLevel || 0;
+          alignYLevel(yAxes, parseFloat(level));
+        }
+      }
+
       // Series could have different timeSteps,
       // let's find the smallest one so that bars are correctly rendered.
       // In addition, only take series which are rendered as bars for this.
@@ -294,6 +305,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
           hooks: {
             draw: [drawHook],
             processOffset: [processOffsetHook],
+            processRange: [processRangeHook],
           },
           legend: { show: false },
           series: {

+ 4 - 0
public/app/plugins/panel/graph/module.ts

@@ -55,6 +55,10 @@ class GraphCtrl extends MetricsPanelCtrl {
       values: [],
       buckets: null,
     },
+    yaxis: {
+      align: false,
+      alignLevel: null,
+    },
     // show/hide lines
     lines: true,
     // fill factor

+ 210 - 0
public/app/plugins/panel/graph/specs/align_yaxes.jest.ts

@@ -0,0 +1,210 @@
+import { alignYLevel } from '../align_yaxes';
+
+describe('Graph Y axes aligner', function() {
+  let yaxes, expected;
+  let alignY = 0;
+
+  describe('on the one hand with respect to zero', () => {
+    it('Should shrink Y axis', () => {
+      yaxes = [{ min: 5, max: 10 }, { min: 2, max: 3 }];
+      expected = [{ min: 5, max: 10 }, { min: 1.5, max: 3 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axis', () => {
+      yaxes = [{ min: 2, max: 3 }, { min: 5, max: 10 }];
+      expected = [{ min: 1.5, max: 3 }, { min: 5, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axis', () => {
+      yaxes = [{ min: -10, max: -5 }, { min: -3, max: -2 }];
+      expected = [{ min: -10, max: -5 }, { min: -3, max: -1.5 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axis', () => {
+      yaxes = [{ min: -3, max: -2 }, { min: -10, max: -5 }];
+      expected = [{ min: -3, max: -1.5 }, { min: -10, max: -5 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('on the opposite sides with respect to zero', () => {
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -3, max: -1 }, { min: 5, max: 10 }];
+      expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: 1, max: 3 }, { min: -10, max: -5 }];
+      expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('both across zero', () => {
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -10, max: 5 }, { min: -2, max: 3 }];
+      expected = [{ min: -10, max: 15 }, { min: -2, max: 3 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -5, max: 10 }, { min: -3, max: 2 }];
+      expected = [{ min: -15, max: 10 }, { min: -3, max: 2 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('one of graphs on zero', () => {
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: 0, max: 3 }, { min: 5, max: 10 }];
+      expected = [{ min: 0, max: 3 }, { min: 0, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: 5, max: 10 }, { min: 0, max: 3 }];
+      expected = [{ min: 0, max: 10 }, { min: 0, max: 3 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -3, max: 0 }, { min: -10, max: -5 }];
+      expected = [{ min: -3, max: 0 }, { min: -10, max: 0 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -10, max: -5 }, { min: -3, max: 0 }];
+      expected = [{ min: -10, max: 0 }, { min: -3, max: 0 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('both graphs on zero', () => {
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: 0, max: 3 }, { min: -10, max: 0 }];
+      expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -3, max: 0 }, { min: 0, max: 10 }];
+      expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('mixed placement of graphs relative to zero', () => {
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -10, max: 5 }, { min: 1, max: 3 }];
+      expected = [{ min: -10, max: 5 }, { min: -6, max: 3 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: 1, max: 3 }, { min: -10, max: 5 }];
+      expected = [{ min: -6, max: 3 }, { min: -10, max: 5 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -10, max: 5 }, { min: -3, max: -1 }];
+      expected = [{ min: -10, max: 5 }, { min: -3, max: 1.5 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      yaxes = [{ min: -3, max: -1 }, { min: -10, max: 5 }];
+      expected = [{ min: -3, max: 1.5 }, { min: -10, max: 5 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('on level not zero', () => {
+    it('Should shrink Y axis', () => {
+      alignY = 1;
+      yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }];
+      expected = [{ min: 4, max: 10 }, { min: 2, max: 4 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      alignY = 2;
+      yaxes = [{ min: -3, max: 1 }, { min: 5, max: 10 }];
+      expected = [{ min: -3, max: 7 }, { min: -6, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      alignY = -1;
+      yaxes = [{ min: -5, max: 5 }, { min: -2, max: 3 }];
+      expected = [{ min: -5, max: 15 }, { min: -2, max: 3 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+
+    it('Should shrink Y axes', () => {
+      alignY = -2;
+      yaxes = [{ min: -2, max: 3 }, { min: 5, max: 10 }];
+      expected = [{ min: -2, max: 3 }, { min: -2, max: 10 }];
+
+      alignYLevel(yaxes, alignY);
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+
+  describe('on level not number value', () => {
+    it('Should ignore without errors', () => {
+      yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }];
+      expected = [{ min: 5, max: 10 }, { min: 2, max: 4 }];
+
+      alignYLevel(yaxes, 'q');
+      expect(yaxes).toMatchObject(expected);
+    });
+  });
+});

+ 30 - 10
public/vendor/flot/jquery.flot.js

@@ -632,6 +632,7 @@ Licensed under the MIT license.
             processRawData: [],
             processDatapoints: [],
             processOffset: [],
+            processRange: [],
             drawBackground: [],
             drawSeries: [],
             draw: [],
@@ -1613,20 +1614,32 @@ Licensed under the MIT license.
                 setRange(axis);
             });
 
+            executeHooks(hooks.processRange, []);
+
             if (showGrid) {
 
                 var allocatedAxes = $.grep(axes, function (axis) {
                     return axis.show || axis.reserveSpace;
                 });
 
-                $.each(allocatedAxes, function (_, axis) {
-                    // make the ticks
-                    setupTickGeneration(axis);
-                    setTicks(axis);
-                    snapRangeToTicks(axis, axis.ticks);
-                    // find labelWidth/Height for axis
-                    measureTickLabels(axis);
-                });
+                var snaped = false;
+                for (var i = 0; i < 2; i++) {
+                    $.each(allocatedAxes, function (_, axis) {
+                        // make the ticks
+                        setupTickGeneration(axis);
+                        setTicks(axis);
+                        snaped = snapRangeToTicks(axis, axis.ticks) || snaped;
+                        // find labelWidth/Height for axis
+                        measureTickLabels(axis);
+                    });
+
+                    if (snaped && hooks.processRange.length > 0) {
+                        executeHooks(hooks.processRange, []);
+                        snaped = false;
+                    } else {
+                        break;
+                    }
+                }
 
                 // with all dimensions calculated, we can compute the
                 // axis bounding boxes, start from the outside
@@ -1643,6 +1656,7 @@ Licensed under the MIT license.
                 });
             }
 
+
             plotWidth = surface.width - plotOffset.left - plotOffset.right;
             plotHeight = surface.height - plotOffset.bottom - plotOffset.top;
 
@@ -1876,13 +1890,19 @@ Licensed under the MIT license.
         }
 
         function snapRangeToTicks(axis, ticks) {
+            var changed = false;
             if (axis.options.autoscaleMargin && ticks.length > 0) {
                 // snap to ticks
-                if (axis.options.min == null)
+                if (axis.options.min == null) {
                     axis.min = Math.min(axis.min, ticks[0].v);
-                if (axis.options.max == null && ticks.length > 1)
+                    changed = true;
+                }
+                if (axis.options.max == null && ticks.length > 1) {
                     axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
+                    changed = true;
+                }
             }
+            return changed;
         }
 
         function draw() {