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 30 import * as utils from './dygraph-utils'; 31 import 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 */ 50 var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) { 51 this.dygraph_ = dygraph; 52 53 this.layout = layout; 54 this.element = element; 55 this.elementContext = elementContext; 56 57 this.height = dygraph.height_; 58 this.width = dygraph.width_; 59 60 // --- check whether everything is ok before we return 61 if (!utils.isCanvasSupported(this.element)) { 62 throw "Canvas is not supported."; 63 } 64 65 // internal state 66 this.area = layout.getPlotArea(); 67 68 // Set up a clipping area for the canvas (and the interaction canvas). 69 // This ensures that we don't overdraw. 70 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 */ 87 DygraphCanvasRenderer.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 */ 98 DygraphCanvasRenderer.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 */ 112 DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) { 113 return connectSeparatedPoints ? 114 DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : 115 null; 116 }; 117 118 DygraphCanvasRenderer._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 */ 128 DygraphCanvasRenderer._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 */ 175 DygraphCanvasRenderer._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 */ 267 DygraphCanvasRenderer._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 */ 283 DygraphCanvasRenderer.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 */ 321 DygraphCanvasRenderer.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 */ 397 DygraphCanvasRenderer._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 */ 415 DygraphCanvasRenderer._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 */ 457 DygraphCanvasRenderer._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 */ 544 DygraphCanvasRenderer._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 */ 676 DygraphCanvasRenderer._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 865 export default DygraphCanvasRenderer; 866