Add 'plotter' option, which allows custom drawing.
authorDan Vanderkam <dan@dygraphs.com>
Tue, 31 Jul 2012 20:03:49 +0000 (16:03 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Tue, 31 Jul 2012 20:03:49 +0000 (16:03 -0400)
This can be used to create bar charts, mixed bar/line charts, box and whisker
charts, chandle charts, or whatever you can dream up.

Squashed commit of the following:

commit 0d5ce69dda5f02db7a4576b220f8cb7bc45cd197
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Tue Jul 31 12:38:43 2012 -0400

    bug fix

commit 15e29ef96fc41e73512edf7c404462cfa6f7ed3c
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Tue Jul 31 12:37:21 2012 -0400

    kberg comments

commit 0a61073b21bb92002f9808356be02e015b584450
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Tue Jul 31 11:04:49 2012 -0400

    copy over more stuff in push-to-web

commit 3b9f9d1b6ca68a61d593ff909f2cdab2827b3410
Merge: 7f808fd 85e975f
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Tue Jul 31 10:49:06 2012 -0400

    Merge branch 'master' into plotter-option

commit 7f808fd2944d486d567a07768c1849aad1cb2768
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Tue Jul 31 10:28:19 2012 -0400

    mixed error/line

commit 5b5d1c37504749fd35ab1d862bcfc871a99ba422
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Mon Jul 30 21:28:04 2012 -0400

    tweaks to plotters demo

commit 371163baa1086f99e126a5896acb09a535c019c2
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Mon Jul 30 21:27:50 2012 -0400

    Dygraph.Plotters

commit 6446f7c315a7f58e9daa39a2c1cbd2c877ae7870
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Mon Jul 30 21:08:49 2012 -0400

    a few more plotter examples

commit 7867dbdf2b4733de76d3ed8d999336ba58ccba86
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Mon Jul 30 18:55:02 2012 -0400

    cleanup & comment

commit d8768ade1b85c81af5df52b7425f870f420babfc
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sun Jul 29 20:08:01 2012 -0400

    fix highlightSeriesOpts issue

commit bfbf2452e7f34088de46242c5bf54e3904c6a49d
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sun Jul 29 19:47:20 2012 -0400

    more cleanup

commit f8fdb6b618443b2a74ce5861f2aa0ce1206320e3
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sun Jul 29 19:43:43 2012 -0400

    reduce number of params to various line drawing function

commit 2d101ef9d34a9a52a7051cc2698f12f435854a04
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sun Jul 29 19:25:15 2012 -0400

    only use fillPlotter when necessary

commit 0a7c6dca3e21261874b6c88c02c626e9c5f5cad9
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sun Jul 29 19:22:32 2012 -0400

    fix issue with fillPlotter & visibility

commit fb86aa8b43bd02f594ea3c05840d0d40d965b1be
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sat Jul 28 19:10:24 2012 -0400

    restructure, comments

commit c77fe2f37e60d394d1cffaf1126955d014b44838
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sat Jul 28 17:52:52 2012 -0400

    port over fillGraph plotter

commit 04b5eb3b52be9f2c12f959378f8ae35c22008ded
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sat Jul 28 17:44:58 2012 -0400

    add candle chart demo

commit 27737ae38eac6b6096687d8960f8b34fd4ec8dfa
Merge: b81d3fc 6f8f19e
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Wed Jul 25 17:49:37 2012 -0400

    Merge branch 'master' into plotter-option

commit b81d3fcbceaa115d16710a328747b8a7b2af9788
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Wed Jul 25 17:47:27 2012 -0400

    pass all series plus seriesIndex to plotters

commit 727229cbe532d2088f59d886ab368df4d499d5d8
Merge: fd784bf 16879a6
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Wed Jul 25 17:43:51 2012 -0400

    merge upstream changes

commit fd784bf9c0d18173238c921a4fde8fc35946b9c0
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Tue Jul 24 19:50:02 2012 -0400

    drawXgrid

commit b638898ad332edc71ed26528604eab94bcc791be
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sat Jul 21 20:29:51 2012 -0400

    Bar chart plotter demo

commit 0eeb143cb02d29e38bda87a273be737c7623686e
Author: Dan Vanderkam <dan@dygraphs.com>
Date:   Sat Jul 21 20:06:51 2012 -0400

    add line & error plotters; highlightSeriesOpts is broken

dygraph-canvas.js
dygraph-options-reference.js
dygraph.js
push-to-web.sh
tests/fillGraph.html
tests/plotters.html [new file with mode: 0644]

index 825a4c2..5286ad5 100644 (file)
@@ -87,8 +87,13 @@ var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
   }
 };
 
-DygraphCanvasRenderer.prototype.attr_ = function(x) {
-  return this.dygraph_.attr_(x);
+/**
+ * This just forwards to dygraph.attr_.
+ * TODO(danvk): remove this?
+ * @private
+ */
+DygraphCanvasRenderer.prototype.attr_ = function(name, opt_seriesName) {
+  return this.dygraph_.attr_(name, opt_seriesName);
 };
 
 /**
@@ -152,6 +157,10 @@ DygraphCanvasRenderer.isSupported = function(canvasName) {
  * @private
  */
 DygraphCanvasRenderer.prototype.render = function() {
+  // attaches point.canvas{x,y}
+  this._updatePoints();
+
+  // actually draws the chart.
   this._renderLineChart();
 };
 
@@ -243,32 +252,39 @@ DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
 };
 
 /**
+ * Draws a line with the styles passed in and calls all the drawPointCallbacks.
+ * @param {Object} e The dictionary passed to the plotter function.
  * @private
  */
