actually draw the graph
[dygraphs.git] / dygraph-canvas.js
... / ...
CommitLineData
1// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2// All Rights Reserved.
3
4/**
5 * @fileoverview Based on PlotKit, but modified to meet the needs of dygraphs.
6 * In particular, support for:
7 * - grid overlays
8 * - error bars
9 * - dygraphs attribute system
10 */
11
12/**
13 * Creates a new DygraphLayout object.
14 * @param {Object} options Options for PlotKit.Layout
15 * @return {Object} The DygraphLayout object
16 */
17DygraphLayout = 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();
22};
23
24DygraphLayout.prototype.attr_ = function(name) {
25 return this.dygraph_.attr_(name);
26};
27
28DygraphLayout.prototype.addDataset = function(setname, set_xy) {
29 this.datasets[setname] = set_xy;
30};
31
32DygraphLayout.prototype.evaluate = function() {
33 this._evaluateLimits();
34 this._evaluateLineCharts();
35 this._evaluateLineTicks();
36};
37
38DygraphLayout.prototype._evaluateLimits = function() {
39 this.minxval = this.maxxval = null;
40 for (var name in this.datasets) {
41 if (!this.datasets.hasOwnProperty(name)) continue;
42 var series = this.datasets[name];
43 var x1 = series[0][0];
44 if (!this.minxval || x1 < this.minxval) this.minxval = x1;
45
46 var x2 = series[series.length - 1][0];
47 if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
48 }
49 this.xrange = this.maxxval - this.minxval;
50 this.xscale = (this.xrange != 0 ? 1/this.xrange : 1.0);
51
52 this.minyval = this.options.yAxis[0];
53 this.maxyval = this.options.yAxis[1];
54 this.yrange = this.maxyval - this.minyval;
55 this.yscale = (this.yrange != 0 ? 1/this.yrange : 1.0);
56};
57
58DygraphLayout.prototype._evaluateLineCharts = function() {
59 // add all the rects
60 this.points = new Array();
61 for (var setName in this.datasets) {
62 if (!this.datasets.hasOwnProperty(setName)) continue;
63
64 var dataset = this.datasets[setName];
65 for (var j = 0; j < dataset.length; j++) {
66 var item = dataset[j];
67 var point = {
68 x: ((parseFloat(item[0]) - this.minxval) * this.xscale),
69 y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale),
70 xval: parseFloat(item[0]),
71 yval: parseFloat(item[1]),
72 name: setName
73 };
74
75 // limit the x, y values so they do not overdraw
76 if (point.y <= 0.0) {
77 point.y = 0.0;
78 }
79 if (point.y >= 1.0) {
80 point.y = 1.0;
81 }
82 if ((point.x >= 0.0) && (point.x <= 1.0)) {
83 this.points.push(point);
84 }
85 }
86 }
87};
88
89DygraphLayout.prototype._evaluateLineTicks = function() {
90 this.xticks = new Array();
91 for (var i = 0; i < this.options.xTicks.length; i++) {
92 var tick = this.options.xTicks[i];
93 var label = tick.label;
94 var pos = this.xscale * (tick.v - this.minxval);
95 if ((pos >= 0.0) && (pos <= 1.0)) {
96 this.xticks.push([pos, label]);
97 }
98 }
99
100 this.yticks = new Array();
101 for (var i = 0; i < this.options.yTicks.length; i++) {
102 var tick = this.options.yTicks[i];
103 var label = tick.label;
104 var pos = 1.0 - (this.yscale * (tick.v - this.minyval));
105 if ((pos >= 0.0) && (pos <= 1.0)) {
106 this.yticks.push([pos, label]);
107 }
108 }
109};
110
111
112/**
113 * Behaves the same way as PlotKit.Layout, but also copies the errors
114 * @private
115 */
116DygraphLayout.prototype.evaluateWithError = function() {
117 this.evaluate();
118 if (!this.options.errorBars) return;
119
120 // Copy over the error terms
121 var i = 0; // index in this.points
122 for (var setName in this.datasets) {
123 if (!this.datasets.hasOwnProperty(setName)) continue;
124 var j = 0;
125 var dataset = this.datasets[setName];
126 for (var j = 0; j < dataset.length; j++, i++) {
127 var item = dataset[j];
128 var xv = parseFloat(item[0]);
129 var yv = parseFloat(item[1]);
130
131 if (xv == this.points[i].xval &&
132 yv == this.points[i].yval) {
133 this.points[i].errorMinus = parseFloat(item[2]);
134 this.points[i].errorPlus = parseFloat(item[3]);
135 }
136 }
137 }
138};
139
140/**
141 * Convenience function to remove all the data sets from a graph
142 */
143DygraphLayout.prototype.removeAllDatasets = function() {
144 delete this.datasets;
145 this.datasets = new Array();
146};
147
148/**
149 * Change the values of various layout options
150 * @param {Object} new_options an associative array of new properties
151 */
152DygraphLayout.prototype.updateOptions = function(new_options) {
153 Dygraph.update(this.options, new_options ? new_options : {});
154};
155
156// Subclass PlotKit.CanvasRenderer to add:
157// 1. X/Y grid overlay
158// 2. Ability to draw error bars (if required)
159
160/**
161 * Sets some PlotKit.CanvasRenderer options
162 * @param {Object} element The canvas to attach to
163 * @param {Layout} layout The DygraphLayout object for this graph.
164 * @param {Object} options Options to pass on to CanvasRenderer
165 */
166DygraphCanvasRenderer = function(dygraph, element, layout, options) {
167 // TODO(danvk): remove options, just use dygraph.attr_.
168 this.dygraph_ = dygraph;
169
170 // default options
171 this.options = {
172 "strokeWidth": 0.5,
173 "drawXAxis": true,
174 "drawYAxis": true,
175 "axisLineColor": "black",
176 "axisLineWidth": 0.5,
177 "axisTickSize": 3,
178 "axisLabelColor": "black",
179 "axisLabelFont": "Arial",
180 "axisLabelFontSize": 9,
181 "axisLabelWidth": 50,
182 "drawYGrid": true,
183 "drawXGrid": true,
184 "gridLineColor": "rgb(128,128,128)"
185 };
186 Dygraph.update(this.options, options);
187
188 this.layout = layout;
189 this.element = element;
190 this.container = this.element.parentNode;
191
192 this.height = this.element.height;
193 this.width = this.element.width;
194
195 // --- check whether everything is ok before we return
196 if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element)))
197 throw "Canvas is not supported.";
198
199 // internal state
200 this.xlabels = new Array();
201 this.ylabels = new Array();
202
203 this.area = {
204 x: this.options.yAxisLabelWidth + 2 * this.options.axisTickSize,
205 y: 0
206 };
207 this.area.w = this.width - this.area.x - this.options.rightGap;
208 this.area.h = this.height - this.options.axisLabelFontSize -
209 2 * this.options.axisTickSize;
210
211 this.container.style.position = "relative";
212 this.container.style.width = this.width + "px";
213};
214
215DygraphCanvasRenderer.prototype.clear = function() {
216 if (this.isIE) {
217 // VML takes a while to start up, so we just poll every this.IEDelay
218 try {
219 if (this.clearDelay) {
220 this.clearDelay.cancel();
221 this.clearDelay = null;
222 }
223 var context = this.element.getContext("2d");
224 }
225 catch (e) {
226 // TODO(danvk): this is broken, since MochiKit.Async is gone.
227 this.clearDelay = MochiKit.Async.wait(this.IEDelay);
228 this.clearDelay.addCallback(bind(this.clear, this));
229 return;
230 }
231 }
232
233 var context = this.element.getContext("2d");
234 context.clearRect(0, 0, this.width, this.height);
235
236 for (var i = 0; i < this.xlabels.length; i++) {
237 var el = this.xlabels[i];
238 el.parentNode.removeChild(el);
239 }
240 for (var i = 0; i < this.ylabels.length; i++) {
241 var el = this.ylabels[i];
242 el.parentNode.removeChild(el);
243 }
244 this.xlabels = new Array();
245 this.ylabels = new Array();
246};
247
248
249DygraphCanvasRenderer.isSupported = function(canvasName) {
250 var canvas = null;
251 try {
252 if (typeof(canvasName) == 'undefined' || canvasName == null)
253 canvas = document.createElement("canvas");
254 else
255 canvas = canvasName;
256 var context = canvas.getContext("2d");
257 }
258 catch (e) {
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))
262 return false;
263 return true;
264 }
265 return true;
266};
267
268/**
269 * Draw an X/Y grid on top of the existing plot
270 */
271DygraphCanvasRenderer.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;
276 ctx.save();
277 ctx.strokeStyle = this.options.gridLineColor;
278 ctx.lineWidth = this.options.axisLineWidth;
279 for (var i = 0; i < ticks.length; i++) {
280 var x = this.area.x;
281 var y = this.area.y + ticks[i][0] * this.area.h;
282 ctx.beginPath();
283 ctx.moveTo(x, y);
284 ctx.lineTo(x + this.area.w, y);
285 ctx.closePath();
286 ctx.stroke();
287 }
288 }
289
290 if (this.options.drawXGrid) {
291 var ticks = this.layout.xticks;
292 ctx.save();
293 ctx.strokeStyle = this.options.gridLineColor;
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;
298 ctx.beginPath();
299 ctx.moveTo(x, y);
300 ctx.lineTo(x, this.area.y);
301 ctx.closePath();
302 ctx.stroke();
303 }
304 }
305
306 // Do the ordinary rendering, as before
307 this._renderLineChart();
308 this._renderAxis();
309};
310
311
312DygraphCanvasRenderer.prototype._renderAxis = function() {
313 if (!this.options.drawXAxis && !this.options.drawYAxis)
314 return;
315
316 var context = this.element.getContext("2d");
317
318 var labelStyle = {
319 "position": "absolute",
320 "fontSize": this.options.axisLabelFontSize + "px",
321 "zIndex": 10,
322 "color": this.options.axisLabelColor,
323 "width": this.options.axisLabelWidth + "px",
324 "overflow": "hidden"
325 };
326 var makeDiv = function(txt) {
327 var div = document.createElement("div");
328 for (var name in labelStyle) {
329 if (labelStyle.hasOwnProperty(name)) {
330 div.style[name] = labelStyle[name];
331 }
332 }
333 div.appendChild(document.createTextNode(txt));
334 return div;
335 };
336
337 // axis lines
338 context.save();
339 context.strokeStyle = this.options.axisLineColor;
340 context.lineWidth = this.options.axisLineWidth;
341
342 if (this.options.drawYAxis) {
343 if (this.layout.yticks && this.layout.yticks.length > 0) {
344 for (var i = 0; i < this.layout.yticks.length; i++) {
345 var tick = this.layout.yticks[i];
346 if (typeof(tick) == "function") return;
347 var x = this.area.x;
348 var y = this.area.y + tick[0] * this.area.h;
349 context.beginPath();
350 context.moveTo(x, y);
351 context.lineTo(x - this.options.axisTickSize, y);
352 context.closePath();
353 context.stroke();
354
355 var label = makeDiv(tick[1]);
356 var top = (y - this.options.axisLabelFontSize / 2);
357 if (top < 0) top = 0;
358
359 if (top + this.options.axisLabelFontSize + 3 > this.height) {
360 label.style.bottom = "0px";
361 } else {
362 label.style.top = top + "px";
363 }
364 label.style.left = "0px";
365 label.style.textAlign = "right";
366 label.style.width = this.options.yAxisLabelWidth + "px";
367 this.container.appendChild(label);
368 this.ylabels.push(label);
369 }
370
371 // The lowest tick on the y-axis often overlaps with the leftmost
372 // tick on the x-axis. Shift the bottom tick up a little bit to
373 // compensate if necessary.
374 var bottomTick = this.ylabels[0];
375 var fontSize = this.options.axisLabelFontSize;
376 var bottom = parseInt(bottomTick.style.top) + fontSize;
377 if (bottom > this.height - fontSize) {
378 bottomTick.style.top = (parseInt(bottomTick.style.top) -
379 fontSize / 2) + "px";
380 }
381 }
382
383 context.beginPath();
384 context.moveTo(this.area.x, this.area.y);
385 context.lineTo(this.area.x, this.area.y + this.area.h);
386 context.closePath();
387 context.stroke();
388 }
389
390 if (this.options.drawXAxis) {
391 if (this.layout.xticks) {
392 for (var i = 0; i < this.layout.xticks.length; i++) {
393 var tick = this.layout.xticks[i];
394 if (typeof(dataset) == "function") return;
395
396 var x = this.area.x + tick[0] * this.area.w;
397 var y = this.area.y + this.area.h;
398 context.beginPath();
399 context.moveTo(x, y);
400 context.lineTo(x, y + this.options.axisTickSize);
401 context.closePath();
402 context.stroke();
403
404 var label = makeDiv(tick[1]);
405 label.style.textAlign = "center";
406 label.style.bottom = "0px";
407
408 var left = (x - this.options.axisLabelWidth/2);
409 if (left + this.options.axisLabelWidth > this.width) {
410 left = this.width - this.options.xAxisLabelWidth;
411 label.style.textAlign = "right";
412 }
413 if (left < 0) {
414 left = 0;
415 label.style.textAlign = "left";
416 }
417
418 label.style.left = left + "px";
419 label.style.width = this.options.xAxisLabelWidth + "px";
420 this.container.appendChild(label);
421 this.xlabels.push(label);
422 }
423 }
424
425 context.beginPath();
426 context.moveTo(this.area.x, this.area.y + this.area.h);
427 context.lineTo(this.area.x + this.area.w, this.area.y + this.area.h);
428 context.closePath();
429 context.stroke();
430 }
431
432 context.restore();
433};
434
435
436/**
437 * Overrides the CanvasRenderer method to draw error bars
438 */
439DygraphCanvasRenderer.prototype._renderLineChart = function() {
440 var context = this.element.getContext("2d");
441 var colorCount = this.options.colorScheme.length;
442 var colorScheme = this.options.colorScheme;
443 var errorBars = this.layout.options.errorBars;
444
445 var setNames = [];
446 for (var name in this.layout.datasets) {
447 if (this.layout.datasets.hasOwnProperty(name)) {
448 setNames.push(name);
449 }
450 }
451 var setCount = setNames.length;
452
453 //Update Points
454 for (var i = 0; i < this.layout.points.length; i++) {
455 var point = this.layout.points[i];
456 point.canvasx = this.area.w * point.x + this.area.x;
457 point.canvasy = this.area.h * point.y + this.area.y;
458 }
459
460 // create paths
461 var isOK = function(x) { return x && !isNaN(x); };
462
463 var ctx = context;
464 if (errorBars) {
465 for (var i = 0; i < setCount; i++) {
466 var setName = setNames[i];
467 var color = colorScheme[i % colorCount];
468
469 // setup graphics context
470 ctx.save();
471 ctx.strokeStyle = color;
472 ctx.lineWidth = this.options.strokeWidth;
473 var prevX = -1;
474 var prevYs = [-1, -1];
475 var count = 0;
476 var yscale = this.layout.yscale;
477 // should be same color as the lines but only 15% opaque.
478 var rgb = new RGBColor(color);
479 var err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',0.15)';
480 ctx.fillStyle = err_color;
481 ctx.beginPath();
482 for (var j = 0; j < this.layout.points.length; j++) {
483 var point = this.layout.points[j];
484 count++;
485 if (point.name == setName) {
486 if (!point.y || isNaN(point.y)) {
487 prevX = -1;
488 continue;
489 }
490 var newYs = [ point.y - point.errorPlus * yscale,
491 point.y + point.errorMinus * yscale ];
492 newYs[0] = this.area.h * newYs[0] + this.area.y;
493 newYs[1] = this.area.h * newYs[1] + this.area.y;
494 if (prevX >= 0) {
495 ctx.moveTo(prevX, prevYs[0]);
496 ctx.lineTo(point.canvasx, newYs[0]);
497 ctx.lineTo(point.canvasx, newYs[1]);
498 ctx.lineTo(prevX, prevYs[1]);
499 ctx.closePath();
500 }
501 prevYs[0] = newYs[0];
502 prevYs[1] = newYs[1];
503 prevX = point.canvasx;
504 }
505 }
506 ctx.fill();
507 }
508 }
509
510 for (var i = 0; i < setCount; i++) {
511 var setName = setNames[i];
512 var color = colorScheme[i%colorCount];
513
514 // setup graphics context
515 context.save();
516 var point = this.layout.points[0];
517 var pointSize = this.dygraph_.attr_("pointSize");
518 var prevX = null, prevY = null;
519 var drawPoints = this.dygraph_.attr_("drawPoints");
520 var points = this.layout.points;
521 for (var j = 0; j < points.length; j++) {
522 var point = points[j];
523 if (point.name == setName) {
524 if (!isOK(point.canvasy)) {
525 // this will make us move to the next point, not draw a line to it.
526 prevX = prevY = null;
527 } else {
528 // A point is "isolated" if it is non-null but both the previous
529 // and next points are null.
530 var isIsolated = (!prevX && (j == points.length - 1 ||
531 !isOK(points[j+1].canvasy)));
532
533 if (!prevX) {
534 prevX = point.canvasx;
535 prevY = point.canvasy;
536 } else {
537 ctx.beginPath();
538 ctx.strokeStyle = color;
539 ctx.lineWidth = this.options.strokeWidth;
540 ctx.moveTo(prevX, prevY);
541 prevX = point.canvasx;
542 prevY = point.canvasy;
543 ctx.lineTo(prevX, prevY);
544 ctx.stroke();
545 }
546
547 if (drawPoints || isIsolated) {
548 ctx.beginPath();
549 ctx.fillStyle = color;
550 ctx.arc(point.canvasx, point.canvasy, pointSize,
551 0, 2 * Math.PI, false);
552 ctx.fill();
553 }
554 }
555 }
556 }
557 }
558
559 context.restore();
560};