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