-DygraphCanvasRenderer.prototype._drawStyledLine = function(
-    ctx, setIdx, setName, color, strokeWidth, strokePattern, drawPoints,
+DygraphCanvasRenderer._drawStyledLine = function(e,
+    color, strokeWidth, strokePattern, drawPoints,
     drawPointCallback, pointSize) {
+  var g = e.dygraph;
   // TODO(konigsberg): Compute attributes outside this method call.
-  var stepPlot = this.attr_("stepPlot");
+  var stepPlot = g.getOption("stepPlot");  // TODO(danvk): per-series
   if (!Dygraph.isArrayLike(strokePattern)) {
     strokePattern = null;
   }
-  var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName);
 
-  var points = this.layout.points[setIdx];
+  var drawGapPoints = g.getOption('drawGapEdgePoints', e.setName);
+
+  var points = e.points;
   var iter = Dygraph.createIterator(points, 0, points.length,
       DygraphCanvasRenderer._getIteratorPredicate(
-          this.attr_("connectSeparatedPoints")));
+          g.getOption("connectSeparatedPoints")));  // TODO(danvk): per-series?
 
   var stroking = strokePattern && (strokePattern.length >= 2);
 
+  var ctx = e.drawingContext;
   ctx.save();
   if (stroking) {
     ctx.installPattern(strokePattern);
   }
 
