From antrob; don't recalculate layout when updateOptions() would not change it.
authorDan Vanderkam <dan@dygraphs.com>
Tue, 2 Aug 2011 04:35:17 +0000 (07:35 +0300)
committerDan Vanderkam <dan@dygraphs.com>
Tue, 2 Aug 2011 04:35:17 +0000 (07:35 +0300)
This suppresses recalculation of the pixel position of points when calling
updateOptions() with options which will not affect the layout. Insignificant
for small graphs, but this change provides a great performance boost for
changing options for graphs with multiple data sets.

Anthony saw something like a 30-40% speedup for dense charts.

auto_tests/misc/local.html
auto_tests/tests/update_options.js [new file with mode: 0644]
dygraph-utils.js
dygraph.js

index 428e444..0d653c1 100644 (file)
@@ -27,6 +27,7 @@
   <script type="text/javascript" src="../tests/custom_bars.js"></script>
   <script type="text/javascript" src="../tests/css.js"></script>
   <script type="text/javascript" src="../tests/selection.js"></script>
+  <script type="text/javascript" src="../tests/update_options.js"></script>
 
 
   <script type="text/javascript">
diff --git a/auto_tests/tests/update_options.js b/auto_tests/tests/update_options.js
new file mode 100644 (file)
index 0000000..21efeff
--- /dev/null
@@ -0,0 +1,112 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+/**
+ * @fileoverview Tests for the updateOptions function.
+ * @author antrob@google.com (Anthony Robledo)
+ */
+var UpdateOptionsTestCase = TestCase("update-options");
+  
+UpdateOptionsTestCase.prototype.opts = {
+  width: 480,
+  height: 320,
+};
+
+UpdateOptionsTestCase.prototype.data = "X,Y1,Y2\n" +
+  "2011-01-01,2,3\n" +
+  "2011-02-02,5,3\n" +
+  "2011-03-03,6,1\n" +
+  "2011-04-04,9,5\n" +
+  "2011-05-05,8,3\n";
+
+UpdateOptionsTestCase.prototype.setUp = function() {
+  document.body.innerHTML = "<div id='graph'></div>";
+};
+
+UpdateOptionsTestCase.prototype.tearDown = function() {
+};
+
+UpdateOptionsTestCase.prototype.wrap = function(g) {
+  g._testDrawCalled = false;
+  var oldDrawGraph = Dygraph.prototype.drawGraph_;
+  Dygraph.prototype.drawGraph_ = function() {
+    g._testDrawCalled = true;
+    oldDrawGraph.call(g);
+  }
+
+  return oldDrawGraph;
+}
+
+UpdateOptionsTestCase.prototype.unWrap = function(oldDrawGraph) {
+  Dygraph.prototype.drawGraph_ = oldDrawGraph;
+}
+
+UpdateOptionsTestCase.prototype.testStrokeAll = function() {
+  var graphDiv = document.getElementById("graph");
+  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var updatedOptions = { };
+
+  updatedOptions['strokeWidth'] = 3;
+
+  // These options will allow us to jump to renderGraph_()
+  // drawGraph_() will be skipped.
+  var oldDrawGraph = this.wrap(graph);
+  graph.updateOptions(updatedOptions);
+  this.unWrap(oldDrawGraph);
+  assertFalse(graph._testDrawCalled);
+};
+
+UpdateOptionsTestCase.prototype.testStrokeSingleSeries = function() {
+  var graphDiv = document.getElementById("graph");
+  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var updatedOptions = { };
+  var optionsForY1 = { };
+
+  optionsForY1['strokeWidth'] = 3;
+  updatedOptions['Y1'] = optionsForY1;
+
+  // These options will allow us to jump to renderGraph_()
+  // drawGraph_() will be skipped.
+  var oldDrawGraph = this.wrap(graph);
+  graph.updateOptions(updatedOptions);
+  this.unWrap(oldDrawGraph);
+  assertFalse(graph._testDrawCalled);
+};
+UpdateOptionsTestCase.prototype.testSingleSeriesRequiresNewPoints = function() {
+  var graphDiv = document.getElementById("graph");
+  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var updatedOptions = { };
+  var optionsForY1 = { };
+  var optionsForY2 = { };
+
+  // This will not require new points.
+  optionsForY1['strokeWidth'] = 2;
+  updatedOptions['Y1'] = optionsForY1;
+
+  // This will require new points.
+  optionsForY2['stepPlot'] = true;
+  updatedOptions['Y2'] = optionsForY2;
+
+  // These options will not allow us to jump to renderGraph_()
+  // drawGraph_() must be called
+  var oldDrawGraph = this.wrap(graph);
+  graph.updateOptions(updatedOptions);
+  this.unWrap(oldDrawGraph);
+  assertTrue(graph._testDrawCalled);
+};
+
+UpdateOptionsTestCase.prototype.testWidthChangeNeedsNewPoints = function() {
+  var graphDiv = document.getElementById("graph");
+  var graph = new Dygraph(graphDiv, this.data, this.opts);
+  var updatedOptions = { };
+
+  // This will require new points.
+  updatedOptions['width'] = 600;
+
+  // These options will not allow us to jump to renderGraph_()
+  // drawGraph_() must be called
+  var oldDrawGraph = this.wrap(graph);
+  graph.updateOptions(updatedOptions);
+  this.unWrap(oldDrawGraph);
+  assertTrue(graph._testDrawCalled);
+};
index e03aaf8..2ba6ab5 100644 (file)
@@ -553,3 +553,108 @@ Dygraph.createCanvas = function() {
 
   return canvas;
 };
