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