      1 /**
      2  * @license
      3  * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
      4  * MIT-licenced: https://opensource.org/licenses/MIT
      5  */
      6
      7 /**
      8  * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
      9  * needs of dygraphs.
     10  *
     11  * In particular, support for:
     12  * - grid overlays
     13  * - error bars
     14  * - dygraphs attribute system
     15  */
     16
     17 /**
     18  * The DygraphCanvasRenderer class does the actual rendering of the chart onto
     19  * a canvas. It's based on PlotKit.CanvasRenderer.
     20  * @param {Object} element The canvas to attach to
     21  * @param {Object} elementContext The 2d context of the canvas (injected so it
     22  * can be mocked for testing.)
     23  * @param {Layout} layout The DygraphLayout object for this graph.
     24  * @constructor
     25  */
     26
     27 /*global Dygraph:false */
     28 "use strict";
     29
     30 import * as utils from './dygraph-utils';
     31 import Dygraph from './dygraph';
     32
     33 /**
     34  * @constructor
     35  *
     36  * This gets called when there are "new points" to chart. This is generally the
     37  * case when the underlying data being charted has changed. It is _not_ called
     38  * in the common case that the user has zoomed or is panning the view.
     39  *
     40  * The chart canvas has already been created by the Dygraph object. The
     41  * renderer simply gets a drawing context.
     42  *
     43  * @param {Dygraph} dygraph The chart to which this renderer belongs.
     44  * @param {HTMLCanvasElement} element The <canvas> DOM element on which to draw.
     45  * @param {CanvasRenderingContext2D} elementContext The drawing context.
     46  * @param {DygraphLayout} layout The chart's DygraphLayout object.
     47  *
     48  * TODO(danvk): remove the elementContext property.
     49  */
     50 var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
     51   this.dygraph_ = dygraph;
     52
     53   this.layout = layout;
     54   this.element = element;
     55   this.elementContext = elementContext;
     56
     57   this.height = dygraph.height_;
     58   this.width = dygraph.width_;
     59
     60   // --- check whether everything is ok before we return
     61   if (!utils.isCanvasSupported(this.element)) {
     62     throw "Canvas is not supported.";
     63   }
     64
     65   // internal state
     66   this.area = layout.getPlotArea();
     67
     68   // Set up a clipping area for the canvas (and the interaction canvas).
     69   // This ensures that we don't overdraw.
     70   var ctx = this.dygraph_.canvas_ctx_;
     71   ctx.beginPath();
     72   ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
     73   ctx.clip();
     74
     75   ctx = this.dygraph_.hidden_ctx_;
     76   ctx.beginPath();
     77   ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h);
     78   ctx.clip();
     79 };
     80
     81 /**
     82  * Clears out all chart content and DOM elements.
     83  * This is called immediately before render() on every frame, including
     84  * during zooms and pans.
     85  * @private
     86  */
     87 DygraphCanvasRenderer.prototype.clear = function() {
     88   this.elementContext.clearRect(0, 0, this.width, this.height);
     89 };
     90
     91 /**
     92  * This method is responsible for drawing everything on the chart, including
     93  * lines, error bars, fills and axes.
     94  * It is called immediately after clear() on every frame, including during pans
     95  * and zooms.
     96  * @private
     97  */
     98 DygraphCanvasRenderer.prototype.render = function() {
     99   // attaches point.canvas{x,y}
    100   this._updatePoints();
    101
    102   // actually draws the chart.
    103   this._renderLineChart();
    104 };
    105
    106 /**
    107  * Returns a predicate to be used with an iterator, which will
    108  * iterate over points appropriately, depending on whether
    109  * connectSeparatedPoints is true. When it's false, the predicate will
    110  * skip over points with missing yVals.
    111  */
    112 DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
    113   return connectSeparatedPoints ?
    114       DygraphCanvasRenderer._predicateThatSkipsEmptyPoints :
    115       null;
    116 };
    117
    118 DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
    119     function(array, idx) {
    120   return array[idx].yval !== null;
    121 };
    122
    123 /**
    124  * Draws a line with the styles passed in and calls all the drawPointCallbacks.
    125  * @param {Object} e The dictionary passed to the plotter function.
    126  * @private
    127  */
    128 DygraphCanvasRenderer._drawStyledLine = function(e,
    129     color, strokeWidth, strokePattern, drawPoints,
    130     drawPointCallback, pointSize) {
    131   var g = e.dygraph;
    132   // TODO(konigsberg): Compute attributes outside this method call.
    133   var stepPlot = g.getBooleanOption("stepPlot", e.setName);
    134
    135   if (!utils.isArrayLike(strokePattern)) {
    136     strokePattern = null;
    137   }
    138
    139   var drawGapPoints = g.getBooleanOption('drawGapEdgePoints', e.setName);
    140
    141   var points = e.points;
    142   var setName = e.setName;
    143   var iter = utils.createIterator(points, 0, points.length,
    144       DygraphCanvasRenderer._getIteratorPredicate(
    145           g.getBooleanOption("connectSeparatedPoints", setName)));
    146
    147   var stroking = strokePattern && (strokePattern.length >= 2);
    148
    149   var ctx = e.drawingContext;
    150   ctx.save();
    151   if (stroking) {
    152     if (ctx.setLineDash) ctx.setLineDash(strokePattern);
    153   }
    154
    155   var pointsOnLine = DygraphCanvasRenderer._drawSeries(
    156       e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color);
    157   DygraphCanvasRenderer._drawPointsOnLine(
    158       e, pointsOnLine, drawPointCallback, color, pointSize);
    159
    160   if (stroking) {
    161     if (ctx.setLineDash) ctx.setLineDash([]);
    162   }
    163
    164   ctx.restore();
    165 };
    166
    167 /**
    168  * This does the actual drawing of lines on the canvas, for just one series.
    169  * Returns a list of [canvasx, canvasy] pairs for points for which a
    170  * drawPointCallback should be fired.  These include isolated points, or all
    171  * points if drawPoints=true.
    172  * @param {Object} e The dictionary passed to the plotter function.
    173  * @private
    174  */
    175 DygraphCanvasRenderer._drawSeries = function(e,
    176     iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color) {
    177
    178   var prevCanvasX = null;
    179   var prevCanvasY = null;
    180   var nextCanvasY = null;
    181   var isIsolated; // true if this point is isolated (no line segments)
    182   var point; // the point being processed in the while loop
    183   var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
    184   var first = true; // the first cycle through the while loop
    185
    186   var ctx = e.drawingContext;
    187   ctx.beginPath();
    188   ctx.strokeStyle = color;
    189   ctx.lineWidth = strokeWidth;
    190
    191   // NOTE: we break the iterator's encapsulation here for about a 25% speedup.
    192   var arr = iter.array_;
    193   var limit = iter.end_;
    194   var predicate = iter.predicate_;
    195
    196   for (var i = iter.start_; i < limit; i++) {
    197     point = arr[i];
    198     if (predicate) {
    199       while (i < limit && !predicate(arr, i)) {
    200         i++;
    201       }
    202       if (i == limit) break;
    203       point = arr[i];
    204     }
    205
    206     // FIXME: The 'canvasy != canvasy' test here catches NaN values but the test
    207     // doesn't catch Infinity values. Could change this to
    208     // !isFinite(point.canvasy), but I assume it avoids isNaN for performance?
    209     if (point.canvasy === null || point.canvasy != point.canvasy) {
    210       if (stepPlot && prevCanvasX !== null) {
    211         // Draw a horizontal line to the start of the missing data
    212         ctx.moveTo(prevCanvasX, prevCanvasY);
    213         ctx.lineTo(point.canvasx, prevCanvasY);
    214       }
    215       prevCanvasX = prevCanvasY = null;
    216     } else {
    217       isIsolated = false;
    218       if (drawGapPoints || prevCanvasX === null) {
    219         iter.nextIdx_ = i;
    220         iter.next();
    221         nextCanvasY = iter.hasNext ? iter.peek.canvasy : null;
    222
    223         var isNextCanvasYNullOrNaN = nextCanvasY === null ||
    224             nextCanvasY != nextCanvasY;
    225         isIsolated = (prevCanvasX === null && isNextCanvasYNullOrNaN);
    226         if (drawGapPoints) {
    227           // Also consider a point to be "isolated" if it's adjacent to a
    228           // null point, excluding the graph edges.
    229           if ((!first && prevCanvasX === null) ||
    230               (iter.hasNext && isNextCanvasYNullOrNaN)) {
    231             isIsolated = true;
    232           }
    233         }
    234       }
    235
    236       if (prevCanvasX !== null) {
    237         if (strokeWidth) {
    238           if (stepPlot) {
    239             ctx.moveTo(prevCanvasX, prevCanvasY);
    240             ctx.lineTo(point.canvasx, prevCanvasY);
    241           }
    242
    243           ctx.lineTo(point.canvasx, point.canvasy);
    244         }
    245       } else {
    246         ctx.moveTo(point.canvasx, point.canvasy);
    247       }
    248       if (drawPoints || isIsolated) {
    249         pointsOnLine.push([point.canvasx, point.canvasy, point.idx]);
    250       }
    251       prevCanvasX = point.canvasx;
    252       prevCanvasY = point.canvasy;
    253     }
    254     first = false;
    255   }
    256   ctx.stroke();
    257   return pointsOnLine;
    258 };
    259
    260 /**
    261  * This fires the drawPointCallback functions, which draw dots on the points by
    262  * default. This gets used when the "drawPoints" option is set, or when there
    263  * are isolated points.
    264  * @param {Object} e The dictionary passed to the plotter function.
    265  * @private
    266  */
    267 DygraphCanvasRenderer._drawPointsOnLine = function(
    268     e, pointsOnLine, drawPointCallback, color, pointSize) {
    269   var ctx = e.drawingContext;
    270   for (var idx = 0; idx < pointsOnLine.length; idx++) {
    271     var cb = pointsOnLine[idx];
    272     ctx.save();
    273     drawPointCallback.call(e.dygraph,
    274         e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize, cb[2]);
    275     ctx.restore();
    276   }
    277 };
    278
    279 /**
    280  * Attaches canvas coordinates to the points array.
    281  * @private
    282  */
    283 DygraphCanvasRenderer.prototype._updatePoints = function() {
    284   // Update Points
    285   // TODO(danvk): here
    286   //
    287   // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
    288   // transformations can be pushed into the canvas via linear transformation
    289   // matrices.
    290   // NOTE(danvk): this is trickier than it sounds at first. The transformation
    291   // needs to be done before the .moveTo() and .lineTo() calls, but must be
    292   // undone before the .stroke() call to ensure that the stroke width is
    293   // unaffected.  An alternative is to reduce the stroke width in the
    294   // transformed coordinate space, but you can't specify different values for
    295   // each dimension (as you can with .scale()). The speedup here is ~12%.
    296   var sets = this.layout.points;
    297   for (var i = sets.length; i--;) {
    298     var points = sets[i];
    299     for (var j = points.length; j--;) {
    300       var point = points[j];
    301       point.canvasx = this.area.w * point.x + this.area.x;
    302       point.canvasy = this.area.h * point.y + this.area.y;
    303     }
    304   }
    305 };
    306
    307 /**
    308  * Add canvas Actually draw the lines chart, including error bars.
    309  *
    310  * This function can only be called if DygraphLayout's points array has been
    311  * updated with canvas{x,y} attributes, i.e. by
    312  * DygraphCanvasRenderer._updatePoints.
    313  *
    314  * @param {string=} opt_seriesName when specified, only that series will
    315  *     be drawn. (This is used for expedited redrawing with highlightSeriesOpts)
    316  * @param {CanvasRenderingContext2D} opt_ctx when specified, the drawing
    317  *     context.  However, lines are typically drawn on the object's
    318  *     elementContext.
    319  * @private
    320  */
    321 DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) {
    322   var ctx = opt_ctx || this.elementContext;
    323   var i;
    324
    325   var sets = this.layout.points;
    326   var setNames = this.layout.setNames;
    327   var setName;
    328
    329   this.colors = this.dygraph_.colorsMap_;
    330
    331   // Determine which series have specialized plotters.
    332   var plotter_attr = this.dygraph_.getOption("plotter");
    333   var plotters = plotter_attr;
    334   if (!utils.isArrayLike(plotters)) {
    335     plotters = [plotters];
    336   }
    337
    338   var setPlotters = {};  // series name -> plotter fn.
    339   for (i = 0; i < setNames.length; i++) {
    340     setName = setNames[i];
    341     var setPlotter = this.dygraph_.getOption("plotter", setName);
    342     if (setPlotter == plotter_attr) continue;  // not specialized.
    343
    344     setPlotters[setName] = setPlotter;
    345   }
    346
    347   for (i = 0; i < plotters.length; i++) {
    348     var plotter = plotters[i];
    349     var is_last = (i == plotters.length - 1);
    350
    351     for (var j = 0; j < sets.length; j++) {
    352       setName = setNames[j];
    353       if (opt_seriesName && setName != opt_seriesName) continue;
    354
    355       var points = sets[j];
    356
    357       // Only throw in the specialized plotters on the last iteration.
    358       var p = plotter;
    359       if (setName in setPlotters) {
    360         if (is_last) {
    361           p = setPlotters[setName];
    362         } else {
    363           // Don't use the standard plotters in this case.
    364           continue;
    365         }
    366       }
    367
    368       var color = this.colors[setName];
    369       var strokeWidth = this.dygraph_.getOption("strokeWidth", setName);
    370
    371       ctx.save();
    372       ctx.strokeStyle = color;
    373       ctx.lineWidth = strokeWidth;
    374       p({
    375         points: points,
    376         setName: setName,
    377         drawingContext: ctx,
    378         color: color,
    379         strokeWidth: strokeWidth,
    380         dygraph: this.dygraph_,
    381         axis: this.dygraph_.axisPropertiesForSeries(setName),
    382         plotArea: this.area,
    383         seriesIndex: j,
    384         seriesCount: sets.length,
    385         singleSeriesName: opt_seriesName,
    386         allSeriesPoints: sets
    387       });
    388       ctx.restore();
    389     }
    390   }
    391 };
    392
    393 /**
    394  * Standard plotters. These may be used by clients via Dygraph.Plotters.
    395  * See comments there for more details.
    396  */
    397 DygraphCanvasRenderer._Plotters = {
    398   linePlotter: function(e) {
    399     DygraphCanvasRenderer._linePlotter(e);
    400   },
    401
    402   fillPlotter: function(e) {
    403     DygraphCanvasRenderer._fillPlotter(e);
    404   },
    405
    406   errorPlotter: function(e) {
    407     DygraphCanvasRenderer._errorPlotter(e);
    408   }
    409 };
    410
    411 /**
    412  * Plotter which draws the central lines for a series.
    413  * @private
    414  */
    415 DygraphCanvasRenderer._linePlotter = function(e) {
    416   var g = e.dygraph;
    417   var setName = e.setName;
    418   var strokeWidth = e.strokeWidth;
    419
    420   // TODO(danvk): Check if there's any performance impact of just calling
    421   // getOption() inside of _drawStyledLine. Passing in so many parameters makes
    422   // this code a bit nasty.
    423   var borderWidth = g.getNumericOption("strokeBorderWidth", setName);
    424   var drawPointCallback = g.getOption("drawPointCallback", setName) ||
    425       utils.Circles.DEFAULT;
    426   var strokePattern = g.getOption("strokePattern", setName);
    427   var drawPoints = g.getBooleanOption("drawPoints", setName);
    428   var pointSize = g.getNumericOption("pointSize", setName);
    429
    430   if (borderWidth && strokeWidth) {
    431     DygraphCanvasRenderer._drawStyledLine(e,
    432         g.getOption("strokeBorderColor", setName),
    433         strokeWidth + 2 * borderWidth,
    434         strokePattern,
    435         drawPoints,
    436         drawPointCallback,
    437         pointSize
    438         );
    439   }
    440
    441   DygraphCanvasRenderer._drawStyledLine(e,
    442       e.color,
    443       strokeWidth,
    444       strokePattern,
    445       drawPoints,
    446       drawPointCallback,
    447       pointSize
    448   );
    449 };
    450
    451 /**
    452  * Draws the shaded error bars/confidence intervals for each series.
    453  * This happens before the center lines are drawn, since the center lines
    454  * need to be drawn on top of the error bars for all series.
    455  * @private
    456  */
    457 DygraphCanvasRenderer._errorPlotter = function(e) {
    458   var g = e.dygraph;
    459   var setName = e.setName;
    460   var errorBars = g.getBooleanOption("errorBars") ||
    461       g.getBooleanOption("customBars");
    462   if (!errorBars) return;
    463
    464   var fillGraph = g.getBooleanOption("fillGraph", setName);
    465   if (fillGraph) {
    466     console.warn("Can't use fillGraph option with error bars");
    467   }
    468
    469   var ctx = e.drawingContext;
    470   var color = e.color;
    471   var fillAlpha = g.getNumericOption('fillAlpha', setName);
    472   var stepPlot = g.getBooleanOption("stepPlot", setName);
    473   var points = e.points;
    474
    475   var iter = utils.createIterator(points, 0, points.length,
    476       DygraphCanvasRenderer._getIteratorPredicate(
    477           g.getBooleanOption("connectSeparatedPoints", setName)));
    478
    479   var newYs;
    480
    481   // setup graphics context
    482   var prevX = NaN;
    483   var prevY = NaN;
    484   var prevYs = [-1, -1];
    485   // should be same color as the lines but only 15% opaque.
    486   var rgb = utils.toRGB_(color);
    487   var err_color =
    488       'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
    489   ctx.fillStyle = err_color;
    490   ctx.beginPath();
    491
    492   var isNullUndefinedOrNaN = function(x) {
    493     return (x === null ||
    494             x === undefined ||
    495             isNaN(x));
    496   };
    497
    498   while (iter.hasNext) {
    499     var point = iter.next();
    500     if ((!stepPlot && isNullUndefinedOrNaN(point.y)) ||
    501         (stepPlot && !isNaN(prevY) && isNullUndefinedOrNaN(prevY))) {
    502       prevX = NaN;
    503       continue;
    504     }
    505
    506     newYs = [ point.y_bottom, point.y_top ];
    507     if (stepPlot) {
    508       prevY = point.y;
    509     }
    510
    511     // The documentation specifically disallows nulls inside the point arrays,
    512     // but in case it happens we should do something sensible.
    513     if (isNaN(newYs[0])) newYs[0] = point.y;
    514     if (isNaN(newYs[1])) newYs[1] = point.y;
    515
    516     newYs[0] = e.plotArea.h * newYs[0] + e.plotArea.y;
    517     newYs[1] = e.plotArea.h * newYs[1] + e.plotArea.y;
    518     if (!isNaN(prevX)) {
    519       if (stepPlot) {
    520         ctx.moveTo(prevX, prevYs[0]);
    521         ctx.lineTo(point.canvasx, prevYs[0]);
    522         ctx.lineTo(point.canvasx, prevYs[1]);
    523       } else {
    524         ctx.moveTo(prevX, prevYs[0]);
    525         ctx.lineTo(point.canvasx, newYs[0]);
    526         ctx.lineTo(point.canvasx, newYs[1]);
    527       }
    528       ctx.lineTo(prevX, prevYs[1]);
    529       ctx.closePath();
    530     }
    531     prevYs = newYs;
    532     prevX = point.canvasx;
    533   }
    534   ctx.fill();
    535 };
    536
    537 /**
    538  * Proxy for CanvasRenderingContext2D which drops moveTo/lineTo calls which are
    539  * superfluous. It accumulates all movements which haven't changed the x-value
    540  * and only applies the two with the most extreme y-values.
    541  *
    542  * Calls to lineTo/moveTo must have non-decreasing x-values.
    543  */
    544 DygraphCanvasRenderer._fastCanvasProxy = function(context) {
    545   var pendingActions = [];  // array of [type, x, y] tuples
    546   var lastRoundedX = null;
    547   var lastFlushedX = null;
    548
    549   var LINE_TO = 1,
    550       MOVE_TO = 2;
    551
    552   var actionCount = 0;  // number of moveTos and lineTos passed to context.
    553
    554   // Drop superfluous motions
    555   // Assumes all pendingActions have the same (rounded) x-value.
    556   var compressActions = function(opt_losslessOnly) {
    557     if (pendingActions.length <= 1) return;
    558
    559     // Lossless compression: drop inconsequential moveTos.
    560     for (var i = pendingActions.length - 1; i > 0; i--) {
    561       var action = pendingActions[i];
    562       if (action[0] == MOVE_TO) {
    563         var prevAction = pendingActions[i - 1];
    564         if (prevAction[1] == action[1] && prevAction[2] == action[2]) {
    565           pendingActions.splice(i, 1);
    566         }
    567       }
    568     }
    569
    570     // Lossless compression: ... drop consecutive moveTos ...
    571     for (var i = 0; i < pendingActions.length - 1; /* incremented internally */) {
    572       var action = pendingActions[i];
    573       if (action[0] == MOVE_TO && pendingActions[i + 1][0] == MOVE_TO) {
    574         pendingActions.splice(i, 1);
    575       } else {
    576         i++;
    577       }
    578     }
    579
    580     // Lossy compression: ... drop all but the extreme y-values ...
    581     if (pendingActions.length > 2 && !opt_losslessOnly) {
    582       // keep an initial moveTo, but drop all others.
    583       var startIdx = 0;
    584       if (pendingActions[0][0] == MOVE_TO) startIdx++;
    585       var minIdx = null, maxIdx = null;
    586       for (var i = startIdx; i < pendingActions.length; i++) {
    587         var action = pendingActions[i];
    588         if (action[0] != LINE_TO) continue;
    589         if (minIdx === null && maxIdx === null) {
    590           minIdx = i;
    591           maxIdx = i;
    592         } else {
    593           var y = action[2];
    594           if (y < pendingActions[minIdx][2]) {
    595             minIdx = i;
    596           } else if (y > pendingActions[maxIdx][2]) {
    597             maxIdx = i;
    598           }
    599         }
    600       }
    601       var minAction = pendingActions[minIdx],
    602           maxAction = pendingActions[maxIdx];
    603       pendingActions.splice(startIdx, pendingActions.length - startIdx);
    604       if (minIdx < maxIdx) {
    605         pendingActions.push(minAction);
    606         pendingActions.push(maxAction);
    607       } else if (minIdx > maxIdx) {
    608         pendingActions.push(maxAction);
    609         pendingActions.push(minAction);
    610       } else {
    611         pendingActions.push(minAction);
    612       }
    613     }
    614   };
    615
    616   var flushActions = function(opt_noLossyCompression) {
    617     compressActions(opt_noLossyCompression);
    618     for (var i = 0, len = pendingActions.length; i < len; i++) {
    619       var action = pendingActions[i];
    620       if (action[0] == LINE_TO) {
    621         context.lineTo(action[1], action[2]);
    622       } else if (action[0] == MOVE_TO) {
    623         context.moveTo(action[1], action[2]);
    624       }
    625     }
    626     if (pendingActions.length) {
    627       lastFlushedX = pendingActions[pendingActions.length - 1][1];
    628     }
    629     actionCount += pendingActions.length;
    630     pendingActions = [];
    631   };
    632
    633   var addAction = function(action, x, y) {
    634     var rx = Math.round(x);
    635     if (lastRoundedX === null || rx != lastRoundedX) {
    636       // if there are large gaps on the x-axis, it's essential to keep the
    637       // first and last point as well.
    638       var hasGapOnLeft = (lastRoundedX - lastFlushedX > 1),
    639           hasGapOnRight = (rx - lastRoundedX > 1),
    640           hasGap = hasGapOnLeft || hasGapOnRight;
    641       flushActions(hasGap);
    642       lastRoundedX = rx;
    643     }
    644     pendingActions.push([action, x, y]);
    645   };
    646
    647   return {
    648     moveTo: function(x, y) {
    649       addAction(MOVE_TO, x, y);
    650     },
    651     lineTo: function(x, y) {
    652       addAction(LINE_TO, x, y);
    653     },
    654
    655     // for major operations like stroke/fill, we skip compression to ensure
    656     // that there are no artifacts at the right edge.
    657     stroke:    function() { flushActions(true); context.stroke(); },
    658     fill:      function() { flushActions(true); context.fill(); },
    659     beginPath: function() { flushActions(true); context.beginPath(); },
    660     closePath: function() { flushActions(true); context.closePath(); },
    661
    662     _count: function() { return actionCount; }
    663   };
    664 };
    665
    666 /**
    667  * Draws the shaded regions when "fillGraph" is set. Not to be confused with
    668  * error bars.
    669  *
    670  * For stacked charts, it's more convenient to handle all the series
    671  * simultaneously. So this plotter plots all the points on the first series
    672  * it's asked to draw, then ignores all the other series.
    673  *
    674  * @private
    675  */
    676 DygraphCanvasRenderer._fillPlotter = function(e) {
    677   // Skip if we're drawing a single series for interactive highlight overlay.
    678   if (e.singleSeriesName) return;
    679
    680   // We'll handle all the series at once, not one-by-one.
    681   if (e.seriesIndex !== 0) return;
    682
    683   var g = e.dygraph;
    684   var setNames = g.getLabels().slice(1);  // remove x-axis
    685
    686   // getLabels() includes names for invisible series, which are not included in
    687   // allSeriesPoints. We remove those to make the two match.
    688   // TODO(danvk): provide a simpler way to get this information.
    689   for (var i = setNames.length; i >= 0; i--) {
    690     if (!g.visibility()[i]) setNames.splice(i, 1);
    691   }
    692
    693   var anySeriesFilled = (function() {
    694     for (var i = 0; i < setNames.length; i++) {
    695       if (g.getBooleanOption("fillGraph", setNames[i])) return true;
    696     }
    697     return false;
    698   })();
    699
    700   if (!anySeriesFilled) return;
    701
    702   var area = e.plotArea;
    703   var sets = e.allSeriesPoints;
    704   var setCount = sets.length;
    705
    706   var stackedGraph = g.getBooleanOption("stackedGraph");
    707   var colors = g.getColors();
    708
    709   // For stacked graphs, track the baseline for filling.
    710   //
    711   // The filled areas below graph lines are trapezoids with two
    712   // vertical edges. The top edge is the line segment being drawn, and
    713   // the baseline is the bottom edge. Each baseline corresponds to the
    714   // top line segment from the previous stacked line. In the case of
    715   // step plots, the trapezoids are rectangles.
    716   var baseline = {};
    717   var currBaseline;
    718   var prevStepPlot;  // for different line drawing modes (line/step) per series
    719
    720   // Helper function to trace a line back along the baseline.
    721   var traceBackPath = function(ctx, baselineX, baselineY, pathBack) {
    722     ctx.lineTo(baselineX, baselineY);
    723     if (stackedGraph) {
    724       for (var i = pathBack.length - 1; i >= 0; i--) {
    725         var pt = pathBack[i];
    726         ctx.lineTo(pt[0], pt[1]);
    727       }
    728     }
    729   };
    730
    731   // process sets in reverse order (needed for stacked graphs)
    732   for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) {
    733     var ctx = e.drawingContext;
    734     var setName = setNames[setIdx];
    735     if (!g.getBooleanOption('fillGraph', setName)) continue;
    736
    737     var fillAlpha = g.getNumericOption('fillAlpha', setName);
    738     var stepPlot = g.getBooleanOption('stepPlot', setName);
    739     var color = colors[setIdx];
    740     var axis = g.axisPropertiesForSeries(setName);
    741     var axisY = 1.0 + axis.minyval * axis.yscale;
    742     if (axisY < 0.0) axisY = 0.0;
    743     else if (axisY > 1.0) axisY = 1.0;
    744     axisY = area.h * axisY + area.y;
    745
    746     var points = sets[setIdx];
    747     var iter = utils.createIterator(points, 0, points.length,
    748         DygraphCanvasRenderer._getIteratorPredicate(
    749             g.getBooleanOption("connectSeparatedPoints", setName)));
    750
    751     // setup graphics context
    752     var prevX = NaN;
    753     var prevYs = [-1, -1];
    754     var newYs;
    755     // should be same color as the lines but only 15% opaque.
    756     var rgb = utils.toRGB_(color);
    757     var err_color =
    758         'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
    759     ctx.fillStyle = err_color;
    760     ctx.beginPath();
    761     var last_x, is_first = true;
    762
    763     // If the point density is high enough, dropping segments on their way to
    764     // the canvas justifies the overhead of doing so.
    765     if (points.length > 2 * g.width_ || Dygraph.FORCE_FAST_PROXY) {
    766       ctx = DygraphCanvasRenderer._fastCanvasProxy(ctx);
    767     }
    768
    769     // For filled charts, we draw points from left to right, then back along
    770     // the x-axis to complete a shape for filling.
    771     // For stacked plots, this "back path" is a more complex shape. This array
    772     // stores the [x, y] values needed to trace that shape.
    773     var pathBack = [];
    774
    775     // TODO(danvk): there are a lot of options at play in this loop.
    776     //     The logic would be much clearer if some (e.g. stackGraph and
    777     //     stepPlot) were split off into separate sub-plotters.
    778     var point;
    779     while (iter.hasNext) {
    780       point = iter.next();
    781       if (!utils.isOK(point.y) && !stepPlot) {
    782         traceBackPath(ctx, prevX, prevYs[1], pathBack);
    783         pathBack = [];
    784         prevX = NaN;
    785         if (point.y_stacked !== null && !isNaN(point.y_stacked)) {
    786           baseline[point.canvasx] = area.h * point.y_stacked + area.y;
    787         }
    788         continue;
    789       }
    790       if (stackedGraph) {
    791         if (!is_first && last_x == point.xval) {
    792           continue;
    793         } else {
    794           is_first = false;
    795           last_x = point.xval;
    796         }
    797
    798         currBaseline = baseline[point.canvasx];
    799         var lastY;
    800         if (currBaseline === undefined) {
    801           lastY = axisY;
    802         } else {
    803           if(prevStepPlot) {
    804             lastY = currBaseline[0];
    805           } else {
    806             lastY = currBaseline;
    807           }
    808         }
    809         newYs = [ point.canvasy, lastY ];
    810
    811         if (stepPlot) {
    812           // Step plots must keep track of the top and bottom of
    813           // the baseline at each point.
    814           if (prevYs[0] === -1) {
    815             baseline[point.canvasx] = [ point.canvasy, axisY ];
    816           } else {
    817             baseline[point.canvasx] = [ point.canvasy, prevYs[0] ];
    818           }
    819         } else {
    820           baseline[point.canvasx] = point.canvasy;
    821         }
    822
    823       } else {
    824         if (isNaN(point.canvasy) && stepPlot) {
    825           newYs = [ area.y + area.h, axisY ];
    826         } else {
    827           newYs = [ point.canvasy, axisY ];
    828         }
    829       }
    830       if (!isNaN(prevX)) {
    831         // Move to top fill point
    832         if (stepPlot) {
    833           ctx.lineTo(point.canvasx, prevYs[0]);
    834           ctx.lineTo(point.canvasx, newYs[0]);
    835         } else {
    836           ctx.lineTo(point.canvasx, newYs[0]);
    837         }
    838
    839         // Record the baseline for the reverse path.
    840         if (stackedGraph) {
    841           pathBack.push([prevX, prevYs[1]]);
    842           if (prevStepPlot && currBaseline) {
    843             // Draw to the bottom of the baseline
    844             pathBack.push([point.canvasx, currBaseline[1]]);
    845           } else {
    846             pathBack.push([point.canvasx, newYs[1]]);
    847           }
    848         }
    849       } else {
    850         ctx.moveTo(point.canvasx, newYs[1]);
    851         ctx.lineTo(point.canvasx, newYs[0]);
    852       }
    853       prevYs = newYs;
    854       prevX = point.canvasx;
    855     }
    856     prevStepPlot = stepPlot;
    857     if (newYs && point) {
    858       traceBackPath(ctx, point.canvasx, newYs[1], pathBack);
    859       pathBack = [];
    860     }
    861     ctx.fill();
    862   }
    863 };
    864
    865 export default DygraphCanvasRenderer;
    866