Merge pull request #127 from kberg/master
authorRobert Konigsberg <konigsberg@google.com>
Sat, 25 Feb 2012 21:48:47 +0000 (13:48 -0800)
committerRobert Konigsberg <konigsberg@google.com>
Sat, 25 Feb 2012 21:48:47 +0000 (13:48 -0800)
Clean up gallery; show HTML, Javascript and CSS in prettier fashion.

26 files changed:
auto_tests/misc/local.html
auto_tests/tests/axis_labels.js
auto_tests/tests/callback.js [new file with mode: 0644]
auto_tests/tests/interaction_model.js
auto_tests/tests/multiple_axes.js
auto_tests/tests/range_tests.js
auto_tests/tests/sanity.js
auto_tests/tests/simple_drawing.js
docs/index.html
dygraph-canvas.js
dygraph-gviz.js
dygraph-interaction-model.js
dygraph-layout.js
dygraph-options-reference.js
dygraph-range-selector.js
dygraph-utils.js
dygraph.js
gallery/interaction-api.js
generate-documentation.py
push-to-web.sh
tests/annotation-gviz.html
tests/custom-circles.html [new file with mode: 0644]
tests/dygraph-many-points-benchmark.html
tests/interaction.js
tests/per-series.html
tests/two-axes-vr.html [new file with mode: 0644]

index d24cab8..8e4cfdd 100644 (file)
@@ -39,6 +39,7 @@
   <script type="text/javascript" src="../tests/update_options.js"></script>
   <script type="text/javascript" src="../tests/utils_test.js"></script>
   <script type="text/javascript" src="../tests/multiple_axes.js"></script>
+  <script type="text/javascript" src="../tests/callback.js"></script>
 
 
   <script type="text/javascript">
index 6fc54e5..02727a3 100644 (file)
@@ -420,3 +420,30 @@ AxisLabelsTestCase.prototype.testGlobalFormatters = function() {
   g.setSelection(9);
   assertEquals("vf9: y:vf18", getLegend());
 };
+
+AxisLabelsTestCase.prototype.testSeriesOrder = function() {
+  var opts = {
+    width: 480,
+    height: 320
+  };
+  var data = "x,00,01,10,11\n" +
+      "0,101,201,301,401\n" +
+      "1,102,202,302,402\n" +
+      "2,103,203,303,403\n" +
+      "3,104,204,304,404\n"
+  ;
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+
+  g.setSelection(2);
+  assertEquals('2: 00:103 01:203 10:303 11:403', getLegend());
+
+  // Sanity checks for indexFromSetName
+  assertEquals(0, g.indexFromSetName("x"));
+  assertEquals(1, g.indexFromSetName("00"));
+  assertEquals(null, g.indexFromSetName("abcde"));
+
+  // Verify that we get the label list back in the right order
+  assertEquals(["x", "00", "01", "10", "11"], g.getLabels());
+};
diff --git a/auto_tests/tests/callback.js b/auto_tests/tests/callback.js
new file mode 100644 (file)
index 0000000..3431070
--- /dev/null
@@ -0,0 +1,145 @@
+/** 
+ * @fileoverview Test cases for the callbacks.
+ *
+ * @author uemit.seren@gmail.com (Ümit Seren)
+ */
+
+var CallbackTestCase = TestCase("callback");
+
+CallbackTestCase.prototype.setUp = function() {
+  document.body.innerHTML = "<div id='graph'></div>";
+};
+
+CallbackTestCase.prototype.tearDown = function() {
+};
+var data = "X,a\,b,c\n" +
+    "10,-1,1,2\n" +
+    "11,0,3,1\n" +
+    "12,1,4,2\n" +
+    "13,0,2,3\n";
+/**
+ * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
+ * is properly called when the first series is hidden (setVisibility = false) 
+ * 
+ */
+CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
+  var h_row;
+  var h_pts;
+
+  var highlightCallback = function(e, x, pts, row) {
+    h_row = row;
+    h_pts = pts;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data,
+      {
+        width: 100,
+        height : 100,
+        visibility: [false, true, true],
+        highlightCallback : highlightCallback,
+      });
+
+  DygraphOps.dispatchMouseMove(g, 13, 10);
+
+  //check correct row is returned
+  assertEquals(3, h_row);
+  //check there are only two points (because first series is hidden)
+  assertEquals(2, h_pts.length);
+};
+
+
+/**
+ * Test that drawPointCallback isn't called when drawPoints is false
+ */
+CallbackTestCase.prototype.testDrawPointCallback_disabled = function() {
+  var called = false;
+
+  var callback = function() {
+    called = true;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, {
+      drawPointCallback : callback,
+    });
+
+  assertFalse(called);
+};
+
+/**
+ * Test that drawPointCallback is called when drawPoints is true
+ */
+CallbackTestCase.prototype.testDrawPointCallback_enabled = function() {
+  var called = false;
+
+  var callback = function() {
+    called = true;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, {
+      drawPoints : true,
+      drawPointCallback : callback
+    });
+
+  assertTrue(called);
+};
+
+/**
+ * Test that drawPointCallback is called when drawPoints is true
+ */
+CallbackTestCase.prototype.testDrawPointCallback_pointSize = function() {
+  var pointSize = 0;
+  var count = 0;
+
+  var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam) {
+    pointSize = pointSizeParam;
+    count++;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, {
+      drawPoints : true,
+      drawPointCallback : callback
+    });
+
+  assertEquals(1.5, pointSize);
+  assertEquals(12, count); // one call per data point.
+
+  var g = new Dygraph(graph, data, {
+      drawPoints : true,
+      drawPointCallback : callback,
+      pointSize : 8
+    });
+
+  assertEquals(8, pointSize);
+};
+
+/**
+ * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
+ * is properly called when the first series is hidden (setVisibility = false) 
+ * 
+ */
+CallbackTestCase.prototype.testDrawHighlightPointCallbackIsCalled = function() {
+  var called = false;
+
+  var drawHighlightPointCallback  = function() {
+    called = true;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data,
+      {
+        width: 100,
+        height : 100,
+        drawHighlightPointCallback : drawHighlightPointCallback
+      });
+
+  assertFalse(called);
+  DygraphOps.dispatchMouseMove(g, 13, 10);
+  assertTrue(called);
+};
index 8eec47c..4646ff8 100644 (file)
@@ -331,3 +331,36 @@ InteractionModelTestCase.prototype.testIsZoomed_updateOptions_both = function()
   assertTrue(g.isZoomed("x"));
   assertTrue(g.isZoomed("y"));
 };
+
+
+InteractionModelTestCase.prototype.testCorrectAxisValueRangeAfterUnzoom = function() {
+  var g = new Dygraph(document.getElementById("graph"), data2, {valueRange:[1,50],dateRange:[1,9],animatedZooms:false});
+  
+  // Zoom x axis
+  DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+  DygraphOps.dispatchMouseMove_Point(g, 30, 10);
+  DygraphOps.dispatchMouseUp_Point(g, 30, 10);
+
+  // Zoom y axis
+  DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+  DygraphOps.dispatchMouseMove_Point(g, 10, 30);
+  DygraphOps.dispatchMouseUp_Point(g, 10, 30);
+  currentYAxisRange = g.yAxisRange();
+  currentXAxisRange = g.xAxisRange();
+  
+  //check that the range for the axis has changed
+  assertNotEquals(1,currentXAxisRange[0]);
+  assertNotEquals(10,currentXAxisRange[1]);
+  assertNotEquals(1,currentYAxisRange[0]);
+  assertNotEquals(50,currentYAxisRange[1]);
+  
+  // unzoom by doubleclick
+  DygraphOps.dispatchDoubleClick(g, null);
+  
+  // check if range for y-axis was reset to original value 
+  // TODO check if range for x-axis is correct. 
+  // Currently not possible because dateRange is set to null and extremes are returned
+  newYAxisRange = g.yAxisRange();
+  assertEquals(1,newYAxisRange[0]);
+  assertEquals(50,newYAxisRange[1]);
+};
index bf75876..05e93c7 100644 (file)
@@ -243,3 +243,51 @@ MultipleAxesTestCase.prototype.testNoY2LabelWithoutSecondaryAxis = function() {
   assertEquals(["y-axis"], getClassTexts("dygraph-ylabel"));
   assertEquals([], getClassTexts("dygraph-y2label"));
 };
+
+MultipleAxesTestCase.prototype.testValueRangePerAxisOptions = function() {
+  var data = MultipleAxesTestCase.getData();
+
+  g = new Dygraph(
+    document.getElementById("graph"),
+    data,
+    {
+      labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+      'Y3': {
+        axis: {
+        }
+      },
+      'Y4': {
+        axis: 'Y3'  // use the same y-axis as series Y3
+      },
+      axes: {
+        y: {
+          valueRange: [40, 70]
+        },
+        y2: {
+          // set axis-related properties here
+          labelsKMB: true
+        }
+      },
+      ylabel: 'Primary y-axis',
+      y2label: 'Secondary y-axis',
+      yAxisLabelWidth: 60
+    }
+  );
+  assertEquals(["40", "45", "50", "55", "60", "65"], getYLabelsForAxis("1"));
+  assertEquals(["900K","1.1M","1.3M","1.5M","1.7M","1.9M"], getYLabelsForAxis("2"));
+  
+  g.updateOptions(
+    {
+      axes: {
+        y: {
+          valueRange: [40, 80]
+        },
+        y2: {
+          valueRange: [1e6, 1.2e6]
+        }
+     }
+    }
+  );
+  assertEquals(["40", "45", "50", "55", "60", "65", "70", "75"], getYLabelsForAxis("1"));
+  assertEquals(["1M", "1.02M", "1.05M", "1.08M", "1.1M", "1.13M", "1.15M", "1.18M"], getYLabelsForAxis("2"));
+};
\ No newline at end of file
index 32c511e..c92c489 100644 (file)
@@ -70,8 +70,12 @@ RangeTestCase.prototype.testRangeSetOperations = function() {
   g.updateOptions({  });
   assertEquals([12, 18], g.xAxisRange());
   assertEquals([10, 40], g.yAxisRange(0));
+  
+  g.updateOptions({valueRange : null, axes: {y:{valueRange : [15, 20]}}});
+  assertEquals([12, 18], g.xAxisRange());
+  assertEquals([15, 20], g.yAxisRange(0));
 
-  g.updateOptions({ dateWindow : null, valueRange : null });
+  g.updateOptions({ dateWindow : null, valueRange : null, axes: null });
   assertEquals([10, 20], g.xAxisRange());
   assertEquals([0, 55], g.yAxisRange(0));
 };