-  var pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color);
-  this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize);
+  var pointsOnLine = DygraphCanvasRenderer._drawSeries(
+      e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color);
+  DygraphCanvasRenderer._drawPointsOnLine(
+      e, pointsOnLine, drawPointCallback, color, pointSize);
 
   if (stroking) {
     ctx.uninstallPattern();
@@ -277,19 +293,16 @@ DygraphCanvasRenderer.prototype._drawStyledLine = function(
   ctx.restore();
 };
 
-DygraphCanvasRenderer.prototype._drawPointsOnLine = function(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize) {
-  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();
-  }
-}
-
-DygraphCanvasRenderer.prototype._drawSeries = function(
-    ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints,
-    stepPlot, color) {
+/**
+ * This does the actual drawing of lines on the canvas, for just one series.
+ * Returns a list of [canvasx, canvasy] pairs for points for which a
+ * drawPointCallback should be fired.  These include isolated points, or all
+ * points if drawPoints=true.
+ * @param {Object} e The dictionary passed to the plotter function.
+ * @private
+ */
+DygraphCanvasRenderer._drawSeries = function(e,
+    iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color) {
 
   var prevCanvasX = null;
   var prevCanvasY = null;
@@ -299,6 +312,7 @@ DygraphCanvasRenderer.prototype._drawSeries = function(
   var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
   var first = true; // the first cycle through the while loop
 
+  var ctx = e.drawingContext;
   ctx.beginPath();
   ctx.strokeStyle = color;
   ctx.lineWidth = strokeWidth;
@@ -370,49 +384,30 @@ DygraphCanvasRenderer.prototype._drawSeries = function(
   return pointsOnLine;
 };
 
-DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) {
-  var setNames = this.layout.setNames;
-  var setName = setNames[i];
-
-  var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
-  var borderWidth = this.dygraph_.attr_("strokeBorderWidth", setName);
-  var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName) ||
-      Dygraph.Circles.DEFAULT;
-
-  if (borderWidth && strokeWidth) {
-    this._drawStyledLine(ctx, i, setName,
-        this.dygraph_.attr_("strokeBorderColor", setName),
-        strokeWidth + 2 * borderWidth,
-        this.dygraph_.attr_("strokePattern", setName),
-        this.dygraph_.attr_("drawPoints", setName),
-        drawPointCallback,
-        this.dygraph_.attr_("pointSize", setName));
+/**
+ * This fires the drawPointCallback functions, which draw dots on the points by
+ * default. This gets used when the "drawPoints" option is set, or when there
+ * are isolated points.
+ * @param {Object} e The dictionary passed to the plotter function.
+ * @private
+ */
+DygraphCanvasRenderer._drawPointsOnLine = function(
+    e, pointsOnLine, drawPointCallback, color, pointSize) {
+  var ctx = e.drawingContext;
+  for (var idx = 0; idx < pointsOnLine.length; idx++) {
+    var cb = pointsOnLine[idx];
+    ctx.save();
+    drawPointCallback(
+        e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize);
+    ctx.restore();
   }
-
-  this._drawStyledLine(ctx, i, setName,
-      this.colors[setName],
-      strokeWidth,
-      this.dygraph_.attr_("strokePattern", setName),
-      this.dygraph_.attr_("drawPoints", setName),
-      drawPointCallback,
-      this.dygraph_.attr_("pointSize", setName));
-};
+}
 
 /**
- * Actually draw the lines chart, including error bars.
+ * Attaches canvas coordinates to the points array.
  * @private
  */
-DygraphCanvasRenderer.prototype._renderLineChart = function() {
-  var ctx = this.elementContext;
-  var errorBars = this.attr_("errorBars") || this.attr_("customBars");
-  var fillGraph = this.attr_("fillGraph");
-  var i;
-
-  var setNames = this.layout.setNames;
-  var setCount = setNames.length;
-
-  this.colors = this.dygraph_.colorsMap_;
-
+DygraphCanvasRenderer.prototype._updatePoints = function() {
   // Update Points
   // TODO(danvk): here
   //
@@ -426,7 +421,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
   // transformed coordinate space, but you can't specify different values for
   // each dimension (as you can with .scale()). The speedup here is ~12%.
   var sets = this.layout.points;
-  for (i = sets.length; i--;) {
+  for (var i = sets.length; i--;) {
     var points = sets[i];
     for (var j = points.length; j--;) {
       var point = points[j];
@@ -434,117 +429,262 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
       point.canvasy = this.area.h * point.y + this.area.y;
     }
   }
+};
 
-  // Draw any "fills", i.e. error bars or the filled area under a series.
-  // These must all be drawn before any lines, so that the main lines of a
-  // series are drawn on top.
-  if (errorBars) {
-    if (fillGraph) {
-      this.dygraph_.warn("Can't use fillGraph option with error bars");
-    }
+/**
+ * Add canvas Actually draw the lines chart, including error bars.
+ * If opt_seriesName is specified, only that series will be drawn.
+ * (This is used for expedited redrawing with highlightSeriesOpts)
+ * Lines are typically drawn in the non-interactive dygraph canvas. If opt_ctx
+ * is specified, they can be drawn elsewhere.
+ *
+ * This function can only be called if DygraphLayout's points array has been
+ * updated with canvas{x,y} attributes, i.e. by
+ * DygraphCanvasRenderer._updatePoints.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) {
+  var ctx = opt_ctx || this.elementContext;
+  var errorBars = this.attr_("errorBars") || this.attr_("customBars");
+  var fillGraph = this.attr_("fillGraph");
+  var i;
 
-    ctx.save();
-    this.drawErrorBars_(points);
-    ctx.restore();
-  } else if (fillGraph) {
-    ctx.save();
-    this.drawFillBars_(points);
-    ctx.restore();
+  var sets = this.layout.points;
+  var setNames = this.layout.setNames;
+  var setCount = setNames.length;
+
+  this.colors = this.dygraph_.colorsMap_;
+
+  // Determine which series have specialized plotters.
+  var plotter_attr = this.attr_("plotter");
+  var plotters = plotter_attr;
+  if (!Dygraph.isArrayLike(plotters)) {
+    plotters = [plotters];
+  }
+
+  var setPlotters = {};  // series name -> plotter fn.
+  for (i = 0; i < setNames.length; i++) {
+    var setName = setNames[i];
+    var setPlotter = this.attr_("plotter", setName);
+    if (setPlotter == plotter_attr) continue;  // not specialized.
+
+    setPlotters[setName] = setPlotter;
+  }
+
+  for (i = 0; i < plotters.length; i++) {
+    var plotter = plotters[i];
+    var is_last = (i == plotters.length - 1);
+
+    for (var j = 0; j < sets.length; j++) {
+      var setName = setNames[j];
+      if (opt_seriesName && setName != opt_seriesName) continue;
+
+      var points = sets[j];
+
+      // Only throw in the specialized plotters on the last iteration.
+      var p = plotter;
+      if (setName in setPlotters) {
+        if (is_last) {
+          p = setPlotters[setName];
+        } else {
+          // Don't use the standard plotters in this case.
+          continue;
+        }
+      }
+
+      var color = this.colors[setName];
+      var strokeWidth = this.dygraph_.getOption("strokeWidth", setName);
+
+      ctx.save();
+      ctx.strokeStyle = color;
+      ctx.lineWidth = strokeWidth;
+      p({
+        points: points,
+        setName: setName,
+        drawingContext: ctx,
+        color: color,
+        strokeWidth: strokeWidth,
+        dygraph: this.dygraph_,
+        axis: this.dygraph_.axisPropertiesForSeries(setName),
+        plotArea: this.area,
+        seriesIndex: j,
+        seriesCount: sets.length,
+        allSeriesPoints: sets
+      });
+      ctx.restore();
+    }
   }
+};
+
+/**
+ * Standard plotters. These may be used by clients via Dygraph.Plotters.
+ * See comments there for more details.
+ */
+DygraphCanvasRenderer._Plotters = {
+  linePlotter: function(e) {
+    DygraphCanvasRenderer._linePlotter(e);
+  },
+
+  fillPlotter: function(e) {
+    DygraphCanvasRenderer._fillPlotter(e);
+  },
 
-  // Drawing the lines.
-  for (i = 0; i < setCount; i += 1) {
-    this._drawLine(ctx, i);
+  errorPlotter: function(e) {
+    DygraphCanvasRenderer._errorPlotter(e);
   }
 };
 
 /**
+ * Plotter which draws the central lines for a series.
+ * @private
+ */
+DygraphCanvasRenderer._linePlotter = function(e) {
+  var g = e.dygraph;
+  var setName = e.setName;
+  var strokeWidth = e.strokeWidth;
+
+  // TODO(danvk): Check if there's any performance impact of just calling
+  // getOption() inside of _drawStyledLine. Passing in so many parameters makes
+  // this code a bit nasty.
+  var borderWidth = g.getOption("strokeBorderWidth", setName);
+  var drawPointCallback = g.getOption("drawPointCallback", setName) ||
+      Dygraph.Circles.DEFAULT;
+  var strokePattern = g.getOption("strokePattern", setName);
+  var drawPoints = g.getOption("drawPoints", setName);
+  var pointSize = g.getOption("pointSize", setName);
+
+  if (borderWidth && strokeWidth) {
+    DygraphCanvasRenderer._drawStyledLine(e,
+        g.getOption("strokeBorderColor", setName),
+        strokeWidth + 2 * borderWidth,
+        strokePattern,
+        drawPoints,
+        drawPointCallback,
+        pointSize
+        );
+  }
+
+  DygraphCanvasRenderer._drawStyledLine(e,
+      e.color,
+      strokeWidth,
+      strokePattern,
+      drawPoints,
+      drawPointCallback,
+      pointSize
+  );
+}
+
+/**
  * Draws the shaded error bars/confidence intervals for each series.
  * This happens before the center lines are drawn, since the center lines
  * need to be drawn on top of the error bars for all series.
- *
  * @private
  */
-DygraphCanvasRenderer.prototype.drawErrorBars_ = function(points) {
-  var ctx = this.elementContext;
-  var setNames = this.layout.setNames;
-  var setCount = setNames.length;
-  var fillAlpha = this.attr_('fillAlpha');
-  var stepPlot = this.attr_('stepPlot');
+DygraphCanvasRenderer._errorPlotter = function(e) {
+  var g = e.dygraph;
+  var errorBars = g.getOption("errorBars") || g.getOption("customBars");
+  if (!errorBars) return;
+
+  var fillGraph = g.getOption("fillGraph");
+  if (fillGraph) {
+    g.warn("Can't use fillGraph option with error bars");
+  }
 
-  var newYs;
+  var setName = e.setName;
+  var ctx = e.drawingContext;
+  var color = e.color;
+  var fillAlpha = g.getOption('fillAlpha', setName);
+  var stepPlot = g.getOption('stepPlot');  // TODO(danvk): per-series
+  var axis = e.axis;
+  var points = e.points;
 
-  for (var setIdx = 0; setIdx < setCount; setIdx++) {
-    var setName = setNames[setIdx];
-    var axis = this.dygraph_.axisPropertiesForSeries(setName);
-    var color = this.colors[setName];
+  var iter = Dygraph.createIterator(points, 0, points.length,
+      DygraphCanvasRenderer._getIteratorPredicate(
+          g.getOption("connectSeparatedPoints")));
 
-    var points = this.layout.points[setIdx];
-    var iter = Dygraph.createIterator(points, 0, points.length,
-        DygraphCanvasRenderer._getIteratorPredicate(
-            this.attr_("connectSeparatedPoints")));
+  var newYs;
 
-    // setup graphics context
-    var prevX = NaN;
-    var prevY = NaN;
-    var prevYs = [-1, -1];
-    var yscale = axis.yscale;
-    // should be same color as the lines but only 15% opaque.
-    var rgb = new RGBColor(color);
-    var err_color =
-        'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
-    ctx.fillStyle = err_color;
-    ctx.beginPath();
-    while (iter.hasNext) {
-      var point = iter.next();
-      if (!Dygraph.isOK(point.y)) {
-        prevX = NaN;
-        continue;
-      }
+  // setup graphics context
+  var prevX = NaN;
+  var prevY = NaN;
+  var prevYs = [-1, -1];
+  var yscale = axis.yscale;
+  // should be same color as the lines but only 15% opaque.
+  var rgb = new RGBColor(color);
+  var err_color =
+      'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
+  ctx.fillStyle = err_color;
+  ctx.beginPath();
+  while (iter.hasNext) {
+    var point = iter.next();
+    if (!Dygraph.isOK(point.y)) {
+      prevX = NaN;
+      continue;
+    }
 
+    if (stepPlot) {
+      newYs = [ point.y_bottom, point.y_top ];
+      prevY = point.y;
+    } else {
+      newYs = [ point.y_bottom, point.y_top ];
+    }
+    newYs[0] = e.plotArea.h * newYs[0] + e.plotArea.y;
+    newYs[1] = e.plotArea.h * newYs[1] + e.plotArea.y;
+    if (!isNaN(prevX)) {
       if (stepPlot) {
-        newYs = [ point.y_bottom, point.y_top ];
-        prevY = point.y;
+        ctx.moveTo(prevX, newYs[0]);
       } else {
-        newYs = [ point.y_bottom, point.y_top ];
+        ctx.moveTo(prevX, prevYs[0]);
       }
-      newYs[0] = this.area.h * newYs[0] + this.area.y;
-      newYs[1] = this.area.h * newYs[1] + this.area.y;
-      if (!isNaN(prevX)) {
-        if (stepPlot) {
-          ctx.moveTo(prevX, newYs[0]);
-        } else {
-          ctx.moveTo(prevX, prevYs[0]);
-        }
-        ctx.lineTo(point.canvasx, newYs[0]);
-        ctx.lineTo(point.canvasx, newYs[1]);
-        if (stepPlot) {
-          ctx.lineTo(prevX, newYs[1]);
-        } else {
-          ctx.lineTo(prevX, prevYs[1]);
-        }
-        ctx.closePath();
+      ctx.lineTo(point.canvasx, newYs[0]);
+      ctx.lineTo(point.canvasx, newYs[1]);
+      if (stepPlot) {
+        ctx.lineTo(prevX, newYs[1]);
+      } else {
+        ctx.lineTo(prevX, prevYs[1]);
       }
-      prevYs = newYs;
-      prevX = point.canvasx;
+      ctx.closePath();
     }
-    ctx.fill();
+    prevYs = newYs;
+    prevX = point.canvasx;
   }
-};
+  ctx.fill();
+}
 
 /**
  * Draws the shaded regions when "fillGraph" is set. Not to be confused with
  * error bars.
  *
+ * For stacked charts, it's more convenient to handle all the series
+ * simultaneously. So this plotter plots all the points on the first series
+ * it's asked to draw, then ignores all the other series.
+ *
  * @private
  */
-DygraphCanvasRenderer.prototype.drawFillBars_ = function(points) {
-  var ctx = this.elementContext;
-  var setNames = this.layout.setNames;
-  var setCount = setNames.length;
-  var fillAlpha = this.attr_('fillAlpha');
-  var stepPlot = this.attr_('stepPlot');
-  var stackedGraph = this.attr_("stackedGraph");
+DygraphCanvasRenderer._fillPlotter = function(e) {
+  var g = e.dygraph;
+  if (!g.getOption("fillGraph")) return;
+
+  // We'll handle all the series at once, not one-by-one.
+  if (e.seriesIndex !== 0) return;
+
+  var ctx = e.drawingContext;
+  var area = e.plotArea;
+  var sets = e.allSeriesPoints;
+  var setCount = sets.length;
+
+  var setNames = g.getLabels().slice(1);  // remove x-axis
+  // getLabels() includes names for invisible series, which are not included in
+  // allSeriesPoints. We remove those to make the two match.
+  // TODO(danvk): provide a simpler way to get this information.
+  for (var i = setNames.length; i >= 0; i--) {
+    if (!g.visibility()[i]) setNames.splice(i, 1);
+  }
+
+  var fillAlpha = g.getOption('fillAlpha');
+  var stepPlot = g.getOption('stepPlot');
+  var stackedGraph = g.getOption("stackedGraph");
+  var colors = g.getColors();
 
   var baseline = {};  // for stacked graphs: baseline for filling
   var currBaseline;
@@ -552,17 +692,17 @@ DygraphCanvasRenderer.prototype.drawFillBars_ = function(points) {
   // process sets in reverse order (needed for stacked graphs)
   for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) {
     var setName = setNames[setIdx];
-    var color = this.colors[setName];
-    var axis = this.dygraph_.axisPropertiesForSeries(setName);
+    var color = colors[setIdx];
+    var axis = g.axisPropertiesForSeries(setName);
     var axisY = 1.0 + axis.minyval * axis.yscale;
     if (axisY < 0.0) axisY = 0.0;
     else if (axisY > 1.0) axisY = 1.0;
-    axisY = this.area.h * axisY + this.area.y;
+    axisY = area.h * axisY + area.y;
 
-    var points = this.layout.points[setIdx];
+    var points = sets[setIdx];
     var iter = Dygraph.createIterator(points, 0, points.length,
         DygraphCanvasRenderer._getIteratorPredicate(
-            this.attr_("connectSeparatedPoints")));
+            g.getOption("connectSeparatedPoints")));
 
     // setup graphics context
     var prevX = NaN;
index 04f13d7..e39c073 100644 (file)
@@ -753,6 +753,12 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "labels": ["Interactive Elements"],
     "type": "boolean",
     "description": "Set this option to animate the transition between zoom windows. Applies to programmatic and interactive zooms. Note that if you also set a drawCallback, it will be called several times on each zoom. If you set a zoomCallback, it will only be called after the animation is complete."
+  },
+  "plotter": {
+    "default": "[DygraphCanvasRenderer.Plotters.fillPlotter, DygraphCanvasRenderer.Plotters.errorPlotter, DygraphCanvasRenderer.Plotters.linePlotter]",
+    "labels": ["Data Line display"],
+    "type": "array or function",
+    "description": "A function (or array of functions) which plot each data series on the chart. TODO(danvk): more details! May be set per-series."
   }
 }
 ;  // </JSON>
index ba6889e..bb74cdc 100644 (file)
@@ -180,6 +180,18 @@ Dygraph.dateAxisFormatter = function(date, granularity) {
   }
 };
 
+/**
+ * Standard plotters. These may be used by clients.
+ * Available plotters are:
+ * - Dygraph.Plotters.linePlotter: draws central lines (most common)
+ * - Dygraph.Plotters.errorPlotter: draws error bars
+ * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
+ *
+ * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
+ * This causes all the lines to be drawn over all the fills/error bars.
+ */
+Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
+
 
 // Default attribute values.
 Dygraph.DEFAULT_ATTRS = {
@@ -261,6 +273,14 @@ Dygraph.DEFAULT_ATTRS = {
   rangeSelectorPlotStrokeColor: "#808FAB",
   rangeSelectorPlotFillColor: "#A7B1C4",
 
+  // The ordering here ensures that central lines always appear above any
+  // fill bars/error bars.
+  plotter: [
+    Dygraph.Plotters.fillPlotter,
+    Dygraph.Plotters.errorPlotter,
+    Dygraph.Plotters.linePlotter
+  ],
+
   // per-axis options
   axes: {
     x: {
@@ -1852,8 +1872,10 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
       ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
       ctx.fillRect(0, 0, this.width_, this.height_);
     }
-    var setIdx = this.datasetIndexFromSetName_(this.highlightSet_);
-    this.plotter_._drawLine(ctx, setIdx);
+
+    // Redraw only the highlighted series in the interactive canvas (not the
+    // static plot canvas, which is where series are usually drawn).
+    this.plotter_._renderLineChart(this.highlightSet_, ctx);
   } else if (this.previousVerticalX_ >= 0) {
     // Determine the maximum highlight circle size.
     var maxCircleSize = 0;
index bd51622..00448c3 100755 (executable)
@@ -24,9 +24,9 @@ if [ -s docs/options.html ] ; then
   find . -path ./.git -prune -o -print | xargs chmod a+rX
 
   # Copy everything to the site.
-  rsync -avzr gallery common tests jsdoc experimental plugins $site \
+  rsync -avzr gallery strftime rgbcolor common tests jsdoc experimental plugins $site \
   && \
-  rsync -avzr dygraph*.js gadget.xml excanvas.js thumbnail.png screenshot.png docs/* $site/
+  rsync -avzr dashed-canvas.js stacktrace.js dygraph*.js gadget.xml excanvas.js thumbnail.png screenshot.png docs/* $site/
 else
   echo "generate-documentation.py failed"
 fi
index 90adb82..b6c501d 100644 (file)
@@ -20,9 +20,6 @@
     <p>Filled, negatives:</p>
     <div id="div_g2" style="width:600px; height:300px;"></div>
 
-    <p>Filled, error bars:</p>
-    <div id="div_g3" style="width:600px; height:300px;"></div>
-
     <script type="text/javascript">
       var g1 = new Dygraph(
         document.getElementById("div_g"),
diff --git a/tests/plotters.html b/tests/plotters.html
new file mode 100644 (file)
index 0000000..91c3a7c
--- /dev/null
@@ -0,0 +1,326 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Plotters demo</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+    <script type="text/javascript" src="data.js"></script>
+    <style type="text/css">
+      body {
+        max-width: 750px;
+      }
+      div.chart {
+        width: 640px;
+        height: 320px;
+      }
+    </style>
+  </head>
+  <body>
+    <p>This page demonstrates how to build custom plotters with dygraphs.
+    The <a href="http://dygraphs.com/options.html#plotter">plotter</a> option
+    allows you to write your own drawing logic. This can be used to achieve
+    powerful customization. View source to see how the examples work.</p>
+
+    <h2>Bar Chart</h2>
+    <p>Here a specialized <a
+    href="http://dygraphs.com/options.html#plotter">plotter</a> is used to draw
+    a bar plot rather than a line plot:</p>
+    <div id="demodiv" class=chart></div>
+
+    <h2>Candle Chart</h2>
+    <p>Here a specialized <a
+    href="http://dygraphs.com/options.html#plotter">plotter</a> is used to
+    combined four series into a unified "Candle" plot:</p>
+    <div id="candlechart" class=chart></div>
+
+    <h2>Bar &amp; Line Chart</h2>
+    <p>The <a href="http://dygraphs.com/options.html#plotter">plotter</a>
+    option may be set on a per-series basis to create mixed charts:</p>
+    <div id="barlinechart" class="chart"></div>
+
+    <h2>Multi-column Bar Chart</h2>
+    <div id="multibar" class="chart"></div>
+
+    <h2>Mixed Error Bars and Lines</h2>
+    <p>You can tweak the standard plotters list to achieve effects which would
+    be difficult otherwise, e.g. drawing series with only confidence intervals
+    and showing error bars only for some series.</p>
+    <div id="mixed-error" class="chart"></div>
+
+    <script type="text/javascript">
+
+      // This function draws bars for a single series. See
+      // multiColumnBarPlotter below for a plotter which can draw multi-series
+      // bar charts.
+      function barChartPlotter(e) {
+        var ctx = e.drawingContext;
+        var points = e.points;
+        var y_bottom = e.dygraph.toDomYCoord(0);
+
+        // The RGBColor class is provided by rgbcolor.js, which is
+        // packed in with dygraphs.
+        var color = new RGBColor(e.color);
+        color.r = Math.floor((255 + color.r) / 2);
+        color.g = Math.floor((255 + color.g) / 2);
+        color.b = Math.floor((255 + color.b) / 2);
+        ctx.fillStyle = color.toRGB();
+
+        // Find the minimum separation between x-values.
+        // This determines the bar width.
+        var min_sep = Infinity;
+        for (var i = 1; i < points.length; i++) {
+          var sep = points[i].canvasx - points[i - 1].canvasx;
+          if (sep < min_sep) min_sep = sep;
+        }
+        var bar_width = Math.floor(2.0 / 3 * min_sep);
+
+        // Do the actual plotting.
+        for (var i = 0; i < points.length; i++) {
+          var p = points[i];
+          var center_x = p.canvasx;
+
+          ctx.fillRect(center_x - bar_width / 2, p.canvasy,
+              bar_width, y_bottom - p.canvasy);
+
+          ctx.strokeRect(center_x - bar_width / 2, p.canvasy,
+              bar_width, y_bottom - p.canvasy);
+        }
+      }
+
+      g = new Dygraph(
+              document.getElementById("demodiv"),
+              "Date,Widgets Sold\n" +
+              "2012/07/21,10\n" +
+              "2012/07/22,12\n" +
+              "2012/07/23,9\n"  +
+              "2012/07/24,16\n" +
+              "2012/07/25,10\n",
+              {
+                legend: 'always',
+                title: 'Daily Widget Sales',
+                includeZero: true,
+                dateWindow: [ Date.parse("2012/07/20"), Date.parse("2012/07/26") ],
+                animatedZooms: true,
+                drawXGrid: false,
+                plotter: barChartPlotter
+              }
+          );
+
+      // The Candle chart plotter is adapted from code written by
+      // Zhenlei Cai (jpenguin@gmail.com)
+      // https://github.com/danvk/dygraphs/pull/141/files
+      
+      var BAR_WIDTH = 8;
+      function candlePlotter(e) {
+        // This is the officially endorsed way to plot all the series at once.
+        if (e.seriesIndex !== 0) return;
+
+        var setCount = e.seriesCount;
+        if (setCount != 4) {
+          throw "Exactly 4 prices each point must be provided for candle chart (open close high low)";
+        }
+
+        var prices = [];
+        var price;
+        var sets = e.allSeriesPoints;
+        for (var p = 0 ; p < sets[0].length; p++) {
+          price = {
+            open : sets[0][p].yval,
+            close : sets[1][p].yval,
+            high : sets[2][p].yval,
+            low : sets[3][p].yval,
+            openY : sets[0][p].y,
+            closeY : sets[1][p].y,
+            highY : sets[2][p].y,
+            lowY : sets[3][p].y
+          };
+          prices.push(price);
+        }
+
+        var area = e.plotArea;
+        var ctx = e.drawingContext;
+        ctx.strokeStyle = '#202020';
+        ctx.lineWidth = 0.6;
+
+        for (p = 0 ; p < prices.length; p++) {
+          ctx.beginPath();
+
+          price = prices[p];
+          var topY = area.h * price.highY + area.y;
+          var bottomY = area.h * price.lowY + area.y;
+          var centerX = area.x + sets[0][p].x * area.w;
+          ctx.moveTo(centerX, topY);
+          ctx.lineTo(centerX, bottomY);
+          ctx.closePath();
+          ctx.stroke();
+          var bodyY;
+          if (price.open > price.close) {
+            ctx.fillStyle ='rgba(244,44,44,1.0)';
+            bodyY = area.h * price.openY + area.y;
+          }
+          else {
+            ctx.fillStyle ='rgba(44,244,44,1.0)';
+            bodyY = area.h * price.closeY  + area.y;
+          }
+          var bodyHeight = area.h * Math.abs(price.openY - price.closeY);
+          ctx.fillRect(centerX - BAR_WIDTH / 2, bodyY, BAR_WIDTH,  bodyHeight);
+        }
+
+      }
+
+var candleData = "Date,Open,Close,High,Low\n" +
+  "2011-12-06,392.54,390.95,394.63,389.38\n" + 
+  "2011-12-07,389.93,389.09,390.94,386.76\n" + 
+  "2011-12-08,391.45,390.66,395.50,390.23\n" + 
+  "2011-12-09,392.85,393.62,394.04,391.03\n" + 
+  "2011-12-12,391.68,391.84,393.90,389.45\n" + 
+  "2011-12-13,393.00,388.81,395.40,387.10\n" + 
+  "2011-12-14,386.70,380.19,387.38,377.68\n" + 
+  "2011-12-15,383.33,378.94,383.74,378.31\n" + 
+  "2011-12-16,380.36,381.02,384.15,379.57\n" + 
+  "2011-12-19,382.47,382.21,384.85,380.48\n" + 
+  "2011-12-20,387.76,395.95,396.10,387.26\n" + 
+  "2011-12-21,396.69,396.45,397.30,392.01\n" + 
+  "2011-12-22,397.00,398.55,399.13,396.10\n" + 
+  "2011-12-23,399.69,403.33,403.59,399.49\n" + 
+  "2011-12-27,403.10,406.53,409.09,403.02\n" + 
+  "2011-12-28,406.89,402.64,408.25,401.34\n" + 
+  "2011-12-29,403.40,405.12,405.65,400.51\n" + 
+  "2011-12-30,403.51,405.00,406.28,403.49\n" + 
+  "2012-01-03,409.50,411.23,412.50,409.00\n" + 
+  "2012-01-04,410.21,413.44,414.68,409.28\n" + 
+  "2012-01-05,414.95,418.03,418.55,412.67\n" + 
+  "2012-01-06,419.77,422.40,422.75,419.22\n" + 
+  "2012-01-09,425.52,421.73,427.75,421.35\n" + 
+  "2012-01-10,425.91,423.24,426.00,421.50\n" + 
+  "2012-01-11,422.59,422.55,422.85,419.31\n" + 
+  "2012-01-12,422.41,421.39,422.90,418.75\n" + 
+  "2012-01-13,419.53,419.81,420.45,418.66\n" + 
+  "2012-01-17,424.20,424.70,425.99,422.96\n" + 
+  "2012-01-18,426.87,429.11,429.47,426.30\n" + 
+  "2012-01-19,430.03,427.75,431.37,426.51\n" + 
+  "2012-01-20,427.49,420.30,427.50,419.75\n" + 
+  "2012-01-23,422.67,427.41,428.45,422.30\n" + 
+  "2012-01-24,425.10,420.41,425.10,419.55\n" + 
+  "2012-01-25,454.26,446.66,454.45,443.73\n" + 
+  "2012-01-26,448.45,444.63,448.79,443.14\n" + 
+  "2012-01-27,444.37,447.28,448.48,443.77\n" + 
+  "2012-01-30,445.71,453.01,453.90,445.39\n" + 
+  "2012-01-31,455.85,456.48,458.24,453.07\n" + 
+  "2012-02-01,458.49,456.19,458.99,455.55\n" + 
+  "2012-02-02,455.90,455.12,457.17,453.98\n" + 
+  "2012-02-03,457.30,459.68,460.00,455.56\n" + 
+  "2012-02-06,458.38,463.97,464.98,458.20\n" + 
+  "2012-02-07,465.25,468.83,469.75,464.58\n" + 
+  "2012-02-08,470.50,476.68,476.79,469.70\n" + 
+  "2012-02-09,480.95,493.17,496.75,480.56\n" + 
+  "2012-02-10,491.17,493.42,497.62,488.55\n" + 
+  "2012-02-13,499.74,502.60,503.83,497.09\n" + 
+  "2012-02-14,504.70,509.46,509.56,502.00\n" ;
+
+    g2 = new Dygraph(
+            document.getElementById("candlechart"),
+            candleData,
+            {
+              plotter: candlePlotter
+            });
+
+
+    // Bar and Line chart
+    var short_data = data_nolabel();
+    short_data = short_data.split('\n').slice(0, 20).join('\n');
+
+    g3 = new Dygraph(
+            document.getElementById("barlinechart"),
+            short_data,
+            {
+              labels: ['Date', 'A', 'B'],
+              includeZero: true,
+              "A": {
+                strokeWidth: 2
+              },
+              "B": {
+                plotter: barChartPlotter
+              }
+            });
+
+
+    // Multiple column bar chart
+    function multiColumnBarPlotter(e) {
+      // We need to handle all the series simultaneously.
+      if (e.seriesIndex !== 0) return;
+
+      var g = e.dygraph;
+      var ctx = e.drawingContext;
+      var sets = e.allSeriesPoints;
+      var y_bottom = e.dygraph.toDomYCoord(0);
+
+      // Find the minimum separation between x-values.
+      // This determines the bar width.
+      var min_sep = Infinity;
+      for (var j = 0; j < sets.length; j++) {
+        var points = sets[j];
+        for (var i = 1; i < points.length; i++) {
+          var sep = points[i].canvasx - points[i - 1].canvasx;
+          if (sep < min_sep) min_sep = sep;
+        }
+      }
+      var bar_width = Math.floor(2.0 / 3 * min_sep);
+
+      var fillColors = [];
+      var strokeColors = g.getColors();
+      for (var i = 0; i < strokeColors.length; i++) {
+        var color = new RGBColor(strokeColors[i]);
+        color.r = Math.floor((255 + color.r) / 2);
+        color.g = Math.floor((255 + color.g) / 2);
+        color.b = Math.floor((255 + color.b) / 2);
+        fillColors.push(color.toRGB());
+      }
+
+      for (var j = 0; j < sets.length; j++) {
+        ctx.fillStyle = fillColors[j];
+        ctx.strokeStyle = strokeColors[j];
+        for (var i = 0; i < sets[j].length; i++) {
+          var p = sets[j][i];
+          var center_x = p.canvasx;
+          var x_left = center_x - (bar_width / 2) * (1 - j/(sets.length-1));
+
+          ctx.fillRect(x_left, p.canvasy,
+              bar_width/sets.length, y_bottom - p.canvasy);
+
+          ctx.strokeRect(x_left, p.canvasy,
+              bar_width/sets.length, y_bottom - p.canvasy);
+        }
+      }
+    }
+
+    g4 = new Dygraph(
+            document.getElementById("multibar"),
+            short_data,
+            {
+              includeZero: true,
+              plotter: multiColumnBarPlotter
+            });
+
+    // Mixed Error Bars and Lines
+    g5 = new Dygraph(
+            document.getElementById("mixed-error"),
+            NoisyData(),
+            {
+              errorBars: true,
+              'A': {
+                plotter: Dygraph.Plotters.errorPlotter
+              },
+              'B': {
+                plotter: Dygraph.Plotters.linePlotter,
+                strokePattern: Dygraph.DASHED_LINE
+              }
+            });
+
+    </script>
+</body>
+</html>