3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
8 * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
11 * In particular, support for:
14 * - dygraphs attribute system
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.
27 /*jshint globalstrict: true */
28 /*global Dygraph:false,RGBColor:false */
35 * This gets called when there are "new points" to chart. This is generally the
36 * case when the underlying data being charted has changed. It is _not_ called
37 * in the common case that the user has zoomed or is panning the view.
39 * The chart canvas has already been created by the Dygraph object. The
40 * renderer simply gets a drawing context.
42 * @param {Dyraph} dygraph The chart to which this renderer belongs.
43 * @param {Canvas} element The <canvas> DOM element on which to draw.
44 * @param {CanvasRenderingContext2D} elementContext The drawing context.
45 * @param {DygraphLayout} layout The chart's DygraphLayout object.
47 * TODO(danvk): remove the elementContext property.
49 var DygraphCanvasRenderer
= function(dygraph
, element
, elementContext
, layout
) {
50 this.dygraph_
= dygraph
;
53 this.element
= element
;
54 this.elementContext
= elementContext
;
55 this.container
= this.element
.parentNode
;
57 this.height
= this.element
.height
;
58 this.width
= this.element
.width
;
60 // --- check whether everything is ok before we return
61 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
62 throw "Canvas is not supported.";
65 this.area
= layout
.getPlotArea();
66 this.container
.style
.position
= "relative";
67 this.container
.style
.width
= this.width
+ "px";
69 // Set up a clipping area for the canvas (and the interaction canvas).
70 // This ensures that we don't overdraw.
71 if (this.dygraph_
.isUsingExcanvas_
) {
72 this._createIEClipArea();
74 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
75 // displaying anything.
76 if (!Dygraph
.isAndroid()) {
77 var ctx
= this.dygraph_
.canvas_ctx_
;
79 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
82 ctx
= this.dygraph_
.hidden_ctx_
;
84 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
90 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
91 return this.dygraph_
.attr_(x
);
95 * Clears out all chart content and DOM elements.
96 * This is called immediately before render() on every frame, including
97 * during zooms and pans.
100 DygraphCanvasRenderer
.prototype.clear
= function() {
103 // VML takes a while to start up, so we just poll every this.IEDelay
105 if (this.clearDelay
) {
106 this.clearDelay
.cancel();
107 this.clearDelay
= null;
109 context
= this.elementContext
;
112 // TODO(danvk): this is broken, since MochiKit.Async is gone.
113 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
114 // this.clearDelay.addCallback(bind(this.clear, this));
119 context
= this.elementContext
;
120 context
.clearRect(0, 0, this.width
, this.height
);
124 * Checks whether the browser supports the <canvas> tag.
127 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
130 if (typeof(canvasName
) == 'undefined' || canvasName
=== null) {
131 canvas
= document
.createElement("canvas");
135 canvas
.getContext("2d");
138 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
139 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
140 if ((!ie
) || (ie
[1] < 6) || (opera
))
148 * This method is responsible for drawing everything on the chart, including
149 * lines, error bars, fills and axes.
150 * It is called immediately after clear() on every frame, including during pans
154 DygraphCanvasRenderer
.prototype.render
= function() {
155 this._renderLineChart();
158 DygraphCanvasRenderer
.prototype._createIEClipArea
= function() {
159 var className
= 'dygraph-clip-div';
160 var graphDiv
= this.dygraph_
.graphDiv
;
162 // Remove old clip divs.
163 for (var i
= graphDiv
.childNodes
.length
-1; i
>= 0; i
--) {
164 if (graphDiv
.childNodes
[i
].className
== className
) {
165 graphDiv
.removeChild(graphDiv
.childNodes
[i
]);
169 // Determine background color to give clip divs.
170 var backgroundColor
= document
.bgColor
;
171 var element
= this.dygraph_
.graphDiv
;
172 while (element
!= document
) {
173 var bgcolor
= element
.currentStyle
.backgroundColor
;
174 if (bgcolor
&& bgcolor
!= 'transparent') {
175 backgroundColor
= bgcolor
;
178 element
= element
.parentNode
;
181 function createClipDiv(area
) {
182 if (area
.w
=== 0 || area
.h
=== 0) {
185 var elem
= document
.createElement('div');
186 elem
.className
= className
;
187 elem
.style
.backgroundColor
= backgroundColor
;
188 elem
.style
.position
= 'absolute';
189 elem
.style
.left
= area
.x
+ 'px';
190 elem
.style
.top
= area
.y
+ 'px';
191 elem
.style
.width
= area
.w
+ 'px';
192 elem
.style
.height
= area
.h
+ 'px';
193 graphDiv
.appendChild(elem
);
196 var plotArea
= this.area
;
207 w
: this.width
- plotArea
.x
,
213 x
: plotArea
.x
+ plotArea
.w
, y
: 0,
214 w
: this.width
-plotArea
.x
- plotArea
.w
,
221 y
: plotArea
.y
+ plotArea
.h
,
222 w
: this.width
- plotArea
.x
,
223 h
: this.height
- plotArea
.h
- plotArea
.y
229 * Returns a predicate to be used with an iterator, which will
230 * iterate over points appropriately, depending on whether
231 * connectSeparatedPoints is true. When it's false, the predicate will
232 * skip over points with missing yVals.
234 DygraphCanvasRenderer
._getIteratorPredicate
= function(connectSeparatedPoints
) {
235 return connectSeparatedPoints
236 ? DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
240 DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
=
241 function(array
, idx
) {
242 return array
[idx
].yval
!== null;
248 DygraphCanvasRenderer
.prototype._drawStyledLine
= function(
249 ctx
, i
, setName
, color
, strokeWidth
, strokePattern
, drawPoints
,
250 drawPointCallback
, pointSize
) {
251 // TODO(konigsberg): Compute attributes outside this method call.
252 var stepPlot
= this.attr_("stepPlot");
253 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
254 var setLength
= this.layout
.setPointsLengths
[i
];
255 var points
= this.layout
.points
;
256 if (!Dygraph
.isArrayLike(strokePattern
)) {
257 strokePattern
= null;
259 var drawGapPoints
= this.dygraph_
.attr_('drawGapEdgePoints', setName
);
261 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
262 DygraphCanvasRenderer
._getIteratorPredicate(
263 this.attr_("connectSeparatedPoints")));
265 var stroking
= strokePattern
&& (strokePattern
.length
>= 2);
269 ctx
.installPattern(strokePattern
);
272 var pointsOnLine
= this._drawSeries(ctx
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
, color
);
273 this._drawPointsOnLine(ctx
, pointsOnLine
, drawPointCallback
, setName
, color
, pointSize
);
276 ctx
.uninstallPattern();
282 DygraphCanvasRenderer
.prototype._drawPointsOnLine
= function(ctx
, pointsOnLine
, drawPointCallback
, setName
, color
, pointSize
) {
283 for (var idx
= 0; idx
< pointsOnLine
.length
; idx
++) {
284 var cb
= pointsOnLine
[idx
];
287 this.dygraph_
, setName
, ctx
, cb
[0], cb
[1], color
, pointSize
);
292 DygraphCanvasRenderer
.prototype._drawSeries
= function(
293 ctx
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
,
296 var prevCanvasX
= null;
297 var prevCanvasY
= null;
298 var nextCanvasY
= null;
299 var isIsolated
; // true if this point is isolated (no line segments)
300 var point
; // the point being processed in the while loop
301 var pointsOnLine
= []; // Array of [canvasx, canvasy] pairs.
302 var first
= true; // the first cycle through the while loop
305 ctx
.strokeStyle
= color
;
306 ctx
.lineWidth
= strokeWidth
;
308 // NOTE: we break the iterator's encapsulation here for about a 25% speedup.
309 var arr
= iter
.array_
;
310 var limit
= iter
.end_
;
311 var predicate
= iter
.predicate_
;
313 for (var i
= iter
.start_
; i
< limit
; i
++) {
316 while (i
< limit
&& !predicate(arr
, i
)) {
319 if (i
== limit
) break;
323 if (point
.canvasy
=== null || point
.canvasy
!= point
.canvasy
) {
324 if (stepPlot
&& prevCanvasX
!== null) {
325 // Draw a horizontal line to the start of the missing data
326 ctx
.moveTo(prevX
, prevY
);
327 ctx
.lineTo(point
.canvasx
, prevY
);
329 prevCanvasX
= prevCanvasY
= null;
332 if (drawGapPoints
|| !prevCanvasX
) {
334 var peek
= iter
.next();
335 nextCanvasY
= iter
.hasNext
? iter
.peek
.canvasy
: null;
337 var isNextCanvasYNullOrNaN
= nextCanvasY
=== null ||
338 nextCanvasY
!= nextCanvasY
;
339 isIsolated
= (!prevCanvasX
&& isNextCanvasYNullOrNaN
);
341 // Also consider a point to be "isolated" if it's adjacent to a
342 // null point, excluding the graph edges.
343 if ((!first
&& !prevCanvasX
) ||
344 (iter
.hasNext
&& isNextCanvasYNullOrNaN
)) {
350 if (prevCanvasX
!== null) {
353 ctx
.moveTo(prevCanvasX
, prevCanvasY
);
354 ctx
.lineTo(point
.canvasx
, prevCanvasY
);
355 prevCanvasX
= point
.canvasx
;
358 // TODO(danvk): this moveTo is rarely necessary
359 ctx
.moveTo(prevCanvasX
, prevCanvasY
);
360 ctx
.lineTo(point
.canvasx
, point
.canvasy
);
363 if (drawPoints
|| isIsolated
) {
364 pointsOnLine
.push([point
.canvasx
, point
.canvasy
]);
366 prevCanvasX
= point
.canvasx
;
367 prevCanvasY
= point
.canvasy
;
375 DygraphCanvasRenderer
.prototype._drawLine
= function(ctx
, i
) {
376 var setNames
= this.layout
.setNames
;
377 var setName
= setNames
[i
];
379 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
380 var borderWidth
= this.dygraph_
.attr_("strokeBorderWidth", setName
);
381 var drawPointCallback
= this.dygraph_
.attr_("drawPointCallback", setName
) ||
382 Dygraph
.Circles
.DEFAULT
;
384 if (borderWidth
&& strokeWidth
) {
385 this._drawStyledLine(ctx
, i
, setName
,
386 this.dygraph_
.attr_("strokeBorderColor", setName
),
387 strokeWidth
+ 2 * borderWidth
,
388 this.dygraph_
.attr_("strokePattern", setName
),
389 this.dygraph_
.attr_("drawPoints", setName
),
391 this.dygraph_
.attr_("pointSize", setName
));
394 this._drawStyledLine(ctx
, i
, setName
,
395 this.colors
[setName
],
397 this.dygraph_
.attr_("strokePattern", setName
),
398 this.dygraph_
.attr_("drawPoints", setName
),
400 this.dygraph_
.attr_("pointSize", setName
));
404 * Actually draw the lines chart, including error bars.
407 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
408 var ctx
= this.elementContext
;
409 var errorBars
= this.attr_("errorBars") || this.attr_("customBars");
410 var fillGraph
= this.attr_("fillGraph");
413 var setNames
= this.layout
.setNames
;
414 var setCount
= setNames
.length
;
416 this.colors
= this.dygraph_
.colorsMap_
;
421 // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
422 // transformations can be pushed into the canvas via linear transformation
424 // NOTE(danvk): this is trickier than it sounds at first. The transformation
425 // needs to be done before the .moveTo() and .lineTo() calls, but must be
426 // undone before the .stroke() call to ensure that the stroke width is
427 // unaffected. An alternative is to reduce the stroke width in the
428 // transformed coordinate space, but you can't specify different values for
429 // each dimension (as you can with .scale()). The speedup here is ~12%.
430 var points
= this.layout
.points
;
431 for (i
= points
.length
; i
--;) {
432 var point
= points
[i
];
433 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
434 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
437 // Draw any "fills", i.e. error bars or the filled area under a series.
438 // These must all be drawn before any lines, so that the main lines of a
439 // series are drawn on top.
442 this.dygraph_
.warn("Can't use fillGraph option with error bars");
446 this.drawErrorBars_(points
);
448 } else if (fillGraph
) {
450 this.drawFillBars_(points
);
454 // Drawing the lines.
455 for (i
= 0; i
< setCount
; i
+= 1) {
456 this._drawLine(ctx
, i
);
461 * Draws the shaded error bars/confidence intervals for each series.
462 * This happens before the center lines are drawn, since the center lines
463 * need to be drawn on top of the error bars for all series.
467 DygraphCanvasRenderer
.prototype.drawErrorBars_
= function(points
) {
468 var ctx
= this.elementContext
;
469 var setNames
= this.layout
.setNames
;
470 var setCount
= setNames
.length
;
471 var fillAlpha
= this.attr_('fillAlpha');
472 var stepPlot
= this.attr_('stepPlot');
476 for (var i
= 0; i
< setCount
; i
++) {
477 var setName
= setNames
[i
];
478 var axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
479 var color
= this.colors
[setName
];
481 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
482 var setLength
= this.layout
.setPointsLengths
[i
];
484 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
485 DygraphCanvasRenderer
._getIteratorPredicate(
486 this.attr_("connectSeparatedPoints")));
488 // setup graphics context
491 var prevYs
= [-1, -1];
492 var yscale
= axis
.yscale
;
493 // should be same color as the lines but only 15% opaque.
494 var rgb
= new RGBColor(color
);
496 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' + fillAlpha
+ ')';
497 ctx
.fillStyle
= err_color
;
499 while (iter
.hasNext
) {
500 var point
= iter
.next();
501 if (!Dygraph
.isOK(point
.y
)) {
507 newYs
= [ point
.y_bottom
, point
.y_top
];
510 newYs
= [ point
.y_bottom
, point
.y_top
];
512 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
513 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
516 ctx
.moveTo(prevX
, newYs
[0]);
518 ctx
.moveTo(prevX
, prevYs
[0]);
520 ctx
.lineTo(point
.canvasx
, newYs
[0]);
521 ctx
.lineTo(point
.canvasx
, newYs
[1]);
523 ctx
.lineTo(prevX
, newYs
[1]);
525 ctx
.lineTo(prevX
, prevYs
[1]);
530 prevX
= point
.canvasx
;
537 * Draws the shaded regions when "fillGraph" is set. Not to be confused with
542 DygraphCanvasRenderer
.prototype.drawFillBars_
= function(points
) {
543 var ctx
= this.elementContext
;
544 var setNames
= this.layout
.setNames
;
545 var setCount
= setNames
.length
;
546 var fillAlpha
= this.attr_('fillAlpha');
547 var stepPlot
= this.attr_('stepPlot');
548 var stackedGraph
= this.attr_("stackedGraph");
550 var baseline
= {}; // for stacked graphs: baseline for filling
553 // process sets in reverse order (needed for stacked graphs)
554 for (var i
= setCount
- 1; i
>= 0; i
--) {
555 var setName
= setNames
[i
];
556 var color
= this.colors
[setName
];
557 var axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
558 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
559 if (axisY
< 0.0) axisY
= 0.0;
560 else if (axisY
> 1.0) axisY
= 1.0;
561 axisY
= this.area
.h
* axisY
+ this.area
.y
;
562 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
563 var setLength
= this.layout
.setPointsLengths
[i
];
565 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
566 DygraphCanvasRenderer
._getIteratorPredicate(
567 this.attr_("connectSeparatedPoints")));
569 // setup graphics context
571 var prevYs
= [-1, -1];
573 var yscale
= axis
.yscale
;
574 // should be same color as the lines but only 15% opaque.
575 var rgb
= new RGBColor(color
);
577 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' + fillAlpha
+ ')';
578 ctx
.fillStyle
= err_color
;
580 while(iter
.hasNext
) {
581 var point
= iter
.next();
582 if (!Dygraph
.isOK(point
.y
)) {
587 currBaseline
= baseline
[point
.canvasx
];
589 if (currBaseline
=== undefined
) {
593 lastY
= currBaseline
[0];
595 lastY
= currBaseline
;
598 newYs
= [ point
.canvasy
, lastY
];
601 // Step plots must keep track of the top and bottom of
602 // the baseline at each point.
603 if(prevYs
[0] === -1) {
604 baseline
[point
.canvasx
] = [ point
.canvasy
, axisY
];
606 baseline
[point
.canvasx
] = [ point
.canvasy
, prevYs
[0] ];
609 baseline
[point
.canvasx
] = point
.canvasy
;
613 newYs
= [ point
.canvasy
, axisY
];
616 ctx
.moveTo(prevX
, prevYs
[0]);
619 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
621 // Draw to the bottom of the baseline
622 ctx
.lineTo(point
.canvasx
, currBaseline
[1]);
624 ctx
.lineTo(point
.canvasx
, newYs
[1]);
627 ctx
.lineTo(point
.canvasx
, newYs
[0]);
628 ctx
.lineTo(point
.canvasx
, newYs
[1]);
631 ctx
.lineTo(prevX
, prevYs
[1]);
635 prevX
= point
.canvasx
;