index 528b7a1..95a1aa2 100644 (file)
@@ -80,6 +80,8 @@ SanityTestCase.prototype.testYAxisRange_custom = function() {
   var graph = document.getElementById("graph");
   var g = new Dygraph(graph, ZERO_TO_FIFTY, { valueRange: [0,50] });
   assertEquals([0, 50], g.yAxisRange(0));
+  g.updateOptions({valueRange: null, axes: {y: {valueRange: [10, 40]}}});
+  assertEquals([10, 40], g.yAxisRange(0));
 };
 
 /**
@@ -95,7 +97,15 @@ SanityTestCase.prototype.testToDomYCoord = function() {
 
   assertEquals(50, g.toDomYCoord(0));
   assertEquals(0, g.toDomYCoord(50));
-  
+
+  for (var x = 0; x <= 50; x++) {
+    assertEqualsDelta(50 - x, g.toDomYCoord(x), 0.00001);
+  }
+  g.updateOptions({valueRange: null, axes: {y: {valueRange: [0, 50]}}});
+
+  assertEquals(50, g.toDomYCoord(0));
+  assertEquals(0, g.toDomYCoord(50));
+
   for (var x = 0; x <= 50; x++) {
     assertEqualsDelta(50 - x, g.toDomYCoord(x), 0.00001);
   }
index ca19301..0ea13a4 100644 (file)
@@ -56,3 +56,27 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangePlusOne = function() {
     lineWidth: 1
   });
 }
+
+/**
+ * Tests that it is drawing dashes, and it remember the dash history between
+ * points.
+ */
+SimpleDrawingTestCase.prototype.testDrawSimpleDash = function() {
+  var opts = {
+      drawXGrid: false,
+      drawYGrid: false,
+      drawXAxis: false,
+      drawYAxis: false,
+      'Y1': {strokePattern: [25, 7, 7, 7]},
+      colors: ['#ff0000']
+  };
+
+  var graph = document.getElementById("graph");
+  // Set the dims so we pass if default changes.
+  graph.style.width='480px';
+  graph.style.height='320px';
+  var g = new Dygraph(graph, [[1, 4], [2, 5], [3, 3], [4, 7], [5, 9]], opts);
+  htx = g.hidden_ctx_;
+
+  assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000"));
+};
index bf5b5fa..5c2444f 100644 (file)
@@ -632,10 +632,42 @@ public static native JavaScriptObject drawDygraph(
     <p>dygraphs has also found use in other organizations:</p>
 
     <ul class='padded-list'>
+  <li><a
+    href="http://iswa.ccmc.gsfc.nasa.gov:8080/IswaSystemWebApp/index.jsp?i_1=388&l_1=99&t_1=316&w_1=800&h_1=400&s_1=0!3!0!ACE.B_x!ACE.B_y!ACE.B_z!">Integrated
+    Space Weather Analysis System</a> (NASA)<br/>
+  <span class="desc">&ldquo;We use [dygraphs] in the Integrated Space Weather
+    Analysis System available from the Space Weather Laboratory at NASA Goddard
+    Space Flight Center. It works quite well for time series data from various
+    missions and simulations that we store.&rdquo;</span></li>
+
+
+  <li><a href="http://www.eutelsat.fr">Eutelsat</a><br/>
+  <span class="desc">&ldquo;Eutelsat uses dygraphs for charting spacecraft
+    telemetry for a fleet of 25 geostationary satellites. The spacecraft
+    engineers are very happy with it.  All satellite combined are producing
+    about 200 millions unique data points per day so we really appreciate the
+    excellent performance of dygraphs.&rdquo;</span></li>
+
+  <li><a href="http://www.10gen.com/mongodb-monitoring-service">10gen MongoDB
+    Monitoring Service</a><br/>
+  <span class="desc">A free monitoring service for MongoDB from 10gen (the
+    creators of MongoDB). Used by thousands of servers and users. Makes use of
+    <a href="tests/synchronize.html">synchronized charts</a> to display many
+    quantities simultaneously.</span></li>
+
     <li><a href="http://toolserver.org/~dartar/moodbar/">Wikimedia Foundation - Moodbar data dashboard</a><br/>
     <span class="desc">dygraphs is used internally at Wikimedia as a handy solution to monitor the
     results of a bunch of small experiments.</span></li>
 
+  <li><a href="http://code.google.com/p/quadrant-framework/">quadrant-framework</a> (MySQL Load Testing Framework)<br/>
+  <span class="desc">A user friendly framework for creating and visualizing
+    MySQL database load test jobs. For more information on its use of dygraphs,
+    see <a href="http://themattreid.com/wordpress/2011/05/20/quadrant-framework-rev7-update-adds-dygraphs-support/">this post</a>.</span></li>
+
+  <li><a href="http://spinwave.wordpress.com/2011/03/28/spinwave-systems-enables-energy-efficiency-case-studies/">Spinwave Systems</a> (Home energy monitoring)<br/>
+  <span class="desc">dygraphs is used to chart energy usage over time.</span></li>
+
+
     <li><a href="http://www.socib.es/jwebchart/?file=http://thredds.socib.es/thredds/dodsC/mooring/weather_station/mobims_calamillor-scb_met001/L1/dep0001_mobims-calamillor_scb-met001_L1_latest.nc">Jwebchart</a><br/>
     <span class="desc">
     jWebChart is a stand-alone and Thredds' embedded plotting system for
index 5e6766d..10936de 100644 (file)
@@ -28,6 +28,7 @@
 /*global Dygraph:false,RGBColor:false */
 "use strict";
 
+
 var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
   this.dygraph_ = dygraph;
 
@@ -311,7 +312,7 @@ DygraphCanvasRenderer.prototype._renderAxis = function() {
     inner_div.className = 'dygraph-axis-label' +
                           ' dygraph-axis-label-' + axis +
                           (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
-    inner_div.appendChild(document.createTextNode(txt));
+    inner_div.innerHTML=txt;
     div.appendChild(inner_div);
     return div;
   };
@@ -679,12 +680,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
   var pointsLength = points.length;
   var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
 
-  var setNames = [];
-  for (var name in this.layout.datasets) {
-    if (this.layout.datasets.hasOwnProperty(name)) {
-      setNames.push(name);
-    }
-  }
+  var setNames = this.layout.setNames;
   var setCount = setNames.length;
 
   // TODO(danvk): Move this mapping into Dygraph and get it out of here.
@@ -826,18 +822,29 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
   var afterLastIndexInSet = 0;
   var setLength = 0;
   for (i = 0; i < setCount; i += 1) {
+    firstIndexInSet = this.layout.setPointsOffsets[i];
     setLength = this.layout.setPointsLengths[i];
-    afterLastIndexInSet += setLength;
+    afterLastIndexInSet = firstIndexInSet + setLength;
     setName = setNames[i];
     color = this.colors[setName];
     var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
 
     // setup graphics context
+    // TODO(konigsberg): This function has ctx and context. Clarify the difference.
     context.save();
     var pointSize = this.dygraph_.attr_("pointSize", setName);
     prevX = null;
     prevY = null;
     var drawPoints = this.dygraph_.attr_("drawPoints", setName);
+    var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName);
+    if (!drawPointCallback) {
+      drawPointCallback = Dygraph.Circles.DEFAULT;
+    }
+    var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
+    var strokePattern = this.dygraph_.attr_("strokePattern", setName);
+    if (!Dygraph.isArrayLike(strokePattern)) {
+      strokePattern = null;
+    }
     for (j = firstIndexInSet; j < afterLastIndexInSet; j++) {
       point = points[j];
       if (isNullOrNaN(point.canvasy)) {
@@ -846,8 +853,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
           ctx.beginPath();
           ctx.strokeStyle = color;
           ctx.lineWidth = this.attr_('strokeWidth');
-          ctx.moveTo(prevX, prevY);
-          ctx.lineTo(point.canvasx, prevY);
+          this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
           ctx.stroke();
         }
         // this will make us move to the next point, not draw a line to it.
@@ -873,28 +879,123 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
             ctx.beginPath();
             ctx.strokeStyle = color;
             ctx.lineWidth = strokeWidth;
-            ctx.moveTo(prevX, prevY);
             if (stepPlot) {
-              ctx.lineTo(point.canvasx, prevY);
+              this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
+              prevX = point.canvasx;
             }
+            this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
             prevX = point.canvasx;
             prevY = point.canvasy;
-            ctx.lineTo(prevX, prevY);
             ctx.stroke();
           }
         }
 
         if (drawPoints || isIsolated) {
-          ctx.beginPath();
-          ctx.fillStyle = color;
-          ctx.arc(point.canvasx, point.canvasy, pointSize,
-                  0, 2 * Math.PI, false);
-          ctx.fill();
+          pointsOnLine.push([point.canvasx, point.canvasy]);
         }
       }
     }
+    for (var idx = 0; idx < pointsOnLine.length; idx++) {
+      var cb = pointsOnLine[idx];
+      ctx.save();
+      drawPointCallback(
+          this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize);
+      ctx.restore();
+
+    }
     firstIndexInSet = afterLastIndexInSet;
   }
 
   context.restore();
 };
