1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Based on PlotKit, but modified to meet the needs of dygraphs.
6 * In particular, support for:
9 * - dygraphs attribute system
13 * Creates a new DygraphLayout object.
14 * @param {Object} options Options for PlotKit.Layout
15 * @return {Object} The DygraphLayout object
17 DygraphLayout
= function(dygraph
, options
) {
18 this.dygraph_
= dygraph
;
19 this.options
= {}; // TODO(danvk): remove, use attr_ instead.
20 Dygraph
.update(this.options
, options
? options
: {});
21 this.datasets
= new Array();
24 DygraphLayout
.prototype.attr_
= function(name
) {
25 return this.dygraph_
.attr_(name
);
28 DygraphLayout
.prototype.addDataset
= function(setname
, set_xy
) {
29 this.datasets
[setname
] = set_xy
;
32 DygraphLayout
.prototype.evaluate
= function() {
33 this._evaluateLimits();
34 this._evaluateLineCharts();
35 this._evaluateLineTicks();
38 DygraphLayout
.prototype._evaluateLimits
= function() {
39 this.minxval
= this.maxxval
= null;
40 if (this.options
.dateWindow
) {
41 this.minxval
= this.options
.dateWindow
[0];
42 this.maxxval
= this.options
.dateWindow
[1];
44 for (var name
in this.datasets
) {
45 if (!this.datasets
.hasOwnProperty(name
)) continue;
46 var series
= this.datasets
[name
];
47 var x1
= series
[0][0];
48 if (!this.minxval
|| x1
< this.minxval
) this.minxval
= x1
;
50 var x2
= series
[series
.length
- 1][0];
51 if (!this.maxxval
|| x2
> this.maxxval
) this.maxxval
= x2
;
54 this.xrange
= this.maxxval
- this.minxval
;
55 this.xscale
= (this.xrange
!= 0 ? 1/this.xrange
: 1.0);
57 this.minyval
= this.options
.yAxis
[0];
58 this.maxyval
= this.options
.yAxis
[1];
59 this.yrange
= this.maxyval
- this.minyval
;
60 this.yscale
= (this.yrange
!= 0 ? 1/this.yrange
: 1.0);
63 DygraphLayout
.prototype._evaluateLineCharts
= function() {
65 this.points
= new Array();
66 for (var setName
in this.datasets
) {
67 if (!this.datasets
.hasOwnProperty(setName
)) continue;
69 var dataset
= this.datasets
[setName
];
70 for (var j
= 0; j
< dataset
.length
; j
++) {
71 var item
= dataset
[j
];
73 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
74 y
: 1.0 - ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
75 xval
: parseFloat(item
[0]),
76 yval
: parseFloat(item
[1]),
80 // limit the x, y values so they do not overdraw
87 if ((point
.x
>= 0.0) && (point
.x
<= 1.0)) {
88 this.points
.push(point
);
94 DygraphLayout
.prototype._evaluateLineTicks
= function() {
95 this.xticks
= new Array();
96 for (var i
= 0; i
< this.options
.xTicks
.length
; i
++) {
97 var tick
= this.options
.xTicks
[i
];
98 var label
= tick
.label
;
99 var pos
= this.xscale
* (tick
.v
- this.minxval
);
100 if ((pos
>= 0.0) && (pos
<= 1.0)) {
101 this.xticks
.push([pos
, label
]);
105 this.yticks
= new Array();
106 for (var i
= 0; i
< this.options
.yTicks
.length
; i
++) {
107 var tick
= this.options
.yTicks
[i
];
108 var label
= tick
.label
;
109 var pos
= 1.0 - (this.yscale
* (tick
.v
- this.minyval
));
110 if ((pos
>= 0.0) && (pos
<= 1.0)) {
111 this.yticks
.push([pos
, label
]);
118 * Behaves the same way as PlotKit.Layout, but also copies the errors
121 DygraphLayout
.prototype.evaluateWithError
= function() {
123 if (!this.options
.errorBars
) return;
125 // Copy over the error terms
126 var i
= 0; // index in this.points
127 for (var setName
in this.datasets
) {
128 if (!this.datasets
.hasOwnProperty(setName
)) continue;
130 var dataset
= this.datasets
[setName
];
131 for (var j
= 0; j
< dataset
.length
; j
++, i
++) {
132 var item
= dataset
[j
];
133 var xv
= parseFloat(item
[0]);
134 var yv
= parseFloat(item
[1]);
136 if (xv
== this.points
[i
].xval
&&
137 yv
== this.points
[i
].yval
) {
138 this.points
[i
].errorMinus
= parseFloat(item
[2]);
139 this.points
[i
].errorPlus
= parseFloat(item
[3]);
146 * Convenience function to remove all the data sets from a graph
148 DygraphLayout
.prototype.removeAllDatasets
= function() {
149 delete this.datasets
;
150 this.datasets
= new Array();
154 * Change the values of various layout options
155 * @param {Object} new_options an associative array of new properties
157 DygraphLayout
.prototype.updateOptions
= function(new_options
) {
158 Dygraph
.update(this.options
, new_options
? new_options
: {});
161 // Subclass PlotKit.CanvasRenderer to add:
162 // 1. X/Y grid overlay
163 // 2. Ability to draw error bars (if required)
166 * Sets some PlotKit.CanvasRenderer options
167 * @param {Object} element The canvas to attach to
168 * @param {Layout} layout The DygraphLayout object for this graph.
169 * @param {Object} options Options to pass on to CanvasRenderer
171 DygraphCanvasRenderer
= function(dygraph
, element
, layout
, options
) {
172 // TODO(danvk): remove options, just use dygraph.attr_.
173 this.dygraph_
= dygraph
;
180 "axisLineColor": "black",
181 "axisLineWidth": 0.5,
183 "axisLabelColor": "black",
184 "axisLabelFont": "Arial",
185 "axisLabelFontSize": 9,
186 "axisLabelWidth": 50,
189 "gridLineColor": "rgb(128,128,128)"
191 Dygraph
.update(this.options
, options
);
193 this.layout
= layout
;
194 this.element
= element
;
195 this.container
= this.element
.parentNode
;
197 this.height
= this.element
.height
;
198 this.width
= this.element
.width
;
200 // --- check whether everything is ok before we return
201 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
202 throw "Canvas is not supported.";
205 this.xlabels
= new Array();
206 this.ylabels
= new Array();
209 x
: this.options
.yAxisLabelWidth
+ 2 * this.options
.axisTickSize
,
212 this.area
.w
= this.width
- this.area
.x
- this.options
.rightGap
;
213 this.area
.h
= this.height
- this.options
.axisLabelFontSize
-
214 2 * this.options
.axisTickSize
;
216 this.container
.style
.position
= "relative";
217 this.container
.style
.width
= this.width
+ "px";
220 DygraphCanvasRenderer
.prototype.clear
= function() {
222 // VML takes a while to start up, so we just poll every this.IEDelay
224 if (this.clearDelay
) {
225 this.clearDelay
.cancel();
226 this.clearDelay
= null;
228 var context
= this.element
.getContext("2d");
231 // TODO(danvk): this is broken, since MochiKit.Async is gone.
232 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
233 this.clearDelay
.addCallback(bind(this.clear
, this));
238 var context
= this.element
.getContext("2d");
239 context
.clearRect(0, 0, this.width
, this.height
);
241 for (var i
= 0; i
< this.xlabels
.length
; i
++) {
242 var el
= this.xlabels
[i
];
243 el
.parentNode
.removeChild(el
);
245 for (var i
= 0; i
< this.ylabels
.length
; i
++) {
246 var el
= this.ylabels
[i
];
247 el
.parentNode
.removeChild(el
);
249 this.xlabels
= new Array();
250 this.ylabels
= new Array();
254 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
257 if (typeof(canvasName
) == 'undefined' || canvasName
== null)
258 canvas
= document
.createElement("canvas");
261 var context
= canvas
.getContext("2d");
264 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
265 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
266 if ((!ie
) || (ie
[1] < 6) || (opera
))
274 * Draw an X/Y grid on top of the existing plot
276 DygraphCanvasRenderer
.prototype.render
= function() {
277 // Draw the new X/Y grid
278 var ctx
= this.element
.getContext("2d");
279 if (this.options
.drawYGrid
) {
280 var ticks
= this.layout
.yticks
;
282 ctx
.strokeStyle
= this.options
.gridLineColor
;
283 ctx
.lineWidth
= this.options
.axisLineWidth
;
284 for (var i
= 0; i
< ticks
.length
; i
++) {
286 var y
= this.area
.y
+ ticks
[i
][0] * this.area
.h
;
289 ctx
.lineTo(x
+ this.area
.w
, y
);
295 if (this.options
.drawXGrid
) {
296 var ticks
= this.layout
.xticks
;
298 ctx
.strokeStyle
= this.options
.gridLineColor
;
299 ctx
.lineWidth
= this.options
.axisLineWidth
;
300 for (var i
=0; i
<ticks
.length
; i
++) {
301 var x
= this.area
.x
+ ticks
[i
][0] * this.area
.w
;
302 var y
= this.area
.y
+ this.area
.h
;
305 ctx
.lineTo(x
, this.area
.y
);
311 // Do the ordinary rendering, as before
312 this._renderLineChart();
317 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
318 if (!this.options
.drawXAxis
&& !this.options
.drawYAxis
)
321 var context
= this.element
.getContext("2d");
324 "position": "absolute",
325 "fontSize": this.options
.axisLabelFontSize
+ "px",
327 "color": this.options
.axisLabelColor
,
328 "width": this.options
.axisLabelWidth
+ "px",
331 var makeDiv
= function(txt
) {
332 var div
= document
.createElement("div");
333 for (var name
in labelStyle
) {
334 if (labelStyle
.hasOwnProperty(name
)) {
335 div
.style
[name
] = labelStyle
[name
];
338 div
.appendChild(document
.createTextNode(txt
));
344 context
.strokeStyle
= this.options
.axisLineColor
;
345 context
.lineWidth
= this.options
.axisLineWidth
;
347 if (this.options
.drawYAxis
) {
348 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
349 for (var i
= 0; i
< this.layout
.yticks
.length
; i
++) {
350 var tick
= this.layout
.yticks
[i
];
351 if (typeof(tick
) == "function") return;
353 var y
= this.area
.y
+ tick
[0] * this.area
.h
;
355 context
.moveTo(x
, y
);
356 context
.lineTo(x
- this.options
.axisTickSize
, y
);
360 var label
= makeDiv(tick
[1]);
361 var top
= (y
- this.options
.axisLabelFontSize
/ 2);
362 if (top
< 0) top
= 0;
364 if (top
+ this.options
.axisLabelFontSize
+ 3 > this.height
) {
365 label
.style
.bottom
= "0px";
367 label
.style
.top
= top
+ "px";
369 label
.style
.left
= "0px";
370 label
.style
.textAlign
= "right";
371 label
.style
.width
= this.options
.yAxisLabelWidth
+ "px";
372 this.container
.appendChild(label
);
373 this.ylabels
.push(label
);
376 // The lowest tick on the y-axis often overlaps with the leftmost
377 // tick on the x-axis. Shift the bottom tick up a little bit to
378 // compensate if necessary.
379 var bottomTick
= this.ylabels
[0];
380 var fontSize
= this.options
.axisLabelFontSize
;
381 var bottom
= parseInt(bottomTick
.style
.top
) + fontSize
;
382 if (bottom
> this.height
- fontSize
) {
383 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
) -
384 fontSize
/ 2) + "px";
389 context
.moveTo(this.area
.x
, this.area
.y
);
390 context
.lineTo(this.area
.x
, this.area
.y
+ this.area
.h
);
395 if (this.options
.drawXAxis
) {
396 if (this.layout
.xticks
) {
397 for (var i
= 0; i
< this.layout
.xticks
.length
; i
++) {
398 var tick
= this.layout
.xticks
[i
];
399 if (typeof(dataset
) == "function") return;
401 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
402 var y
= this.area
.y
+ this.area
.h
;
404 context
.moveTo(x
, y
);
405 context
.lineTo(x
, y
+ this.options
.axisTickSize
);
409 var label
= makeDiv(tick
[1]);
410 label
.style
.textAlign
= "center";
411 label
.style
.bottom
= "0px";
413 var left
= (x
- this.options
.axisLabelWidth
/2);
414 if (left
+ this.options
.axisLabelWidth
> this.width
) {
415 left
= this.width
- this.options
.xAxisLabelWidth
;
416 label
.style
.textAlign
= "right";
420 label
.style
.textAlign
= "left";
423 label
.style
.left
= left
+ "px";
424 label
.style
.width
= this.options
.xAxisLabelWidth
+ "px";
425 this.container
.appendChild(label
);
426 this.xlabels
.push(label
);
431 context
.moveTo(this.area
.x
, this.area
.y
+ this.area
.h
);
432 context
.lineTo(this.area
.x
+ this.area
.w
, this.area
.y
+ this.area
.h
);
442 * Overrides the CanvasRenderer method to draw error bars
444 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
445 var context
= this.element
.getContext("2d");
446 var colorCount
= this.options
.colorScheme
.length
;
447 var colorScheme
= this.options
.colorScheme
;
448 var errorBars
= this.layout
.options
.errorBars
;
449 var fillGraph
= this.layout
.options
.fillGraph
;
452 for (var name
in this.layout
.datasets
) {
453 if (this.layout
.datasets
.hasOwnProperty(name
)) {
457 var setCount
= setNames
.length
;
460 for (var i
= 0; i
< this.layout
.points
.length
; i
++) {
461 var point
= this.layout
.points
[i
];
462 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
463 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
467 var isOK
= function(x
) { return x
&& !isNaN(x
); };
472 this.dygraph_
.warn("Can't use fillGraph option with error bars");
475 for (var i
= 0; i
< setCount
; i
++) {
476 var setName
= setNames
[i
];
477 var color
= colorScheme
[i
% colorCount
];
479 // setup graphics context
481 ctx
.strokeStyle
= color
;
482 ctx
.lineWidth
= this.options
.strokeWidth
;
484 var prevYs
= [-1, -1];
486 var yscale
= this.layout
.yscale
;
487 // should be same color as the lines but only 15% opaque.
488 var rgb
= new RGBColor(color
);
489 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',0.15)';
490 ctx
.fillStyle
= err_color
;
492 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
493 var point
= this.layout
.points
[j
];
495 if (point
.name
== setName
) {
496 if (!isOK(point
.y
)) {
500 var newYs
= [ point
.y
- point
.errorPlus
* yscale
,
501 point
.y
+ point
.errorMinus
* yscale
];
502 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
503 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
505 ctx
.moveTo(prevX
, prevYs
[0]);
506 ctx
.lineTo(point
.canvasx
, newYs
[0]);
507 ctx
.lineTo(point
.canvasx
, newYs
[1]);
508 ctx
.lineTo(prevX
, prevYs
[1]);
511 prevYs
[0] = newYs
[0];
512 prevYs
[1] = newYs
[1];
513 prevX
= point
.canvasx
;
518 } else if (fillGraph
) {
519 for (var i
= 0; i
< setCount
; i
++) {
520 var setName
= setNames
[i
];
522 if (i
>0) setNameLast
= setNames
[i
-1];
523 var color
= colorScheme
[i
% colorCount
];
525 // setup graphics context
527 ctx
.strokeStyle
= color
;
528 ctx
.lineWidth
= this.options
.strokeWidth
;
530 var prevYs
= [-1, -1];
532 var yscale
= this.layout
.yscale
;
533 // should be same color as the lines but only 15% opaque.
534 var rgb
= new RGBColor(color
);
535 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',0.15)';
536 ctx
.fillStyle
= err_color
;
538 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
539 var point
= this.layout
.points
[j
];
541 if (point
.name
== setName
) {
542 if (!isOK(point
.y
)) {
546 var pX
= 1.0 + this.layout
.minyval
* this.layout
.yscale
;
547 if (pX
< 0.0) pX
= 0.0;
548 else if (pX
> 1.0) pX
= 1.0;
549 var newYs
= [ point
.y
, pX
];
550 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
551 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
553 ctx
.moveTo(prevX
, prevYs
[0]);
554 ctx
.lineTo(point
.canvasx
, newYs
[0]);
555 ctx
.lineTo(point
.canvasx
, newYs
[1]);
556 ctx
.lineTo(prevX
, prevYs
[1]);
559 prevYs
[0] = newYs
[0];
560 prevYs
[1] = newYs
[1];
561 prevX
= point
.canvasx
;
568 for (var i
= 0; i
< setCount
; i
++) {
569 var setName
= setNames
[i
];
570 var color
= colorScheme
[i
%colorCount
];
572 // setup graphics context
574 var point
= this.layout
.points
[0];
575 var pointSize
= this.dygraph_
.attr_("pointSize");
576 var prevX
= null, prevY
= null;
577 var drawPoints
= this.dygraph_
.attr_("drawPoints");
578 var points
= this.layout
.points
;
579 for (var j
= 0; j
< points
.length
; j
++) {
580 var point
= points
[j
];
581 if (point
.name
== setName
) {
582 if (!isOK(point
.canvasy
)) {
583 // this will make us move to the next point, not draw a line to it.
584 prevX
= prevY
= null;
586 // A point is "isolated" if it is non-null but both the previous
587 // and next points are null.
588 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
589 !isOK(points
[j
+1].canvasy
)));
592 prevX
= point
.canvasx
;
593 prevY
= point
.canvasy
;
596 ctx
.strokeStyle
= color
;
597 ctx
.lineWidth
= this.options
.strokeWidth
;
598 ctx
.moveTo(prevX
, prevY
);
599 prevX
= point
.canvasx
;
600 prevY
= point
.canvasy
;
601 ctx
.lineTo(prevX
, prevY
);
605 if (drawPoints
|| isIsolated
) {
607 ctx
.fillStyle
= color
;
608 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
,
609 0, 2 * Math
.PI
, false);