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 MochiKit
.Base
.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 for (var name
in this.datasets
) {
41 var series
= this.datasets
[name
];
42 var x1
= series
[0][0];
43 if (!this.minxval
|| x1
< this.minxval
) this.minxval
= x1
;
45 var x2
= series
[series
.length
- 1][0];
46 if (!this.maxxval
|| x2
> this.maxxval
) this.maxxval
= x2
;
48 this.xrange
= this.maxxval
- this.minxval
;
49 this.xscale
= (this.xrange
!= 0 ? 1/this.xrange
: 1.0);
51 this.minyval
= this.options
.yAxis
[0];
52 this.maxyval
= this.options
.yAxis
[1];
53 this.yrange
= this.maxyval
- this.minyval
;
54 this.yscale
= (this.yrange
!= 0 ? 1/this.yrange
: 1.0);
57 DygraphLayout
.prototype._evaluateLineCharts
= function() {
59 this.points
= new Array();
60 for (var setName
in this.datasets
) {
61 var dataset
= this.datasets
[setName
];
62 for (var j
= 0; j
< dataset
.length
; j
++) {
63 var item
= dataset
[j
];
65 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
66 y
: 1.0 - ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
67 xval
: parseFloat(item
[0]),
68 yval
: parseFloat(item
[1]),
72 // limit the x, y values so they do not overdraw
79 if ((point
.x
>= 0.0) && (point
.x
<= 1.0)) {
80 this.points
.push(point
);
86 DygraphLayout
.prototype._evaluateLineTicks
= function() {
87 this.xticks
= new Array();
88 for (var i
= 0; i
< this.options
.xTicks
.length
; i
++) {
89 var tick
= this.options
.xTicks
[i
];
90 var label
= tick
.label
;
91 var pos
= this.xscale
* (tick
.v
- this.minxval
);
92 if ((pos
>= 0.0) && (pos
<= 1.0)) {
93 this.xticks
.push([pos
, label
]);
97 this.yticks
= new Array();
98 for (var i
= 0; i
< this.options
.yTicks
.length
; i
++) {
99 var tick
= this.options
.yTicks
[i
];
100 var label
= tick
.label
;
101 var pos
= 1.0 - (this.yscale
* (tick
.v
- this.minyval
));
102 if ((pos
>= 0.0) && (pos
<= 1.0)) {
103 this.yticks
.push([pos
, label
]);
110 * Behaves the same way as PlotKit.Layout, but also copies the errors
113 DygraphLayout
.prototype.evaluateWithError
= function() {
115 if (!this.options
.errorBars
) return;
117 // Copy over the error terms
118 var i
= 0; // index in this.points
119 for (var setName
in this.datasets
) {
121 var dataset
= this.datasets
[setName
];
122 for (var j
= 0; j
< dataset
.length
; j
++, i
++) {
123 var item
= dataset
[j
];
124 var xv
= parseFloat(item
[0]);
125 var yv
= parseFloat(item
[1]);
127 if (xv
== this.points
[i
].xval
&&
128 yv
== this.points
[i
].yval
) {
129 this.points
[i
].errorMinus
= parseFloat(item
[2]);
130 this.points
[i
].errorPlus
= parseFloat(item
[3]);
137 * Convenience function to remove all the data sets from a graph
139 DygraphLayout
.prototype.removeAllDatasets
= function() {
140 delete this.datasets
;
141 this.datasets
= new Array();
145 * Change the values of various layout options
146 * @param {Object} new_options an associative array of new properties
148 DygraphLayout
.prototype.updateOptions
= function(new_options
) {
149 MochiKit
.Base
.update(this.options
, new_options
? new_options
: {});
152 // Subclass PlotKit.CanvasRenderer to add:
153 // 1. X/Y grid overlay
154 // 2. Ability to draw error bars (if required)
157 * Sets some PlotKit.CanvasRenderer options
158 * @param {Object} element The canvas to attach to
159 * @param {Layout} layout The DygraphLayout object for this graph.
160 * @param {Object} options Options to pass on to CanvasRenderer
162 DygraphCanvasRenderer
= function(dygraph
, element
, layout
, options
) {
163 // TODO(danvk): remove options, just use dygraph.attr_.
164 this.dygraph_
= dygraph
;
171 "axisLineColor": Color
.blackColor(),
172 "axisLineWidth": 0.5,
174 "axisLabelColor": Color
.blackColor(),
175 "axisLabelFont": "Arial",
176 "axisLabelFontSize": 9,
177 "axisLabelWidth": 50,
180 "gridLineColor": MochiKit
.Color
.Color
.grayColor()
182 MochiKit
.Base
.update(this.options
, options
);
184 this.layout
= layout
;
185 this.element
= MochiKit
.DOM
.getElement(element
);
186 this.container
= this.element
.parentNode
;
188 // Stuff relating to Canvas on IE support
189 this.isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
191 if (this.isIE
&& !isNil(G_vmlCanvasManager
)) {
194 this.renderDelay
= null;
195 this.clearDelay
= null;
196 this.element
= G_vmlCanvasManager
.initElement(this.element
);
199 this.height
= this.element
.height
;
200 this.width
= this.element
.width
;
202 // --- check whether everything is ok before we return
203 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
204 throw "Canvas is not supported.";
207 this.xlabels
= new Array();
208 this.ylabels
= new Array();
211 x
: this.options
.yAxisLabelWidth
+ 2 * this.options
.axisTickSize
,
214 this.area
.w
= this.width
- this.area
.x
- this.options
.rightGap
;
215 this.area
.h
= this.height
- this.options
.axisLabelFontSize
-
216 2 * this.options
.axisTickSize
;
218 MochiKit
.DOM
.updateNodeAttributes(this.container
,
219 {"style":{ "position": "relative", "width": this.width
+ "px"}});
222 DygraphCanvasRenderer
.prototype.clear
= function() {
224 // VML takes a while to start up, so we just poll every this.IEDelay
226 if (this.clearDelay
) {
227 this.clearDelay
.cancel();
228 this.clearDelay
= null;
230 var context
= this.element
.getContext("2d");
233 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
234 this.clearDelay
.addCallback(bind(this.clear
, this));
239 var context
= this.element
.getContext("2d");
240 context
.clearRect(0, 0, this.width
, this.height
);
242 MochiKit
.Iter
.forEach(this.xlabels
, MochiKit
.DOM
.removeElement
);
243 MochiKit
.Iter
.forEach(this.ylabels
, MochiKit
.DOM
.removeElement
);
244 this.xlabels
= new Array();
245 this.ylabels
= new Array();
249 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
252 if (MochiKit
.Base
.isUndefinedOrNull(canvasName
))
253 canvas
= MochiKit
.DOM
.CANVAS({});
255 canvas
= MochiKit
.DOM
.getElement(canvasName
);
256 var context
= canvas
.getContext("2d");
259 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
260 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
261 if ((!ie
) || (ie
[1] < 6) || (opera
))
269 * Draw an X/Y grid on top of the existing plot
271 DygraphCanvasRenderer
.prototype.render
= function() {
272 // Draw the new X/Y grid
273 var ctx
= this.element
.getContext("2d");
274 if (this.options
.drawYGrid
) {
275 var ticks
= this.layout
.yticks
;
277 ctx
.strokeStyle
= this.options
.gridLineColor
.toRGBString();
278 ctx
.lineWidth
= this.options
.axisLineWidth
;
279 for (var i
= 0; i
< ticks
.length
; i
++) {
281 var y
= this.area
.y
+ ticks
[i
][0] * this.area
.h
;
284 ctx
.lineTo(x
+ this.area
.w
, y
);
290 if (this.options
.drawXGrid
) {
291 var ticks
= this.layout
.xticks
;
293 ctx
.strokeStyle
= this.options
.gridLineColor
.toRGBString();
294 ctx
.lineWidth
= this.options
.axisLineWidth
;
295 for (var i
=0; i
<ticks
.length
; i
++) {
296 var x
= this.area
.x
+ ticks
[i
][0] * this.area
.w
;
297 var y
= this.area
.y
+ this.area
.h
;
300 ctx
.lineTo(x
, this.area
.y
);
306 // Do the ordinary rendering, as before
307 // TODO(danvk) Call super.render()
308 this._renderLineChart();
313 DygraphCanvasRenderer
.prototype._renderAxis
= function() {
314 if (!this.options
.drawXAxis
&& !this.options
.drawYAxis
)
317 var context
= this.element
.getContext("2d");
319 var labelStyle
= {"style":
320 {"position": "absolute",
321 "fontSize": this.options
.axisLabelFontSize
+ "px",
323 "color": this.options
.axisLabelColor
.toRGBString(),
324 "width": this.options
.axisLabelWidth
+ "px",
331 context
.strokeStyle
= this.options
.axisLineColor
.toRGBString();
332 context
.lineWidth
= this.options
.axisLineWidth
;
335 if (this.options
.drawYAxis
) {
336 if (this.layout
.yticks
) {
337 var drawTick
= function(tick
) {
338 if (typeof(tick
) == "function") return;
340 var y
= this.area
.y
+ tick
[0] * this.area
.h
;
342 context
.moveTo(x
, y
);
343 context
.lineTo(x
- this.options
.axisTickSize
, y
);
347 var label
= DIV(labelStyle
, tick
[1]);
348 var top
= (y
- this.options
.axisLabelFontSize
/ 2);
349 if (top
< 0) top
= 0;
351 if (top
+ this.options
.axisLabelFontSize
+ 3 > this.height
) {
352 label
.style
.bottom
= "0px";
354 label
.style
.top
= top
+ "px";
356 label
.style
.left
= "0px";
357 label
.style
.textAlign
= "right";
358 label
.style
.width
= this.options
.yAxisLabelWidth
+ "px";
359 MochiKit
.DOM
.appendChildNodes(this.container
, label
);
360 this.ylabels
.push(label
);
363 MochiKit
.Iter
.forEach(this.layout
.yticks
, bind(drawTick
, this));
365 // The lowest tick on the y-axis often overlaps with the leftmost
366 // tick on the x-axis. Shift the bottom tick up a little bit to
367 // compensate if necessary.
368 var bottomTick
= this.ylabels
[0];
369 var fontSize
= this.options
.axisLabelFontSize
;
370 var bottom
= parseInt(bottomTick
.style
.top
) + fontSize
;
371 if (bottom
> this.height
- fontSize
) {
372 bottomTick
.style
.top
= (parseInt(bottomTick
.style
.top
) -
373 fontSize
/ 2) + "px";
378 context
.moveTo(this.area
.x
, this.area
.y
);
379 context
.lineTo(this.area
.x
, this.area
.y
+ this.area
.h
);
384 if (this.options
.drawXAxis
) {
385 if (this.layout
.xticks
) {
386 var drawTick
= function(tick
) {
387 if (typeof(dataset
) == "function") return;
389 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
390 var y
= this.area
.y
+ this.area
.h
;
392 context
.moveTo(x
, y
);
393 context
.lineTo(x
, y
+ this.options
.axisTickSize
);
397 var label
= DIV(labelStyle
, tick
[1]);
398 label
.style
.textAlign
= "center";
399 label
.style
.bottom
= "0px";
401 var left
= (x
- this.options
.axisLabelWidth
/2);
402 if (left
+ this.options
.axisLabelWidth
> this.width
) {
403 left
= this.width
- this.options
.xAxisLabelWidth
;
404 label
.style
.textAlign
= "right";
408 label
.style
.textAlign
= "left";
411 label
.style
.left
= left
+ "px";
412 label
.style
.width
= this.options
.xAxisLabelWidth
+ "px";
413 MochiKit
.DOM
.appendChildNodes(this.container
, label
);
414 this.xlabels
.push(label
);
417 MochiKit
.Iter
.forEach(this.layout
.xticks
, bind(drawTick
, this));
421 context
.moveTo(this.area
.x
, this.area
.y
+ this.area
.h
);
422 context
.lineTo(this.area
.x
+ this.area
.w
, this.area
.y
+ this.area
.h
);
432 * Overrides the CanvasRenderer method to draw error bars
434 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
435 var context
= this.element
.getContext("2d");
436 var colorCount
= this.options
.colorScheme
.length
;
437 var colorScheme
= this.options
.colorScheme
;
438 var setNames
= MochiKit
.Base
.keys(this.layout
.datasets
);
439 var errorBars
= this.layout
.options
.errorBars
;
440 var setCount
= setNames
.length
;
441 var bind
= MochiKit
.Base
.bind
;
442 var partial
= MochiKit
.Base
.partial
;
445 var updatePoint
= function(point
) {
446 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
447 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
449 MochiKit
.Iter
.forEach(this.layout
.points
, updatePoint
, this);
452 var isOK
= function(x
) { return x
&& !isNaN(x
); };
453 var makePath
= function(ctx
) {
454 for (var i
= 0; i
< setCount
; i
++) {
455 var setName
= setNames
[i
];
456 var color
= colorScheme
[i
%colorCount
];
457 var strokeX
= this.options
.strokeColorTransform
;
459 // setup graphics context
461 context
.strokeStyle
= color
.toRGBString();
462 context
.lineWidth
= this.options
.strokeWidth
;
463 var point
= this.layout
.points
[0];
464 var pointSize
= this.dygraph_
.attr_("pointSize");
465 var prevX
= null, prevY
= null;
466 var drawPoints
= this.dygraph_
.attr_("drawPoints");
467 var points
= this.layout
.points
;
468 for (var j
= 0; j
< points
.length
; j
++) {
469 var point
= points
[j
];
470 if (point
.name
== setName
) {
471 if (!isOK(point
.canvasy
)) {
472 // this will make us move to the next point, not draw a line to it.
473 prevX
= prevY
= null;
475 // A point is "isolated" if it is non-null but both the previous
476 // and next points are null.
477 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
478 !isOK(points
[j
+1].canvasy
)));
481 prevX
= point
.canvasx
;
482 prevY
= point
.canvasy
;
485 ctx
.moveTo(prevX
, prevY
);
486 prevX
= point
.canvasx
;
487 prevY
= point
.canvasy
;
488 ctx
.lineTo(prevX
, prevY
);
492 if (drawPoints
|| isIsolated
) {
494 ctx
.fillStyle
= color
.toRGBString();
495 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
, 0, 360, false);
504 var makeErrorBars
= function(ctx
) {
505 for (var i
= 0; i
< setCount
; i
++) {
506 var setName
= setNames
[i
];
507 var color
= colorScheme
[i
% colorCount
];
508 var strokeX
= this.options
.strokeColorTransform
;
510 // setup graphics context
512 context
.strokeStyle
= color
.toRGBString();
513 context
.lineWidth
= this.options
.strokeWidth
;
515 var prevYs
= [-1, -1];
517 var yscale
= this.layout
.yscale
;
518 var errorTrapezoid
= function(ctx_
,point
) {
520 if (point
.name
== setName
) {
521 if (!point
.y
|| isNaN(point
.y
)) {
525 var newYs
= [ point
.y
- point
.errorPlus
* yscale
,
526 point
.y
+ point
.errorMinus
* yscale
];
527 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
528 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
530 ctx_
.moveTo(prevX
, prevYs
[0]);
531 ctx_
.lineTo(point
.canvasx
, newYs
[0]);
532 ctx_
.lineTo(point
.canvasx
, newYs
[1]);
533 ctx_
.lineTo(prevX
, prevYs
[1]);
536 prevYs
[0] = newYs
[0];
537 prevYs
[1] = newYs
[1];
538 prevX
= point
.canvasx
;
541 // should be same color as the lines
542 var err_color
= color
.colorWithAlpha(0.15);
543 ctx
.fillStyle
= err_color
.toRGBString();
545 MochiKit
.Iter
.forEach(this.layout
.points
, partial(errorTrapezoid
, ctx
), this);
551 bind(makeErrorBars
, this)(context
);
552 bind(makePath
, this)(context
);