+
+/**
+ * This does dashed lines onto a canvas for a given pattern. You must call
+ * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
+ * the state of the line in regards to where we left off on drawing the pattern.
+ * You can draw a dashed line in several function calls and the pattern will be
+ * continous as long as you didn't call this function with a different pattern
+ * in between.
+ * @param ctx The canvas 2d context to draw on.
+ * @param x The start of the line's x coordinate.
+ * @param y The start of the line's y coordinate.
+ * @param x2 The end of the line's x coordinate.
+ * @param y2 The end of the line's y coordinate.
+ * @param pattern The dash pattern to draw, an array of integers where even 
+ * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
+ * is drawn, 2 is the space between.). A null pattern, array of length one, or
+ * empty array will do just a solid line.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) {
+  // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
+  // Modified by Russell Valentine to keep line history and continue the pattern
+  // where it left off.
+  var dx, dy, len, rot, patternIndex, segment;
+
+  // If we don't have a pattern or it is an empty array or of size one just
+  // do a solid line.
+  if (!pattern || pattern.length <= 1) {
+    ctx.moveTo(x, y);
+    ctx.lineTo(x2, y2);
+    return;
+  }
+
+  // If we have a different dash pattern than the last time this was called we
+  // reset our dash history and start the pattern from the begging 
+  // regardless of state of the last pattern.
+  if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) {
+    this._dashedLineToHistoryPattern = pattern;
+    this._dashedLineToHistory = [0, 0];
+  }
+  ctx.save();
+
+  // Calculate transformation parameters
+  dx = (x2-x);
+  dy = (y2-y);
+  len = Math.sqrt(dx*dx + dy*dy);
+  rot = Math.atan2(dy, dx);
+
+  // Set transformation
+  ctx.translate(x, y);
+  ctx.moveTo(0, 0);
+  ctx.rotate(rot);
+
+  // Set last pattern index we used for this pattern.
+  patternIndex = this._dashedLineToHistory[0];
+  x = 0;
+  while (len > x) {
+    // Get the length of the pattern segment we are dealing with.
+    segment = pattern[patternIndex];
+    // If our last draw didn't complete the pattern segment all the way we 
+    // will try to finish it. Otherwise we will try to do the whole segment.
+    if (this._dashedLineToHistory[1]) {
+      x += this._dashedLineToHistory[1];
+    } else {
+      x += segment;
+    }
+    if (x > len) {
+      // We were unable to complete this pattern index all the way, keep
+      // where we are the history so our next draw continues where we left off
+      // in the pattern.
+      this._dashedLineToHistory = [patternIndex, x-len];
+      x = len;
+    } else {
+      // We completed this patternIndex, we put in the history that we are on
+      // the beginning of the next segment.
+      this._dashedLineToHistory = [(patternIndex+1)%pattern.length, 0];
+    }
+
+    // We do a line on a even pattern index and just move on a odd pattern index.
+    // The move is the empty space in the dash.
+    if(patternIndex % 2 === 0) {
+      ctx.lineTo(x, 0);
+    } else {
+      ctx.moveTo(x, 0);
+    }
+    // If we are not done, next loop process the next pattern segment, or the
+    // first segment again if we are at the end of the pattern.
+    patternIndex = (patternIndex+1) % pattern.length;
+  }
+  ctx.restore();
+};
index b87fe80..114263a 100644 (file)
@@ -67,14 +67,10 @@ Dygraph.GVizChart.prototype.getSelection = function() {
 
   if (row < 0) return selection;
 
-  var col = 1;
   var datasets = this.date_graph.layout_.datasets;
-  for (var k in datasets) {
-    if (!datasets.hasOwnProperty(k)) continue;
-    selection.push({row: row, column: col});
-    col++;
+  for (var setIdx = 0; setIdx < datasets.length; ++setIdx) {
+    selection.push({row: row, column: setIdx + 1});
   }
 
   return selection;
 };
-
index 55184ec..a3de6f7 100644 (file)
@@ -418,3 +418,21 @@ Dygraph.Interaction.nonInteractiveModel_ = {
     }
   }
 };
+
+// Default interaction model when using the range selector.
+Dygraph.Interaction.dragIsPanInteractionModel = {
+  mousedown: function(event, g, context) {
+    context.initializeMouseDown(event, g, context);
+    Dygraph.startPan(event, g, context);
+  },
+  mousemove: function(event, g, context) {
+    if (context.isPanning) {
+      Dygraph.movePan(event, g, context);
+    }
+  },
+  mouseup: function(event, g, context) {
+    if (context.isPanning) {
+      Dygraph.endPan(event, g, context);
+    }
+  }
+};
index 328da81..1563817 100644 (file)
@@ -32,6 +32,7 @@
 var DygraphLayout = function(dygraph) {
   this.dygraph_ = dygraph;
   this.datasets = [];
+  this.setNames = [];
   this.annotations = [];
   this.yAxes_ = null;
 
@@ -46,7 +47,8 @@ DygraphLayout.prototype.attr_ = function(name) {
 };
 
 DygraphLayout.prototype.addDataset = function(setname, set_xy) {
-  this.datasets[setname] = set_xy;
+  this.datasets.push(set_xy);
+  this.setNames.push(setname);
 };
 
 DygraphLayout.prototype.getPlotArea = function() {
@@ -162,9 +164,8 @@ DygraphLayout.prototype._evaluateLimits = function() {
     this.minxval = this.dateWindow_[0];
     this.maxxval = this.dateWindow_[1];
   } else {
-    for (var name in this.datasets) {
-      if (!this.datasets.hasOwnProperty(name)) continue;
-      var series = this.datasets[name];
+    for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+      var series = this.datasets[setIdx];
       if (series.length > 1) {
         var x1 = series[0][0];
         if (!this.minxval || x1 < this.minxval) this.minxval = x1;
@@ -212,13 +213,14 @@ DygraphLayout.prototype._evaluateLineCharts = function() {
   // for every data set since the points are added in order of the sets in
   // datasets.
   this.setPointsLengths = [];
+  this.setPointsOffsets = [];
 
-  for (var setName in this.datasets) {
-    if (!this.datasets.hasOwnProperty(setName)) continue;
-
-    var dataset = this.datasets[setName];
+  for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+    var dataset = this.datasets[setIdx];
+    var setName = this.setNames[setIdx];
     var axis = this.dygraph_.axisPropertiesForSeries(setName);
 
+    this.setPointsOffsets.push(this.points.length);
     var setPointsLength = 0;
 
     for (var j = 0; j < dataset.length; j++) {
@@ -283,10 +285,10 @@ DygraphLayout.prototype.evaluateWithError = function() {
 
   // Copy over the error terms
   var i = 0;  // index in this.points
-  for (var setName in this.datasets) {
-    if (!this.datasets.hasOwnProperty(setName)) continue;
+  for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
     var j = 0;
-    var dataset = this.datasets[setName];
+    var dataset = this.datasets[setIdx];
+    var setName = this.setNames[setIdx];
     var axis = this.dygraph_.axisPropertiesForSeries(setName);
     for (j = 0; j < dataset.length; j++, i++) {
       var item = dataset[j];
@@ -340,7 +342,13 @@ DygraphLayout.prototype._evaluateAnnotations = function() {
  */
 DygraphLayout.prototype.removeAllDatasets = function() {
   delete this.datasets;
+  delete this.setNames;
+  delete this.setPointsLengths;
+  delete this.setPointsOffsets;
   this.datasets = [];
+  this.setNames = [];
+  this.setPointsLengths = [];
+  this.setPointsOffsets = [];
 };
 
 /**
index 92846ee..3bef499 100644 (file)
@@ -41,26 +41,59 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "false",
     "labels": ["Data Line display"],
     "type": "boolean",
-    "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart."
+    "description": "Draw a small dot at each point, in addition to a line going through \
+        the point. This makes the individual data points easier to see, but  \
+        can increase visual clutter in the chart. The small dot can be \
+        replaced with a custom rendering by supplying a \
+        <a href='#drawPointCallback'>drawPointCallback</a>."
+  },
+  "drawPointCallback": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
+    "parameters": [
+      [ "g" , "the reference graph" ],
+      [ "seriesName" , "the name of the series" ],
+      [ "canvasContext" , "the canvas to draw on" ],
+      [ "cx" , "center x coordinate" ],
+      [ "cy" , "center y coordinate" ],
+      [ "color" , "series color" ],
+      [ "pointSize" , "the radius of the image." ]
+    ],
+    "description": "Draw a custom item when drawPoints is enabled. \
+        Default is a small dot matching the series color. This method \
+        should constrain drawing to within pointSize pixels from (cx, cy). \
+        Also see <a href='#drawHighlightPointCallback'>drawHighlightPointCallback</a>"
   },
   "height": {
     "default": "320",
     "labels": ["Overall display"],
     "type": "integer",
-    "description": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
+    "description": "Height, in pixels, of the chart. If the container div has \
+      been explicitly sized, this will be ignored."
   },
   "zoomCallback": {
     "default": "null",
     "labels": ["Callbacks"],
     "type": "function(minDate, maxDate, yRanges)",
-    "description": "A function to call when the zoom window is changed (either by zooming in or out). minDate and maxDate are milliseconds since epoch. yRanges is an array of [bottom, top] pairs, one for each y-axis."
+    "parameters": [
+      [ "minDate" , "milliseconds since epoch" ],
+      [ "maxDate" , "milliseconds since epoch." ],
+      [ "yRanges" , "is an array of [bottom, top] pairs, one for each y-axis." ]
+    ],
+    "description": "A function to call when the zoom window is changed (either \
+      by zooming in or out)."
   },
   "pointClickCallback": {
     "snippet": "function(e, point){<br>&nbsp;&nbsp;alert(point);<br>}",
     "default": "null",
     "labels": ["Callbacks", "Interactive Elements"],
     "type": "function(e, point)",
-    "description": "A function to call when a data point is clicked. The function should take two arguments, the event object for the click, and the point that was clicked. The 'point' argument has these properties:\n * xval/yval: The data coordinates of the point (with dates/times as millis since epoch) \n * canvasx/canvasy: The canvas coordinates at which the point is drawn. \n name: The name of the data series to which the point belongs"
+    "parameters": [
+      [ "e" , "the event object for the click" ],
+      [ "point" , "the point that was clicked See <a href='#point_properties'>Point properties</a> for details" ]
+    ],
+    "description": "A function to call when a data point is clicked. and the point that was clicked."
   },
   "colors": {
     "default": "(see description)",
@@ -78,8 +111,32 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
   "highlightCallback": {
     "default": "null",
     "labels": ["Callbacks"],
-    "type": "function(event, x, points,row)",
-    "description": "When set, this callback gets called every time a new point is highlighted. The parameters are the JavaScript mousemove event, the x-coordinate of the highlighted points and an array of highlighted points: <code>[ {name: 'series', yval: y-value}, &hellip; ]</code>"
+    "type": "function(event, x, points, row)",
+    "description": "When set, this callback gets called every time a new point is highlighted.",
+    "parameters": [
+      ["event", "the JavaScript mousemove event"],
+      ["x", "the x-coordinate of the highlighted points"],
+      ["points", "an array of highlighted points: <code>[ {name: 'series', yval: y-value}, &hellip; ]</code>"],
+      ["row", "???"]
+    ]
+  },
+  "drawHighlightPointCallback": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
+    "parameters": [
+      [ "g" , "the reference graph" ],
+      [ "seriesName" , "the name of the series" ],
+      [ "canvasContext" , "the canvas to draw on" ],
+      [ "cx" , "center x coordinate" ],
+      [ "cy" , "center y coordinate" ],
+      [ "color" , "series color" ],
+      [ "pointSize" , "the radius of the image." ]
+    ],
+    "description": "Draw a custom item when a point is highlighted. \
+        Default is a small dot matching the series color. This method \
+        should constrain drawing to within pointSize pixels from (cx, cy) \
+        Also see <a href='#drawPointCallback'>drawPointCallback</a>"
   },
   "includeZero": {
     "default": "false",
@@ -97,7 +154,10 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "null",
     "labels": ["Callbacks"],
     "type": "function(event)",
-    "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph.  The parameter is the mouseout event."
+    "parameters": [
+      [ "event" , "the mouse event" ]
+    ],
+    "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph."
   },
   "axisTickSize": {
     "default": "3.0",
@@ -139,25 +199,47 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "null",
     "labels": ["Annotations"],
     "type": "function(annotation, point, dygraph, event)",
+    "parameters": [
+      [ "annotation" , "the annotation left" ],
+      [ "point" , "the point associated with the annotation" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "event" , "the mouse event" ]
+    ],
     "description": "If provided, this function is called whenever the user mouses out of an annotation."
   },
   "annotationClickHandler": {
     "default": "null",
     "labels": ["Annotations"],
     "type": "function(annotation, point, dygraph, event)",
+    "parameters": [
+      [ "annotation" , "the annotation left" ],
+      [ "point" , "the point associated with the annotation" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "event" , "the mouse event" ]
+    ],
     "description": "If provided, this function is called whenever the user clicks on an annotation."
   },
   "annotationDblClickHandler": {
     "default": "null",
     "labels": ["Annotations"],
     "type": "function(annotation, point, dygraph, event)",
+    "parameters": [
+      [ "annotation" , "the annotation left" ],
+      [ "point" , "the point associated with the annotation" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "event" , "the mouse event" ]
+    ],
     "description": "If provided, this function is called whenever the user double-clicks on an annotation."
   },
   "drawCallback": {
     "default": "null",
     "labels": ["Callbacks"],
     "type": "function(dygraph, is_initial)",
-    "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning. The first parameter is the dygraph being drawn. The second is a boolean value indicating whether this is the initial draw."
+    "parameters": [
+      [ "dygraph" , "The graph being drawn" ],
+      [ "is_initial" , "True if this is the initial draw, false for subsequent draws." ]
+    ],
+    "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning."
   },
   "labelsKMG2": {
     "default": "false",
@@ -181,6 +263,11 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "null",
     "labels": ["Callbacks"],
     "type": "function(canvas, area, dygraph)",
+    "parameters": [
+      [ "canvas" , "the canvas to draw on" ],
+      [ "area" , "" ],
+      [ "dygraph" , "the reference graph" ]
+    ],
     "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
   },
   "width": {
@@ -199,6 +286,14 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "Dygraph.dateTicker or Dygraph.numericTicks",
     "labels": ["Axis display"],
     "type": "function(min, max, pixels, opts, dygraph, vals) -> [{v: ..., label: ...}, ...]",
+    "parameters": [
+      [ "min" , "" ],
+      [ "max" , "" ],
+      [ "pixels" , "" ],
+      [ "opts" , "" ],
+      [ "dygraph" , "the reference graph" ],
+      [ "vals" , "" ]
+    ],
     "description": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result. See dygraph-tickers.js for an extensive discussion. This is set on a <a href='per-axis.html'>per-axis</a> basis."
   },
   "xAxisLabelWidth": {
@@ -263,6 +358,13 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "example": "0.5, 2.0",
     "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
   },
+  "strokePattern": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "array<integer>",
+    "example": "[10, 2, 5, 2]",
+    "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array."
+  },
   "wilsonInterval": {
     "default": "true",
     "labels": ["Error Bars"],
@@ -298,7 +400,7 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "labels": ["Axis display"],
     "type": "Array of two numbers",
     "example": "[10, 110]",
-    "description": "Explicitly set the vertical range of the graph to [low, high]."
+    "description": "Explicitly set the vertical range of the graph to [low, high]. This may be set on a per-axis basis to define each y-axis separately."
   },
   "labelsDivWidth": {
     "default": "250",
@@ -376,14 +478,25 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "Depends on the data type",
     "labels": ["Axis display"],
     "type": "function(number or Date, granularity, opts, dygraph)",
-    "description": "Function to call to format the tick values that appear along an axis. This is usually set on a <a href='per-axis.html'>per-axis</a> basis. The first parameter is either a number (for a numeric axis) or a Date object (for a date axis). The second argument specifies how fine-grained the axis is. For date axes, this is a reference to the time granularity enumeration, defined in dygraph-tickers.js, e.g. Dygraph.WEEKLY. opts is a function which provides access to various options on the dygraph, e.g. opts('labelsKMB')."
+    "parameters": [
+      [ "number or date" , "Either a number (for a numeric axis) or a Date object (for a date axis)" ],
+      [ "granularity" , "specifies how fine-grained the axis is. For date axes, this is a reference to the time granularity enumeration, defined in dygraph-tickers.js, e.g. Dygraph.WEEKLY." ],
+      [ "opts" , "a function which provides access to various options on the dygraph, e.g. opts('labelsKMB')." ],
+      [ "dygraph" , "the referenced graph" ]
+    ],
+    "description": "Function to call to format the tick values that appear along an axis. This is usually set on a <a href='per-axis.html'>per-axis</a> basis."
   },
   "clickCallback": {
     "snippet": "function(e, date_millis){<br>&nbsp;&nbsp;alert(new Date(date_millis));<br>}",
     "default": "null",
     "labels": ["Callbacks"],
     "type": "function(e, x, points)",
-    "description": "A function to call when the canvas is clicked. The function should take three arguments, the event object for the click, the x-value that was clicked (for dates this is millis since epoch), and the closest points along that date. The points have these properties:\n * xval/yval: The data coordinates of the point (with dates/times as millis since epoch) \n * canvasx/canvasy: The canvas coordinates at which the point is drawn. \n name: The name of the data series to which the point belongs"
+    "parameters": [
+      [ "e" , "The event object for the click" ],
+      [ "x" , "The x value that was clicked (for dates, this is milliseconds since epoch)" ],
+      [ "points" , "The closest points along that date. See <a href='#point_properties'>Point properties</a> for details." ]
+    ],
+    "description": "A function to call when the canvas is clicked."
   },
   "yAxisLabelFormatter": {
     "default": "",
index fe69864..9ae04d0 100644 (file)
@@ -255,7 +255,12 @@ DygraphRangeSelector.prototype.initInteraction_ = function() {
         return e.srcElement == self.iePanOverlay_;
     } else {
       // Getting clientX directly from the event is not accurate enough :(
-      var clientX = self.canvasRect_.x + (e.layerX !== undefined ? e.layerX : e.offsetX);
+      var clientX;
+      if (e.offsetX != undefined) {
+        clientX = self.canvasRect_.x + e.offsetX;
+      } else {
+        clientX = e.clientX;
+      }
       var zoomHandleStatus = self.getZoomHandleStatus_();
       return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
     }
@@ -342,24 +347,8 @@ DygraphRangeSelector.prototype.initInteraction_ = function() {
     }
   };
 
-  var interactionModel = {
-    mousedown: function(event, g, context) {
-      context.initializeMouseDown(event, g, context);
-      Dygraph.startPan(event, g, context);
-    },
-    mousemove: function(event, g, context) {
-      if (context.isPanning) {
-        Dygraph.movePan(event, g, context);
-      }
-    },
-    mouseup: function(event, g, context) {
-      if (context.isPanning) {
-        Dygraph.endPan(event, g, context);
-      }
-    }
-  };
-
-  this.dygraph_.attrs_.interactionModel = interactionModel;
+  this.dygraph_.attrs_.interactionModel =
+      Dygraph.Interaction.dragIsPanInteractionModel;
   this.dygraph_.attrs_.panEdgeFraction = 0.0001;
 
   var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
index 8b9dfd9..61aba43 100644 (file)
@@ -35,6 +35,13 @@ Dygraph.ERROR = 3;
 // https://github.com/eriwen/javascript-stacktrace
 Dygraph.LOG_STACK_TRACES = false;
 
+/** A dotted line stroke pattern. */
+Dygraph.DOTTED_LINE = [2, 2];
+/** A dashed line stroke pattern. */
+Dygraph.DASHED_LINE = [7, 3];
+/** A dot dash stroke pattern. */
+Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
+
 /**
  * @private
  * Log an error on the JS console at the given severity.
@@ -693,7 +700,9 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     'clickCallback': true,
     'digitsAfterDecimal': true,
     'drawCallback': true,
+    'drawHighlightPointCallback': true,
     'drawPoints': true,
+    'drawPointCallback': true,
     'drawXGrid': true,
     'drawYGrid': true,
     'fillAlpha': true,
@@ -775,3 +784,133 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
 
   return requiresNewPoints;
 };
+
+/**
+ * Compares two arrays to see if they are equal. If either parameter is not an
+ * array it will return false. Does a shallow compare 
+ * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
+ * @param array1 first array
+ * @param array2 second array
+ * @return True if both parameters are arrays, and contents are equal.
+ */
+Dygraph.compareArrays = function(array1, array2) {
+  if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
+    return false;
+  }
+  if (array1.length !== array2.length) {
+    return false;
+  }
+  for (var i = 0; i < array1.length; i++) {
+    if (array1[i] !== array2[i]) {
+      return false;
+    }
+  }
+  return true;
+};
+
+/**
+ * ctx: the canvas context
+ * sides: the number of sides in the shape.
+ * radius: the radius of the image.
+ * cx: center x coordate
+ * cy: center y coordinate
+ * rotationRadians: the shift of the initial angle, in radians.
+ * delta: the angle shift for each line. If missing, creates a regular
+ *   polygon.
+ */
+Dygraph.regularShape_ = function(
+    ctx, sides, radius, cx, cy, rotationRadians, delta) {
+  rotationRadians = rotationRadians ? rotationRadians : 0;
+  delta = delta ? delta : Math.PI * 2 / sides;
+
+  ctx.beginPath();
+  var first = true;
+  var initialAngle = rotationRadians;
+  var angle = initialAngle;
+
+  var computeCoordinates = function() {
+    var x = cx + (Math.sin(angle) * radius);
+    var y = cy + (-Math.cos(angle) * radius);
+    return [x, y]; 
+  };
+
+  var initialCoordinates = computeCoordinates();
+  var x = initialCoordinates[0];
+  var y = initialCoordinates[1];
+  ctx.moveTo(x, y);
+
+  for (var idx = 0; idx < sides; idx++) {
+    angle = (idx == sides - 1) ? initialAngle : (angle + delta);
+    var coords = computeCoordinates();
+    ctx.lineTo(coords[0], coords[1]);
+  }
+  ctx.stroke();
+  ctx.closePath();
+}
+
+Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) {
+  return function(g, name, ctx, cx, cy, color, radius) {
+    ctx.lineWidth = 1;
+    ctx.strokeStyle = color;
+    Dygraph.regularShape_(ctx, sides, radius, cx, cy, rotationRadians, delta);
+  };
+};
+
+Dygraph.DrawPolygon_ = function(sides, rotationRadians, ctx, cx, cy, color, radius, delta) {
+  new Dygraph.RegularShape_(sides, rotationRadians, delta).draw(ctx, cx, cy, radius);
+}
+
+Dygraph.Circles = {
+  DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
+    ctx.beginPath();
+    ctx.fillStyle = color;
+    ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false);
+    ctx.fill();
+  },
+  TRIANGLE : Dygraph.shapeFunction_(3),
+  SQUARE : Dygraph.shapeFunction_(4, Math.PI / 4),
+  DIAMOND : Dygraph.shapeFunction_(4),
+  PENTAGON : Dygraph.shapeFunction_(5),
+  HEXAGON : Dygraph.shapeFunction_(6),
+  CIRCLE : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.beginPath();
+    ctx.strokeStyle = color;
+    ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
+    ctx.stroke();
+  },
+  STAR : Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5),
+  PLUS : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.lineWidth = 1;
+    ctx.strokeStyle = color;
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy);
+    ctx.lineTo(cx - radius, cy);
+    ctx.closePath();
+    ctx.stroke();
+
+    ctx.beginPath();
+    ctx.moveTo(cx, cy + radius);
+    ctx.lineTo(cx, cy - radius);
+    ctx.closePath();
+
+    ctx.stroke();
+  },
+  EX : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.lineWidth = 1;
+    ctx.strokeStyle = "black";
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy + radius);
+    ctx.lineTo(cx - radius, cy - radius);
+    ctx.closePath();
+    ctx.stroke();
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy - radius);
+    ctx.lineTo(cx - radius, cy + radius);
+    ctx.closePath();
+
+    ctx.stroke();
+  }
+};
index 66432f7..680aa81 100644 (file)
@@ -397,6 +397,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
 
   this.boundaryIds_ = [];
