dygraphs finally stays within its bounds! removed padding property and tweaked PlotKi...
[dygraphs.git] / plotkit_v091 / PlotKit / Canvas.js
1 /*
2 PlotKit Canvas
3 ==============
4
5 Provides HTML Canvas Renderer. This is supported under:
6
7 - Safari 2.0
8 - Mozilla Firefox 1.5
9 - Opera 9.0 preview 2
10 - IE 6 (via VML Emulation)
11
12 It uses DIVs for labels.
13
14 Copyright
15 ---------
16 Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
17 For use under the BSD license. <http://www.liquidx.net/plotkit>
18
19 */
20 // --------------------------------------------------------------------
21 // Check required components
22 // --------------------------------------------------------------------
23
24 try {
25 if ((typeof(PlotKit.Base) == 'undefined') ||
26 (typeof(PlotKit.Layout) == 'undefined'))
27 {
28 throw "";
29 }
30 }
31 catch (e) {
32 throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.{Base,Layout}"
33 }
34
35
36 // ------------------------------------------------------------------------
37 // Defines the renderer class
38 // ------------------------------------------------------------------------
39
40 if (typeof(PlotKit.CanvasRenderer) == 'undefined') {
41 PlotKit.CanvasRenderer = {};
42 }
43
44 PlotKit.CanvasRenderer.NAME = "PlotKit.CanvasRenderer";
45 PlotKit.CanvasRenderer.VERSION = PlotKit.VERSION;
46
47 PlotKit.CanvasRenderer.__repr__ = function() {
48 return "[" + this.NAME + " " + this.VERSION + "]";
49 };
50
51 PlotKit.CanvasRenderer.toString = function() {
52 return this.__repr__();
53 }
54
55 PlotKit.CanvasRenderer = function(element, layout, options) {
56 if (arguments.length > 0)
57 this.__init__(element, layout, options);
58 };
59
60 PlotKit.CanvasRenderer.prototype.__init__ = function(element, layout, options) {
61 var isNil = MochiKit.Base.isUndefinedOrNull;
62 var Color = MochiKit.Color.Color;
63
64 // default options
65 this.options = {
66 "drawBackground": true,
67 "backgroundColor": Color.whiteColor(),
68 "colorScheme": PlotKit.Base.palette(PlotKit.Base.baseColors()[0]),
69 "strokeColor": Color.whiteColor(),
70 "strokeColorTransform": "asStrokeColor",
71 "strokeWidth": 0.5,
72 "shouldFill": true,
73 "shouldStroke": true,
74 "drawXAxis": true,
75 "drawYAxis": true,
76 "axisLineColor": Color.blackColor(),
77 "axisLineWidth": 0.5,
78 "axisTickSize": 3,
79 "axisLabelColor": Color.blackColor(),
80 "axisLabelFont": "Arial",
81 "axisLabelFontSize": 9,
82 "axisLabelWidth": 50,
83 "pieRadius": 0.4,
84 "enableEvents": true
85 };
86 MochiKit.Base.update(this.options, options ? options : {});
87
88 this.layout = layout;
89 this.element = MochiKit.DOM.getElement(element);
90 this.container = this.element.parentNode;
91
92 // Stuff relating to Canvas on IE support
93 this.isIE = PlotKit.Base.excanvasSupported();
94
95 if (this.isIE && !isNil(G_vmlCanvasManager)) {
96 this.IEDelay = 0.5;
97 this.maxTries = 5;
98 this.renderDelay = null;
99 this.clearDelay = null;
100 this.element = G_vmlCanvasManager.initElement(this.element);
101 }
102
103 this.height = this.element.height;
104 this.width = this.element.width;
105
106 // --- check whether everything is ok before we return
107
108 if (isNil(this.element))
109 throw "CanvasRenderer() - passed canvas is not found";
110
111 if (!this.isIE && !(PlotKit.CanvasRenderer.isSupported(this.element)))
112 throw "CanvasRenderer() - Canvas is not supported.";
113
114 if (isNil(this.container) || (this.container.nodeName.toLowerCase() != "div"))
115 throw "CanvasRenderer() - <canvas> needs to be enclosed in <div>";
116
117 // internal state
118 this.xlabels = new Array();
119 this.ylabels = new Array();
120 this.isFirstRender = true;
121
122 this.area = {
123 x: this.options.yAxisLabelWidth + 2 * this.options.axisTickSize,
124 y: 0
125 };
126 this.area.w = this.width - this.area.x - this.options.rightGap;
127 this.area.h = this.height - this.options.axisLabelFontSize -
128 2 * this.options.axisTickSize;
129
130 MochiKit.DOM.updateNodeAttributes(this.container,
131 {"style":{ "position": "relative", "width": this.width + "px"}});
132
133 // load event system if we have Signals
134 /* Disabled until we have a proper implementation
135 try {
136 this.event_isinside = null;
137 if (MochiKit.Signal && this.options.enableEvents) {
138 this._initialiseEvents();
139 }
140 }
141 catch (e) {
142 // still experimental
143 }
144 */
145 };
146
147 PlotKit.CanvasRenderer.prototype.render = function() {
148 if (this.isIE) {
149 // VML takes a while to start up, so we just poll every this.IEDelay
150 try {
151 if (this.renderDelay) {
152 this.renderDelay.cancel();
153 this.renderDelay = null;
154 }
155 var context = this.element.getContext("2d");
156 }
157 catch (e) {
158 this.isFirstRender = false;
159 if (this.maxTries-- > 0) {
160 this.renderDelay = MochiKit.Async.wait(this.IEDelay);
161 this.renderDelay.addCallback(bind(this.render, this));
162 }
163 return;
164 }
165 }
166
167 if (this.options.drawBackground)
168 this._renderBackground();
169
170 if (this.layout.style == "line") {
171 this._renderLineChart();
172 this._renderLineAxis();
173 }
174 };
175
176 PlotKit.CanvasRenderer.prototype._renderLineChart = function() {
177 var context = this.element.getContext("2d");
178 var colorCount = this.options.colorScheme.length;
179 var colorScheme = this.options.colorScheme;
180 var setNames = MochiKit.Base.keys(this.layout.datasets);
181 var setCount = setNames.length;
182 var bind = MochiKit.Base.bind;
183 var partial = MochiKit.Base.partial;
184
185 for (var i = 0; i < setCount; i++) {
186 var setName = setNames[i];
187 var color = colorScheme[i%colorCount];
188 var strokeX = this.options.strokeColorTransform;
189
190 // setup graphics context
191 context.save();
192 context.fillStyle = color.toRGBString();
193 if (this.options.strokeColor)
194 context.strokeStyle = this.options.strokeColor.toRGBString();
195 else if (this.options.strokeColorTransform)
196 context.strokeStyle = color[strokeX]().toRGBString();
197
198 context.lineWidth = this.options.strokeWidth;
199
200 // create paths
201 var makePath = function(ctx) {
202 ctx.beginPath();
203 ctx.moveTo(this.area.x, this.area.y + this.area.h);
204 var addPoint = function(ctx_, point) {
205 if (point.name == setName)
206 ctx_.lineTo(this.area.w * point.x + this.area.x,
207 this.area.h * point.y + this.area.y);
208 };
209 MochiKit.Iter.forEach(this.layout.points, partial(addPoint, ctx), this);
210 ctx.lineTo(this.area.w + this.area.x,
211 this.area.h + this.area.y);
212 ctx.lineTo(this.area.x, this.area.y + this.area.h);
213 ctx.closePath();
214 };
215
216 if (this.options.shouldFill) {
217 bind(makePath, this)(context);
218 context.fill();
219 }
220 if (this.options.shouldStroke) {
221 bind(makePath, this)(context);
222 context.stroke();
223 }
224
225 context.restore();
226 }
227 };
228
229
230 PlotKit.CanvasRenderer.prototype._renderLineAxis = function() {
231 this._renderAxis();
232 };
233
234
235 PlotKit.CanvasRenderer.prototype._renderAxis = function() {
236 if (!this.options.drawXAxis && !this.options.drawYAxis)
237 return;
238
239 var context = this.element.getContext("2d");
240
241 var labelStyle = {"style":
242 {"position": "absolute",
243 "fontSize": this.options.axisLabelFontSize + "px",
244 "zIndex": 10,
245 "color": this.options.axisLabelColor.toRGBString(),
246 "width": this.options.axisLabelWidth + "px",
247 "overflow": "hidden"
248 }
249 };
250
251 // axis lines
252 context.save();
253 context.strokeStyle = this.options.axisLineColor.toRGBString();
254 context.lineWidth = this.options.axisLineWidth;
255
256
257 if (this.options.drawYAxis) {
258 if (this.layout.yticks) {
259 var drawTick = function(tick) {
260 if (typeof(tick) == "function") return;
261 var x = this.area.x;
262 var y = this.area.y + tick[0] * this.area.h;
263 context.beginPath();
264 context.moveTo(x, y);
265 context.lineTo(x - this.options.axisTickSize, y);
266 context.closePath();
267 context.stroke();
268
269 var label = DIV(labelStyle, tick[1]);
270 var top = (y - this.options.axisLabelFontSize / 2);
271 if (top < 0) top = 0;
272
273 if (top + this.options.axisLabelFontSize + 3 > this.height) {
274 label.style.bottom = "0px";
275 } else {
276 label.style.top = top + "px";
277 }
278 label.style.left = "0px";
279 label.style.textAlign = "right";
280 label.style.width = this.options.yAxisLabelWidth + "px";
281 MochiKit.DOM.appendChildNodes(this.container, label);
282 this.ylabels.push(label);
283 };
284
285 MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this));
286
287 // The lowest tick on the y-axis often overlaps with the leftmost
288 // tick on the x-axis. Shift the bottom tick up a little bit to
289 // compensate if necessary.
290 var bottomTick = this.ylabels[0];
291 var fontSize = this.options.axisLabelFontSize;
292 var bottom = parseInt(bottomTick.style.top) + fontSize;
293 if (bottom > this.height - fontSize) {
294 bottomTick.style.top = (parseInt(bottomTick.style.top) -
295 fontSize / 2) + "px";
296 }
297 }
298
299 context.beginPath();
300 context.moveTo(this.area.x, this.area.y);
301 context.lineTo(this.area.x, this.area.y + this.area.h);
302 context.closePath();
303 context.stroke();
304 }
305
306 if (this.options.drawXAxis) {
307 if (this.layout.xticks) {
308 var drawTick = function(tick) {
309 if (typeof(dataset) == "function") return;
310
311 var x = this.area.x + tick[0] * this.area.w;
312 var y = this.area.y + this.area.h;
313 context.beginPath();
314 context.moveTo(x, y);
315 context.lineTo(x, y + this.options.axisTickSize);
316 context.closePath();
317 context.stroke();
318
319 var label = DIV(labelStyle, tick[1]);
320 label.style.textAlign = "center";
321 label.style.bottom = "0px";
322
323 var left = (x - this.options.axisLabelWidth/2);
324 if (left + this.options.axisLabelWidth > this.width) {
325 left = this.width - this.options.xAxisLabelWidth;
326 label.style.textAlign = "right";
327 }
328 if (left < 0) {
329 left = 0;
330 label.style.textAlign = "left";
331 }
332
333 label.style.left = left + "px";
334 label.style.width = this.options.xAxisLabelWidth + "px";
335 MochiKit.DOM.appendChildNodes(this.container, label);
336 this.xlabels.push(label);
337 };
338
339 MochiKit.Iter.forEach(this.layout.xticks, bind(drawTick, this));
340 }
341
342 context.beginPath();
343 context.moveTo(this.area.x, this.area.y + this.area.h);
344 context.lineTo(this.area.x + this.area.w, this.area.y + this.area.h);
345 context.closePath();
346 context.stroke();
347 }
348
349 context.restore();
350
351 };
352
353 PlotKit.CanvasRenderer.prototype._renderBackground = function() {
354 var context = this.element.getContext("2d");
355 context.save();
356 context.fillStyle = this.options.backgroundColor.toRGBString();
357 context.fillRect(0, 0, this.width, this.height);
358 context.restore();
359 };
360
361 PlotKit.CanvasRenderer.prototype.clear = function() {
362 if (this.isIE) {
363 // VML takes a while to start up, so we just poll every this.IEDelay
364 try {
365 if (this.clearDelay) {
366 this.clearDelay.cancel();
367 this.clearDelay = null;
368 }
369 var context = this.element.getContext("2d");
370 }
371 catch (e) {
372 this.isFirstRender = false;
373 this.clearDelay = MochiKit.Async.wait(this.IEDelay);
374 this.clearDelay.addCallback(bind(this.clear, this));
375 return;
376 }
377 }
378
379 var context = this.element.getContext("2d");
380 context.clearRect(0, 0, this.width, this.height);
381
382 MochiKit.Iter.forEach(this.xlabels, MochiKit.DOM.removeElement);
383 MochiKit.Iter.forEach(this.ylabels, MochiKit.DOM.removeElement);
384 this.xlabels = new Array();
385 this.ylabels = new Array();
386 };
387
388 // ----------------------------------------------------------------
389 // Everything below here is experimental and undocumented.
390 // ----------------------------------------------------------------
391
392 PlotKit.CanvasRenderer.prototype._initialiseEvents = function() {
393 var connect = MochiKit.Signal.connect;
394 var bind = MochiKit.Base.bind;
395 //MochiKit.Signal.registerSignals(this, ['onmouseover', 'onclick', 'onmouseout', 'onmousemove']);
396 //connect(this.element, 'onmouseover', bind(this.onmouseover, this));
397 //connect(this.element, 'onmouseout', bind(this.onmouseout, this));
398 //connect(this.element, 'onmousemove', bind(this.onmousemove, this));
399 connect(this.element, 'onclick', bind(this.onclick, this));
400 };
401
402 PlotKit.CanvasRenderer.prototype._resolveObject = function(e) {
403 // does not work in firefox
404 //var x = (e.event().offsetX - this.area.x) / this.area.w;
405 //var y = (e.event().offsetY - this.area.y) / this.area.h;
406
407 var x = (e.mouse().page.x - PlotKit.Base.findPosX(this.element) - this.area.x) / this.area.w;
408 var y = (e.mouse().page.y - PlotKit.Base.findPosY(this.element) - this.area.y) / this.area.h;
409
410 //log(x, y);
411
412 var isHit = this.layout.hitTest(x, y);
413 if (isHit)
414 return isHit;
415 return null;
416 };
417
418 PlotKit.CanvasRenderer.prototype._createEventObject = function(layoutObj, e) {
419 if (layoutObj == null) {
420 return null;
421 }
422
423 e.chart = layoutObj
424 return e;
425 };
426
427
428 PlotKit.CanvasRenderer.prototype.onclick = function(e) {
429 var layoutObject = this._resolveObject(e);
430 var eventObject = this._createEventObject(layoutObject, e);
431 if (eventObject != null)
432 MochiKit.Signal.signal(this, "onclick", eventObject);
433 };
434
435 PlotKit.CanvasRenderer.prototype.onmouseover = function(e) {
436 var layoutObject = this._resolveObject(e);
437 var eventObject = this._createEventObject(layoutObject, e);
438 if (eventObject != null)
439 signal(this, "onmouseover", eventObject);
440 };
441
442 PlotKit.CanvasRenderer.prototype.onmouseout = function(e) {
443 var layoutObject = this._resolveObject(e);
444 var eventObject = this._createEventObject(layoutObject, e);
445 if (eventObject == null)
446 signal(this, "onmouseout", e);
447 else
448 signal(this, "onmouseout", eventObject);
449
450 };
451
452 PlotKit.CanvasRenderer.prototype.onmousemove = function(e) {
453 var layoutObject = this._resolveObject(e);
454 var eventObject = this._createEventObject(layoutObject, e);
455
456 if ((layoutObject == null) && (this.event_isinside == null)) {
457 // TODO: should we emit an event anyway?
458 return;
459 }
460
461 if ((layoutObject != null) && (this.event_isinside == null))
462 signal(this, "onmouseover", eventObject);
463
464 if ((layoutObject == null) && (this.event_isinside != null))
465 signal(this, "onmouseout", eventObject);
466
467 if ((layoutObject != null) && (this.event_isinside != null))
468 signal(this, "onmousemove", eventObject);
469
470 this.event_isinside = layoutObject;
471 //log("move", x, y);
472 };
473
474 PlotKit.CanvasRenderer.isSupported = function(canvasName) {
475 var canvas = null;
476 try {
477 if (MochiKit.Base.isUndefinedOrNull(canvasName))
478 canvas = MochiKit.DOM.CANVAS({});
479 else
480 canvas = MochiKit.DOM.getElement(canvasName);
481 var context = canvas.getContext("2d");
482 }
483 catch (e) {
484 var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
485 var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
486 if ((!ie) || (ie[1] < 6) || (opera))
487 return false;
488 return true;
489 }
490 return true;
491 };
492
493 // Namespace Iniitialisation
494
495 PlotKit.Canvas = {}
496 PlotKit.Canvas.CanvasRenderer = PlotKit.CanvasRenderer;
497
498 PlotKit.Canvas.EXPORT = [
499 "CanvasRenderer"
500 ];
501
502 PlotKit.Canvas.EXPORT_OK = [
503 "CanvasRenderer"
504 ];
505
506 PlotKit.Canvas.__new__ = function() {
507 var m = MochiKit.Base;
508
509 m.nameFunctions(this);
510
511 this.EXPORT_TAGS = {
512 ":common": this.EXPORT,
513 ":all": m.concat(this.EXPORT, this.EXPORT_OK)
514 };
515 };
516
517 PlotKit.Canvas.__new__();
518 MochiKit.Base._exportSymbols(this, PlotKit.Canvas);
519