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 */
31 var DygraphCanvasRenderer
= function(dygraph
, element
, elementContext
, layout
) {
32 this.dygraph_
= dygraph
;
35 this.element
= element
;
36 this.elementContext
= elementContext
;
37 this.container
= this.element
.parentNode
;
39 this.height
= this.element
.height
;
40 this.width
= this.element
.width
;
42 // --- check whether everything is ok before we return
43 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
44 throw "Canvas is not supported.";
49 this.annotations
= [];
50 this.chartLabels
= {};
52 this.area
= layout
.getPlotArea();
53 this.container
.style
.position
= "relative";
54 this.container
.style
.width
= this.width
+ "px";
56 // Set up a clipping area for the canvas (and the interaction canvas).
57 // This ensures that we don't overdraw.
58 if (this.dygraph_
.isUsingExcanvas_
) {
59 this._createIEClipArea();
61 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
62 // displaying anything.
63 if (!Dygraph
.isAndroid()) {
64 var ctx
= this.dygraph_
.canvas_ctx_
;
66 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
69 ctx
= this.dygraph_
.hidden_ctx_
;
71 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
77 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
78 return this.dygraph_
.attr_(x
);
81 DygraphCanvasRenderer
.prototype.clear
= function() {
84 // VML takes a while to start up, so we just poll every this.IEDelay
86 if (this.clearDelay
) {
87 this.clearDelay
.cancel();
88 this.clearDelay
= null;
90 context
= this.elementContext
;
93 // TODO(danvk): this is broken, since MochiKit.Async is gone.
94 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
95 // this.clearDelay.addCallback(bind(this.clear, this));
100 context
= this.elementContext
;
101 context
.clearRect(0, 0, this.width
, this.height
);
103 function removeArray(ary
) {
104 for (var i
= 0; i
< ary
.length
; i
++) {
106 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
110 removeArray(this.xlabels
);
111 removeArray(this.ylabels
);
112 removeArray(this.annotations
);
114 for (var k
in this.chartLabels
) {
115 if (!this.chartLabels
.hasOwnProperty(k
)) continue;
116 var el
= this.chartLabels
[k
];
117 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
121 this.annotations
= [];
122 this.chartLabels
= {};
126 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
129 if (typeof(canvasName
) == 'undefined' || canvasName
=== null) {
130 canvas
= document
.createElement("canvas");
134 canvas
.getContext("2d");
137 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
138 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
139 if ((!ie
) || (ie
[1] < 6) || (opera
))
147 * @param { [String] } colors Array of color strings. Should have one entry for
148 * each series to be rendered.
150 DygraphCanvasRenderer
.prototype.setColors
= function(colors
) {
151 this.colorScheme_
= colors
;
155 * Draw an X/Y grid on top of the existing plot
157 DygraphCanvasRenderer
.prototype.render
= function() {
158 // Draw the new X/Y grid
. Lines appear crisper when pixels are rounded to
159 // half-integers. This prevents them from drawing in two rows/cols.
160 var ctx
= this.elementContext
;
161 function halfUp(x
) { return Math
.round(x
) + 0.5; }
162 function halfDown(y
){ return Math
.round(y
) - 0.5; }
164 if (this.attr_('underlayCallback')) {
165 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
166 // users who expect a deprecated form of this callback.
167 this.attr_('underlayCallback')(ctx
, this.area
, this.dygraph_
, this.dygraph_
);
171 if (this.attr_('drawYGrid')) {
172 ticks
= this.layout
.yticks
;
173 // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
175 ctx
.strokeStyle
= this.attr_('gridLineColor');
176 ctx
.lineWidth
= this.attr_('gridLineWidth');
177 for (i
= 0; i
< ticks
.length
; i
++) {
178 // TODO(danvk): allow secondary axes to draw a grid, too.
179 if (ticks
[i
][0] !== 0) continue;
180 x
= halfUp(this.area
.x
);
181 y
= halfDown(this.area
.y
+ ticks
[i
][1] * this.area
.h
);
184 ctx
.lineTo(x
+ this.area
.w
, y
);
190 if (this.attr_('drawXGrid')) {
191 ticks
= this.layout
.xticks
;
193 ctx
.strokeStyle
= this.attr_('gridLineColor');
194 ctx
.lineWidth
= this.attr_('gridLineWidth');
195 for (i
=0; i
<ticks
.length
; i
++) {
196 x
= halfUp(this.area
.x
+ ticks
[i
][0] * this.area
.w
);
197 y
= halfDown(this.area
.y
+ this.area
.h
);
200 ctx
.lineTo(x
, this.area
.y
);
206 // Do the ordinary rendering, as before
207 this._renderLineChart();
209 this._renderChartLabels();
210 this._renderAnnotations();
213 DygraphCanvasRenderer
.prototype._createIEClipArea
= function() {
214 var className
= 'dygraph-clip-div';
215 var graphDiv
= this.dygraph_
.graphDiv
;
217 // Remove old clip divs.
218 for (var i
= graphDiv
.childNodes
.length
-1; i
>= 0; i
--) {
219 if (graphDiv
.childNodes
[i
].className
== className
) {
220 graphDiv
.removeChild(graphDiv
.childNodes
[i
]);
224 // Determine background color to give clip divs.
225 var backgroundColor
= document
.bgColor
;
226 var element
= this.dygraph_
.graphDiv
;
227 while (element
!= document
) {
228 var bgcolor
= element
.currentStyle
.backgroundColor
;
229 if (bgcolor
&& bgcolor
!= 'transparent') {
230 backgroundColor
= bgcolor
;
233 element
= element
.parentNode
;
236 function createClipDiv(area
) {
237 if (area
.w
=== 0 || area
.h
=== 0) {
240 var elem
= document
.createElement('div');
241 elem
.className
= className
;
242 elem
.style
.backgroundColor
= backgroundColor
;
243 elem
.style
.position
= 'absolute';
244 elem
.style
.left
= area
.x
+ 'px';
245 elem
.style
.top
= area
.y
+ 'px';
246 elem
.style
.width
= area
.w
+ 'px';
247 elem
.style
.height
= area
.h
+ 'px';
248 graphDiv
.appendChild(elem
);
251 var plotArea
= this.area
;
262 w
: this.width
- plotArea
.x
,
268 x
: plotArea
.x
+ plotArea
.w
, y
: 0,
269 w
: this.width
-plotArea
.x
- plotArea
.w
,
276 y
: plotArea
.y
+ plotArea
.h
,
277 w
: this.width
- plotArea
.x
,
278 h
: this.height
- plotArea
.h
- plotArea
.y
282 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
283 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
285 // Round pixels to half-integer boundaries for crisper drawing.
286 function halfUp(x
) { return Math
.round(x
) + 0.5; }
287 function halfDown(y
){ return Math
.round(y
) - 0.5; }
289 var context
= this.elementContext
;
291 var label
, x
, y
, tick
, i
;
294 position
: "absolute",
295 fontSize
: this.attr_('axisLabelFontSize') + "px",
297 color
: this.attr_('axisLabelColor'),
298 width
: this.attr_('axisLabelWidth') + "px",
299 // height: this.attr_('axisLabelFontSize') + 2 + "px",
300 lineHeight
: "normal", // Something other than "normal" line-height screws up label positioning.
303 var makeDiv
= function(txt
, axis
, prec_axis
) {
304 var div
= document
.createElement("div");
305 for (var name
in labelStyle
) {
306 if (labelStyle
.hasOwnProperty(name
)) {
307 div
.style
[name
] = labelStyle
[name
];
310 var inner_div
= document
.createElement("div");
311 inner_div
.className
= 'dygraph-axis-label' +
312 ' dygraph-axis-label-' + axis
+
313 (prec_axis
? ' dygraph-axis-label-' + prec_axis
: '');
314 inner_div
.appendChild(document
.createTextNode(txt
));
315 div
.appendChild(inner_div
);
321 context
.strokeStyle
= this.attr_('axisLineColor');
322 context
.lineWidth
= this.attr_('axisLineWidth');
324 if (this.attr_('drawYAxis')) {
325 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
326 var num_axes
= this.dygraph_
.numAxes();
327 for (i
= 0; i
< this.layout
.yticks
.length
; i
++) {
328 tick
= this.layout
.yticks
[i
];
329 if (typeof(tick
) == "function") return;
332 var prec_axis
= 'y1';
333 if (tick
[0] == 1) { // right-side y-axis
334 x
= this.area
.x
+ this.area
.w
;
338 y
= this.area
.y
+ tick
[1] * this.area
.h
;
340 /* Tick marks are currently clipped, so don't bother drawing them.
342 context.moveTo(halfUp(x), halfDown(y));
343 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
348 label
= makeDiv(tick
[2], 'y', num_axes
== 2 ? prec_axis
: null);
349 var top
= (y
- this.attr_('axisLabelFontSize') / 2);
350 if (top
< 0) top
= 0;
352 if (top
+ this.attr_('axisLabelFontSize') + 3 > this.height
) {
353 label
.style
.bottom
= "0px";
355 label
.style
.top
= top
+ "px";
358 label
.style
.left
= (this.area
.x
- this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
359 label
.style
.textAlign
= "right";
360 } else if (tick
[0] == 1) {
361 label
.style
.left
= (this.area
.x
+ this.area
.w
+
362 this.attr_('axisTickSize')) + "px";
363 label
.style
.textAlign
= "left";
365 label
.style
.width
= this.attr_('yAxisLabelWidth') + "px";
366 this.container
.appendChild(label
);
367 this.ylabels
.push(label
);
370 // The lowest tick on the y-axis often overlaps with the leftmost
371 // tick on the x-axis. Shift the bottom tick up a little bit to
372 // compensate if necessary.
373 var bottomTick
= this.ylabels
[0];
374 var fontSize
= this.attr_('axisLabelFontSize');
375 var bottom
= parseInt(bottomTick
.style
.top
, 10) + fontSize
;
376 if (bottom
> this.height
- fontSize
) {
377 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
, 10) -
378 fontSize
/ 2) + "px";
382 // draw a vertical line on the left to separate the chart from the labels.
384 context
.moveTo(halfUp(this.area
.x
), halfDown(this.area
.y
));
385 context
.lineTo(halfUp(this.area
.x
), halfDown(this.area
.y
+ this.area
.h
));
389 // if there's a secondary y-axis, draw a vertical line for that, too.
390 if (this.dygraph_
.numAxes() == 2) {
392 context
.moveTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
));
393 context
.lineTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
399 if (this.attr_('drawXAxis')) {
400 if (this.layout
.xticks
) {
401 for (i
= 0; i
< this.layout
.xticks
.length
; i
++) {
402 tick
= this.layout
.xticks
[i
];
403 x
= this.area
.x
+ tick
[0] * this.area
.w
;
404 y
= this.area
.y
+ this.area
.h
;
406 /* Tick marks are currently clipped, so don't bother drawing them.
408 context.moveTo(halfUp(x), halfDown(y));
409 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
414 label
= makeDiv(tick
[1], 'x');
415 label
.style
.textAlign
= "center";
416 label
.style
.top
= (y
+ this.attr_('axisTickSize')) + 'px';
418 var left
= (x
- this.attr_('axisLabelWidth')/2);
419 if (left
+ this.attr_('axisLabelWidth') > this.width
) {
420 left
= this.width
- this.attr_('xAxisLabelWidth');
421 label
.style
.textAlign
= "right";
425 label
.style
.textAlign
= "left";
428 label
.style
.left
= left
+ "px";
429 label
.style
.width
= this.attr_('xAxisLabelWidth') + "px";
430 this.container
.appendChild(label
);
431 this.xlabels
.push(label
);
436 context
.moveTo(halfUp(this.area
.x
), halfDown(this.area
.y
+ this.area
.h
));
437 context
.lineTo(halfUp(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
446 DygraphCanvasRenderer
.prototype._renderChartLabels
= function() {
449 // Generate divs for the chart title, xlabel and ylabel.
450 // Space for these divs has already been taken away from the charting area in
451 // the DygraphCanvasRenderer constructor.
452 if (this.attr_('title')) {
453 div
= document
.createElement("div");
454 div
.style
.position
= 'absolute';
455 div
.style
.top
= '0px';
456 div
.style
.left
= this.area
.x
+ 'px';
457 div
.style
.width
= this.area
.w
+ 'px';
458 div
.style
.height
= this.attr_('titleHeight') + 'px';
459 div
.style
.textAlign
= 'center';
460 div
.style
.fontSize
= (this.attr_('titleHeight') - 8) + 'px';
461 div
.style
.fontWeight
= 'bold';
462 class_div
= document
.createElement("div");
463 class_div
.className
= 'dygraph-label dygraph-title';
464 class_div
.innerHTML
= this.attr_('title');
465 div
.appendChild(class_div
);
466 this.container
.appendChild(div
);
467 this.chartLabels
.title
= div
;
470 if (this.attr_('xlabel')) {
471 div
= document
.createElement("div");
472 div
.style
.position
= 'absolute';
473 div
.style
.bottom
= 0; // TODO(danvk): this is lazy. Calculate style.top.
474 div
.style
.left
= this.area
.x
+ 'px';
475 div
.style
.width
= this.area
.w
+ 'px';
476 div
.style
.height
= this.attr_('xLabelHeight') + 'px';
477 div
.style
.textAlign
= 'center';
478 div
.style
.fontSize
= (this.attr_('xLabelHeight') - 2) + 'px';
480 class_div
= document
.createElement("div");
481 class_div
.className
= 'dygraph-label dygraph-xlabel';
482 class_div
.innerHTML
= this.attr_('xlabel');
483 div
.appendChild(class_div
);
484 this.container
.appendChild(div
);
485 this.chartLabels
.xlabel
= div
;
489 function createRotatedDiv(axis
, classes
, html
) {
493 width
: that
.attr_('yLabelWidth'),
496 // TODO(danvk): is this outer div actually necessary?
497 div
= document
.createElement("div");
498 div
.style
.position
= 'absolute';
500 div
.style
.left
= box
.left
;
502 div
.style
.right
= box
.left
;
504 div
.style
.top
= box
.top
+ 'px';
505 div
.style
.width
= box
.width
+ 'px';
506 div
.style
.height
= box
.height
+ 'px';
507 div
.style
.fontSize
= (that
.attr_('yLabelWidth') - 2) + 'px';
509 var inner_div
= document
.createElement("div");
510 inner_div
.style
.position
= 'absolute';
511 inner_div
.style
.width
= box
.height
+ 'px';
512 inner_div
.style
.height
= box
.width
+ 'px';
513 inner_div
.style
.top
= (box
.height
/ 2 - box.width / 2) + 'px';
514 inner_div
.style
.left
= (box
.width
/ 2 - box.height / 2) + 'px';
515 inner_div
.style
.textAlign
= 'center';
517 // CSS rotation is an HTML5 feature which is not standardized. Hence every
518 // browser has its own name for the CSS style.
519 var val
= 'rotate(' + (axis
== 1 ? '-' : '') + '90deg)';
520 inner_div
.style
.transform
= val
; // HTML5
521 inner_div
.style
.WebkitTransform
= val
; // Safari/Chrome
522 inner_div
.style
.MozTransform
= val
; // Firefox
523 inner_div
.style
.OTransform
= val
; // Opera
524 inner_div
.style
.msTransform
= val
; // IE9
526 if (typeof(document
.documentMode
) !== 'undefined' &&
527 document
.documentMode
< 9) {
528 // We're dealing w/ an old version of IE
, so we have to rotate the text
529 // using a BasicImage transform. This uses a different origin of rotation
530 // than HTML5 rotation (top left of div vs. its center).
531 inner_div
.style
.filter
=
532 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' +
533 (axis
== 1 ? '3' : '1') + ')';
534 inner_div
.style
.left
= '0px';
535 inner_div
.style
.top
= '0px';
538 class_div
= document
.createElement("div");
539 class_div
.className
= classes
;
540 class_div
.innerHTML
= html
;
542 inner_div
.appendChild(class_div
);
543 div
.appendChild(inner_div
);
548 if (this.attr_('ylabel')) {
549 div
= createRotatedDiv(1, 'dygraph-label dygraph-ylabel',
550 this.attr_('ylabel'));
551 this.container
.appendChild(div
);
552 this.chartLabels
.ylabel
= div
;
554 if (this.attr_('y2label') && this.dygraph_
.numAxes() == 2) {
555 div
= createRotatedDiv(2, 'dygraph-label dygraph-y2label',
556 this.attr_('y2label'));
557 this.container
.appendChild(div
);
558 this.chartLabels
.y2label
= div
;
563 DygraphCanvasRenderer
.prototype._renderAnnotations
= function() {
564 var annotationStyle
= {
565 "position": "absolute",
566 "fontSize": this.attr_('axisLabelFontSize') + "px",
571 var bindEvt
= function(eventName
, classEventName
, p
, self
) {
573 var a
= p
.annotation
;
574 if (a
.hasOwnProperty(eventName
)) {
575 a
[eventName
](a
, p
, self
.dygraph_
, e
);
576 } else if (self
.dygraph_
.attr_(classEventName
)) {
577 self
.dygraph_
.attr_(classEventName
)(a
, p
, self
.dygraph_
,e
);
582 // Get a list of point with annotations.
583 var points
= this.layout
.annotated_points
;
584 for (var i
= 0; i
< points
.length
; i
++) {
586 if (p
.canvasx
< this.area
.x
|| p
.canvasx
> this.area
.x
+ this.area
.w
) {
590 var a
= p
.annotation
;
592 if (a
.hasOwnProperty("tickHeight")) {
593 tick_height
= a
.tickHeight
;
596 var div
= document
.createElement("div");
597 for (var name
in annotationStyle
) {
598 if (annotationStyle
.hasOwnProperty(name
)) {
599 div
.style
[name
] = annotationStyle
[name
];
602 if (!a
.hasOwnProperty('icon')) {
603 div
.className
= "dygraphDefaultAnnotation";
605 if (a
.hasOwnProperty('cssClass')) {
606 div
.className
+= " " + a
.cssClass
;
609 var width
= a
.hasOwnProperty('width') ? a
.width
: 16;
610 var height
= a
.hasOwnProperty('height') ? a
.height
: 16;
611 if (a
.hasOwnProperty('icon')) {
612 var img
= document
.createElement("img");
616 div
.appendChild(img
);
617 } else if (p
.annotation
.hasOwnProperty('shortText')) {
618 div
.appendChild(document
.createTextNode(p
.annotation
.shortText
));
620 div
.style
.left
= (p
.canvasx
- width
/ 2) + "px";
621 if (a
.attachAtBottom
) {
622 div
.style
.top
= (this.area
.h
- height
- tick_height
) + "px";
624 div
.style
.top
= (p
.canvasy
- height
- tick_height
) + "px";
626 div
.style
.width
= width
+ "px";
627 div
.style
.height
= height
+ "px";
628 div
.title
= p
.annotation
.text
;
629 div
.style
.color
= this.colors
[p
.name
];
630 div
.style
.borderColor
= this.colors
[p
.name
];
633 Dygraph
.addEvent(div
, 'click',
634 bindEvt('clickHandler', 'annotationClickHandler', p
, this));
635 Dygraph
.addEvent(div
, 'mouseover',
636 bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p
, this));
637 Dygraph
.addEvent(div
, 'mouseout',
638 bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p
, this));
639 Dygraph
.addEvent(div
, 'dblclick',
640 bindEvt('dblClickHandler', 'annotationDblClickHandler', p
, this));
642 this.container
.appendChild(div
);
643 this.annotations
.push(div
);
645 var ctx
= this.elementContext
;
646 ctx
.strokeStyle
= this.colors
[p
.name
];
648 if (!a
.attachAtBottom
) {
649 ctx
.moveTo(p
.canvasx
, p
.canvasy
);
650 ctx
.lineTo(p
.canvasx
, p
.canvasy
- 2 - tick_height
);
652 ctx
.moveTo(p
.canvasx
, this.area
.h
);
653 ctx
.lineTo(p
.canvasx
, this.area
.h
- 2 - tick_height
);
662 * Actually draw the lines chart, including error bars.
663 * TODO(danvk): split this into several smaller functions.
666 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
667 var isNullOrNaN
= function(x
) {
668 return (x
=== null || isNaN(x
));
671 // TODO(danvk): use this.attr_ for many of these.
672 var context
= this.elementContext
;
673 var fillAlpha
= this.attr_('fillAlpha');
674 var errorBars
= this.attr_("errorBars") || this.attr_("customBars");
675 var fillGraph
= this.attr_("fillGraph");
676 var stackedGraph
= this.attr_("stackedGraph");
677 var stepPlot
= this.attr_("stepPlot");
678 var points
= this.layout
.points
;
679 var pointsLength
= points
.length
;
680 var point
, i
, j
, prevX
, prevY
, prevYs
, color
, setName
, newYs
, err_color
, rgb
, yscale
, axis
;
683 for (var name
in this.layout
.datasets
) {
684 if (this.layout
.datasets
.hasOwnProperty(name
)) {
688 var setCount
= setNames
.length
;
690 // TODO(danvk): Move this mapping into Dygraph and get it out of here.
692 for (i
= 0; i
< setCount
; i
++) {
693 this.colors
[setNames
[i
]] = this.colorScheme_
[i
% this.colorScheme_
.length
];
698 for (i
= pointsLength
; i
--;) {
700 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
701 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
708 this.dygraph_
.warn("Can't use fillGraph option with error bars");
711 for (i
= 0; i
< setCount
; i
++) {
712 setName
= setNames
[i
];
713 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
714 color
= this.colors
[setName
];
716 // setup graphics context
721 yscale
= axis
.yscale
;
722 // should be same color as the lines but only 15% opaque.
723 rgb
= new RGBColor(color
);
724 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
726 ctx
.fillStyle
= err_color
;
728 for (j
= 0; j
< pointsLength
; j
++) {
730 if (point
.name
== setName
) {
731 if (!Dygraph
.isOK(point
.y
)) {
738 newYs
= [ point
.y_bottom
, point
.y_top
];
741 newYs
= [ point
.y_bottom
, point
.y_top
];
743 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
744 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
747 ctx
.moveTo(prevX
, newYs
[0]);
749 ctx
.moveTo(prevX
, prevYs
[0]);
751 ctx
.lineTo(point
.canvasx
, newYs
[0]);
752 ctx
.lineTo(point
.canvasx
, newYs
[1]);
754 ctx
.lineTo(prevX
, newYs
[1]);
756 ctx
.lineTo(prevX
, prevYs
[1]);
761 prevX
= point
.canvasx
;
766 } else if (fillGraph
) {
767 var baseline
= []; // for stacked graphs: baseline for filling
769 // process sets in reverse order (needed for stacked graphs)
770 for (i
= setCount
- 1; i
>= 0; i
--) {
771 setName
= setNames
[i
];
772 color
= this.colors
[setName
];
773 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
774 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
775 if (axisY
< 0.0) axisY
= 0.0;
776 else if (axisY
> 1.0) axisY
= 1.0;
777 axisY
= this.area
.h
* axisY
+ this.area
.y
;
779 // setup graphics context
783 yscale
= axis
.yscale
;
784 // should be same color as the lines but only 15% opaque.
785 rgb
= new RGBColor(color
);
786 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
788 ctx
.fillStyle
= err_color
;
790 for (j
= 0; j
< pointsLength
; j
++) {
792 if (point
.name
== setName
) {
793 if (!Dygraph
.isOK(point
.y
)) {
798 var lastY
= baseline
[point
.canvasx
];
799 if (lastY
=== undefined
) lastY
= axisY
;
800 baseline
[point
.canvasx
] = point
.canvasy
;
801 newYs
= [ point
.canvasy
, lastY
];
803 newYs
= [ point
.canvasy
, axisY
];
806 ctx
.moveTo(prevX
, prevYs
[0]);
808 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
810 ctx
.lineTo(point
.canvasx
, newYs
[0]);
812 ctx
.lineTo(point
.canvasx
, newYs
[1]);
813 ctx
.lineTo(prevX
, prevYs
[1]);
817 prevX
= point
.canvasx
;
824 // Drawing the lines.
825 var firstIndexInSet
= 0;
826 var afterLastIndexInSet
= 0;
828 for (i
= 0; i
< setCount
; i
+= 1) {
829 setLength
= this.layout
.setPointsLengths
[i
];
830 afterLastIndexInSet
+= setLength
;
831 setName
= setNames
[i
];
832 color
= this.colors
[setName
];
833 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
835 // setup graphics context
837 var pointSize
= this.dygraph_
.attr_("pointSize", setName
);
840 var drawPoints
= this.dygraph_
.attr_("drawPoints", setName
);
841 for (j
= firstIndexInSet
; j
< afterLastIndexInSet
; j
++) {
843 if (isNullOrNaN(point
.canvasy
)) {
844 if (stepPlot
&& prevX
!== null) {
845 // Draw a horizontal line to the start of the missing data
847 ctx
.strokeStyle
= color
;
848 ctx
.lineWidth
= this.attr_('strokeWidth');
849 ctx
.moveTo(prevX
, prevY
);
850 ctx
.lineTo(point
.canvasx
, prevY
);
853 // this will make us move to the next point, not draw a line to it.
854 prevX
= prevY
= null;
856 // A point is "isolated" if it is non-null but both the previous
857 // and next points are null.
858 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
859 isNullOrNaN(points
[j
+1].canvasy
)));
860 if (prevX
=== null) {
861 prevX
= point
.canvasx
;
862 prevY
= point
.canvasy
;
864 // Skip over points that will be drawn in the same pixel.
865 if (Math
.round(prevX
) == Math
.round(point
.canvasx
) &&
866 Math
.round(prevY
) == Math
.round(point
.canvasy
)) {
869 // TODO(antrob): skip over points that lie on a line that is already
870 // going to be drawn. There is no need to have more than 2
871 // consecutive points that are collinear.
874 ctx
.strokeStyle
= color
;
875 ctx
.lineWidth
= strokeWidth
;
876 ctx
.moveTo(prevX
, prevY
);
878 ctx
.lineTo(point
.canvasx
, prevY
);
880 prevX
= point
.canvasx
;
881 prevY
= point
.canvasy
;
882 ctx
.lineTo(prevX
, prevY
);
887 if (drawPoints
|| isIsolated
) {
889 ctx
.fillStyle
= color
;
890 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
,
891 0, 2 * Math
.PI
, false);
896 firstIndexInSet
= afterLastIndexInSet
;