05ac0cc99a6a793f460183930585496e46e72a21
1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Subclasses various parts of PlotKit to meet the additional
6 * needs of Dygraph: grid overlays and error bars
9 // Subclass PlotKit.Layout to add:
10 // 1. Sigma/errorBars properties
11 // 2. Copy error terms for PlotKit.CanvasRenderer._renderLineChart
14 * Creates a new DygraphLayout object. Options are the same as those allowed
15 * by the PlotKit.Layout constructor.
16 * @param {Object} options Options for PlotKit.Layout
17 * @return {Object} The DygraphLayout object
19 DygraphLayout
= function(dygraph
, options
) {
20 this.dygraph_
= dygraph
;
21 this.options
= {}; // TODO(danvk): remove, use attr_ instead.
22 MochiKit
.Base
.update(this.options
, options
? options
: {});
23 this.datasets
= new Array();
26 DygraphLayout
.prototype.attr_
= function(name
) {
27 return this.dygraph_
.attr_(name
);
30 DygraphLayout
.prototype.addDataset
= function(setname
, set_xy
) {
31 this.datasets
[setname
] = set_xy
;
34 DygraphLayout
.prototype.evaluate
= function() {
35 this._evaluateLimits();
36 this._evaluateLineCharts();
37 this._evaluateLineTicks();
40 DygraphLayout
.prototype._evaluateLimits
= function() {
41 this.minxval
= this.maxxval
= null;
42 for (var name
in this.datasets
) {
43 var series
= this.datasets
[name
];
44 var x1
= series
[0][0];
45 if (!this.minxval
|| x1
< this.minxval
) this.minxval
= x1
;
47 var x2
= series
[series
.length
- 1][0];
48 if (!this.maxxval
|| x2
> this.maxxval
) this.maxxval
= x2
;
50 this.xrange
= this.maxxval
- this.minxval
;
51 this.xscale
= (this.xrange
!= 0 ? 1/this.xrange
: 1.0);
53 this.minyval
= this.options
.yAxis
[0];
54 this.maxyval
= this.options
.yAxis
[1];
55 this.yrange
= this.maxyval
- this.minyval
;
56 this.yscale
= (this.yrange
!= 0 ? 1/this.yrange
: 1.0);
59 DygraphLayout
.prototype._evaluateLineCharts
= function() {
61 this.points
= new Array();
62 for (var setName
in this.datasets
) {
63 var dataset
= this.datasets
[setName
];
64 for (var j
= 0; j
< dataset
.length
; j
++) {
65 var item
= dataset
[j
];
67 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
68 y
: 1.0 - ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
69 xval
: parseFloat(item
[0]),
70 yval
: parseFloat(item
[1]),
74 // limit the x, y values so they do not overdraw
81 if ((point
.x
>= 0.0) && (point
.x
<= 1.0)) {
82 this.points
.push(point
);
88 DygraphLayout
.prototype._evaluateLineTicks
= function() {
89 this.xticks
= new Array();
90 for (var i
= 0; i
< this.options
.xTicks
.length
; i
++) {
91 var tick
= this.options
.xTicks
[i
];
92 var label
= tick
.label
;
93 var pos
= this.xscale
* (tick
.v
- this.minxval
);
94 if ((pos
>= 0.0) && (pos
<= 1.0)) {
95 this.xticks
.push([pos
, label
]);
99 this.yticks
= new Array();
100 for (var i
= 0; i
< this.options
.yTicks
.length
; i
++) {
101 var tick
= this.options
.yTicks
[i
];
102 var label
= tick
.label
;
103 var pos
= 1.0 - (this.yscale
* (tick
.v
- this.minyval
));
104 if ((pos
>= 0.0) && (pos
<= 1.0)) {
105 this.yticks
.push([pos
, label
]);
112 * Behaves the same way as PlotKit.Layout, but also copies the errors
115 DygraphLayout
.prototype.evaluateWithError
= function() {
117 if (!this.options
.errorBars
) return;
119 // Copy over the error terms
120 var i
= 0; // index in this.points
121 for (var setName
in this.datasets
) {
123 var dataset
= this.datasets
[setName
];
124 if (PlotKit
.Base
.isFuncLike(dataset
)) continue;
125 for (var j
= 0; j
< dataset
.length
; j
++, i
++) {
126 var item
= dataset
[j
];
127 var xv
= parseFloat(item
[0]);
128 var yv
= parseFloat(item
[1]);
130 if (xv
== this.points
[i
].xval
&&
131 yv
== this.points
[i
].yval
) {
132 this.points
[i
].errorMinus
= parseFloat(item
[2]);
133 this.points
[i
].errorPlus
= parseFloat(item
[3]);
140 * Convenience function to remove all the data sets from a graph
142 DygraphLayout
.prototype.removeAllDatasets
= function() {
143 delete this.datasets
;
144 this.datasets
= new Array();
148 * Change the values of various layout options
149 * @param {Object} new_options an associative array of new properties
151 DygraphLayout
.prototype.updateOptions
= function(new_options
) {
152 MochiKit
.Base
.update(this.options
, new_options
? new_options
: {});
155 // Subclass PlotKit.CanvasRenderer to add:
156 // 1. X/Y grid overlay
157 // 2. Ability to draw error bars (if required)
160 * Sets some PlotKit.CanvasRenderer options
161 * @param {Object} element The canvas to attach to
162 * @param {Layout} layout The DygraphLayout object for this graph.
163 * @param {Object} options Options to pass on to CanvasRenderer
165 DygraphCanvasRenderer
= function(dygraph
, element
, layout
, options
) {
166 // TODO(danvk): remove options, just use dygraph.attr_.
167 PlotKit
.CanvasRenderer
.call(this, element
, layout
, options
);
168 this.dygraph_
= dygraph
;
169 this.options
.shouldFill
= false;
170 this.options
.shouldStroke
= true;
171 this.options
.drawYGrid
= true;
172 this.options
.drawXGrid
= true;
173 this.options
.gridLineColor
= MochiKit
.Color
.Color
.grayColor();
174 MochiKit
.Base
.update(this.options
, options
);
176 // TODO(danvk) This shouldn't be necessary: effects should be overlaid
177 this.options
.drawBackground
= false;
179 DygraphCanvasRenderer
.prototype = new PlotKit
.CanvasRenderer();
182 * Draw an X/Y grid on top of the existing plot
184 DygraphCanvasRenderer
.prototype.render
= function() {
185 // Draw the new X/Y grid
186 var ctx
= this.element
.getContext("2d");
187 if (this.options
.drawYGrid
) {
188 var ticks
= this.layout
.yticks
;
190 ctx
.strokeStyle
= this.options
.gridLineColor
.toRGBString();
191 ctx
.lineWidth
= this.options
.axisLineWidth
;
192 for (var i
= 0; i
< ticks
.length
; i
++) {
194 var y
= this.area
.y
+ ticks
[i
][0] * this.area
.h
;
197 ctx
.lineTo(x
+ this.area
.w
, y
);
203 if (this.options
.drawXGrid
) {
204 var ticks
= this.layout
.xticks
;
206 ctx
.strokeStyle
= this.options
.gridLineColor
.toRGBString();
207 ctx
.lineWidth
= this.options
.axisLineWidth
;
208 for (var i
=0; i
<ticks
.length
; i
++) {
209 var x
= this.area
.x
+ ticks
[i
][0] * this.area
.w
;
210 var y
= this.area
.y
+ this.area
.h
;
213 ctx
.lineTo(x
, this.area
.y
);
219 // Do the ordinary rendering, as before
220 // TODO(danvk) Call super.render()
221 this._renderLineChart();
222 this._renderLineAxis();
226 * Overrides the CanvasRenderer method to draw error bars
228 DygraphCanvasRenderer
.prototype._renderLineChart
= function() {
229 var context
= this.element
.getContext("2d");
230 var colorCount
= this.options
.colorScheme
.length
;
231 var colorScheme
= this.options
.colorScheme
;
232 var setNames
= MochiKit
.Base
.keys(this.layout
.datasets
);
233 var errorBars
= this.layout
.options
.errorBars
;
234 var setCount
= setNames
.length
;
235 var bind
= MochiKit
.Base
.bind
;
236 var partial
= MochiKit
.Base
.partial
;
239 var updatePoint
= function(point
) {
240 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
241 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
243 MochiKit
.Iter
.forEach(this.layout
.points
, updatePoint
, this);
246 var isOK
= function(x
) { return x
&& !isNaN(x
); };
247 var makePath
= function(ctx
) {
248 for (var i
= 0; i
< setCount
; i
++) {
249 var setName
= setNames
[i
];
250 var color
= colorScheme
[i
%colorCount
];
251 var strokeX
= this.options
.strokeColorTransform
;
253 // setup graphics context
255 context
.strokeStyle
= color
.toRGBString();
256 context
.lineWidth
= this.options
.strokeWidth
;
257 var point
= this.layout
.points
[0];
258 var pointSize
= this.dygraph_
.attr_("pointSize");
259 var prevX
= null, prevY
= null;
260 var drawPoints
= this.dygraph_
.attr_("drawPoints");
261 var points
= this.layout
.points
;
262 for (var j
= 0; j
< points
.length
; j
++) {
263 var point
= points
[j
];
264 if (point
.name
== setName
) {
265 if (!isOK(point
.canvasy
)) {
266 // this will make us move to the next point, not draw a line to it.
267 prevX
= prevY
= null;
269 // A point is "isolated" if it is non-null but both the previous
270 // and next points are null.
271 var isIsolated
= (!prevX
&& (j
== points
.length
- 1 ||
272 !isOK(points
[j
+1].canvasy
)));
275 prevX
= point
.canvasx
;
276 prevY
= point
.canvasy
;
279 ctx
.moveTo(prevX
, prevY
);
280 prevX
= point
.canvasx
;
281 prevY
= point
.canvasy
;
282 ctx
.lineTo(prevX
, prevY
);
286 if (drawPoints
|| isIsolated
) {
288 ctx
.fillStyle
= color
.toRGBString();
289 ctx
.arc(point
.canvasx
, point
.canvasy
, pointSize
, 0, 360, false);
298 var makeErrorBars
= function(ctx
) {
299 for (var i
= 0; i
< setCount
; i
++) {
300 var setName
= setNames
[i
];
301 var color
= colorScheme
[i
% colorCount
];
302 var strokeX
= this.options
.strokeColorTransform
;
304 // setup graphics context
306 context
.strokeStyle
= color
.toRGBString();
307 context
.lineWidth
= this.options
.strokeWidth
;
309 var prevYs
= [-1, -1];
311 var yscale
= this.layout
.yscale
;
312 var errorTrapezoid
= function(ctx_
,point
) {
314 if (point
.name
== setName
) {
315 if (!point
.y
|| isNaN(point
.y
)) {
319 var newYs
= [ point
.y
- point
.errorPlus
* yscale
,
320 point
.y
+ point
.errorMinus
* yscale
];
321 newYs
[0] = this.area
.h
* newYs
[0] + this.area
.y
;
322 newYs
[1] = this.area
.h
* newYs
[1] + this.area
.y
;
324 ctx_
.moveTo(prevX
, prevYs
[0]);
325 ctx_
.lineTo(point
.canvasx
, newYs
[0]);
326 ctx_
.lineTo(point
.canvasx
, newYs
[1]);
327 ctx_
.lineTo(prevX
, prevYs
[1]);
330 prevYs
[0] = newYs
[0];
331 prevYs
[1] = newYs
[1];
332 prevX
= point
.canvasx
;
335 // should be same color as the lines
336 var err_color
= color
.colorWithAlpha(0.15);
337 ctx
.fillStyle
= err_color
.toRGBString();
339 MochiKit
.Iter
.forEach(this.layout
.points
, partial(errorTrapezoid
, ctx
), this);
345 bind(makeErrorBars
, this)(context
);
346 bind(makePath
, this)(context
);