+  this.setIndexByName_ = {};
 
   // Create the containing DIV and other interactive elements
   this.createInterface_();
@@ -451,14 +452,14 @@ Dygraph.prototype.attr_ = function(name, seriesName) {
     Dygraph.OPTIONS_REFERENCE[name] = true;
   }
 // </REMOVE_FOR_COMBINED>
-  if (seriesName &&
+  if (this.user_attrs_ !== null && seriesName &&
       typeof(this.user_attrs_[seriesName]) != 'undefined' &&
       this.user_attrs_[seriesName] !== null &&
       typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
     return this.user_attrs_[seriesName][name];
-  } else if (typeof(this.user_attrs_[name]) != 'undefined') {
+  } else if (this.user_attrs_ !== null && typeof(this.user_attrs_[name]) != 'undefined') {
     return this.user_attrs_[name];
-  } else if (typeof(this.attrs_[name]) != 'undefined') {
+  } else if (this.attrs_ !== null && typeof(this.attrs_[name]) != 'undefined') {
     return this.attrs_[name];
   } else {
     return null;
@@ -830,21 +831,27 @@ Dygraph.prototype.createInterface_ = function() {
   }
 
   var dygraph = this;
-  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
-    dygraph.mouseMove_(e);
-  });
-  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
-    dygraph.mouseOut_(e);
-  });
+  
+  this.mouseMoveHandler = function(e) {
+         dygraph.mouseMove_(e);
+  };
+  Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
+  
+  this.mouseOutHandler = function(e) {
+         dygraph.mouseOut_(e);
+  };
+  Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
 
   this.createStatusMessage_();
   this.createDragInterface_();
 
