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.";
67 this.annotations
= [];
69 this.area
= layout
.getPlotArea();
70 this.container
.style
.position
= "relative";
71 this.container
.style
.width
= this.width
+ "px";
73 // Set up a clipping area for the canvas (and the interaction canvas).
74 // This ensures that we don't overdraw.
75 if (this.dygraph_
.isUsingExcanvas_
) {
76 this._createIEClipArea();
78 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
79 // displaying anything.
80 if (!Dygraph
.isAndroid()) {
81 var ctx
= this.dygraph_
.canvas_ctx_
;
83 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
86 ctx
= this.dygraph_
.hidden_ctx_
;
88 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
94 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
95 return this.dygraph_
.attr_(x
);
99 * Clears out all chart content and DOM elements.
100 * This is called immediately before render() on every frame, including
101 * during zooms and pans.
104 DygraphCanvasRenderer
.prototype.clear
= function() {
107 // VML takes a while to start up, so we just poll every this.IEDelay
109 if (this.clearDelay
) {
110 this.clearDelay
.cancel();
111 this.clearDelay
= null;
113 context
= this.elementContext
;
116 // TODO(danvk): this is broken, since MochiKit.Async is gone.
117 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
118 // this.clearDelay.addCallback(bind(this.clear, this));
123 context
= this.elementContext
;
124 context
.clearRect(0, 0, this.width
, this.height
);
126 function removeArray(ary
) {
127 for (var i
= 0; i
< ary
.length
; i
++) {
129 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
133 removeArray(this.xlabels
);
134 removeArray(this.ylabels
);
135 removeArray(this.annotations
);
139 this.annotations
= [];
143 * Checks whether the browser supports the <canvas> tag.
146 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
149 if (typeof(canvasName
) == 'undefined' || canvasName
=== null) {
150 canvas
= document
.createElement("canvas");
154 canvas
.getContext("2d");
157 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
158 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
159 if ((!ie
) || (ie
[1] < 6) || (opera
))
167 * @param { [String] } colors Array of color strings. Should have one entry for
168 * each series to be rendered.
170 DygraphCanvasRenderer
.prototype.setColors
= function(colors
) {
171 this.colorScheme_
= colors
;
175 * This method is responsible for drawing everything on the chart, including
176 * lines, error bars, fills and axes.
177 * It is called immediately after clear() on every frame, including during pans
181 DygraphCanvasRenderer
.prototype.render
= function() {
182 // Draw the new X/Y grid
. Lines appear crisper when pixels are rounded to
183 // half-integers. This prevents them from drawing in two rows/cols.
184 var ctx
= this.elementContext
;
185 function halfUp(x
) { return Math
.round(x
) + 0.5; }
186 function halfDown(y
){ return Math
.round(y
) - 0.5; }
188 if (this.attr_('underlayCallback')) {
189 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
190 // users who expect a deprecated form of this callback.
191 this.attr_('underlayCallback')(ctx
, this.area
, this.dygraph_
, this.dygraph_
);
195 if (this.attr_('drawYGrid')) {
196 ticks
= this.layout
.yticks
;
197 // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
199 ctx
.strokeStyle
= this.attr_('gridLineColor');
200 ctx
.lineWidth
= this.attr_('gridLineWidth');
201 for (i
= 0; i
< ticks
.length
; i
++) {
202 // TODO(danvk): allow secondary axes to draw a grid, too.
203 if (ticks
[i
][0] !== 0) continue;
204 x
= halfUp(this.area
.x
);
205 y
= halfDown(this.area
.y
+ ticks
[i
][1] * this.area
.h
);
208 ctx
.lineTo(x
+ this.area
.w
, y
);
215 if (this.attr_('drawXGrid')) {
216 ticks
= this.layout
.xticks
;
218 ctx
.strokeStyle
= this.attr_('gridLineColor');
219 ctx
.lineWidth
= this.attr_('gridLineWidth');
220 for (i
=0; i
<ticks
.length
; i
++) {
221 x
= halfUp(this.area
.x
+ ticks
[i
][0] * this.area
.w
);
222 y
= halfDown(this.area
.y
+ this.area
.h
);
225 ctx
.lineTo(x
, this.area
.y
);
232 // Do the ordinary rendering, as before
233 this._renderLineChart();
235 // this._renderAnnotations();
238 DygraphCanvasRenderer
.prototype._createIEClipArea
= function() {
239 var className
= 'dygraph-clip-div';
240 var graphDiv
= this.dygraph_
.graphDiv
;
242 // Remove old clip divs.
243 for (var i
= graphDiv
.childNodes
.length
-1; i
>= 0; i
--) {
244 if (graphDiv
.childNodes
[i
].className
== className
) {
245 graphDiv
.removeChild(graphDiv
.childNodes
[i
]);
249 // Determine background color to give clip divs.
250 var backgroundColor
= document
.bgColor
;
251 var element
= this.dygraph_
.graphDiv
;
252 while (element
!= document
) {
253 var bgcolor
= element
.currentStyle
.backgroundColor
;
254 if (bgcolor
&& bgcolor
!= 'transparent') {
255 backgroundColor
= bgcolor
;
258 element
= element
.parentNode
;
261 function createClipDiv(area
) {
262 if (area
.w
=== 0 || area
.h
=== 0) {
265 var elem
= document
.createElement('div');
266 elem
.className
= className
;
267 elem
.style
.backgroundColor
= backgroundColor
;
268 elem
.style
.position
= 'absolute';
269 elem
.style
.left
= area
.x
+ 'px';
270 elem
.style
.top
= area
.y
+ 'px';
271 elem
.style
.width
= area
.w
+ 'px';
272 elem
.style
.height
= area
.h
+ 'px';
273 graphDiv
.appendChild(elem
);
276 var plotArea
= this.area
;
287 w
: this.width
- plotArea
.x
,
293 x
: plotArea
.x
+ plotArea
.w
, y
: 0,
294 w
: this.width
-plotArea
.x
- plotArea
.w
,
301 y
: plotArea
.y
+ plotArea
.h
,
302 w
: this.width
- plotArea
.x
,
303 h
: this.height
- plotArea
.h
- plotArea
.y
307 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
308 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
310 // Round pixels to half-integer boundaries for crisper drawing.
311 function halfUp(x
) { return Math
.round(x
) + 0.5; }
312 function halfDown(y
){ return Math
.round(y
) - 0.5; }
314 var context
= this.elementContext
;
316 var label
, x
, y
, tick
, i
;
319 position
: "absolute",
320 fontSize
: this.attr_('axisLabelFontSize') + "px",
322 color
: this.attr_('axisLabelColor'),
323 width
: this.attr_('axisLabelWidth') + "px",
324 // height: this.attr_('axisLabelFontSize') + 2 + "px",
325 lineHeight
: "normal", // Something other than "normal" line-height screws up label positioning.
328 var makeDiv
= function(txt
, axis
, prec_axis
) {
329 var div
= document
.createElement("div");
330 for (var name
in labelStyle
) {
331 if (labelStyle
.hasOwnProperty(name
)) {
332 div
.style
[name
] = labelStyle
[name
];
335 var inner_div
= document
.createElement("div");
336 inner_div
.className
= 'dygraph-axis-label' +
337 ' dygraph-axis-label-' + axis
+
338 (prec_axis
? ' dygraph-axis-label-' + prec_axis
: '');
339 inner_div
.innerHTML
=txt
;
340 div
.appendChild(inner_div
);
346 context
.strokeStyle
= this.attr_('axisLineColor');
347 context
.lineWidth
= this.attr_('axisLineWidth');
349 if (this.attr_('drawYAxis')) {
350 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
351 var num_axes
= this.dygraph_
.numAxes();
352 for (i
= 0; i
< this.layout
.yticks
.length
; i
++) {
353 tick
= this.layout
.yticks
[i
];
354 if (typeof(tick
) == "function") return;
357 var prec_axis
= 'y1';
358 if (tick
[0] == 1) { // right-side y-axis
359 x
= this.area
.x
+ this.area
.w
;
363 y
= this.area
.y
+ tick
[1] * this.area
.h
;
365 /* Tick marks are currently clipped, so don't bother drawing them.
367 context.moveTo(halfUp(x), halfDown(y));
368 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
373 label
= makeDiv(tick
[2], 'y', num_axes
== 2 ? prec_axis
: null);
374 var top
= (y
- this.attr_('axisLabelFontSize') / 2);
375 if (top
< 0) top
= 0;
377 if (top
+ this.attr_('axisLabelFontSize') + 3 > this.height
) {
378 label
.style
.bottom
= "0px";
380 label
.style
.top
= top
+ "px";
383 label
.style
.left
= (this.area
.x
- this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
384 label
.style
.textAlign
= "right";
385 } else if (tick
[0] == 1) {
386 label
.style
.left
= (this.area
.x
+ this.area
.w
+
387 this.attr_('axisTickSize')) + "px";
388 label
.style
.textAlign
= "left";
390 label
.style
.width
= this.attr_('yAxisLabelWidth') + "px";
391 this.container
.appendChild(label
);
392 this.ylabels
.push(label
);
395 // The lowest tick on the y-axis often overlaps with the leftmost
396 // tick on the x-axis. Shift the bottom tick up a little bit to
397 // compensate if necessary.
398 var bottomTick
= this.ylabels
[0];
399 var fontSize
= this.attr_('axisLabelFontSize');
400 var bottom
= parseInt(bottomTick
.style
.top
, 10) + fontSize
;
401 if (bottom
> this.height
- fontSize
) {
402 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
, 10) -
403 fontSize
/ 2) + "px";
407 // draw a vertical line on the left to separate the chart from the labels.
409 if (this.attr_('drawAxesAtZero')) {
410 var r
= this.dygraph_
.toPercentXCoord(0);
411 if (r
> 1 || r
< 0) r
= 0;
412 axisX
= halfUp(this.area
.x
+ r
* this.area
.w
);
414 axisX
= halfUp(this.area
.x
);
417 context
.moveTo(axisX
, halfDown(this.area
.y
));
418 context
.lineTo(axisX
, halfDown(this.area
.y
+ this.area
.h
));
422 // if there's a secondary y-axis, draw a vertical line for that, too.
423 if (this.dygraph_
.numAxes() == 2) {
425 context
.moveTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
));
426 context
.lineTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
432 if (this.attr_('drawXAxis')) {
433 if (this.layout
.xticks
) {
434 for (i
= 0; i
< this.layout
.xticks
.length
; i
++) {
435 tick
= this.layout
.xticks
[i
];
436 x
= this.area
.x
+ tick
[0] * this.area
.w
;
437 y
= this.area
.y
+ this.area
.h
;
439 /* Tick marks are currently clipped, so don't bother drawing them.
441 context.moveTo(halfUp(x), halfDown(y));
442 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
447 label
= makeDiv(tick
[1], 'x');
448 label
.style
.textAlign
= "center";
449 label
.style
.top
= (y
+ this.attr_('axisTickSize')) + 'px';
451 var left
= (x
- this.attr_('axisLabelWidth')/2);
452 if (left
+ this.attr_('axisLabelWidth') > this.width
) {
453 left
= this.width
- this.attr_('xAxisLabelWidth');
454 label
.style
.textAlign
= "right";
458 label
.style
.textAlign
= "left";
461 label
.style
.left
= left
+ "px";
462 label
.style
.width
= this.attr_('xAxisLabelWidth') + "px";
463 this.container
.appendChild(label
);
464 this.xlabels
.push(label
);
470 if (this.attr_('drawAxesAtZero')) {
471 var r
= this.dygraph_
.toPercentYCoord(0, 0);
472 if (r
> 1 || r
< 0) r
= 1;
473 axisY
= halfDown(this.area
.y
+ r
* this.area
.h
);
475 axisY
= halfDown(this.area
.y
+ this.area
.h
);
477 context
.moveTo(halfUp(this.area
.x
), axisY
);
478 context
.lineTo(halfUp(this.area
.x
+ this.area
.w
), axisY
);
487 DygraphCanvasRenderer
.prototype._renderAnnotations
= function() {
488 var annotationStyle
= {
489 "position": "absolute",
490 "fontSize": this.attr_('axisLabelFontSize') + "px",
495 var bindEvt
= function(eventName
, classEventName
, p
, self
) {
497 var a
= p
.annotation
;
498 if (a
.hasOwnProperty(eventName
)) {
499 a
[eventName
](a
, p
, self
.dygraph_
, e
);
500 } else if (self
.dygraph_
.attr_(classEventName
)) {
501 self
.dygraph_
.attr_(classEventName
)(a
, p
, self
.dygraph_
,e
);
506 // Get a list of point with annotations.
507 var points
= this.layout
.annotated_points
;
508 for (var i
= 0; i
< points
.length
; i
++) {
510 if (p
.canvasx
< this.area
.x
|| p
.canvasx
> this.area
.x
+ this.area
.w
||
511 p
.canvasy
< this.area
.y
|| p
.canvasy
> this.area
.y
+ this.area
.h
) {
515 var a
= p
.annotation
;
517 if (a
.hasOwnProperty("tickHeight")) {
518 tick_height
= a
.tickHeight
;
521 var div
= document
.createElement("div");
522 for (var name
in annotationStyle
) {
523 if (annotationStyle
.hasOwnProperty(name
)) {
524 div
.style
[name
] = annotationStyle
[name
];
527 if (!a
.hasOwnProperty('icon')) {
528 div
.className
= "dygraphDefaultAnnotation";
530 if (a
.hasOwnProperty('cssClass')) {
531 div
.className
+= " " + a
.cssClass
;
534 var width
= a
.hasOwnProperty('width') ? a
.width
: 16;
535 var height
= a
.hasOwnProperty('height') ? a
.height
: 16;
536 if (a
.hasOwnProperty('icon')) {
537 var img
= document
.createElement("img");
541 div
.appendChild(img
);
542 } else if (p
.annotation
.hasOwnProperty('shortText')) {
543 div
.appendChild(document
.createTextNode(p
.annotation
.shortText
));
545 div
.style
.left
= (p
.canvasx
- width
/ 2) + "px";
546 if (a
.attachAtBottom
) {
547 div
.style
.top
= (this.area
.h
- height
- tick_height
) + "px";
549 div
.style
.top
= (p
.canvasy
- height
- tick_height
) + "px";
551 div
.style
.width
= width
+ "px";
552 div
.style
.height
= height
+ "px";
553 div
.title
= p
.annotation
.text
;
554 div
.style
.color
= this.colors
[p
.name
];
555 div
.style
.borderColor
= this.colors
[p
.name
];
558 this.dygraph_
.addEvent(div
, 'click',
559 bindEvt('clickHandler', 'annotationClickHandler', p
, this));
560 this.dygraph_
.addEvent(div
, 'mouseover',
561 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p
, this));
562 this.dygraph_
.addEvent(div
, 'mouseout',
563 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p
, this));
564 this.dygraph_
.addEvent(div
, 'dblclick',
565 bindEvt('dblClickHandler', 'annotationDblClickHandler', p
, this));
567 this.container
.appendChild(div
);
568 this.annotations
.push(div
);
570 var ctx
= this.elementContext
;
571 ctx
.strokeStyle
= this.colors
[p
.name
];
573 if (!a
.attachAtBottom
) {
574 ctx
.moveTo(p
.canvasx
, p
.canvasy
);
575 ctx
.lineTo(p
.canvasx
, p
.canvasy
- 2 - tick_height
);
577 ctx
.moveTo(p
.canvasx
, this.area
.h
);
578 ctx
.lineTo(p
.canvasx
, this.area
.h
- 2 - tick_height
);
586 * Returns a predicate to be used with an iterator, which will
587 * iterate over points appropriately, depending on whether
588 * connectSeparatedPoints is true. When it's false, the predicate will
589 * skip over points with missing yVals.
591 DygraphCanvasRenderer
._getIteratorPredicate
= function(connectSeparatedPoints
) {
592 return connectSeparatedPoints
? DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
: null;
595 DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
=
596 function(array
, idx
) { return array
[idx
].yval
!== null; }
598 DygraphCanvasRenderer
.prototype._drawStyledLine
= function(
599 ctx
, i
, setName
, color
, strokeWidth
, strokePattern
, drawPoints
,
600 drawPointCallback
, pointSize
) {
601 // TODO(konigsberg): Compute attributes outside this method call.
602 var stepPlot
= this.attr_("stepPlot");
603 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
604 var setLength
= this.layout
.setPointsLengths
[i
];
605 var points
= this.layout
.points
;
606 if (!Dygraph
.isArrayLike(strokePattern
)) {
607 strokePattern
= null;
609 var drawGapPoints
= this.dygraph_
.attr_('drawGapEdgePoints', setName
);
613 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
614 DygraphCanvasRenderer
._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
618 if (!strokePattern
|| strokePattern
.length
<= 1) {
619 strategy
= trivialStrategy(ctx
, color
, strokeWidth
);
621 strategy
= nonTrivialStrategy(this, ctx
, color
, strokeWidth
, strokePattern
);
623 pointsOnLine
= this._drawSeries(ctx
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
, strategy
);
624 this._drawPointsOnLine(ctx
, pointsOnLine
, drawPointCallback
, setName
, color
, pointSize
);
629 var nonTrivialStrategy
= function(renderer
, ctx
, color
, strokeWidth
, strokePattern
) {
630 return new function() {
631 this.init
= function() { };
632 this.finish
= function() { };
633 this.startSegment
= function() {
635 ctx
.strokeStyle
= color
;
636 ctx
.lineWidth
= strokeWidth
;
638 this.endSegment
= function() {
639 ctx
.stroke(); // should this include closePath?
641 this.drawLine
= function(x1
, y1
, x2
, y2
) {
642 renderer
._dashedLine(ctx
, x1
, y1
, x2
, y2
, strokePattern
);
644 this.skipPixel
= function(prevX
, prevY
, curX
, curY
) {
645 // TODO(konigsberg): optimize with http://jsperf.com/math-round
-vs
-hack
/6 ?
646 return (Math
.round(prevX
) == Math
.round(curX
) &&
647 Math
.round(prevY
) == Math
.round(curY
));
652 var trivialStrategy
= function(ctx
, color
, strokeWidth
) {
653 return new function() {
654 this.init
= function() {
656 ctx
.strokeStyle
= color
;
657 ctx
.lineWidth
= strokeWidth
;
659 this.finish
= function() {
660 ctx
.stroke(); // should this include closePath?
662 this.startSegment
= function() { };
663 this.endSegment
= function() { };
664 this.drawLine
= function(x1
, y1
, x2
, y2
) {
668 // don't skip pixels.
669 this.skipPixel
= function() {
675 DygraphCanvasRenderer
.prototype._drawPointsOnLine
= function(ctx
, pointsOnLine
, drawPointCallback
, setName
, color
, pointSize
) {
676 for (var idx
= 0; idx
< pointsOnLine
.length
; idx
++) {
677 var cb
= pointsOnLine
[idx
];
680 this.dygraph_
, setName
, ctx
, cb
[0], cb
[1], color
, pointSize
);
685 DygraphCanvasRenderer
.prototype._drawSeries
= function(
686 ctx
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
,
687 stepPlot
, strategy
) {
689 var prevCanvasX
= null;
690 var prevCanvasY
= null;
691 var nextCanvasY
= null;
692 var isIsolated
; // true if this point is isolated (no line segments)
693 var point
; // the point being processed in the while loop
694 var pointsOnLine
= []; // Array of [canvasx, canvasy] pairs.
695 var first
= true; // the first cycle through the while loop
699 while(iter
.hasNext()) {
701 if (point
.canvasy
=== null || point
.canvasy
!= point
.canvasy
) {
702 if (stepPlot
&& prevCanvasX
!== null) {
703 // Draw a horizontal line to the start of the missing data
704 strategy
.startSegment();
705 strategy
.drawLine(prevX
, prevY
, point
.canvasx
, prevY
);
706 strategy
.endSegment();
708 prevCanvasX
= prevCanvasY
= null;
710 nextCanvasY
= iter
.hasNext() ? iter
.peek().canvasy
: null;
711 // TODO: we calculate isNullOrNaN for this point, and the next, and then, when
712 // we iterate, test for isNullOrNaN again. Why bother?
713 var isNextCanvasYNullOrNaN
= nextCanvasY
=== null || nextCanvasY
!= nextCanvasY
;
714 isIsolated
= (!prevCanvasX
&& isNextCanvasYNullOrNaN
);
716 // Also consider a point to be "isolated" if it's adjacent to a
717 // null point, excluding the graph edges.
718 if ((!first
&& !prevCanvasX
) ||
719 (iter
.hasNext() && isNextCanvasYNullOrNaN
)) {
723 if (prevCanvasX
!== null) {
724 if (strategy
.skipPixel(prevCanvasX
, prevCanvasY
, point
.canvasx
, point
.canvasy
)) {
728 strategy
.startSegment();
730 strategy
.drawLine(prevCanvasX
, prevCanvasY
, point
.canvasx
, prevCanvasY
);
731 prevCanvasX
= point
.canvasx
;
733 strategy
.drawLine(prevCanvasX
, prevCanvasY
, point
.canvasx
, point
.canvasy
);
734 strategy
.endSegment();
737 if (drawPoints
|| isIsolated
) {
738 pointsOnLine
.push([point
.canvasx
, point
.canvasy
]);
740 prevCanvasX
= point
.canvasx
;
741 prevCanvasY
= point
.canvasy
;
749 DygraphCanvasRenderer
.prototype._drawLine
= function(ctx
, i
) {
750 var setNames
= this.layout
.setNames
;
751 var setName
= setNames
[i
];
753 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
754 var borderWidth
= this.dygraph_
.attr_("strokeBorderWidth", setName
);
755 var drawPointCallback
= this.dygraph_
.attr_("drawPointCallback", setName
) ||
756 Dygraph
.Circles
.DEFAULT
;
758 if (borderWidth
&& strokeWidth
) {
759 this._drawStyledLine(ctx
, i
, setName
,
760 this.dygraph_
.attr_("strokeBorderColor", setName
),
761 strokeWidth
+ 2 * borderWidth
,
762 this.dygraph_
.attr_("strokePattern", setName
),
763 this.dygraph_
.attr_("drawPoints", setName
),
765 this.dygraph_
.attr_("pointSize", setName
));
768 this._drawStyledLine(ctx
, i
, setName
,
769 this.colors
[setName
],
771 this.dygraph_
.attr_("strokePattern", setName
),
772 this.dygraph_
.attr_("drawPoints", setName
),
774 this.dygraph_
.attr_("pointSize", setName
));
778 * Actually draw the lines chart, including error bars.
779 * TODO(danvk): split this into several smaller functions.
782 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
783 // TODO(danvk): use this.attr_ for many of these.
784 var ctx
= this.elementContext
;
785 var fillAlpha
= this.attr_('fillAlpha');
786 var errorBars
= this.attr_("errorBars") || this.attr_("customBars");
787 var fillGraph
= this.attr_("fillGraph");
788 var stackedGraph
= this.attr_("stackedGraph");
789 var stepPlot
= this.attr_("stepPlot");
790 var points
= this.layout
.points
;
791 var pointsLength
= points
.length
;
792 var point
, i
, prevX
, prevY
, prevYs
, color
, setName
, newYs
, err_color
, rgb
, yscale
, axis
;
794 var setNames
= this.layout
.setNames
;
795 var setCount
= setNames
.length
;
797 // TODO(danvk): Move this mapping into Dygraph and get it out of here.
798 this.colors
= this.dygraph_
.colorsMap_
;
799 // for (i = 0; i < setCount; i++) {
800 // this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length];
806 // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
807 // transformations can be pushed into the canvas via linear transformation
809 for (i
= pointsLength
; i
--;) {
811 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
812 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
819 this.dygraph_
.warn("Can't use fillGraph option with error bars");
822 for (i
= 0; i
< setCount
; i
++) {
823 setName
= setNames
[i
];
824 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
825 color
= this.colors
[setName
];
827 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
828 var setLength
= this.layout
.setPointsLengths
[i
];
830 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
831 DygraphCanvasRenderer
._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
833 // setup graphics context
837 yscale
= axis
.yscale
;
838 // should be same color as the lines but only 15% opaque.
839 rgb
= new RGBColor(color
);
840 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
842 ctx
.fillStyle
= err_color
;
844 while (iter
.hasNext()) {
846 if (point
.name
== setName
) { // TODO(klausw): this is always true
847 if (!Dygraph
.isOK(point
.y
)) {
854 newYs
= [ point
.y_bottom
, point
.y_top
];
857 newYs
= [ point
.y_bottom
, point
.y_top
];
859 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
860 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
863 ctx
.moveTo(prevX
, newYs
[0]);
865 ctx
.moveTo(prevX
, prevYs
[0]);
867 ctx
.lineTo(point
.canvasx
, newYs
[0]);
868 ctx
.lineTo(point
.canvasx
, newYs
[1]);
870 ctx
.lineTo(prevX
, newYs
[1]);
872 ctx
.lineTo(prevX
, prevYs
[1]);
877 prevX
= point
.canvasx
;
883 } else if (fillGraph
) {
885 var baseline
= {}; // for stacked graphs: baseline for filling
888 // process sets in reverse order (needed for stacked graphs)
889 for (i
= setCount
- 1; i
>= 0; i
--) {
890 setName
= setNames
[i
];
891 color
= this.colors
[setName
];
892 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
893 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
894 if (axisY
< 0.0) axisY
= 0.0;
895 else if (axisY
> 1.0) axisY
= 1.0;
896 axisY
= this.area
.h
* axisY
+ this.area
.y
;
897 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
898 var setLength
= this.layout
.setPointsLengths
[i
];
900 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
901 DygraphCanvasRenderer
._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
903 // setup graphics context
906 yscale
= axis
.yscale
;
907 // should be same color as the lines but only 15% opaque.
908 rgb
= new RGBColor(color
);
909 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
911 ctx
.fillStyle
= err_color
;
913 while(iter
.hasNext()) {
915 if (point
.name
== setName
) { // TODO(klausw): this is always true
916 if (!Dygraph
.isOK(point
.y
)) {
921 currBaseline
= baseline
[point
.canvasx
];
923 if (currBaseline
=== undefined
) {
927 lastY
= currBaseline
[0];
929 lastY
= currBaseline
;
932 newYs
= [ point
.canvasy
, lastY
];
935 // Step plots must keep track of the top and bottom of
936 // the baseline at each point.
937 if(prevYs
[0] === -1) {
938 baseline
[point
.canvasx
] = [ point
.canvasy
, axisY
];
940 baseline
[point
.canvasx
] = [ point
.canvasy
, prevYs
[0] ];
943 baseline
[point
.canvasx
] = point
.canvasy
;
947 newYs
= [ point
.canvasy
, axisY
];
950 ctx
.moveTo(prevX
, prevYs
[0]);
953 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
955 // Draw to the bottom of the baseline
956 ctx
.lineTo(point
.canvasx
, currBaseline
[1]);
958 ctx
.lineTo(point
.canvasx
, newYs
[1]);
961 ctx
.lineTo(point
.canvasx
, newYs
[0]);
962 ctx
.lineTo(point
.canvasx
, newYs
[1]);
965 ctx
.lineTo(prevX
, prevYs
[1]);
969 prevX
= point
.canvasx
;
977 // Drawing the lines.
978 for (i
= 0; i
< setCount
; i
+= 1) {
979 this._drawLine(ctx
, i
);
984 * This does dashed lines onto a canvas for a given pattern. You must call
985 * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
986 * the state of the line in regards to where we left off on drawing the pattern.
987 * You can draw a dashed line in several function calls and the pattern will be
988 * continous as long as you didn't call this function with a different pattern
990 * @param ctx The canvas 2d context to draw on.
991 * @param x The start of the line's x coordinate.
992 * @param y The start of the line's y coordinate.
993 * @param x2 The end of the line's x coordinate.
994 * @param y2 The end of the line's y coordinate.
995 * @param pattern The dash pattern to draw, an array of integers where even
996 * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
997 * is drawn, 2 is the space between.). A null pattern, array of length one, or
998 * empty array will do just a solid line.
1001 DygraphCanvasRenderer
.prototype._dashedLine
= function(ctx
, x
, y
, x2
, y2
, pattern
) {
1002 // Original version http://stackoverflow.com/questions
/4576724/dotted-stroke
-in-canvas
1003 // Modified by Russell Valentine to keep line history and continue the pattern
1004 // where it left off.
1005 var dx
, dy
, len
, rot
, patternIndex
, segment
;
1007 // If we don't have a pattern or it is an empty array or of size one just
1009 if (!pattern
|| pattern
.length
<= 1) {
1015 // If we have a different dash pattern than the last time this was called we
1016 // reset our dash history and start the pattern from the begging
1017 // regardless of state of the last pattern.
1018 if (!Dygraph
.compareArrays(pattern
, this._dashedLineToHistoryPattern
)) {
1019 this._dashedLineToHistoryPattern
= pattern
;
1020 this._dashedLineToHistory
= [0, 0];
1024 // Calculate transformation parameters
1027 len
= Math
.sqrt(dx
*dx
+ dy
*dy
);
1028 rot
= Math
.atan2(dy
, dx
);
1030 // Set transformation
1031 ctx
.translate(x
, y
);
1035 // Set last pattern index we used for this pattern.
1036 patternIndex
= this._dashedLineToHistory
[0];
1039 // Get the length of the pattern segment we are dealing with.
1040 segment
= pattern
[patternIndex
];
1041 // If our last draw didn't complete the pattern segment all the way we
1042 // will try to finish it. Otherwise we will try to do the whole segment.
1043 if (this._dashedLineToHistory
[1]) {
1044 x
+= this._dashedLineToHistory
[1];
1049 // We were unable to complete this pattern index all the way, keep
1050 // where we are the history so our next draw continues where we left off
1052 this._dashedLineToHistory
= [patternIndex
, x
-len
];
1055 // We completed this patternIndex, we put in the history that we are on
1056 // the beginning of the next segment.
1057 this._dashedLineToHistory
= [(patternIndex
+1)%pattern
.length
, 0];
1060 // We do a line on a even pattern index and just move on a odd pattern index.
1061 // The move is the empty space in the dash.
1062 if(patternIndex
% 2 === 0) {
1067 // If we are not done, next loop process the next pattern segment, or the
1068 // first segment again if we are at the end of the pattern.
1069 patternIndex
= (patternIndex
+1) % pattern
.length
;