Remove isAndroid checks (#784)
[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 import Dygraph from './dygraph';
32
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, error bars, 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 error bars.
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 error bars/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 error bars 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 error bars");
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 /**
540 * Proxy for CanvasRenderingContext2D which drops moveTo/lineTo calls which are
541 * superfluous. It accumulates all movements which haven't changed the x-value
542 * and only applies the two with the most extreme y-values.
543 *
544 * Calls to lineTo/moveTo must have non-decreasing x-values.
545 */
546 DygraphCanvasRenderer._fastCanvasProxy = function(context) {
547 var pendingActions = []; // array of [type, x, y] tuples
548 var lastRoundedX = null;
549 var lastFlushedX = null;
550
551 var LINE_TO = 1,
552 MOVE_TO = 2;
553
554 var actionCount = 0; // number of moveTos and lineTos passed to context.
555
556 // Drop superfluous motions
557 // Assumes all pendingActions have the same (rounded) x-value.
558 var compressActions = function(opt_losslessOnly) {
559 if (pendingActions.length <= 1) return;
560
561 // Lossless compression: drop inconsequential moveTos.
562 for (var i = pendingActions.length - 1; i > 0; i--) {
563 var action = pendingActions[i];
564 if (action[0] == MOVE_TO) {
565 var prevAction = pendingActions[i - 1];
566 if (prevAction[1] == action[1] && prevAction[2] == action[2]) {
567 pendingActions.splice(i, 1);
568 }
569 }
570 }
571
572 // Lossless compression: ... drop consecutive moveTos ...
573 for (var i = 0; i < pendingActions.length - 1; /* incremented internally */) {
574 var action = pendingActions[i];
575 if (action[0] == MOVE_TO && pendingActions[i + 1][0] == MOVE_TO) {
576 pendingActions.splice(i, 1);
577 } else {
578 i++;
579 }
580 }
581
582 // Lossy compression: ... drop all but the extreme y-values ...
583 if (pendingActions.length > 2 && !opt_losslessOnly) {
584 // keep an initial moveTo, but drop all others.
585 var startIdx = 0;
586 if (pendingActions[0][0] == MOVE_TO) startIdx++;
587 var minIdx = null, maxIdx = null;
588 for (var i = startIdx; i < pendingActions.length; i++) {
589 var action = pendingActions[i];
590 if (action[0] != LINE_TO) continue;
591 if (minIdx === null && maxIdx === null) {
592 minIdx = i;
593 maxIdx = i;
594 } else {
595 var y = action[2];
596 if (y < pendingActions[minIdx][2]) {
597 minIdx = i;
598 } else if (y > pendingActions[maxIdx][2]) {
599 maxIdx = i;
600 }
601 }
602 }
603 var minAction = pendingActions[minIdx],
604 maxAction = pendingActions[maxIdx];
605 pendingActions.splice(startIdx, pendingActions.length - startIdx);
606 if (minIdx < maxIdx) {
607 pendingActions.push(minAction);
608 pendingActions.push(maxAction);
609 } else if (minIdx > maxIdx) {
610 pendingActions.push(maxAction);
611 pendingActions.push(minAction);
612 } else {
613 pendingActions.push(minAction);
614 }
615 }
616 };
617
618 var flushActions = function(opt_noLossyCompression) {
619 compressActions(opt_noLossyCompression);
620 for (var i = 0, len = pendingActions.length; i < len; i++) {
621 var action = pendingActions[i];
622 if (action[0] == LINE_TO) {
623 context.lineTo(action[1], action[2]);
624 } else if (action[0] == MOVE_TO) {
625 context.moveTo(action[1], action[2]);
626 }
627 }
628 if (pendingActions.length) {
629 lastFlushedX = pendingActions[pendingActions.length - 1][1];
630 }
631 actionCount += pendingActions.length;
632 pendingActions = [];
633 };
634
635 var addAction = function(action, x, y) {
636 var rx = Math.round(x);
637 if (lastRoundedX === null || rx != lastRoundedX) {
638 // if there are large gaps on the x-axis, it's essential to keep the
639 // first and last point as well.
640 var hasGapOnLeft = (lastRoundedX - lastFlushedX > 1),
641 hasGapOnRight = (rx - lastRoundedX > 1),
642 hasGap = hasGapOnLeft || hasGapOnRight;
643 flushActions(hasGap);
644 lastRoundedX = rx;
645 }
646 pendingActions.push([action, x, y]);
647 };
648
649 return {
650 moveTo: function(x, y) {
651 addAction(MOVE_TO, x, y);
652 },
653 lineTo: function(x, y) {
654 addAction(LINE_TO, x, y);
655 },
656
657 // for major operations like stroke/fill, we skip compression to ensure
658 // that there are no artifacts at the right edge.
659 stroke: function() { flushActions(true); context.stroke(); },
660 fill: function() { flushActions(true); context.fill(); },
661 beginPath: function() { flushActions(true); context.beginPath(); },
662 closePath: function() { flushActions(true); context.closePath(); },
663
664 _count: function() { return actionCount; }
665 };
666 };
667
668 /**
669 * Draws the shaded regions when "fillGraph" is set. Not to be confused with
670 * error bars.
671 *
672 * For stacked charts, it's more convenient to handle all the series
673 * simultaneously. So this plotter plots all the points on the first series
674 * it's asked to draw, then ignores all the other series.
675 *
676 * @private
677 */
678 DygraphCanvasRenderer._fillPlotter = function(e) {
679 // Skip if we're drawing a single series for interactive highlight overlay.
680 if (e.singleSeriesName) return;
681
682 // We'll handle all the series at once, not one-by-one.
683 if (e.seriesIndex !== 0) return;
684
685 var g = e.dygraph;
686 var setNames = g.getLabels().slice(1); // remove x-axis
687
688 // getLabels() includes names for invisible series, which are not included in
689 // allSeriesPoints. We remove those to make the two match.
690 // TODO(danvk): provide a simpler way to get this information.
691 for (var i = setNames.length; i >= 0; i--) {
692 if (!g.visibility()[i]) setNames.splice(i, 1);
693 }
694
695 var anySeriesFilled = (function() {
696 for (var i = 0; i < setNames.length; i++) {
697 if (g.getBooleanOption("fillGraph", setNames[i])) return true;
698 }
699 return false;
700 })();
701
702 if (!anySeriesFilled) return;
703
704 var area = e.plotArea;
705 var sets = e.allSeriesPoints;
706 var setCount = sets.length;
707
708 var stackedGraph = g.getBooleanOption("stackedGraph");
709 var colors = g.getColors();
710
711 // For stacked graphs, track the baseline for filling.
712 //
713 // The filled areas below graph lines are trapezoids with two
714 // vertical edges. The top edge is the line segment being drawn, and
715 // the baseline is the bottom edge. Each baseline corresponds to the
716 // top line segment from the previous stacked line. In the case of
717 // step plots, the trapezoids are rectangles.
718 var baseline = {};
719 var currBaseline;
720 var prevStepPlot; // for different line drawing modes (line/step) per series
721
722 // Helper function to trace a line back along the baseline.
723 var traceBackPath = function(ctx, baselineX, baselineY, pathBack) {
724 ctx.lineTo(baselineX, baselineY);
725 if (stackedGraph) {
726 for (var i = pathBack.length - 1; i >= 0; i--) {
727 var pt = pathBack[i];
728 ctx.lineTo(pt[0], pt[1]);
729 }
730 }
731 };
732
733 // process sets in reverse order (needed for stacked graphs)
734 for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) {
735 var ctx = e.drawingContext;
736 var setName = setNames[setIdx];
737 if (!g.getBooleanOption('fillGraph', setName)) continue;
738
739 var fillAlpha = g.getNumericOption('fillAlpha', setName);
740 var stepPlot = g.getBooleanOption('stepPlot', setName);
741 var color = colors[setIdx];
742 var axis = g.axisPropertiesForSeries(setName);
743 var axisY = 1.0 + axis.minyval * axis.yscale;
744 if (axisY < 0.0) axisY = 0.0;
745 else if (axisY > 1.0) axisY = 1.0;
746 axisY = area.h * axisY + area.y;
747
748 var points = sets[setIdx];
749 var iter = utils.createIterator(points, 0, points.length,
750 DygraphCanvasRenderer._getIteratorPredicate(
751 g.getBooleanOption("connectSeparatedPoints", setName)));
752
753 // setup graphics context
754 var prevX = NaN;
755 var prevYs = [-1, -1];
756 var newYs;
757 // should be same color as the lines but only 15% opaque.
758 var rgb = utils.toRGB_(color);
759 var err_color =
760 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
761 ctx.fillStyle = err_color;
762 ctx.beginPath();
763 var last_x, is_first = true;
764
765 // If the point density is high enough, dropping segments on their way to
766 // the canvas justifies the overhead of doing so.
767 if (points.length > 2 * g.width_ || Dygraph.FORCE_FAST_PROXY) {
768 ctx = DygraphCanvasRenderer._fastCanvasProxy(ctx);
769 }
770
771 // For filled charts, we draw points from left to right, then back along
772 // the x-axis to complete a shape for filling.
773 // For stacked plots, this "back path" is a more complex shape. This array
774 // stores the [x, y] values needed to trace that shape.
775 var pathBack = [];
776
777 // TODO(danvk): there are a lot of options at play in this loop.
778 // The logic would be much clearer if some (e.g. stackGraph and
779 // stepPlot) were split off into separate sub-plotters.
780 var point;
781 while (iter.hasNext) {
782 point = iter.next();
783 if (!utils.isOK(point.y) && !stepPlot) {
784 traceBackPath(ctx, prevX, prevYs[1], pathBack);
785 pathBack = [];
786 prevX = NaN;
787 if (point.y_stacked !== null && !isNaN(point.y_stacked)) {
788 baseline[point.canvasx] = area.h * point.y_stacked + area.y;
789 }
790 continue;
791 }
792 if (stackedGraph) {
793 if (!is_first && last_x == point.xval) {
794 continue;
795 } else {
796 is_first = false;
797 last_x = point.xval;
798 }
799
800 currBaseline = baseline[point.canvasx];
801 var lastY;
802 if (currBaseline === undefined) {
803 lastY = axisY;
804 } else {
805 if(prevStepPlot) {
806 lastY = currBaseline[0];
807 } else {
808 lastY = currBaseline;
809 }
810 }
811 newYs = [ point.canvasy, lastY ];
812
813 if (stepPlot) {
814 // Step plots must keep track of the top and bottom of
815 // the baseline at each point.
816 if (prevYs[0] === -1) {
817 baseline[point.canvasx] = [ point.canvasy, axisY ];
818 } else {
819 baseline[point.canvasx] = [ point.canvasy, prevYs[0] ];
820 }
821 } else {
822 baseline[point.canvasx] = point.canvasy;
823 }
824
825 } else {
826 if (isNaN(point.canvasy) && stepPlot) {
827 newYs = [ area.y + area.h, axisY ];
828 } else {
829 newYs = [ point.canvasy, axisY ];
830 }
831 }
832 if (!isNaN(prevX)) {
833 // Move to top fill point
834 if (stepPlot) {
835 ctx.lineTo(point.canvasx, prevYs[0]);
836 ctx.lineTo(point.canvasx, newYs[0]);
837 } else {
838 ctx.lineTo(point.canvasx, newYs[0]);
839 }
840
841 // Record the baseline for the reverse path.
842 if (stackedGraph) {
843 pathBack.push([prevX, prevYs[1]]);
844 if (prevStepPlot && currBaseline) {
845 // Draw to the bottom of the baseline
846 pathBack.push([point.canvasx, currBaseline[1]]);
847 } else {
848 pathBack.push([point.canvasx, newYs[1]]);
849 }
850 }
851 } else {
852 ctx.moveTo(point.canvasx, newYs[1]);
853 ctx.lineTo(point.canvasx, newYs[0]);
854 }
855 prevYs = newYs;
856 prevX = point.canvasx;
857 }
858 prevStepPlot = stepPlot;
859 if (newYs && point) {
860 traceBackPath(ctx, point.canvasx, newYs[1], pathBack);
861 pathBack = [];
862 }
863 ctx.fill();
864 }
865 };
866
867 export default DygraphCanvasRenderer;