+  this.resizeHandler = function(e) {
+    dygraph.resize();
+  };
+
   // Update when the window is resized.
   // TODO(danvk): drop frames depending on complexity of the chart.
-  Dygraph.addEvent(window, 'resize', function(e) {
-    dygraph.resize();
-  });
+  Dygraph.addEvent(window, 'resize', this.resizeHandler);
 };
 
 /**
@@ -859,6 +866,10 @@ Dygraph.prototype.destroy = function() {
       node.removeChild(node.firstChild);
     }
   };
+  
+  // remove mouse event handlers
+  Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
+  Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
   removeRecursive(this.maindiv_);
 
   var nullOut = function(obj) {
@@ -868,7 +879,9 @@ Dygraph.prototype.destroy = function() {
       }
     }
   };
-
+  // remove event handlers
+  Dygraph.removeEvent(window,'resize',this.resizeHandler);
+  this.resizeHandler = null;
   // These may not all be necessary, but it can't hurt...
   nullOut(this.layout_);
   nullOut(this.plotter_);
@@ -1327,7 +1340,7 @@ Dygraph.prototype.doUnzoom_ = function() {
   }
 
   for (var i = 0; i < this.axes_.length; i++) {
-    if (this.axes_[i].valueWindow !== null) {
+    if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) {
       dirty = true;
       dirtyY = true;
     }
@@ -1379,7 +1392,8 @@ Dygraph.prototype.doUnzoom_ = function() {
 
       newValueRanges = [];
       for (i = 0; i < this.axes_.length; i++) {
-        newValueRanges.push(this.axes_[i].extremeRange);
+        var axis = this.axes_[i];
+        newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange);
       }
     }
 
@@ -1526,29 +1540,113 @@ Dygraph.prototype.mouseMove_ = function(event) {
 Dygraph.prototype.idxToRow_ = function(idx) {
   if (idx < 0) return -1;
 
-  for (var i in this.layout_.datasets) {
-    if (idx < this.layout_.datasets[i].length) {
-      return this.boundaryIds_[0][0]+idx;
+  // make sure that you get the boundaryIds record which is also defined (see bug #236)
+  var boundaryIdx = -1;
+  for (var i = 0; i < this.boundaryIds_.length; i++) {
+    if (this.boundaryIds_[i] !== undefined) {
+      boundaryIdx = i;
+      break;
+    }
+  }
+  if (boundaryIdx < 0) return -1;
+  for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+    var set = this.layout_.datasets[setIdx];
+    if (idx < set.length) {
+      return this.boundaryIds_[boundaryIdx][0] + idx;
     }
-    idx -= this.layout_.datasets[i].length;
+    idx -= set.length;
   }
   return -1;
 };
 
 /**
  * @private
+ * Generates legend html dash for any stroke pattern. It will try to scale the
+ * pattern to fit in 1em width. Or if small enough repeat the partern for 1em
+ * width.
+ * @param strokePattern The pattern
+ * @param color The color of the series.
+ * @param oneEmWidth The width in pixels of 1em in the legend.
+ */
+Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
+  var dash = "";
+  var i, j, paddingLeft, marginRight;
+  var strokePixelLength = 0, segmentLoop = 0;
+  var normalizedPattern = [];
+  var loop;
+  // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
+  var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+  if(isIE) {
+    return "&mdash;";
+  }
+  if (!strokePattern || strokePattern.length <= 1) {
+    // Solid line
+    dash = "<div style=\"display: inline-block; position: relative; " +
+    "bottom: .5ex; padding-left: 1em; height: 1px; " +
+    "border-bottom: 2px solid " + color + ";\"></div>";
+  } else {
+    // Compute the length of the pixels including the first segment twice, 
+    // since we repeat it.
+    for (i = 0; i <= strokePattern.length; i++) {
+      strokePixelLength += strokePattern[i%strokePattern.length];
+    }
+
+    // See if we can loop the pattern by itself at least twice.
+    loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0]));
+    if (loop > 1) {
+      // This pattern fits at least two times, no scaling just convert to em;
+      for (i = 0; i < strokePattern.length; i++) {
+        normalizedPattern[i] = strokePattern[i]/oneEmWidth;
+      }
+      // Since we are repeating the pattern, we don't worry about repeating the
+      // first segment in one draw.
+      segmentLoop = normalizedPattern.length;
+    } else {
+      // If the pattern doesn't fit in the legend we scale it to fit.
+      loop = 1;
+      for (i = 0; i < strokePattern.length; i++) {
+        normalizedPattern[i] = strokePattern[i]/strokePixelLength;
+      }
+      // For the scaled patterns we do redraw the first segment.
+      segmentLoop = normalizedPattern.length+1;
+    }
+    // Now make the pattern.
+    for (j = 0; j < loop; j++) {
+      for (i = 0; i < segmentLoop; i+=2) {
+        // The padding is the drawn segment.
+        paddingLeft = normalizedPattern[i%normalizedPattern.length];
+        if (i < strokePattern.length) {
+          // The margin is the space segment.
+          marginRight = normalizedPattern[(i+1)%normalizedPattern.length];
+        } else {
+          // The repeated first segment has no right margin.
+          marginRight = 0;
+        }
+        dash += "<div style=\"display: inline-block; position: relative; " +
+          "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
+          paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
+          ";\"></div>";
+      }
+    }
+  }
+  return dash;
+};
+
+/**
+ * @private
  * Generates HTML for the legend which is displayed when hovering over the
  * chart. If no selected points are specified, a default legend is returned
  * (this may just be the empty string).
  * @param { Number } [x] The x-value of the selected points.
  * @param { [Object] } [sel_points] List of selected points for the given
  * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
+ * @param { Number } [oneEmWidth] The pixel width for 1em in the legend.
  */
-Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
+Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
   // If no points are selected, we display a default legend. Traditionally,
   // this has been blank. But a better default would be a conventional legend,
   // which provides essential information for a non-interactive chart.
-  var html, sepLines, i, c;
+  var html, sepLines, i, c, dash, strokePattern;
   if (typeof(x) === 'undefined') {
     if (this.attr_('legend') != 'always') return '';
 
@@ -1559,8 +1657,10 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
       if (!this.visibility()[i - 1]) continue;
       c = this.plotter_.colors[labels[i]];
       if (html !== '') html += (sepLines ? '<br/>' : ' ');
-      html += "<b><span style='color: " + c + ";'>&mdash;" + labels[i] +
-        "</span></b>";
+      strokePattern = this.attr_("strokePattern", labels[i]);
+      dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
+      html += "<span style='font-weight: bold; color: " + c + ";'>" + dash + 
+        " " + labels[i] + "</span>";
     }
     return html;
   }
@@ -1603,8 +1703,14 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
  * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
  */
 Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
-  var html = this.generateLegendHTML_(x, sel_points);
   var labelsDiv = this.attr_("labelsDiv");
+  var sizeSpan = document.createElement('span');
+  // Calculates the width of 1em in pixels for the legend.
+  sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+  labelsDiv.appendChild(sizeSpan);
+  var oneEmWidth=sizeSpan.offsetWidth;
+
+  var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
   if (labelsDiv !== null) {
     labelsDiv.innerHTML = html;
   } else {
@@ -1655,10 +1761,12 @@ Dygraph.prototype.updateSelection_ = function() {
       if (!Dygraph.isOK(pt.canvasy)) continue;
 
       var circleSize = this.attr_('highlightCircleSize', pt.name);
-      ctx.beginPath();
-      ctx.fillStyle = this.plotter_.colors[pt.name];
-      ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
-      ctx.fill();
+      var callback = this.attr_("drawHighlightPointCallback", pt.name);
+      if (!callback) {
+        callback = Dygraph.Circles.DEFAULT;
+      }
+      callback(this.g, pt.name, ctx, canvasx, pt.canvasy,
+          this.plotter_.colors[pt.name], circleSize);
     }
     ctx.restore();
 
@@ -1683,8 +1791,9 @@ Dygraph.prototype.setSelection = function(row) {
   }
 
   if (row !== false && row >= 0) {
-    for (var i in this.layout_.datasets) {
-      if (row < this.layout_.datasets[i].length) {
+    for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+      var set = this.layout_.datasets[setIdx];
+      if (row < set.length) {
         var point = this.layout_.points[pos+row];
 
         if (this.attr_("stackedGraph")) {
@@ -1693,7 +1802,7 @@ Dygraph.prototype.setSelection = function(row) {
 
         this.selPoints_.push(point);
       }
-      pos += this.layout_.datasets[i].length;
+      pos += set.length;
     }
   }
 
@@ -2019,9 +2128,15 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) {
   var extremes = packed[1];
   this.boundaryIds_ = packed[2];
 
+  this.setIndexByName_ = {};
+  var labels = this.attr_("labels");
+  if (labels.length > 0) {
+    this.setIndexByName_[labels[0]] = 0;
+  }
   for (var i = 1; i < datasets.length; i++) {
+    this.setIndexByName_[labels[i]] = i;
     if (!this.visibility()[i - 1]) continue;
-    this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
+    this.layout_.addDataset(labels[i], datasets[i]);
   }
 
   this.computeYAxisRanges_(extremes);
@@ -2090,7 +2205,7 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
 Dygraph.prototype.computeYAxes_ = function() {
   // Preserve valueWindow settings if they exist, and if the user hasn't
   // specified a new valueRange.
-  var i, valueWindows, seriesName, axis, index;
+  var i, valueWindows, seriesName, axis, index, opts, v;
   if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
     valueWindows = [];
     for (index = 0; index < this.axes_.length; index++) {
@@ -2122,7 +2237,7 @@ Dygraph.prototype.computeYAxes_ = function() {
   // Copy global axis options over to the first axis.
   for (i = 0; i < axisOptions.length; i++) {
     var k = axisOptions[i];
-    var v = this.attr_(k);
+    v = this.attr_(k);
     if (v) this.axes_[0][k] = v;
   }
 
@@ -2136,7 +2251,7 @@ Dygraph.prototype.computeYAxes_ = function() {
     }
     if (typeof(axis) == 'object') {
       // Add a new axis, making a copy of its per-axis options.
-      var opts = {};
+      opts = {};
       Dygraph.update(opts, this.axes_[0]);
       Dygraph.update(opts, { valueRange: null });  // shouldn't inherit this.
       var yAxisId = this.axes_.length;
@@ -2170,6 +2285,22 @@ Dygraph.prototype.computeYAxes_ = function() {
       this.axes_[index].valueWindow = valueWindows[index];
     }
   }
+
+  // New axes options
+  for (axis = 0; axis < this.axes_.length; axis++) {
+    if (axis === 0) {
+      opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
+      v = opts("valueRange");
+      if (v) this.axes_[axis].valueRange = v;
+    } else {  // To keep old behavior
+      var axes = this.user_attrs_.axes;
+      if (axes && axes.y2) {
+        v = axes.y2.valueRange;
+        if (v) this.axes_[axis].valueRange = v;
+      }
+    }
+  }
+
 };
 
 /**
@@ -2774,6 +2905,19 @@ Dygraph.prototype.parseArray_ = function(data) {
  * @private
  */
 Dygraph.prototype.parseDataTable_ = function(data) {
+  var shortTextForAnnotationNum = function(num) {
+    // converts [0-9]+ [A-Z][a-z]*
+    // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
+    // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
+    var shortText = String.fromCharCode(65 /* A */ + num % 26);
+    num = Math.floor(num / 26);
+    while ( num > 0 ) {
+      shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
+      num = Math.floor((num - 1) / 26);
+    }
+    return shortText;
+  }
+
   var cols = data.getNumberOfColumns();
   var rows = data.getNumberOfRows();
 
@@ -2855,7 +2999,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
           var ann = {};
           ann.series = data.getColumnLabel(col);
           ann.xval = row[0];
-          ann.shortText = String.fromCharCode(65 /* A */ + annotations.length);
+          ann.shortText = shortTextForAnnotationNum(annotations.length);
           ann.text = '';
           for (var k = 0; k < annotationCols[col].length; k++) {
             if (k) ann.text += "\n";
@@ -3162,15 +3306,19 @@ Dygraph.prototype.annotations = function() {
 };
 
 /**
+ * Get the list of label names for this graph. The first column is the
+ * x-axis, so the data series names start at index 1.
+ */
+Dygraph.prototype.getLabels = function(name) {
+  return this.attr_("labels").slice();
+};
+
+/**
  * Get the index of a series (column) given its name. The first column is the
  * x-axis, so the data series start with index 1.
  */
 Dygraph.prototype.indexFromSetName = function(name) {
-  var labels = this.attr_("labels");
-  for (var i = 0; i < labels.length; i++) {
-    if (labels[i] == name) return i;
-  }
-  return null;
+  return this.setIndexByName_[name];
 };
 
 /**
index 38a5b68..08efc6f 100644 (file)
@@ -62,6 +62,12 @@ function offsetToPercentage(g, offsetX, offsetY) {
 function dblClickV3(event, g, context) {
   // Reducing by 20% makes it 80% the original size, which means
   // to restore to original size it must grow by 25%
+
+  if (!(event.offsetX && event.offsetY)){
+    event.offsetX = event.layerX - event.target.offsetLeft;
+    event.offsetY = event.layerY - event.target.offsetTop;
+  }
+
   var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
   var xPct = percentages[0];
   var yPct = percentages[1];
@@ -89,6 +95,11 @@ function scrollV3(event, g, context) {
   // that verbatim, it would be a 7.5%.
   var percentage = normal / 50;
 
+  if (!(event.offsetX && event.offsetY)){
+    event.offsetX = event.layerX - event.target.offsetLeft;
+    event.offsetY = event.layerY - event.target.offsetTop;
+  }
+
   var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
   var xPct = percentages[0];
   var yPct = percentages[1];
index 2dd5b98..613dec7 100755 (executable)
@@ -21,6 +21,8 @@ for line in file('dygraph-options-reference.js'):
   elif '</JSON>' in line:
     in_json = False
   elif in_json:
+    if line.endswith("\\\n"): # hacked in line continuation support with trailing \.
+      line = line[:-2]
     js += line
 
 # TODO(danvk): better errors here.
@@ -71,7 +73,7 @@ for nu, opt in docs.iteritems():
     if label not in labels:
       labels.append(label)
 
-print """
+print """<!DOCTYPE HTML>
 <html>
 <head>
   <title>Dygraphs Options Reference</title>
@@ -80,6 +82,9 @@ print """
     p.option {
       padding-left: 25px;
     }
+    div.parameters {
+      padding-left: 15px;
+    }
     #nav {
       position: fixed;
     }
@@ -92,7 +97,7 @@ print """
 """
 
 print """
-<div id=nav>
+<div id='nav'>
 <h2>Dygraphs</h2>
 <ul>
   <li><a href="index.html">Home</a>
@@ -112,12 +117,12 @@ def name(f):
   return f.replace('tests/', '').replace('.html', '')
 
 print """
-<div id=content>
+<div id='content'>
 <h2>Options Reference</h2>
 <p>Dygraphs tries to do a good job of displaying your data without any further configuration. But inevitably, you're going to want to tinker. Dygraphs provides a rich set of options for configuring its display and behavior.</p>
 
-<a name="usage"><h3>Usage</h3>
-<p>You specify options in the third parameter to the dygraphs constructor:
+<a name="usage"></a><h3>Usage</h3>
+<p>You specify options in the third parameter to the dygraphs constructor:</p>
 <pre>g = new Dygraph(div,
                 data,
                 {
@@ -127,13 +132,12 @@ print """
                 });
 </pre>
 
-After you've created a Dygraph, you can change an option by calling the <code>updateOptions</code> method:
+<p>After you've created a Dygraph, you can change an option by calling the <code>updateOptions</code> method:</p>
 <pre>g.updateOptions({
                   new_option1: value1,
                   new_option2: value2
                 });
 </pre>
-
 <p>And, without further ado, here's the complete list of options:</p>
 """
 for label in sorted(labels):
@@ -149,25 +153,40 @@ for label in sorted(labels):
       examples_html = ' '.join(
         '<a href="%s">%s</a>' % (f, name(f)) for f in tests)
 
+    if 'parameters' in opt:
+      parameters = opt['parameters']
+      parameters_html = '\n'.join("<i>%s</i>: %s<br/>" % (p[0], p[1]) for p in parameters)
+      parameters_html = "\n  <div class='parameters'>\n%s</div>" % (parameters_html);
+    else:
+      parameters_html = ''
+
     if not opt['type']: opt['type'] = '(missing)'
     if not opt['default']: opt['default'] = '(missing)'
     if not opt['description']: opt['description'] = '(missing)'
 
     print """
-  <p class='option'><a name="%(name)s"/><b>%(name)s</b><br/>
+  <div class='option'><a name="%(name)s"></a><b>%(name)s</b><br/>
   %(desc)s<br/>
-  <i>Type: %(type)s<br/>
-  Default: %(default)s</i><br/>
+  <i>Type: %(type)s</i><br/>%(parameters)s
+  <i>Default: %(default)s</i><br/>
   Examples: %(examples_html)s<br/>
-  <br/>
+  <br/></div>
   """ % { 'name': opt_name,
           'type': opt['type'],
+          'parameters': parameters_html,
           'default': opt['default'],
           'desc': opt['description'],
           'examples_html': examples_html}
 
 
 print """
+<a name="point_properties"></a><h3>Point Properties</h3>
+Some callbacks take a point argument. Its properties are:<br/>
+<ul>
+<li>xval/yval: The data coordinates of the point (with dates/times as millis since epoch)</li>
+<li>canvasx/canvasy: The canvas coordinates at which the point is drawn.</li>
+<li>name: The name of the data series to which the point belongs</li>
+</ul>
 </div>
 </body>
 </html>
index 33bb7a9..c5826cb 100755 (executable)
@@ -18,7 +18,7 @@ if [ -s docs/options.html ] ; then
   ./generate-jsdoc.sh
 
   # Copy everything to the site.
-  scp -r gallery tests jsdoc experimental $site \
+  scp -r gallery common tests jsdoc experimental $site \
   && \
   scp dygraph*.js gadget.xml excanvas.js thumbnail.png screenshot.png docs/* $site/
 else
index c043894..ec458b0 100644 (file)
         data.addColumn('string', 'title2');
         data.addColumn('string', 'text2');
         data.addRows([
-          [new Date(2008, 1 ,1), 30000, undefined, undefined, 40645, undefined, undefined],
-          [new Date(2008, 1 ,2), 14045, undefined, undefined, 20374, undefined, undefined],
-          [new Date(2008, 1 ,3), 55022, undefined, undefined, 50766, undefined, undefined],
-          [new Date(2008, 1 ,4), 75284, undefined, undefined, 14334, 'Out of Stock','Ran out of stock on pens at 4pm'],
-          [new Date(2008, 1 ,5), 41476, 'Bought Pens','Bought 200k pens', 66467, undefined, undefined],
-          [new Date(2008, 1 ,6), 33322, undefined, undefined, 39463, undefined, undefined]
+          [new Date(2008, 11 ,1), 30000, undefined, undefined, 40645, undefined, undefined],
+          [new Date(2008, 11 ,2), 14045, undefined, undefined, 20374, undefined, undefined],
+          [new Date(2008, 11 ,3), 55022, undefined, undefined, 50766, undefined, undefined],
+          [new Date(2008, 11 ,4), 75284, undefined, undefined, 14334, 'Out of Stock','Ran out of stock on pens at 4pm'],
+          [new Date(2008, 11 ,5), 41476, 'Bought Pens','Bought 200k pens', 66467, undefined, undefined],
+          [new Date(2008, 11 ,6), 33322, undefined, undefined, 39463, undefined, undefined]
         ]);
 
+        for (var i = 1; i < 14; i++) {
+          data.addRows([
+            [new Date(2008, 11 , 6 + i), i * 1000, 'title1-' + i, 'text1 ' + (i * 1000 - i), (i * 2000 + i), 'title2-' + i, 'text2' + i * 1000],
+          ]);
+        }
+
         var chart = new google.visualization.AnnotatedTimeLine(document.getElementById('gviz_div'));
         chart.draw(data, {displayAnnotations: true});
 
@@ -50,4 +56,3 @@
     <div id='dg_div' style='width: 700px; height: 240px;'></div>
   </body>
 </html>
-
diff --git a/tests/custom-circles.html b/tests/custom-circles.html
new file mode 100644 (file)
index 0000000..7a30ea2
--- /dev/null
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Custom Circles</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <!--
+    For production (minified) code, use:
+    <script type="text/javascript" src="dygraph-combined.js"></script>
+    -->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+  </head>
+  <body>
+    <h2>Custom circles and hover circles</h2>
+    <div id="demodiv"></div>
+
+    <script type="text/javascript">
+      var smile = function(g, series, ctx, cx, cy, color, radius) {
+        mouthlessFace(g, series, ctx, cx, cy, color, radius);
+
+        ctx.fillStyle = "#000000";
+        ctx.beginPath();
+        ctx.arc(cx, cy, radius - 2, .3, Math.PI - .3, false);
+        ctx.stroke();
+      };
+
+      var frown = function(g, series, ctx, cx, cy, color, radius) {
+        mouthlessFace(g, series, ctx, cx, cy, color, radius);
+
+        ctx.fillStyle = "#000000";
+        ctx.beginPath();
+        ctx.arc(cx, cy + radius, radius - 2, Math.PI + .3, -.3, false);
+        ctx.stroke();
+      };
+
+      var mouthlessFace = function(g, series, ctx, cx, cy, color, radius) {
+        ctx.strokeStyle = "#000000";
+        ctx.fillStyle = "#FFFF00";
+        ctx.beginPath();
+        ctx.arc(cx, cy, radius, Math.PI * 2, false);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+
+        ctx.fillStyle = "#000000";
+        ctx.beginPath();
+        ctx.arc(cx - (radius / 3) , cy - (radius / 4), 1, Math.PI * 2, false);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+
+        ctx.beginPath();
+        ctx.arc(cx + (radius / 3) , cy - (radius / 4), 1, Math.PI * 2, false);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+      };
+
+      g = new Dygraph(
+          document.getElementById("demodiv"),
+          function() {
+
+            var r = "xval,default,triangle,square,diamond,pentagon,hexagon,circle,star,plus,ex,custom\n";
+            for (var i=1; i<=20; i++) {
+              r += i;
+              for (var j = 0; j < 11; j++) {
+                r += "," + j + (i / 3);
+              }
+              r += "\n";
+            }
+            return r;
+          },
+          {
+            drawPoints : true,
+            pointSize : 5,
+            highlightCircleSize : 8,
+            'default' : {
+              drawPointCallback : Dygraph.Circles.DEFAULT,
+              drawHighlightPointCallback : Dygraph.Circles.DEFAULT
+            },
+            'triangle' : {
+              drawPointCallback : Dygraph.Circles.TRIANGLE,
+              drawHighlightPointCallback : Dygraph.Circles.TRIANGLE
+            },
+            'square' : {
+              drawPointCallback : Dygraph.Circles.SQUARE,
+              drawHighlightPointCallback : Dygraph.Circles.SQUARE
+            },
+            'diamond' : {
+              drawPointCallback : Dygraph.Circles.DIAMOND,
+              drawHighlightPointCallback : Dygraph.Circles.DIAMOND
+            },
+            'pentagon' : {
+              drawPointCallback : Dygraph.Circles.PENTAGON,
+              drawHighlightPointCallback : Dygraph.Circles.PENTAGON
+            },
+            'hexagon' : {
+              drawPointCallback : Dygraph.Circles.HEXAGON,
+              drawHighlightPointCallback : Dygraph.Circles.HEXAGON
+            },
+            'circle' : {
+              drawPointCallback : Dygraph.Circles.CIRCLE,
+              drawHighlightPointCallback : Dygraph.Circles.CIRCLE
+            },
+            'star' : {
+              drawPointCallback : Dygraph.Circles.STAR,
+              drawHighlightPointCallback : Dygraph.Circles.STAR
+            },
+            'plus' : {
+              drawPointCallback : Dygraph.Circles.PLUS,
+              drawHighlightPointCallback : Dygraph.Circles.PLUS
+            },
+            'ex' : {
+              drawPointCallback : Dygraph.Circles.EX,
+              drawHighlightPointCallback : Dygraph.Circles.EX
+            },
+            'custom' : {
+              drawPointCallback : frown,
+              drawHighlightPointCallback : smile
+            }
+          }
+      );
+    </script>
+</body>
+</html>
index 7dc69f7..7aadb19 100644 (file)
   <body>
     <p>Plot which can be easily generated with different numbers of points for
        benchmarking/profiling and improving performance of dygraphs.</p>    
-    <p>Data to plot:
-      <input type="radio" id="sine" name="group1" value="sine"
-        onclick="clickedRadioButton(this);" checked> sinusoid function
-      <input type="radio" id="rand" name="group1" value="rand"
-        onclick="clickedRadioButton(this);"> random points <br></p>
-    <p>Number of points:
-       <input type="text" id="points" size="20"></p>
-    <p>Number of series:
-       <input type="text" id="series" size="20"></p>
-    <p>Roll period (in points):
-      <input type="text" id="rollPeriod" size="20"></p>
-    <p>Repetitions:
-      <input type="text" id="repetitions" size="20"></p>
-
-    <input type="button" value="Go!" onclick="updatePlot();">
+    <div id='parameters'>
+      <p>Data to plot:
+        <input type="radio" id="sine" name="group1" value="sine"
+          onclick="clickedRadioButton(this);" checked> sinusoid function
+        <input type="radio" id="rand" name="group1" value="rand"
+          onclick="clickedRadioButton(this);"> random points <br></p>
+      <p>Number of points:
+         <input type="text" id="points" size="20"></p>
+      <p>Number of series:
+         <input type="text" id="series" size="20"></p>
+      <p>Roll period (in points):
+        <input type="text" id="rollPeriod" size="20"></p>
+      <p>Repetitions:
+        <input type="text" id="repetitions" size="20"></p>
+      <input type="button" value="Go!" onclick="updatePlot();">
+    </div>
     <br>
     <br>
     <div id="plot"></div>
@@ -39,7 +40,8 @@
     <div id="metaperformance"></div>
 
     <script type="text/javascript">
-      var plot;
+      var graph = null;
+      var metrics = null;
       var dataType = "sine";
 
       var durations = [];
         var opts = {labels: labels, rollPeriod: rollPeriod, timingName: "x"};
         var millisecondss = [];
         for (var i = 0; i < repetitions; i++) {
+          if (graph != null) {
+            graph.destroy(); // release memory from prior graph.
+          }
           var start = new Date();
-          plot = new Dygraph(plotDiv, data, opts);
+          graph = new Dygraph(plotDiv, data, opts);
           var end = new Date();
           durations.push([start, end - start]);
           millisecondss.push(end - start);
 
         if (durations.length > 0) {
           var start2 = new Date();
-          new Dygraph(
-              document.getElementById('metrics'),
-              durations,
-              {
-                highlightCircleSize: 4,
-                labels: [ "Date", "ms" ]
-                });
+          if (!metrics) {
+             metrics = new Dygraph(
+                document.getElementById('metrics'),
+                durations,
+                {
+                  highlightCircleSize: 4,
+                  labels: [ "Date", "ms" ]
+                  });
+          } else {
+            metrics.updateOptions({file: durations});
+          }
           var end2 = new Date();
           document.getElementById("metaperformance").innerHTML =
               "completed in " + (end2 - start2) + " milliseconds.";
index ced5aef..08efc6f 100644 (file)
@@ -62,6 +62,12 @@ function offsetToPercentage(g, offsetX, offsetY) {
 function dblClickV3(event, g, context) {
   // Reducing by 20% makes it 80% the original size, which means
   // to restore to original size it must grow by 25%
+
+  if (!(event.offsetX && event.offsetY)){
+    event.offsetX = event.layerX - event.target.offsetLeft;
+    event.offsetY = event.layerY - event.target.offsetTop;
+  }
+
   var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
   var xPct = percentages[0];
   var yPct = percentages[1];
@@ -89,6 +95,11 @@ function scrollV3(event, g, context) {
   // that verbatim, it would be a 7.5%.
   var percentage = normal / 50;
 
+  if (!(event.offsetX && event.offsetY)){
+    event.offsetX = event.layerX - event.target.offsetLeft;
+    event.offsetY = event.layerY - event.target.offsetTop;
+  }
+
   var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
   var xPct = percentages[0];
   var yPct = percentages[1];
@@ -184,7 +195,7 @@ function upV4(event, g, context) {
 }
 
 function dblClickV4(event, g, context) {
-  restorePositioning(g4);
+  restorePositioning(g);
 }
 
 function drawV4(x, y) {
index c1b400a..98501ac 100644 (file)
   <body>
     <h2>Chart with per-series properties</h2>
     <div id="demodiv"></div>
-
+    <h2>Chart with per-series properties with legend.</h2>
+    <div id="demodiv2"></div>
     <script type="text/javascript">
+      data = function() {
+        var zp = function(x) { if (x < 10) return "0"+x; else return x; };
+        var r = "date,parabola,line,another line,sine wave,sine wave2\n";
+        for (var i=1; i<=31; i++) {
+          r += "200610" + zp(i);
+          r += "," + 10*(i*(31-i));
+          r += "," + 10*(8*i);
+          r += "," + 10*(250 - 8*i);
+          r += "," + 10*(125 + 125 * Math.sin(0.3*i));
+          r += "," + 10*(125 + 125 * Math.sin(0.3*i+Math.PI));
+          r += "\n";
+        }
+        return r;
+      };
       g = new Dygraph(
               document.getElementById("demodiv"),
-              function() {
-                var zp = function(x) { if (x < 10) return "0"+x; else return x; };
-                var r = "date,parabola,line,another line,sine wave\n";
-                for (var i=1; i<=31; i++) {
-                r += "200610" + zp(i);
-                r += "," + 10*(i*(31-i));
-                r += "," + 10*(8*i);
-                r += "," + 10*(250 - 8*i);
-                r += "," + 10*(125 + 125 * Math.sin(0.3*i));
-                r += "\n";
-                }
-                return r;
-              },
+              data,
               {
                 strokeWidth: 2,
                 'parabola': {
                 'sine wave': {
                   strokeWidth: 3,
                   highlightCircleSize: 10
+                },
+                'sine wave2': {
+                  strokePattern: [10, 2, 5, 2],
+                  strokeWidth: 2,
+                  highlightCircleSize: 3
+                }
+              }
+          );
+      g2 = new Dygraph(
+              document.getElementById("demodiv2"),
+              data,
+              {
+                legend: 'always',
+                strokeWidth: 2,
+                'parabola': {
+                  strokePattern: null,
+                  drawPoints: true,
+                  pointSize: 4,
+                  highlightCircleSize: 6
+                },
+                'line': {
+                  strokePattern: Dygraph.DASHED_LINE,
+                  strokeWidth: 1.0,
+                  drawPoints: true,
+                  pointSize: 1.5
+                },
+                'another line': {
+                  strokePattern: [25, 5]
+                },
+                'sine wave': {
+                  strokePattern: Dygraph.DOTTED_LINE,
+                  strokeWidth: 3,
+                  highlightCircleSize: 10
+                },
+                'sine wave2': {
+                  strokePattern: Dygraph.DOT_DASH_LINE,
+                  strokeWidth: 2,
+                  highlightCircleSize: 3
                 }
               }
           );
+
     </script>
 </body>
 </html>
diff --git a/tests/two-axes-vr.html b/tests/two-axes-vr.html
new file mode 100644 (file)
index 0000000..5814435
--- /dev/null
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Multiple y-axes with valueRange</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <!--
+    For production (minified) code, use:
+    <script type="text/javascript" src="dygraph-combined.js"></script>
+    -->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+  </head>
+  <body>
+    <h2>Multiple y-axes with valueRange</h2>
+    <p>The same data with both different valueRanges. Two-axis old y[40, 70] valueRange:</p>
+    <div id="demodiv" style="width: 640; height: 350; border: 1px solid black"></div>
+    <p>Two-axis new valueRange y[40, 80] set:</p>
+    <div id="demodiv_one" style="width: 640; height: 350; border: 1px solid black"></div>
+    <p>Two-axis new valueRange y[40, 80] &amp; y2[1e6, 1.2e6] set:</p>
+    <div id="demodiv_two" style="width: 640; height: 350; border: 1px solid black"></div>
+    <script type="text/javascript">
+      var data = [];
+      for (var i = 1; i <= 100; i++) {
+        var m = "01", d = i;
+        if (d > 31) { m = "02"; d -= 31; }
+        if (m == "02" && d > 28) { m = "03"; d -= 28; }
+        if (m == "03" && d > 31) { m = "04"; d -= 31; }
+        if (d < 10) d = "0" + d;
+        // two series, one with range 1-100, one with range 1-2M
+        data.push([new Date("2010/" + m + "/" + d),
+                   i,
+                   100 - i,
+                   1e6 * (1 + i * (100 - i) / (50 * 50)),
+                   1e6 * (2 - i * (100 - i) / (50 * 50))]);
+      }
+
+      g = new Dygraph(
+          document.getElementById("demodiv"),
+          data,
+          {
+            labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+            'Y3': {
+              axis: {
+              }
+            },
+            'Y4': {
+              axis: 'Y3'  // use the same y-axis as series Y3
+            },
+            valueRange: [40, 70],
+            axes: {
+              y2: {
+                // set axis-related properties here
+                labelsKMB: true
+              }
+            },
+            ylabel: 'Primary y-axis',
+            y2label: 'Secondary y-axis',
+            yAxisLabelWidth: 60
+          }
+      );
+
+      g2 = new Dygraph(
+          document.getElementById("demodiv_one"),
+          data,
+          {
+            labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+            'Y3': {
+              axis: {
+              }
+            },
+            'Y4': {
+              axis: 'Y3'  // use the same y-axis as series Y3
+            },
+            axes: {
+              y: {
+                valueRange: [40, 80]
+              },
+              y2: {
+                // set axis-related properties here
+                labelsKMB: true
+              }
+            },
+            ylabel: 'Primary y-axis',
+            y2label: 'Secondary y-axis',
+            yAxisLabelWidth: 60
+          }
+      );
+
+      g2 = new Dygraph(
+          document.getElementById("demodiv_two"),
+          data,
+          {
+            labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+            'Y3': {
+              axis: {
+              }
+            },
+            'Y4': {
+              axis: 'Y3'  // use the same y-axis as series Y3
+            },
+            axes: {
+              y: {
+                valueRange: [40, 80]
+              },
+              y2: {
+                // set axis-related properties here
+                valueRange: [1e6, 1.2e6],
+                labelsKMB: true
+              }
+            },
+            ylabel: 'Primary y-axis',
+            y2label: 'Secondary y-axis',
+            yAxisLabelWidth: 60
+          }
+      );
+
+
+    </script>
+</body>
+</html>