      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