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