UNPKG

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