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 // TODO(danvk): CONTRACT remove
33 DygraphLayout
.prototype.addAnnotation
= function() {
34 // Add an annotation to one series.
35 this.annotations
= [];
36 for (var x
= 10; x
< 30; x
+= 2) {
37 this.annotations
.push( {
39 xval
: this.attr_('xValueParser')("200610" + x
),
41 text
: 'Stock Market Crash ' + x
44 this.annotations
.push( {
45 series
: 'another line',
46 xval
: this.attr_('xValueParser')("20061013"),
52 DygraphLayout
.prototype.evaluate
= function() {
53 this._evaluateLimits();
54 this._evaluateLineCharts();
55 this._evaluateLineTicks();
56 this._evaluateAnnotations();
59 DygraphLayout
.prototype._evaluateLimits
= function() {
60 this.minxval
= this.maxxval
= null;
61 if (this.options
.dateWindow
) {
62 this.minxval
= this.options
.dateWindow
[0];
63 this.maxxval
= this.options
.dateWindow
[1];
65 for (var name
in this.datasets
) {
66 if (!this.datasets
.hasOwnProperty(name
)) continue;
67 var series
= this.datasets
[name
];
68 var x1
= series
[0][0];
69 if (!this.minxval
|| x1
< this.minxval
) this.minxval
= x1
;
71 var x2
= series
[series
.length
- 1][0];
72 if (!this.maxxval
|| x2
> this.maxxval
) this.maxxval
= x2
;
75 this.xrange
= this.maxxval
- this.minxval
;
76 this.xscale
= (this.xrange
!= 0 ? 1/this.xrange
: 1.0);
78 this.minyval
= this.options
.yAxis
[0];
79 this.maxyval
= this.options
.yAxis
[1];
80 this.yrange
= this.maxyval
- this.minyval
;
81 this.yscale
= (this.yrange
!= 0 ? 1/this.yrange
: 1.0);
84 DygraphLayout
.prototype._evaluateLineCharts
= function() {
86 this.points
= new Array();
87 for (var setName
in this.datasets
) {
88 if (!this.datasets
.hasOwnProperty(setName
)) continue;
90 var dataset
= this.datasets
[setName
];
91 for (var j
= 0; j
< dataset
.length
; j
++) {
92 var item
= dataset
[j
];
95 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
96 y
: 1.0 - ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
97 xval
: parseFloat(item
[0]),
98 yval
: parseFloat(item
[1]),
102 // limit the x, y values so they do not overdraw
103 if (point
.y
<= 0.0) {
106 if (point
.y
>= 1.0) {
109 this.points
.push(point
);
114 DygraphLayout
.prototype._evaluateLineTicks
= function() {
115 this.xticks
= new Array();
116 for (var i
= 0; i
< this.options
.xTicks
.length
; i
++) {
117 var tick
= this.options
.xTicks
[i
];
118 var label
= tick
.label
;
119 var pos
= this.xscale
* (tick
.v
- this.minxval
);
120 if ((pos
>= 0.0) && (pos
<= 1.0)) {
121 this.xticks
.push([pos
, label
]);
125 this.yticks
= new Array();
126 for (var i
= 0; i
< this.options
.yTicks
.length
; i
++) {
127 var tick
= this.options
.yTicks
[i
];
128 var label
= tick
.label
;
129 var pos
= 1.0 - (this.yscale
* (tick
.v
- this.minyval
));
130 if ((pos
>= 0.0) && (pos
<= 1.0)) {
131 this.yticks
.push([pos
, label
]);
138 * Behaves the same way as PlotKit.Layout, but also copies the errors
141 DygraphLayout
.prototype.evaluateWithError
= function() {
143 if (!this.options
.errorBars
) return;
145 // Copy over the error terms
146 var i
= 0; // index in this.points
147 for (var setName
in this.datasets
) {
148 if (!this.datasets
.hasOwnProperty(setName
)) continue;
150 var dataset
= this.datasets
[setName
];
151 for (var j
= 0; j
< dataset
.length
; j
++, i
++) {
152 var item
= dataset
[j
];
153 var xv
= parseFloat(item
[0]);
154 var yv
= parseFloat(item
[1]);
156 if (xv
== this.points
[i
].xval
&&
157 yv
== this.points
[i
].yval
) {
158 this.points
[i
].errorMinus
= parseFloat(item
[2]);
159 this.points
[i
].errorPlus
= parseFloat(item
[3]);
165 DygraphLayout
.prototype._evaluateAnnotations
= function() {
166 // Add the annotations to the point to which they belong.
167 // Make a map from (setName, xval) to annotation for quick lookups.
168 var annotations
= {};
169 for (var i
= 0; i
< this.annotations
.length
; i
++) {
170 var a
= this.annotations
[i
];
171 annotations
[a
.xval
+ "," + a
.series
] = a
;
174 this.annotated_points
= [];
175 for (var i
= 0; i
< this.points
.length
; i
++) {
176 var p
= this.points
[i
];
177 var k
= p
.xval
+ "," + p
.name
;
178 if (k
in annotations
) {
179 p
.annotation
= annotations
[k
];
180 this.annotated_points
.push(p
);
186 * Convenience function to remove all the data sets from a graph
188 DygraphLayout
.prototype.removeAllDatasets
= function() {
189 delete this.datasets
;
190 this.datasets
= new Array();
194 * Change the values of various layout options
195 * @param {Object} new_options an associative array of new properties
197 DygraphLayout
.prototype.updateOptions
= function(new_options
) {
198 Dygraph
.update(this.options
, new_options
? new_options
: {});
201 // Subclass PlotKit.CanvasRenderer to add:
202 // 1. X/Y grid overlay
203 // 2. Ability to draw error bars (if required)
206 * Sets some PlotKit.CanvasRenderer options
207 * @param {Object} element The canvas to attach to
208 * @param {Layout} layout The DygraphLayout object for this graph.
209 * @param {Object} options Options to pass on to CanvasRenderer
211 DygraphCanvasRenderer
= function(dygraph
, element
, layout
, options
) {
212 // TODO(danvk): remove options, just use dygraph.attr_.
213 this.dygraph_
= dygraph
;
220 "axisLineColor": "black",
221 "axisLineWidth": 0.5,
223 "axisLabelColor": "black",
224 "axisLabelFont": "Arial",
225 "axisLabelFontSize": 9,
226 "axisLabelWidth": 50,
229 "gridLineColor": "rgb(128,128,128)",
231 "underlayCallback": null
233 Dygraph
.update(this.options
, options
);
235 this.layout
= layout
;
236 this.element
= element
;
237 this.container
= this.element
.parentNode
;
239 this.height
= this.element
.height
;
240 this.width
= this.element
.width
;
242 // --- check whether everything is ok before we return
243 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
244 throw "Canvas is not supported.";
247 this.xlabels
= new Array();
248 this.ylabels
= new Array();
249 this.annotations
= new Array();
252 x
: this.options
.yAxisLabelWidth
+ 2 * this.options
.axisTickSize
,
255 this.area
.w
= this.width
- this.area
.x
- this.options
.rightGap
;
256 this.area
.h
= this.height
- this.options
.axisLabelFontSize
-
257 2 * this.options
.axisTickSize
;
259 this.container
.style
.position
= "relative";
260 this.container
.style
.width
= this.width
+ "px";
263 DygraphCanvasRenderer
.prototype.clear
= function() {
265 // VML takes a while to start up, so we just poll every this.IEDelay
267 if (this.clearDelay
) {
268 this.clearDelay
.cancel();
269 this.clearDelay
= null;
271 var context
= this.element
.getContext("2d");
274 // TODO(danvk): this is broken, since MochiKit.Async is gone.
275 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
276 this.clearDelay
.addCallback(bind(this.clear
, this));
281 var context
= this.element
.getContext("2d");
282 context
.clearRect(0, 0, this.width
, this.height
);
284 for (var i
= 0; i
< this.xlabels
.length
; i
++) {
285 var el
= this.xlabels
[i
];
286 el
.parentNode
.removeChild(el
);
288 for (var i
= 0; i
< this.ylabels
.length
; i
++) {
289 var el
= this.ylabels
[i
];
290 el
.parentNode
.removeChild(el
);
292 for (var i
= 0; i
< this.annotations
.length
; i
++) {
293 var el
= this.annotations
[i
];
294 el
.parentNode
.removeChild(el
);
296 this.xlabels
= new Array();
297 this.ylabels
= new Array();
298 this.annotations
= new Array();
302 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
305 if (typeof(canvasName
) == 'undefined' || canvasName
== null)
306 canvas
= document
.createElement("canvas");
309 var context
= canvas
.getContext("2d");
312 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
313 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
314 if ((!ie
) || (ie
[1] < 6) || (opera
))
322 * Draw an X/Y grid on top of the existing plot
324 DygraphCanvasRenderer
.prototype.render
= function() {
325 // Draw the new X/Y grid
326 var ctx
= this.element
.getContext("2d");
328 if (this.options
.underlayCallback
) {
329 this.options
.underlayCallback(ctx
, this.area
, this.layout
, this.dygraph_
);
332 if (this.options
.drawYGrid
) {
333 var ticks
= this.layout
.yticks
;
335 ctx
.strokeStyle
= this.options
.gridLineColor
;
336 ctx
.lineWidth
= this.options
.axisLineWidth
;
337 for (var i
= 0; i
< ticks
.length
; i
++) {
339 var y
= this.area
.y
+ ticks
[i
][0] * this.area
.h
;
342 ctx
.lineTo(x
+ this.area
.w
, y
);
348 if (this.options
.drawXGrid
) {
349 var ticks
= this.layout
.xticks
;
351 ctx
.strokeStyle
= this.options
.gridLineColor
;
352 ctx
.lineWidth
= this.options
.axisLineWidth
;
353 for (var i
=0; i
<ticks
.length
; i
++) {
354 var x
= this.area
.x
+ ticks
[i
][0] * this.area
.w
;
355 var y
= this.area
.y
+ this.area
.h
;
358 ctx
.lineTo(x
, this.area
.y
);
364 // Do the ordinary rendering, as before
365 this._renderLineChart();
367 this._renderAnnotations();
371 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
372 if (!this.options
.drawXAxis
&& !this.options
.drawYAxis
)
375 var context
= this.element
.getContext("2d");
378 "position": "absolute",
379 "fontSize": this.options
.axisLabelFontSize
+ "px",
381 "color": this.options
.axisLabelColor
,
382 "width": this.options
.axisLabelWidth
+ "px",
385 var makeDiv
= function(txt
) {
386 var div
= document
.createElement("div");
387 for (var name
in labelStyle
) {
388 if (labelStyle
.hasOwnProperty(name
)) {
389 div
.style
[name
] = labelStyle
[name
];
392 div
.appendChild(document
.createTextNode(txt
));
398 context
.strokeStyle
= this.options
.axisLineColor
;
399 context
.lineWidth
= this.options
.axisLineWidth
;
401 if (this.options
.drawYAxis
) {
402 if (this.layout
.yticks
&& this.layout
.yticks
.length
> 0) {
403 for (var i
= 0; i
< this.layout
.yticks
.length
; i
++) {
404 var tick
= this.layout
.yticks
[i
];
405 if (typeof(tick
) == "function") return;
407 var y
= this.area
.y
+ tick
[0] * this.area
.h
;
409 context
.moveTo(x
, y
);
410 context
.lineTo(x
- this.options
.axisTickSize
, y
);
414 var label
= makeDiv(tick
[1]);
415 var top
= (y
- this.options
.axisLabelFontSize
/ 2);
416 if (top
< 0) top
= 0;
418 if (top
+ this.options
.axisLabelFontSize
+ 3 > this.height
) {
419 label
.style
.bottom
= "0px";
421 label
.style
.top
= top
+ "px";
423 label
.style
.left
= "0px";
424 label
.style
.textAlign
= "right";
425 label
.style
.width
= this.options
.yAxisLabelWidth
+ "px";
426 this.container
.appendChild(label
);
427 this.ylabels
.push(label
);
430 // The lowest tick on the y-axis often overlaps with the leftmost
431 // tick on the x-axis. Shift the bottom tick up a little bit to
432 // compensate if necessary.
433 var bottomTick
= this.ylabels
[0];
434 var fontSize
= this.options
.axisLabelFontSize
;
435 var bottom
= parseInt(bottomTick
.style
.top
) + fontSize
;
436 if (bottom
> this.height
- fontSize
) {
437 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
) -
438 fontSize
/ 2) + "px";
443 context
.moveTo(this.area
.x
, this.area
.y
);
444 context
.lineTo(this.area
.x
, this.area
.y
+ this.area
.h
);
449 if (this.options
.drawXAxis
) {
450 if (this.layout
.xticks
) {
451 for (var i
= 0; i
< this.layout
.xticks
.length
; i
++) {
452 var tick
= this.layout
.xticks
[i
];
453 if (typeof(dataset
) == "function") return;
455 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
456 var y
= this.area
.y
+ this.area
.h
;
458 context
.moveTo(x
, y
);
459 context
.lineTo(x
, y
+ this.options
.axisTickSize
);
463 var label
= makeDiv(tick
[1]);
464 label
.style
.textAlign
= "center";
465 label
.style
.bottom
= "0px";
467 var left
= (x
- this.options
.axisLabelWidth
/2);
468 if (left
+ this.options
.axisLabelWidth
> this.width
) {
469 left
= this.width
- this.options
.xAxisLabelWidth
;
470 label
.style
.textAlign
= "right";
474 label
.style
.textAlign
= "left";
477 label
.style
.left
= left
+ "px";
478 label
.style
.width
= this.options
.xAxisLabelWidth
+ "px";
479 this.container
.appendChild(label
);
480 this.xlabels
.push(label
);
485 context
.moveTo(this.area
.x
, this.area
.y
+ this.area
.h
);
486 context
.lineTo(this.area
.x
+ this.area
.w
, this.area
.y
+ this.area
.h
);
495 DygraphCanvasRenderer
.prototype._renderAnnotations
= function() {
496 var annotationStyle
= {
497 "position": "absolute",
498 "fontSize": this.options
.axisLabelFontSize
+ "px",
501 "overflow": "hidden",
502 "border": "1px solid black",
503 "background-color": "white",
504 "text-align": "center"
507 // Get a list of point with annotations.
508 var points
= this.layout
.annotated_points
;
509 for (var i
= 0; i
< points
.length
; i
++) {
511 var div
= document
.createElement("div");
512 for (var name
in annotationStyle
) {
513 if (annotationStyle
.hasOwnProperty(name
)) {
514 div
.style
[name
] = annotationStyle
[name
];
517 div
.appendChild(document
.createTextNode(p
.annotation
.shortText
));
518 div
.style
.left
= (p
.canvasx
- 10) + "px";
519 div
.style
.top
= p
.canvasy
+ "px";
520 div
.title
= p
.annotation
.text
;
521 div
.style
.color
= this.colors
[p
.name
];
522 div
.style
.borderColor
= this.colors
[p
.name
];
523 this.container
.appendChild(div
);
524 this.annotations
.push(div
);
530 * Overrides the CanvasRenderer method to draw error bars
532 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
533 var context
= this.element
.getContext("2d");
534 var colorCount
= this.options
.colorScheme
.length
;
535 var colorScheme
= this.options
.colorScheme
;
536 var fillAlpha
= this.options
.fillAlpha
;
537 var errorBars
= this.layout
.options
.errorBars
;
538 var fillGraph
= this.layout
.options
.fillGraph
;
539 var stackedGraph
= this.layout
.options
.stackedGraph
;
540 var stepPlot
= this.layout
.options
.stepPlot
;
543 for (var name
in this.layout
.datasets
) {
544 if (this.layout
.datasets
.hasOwnProperty(name
)) {
548 var setCount
= setNames
.length
;
551 for (var i
= 0; i
< setCount
; i
++) {
552 this.colors
[setNames
[i
]] = colorScheme
[i
% colorCount
];
557 for (var i
= 0; i
< this.layout
.points
.length
; i
++) {
558 var point
= this.layout
.points
[i
];
559 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
560 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
564 var isOK
= function(x
) { return x
&& !isNaN(x
); };
569 this.dygraph_
.warn("Can't use fillGraph option with error bars");
572 for (var i
= 0; i
< setCount
; i
++) {
573 var setName
= setNames
[i
];
574 var color
= this.colors
[setName
];
576 // setup graphics context
580 var prevYs
= [-1, -1];
581 var yscale
= this.layout
.yscale
;
582 // should be same color as the lines but only 15% opaque.
583 var rgb
= new RGBColor(color
);
584 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
586 ctx
.fillStyle
= err_color
;
588 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
589 var point
= this.layout
.points
[j
];
590 if (point
.name
== setName
) {
591 if (!isOK(point
.y
)) {
598 var newYs
= [ prevY
- point
.errorPlus
* yscale
,
599 prevY
+ point
.errorMinus
* yscale
];
602 var newYs
= [ point
.y
- point
.errorPlus
* yscale
,
603 point
.y
+ point
.errorMinus
* yscale
];
605 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
606 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
609 ctx
.moveTo(prevX
, newYs
[0]);
611 ctx
.moveTo(prevX
, prevYs
[0]);
613 ctx
.lineTo(point
.canvasx
, newYs
[0]);
614 ctx
.lineTo(point
.canvasx
, newYs
[1]);
616 ctx
.lineTo(prevX
, newYs
[1]);
618 ctx
.lineTo(prevX
, prevYs
[1]);
623 prevX
= point
.canvasx
;
628 } else if (fillGraph
) {
629 var axisY
= 1.0 + this.layout
.minyval
* this.layout
.yscale
;
630 if (axisY
< 0.0) axisY
= 0.0;
631 else if (axisY
> 1.0) axisY
= 1.0;
632 axisY
= this.area
.h
* axisY
+ this.area
.y
;
634 var baseline
= [] // for stacked graphs: baseline for filling
636 // process sets in reverse order (needed for stacked graphs)
637 for (var i
= setCount
- 1; i
>= 0; i
--) {
638 var setName
= setNames
[i
];
639 var color
= this.colors
[setName
];
641 // setup graphics context
644 var prevYs
= [-1, -1];
645 var yscale
= this.layout
.yscale
;
646 // should be same color as the lines but only 15% opaque.
647 var rgb
= new RGBColor(color
);
648 var err_color
= 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' +
650 ctx
.fillStyle
= err_color
;
652 for (var j
= 0; j
< this.layout
.points
.length
; j
++) {
653 var point
= this.layout
.points
[j
];
654 if (point
.name
== setName
) {
655 if (!isOK(point
.y
)) {
661 lastY
= baseline
[point
.canvasx
];
662 if (lastY
=== undefined
) lastY
= axisY
;
663 baseline
[point
.canvasx
] = point
.canvasy
;
664 newYs
= [ point
.canvasy
, lastY
];
666 newYs
= [ point
.canvasy
, axisY
];
669 ctx
.moveTo(prevX
, prevYs
[0]);
671 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
673 ctx
.lineTo(point
.canvasx
, newYs
[0]);
675 ctx
.lineTo(point
.canvasx
, newYs
[1]);
676 ctx
.lineTo(prevX
, prevYs
[1]);
680 prevX
= point
.canvasx
;
687 for (var i
= 0; i
< setCount
; i
++) {
688 var setName
= setNames
[i
];
689 var color
= this.colors
[setName
];
691 // setup graphics context
693 var point
= this.layout
.points
[0];
694 var pointSize
= this.dygraph_
.attr_("pointSize");
695 var prevX
= null, prevY
= null;
696 var drawPoints
= this.dygraph_
.attr_("drawPoints");
697 var points
= this.layout
.points
;
698 for (var j
= 0; j
< points
.length
; j
++) {
699 var point
= points
[j
];
700 if (point
.name
== setName
) {
701 if (!isOK(point
.canvasy
)) {
702 // this will make us move to the next point, not draw a line to it.
703 prevX
= prevY
= null;
705 // A point is "isolated" if it is non-null but both the previous
706 // and next points are null.
707 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
708 !isOK(points
[j
+1].canvasy
)));
711 prevX
= point
.canvasx
;
712 prevY
= point
.canvasy
;
715 ctx
.strokeStyle
= color
;
716 ctx
.lineWidth
= this.options
.strokeWidth
;
717 ctx
.moveTo(prevX
, prevY
);
719 ctx
.lineTo(point
.canvasx
, prevY
);
721 prevX
= point
.canvasx
;
722 prevY
= point
.canvasy
;
723 ctx
.lineTo(prevX
, prevY
);
727 if (drawPoints
|| isIsolated
) {
729 ctx
.fillStyle
= color
;
730 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
,
731 0, 2 * Math
.PI
, false);