Remove isAndroid checks (#784)
[dygraphs.git] / src / dygraph-canvas.js
CommitLineData
88e95c46
DV
1/**
2 * @license
3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
6a1aa64f
DV
6
7/**
74a5af31
DV
8 * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
9 * needs of dygraphs.
10 *
3df0ccf0 11 * In particular, support for:
0abfbd7e 12 * - grid overlays
3df0ccf0
DV
13 * - error bars
14 * - dygraphs attribute system
6a1aa64f
DV
15 */
16
6a1aa64f 17/**
423f5ed3
DV
18 * The DygraphCanvasRenderer class does the actual rendering of the chart onto
19 * a canvas. It's based on PlotKit.CanvasRenderer.
6a1aa64f 20 * @param {Object} element The canvas to attach to
2cf95fff
RK
21 * @param {Object} elementContext The 2d context of the canvas (injected so it
22 * can be mocked for testing.)
285a6bda 23 * @param {Layout} layout The DygraphLayout object for this graph.
74a5af31 24 * @constructor
6a1aa64f 25 */
c0f54d4f 26
464b5f50 27/*global Dygraph:false */
c0f54d4f
DV
28"use strict";
29
6ecc0739 30import * as utils from './dygraph-utils';
e8c70e4e 31import Dygraph from './dygraph';
6ecc0739 32
79253bd0 33
8cfe592f
DV
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 *
7b00a3cd 44 * @param {Dygraph} dygraph The chart to which this renderer belongs.
48fc4786 45 * @param {HTMLCanvasElement} element The <canvas> DOM element on which to draw.
8cfe592f
DV
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 */
c0f54d4f 51var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
9317362d 52 this.dygraph_ = dygraph;
fbe31dc8 53
fbe31dc8 54 this.layout = layout;
b0c3b730 55 this.element = element;
2cf95fff 56 this.elementContext = elementContext;
fbe31dc8 57
7c39bb3a
DV
58 this.height = dygraph.height_;
59 this.width = dygraph.width_;
fbe31dc8
DV
60
61 // --- check whether everything is ok before we return
6ecc0739 62 if (!utils.isCanvasSupported(this.element)) {
9901b0c1
DV
63 throw "Canvas is not supported.";
64 }
fbe31dc8
DV
65
66 // internal state
70be5ed1 67 this.area = layout.getPlotArea();
423f5ed3
DV
68
69 // Set up a clipping area for the canvas (and the interaction canvas).
70 // This ensures that we don't overdraw.
aec24511
DV
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();
423f5ed3
DV
80};
81
38e3d209 82/**
8cfe592f
DV
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 */
fbe31dc8 88DygraphCanvasRenderer.prototype.clear = function() {
9901b0c1 89 this.elementContext.clearRect(0, 0, this.width, this.height);
fbe31dc8
DV
90};
91
8cfe592f 92/**
8cfe592f
DV
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
6a1aa64f 98 */
285a6bda 99DygraphCanvasRenderer.prototype.render = function() {
38e3d209
DV
100 // attaches point.canvas{x,y}
101 this._updatePoints();
102
103 // actually draws the chart.
2ce09b19 104 this._renderLineChart();
fbe31dc8
DV
105};
106
ccb0001c 107/**
8722284b
RK
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.
ccb0001c 112 */
8722284b 113DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) {
42a9ebb8
DV
114 return connectSeparatedPoints ?
115 DygraphCanvasRenderer._predicateThatSkipsEmptyPoints :
116 null;
0f20de1c 117};
8722284b
RK
118
119DygraphCanvasRenderer._predicateThatSkipsEmptyPoints =
0f20de1c
DV
120 function(array, idx) {
121 return array[idx].yval !== null;
122};
04c104d7 123
9f6db80e 124/**
38e3d209
DV
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.
9f6db80e
DV
127 * @private
128 */
38e3d209
DV
129DygraphCanvasRenderer._drawStyledLine = function(e,
130 color, strokeWidth, strokePattern, drawPoints,
5469113b 131 drawPointCallback, pointSize) {
38e3d209 132 var g = e.dygraph;
99a77a04 133 // TODO(konigsberg): Compute attributes outside this method call.
0e85a437 134 var stepPlot = g.getBooleanOption("stepPlot", e.setName);
2f56cd46 135
6ecc0739 136 if (!utils.isArrayLike(strokePattern)) {
857a6931
KW
137 strokePattern = null;
138 }
139
0e85a437 140 var drawGapPoints = g.getBooleanOption('drawGapEdgePoints', e.setName);
38e3d209
DV
141
142 var points = e.points;
b85358e2 143 var setName = e.setName;
6ecc0739 144 var iter = utils.createIterator(points, 0, points.length,
9f6db80e 145 DygraphCanvasRenderer._getIteratorPredicate(
0e85a437 146 g.getBooleanOption("connectSeparatedPoints", setName)));
7d1afbb9 147
fb63bf1b
DV
148 var stroking = strokePattern && (strokePattern.length >= 2);
149
38e3d209 150 var ctx = e.drawingContext;
0140347d 151 ctx.save();
fb63bf1b 152 if (stroking) {
e8c70e4e 153 if (ctx.setLineDash) ctx.setLineDash(strokePattern);
b843b52c 154 }
fb63bf1b 155
38e3d209
DV
156 var pointsOnLine = DygraphCanvasRenderer._drawSeries(
157 e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color);
158 DygraphCanvasRenderer._drawPointsOnLine(
159 e, pointsOnLine, drawPointCallback, color, pointSize);
31f8e58b 160
fb63bf1b 161 if (stroking) {
e8c70e4e 162 if (ctx.setLineDash) ctx.setLineDash([]);
fb63bf1b 163 }
b843b52c 164
fb63bf1b 165 ctx.restore();
31f8e58b
RK
166};
167
38e3d209
DV
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 */
176DygraphCanvasRenderer._drawSeries = function(e,
177 iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color) {
31f8e58b 178
31f8e58b
RK
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
b843b52c 184 var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
31f8e58b
RK
185 var first = true; // the first cycle through the while loop
186
38e3d209 187 var ctx = e.drawingContext;
0140347d
DV
188 ctx.beginPath();
189 ctx.strokeStyle = color;
190 ctx.lineWidth = strokeWidth;
31f8e58b 191
239454e2 192 // NOTE: we break the iterator's encapsulation here for about a 25% speedup.
c560c848
DV
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)) {
0f20de1c
DV
201 i++;
202 }
c560c848
DV
203 if (i == limit) break;
204 point = arr[i];
0f20de1c
DV
205 }
206
b7ec6c55
PH
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?
a02978e2 210 if (point.canvasy === null || point.canvasy != point.canvasy) {
31f8e58b 211 if (stepPlot && prevCanvasX !== null) {
857a6931 212 // Draw a horizontal line to the start of the missing data
42a9ebb8
DV
213 ctx.moveTo(prevCanvasX, prevCanvasY);
214 ctx.lineTo(point.canvasx, prevCanvasY);
857a6931 215 }
31f8e58b 216 prevCanvasX = prevCanvasY = null;
857a6931 217 } else {
0f20de1c 218 isIsolated = false;
902091b8 219 if (drawGapPoints || prevCanvasX === null) {
0f20de1c 220 iter.nextIdx_ = i;
0cd1ad15 221 iter.next();
82f9b10f 222 nextCanvasY = iter.hasNext ? iter.peek.canvasy : null;
0f20de1c 223
0f20de1c
DV
224 var isNextCanvasYNullOrNaN = nextCanvasY === null ||
225 nextCanvasY != nextCanvasY;
902091b8 226 isIsolated = (prevCanvasX === null && isNextCanvasYNullOrNaN);
0f20de1c
DV
227 if (drawGapPoints) {
228 // Also consider a point to be "isolated" if it's adjacent to a
229 // null point, excluding the graph edges.
902091b8 230 if ((!first && prevCanvasX === null) ||
0f20de1c
DV
231 (iter.hasNext && isNextCanvasYNullOrNaN)) {
232 isIsolated = true;
233 }
19b84fe7
KW
234 }
235 }
0f20de1c 236
31f8e58b 237 if (prevCanvasX !== null) {
857a6931 238 if (strokeWidth) {
857a6931 239 if (stepPlot) {
0140347d
DV
240 ctx.moveTo(prevCanvasX, prevCanvasY);
241 ctx.lineTo(point.canvasx, prevCanvasY);
857a6931 242 }
239454e2 243
0140347d 244 ctx.lineTo(point.canvasx, point.canvasy);
b843b52c 245 }
9f636500
DV
246 } else {
247 ctx.moveTo(point.canvasx, point.canvasy);
b843b52c 248 }
b843b52c 249 if (drawPoints || isIsolated) {
b616fad1 250 pointsOnLine.push([point.canvasx, point.canvasy, point.idx]);
b843b52c 251 }
31f8e58b
RK
252 prevCanvasX = point.canvasx;
253 prevCanvasY = point.canvasy;
b843b52c 254 }
7d1afbb9 255 first = false;
b843b52c 256 }
0140347d 257 ctx.stroke();
31f8e58b 258 return pointsOnLine;
857a6931
KW
259};
260
38e3d209
DV
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 */
268DygraphCanvasRenderer._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();
4ee251cb 274 drawPointCallback.call(e.dygraph,
ba697462 275 e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize, cb[2]);
38e3d209 276 ctx.restore();
857a6931 277 }
42a9ebb8 278};
ce49c2fa 279
6a1aa64f 280/**
38e3d209 281 * Attaches canvas coordinates to the points array.
758a629f 282 * @private
6a1aa64f 283 */
38e3d209 284DygraphCanvasRenderer.prototype._updatePoints = function() {
ff00d3e2
DV
285 // Update Points
286 // TODO(danvk): here
b843b52c
RK
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.
e60234cd
DV
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%.
a12a78ae 297 var sets = this.layout.points;
38e3d209 298 for (var i = sets.length; i--;) {
a12a78ae
DV
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 }
6a1aa64f 305 }
38e3d209 306};
6a1aa64f 307
38e3d209
DV
308/**
309 * Add canvas Actually draw the lines chart, including error bars.
38e3d209
DV
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.
48fc4786
RK
314 *
315 * @param {string=} opt_seriesName when specified, only that series will
34655aba
RK
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.
38e3d209
DV
320 * @private
321 */
322DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) {
323 var ctx = opt_ctx || this.elementContext;
38e3d209 324 var i;
6a834bbb 325
38e3d209
DV
326 var sets = this.layout.points;
327 var setNames = this.layout.setNames;
42a9ebb8 328 var setName;
38e3d209
DV
329
330 this.colors = this.dygraph_.colorsMap_;
331
332 // Determine which series have specialized plotters.
0e85a437 333 var plotter_attr = this.dygraph_.getOption("plotter");
38e3d209 334 var plotters = plotter_attr;
6ecc0739 335 if (!utils.isArrayLike(plotters)) {
38e3d209 336 plotters = [plotters];
80aaae18
DV
337 }
338
38e3d209
DV
339 var setPlotters = {}; // series name -> plotter fn.
340 for (i = 0; i < setNames.length; i++) {
42a9ebb8 341 setName = setNames[i];
0e85a437 342 var setPlotter = this.dygraph_.getOption("plotter", setName);
38e3d209
DV
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++) {
42a9ebb8 353 setName = setNames[j];
4b2e41a4 354 if (opt_seriesName && setName != opt_seriesName) continue;
38e3d209
DV
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,
3c080cd0 386 singleSeriesName: opt_seriesName,
38e3d209
DV
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 */
398DygraphCanvasRenderer._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);
80aaae18 409 }
6a1aa64f 410};
79253bd0 411
01a14b85 412/**
38e3d209
DV
413 * Plotter which draws the central lines for a series.
414 * @private
415 */
416DygraphCanvasRenderer._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.
0e85a437 424 var borderWidth = g.getNumericOption("strokeBorderWidth", setName);
38e3d209 425 var drawPointCallback = g.getOption("drawPointCallback", setName) ||
6ecc0739 426 utils.Circles.DEFAULT;
38e3d209 427 var strokePattern = g.getOption("strokePattern", setName);
0e85a437
DV
428 var drawPoints = g.getBooleanOption("drawPoints", setName);
429 var pointSize = g.getNumericOption("pointSize", setName);
38e3d209
DV
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 );
42a9ebb8 450};
38e3d209
DV
451
452/**
01a14b85
DV
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.
01a14b85
DV
456 * @private
457 */
38e3d209
DV
458DygraphCanvasRenderer._errorPlotter = function(e) {
459 var g = e.dygraph;
e2d8db3a 460 var setName = e.setName;
0e85a437
DV
461 var errorBars = g.getBooleanOption("errorBars") ||
462 g.getBooleanOption("customBars");
38e3d209
DV
463 if (!errorBars) return;
464
0e85a437 465 var fillGraph = g.getBooleanOption("fillGraph", setName);
38e3d209 466 if (fillGraph) {
8a68db7d 467 console.warn("Can't use fillGraph option with error bars");
38e3d209 468 }
6a6439da 469
38e3d209
DV
470 var ctx = e.drawingContext;
471 var color = e.color;
0e85a437
DV
472 var fillAlpha = g.getNumericOption('fillAlpha', setName);
473 var stepPlot = g.getBooleanOption("stepPlot", setName);
38e3d209 474 var points = e.points;
6a6439da 475
6ecc0739 476 var iter = utils.createIterator(points, 0, points.length,
38e3d209 477 DygraphCanvasRenderer._getIteratorPredicate(
0e85a437 478 g.getBooleanOption("connectSeparatedPoints", setName)));
6a6439da 479
38e3d209 480 var newYs;
6a6439da 481
38e3d209
DV
482 // setup graphics context
483 var prevX = NaN;
484 var prevY = NaN;
485 var prevYs = [-1, -1];
38e3d209 486 // should be same color as the lines but only 15% opaque.
6ecc0739 487 var rgb = utils.toRGB_(color);
38e3d209
DV
488 var err_color =
489 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
490 ctx.fillStyle = err_color;
491 ctx.beginPath();
cf89eeed
DV
492
493 var isNullUndefinedOrNaN = function(x) {
494 return (x === null ||
495 x === undefined ||
496 isNaN(x));
497 };
498
38e3d209
DV
499 while (iter.hasNext) {
500 var point = iter.next();
cf89eeed
DV
501 if ((!stepPlot && isNullUndefinedOrNaN(point.y)) ||
502 (stepPlot && !isNaN(prevY) && isNullUndefinedOrNaN(prevY))) {
38e3d209
DV
503 prevX = NaN;
504 continue;
505 }
6a6439da 506
87c5a64c 507 newYs = [ point.y_bottom, point.y_top ];
38e3d209 508 if (stepPlot) {
38e3d209 509 prevY = point.y;
38e3d209 510 }
87c5a64c
DV
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
38e3d209
DV
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)) {
a5701188 520 if (stepPlot) {
38e3d209 521 ctx.moveTo(prevX, prevYs[0]);
82dd90c5 522 ctx.lineTo(point.canvasx, prevYs[0]);
523 ctx.lineTo(point.canvasx, prevYs[1]);
38e3d209 524 } else {
82dd90c5 525 ctx.moveTo(prevX, prevYs[0]);
526 ctx.lineTo(point.canvasx, newYs[0]);
527 ctx.lineTo(point.canvasx, newYs[1]);
6a6439da 528 }
82dd90c5 529 ctx.lineTo(prevX, prevYs[1]);
38e3d209 530 ctx.closePath();
6a6439da 531 }
38e3d209
DV
532 prevYs = newYs;
533 prevX = point.canvasx;
6a6439da 534 }
38e3d209 535 ctx.fill();
42a9ebb8 536};
6a6439da 537
20b87d28
DV
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.
aec24511 543 *
20b87d28
DV
544 * Calls to lineTo/moveTo must have non-decreasing x-values.
545 */
546DygraphCanvasRenderer._fastCanvasProxy = function(context) {
547 var pendingActions = []; // array of [type, x, y] tuples
548 var lastRoundedX = null;
c0ec1a37 549 var lastFlushedX = null;
20b87d28
DV
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 }
c0ec1a37
DV
628 if (pendingActions.length) {
629 lastFlushedX = pendingActions[pendingActions.length - 1][1];
630 }
20b87d28
DV
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) {
c0ec1a37
DV
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);
20b87d28
DV
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 };
46fd9089 666};
20b87d28 667
79253bd0 668/**
01a14b85
DV
669 * Draws the shaded regions when "fillGraph" is set. Not to be confused with
670 * error bars.
671 *
38e3d209
DV
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 *
01a14b85
DV
676 * @private
677 */
38e3d209 678DygraphCanvasRenderer._fillPlotter = function(e) {
3c080cd0
KW
679 // Skip if we're drawing a single series for interactive highlight overlay.
680 if (e.singleSeriesName) return;
681
38e3d209
DV
682 // We'll handle all the series at once, not one-by-one.
683 if (e.seriesIndex !== 0) return;
684
e2d8db3a 685 var g = e.dygraph;
38e3d209 686 var setNames = g.getLabels().slice(1); // remove x-axis
e2d8db3a 687
38e3d209
DV
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
e2d8db3a
DV
695 var anySeriesFilled = (function() {
696 for (var i = 0; i < setNames.length; i++) {
0e85a437 697 if (g.getBooleanOption("fillGraph", setNames[i])) return true;
e2d8db3a
DV
698 }
699 return false;
700 })();
701
702 if (!anySeriesFilled) return;
703
e2d8db3a
DV
704 var area = e.plotArea;
705 var sets = e.allSeriesPoints;
706 var setCount = sets.length;
707
0e85a437 708 var stackedGraph = g.getBooleanOption("stackedGraph");
38e3d209 709 var colors = g.getColors();
01a14b85 710
30a5cfc6
KW
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 = {};
01a14b85 719 var currBaseline;
104d87c5 720 var prevStepPlot; // for different line drawing modes (line/step) per series
01a14b85 721
46fd9089
DV
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
01a14b85 733 // process sets in reverse order (needed for stacked graphs)
9e85a8f4 734 for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) {
20b87d28 735 var ctx = e.drawingContext;
9e85a8f4 736 var setName = setNames[setIdx];
0e85a437 737 if (!g.getBooleanOption('fillGraph', setName)) continue;
20b87d28 738
1b464274 739 var fillAlpha = g.getNumericOption('fillAlpha', setName);
0e85a437 740 var stepPlot = g.getBooleanOption('stepPlot', setName);
38e3d209
DV
741 var color = colors[setIdx];
742 var axis = g.axisPropertiesForSeries(setName);
01a14b85
DV
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;
38e3d209 746 axisY = area.h * axisY + area.y;
01a14b85 747
38e3d209 748 var points = sets[setIdx];
6ecc0739 749 var iter = utils.createIterator(points, 0, points.length,
01a14b85 750 DygraphCanvasRenderer._getIteratorPredicate(
0e85a437 751 g.getBooleanOption("connectSeparatedPoints", setName)));
01a14b85
DV
752
753 // setup graphics context
754 var prevX = NaN;
755 var prevYs = [-1, -1];
756 var newYs;
01a14b85 757 // should be same color as the lines but only 15% opaque.
6ecc0739 758 var rgb = utils.toRGB_(color);
01a14b85
DV
759 var err_color =
760 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')';
761 ctx.fillStyle = err_color;
762 ctx.beginPath();
12b879f4 763 var last_x, is_first = true;
20b87d28
DV
764
765 // If the point density is high enough, dropping segments on their way to
766 // the canvas justifies the overhead of doing so.
c0ec1a37 767 if (points.length > 2 * g.width_ || Dygraph.FORCE_FAST_PROXY) {
20b87d28
DV
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
20b87d28
DV
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.
46fd9089 780 var point;
12b879f4 781 while (iter.hasNext) {
46fd9089 782 point = iter.next();
6ecc0739 783 if (!utils.isOK(point.y) && !stepPlot) {
46fd9089
DV
784 traceBackPath(ctx, prevX, prevYs[1], pathBack);
785 pathBack = [];
16febe6b 786 prevX = NaN;
30a5cfc6
KW
787 if (point.y_stacked !== null && !isNaN(point.y_stacked)) {
788 baseline[point.canvasx] = area.h * point.y_stacked + area.y;
789 }
16febe6b
DV
790 continue;
791 }
792 if (stackedGraph) {
12b879f4
DV
793 if (!is_first && last_x == point.xval) {
794 continue;
795 } else {
796 is_first = false;
797 last_x = point.xval;
798 }
799
16febe6b
DV
800 currBaseline = baseline[point.canvasx];
801 var lastY;
802 if (currBaseline === undefined) {
803 lastY = axisY;
804 } else {
104d87c5 805 if(prevStepPlot) {
16febe6b 806 lastY = currBaseline[0];
01a14b85 807 } else {
16febe6b 808 lastY = currBaseline;
01a14b85 809 }
16febe6b
DV
810 }
811 newYs = [ point.canvasy, lastY ];
01a14b85 812
20b87d28 813 if (stepPlot) {
16febe6b
DV
814 // Step plots must keep track of the top and bottom of
815 // the baseline at each point.
20b87d28 816 if (prevYs[0] === -1) {
16febe6b 817 baseline[point.canvasx] = [ point.canvasy, axisY ];
01a14b85 818 } else {
16febe6b 819 baseline[point.canvasx] = [ point.canvasy, prevYs[0] ];
01a14b85 820 }
01a14b85 821 } else {
16febe6b 822 baseline[point.canvasx] = point.canvasy;
01a14b85 823 }
01a14b85 824
16febe6b 825 } else {
8c31c7db 826 if (isNaN(point.canvasy) && stepPlot) {
e988d192 827 newYs = [ area.y + area.h, axisY ];
8c31c7db 828 } else {
e988d192
BB
829 newYs = [ point.canvasy, axisY ];
830 }
16febe6b
DV
831 }
832 if (!isNaN(prevX)) {
104d87c5 833 // Move to top fill point
16febe6b
DV
834 if (stepPlot) {
835 ctx.lineTo(point.canvasx, prevYs[0]);
16febe6b 836 ctx.lineTo(point.canvasx, newYs[0]);
104d87c5 837 } else {
20b87d28 838 ctx.lineTo(point.canvasx, newYs[0]);
01a14b85 839 }
16febe6b 840
20b87d28
DV
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]);
01a14b85 854 }
16febe6b
DV
855 prevYs = newYs;
856 prevX = point.canvasx;
01a14b85 857 }
104d87c5 858 prevStepPlot = stepPlot;
46fd9089
DV
859 if (newYs && point) {
860 traceBackPath(ctx, point.canvasx, newYs[1], pathBack);
861 pathBack = [];
20b87d28 862 }
01a14b85
DV
863 ctx.fill();
864 }
865};
3ce712e6 866
6ecc0739 867export default DygraphCanvasRenderer;