+
+/**
+ * @private
+ * This function will scan the option list and determine if they
+ * require us to recalculate the pixel positions of each point.
+ * @param { List } a list of options to check.
+ * @return { Boolean } true if the graph needs new points else false.
+ */
+Dygraph.isPixelChangingOptionList = function(labels, attrs) {
+  // A whitelist of options that do not change pixel positions.
+  var pixelSafeOptions = {
+    'annotationClickHandler': true,
+    'annotationDblClickHandler': true,
+    'annotationMouseOutHandler': true,
+    'annotationMouseOverHandler': true,
+    'axisLabelColor': true,
+    'axisLineColor': true,
+    'axisLineWidth': true,
+    'clickCallback': true,
+    'colorSaturation': true,
+    'colorValue': true,
+    'colors': true,
+    'connectSeparatedPoints': true,
+    'digitsAfterDecimal': true,
+    'drawCallback': true,
+    'drawPoints': true,
+    'drawXGrid': true,
+    'drawYGrid': true,
+    'fillAlpha': true,
+    'gridLineColor': true,
+    'gridLineWidth': true,
+    'hideOverlayOnMouseOut': true,
+    'highlightCallback': true,
+    'highlightCircleSize': true,
+    'interactionModel': true,
+    'isZoomedIgnoreProgrammaticZoom': true,
+    'labelsDiv': true,
+    'labelsDivStyles': true,
+    'labelsDivWidth': true,
+    'labelsKMB': true,
+    'labelsKMG2': true,
+    'labelsSeparateLines': true,
+    'labelsShowZeroValues': true,
+    'legend': true,
+    'maxNumberWidth': true,
+    'panEdgeFraction': true,
+    'pixelsPerYLabel': true,
+    'pointClickCallback': true,
+    'pointSize': true,
+    'showLabelsOnHighlight': true,
+    'showRoller': true,
+    'sigFigs': true,
+    'strokeWidth': true,
+    'underlayCallback': true,
+    'unhighlightCallback': true,
+    'xAxisLabelFormatter': true,
+    'xTicker': true,
+    'xValueFormatter': true,
+    'yAxisLabelFormatter': true,
+    'yValueFormatter': true,
+    'zoomCallback': true
+  };    
+
+  // Assume that we do not require new points.
+  // This will change to true if we actually do need new points.
+  var requiresNewPoints = false;
+
+  // Create a dictionary of series names for faster lookup.
+  // If there are no labels, then the dictionary stays empty.
+  var seriesNamesDictionary = { };
+  if (labels) {
+    for (var i = 1; i < labels.length; i++) {
+      seriesNamesDictionary[labels[i]] = true;
+    }
+  }
+
+  // Iterate through the list of updated options.
+  for (property in attrs) {
+    // Break early if we already know we need new points from a previous option.
+    if (requiresNewPoints) {
+      break;
+    }
+    if (attrs.hasOwnProperty(property)) {
+      // Find out of this field is actually a series specific options list.
+      if (seriesNamesDictionary[property]) {
+        // This property value is a list of options for this series.
+        // If any of these sub properties are not pixel safe, set the flag.
+        for (subProperty in attrs[property]) {
+          // Break early if we already know we need new points from a previous option.
+          if (requiresNewPoints) {
+            break;
+          }
+          if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
+            requiresNewPoints = true;
+          }
+        }
+      // If this was not a series specific option list, check if its a pixel changing property.
+      } else if (!pixelSafeOptions[property]) {
+        requiresNewPoints = true;
+      }   
+    }
+  }
+
+  return requiresNewPoints;
+};
index 1861fb6..d80cc71 100644 (file)
@@ -2064,6 +2064,17 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) {
   this.layout_.setDateWindow(this.dateWindow_);
   this.zoomed_x_ = tmp_zoomed_x;
   this.layout_.evaluateWithError();
+  this.renderGraph_(is_initial_draw, false);
+
+  if (this.attr_("timingName")) {
+    var end = new Date();
+    if (console) {
+      console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms")
+    }
+  }
+};
+
+Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
   this.plotter_.clear();
   this.plotter_.render();
   this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
@@ -2088,13 +2099,6 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) {
   if (this.attr_("drawCallback") !== null) {
     this.attr_("drawCallback")(this, is_initial_draw);
   }
-
-  if (this.attr_("timingName")) {
-    var end = new Date();
-    if (console) {
-      console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms")
-    }
-  }
 };
 
 /**
@@ -2963,13 +2967,22 @@ Dygraph.prototype.updateOptions = function(attrs, block_redraw) {
   // drawPoints
   // highlightCircleSize
 
+  // Check if this set options will require new points.
+  var requiresNewPoints = Dygraph.isPixelChangingOptionList(this.attr_("labels"), attrs);
+
   Dygraph.update(this.user_attrs_, attrs);
 
   if (attrs['file']) {
     this.file_ = attrs['file'];
     if (!block_redraw) this.start_();
   } else {
-    if (!block_redraw) this.predraw_();
+    if (!block_redraw) {
+      if (requiresNewPoints) {
+        this.predraw_(); 
+      } else {
+        this.renderGraph_(false, false);
+      }
+    }
   }
 };