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 */
32 var DygraphCanvasRenderer
= function(dygraph
, element
, elementContext
, layout
) {
33 this.dygraph_
= dygraph
;
36 this.element
= element
;
37 this.elementContext
= elementContext
;
38 this.container
= this.element
.parentNode
;
40 this.height
= this.element
.height
;
41 this.width
= this.element
.width
;
43 // --- check whether everything is ok before we return
44 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
45 throw "Canvas is not supported.";
50 this.annotations
= [];
51 this.chartLabels
= {};
53 this.area
= layout
.getPlotArea();
54 this.container
.style
.position
= "relative";
55 this.container
.style
.width
= this.width
+ "px";
57 // Set up a clipping area for the canvas (and the interaction canvas).
58 // This ensures that we don't overdraw.
59 if (this.dygraph_
.isUsingExcanvas_
) {
60 this._createIEClipArea();
62 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
63 // displaying anything.
64 if (!Dygraph
.isAndroid()) {
65 var ctx
= this.dygraph_
.canvas_ctx_
;
67 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
70 ctx
= this.dygraph_
.hidden_ctx_
;
72 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
78 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
79 return this.dygraph_
.attr_(x
);
82 DygraphCanvasRenderer
.prototype.clear
= function() {
85 // VML takes a while to start up, so we just poll every this.IEDelay
87 if (this.clearDelay
) {
88 this.clearDelay
.cancel();
89 this.clearDelay
= null;
91 context
= this.elementContext
;
94 // TODO(danvk): this is broken, since MochiKit.Async is gone.
95 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
96 // this.clearDelay.addCallback(bind(this.clear, this));
101 context
= this.elementContext
;
102 context
.clearRect(0, 0, this.width
, this.height
);
104 function removeArray(ary
) {
105 for (var i
= 0; i
< ary
.length
; i
++) {
107 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
111 removeArray(this.xlabels
);
112 removeArray(this.ylabels
);
113 removeArray(this.annotations
);
115 for (var k
in this.chartLabels
) {
116 if (!this.chartLabels
.hasOwnProperty(k
)) continue;
117 var el
= this.chartLabels
[k
];
118 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
122 this.annotations
= [];
123 this.chartLabels
= {};
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 * @param { [String] } colors Array of color strings. Should have one entry for
149 * each series to be rendered.
151 DygraphCanvasRenderer
.prototype.setColors
= function(colors
) {
152 this.colorScheme_
= colors
;
156 * Draw an X/Y grid on top of the existing plot
158 DygraphCanvasRenderer
.prototype.render
= function() {
159 // Draw the new X/Y grid
. Lines appear crisper when pixels are rounded to
160 // half-integers. This prevents them from drawing in two rows/cols.
161 var ctx
= this.elementContext
;
162 function halfUp(x
) { return Math
.round(x
) + 0.5; }
163 function halfDown(y
){ return Math
.round(y
) - 0.5; }
165 if (this.attr_('underlayCallback')) {
166 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
167 // users who expect a deprecated form of this callback.
168 this.attr_('underlayCallback')(ctx
, this.area
, this.dygraph_
, this.dygraph_
);
172 if (this.attr_('drawYGrid')) {
173 ticks
= this.layout
.yticks
;
174 // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
176 ctx
.strokeStyle
= this.attr_('gridLineColor');
177 ctx
.lineWidth
= this.attr_('gridLineWidth');
178 for (i
= 0; i
< ticks
.length
; i
++) {
179 // TODO(danvk): allow secondary axes to draw a grid, too.
180 if (ticks
[i
][0] !== 0) continue;
181 x
= halfUp(this.area
.x
);
182 y
= halfDown(this.area
.y
+ ticks
[i
][1] * this.area
.h
);
185 ctx
.lineTo(x
+ this.area
.w
, y
);
192 if (this.attr_('drawXGrid')) {
193 ticks
= this.layout
.xticks
;
195 ctx
.strokeStyle
= this.attr_('gridLineColor');
196 ctx
.lineWidth
= this.attr_('gridLineWidth');
197 for (i
=0; i
<ticks
.length
; i
++) {
198 x
= halfUp(this.area
.x
+ ticks
[i
][0] * this.area
.w
);
199 y
= halfDown(this.area
.y
+ this.area
.h
);
202 ctx
.lineTo(x
, this.area
.y
);
209 // Do the ordinary rendering, as before
210 this._renderLineChart();
212 this._renderChartLabels();
213 this._renderAnnotations();
216 DygraphCanvasRenderer
.prototype._createIEClipArea
= function() {
217 var className
= 'dygraph-clip-div';
218 var graphDiv
= this.dygraph_
.graphDiv
;
220 // Remove old clip divs.
221 for (var i
= graphDiv
.childNodes
.length
-1; i
>= 0; i
--) {
222 if (graphDiv
.childNodes
[i
].className
== className
) {
223 graphDiv
.removeChild(graphDiv
.childNodes
[i
]);
227 // Determine background color to give clip divs.
228 var backgroundColor
= document
.bgColor
;
229 var element
= this.dygraph_
.graphDiv
;
230 while (element
!= document
) {
231 var bgcolor
= element
.currentStyle
.backgroundColor
;
232 if (bgcolor
&& bgcolor
!= 'transparent') {
233 backgroundColor
= bgcolor
;
236 element
= element
.parentNode
;
239 function createClipDiv(area
) {
240 if (area
.w
=== 0 || area
.h
=== 0) {
243 var elem
= document
.createElement('div');
244 elem
.className
= className
;
245 elem
.style
.backgroundColor
= backgroundColor
;
246 elem
.style
.position
= 'absolute';
247 elem
.style
.left
= area
.x
+ 'px';
248 elem
.style
.top
= area
.y
+ 'px';
249 elem
.style
.width
= area
.w
+ 'px';
250 elem
.style
.height
= area
.h
+ 'px';
251 graphDiv
.appendChild(elem
);
254 var plotArea
= this.area
;
265 w
: this.width
- plotArea
.x
,
271 x
: plotArea
.x
+ plotArea
.w
, y
: 0,
272 w
: this.width
-plotArea
.x
- plotArea
.w
,
279 y
: plotArea
.y
+ plotArea
.h
,
280 w
: this.width
- plotArea
.x
,
281 h
: this.height
- plotArea
.h
- plotArea
.y
285 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
286 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
288 // Round pixels to half-integer boundaries for crisper drawing.
289 function halfUp(x
) { return Math
.round(x
) + 0.5; }
290 function halfDown(y
){ return Math
.round(y
) - 0.5; }
292 var context
= this.elementContext
;
294 var label
, x
, y
, tick
, i
;
297 position
: "absolute",
298 fontSize
: this.attr_('axisLabelFontSize') + "px",
300 color
: this.attr_('axisLabelColor'),
301 width
: this.attr_('axisLabelWidth') + "px",
302 // height: this.attr_('axisLabelFontSize') + 2 + "px",
303 lineHeight
: "normal", // Something other than "normal" line-height screws up label positioning.
306 var makeDiv
= function(txt
, axis
, prec_axis
) {
307 var div
= document
.createElement("div");
308 for (var name
in labelStyle
) {
309 if (labelStyle
.hasOwnProperty(name
)) {
310 div
.style
[name
] = labelStyle
[name
];
313 var inner_div
= document
.createElement("div");
314 inner_div
.className
= 'dygraph-axis-label' +
315 ' dygraph-axis-label-' + axis
+
316 (prec_axis
? ' dygraph-axis-label-' + prec_axis
: '');
317 inner_div
.innerHTML
=txt
;
318 div
.appendChild(inner_div
);
324 context
.strokeStyle
= this.attr_('axisLineColor');
325 context
.lineWidth
= this.attr_('axisLineWidth');
327 if (this.attr_('drawYAxis')) {
328 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
329 var num_axes
= this.dygraph_
.numAxes();
330 for (i
= 0; i
< this.layout
.yticks
.length
; i
++) {
331 tick
= this.layout
.yticks
[i
];
332 if (typeof(tick
) == "function") return;
335 var prec_axis
= 'y1';
336 if (tick
[0] == 1) { // right-side y-axis
337 x
= this.area
.x
+ this.area
.w
;
341 y
= this.area
.y
+ tick
[1] * this.area
.h
;
343 /* Tick marks are currently clipped, so don't bother drawing them.
345 context.moveTo(halfUp(x), halfDown(y));
346 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
351 label
= makeDiv(tick
[2], 'y', num_axes
== 2 ? prec_axis
: null);
352 var top
= (y
- this.attr_('axisLabelFontSize') / 2);
353 if (top
< 0) top
= 0;
355 if (top
+ this.attr_('axisLabelFontSize') + 3 > this.height
) {
356 label
.style
.bottom
= "0px";
358 label
.style
.top
= top
+ "px";
361 label
.style
.left
= (this.area
.x
- this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
362 label
.style
.textAlign
= "right";
363 } else if (tick
[0] == 1) {
364 label
.style
.left
= (this.area
.x
+ this.area
.w
+
365 this.attr_('axisTickSize')) + "px";
366 label
.style
.textAlign
= "left";
368 label
.style
.width
= this.attr_('yAxisLabelWidth') + "px";
369 this.container
.appendChild(label
);
370 this.ylabels
.push(label
);
373 // The lowest tick on the y-axis often overlaps with the leftmost
374 // tick on the x-axis. Shift the bottom tick up a little bit to
375 // compensate if necessary.
376 var bottomTick
= this.ylabels
[0];
377 var fontSize
= this.attr_('axisLabelFontSize');
378 var bottom
= parseInt(bottomTick
.style
.top
, 10) + fontSize
;
379 if (bottom
> this.height
- fontSize
) {
380 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
, 10) -
381 fontSize
/ 2) + "px";
385 // draw a vertical line on the left to separate the chart from the labels.
387 if (this.attr_('drawAxesAtZero')) {
388 var r
= this.dygraph_
.toPercentXCoord(0);
389 if (r
> 1 || r
< 0) r
= 0;
390 axisX
= halfUp(this.area
.x
+ r
* this.area
.w
);
392 axisX
= halfUp(this.area
.x
);
395 context
.moveTo(axisX
, halfDown(this.area
.y
));
396 context
.lineTo(axisX
, halfDown(this.area
.y
+ this.area
.h
));
400 // if there's a secondary y-axis, draw a vertical line for that, too.
401 if (this.dygraph_
.numAxes() == 2) {
403 context
.moveTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
));
404 context
.lineTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
410 if (this.attr_('drawXAxis')) {
411 if (this.layout
.xticks
) {
412 for (i
= 0; i
< this.layout
.xticks
.length
; i
++) {
413 tick
= this.layout
.xticks
[i
];
414 x
= this.area
.x
+ tick
[0] * this.area
.w
;
415 y
= this.area
.y
+ this.area
.h
;
417 /* Tick marks are currently clipped, so don't bother drawing them.
419 context.moveTo(halfUp(x), halfDown(y));
420 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
425 label
= makeDiv(tick
[1], 'x');
426 label
.style
.textAlign
= "center";
427 label
.style
.top
= (y
+ this.attr_('axisTickSize')) + 'px';
429 var left
= (x
- this.attr_('axisLabelWidth')/2);
430 if (left
+ this.attr_('axisLabelWidth') > this.width
) {
431 left
= this.width
- this.attr_('xAxisLabelWidth');
432 label
.style
.textAlign
= "right";
436 label
.style
.textAlign
= "left";
439 label
.style
.left
= left
+ "px";
440 label
.style
.width
= this.attr_('xAxisLabelWidth') + "px";
441 this.container
.appendChild(label
);
442 this.xlabels
.push(label
);
448 if (this.attr_('drawAxesAtZero')) {
449 var r
= this.dygraph_
.toPercentYCoord(0, 0);
450 if (r
> 1 || r
< 0) r
= 1;
451 axisY
= halfDown(this.area
.y
+ r
* this.area
.h
);
453 axisY
= halfDown(this.area
.y
+ this.area
.h
);
455 context
.moveTo(halfUp(this.area
.x
), axisY
);
456 context
.lineTo(halfUp(this.area
.x
+ this.area
.w
), axisY
);
465 DygraphCanvasRenderer
.prototype._renderChartLabels
= function() {
468 // Generate divs for the chart title, xlabel and ylabel.
469 // Space for these divs has already been taken away from the charting area in
470 // the DygraphCanvasRenderer constructor.
471 if (this.attr_('title')) {
472 div
= document
.createElement("div");
473 div
.style
.position
= 'absolute';
474 div
.style
.top
= '0px';
475 div
.style
.left
= this.area
.x
+ 'px';
476 div
.style
.width
= this.area
.w
+ 'px';
477 div
.style
.height
= this.attr_('titleHeight') + 'px';
478 div
.style
.textAlign
= 'center';
479 div
.style
.fontSize
= (this.attr_('titleHeight') - 8) + 'px';
480 div
.style
.fontWeight
= 'bold';
481 class_div
= document
.createElement("div");
482 class_div
.className
= 'dygraph-label dygraph-title';
483 class_div
.innerHTML
= this.attr_('title');
484 div
.appendChild(class_div
);
485 this.container
.appendChild(div
);
486 this.chartLabels
.title
= div
;
489 if (this.attr_('xlabel')) {
490 div
= document
.createElement("div");
491 div
.style
.position
= 'absolute';
492 div
.style
.bottom
= 0; // TODO(danvk): this is lazy. Calculate style.top.
493 div
.style
.left
= this.area
.x
+ 'px';
494 div
.style
.width
= this.area
.w
+ 'px';
495 div
.style
.height
= this.attr_('xLabelHeight') + 'px';
496 div
.style
.textAlign
= 'center';
497 div
.style
.fontSize
= (this.attr_('xLabelHeight') - 2) + 'px';
499 class_div
= document
.createElement("div");
500 class_div
.className
= 'dygraph-label dygraph-xlabel';
501 class_div
.innerHTML
= this.attr_('xlabel');
502 div
.appendChild(class_div
);
503 this.container
.appendChild(div
);
504 this.chartLabels
.xlabel
= div
;
508 function createRotatedDiv(axis
, classes
, html
) {
512 width
: that
.attr_('yLabelWidth'),
515 // TODO(danvk): is this outer div actually necessary?
516 div
= document
.createElement("div");
517 div
.style
.position
= 'absolute';
519 div
.style
.left
= box
.left
;
521 div
.style
.right
= box
.left
;
523 div
.style
.top
= box
.top
+ 'px';
524 div
.style
.width
= box
.width
+ 'px';
525 div
.style
.height
= box
.height
+ 'px';
526 div
.style
.fontSize
= (that
.attr_('yLabelWidth') - 2) + 'px';
528 var inner_div
= document
.createElement("div");
529 inner_div
.style
.position
= 'absolute';
530 inner_div
.style
.width
= box
.height
+ 'px';
531 inner_div
.style
.height
= box
.width
+ 'px';
532 inner_div
.style
.top
= (box
.height
/ 2 - box.width / 2) + 'px';
533 inner_div
.style
.left
= (box
.width
/ 2 - box.height / 2) + 'px';
534 inner_div
.style
.textAlign
= 'center';
536 // CSS rotation is an HTML5 feature which is not standardized. Hence every
537 // browser has its own name for the CSS style.
538 var val
= 'rotate(' + (axis
== 1 ? '-' : '') + '90deg)';
539 inner_div
.style
.transform
= val
; // HTML5
540 inner_div
.style
.WebkitTransform
= val
; // Safari/Chrome
541 inner_div
.style
.MozTransform
= val
; // Firefox
542 inner_div
.style
.OTransform
= val
; // Opera
543 inner_div
.style
.msTransform
= val
; // IE9
545 if (typeof(document
.documentMode
) !== 'undefined' &&
546 document
.documentMode
< 9) {
547 // We're dealing w/ an old version of IE
, so we have to rotate the text
548 // using a BasicImage transform. This uses a different origin of rotation
549 // than HTML5 rotation (top left of div vs. its center).
550 inner_div
.style
.filter
=
551 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
552 (axis
== 1 ? '3' : '1') + ')';
553 inner_div
.style
.left
= '0px';
554 inner_div
.style
.top
= '0px';
557 class_div
= document
.createElement("div");
558 class_div
.className
= classes
;
559 class_div
.innerHTML
= html
;
561 inner_div
.appendChild(class_div
);
562 div
.appendChild(inner_div
);
567 if (this.attr_('ylabel')) {
568 div
= createRotatedDiv(1, 'dygraph-label dygraph-ylabel',
569 this.attr_('ylabel'));
570 this.container
.appendChild(div
);
571 this.chartLabels
.ylabel
= div
;
573 if (this.attr_('y2label') && this.dygraph_
.numAxes() == 2) {
574 div
= createRotatedDiv(2, 'dygraph-label dygraph-y2label',
575 this.attr_('y2label'));
576 this.container
.appendChild(div
);
577 this.chartLabels
.y2label
= div
;
582 DygraphCanvasRenderer
.prototype._renderAnnotations
= function() {
583 var annotationStyle
= {
584 "position": "absolute",
585 "fontSize": this.attr_('axisLabelFontSize') + "px",
590 var bindEvt
= function(eventName
, classEventName
, p
, self
) {
592 var a
= p
.annotation
;
593 if (a
.hasOwnProperty(eventName
)) {
594 a
[eventName
](a
, p
, self
.dygraph_
, e
);
595 } else if (self
.dygraph_
.attr_(classEventName
)) {
596 self
.dygraph_
.attr_(classEventName
)(a
, p
, self
.dygraph_
,e
);
601 // Get a list of point with annotations.
602 var points
= this.layout
.annotated_points
;
603 for (var i
= 0; i
< points
.length
; i
++) {
605 if (p
.canvasx
< this.area
.x
|| p
.canvasx
> this.area
.x
+ this.area
.w
||
606 p
.canvasy
< this.area
.y
|| p
.canvasy
> this.area
.y
+ this.area
.h
) {
610 var a
= p
.annotation
;
612 if (a
.hasOwnProperty("tickHeight")) {
613 tick_height
= a
.tickHeight
;
616 var div
= document
.createElement("div");
617 for (var name
in annotationStyle
) {
618 if (annotationStyle
.hasOwnProperty(name
)) {
619 div
.style
[name
] = annotationStyle
[name
];
622 if (!a
.hasOwnProperty('icon')) {
623 div
.className
= "dygraphDefaultAnnotation";
625 if (a
.hasOwnProperty('cssClass')) {
626 div
.className
+= " " + a
.cssClass
;
629 var width
= a
.hasOwnProperty('width') ? a
.width
: 16;
630 var height
= a
.hasOwnProperty('height') ? a
.height
: 16;
631 if (a
.hasOwnProperty('icon')) {
632 var img
= document
.createElement("img");
636 div
.appendChild(img
);
637 } else if (p
.annotation
.hasOwnProperty('shortText')) {
638 div
.appendChild(document
.createTextNode(p
.annotation
.shortText
));
640 div
.style
.left
= (p
.canvasx
- width
/ 2) + "px";
641 if (a
.attachAtBottom
) {
642 div
.style
.top
= (this.area
.h
- height
- tick_height
) + "px";
644 div
.style
.top
= (p
.canvasy
- height
- tick_height
) + "px";
646 div
.style
.width
= width
+ "px";
647 div
.style
.height
= height
+ "px";
648 div
.title
= p
.annotation
.text
;
649 div
.style
.color
= this.colors
[p
.name
];
650 div
.style
.borderColor
= this.colors
[p
.name
];
653 this.dygraph_
.addEvent(div
, 'click',
654 bindEvt('clickHandler', 'annotationClickHandler', p
, this));
655 this.dygraph_
.addEvent(div
, 'mouseover',
656 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p
, this));
657 this.dygraph_
.addEvent(div
, 'mouseout',
658 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p
, this));
659 this.dygraph_
.addEvent(div
, 'dblclick',
660 bindEvt('dblClickHandler', 'annotationDblClickHandler', p
, this));
662 this.container
.appendChild(div
);
663 this.annotations
.push(div
);
665 var ctx
= this.elementContext
;
666 ctx
.strokeStyle
= this.colors
[p
.name
];
668 if (!a
.attachAtBottom
) {
669 ctx
.moveTo(p
.canvasx
, p
.canvasy
);
670 ctx
.lineTo(p
.canvasx
, p
.canvasy
- 2 - tick_height
);
672 ctx
.moveTo(p
.canvasx
, this.area
.h
);
673 ctx
.lineTo(p
.canvasx
, this.area
.h
- 2 - tick_height
);
681 * Returns a function which returns the next index with a renderable point.
682 * When connectSeparatedPoints is false, it just returns the next index.
683 * But when it's true, the returned function will skip past points with null
688 DygraphCanvasRenderer
.makePointIteratorFunction_
= function(
689 connectSeparatedPoints
, points
, start
, end
) {
690 if (connectSeparatedPoints
) {
692 while (++j
+ start
< end
) {
693 if (!(points
[start
+ j
].yval
=== null)) break;
698 return function(j
) { return j
+ 1 };
702 DygraphCanvasRenderer
.isNullOrNaN_
= function(x
) {
703 return (x
=== null || isNaN(x
));
706 DygraphCanvasRenderer
.prototype._drawStyledLine
= function(
707 ctx
, i
, setName
, color
, strokeWidth
, strokePattern
, drawPoints
,
708 drawPointCallback
, pointSize
) {
709 // TODO(konigsberg): Compute attributes outside this method call.
710 var stepPlot
= this.attr_("stepPlot");
711 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
712 var setLength
= this.layout
.setPointsLengths
[i
];
713 var points
= this.layout
.points
;
714 if (!Dygraph
.isArrayLike(strokePattern
)) {
715 strokePattern
= null;
717 var drawGapPoints
= this.dygraph_
.attr_('drawGapEdgePoints', setName
);
720 if (strokeWidth
&& !stepPlot
&& (!strokePattern
|| strokePattern
.length
<= 1)) {
721 this._drawTrivialLine(ctx
, points
, setLength
, firstIndexInSet
, setName
, color
, strokeWidth
, drawPointCallback
, pointSize
, drawPoints
, drawGapPoints
);
723 this._drawNonTrivialLine(ctx
, points
, setLength
, firstIndexInSet
, setName
, color
, strokeWidth
, strokePattern
, drawPointCallback
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
);
728 DygraphCanvasRenderer
.prototype._drawNonTrivialLine
= function(
729 ctx
, points
, setLength
, firstIndexInSet
, setName
, color
, strokeWidth
, strokePattern
, drawPointCallback
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
) {
733 var point
, nextPoint
;
734 var pointsOnLine
= []; // Array of [canvasx, canvasy] pairs.
735 var nextFunc
= DygraphCanvasRenderer
.makePointIteratorFunction_(
736 this.attr_('connectSeparatedPoints'), points
, firstIndexInSet
,
737 firstIndexInSet
+ setLength
);
738 for (var j
= 0; j
< setLength
; j
= nextFunc(j
)) {
739 point
= points
[firstIndexInSet
+ j
];
740 nextY
= (nextFunc(j
) < setLength
) ?
741 points
[firstIndexInSet
+ nextFunc(j
)].canvasy
: null;
742 if (DygraphCanvasRenderer
.isNullOrNaN_(point
.canvasy
)) {
743 if (stepPlot
&& prevX
!== null) {
744 // Draw a horizontal line to the start of the missing data
746 ctx
.strokeStyle
= color
;
747 ctx
.lineWidth
= this.attr_('strokeWidth');
748 this._dashedLine(ctx
, prevX
, prevY
, point
.canvasx
, prevY
, strokePattern
);
751 // this will make us move to the next point, not draw a line to it.
752 prevX
= prevY
= null;
754 // A point is "isolated" if it is non-null but both the previous
755 // and next points are null.
756 var isIsolated
= (!prevX
&& DygraphCanvasRenderer
.isNullOrNaN_(nextY
));
758 // Also consider a point to be is "isolated" if it's adjacent to a
759 // null point, excluding the graph edges.
760 if ((j
> 0 && !prevX
) ||
761 (nextFunc(j
) < setLength
&& DygraphCanvasRenderer
.isNullOrNaN_(nextY
))) {
765 if (prevX
=== null) {
766 prevX
= point
.canvasx
;
767 prevY
= point
.canvasy
;
769 // Skip over points that will be drawn in the same pixel.
770 if (Math
.round(prevX
) == Math
.round(point
.canvasx
) &&
771 Math
.round(prevY
) == Math
.round(point
.canvasy
)) {
774 // TODO(antrob): skip over points that lie on a line that is already
775 // going to be drawn. There is no need to have more than 2
776 // consecutive points that are collinear.
779 ctx
.strokeStyle
= color
;
780 ctx
.lineWidth
= strokeWidth
;
782 this._dashedLine(ctx
, prevX
, prevY
, point
.canvasx
, prevY
, strokePattern
);
783 prevX
= point
.canvasx
;
785 this._dashedLine(ctx
, prevX
, prevY
, point
.canvasx
, point
.canvasy
, strokePattern
);
786 prevX
= point
.canvasx
;
787 prevY
= point
.canvasy
;
792 if (drawPoints
|| isIsolated
) {
793 pointsOnLine
.push([point
.canvasx
, point
.canvasy
]);
797 for (var idx
= 0; idx
< pointsOnLine
.length
; idx
++) {
798 var cb
= pointsOnLine
[idx
];
801 this.dygraph_
, setName
, ctx
, cb
[0], cb
[1], color
, pointSize
);
806 DygraphCanvasRenderer
.prototype._drawTrivialLine
= function(
807 ctx
, points
, setLength
, firstIndexInSet
, setName
, color
, strokeWidth
, drawPointCallback
, pointSize
, drawPoints
, drawGapPoints
) {
811 var pointsOnLine
= []; // Array of [canvasx, canvasy] pairs.
813 ctx
.strokeStyle
= color
;
814 ctx
.lineWidth
= strokeWidth
;
815 var nextFunc
= DygraphCanvasRenderer
.makePointIteratorFunction_(
816 this.attr_('connectSeparatedPoints'), points
, firstIndexInSet
,
817 firstIndexInSet
+ setLength
);
818 for (var j
= firstIndexInSet
; j
< firstIndexInSet
+ setLength
; j
= nextFunc(j
)) {
819 var nextJ
= nextFunc(j
);
820 var point
= points
[j
];
821 nextY
= (nextJ
< firstIndexInSet
+ setLength
) ? points
[nextJ
].canvasy
: null;
822 if (DygraphCanvasRenderer
.isNullOrNaN_(point
.canvasy
)) {
823 prevX
= prevY
= null;
825 var isIsolated
= (!prevX
&& DygraphCanvasRenderer
.isNullOrNaN_(nextY
));
827 // Also consider a point to be is "isolated" if it's adjacent to a
828 // null point, excluding the graph edges.
829 if ((j
> firstIndexInSet
&& !prevX
) ||
830 ((nextJ
< firstIndexInSet
+ setLength
) && DygraphCanvasRenderer
.isNullOrNaN_(nextY
))) {
834 if (prevX
=== null) {
835 prevX
= point
.canvasx
;
836 prevY
= point
.canvasy
;
837 ctx
.moveTo(point
.canvasx
, point
.canvasy
);
839 ctx
.lineTo(point
.canvasx
, point
.canvasy
);
841 if (drawPoints
|| isIsolated
) {
842 pointsOnLine
.push([point
.canvasx
, point
.canvasy
]);
847 for (var idx
= 0; idx
< pointsOnLine
.length
; idx
++) {
848 var cb
= pointsOnLine
[idx
];
851 this.dygraph_
, setName
, ctx
, cb
[0], cb
[1], color
, pointSize
);
856 DygraphCanvasRenderer
.prototype._drawLine
= function(ctx
, i
) {
857 var setNames
= this.layout
.setNames
;
858 var setName
= setNames
[i
];
860 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
861 var borderWidth
= this.dygraph_
.attr_("strokeBorderWidth", setName
);
862 var drawPointCallback
= this.dygraph_
.attr_("drawPointCallback", setName
) ||
863 Dygraph
.Circles
.DEFAULT
;
865 // TODO(konigsberg): Turn this into one call, and then consider inlining drawStyledLine.
866 if (borderWidth
&& strokeWidth
) {
867 this._drawStyledLine(ctx
, i
, setName
,
868 this.dygraph_
.attr_("strokeBorderColor", setName
),
869 strokeWidth
+ 2 * borderWidth
,
870 this.dygraph_
.attr_("strokePattern", setName
),
871 this.dygraph_
.attr_("drawPoints", setName
),
873 this.dygraph_
.attr_("pointSize", setName
));
876 this._drawStyledLine(ctx
, i
, setName
,
877 this.colors
[setName
],
879 this.dygraph_
.attr_("strokePattern", setName
),
880 this.dygraph_
.attr_("drawPoints", setName
),
882 this.dygraph_
.attr_("pointSize", setName
));
886 * Actually draw the lines chart, including error bars.
887 * TODO(danvk): split this into several smaller functions.
890 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
891 // TODO(danvk): use this.attr_ for many of these.
892 var ctx
= this.elementContext
;
893 var fillAlpha
= this.attr_('fillAlpha');
894 var errorBars
= this.attr_("errorBars") || this.attr_("customBars");
895 var fillGraph
= this.attr_("fillGraph");
896 var stackedGraph
= this.attr_("stackedGraph");
897 var stepPlot
= this.attr_("stepPlot");
898 var points
= this.layout
.points
;
899 var pointsLength
= points
.length
;
900 var point
, i
, j
, prevX
, prevY
, prevYs
, color
, setName
, newYs
, err_color
, rgb
, yscale
, axis
;
902 var setNames
= this.layout
.setNames
;
903 var setCount
= setNames
.length
;
905 // TODO(danvk): Move this mapping into Dygraph and get it out of here.
907 for (i
= 0; i
< setCount
; i
++) {
908 this.colors
[setNames
[i
]] = this.colorScheme_
[i
% this.colorScheme_
.length
];
914 // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
915 // transformations can be pushed into the canvas via linear transformation
917 for (i
= pointsLength
; i
--;) {
919 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
920 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
927 this.dygraph_
.warn("Can't use fillGraph option with error bars");
930 for (i
= 0; i
< setCount
; i
++) {
931 setName
= setNames
[i
];
932 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
933 color
= this.colors
[setName
];
935 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
936 var setLength
= this.layout
.setPointsLengths
[i
];
937 var afterLastIndexInSet
= firstIndexInSet
+ setLength
;
939 var nextFunc
= DygraphCanvasRenderer
.makePointIteratorFunction_(
940 this.attr_('connectSeparatedPoints'), points
,
941 afterLastIndexInSet
);
943 // setup graphics context
947 yscale
= axis
.yscale
;
948 // should be same color as the lines but only 15% opaque.
949 rgb
= new RGBColor(color
);
950 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
952 ctx
.fillStyle
= err_color
;
954 for (j
= firstIndexInSet
; j
< afterLastIndexInSet
; j
= nextFunc(j
)) {
956 if (point
.name
== setName
) { // TODO(klausw): this is always true
957 if (!Dygraph
.isOK(point
.y
)) {
964 newYs
= [ point
.y_bottom
, point
.y_top
];
967 newYs
= [ point
.y_bottom
, point
.y_top
];
969 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
970 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
973 ctx
.moveTo(prevX
, newYs
[0]);
975 ctx
.moveTo(prevX
, prevYs
[0]);
977 ctx
.lineTo(point
.canvasx
, newYs
[0]);
978 ctx
.lineTo(point
.canvasx
, newYs
[1]);
980 ctx
.lineTo(prevX
, newYs
[1]);
982 ctx
.lineTo(prevX
, prevYs
[1]);
987 prevX
= point
.canvasx
;
993 } else if (fillGraph
) {
995 var baseline
= {}; // for stacked graphs: baseline for filling
998 // process sets in reverse order (needed for stacked graphs)
999 for (i
= setCount
- 1; i
>= 0; i
--) {
1000 setName
= setNames
[i
];
1001 color
= this.colors
[setName
];
1002 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
1003 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
1004 if (axisY
< 0.0) axisY
= 0.0;
1005 else if (axisY
> 1.0) axisY
= 1.0;
1006 axisY
= this.area
.h
* axisY
+ this.area
.y
;
1007 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
1008 var setLength
= this.layout
.setPointsLengths
[i
];
1009 var afterLastIndexInSet
= firstIndexInSet
+ setLength
;
1011 var nextFunc
= DygraphCanvasRenderer
.makePointIteratorFunction_(
1012 this.attr_('connectSeparatedPoints'), points
,
1013 afterLastIndexInSet
);
1015 // setup graphics context
1018 yscale
= axis
.yscale
;
1019 // should be same color as the lines but only 15% opaque.
1020 rgb
= new RGBColor(color
);
1021 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
1023 ctx
.fillStyle
= err_color
;
1025 for (j
= firstIndexInSet
; j
< afterLastIndexInSet
; j
= nextFunc(j
)) {
1027 if (point
.name
== setName
) { // TODO(klausw): this is always true
1028 if (!Dygraph
.isOK(point
.y
)) {
1033 currBaseline
= baseline
[point
.canvasx
];
1035 if (currBaseline
=== undefined
) {
1039 lastY
= currBaseline
[0];
1041 lastY
= currBaseline
;
1044 newYs
= [ point
.canvasy
, lastY
];
1047 // Step plots must keep track of the top and bottom of
1048 // the baseline at each point.
1049 if(prevYs
[0] === -1) {
1050 baseline
[point
.canvasx
] = [ point
.canvasy
, axisY
];
1052 baseline
[point
.canvasx
] = [ point
.canvasy
, prevYs
[0] ];
1055 baseline
[point
.canvasx
] = point
.canvasy
;
1059 newYs
= [ point
.canvasy
, axisY
];
1061 if (!isNaN(prevX
)) {
1062 ctx
.moveTo(prevX
, prevYs
[0]);
1065 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
1067 // Draw to the bottom of the baseline
1068 ctx
.lineTo(point
.canvasx
, currBaseline
[1]);
1070 ctx
.lineTo(point
.canvasx
, newYs
[1]);
1073 ctx
.lineTo(point
.canvasx
, newYs
[0]);
1074 ctx
.lineTo(point
.canvasx
, newYs
[1]);
1077 ctx
.lineTo(prevX
, prevYs
[1]);
1081 prevX
= point
.canvasx
;
1089 // Drawing the lines.
1090 for (i
= 0; i
< setCount
; i
+= 1) {
1091 this._drawLine(ctx
, i
);
1096 * This does dashed lines onto a canvas for a given pattern. You must call
1097 * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
1098 * the state of the line in regards to where we left off on drawing the pattern.
1099 * You can draw a dashed line in several function calls and the pattern will be
1100 * continous as long as you didn't call this function with a different pattern
1102 * @param ctx The canvas 2d context to draw on.
1103 * @param x The start of the line's x coordinate.
1104 * @param y The start of the line's y coordinate.
1105 * @param x2 The end of the line's x coordinate.
1106 * @param y2 The end of the line's y coordinate.
1107 * @param pattern The dash pattern to draw, an array of integers where even
1108 * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
1109 * is drawn, 2 is the space between.). A null pattern, array of length one, or
1110 * empty array will do just a solid line.
1113 DygraphCanvasRenderer
.prototype._dashedLine
= function(ctx
, x
, y
, x2
, y2
, pattern
) {
1114 // Original version http://stackoverflow.com/questions
/4576724/dotted-stroke
-in-canvas
1115 // Modified by Russell Valentine to keep line history and continue the pattern
1116 // where it left off.
1117 var dx
, dy
, len
, rot
, patternIndex
, segment
;
1119 // If we don't have a pattern or it is an empty array or of size one just
1121 if (!pattern
|| pattern
.length
<= 1) {
1127 // If we have a different dash pattern than the last time this was called we
1128 // reset our dash history and start the pattern from the begging
1129 // regardless of state of the last pattern.
1130 if (!Dygraph
.compareArrays(pattern
, this._dashedLineToHistoryPattern
)) {
1131 this._dashedLineToHistoryPattern
= pattern
;
1132 this._dashedLineToHistory
= [0, 0];
1136 // Calculate transformation parameters
1139 len
= Math
.sqrt(dx
*dx
+ dy
*dy
);
1140 rot
= Math
.atan2(dy
, dx
);
1142 // Set transformation
1143 ctx
.translate(x
, y
);
1147 // Set last pattern index we used for this pattern.
1148 patternIndex
= this._dashedLineToHistory
[0];
1151 // Get the length of the pattern segment we are dealing with.
1152 segment
= pattern
[patternIndex
];
1153 // If our last draw didn't complete the pattern segment all the way we
1154 // will try to finish it. Otherwise we will try to do the whole segment.
1155 if (this._dashedLineToHistory
[1]) {
1156 x
+= this._dashedLineToHistory
[1];
1161 // We were unable to complete this pattern index all the way, keep
1162 // where we are the history so our next draw continues where we left off
1164 this._dashedLineToHistory
= [patternIndex
, x
-len
];
1167 // We completed this patternIndex, we put in the history that we are on
1168 // the beginning of the next segment.
1169 this._dashedLineToHistory
= [(patternIndex
+1)%pattern
.length
, 0];
1172 // We do a line on a even pattern index and just move on a odd pattern index.
1173 // The move is the empty space in the dash.
1174 if(patternIndex
% 2 === 0) {
1179 // If we are not done, next loop process the next pattern segment, or the
1180 // first segment again if we are at the end of the pattern.
1181 patternIndex
= (patternIndex
+1) % pattern
.length
;