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.";
68 this.area
= layout
.getPlotArea();
69 this.container
.style
.position
= "relative";
70 this.container
.style
.width
= this.width
+ "px";
72 // Set up a clipping area for the canvas (and the interaction canvas).
73 // This ensures that we don't overdraw.
74 if (this.dygraph_
.isUsingExcanvas_
) {
75 this._createIEClipArea();
77 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
78 // displaying anything.
79 if (!Dygraph
.isAndroid()) {
80 var ctx
= this.dygraph_
.canvas_ctx_
;
82 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
85 ctx
= this.dygraph_
.hidden_ctx_
;
87 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
93 DygraphCanvasRenderer
.prototype.attr_
= function(x
) {
94 return this.dygraph_
.attr_(x
);
98 * Clears out all chart content and DOM elements.
99 * This is called immediately before render() on every frame, including
100 * during zooms and pans.
103 DygraphCanvasRenderer
.prototype.clear
= function() {
106 // VML takes a while to start up, so we just poll every this.IEDelay
108 if (this.clearDelay
) {
109 this.clearDelay
.cancel();
110 this.clearDelay
= null;
112 context
= this.elementContext
;
115 // TODO(danvk): this is broken, since MochiKit.Async is gone.
116 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
117 // this.clearDelay.addCallback(bind(this.clear, this));
122 context
= this.elementContext
;
123 context
.clearRect(0, 0, this.width
, this.height
);
125 function removeArray(ary
) {
126 for (var i
= 0; i
< ary
.length
; i
++) {
128 if (el
.parentNode
) el
.parentNode
.removeChild(el
);
132 removeArray(this.xlabels
);
133 removeArray(this.ylabels
);
140 * Checks whether the browser supports the <canvas> tag.
143 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
146 if (typeof(canvasName
) == 'undefined' || canvasName
=== null) {
147 canvas
= document
.createElement("canvas");
151 canvas
.getContext("2d");
154 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
155 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
156 if ((!ie
) || (ie
[1] < 6) || (opera
))
164 * @param { [String] } colors Array of color strings. Should have one entry for
165 * each series to be rendered.
167 DygraphCanvasRenderer
.prototype.setColors
= function(colors
) {
168 this.colorScheme_
= colors
;
172 * This method is responsible for drawing everything on the chart, including
173 * lines, error bars, fills and axes.
174 * It is called immediately after clear() on every frame, including during pans
178 DygraphCanvasRenderer
.prototype.render
= function() {
179 // Draw the new X/Y grid
. Lines appear crisper when pixels are rounded to
180 // half-integers. This prevents them from drawing in two rows/cols.
181 var ctx
= this.elementContext
;
182 function halfUp(x
) { return Math
.round(x
) + 0.5; }
183 function halfDown(y
){ return Math
.round(y
) - 0.5; }
185 if (this.attr_('underlayCallback')) {
186 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
187 // users who expect a deprecated form of this callback.
188 this.attr_('underlayCallback')(ctx
, this.area
, this.dygraph_
, this.dygraph_
);
192 if (this.attr_('drawYGrid')) {
193 ticks
= this.layout
.yticks
;
194 // TODO(konigsberg): I don't think these calls to save() have a corresponding restore().
196 ctx
.strokeStyle
= this.attr_('gridLineColor');
197 ctx
.lineWidth
= this.attr_('gridLineWidth');
198 for (i
= 0; i
< ticks
.length
; i
++) {
199 // TODO(danvk): allow secondary axes to draw a grid, too.
200 if (ticks
[i
][0] !== 0) continue;
201 x
= halfUp(this.area
.x
);
202 y
= halfDown(this.area
.y
+ ticks
[i
][1] * this.area
.h
);
205 ctx
.lineTo(x
+ this.area
.w
, y
);
212 if (this.attr_('drawXGrid')) {
213 ticks
= this.layout
.xticks
;
215 ctx
.strokeStyle
= this.attr_('gridLineColor');
216 ctx
.lineWidth
= this.attr_('gridLineWidth');
217 for (i
=0; i
<ticks
.length
; i
++) {
218 x
= halfUp(this.area
.x
+ ticks
[i
][0] * this.area
.w
);
219 y
= halfDown(this.area
.y
+ this.area
.h
);
222 ctx
.lineTo(x
, this.area
.y
);
229 // Do the ordinary rendering, as before
230 this._renderLineChart();
231 // this._renderAxis();
234 DygraphCanvasRenderer
.prototype._createIEClipArea
= function() {
235 var className
= 'dygraph-clip-div';
236 var graphDiv
= this.dygraph_
.graphDiv
;
238 // Remove old clip divs.
239 for (var i
= graphDiv
.childNodes
.length
-1; i
>= 0; i
--) {
240 if (graphDiv
.childNodes
[i
].className
== className
) {
241 graphDiv
.removeChild(graphDiv
.childNodes
[i
]);
245 // Determine background color to give clip divs.
246 var backgroundColor
= document
.bgColor
;
247 var element
= this.dygraph_
.graphDiv
;
248 while (element
!= document
) {
249 var bgcolor
= element
.currentStyle
.backgroundColor
;
250 if (bgcolor
&& bgcolor
!= 'transparent') {
251 backgroundColor
= bgcolor
;
254 element
= element
.parentNode
;
257 function createClipDiv(area
) {
258 if (area
.w
=== 0 || area
.h
=== 0) {
261 var elem
= document
.createElement('div');
262 elem
.className
= className
;
263 elem
.style
.backgroundColor
= backgroundColor
;
264 elem
.style
.position
= 'absolute';
265 elem
.style
.left
= area
.x
+ 'px';
266 elem
.style
.top
= area
.y
+ 'px';
267 elem
.style
.width
= area
.w
+ 'px';
268 elem
.style
.height
= area
.h
+ 'px';
269 graphDiv
.appendChild(elem
);
272 var plotArea
= this.area
;
283 w
: this.width
- plotArea
.x
,
289 x
: plotArea
.x
+ plotArea
.w
, y
: 0,
290 w
: this.width
-plotArea
.x
- plotArea
.w
,
297 y
: plotArea
.y
+ plotArea
.h
,
298 w
: this.width
- plotArea
.x
,
299 h
: this.height
- plotArea
.h
- plotArea
.y
303 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
304 if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return;
306 // Round pixels to half-integer boundaries for crisper drawing.
307 function halfUp(x
) { return Math
.round(x
) + 0.5; }
308 function halfDown(y
){ return Math
.round(y
) - 0.5; }
310 var context
= this.elementContext
;
312 var label
, x
, y
, tick
, i
;
315 position
: "absolute",
316 fontSize
: this.attr_('axisLabelFontSize') + "px",
318 color
: this.attr_('axisLabelColor'),
319 width
: this.attr_('axisLabelWidth') + "px",
320 // height: this.attr_('axisLabelFontSize') + 2 + "px",
321 lineHeight
: "normal", // Something other than "normal" line-height screws up label positioning.
324 var makeDiv
= function(txt
, axis
, prec_axis
) {
325 var div
= document
.createElement("div");
326 for (var name
in labelStyle
) {
327 if (labelStyle
.hasOwnProperty(name
)) {
328 div
.style
[name
] = labelStyle
[name
];
331 var inner_div
= document
.createElement("div");
332 inner_div
.className
= 'dygraph-axis-label' +
333 ' dygraph-axis-label-' + axis
+
334 (prec_axis
? ' dygraph-axis-label-' + prec_axis
: '');
335 inner_div
.innerHTML
=txt
;
336 div
.appendChild(inner_div
);
342 context
.strokeStyle
= this.attr_('axisLineColor');
343 context
.lineWidth
= this.attr_('axisLineWidth');
345 if (this.attr_('drawYAxis')) {
346 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
347 var num_axes
= this.dygraph_
.numAxes();
348 for (i
= 0; i
< this.layout
.yticks
.length
; i
++) {
349 tick
= this.layout
.yticks
[i
];
350 if (typeof(tick
) == "function") return;
353 var prec_axis
= 'y1';
354 if (tick
[0] == 1) { // right-side y-axis
355 x
= this.area
.x
+ this.area
.w
;
359 y
= this.area
.y
+ tick
[1] * this.area
.h
;
361 /* Tick marks are currently clipped, so don't bother drawing them.
363 context.moveTo(halfUp(x), halfDown(y));
364 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
369 label
= makeDiv(tick
[2], 'y', num_axes
== 2 ? prec_axis
: null);
370 var top
= (y
- this.attr_('axisLabelFontSize') / 2);
371 if (top
< 0) top
= 0;
373 if (top
+ this.attr_('axisLabelFontSize') + 3 > this.height
) {
374 label
.style
.bottom
= "0px";
376 label
.style
.top
= top
+ "px";
379 label
.style
.left
= (this.area
.x
- this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px";
380 label
.style
.textAlign
= "right";
381 } else if (tick
[0] == 1) {
382 label
.style
.left
= (this.area
.x
+ this.area
.w
+
383 this.attr_('axisTickSize')) + "px";
384 label
.style
.textAlign
= "left";
386 label
.style
.width
= this.attr_('yAxisLabelWidth') + "px";
387 this.container
.appendChild(label
);
388 this.ylabels
.push(label
);
391 // The lowest tick on the y-axis often overlaps with the leftmost
392 // tick on the x-axis. Shift the bottom tick up a little bit to
393 // compensate if necessary.
394 var bottomTick
= this.ylabels
[0];
395 var fontSize
= this.attr_('axisLabelFontSize');
396 var bottom
= parseInt(bottomTick
.style
.top
, 10) + fontSize
;
397 if (bottom
> this.height
- fontSize
) {
398 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
, 10) -
399 fontSize
/ 2) + "px";
403 // draw a vertical line on the left to separate the chart from the labels.
405 if (this.attr_('drawAxesAtZero')) {
406 var r
= this.dygraph_
.toPercentXCoord(0);
407 if (r
> 1 || r
< 0) r
= 0;
408 axisX
= halfUp(this.area
.x
+ r
* this.area
.w
);
410 axisX
= halfUp(this.area
.x
);
413 context
.moveTo(axisX
, halfDown(this.area
.y
));
414 context
.lineTo(axisX
, halfDown(this.area
.y
+ this.area
.h
));
418 // if there's a secondary y-axis, draw a vertical line for that, too.
419 if (this.dygraph_
.numAxes() == 2) {
421 context
.moveTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
));
422 context
.lineTo(halfDown(this.area
.x
+ this.area
.w
), halfDown(this.area
.y
+ this.area
.h
));
428 if (this.attr_('drawXAxis')) {
429 if (this.layout
.xticks
) {
430 for (i
= 0; i
< this.layout
.xticks
.length
; i
++) {
431 tick
= this.layout
.xticks
[i
];
432 x
= this.area
.x
+ tick
[0] * this.area
.w
;
433 y
= this.area
.y
+ this.area
.h
;
435 /* Tick marks are currently clipped, so don't bother drawing them.
437 context.moveTo(halfUp(x), halfDown(y));
438 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
443 label
= makeDiv(tick
[1], 'x');
444 label
.style
.textAlign
= "center";
445 label
.style
.top
= (y
+ this.attr_('axisTickSize')) + 'px';
447 var left
= (x
- this.attr_('axisLabelWidth')/2);
448 if (left
+ this.attr_('axisLabelWidth') > this.width
) {
449 left
= this.width
- this.attr_('xAxisLabelWidth');
450 label
.style
.textAlign
= "right";
454 label
.style
.textAlign
= "left";
457 label
.style
.left
= left
+ "px";
458 label
.style
.width
= this.attr_('xAxisLabelWidth') + "px";
459 this.container
.appendChild(label
);
460 this.xlabels
.push(label
);
466 if (this.attr_('drawAxesAtZero')) {
467 var r
= this.dygraph_
.toPercentYCoord(0, 0);
468 if (r
> 1 || r
< 0) r
= 1;
469 axisY
= halfDown(this.area
.y
+ r
* this.area
.h
);
471 axisY
= halfDown(this.area
.y
+ this.area
.h
);
473 context
.moveTo(halfUp(this.area
.x
), axisY
);
474 context
.lineTo(halfUp(this.area
.x
+ this.area
.w
), axisY
);
484 * Returns a predicate to be used with an iterator, which will
485 * iterate over points appropriately, depending on whether
486 * connectSeparatedPoints is true. When it's false, the predicate will
487 * skip over points with missing yVals.
489 DygraphCanvasRenderer
._getIteratorPredicate
= function(connectSeparatedPoints
) {
490 return connectSeparatedPoints
? DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
: null;
493 DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
=
494 function(array
, idx
) { return array
[idx
].yval
!== null; }
496 DygraphCanvasRenderer
.prototype._drawStyledLine
= function(
497 ctx
, i
, setName
, color
, strokeWidth
, strokePattern
, drawPoints
,
498 drawPointCallback
, pointSize
) {
499 // TODO(konigsberg): Compute attributes outside this method call.
500 var stepPlot
= this.attr_("stepPlot");
501 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
502 var setLength
= this.layout
.setPointsLengths
[i
];
503 var points
= this.layout
.points
;
504 if (!Dygraph
.isArrayLike(strokePattern
)) {
505 strokePattern
= null;
507 var drawGapPoints
= this.dygraph_
.attr_('drawGapEdgePoints', setName
);
511 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
512 DygraphCanvasRenderer
._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
516 if (!strokePattern
|| strokePattern
.length
<= 1) {
517 strategy
= trivialStrategy(ctx
, color
, strokeWidth
);
519 strategy
= nonTrivialStrategy(this, ctx
, color
, strokeWidth
, strokePattern
);
521 pointsOnLine
= this._drawSeries(ctx
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
, strategy
);
522 this._drawPointsOnLine(ctx
, pointsOnLine
, drawPointCallback
, setName
, color
, pointSize
);
527 var nonTrivialStrategy
= function(renderer
, ctx
, color
, strokeWidth
, strokePattern
) {
528 return new function() {
529 this.init
= function() { };
530 this.finish
= function() { };
531 this.startSegment
= function() {
533 ctx
.strokeStyle
= color
;
534 ctx
.lineWidth
= strokeWidth
;
536 this.endSegment
= function() {
537 ctx
.stroke(); // should this include closePath?
539 this.drawLine
= function(x1
, y1
, x2
, y2
) {
540 renderer
._dashedLine(ctx
, x1
, y1
, x2
, y2
, strokePattern
);
542 this.skipPixel
= function(prevX
, prevY
, curX
, curY
) {
543 // TODO(konigsberg): optimize with http://jsperf.com/math-round
-vs
-hack
/6 ?
544 return (Math
.round(prevX
) == Math
.round(curX
) &&
545 Math
.round(prevY
) == Math
.round(curY
));
550 var trivialStrategy
= function(ctx
, color
, strokeWidth
) {
551 return new function() {
552 this.init
= function() {
554 ctx
.strokeStyle
= color
;
555 ctx
.lineWidth
= strokeWidth
;
557 this.finish
= function() {
558 ctx
.stroke(); // should this include closePath?
560 this.startSegment
= function() { };
561 this.endSegment
= function() { };
562 this.drawLine
= function(x1
, y1
, x2
, y2
) {
566 // don't skip pixels.
567 this.skipPixel
= function() {
573 DygraphCanvasRenderer
.prototype._drawPointsOnLine
= function(ctx
, pointsOnLine
, drawPointCallback
, setName
, color
, pointSize
) {
574 for (var idx
= 0; idx
< pointsOnLine
.length
; idx
++) {
575 var cb
= pointsOnLine
[idx
];
578 this.dygraph_
, setName
, ctx
, cb
[0], cb
[1], color
, pointSize
);
583 DygraphCanvasRenderer
.prototype._drawSeries
= function(
584 ctx
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
,
585 stepPlot
, strategy
) {
587 var prevCanvasX
= null;
588 var prevCanvasY
= null;
589 var nextCanvasY
= null;
590 var isIsolated
; // true if this point is isolated (no line segments)
591 var point
; // the point being processed in the while loop
592 var pointsOnLine
= []; // Array of [canvasx, canvasy] pairs.
593 var first
= true; // the first cycle through the while loop
597 while(iter
.hasNext()) {
599 if (point
.canvasy
=== null || point
.canvasy
!= point
.canvasy
) {
600 if (stepPlot
&& prevCanvasX
!== null) {
601 // Draw a horizontal line to the start of the missing data
602 strategy
.startSegment();
603 strategy
.drawLine(prevX
, prevY
, point
.canvasx
, prevY
);
604 strategy
.endSegment();
606 prevCanvasX
= prevCanvasY
= null;
608 nextCanvasY
= iter
.hasNext() ? iter
.peek().canvasy
: null;
609 // TODO: we calculate isNullOrNaN for this point, and the next, and then, when
610 // we iterate, test for isNullOrNaN again. Why bother?
611 var isNextCanvasYNullOrNaN
= nextCanvasY
=== null || nextCanvasY
!= nextCanvasY
;
612 isIsolated
= (!prevCanvasX
&& isNextCanvasYNullOrNaN
);
614 // Also consider a point to be "isolated" if it's adjacent to a
615 // null point, excluding the graph edges.
616 if ((!first
&& !prevCanvasX
) ||
617 (iter
.hasNext() && isNextCanvasYNullOrNaN
)) {
621 if (prevCanvasX
!== null) {
622 if (strategy
.skipPixel(prevCanvasX
, prevCanvasY
, point
.canvasx
, point
.canvasy
)) {
626 strategy
.startSegment();
628 strategy
.drawLine(prevCanvasX
, prevCanvasY
, point
.canvasx
, prevCanvasY
);
629 prevCanvasX
= point
.canvasx
;
631 strategy
.drawLine(prevCanvasX
, prevCanvasY
, point
.canvasx
, point
.canvasy
);
632 strategy
.endSegment();
635 if (drawPoints
|| isIsolated
) {
636 pointsOnLine
.push([point
.canvasx
, point
.canvasy
]);
638 prevCanvasX
= point
.canvasx
;
639 prevCanvasY
= point
.canvasy
;
647 DygraphCanvasRenderer
.prototype._drawLine
= function(ctx
, i
) {
648 var setNames
= this.layout
.setNames
;
649 var setName
= setNames
[i
];
651 var strokeWidth
= this.dygraph_
.attr_("strokeWidth", setName
);
652 var borderWidth
= this.dygraph_
.attr_("strokeBorderWidth", setName
);
653 var drawPointCallback
= this.dygraph_
.attr_("drawPointCallback", setName
) ||
654 Dygraph
.Circles
.DEFAULT
;
656 if (borderWidth
&& strokeWidth
) {
657 this._drawStyledLine(ctx
, i
, setName
,
658 this.dygraph_
.attr_("strokeBorderColor", setName
),
659 strokeWidth
+ 2 * borderWidth
,
660 this.dygraph_
.attr_("strokePattern", setName
),
661 this.dygraph_
.attr_("drawPoints", setName
),
663 this.dygraph_
.attr_("pointSize", setName
));
666 this._drawStyledLine(ctx
, i
, setName
,
667 this.colors
[setName
],
669 this.dygraph_
.attr_("strokePattern", setName
),
670 this.dygraph_
.attr_("drawPoints", setName
),
672 this.dygraph_
.attr_("pointSize", setName
));
676 * Actually draw the lines chart, including error bars.
677 * TODO(danvk): split this into several smaller functions.
680 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
681 // TODO(danvk): use this.attr_ for many of these.
682 var ctx
= this.elementContext
;
683 var fillAlpha
= this.attr_('fillAlpha');
684 var errorBars
= this.attr_("errorBars") || this.attr_("customBars");
685 var fillGraph
= this.attr_("fillGraph");
686 var stackedGraph
= this.attr_("stackedGraph");
687 var stepPlot
= this.attr_("stepPlot");
688 var points
= this.layout
.points
;
689 var pointsLength
= points
.length
;
690 var point
, i
, prevX
, prevY
, prevYs
, color
, setName
, newYs
, err_color
, rgb
, yscale
, axis
;
692 var setNames
= this.layout
.setNames
;
693 var setCount
= setNames
.length
;
695 this.colors
= this.dygraph_
.colorsMap_
;
700 // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
701 // transformations can be pushed into the canvas via linear transformation
703 for (i
= pointsLength
; i
--;) {
705 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
706 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
713 this.dygraph_
.warn("Can't use fillGraph option with error bars");
716 for (i
= 0; i
< setCount
; i
++) {
717 setName
= setNames
[i
];
718 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
719 color
= this.colors
[setName
];
721 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
722 var setLength
= this.layout
.setPointsLengths
[i
];
724 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
725 DygraphCanvasRenderer
._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
727 // setup graphics context
731 yscale
= axis
.yscale
;
732 // should be same color as the lines but only 15% opaque.
733 rgb
= new RGBColor(color
);
734 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
736 ctx
.fillStyle
= err_color
;
738 while (iter
.hasNext()) {
740 if (point
.name
== setName
) { // TODO(klausw): this is always true
741 if (!Dygraph
.isOK(point
.y
)) {
748 newYs
= [ point
.y_bottom
, point
.y_top
];
751 newYs
= [ point
.y_bottom
, point
.y_top
];
753 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
754 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
757 ctx
.moveTo(prevX
, newYs
[0]);
759 ctx
.moveTo(prevX
, prevYs
[0]);
761 ctx
.lineTo(point
.canvasx
, newYs
[0]);
762 ctx
.lineTo(point
.canvasx
, newYs
[1]);
764 ctx
.lineTo(prevX
, newYs
[1]);
766 ctx
.lineTo(prevX
, prevYs
[1]);
771 prevX
= point
.canvasx
;
777 } else if (fillGraph
) {
779 var baseline
= {}; // for stacked graphs: baseline for filling
782 // process sets in reverse order (needed for stacked graphs)
783 for (i
= setCount
- 1; i
>= 0; i
--) {
784 setName
= setNames
[i
];
785 color
= this.colors
[setName
];
786 axis
= this.dygraph_
.axisPropertiesForSeries(setName
);
787 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
788 if (axisY
< 0.0) axisY
= 0.0;
789 else if (axisY
> 1.0) axisY
= 1.0;
790 axisY
= this.area
.h
* axisY
+ this.area
.y
;
791 var firstIndexInSet
= this.layout
.setPointsOffsets
[i
];
792 var setLength
= this.layout
.setPointsLengths
[i
];
794 var iter
= Dygraph
.createIterator(points
, firstIndexInSet
, setLength
,
795 DygraphCanvasRenderer
._getIteratorPredicate(this.attr_("connectSeparatedPoints")));
797 // setup graphics context
800 yscale
= axis
.yscale
;
801 // should be same color as the lines but only 15% opaque.
802 rgb
= new RGBColor(color
);
803 err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
805 ctx
.fillStyle
= err_color
;
807 while(iter
.hasNext()) {
809 if (point
.name
== setName
) { // TODO(klausw): this is always true
810 if (!Dygraph
.isOK(point
.y
)) {
815 currBaseline
= baseline
[point
.canvasx
];
817 if (currBaseline
=== undefined
) {
821 lastY
= currBaseline
[0];
823 lastY
= currBaseline
;
826 newYs
= [ point
.canvasy
, lastY
];
829 // Step plots must keep track of the top and bottom of
830 // the baseline at each point.
831 if(prevYs
[0] === -1) {
832 baseline
[point
.canvasx
] = [ point
.canvasy
, axisY
];
834 baseline
[point
.canvasx
] = [ point
.canvasy
, prevYs
[0] ];
837 baseline
[point
.canvasx
] = point
.canvasy
;
841 newYs
= [ point
.canvasy
, axisY
];
844 ctx
.moveTo(prevX
, prevYs
[0]);
847 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
849 // Draw to the bottom of the baseline
850 ctx
.lineTo(point
.canvasx
, currBaseline
[1]);
852 ctx
.lineTo(point
.canvasx
, newYs
[1]);
855 ctx
.lineTo(point
.canvasx
, newYs
[0]);
856 ctx
.lineTo(point
.canvasx
, newYs
[1]);
859 ctx
.lineTo(prevX
, prevYs
[1]);
863 prevX
= point
.canvasx
;
871 // Drawing the lines.
872 for (i
= 0; i
< setCount
; i
+= 1) {
873 this._drawLine(ctx
, i
);
878 * This does dashed lines onto a canvas for a given pattern. You must call
879 * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
880 * the state of the line in regards to where we left off on drawing the pattern.
881 * You can draw a dashed line in several function calls and the pattern will be
882 * continous as long as you didn't call this function with a different pattern
884 * @param ctx The canvas 2d context to draw on.
885 * @param x The start of the line's x coordinate.
886 * @param y The start of the line's y coordinate.
887 * @param x2 The end of the line's x coordinate.
888 * @param y2 The end of the line's y coordinate.
889 * @param pattern The dash pattern to draw, an array of integers where even
890 * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
891 * is drawn, 2 is the space between.). A null pattern, array of length one, or
892 * empty array will do just a solid line.
895 DygraphCanvasRenderer
.prototype._dashedLine
= function(ctx
, x
, y
, x2
, y2
, pattern
) {
896 // Original version http://stackoverflow.com/questions
/4576724/dotted-stroke
-in-canvas
897 // Modified by Russell Valentine to keep line history and continue the pattern
898 // where it left off.
899 var dx
, dy
, len
, rot
, patternIndex
, segment
;
901 // If we don't have a pattern or it is an empty array or of size one just
903 if (!pattern
|| pattern
.length
<= 1) {
909 // If we have a different dash pattern than the last time this was called we
910 // reset our dash history and start the pattern from the begging
911 // regardless of state of the last pattern.
912 if (!Dygraph
.compareArrays(pattern
, this._dashedLineToHistoryPattern
)) {
913 this._dashedLineToHistoryPattern
= pattern
;
914 this._dashedLineToHistory
= [0, 0];
918 // Calculate transformation parameters
921 len
= Math
.sqrt(dx
*dx
+ dy
*dy
);
922 rot
= Math
.atan2(dy
, dx
);
924 // Set transformation
929 // Set last pattern index we used for this pattern.
930 patternIndex
= this._dashedLineToHistory
[0];
933 // Get the length of the pattern segment we are dealing with.
934 segment
= pattern
[patternIndex
];
935 // If our last draw didn't complete the pattern segment all the way we
936 // will try to finish it. Otherwise we will try to do the whole segment.
937 if (this._dashedLineToHistory
[1]) {
938 x
+= this._dashedLineToHistory
[1];
943 // We were unable to complete this pattern index all the way, keep
944 // where we are the history so our next draw continues where we left off
946 this._dashedLineToHistory
= [patternIndex
, x
-len
];
949 // We completed this patternIndex, we put in the history that we are on
950 // the beginning of the next segment.
951 this._dashedLineToHistory
= [(patternIndex
+1)%pattern
.length
, 0];
954 // We do a line on a even pattern index and just move on a odd pattern index.
955 // The move is the empty space in the dash.
956 if(patternIndex
% 2 === 0) {
961 // If we are not done, next loop process the next pattern segment, or the
962 // first segment again if we are at the end of the pattern.
963 patternIndex
= (patternIndex
+1) % pattern
.length
;