mercilessly rip out irrelevant PlotKit functions. This may be ill-advised, but 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 "padding": {left: 30, right: 30, top: 5, bottom: 10},
69 "colorScheme": PlotKit.Base.palette(PlotKit.Base.baseColors()[0]),
70 "strokeColor": Color.whiteColor(),
71 "strokeColorTransform": "asStrokeColor",
72 "strokeWidth": 0.5,
73 "shouldFill": true,
74 "shouldStroke": true,
75 "drawXAxis": true,
76 "drawYAxis": true,
77 "axisLineColor": Color.blackColor(),
78 "axisLineWidth": 0.5,
79 "axisTickSize": 3,
80 "axisLabelColor": Color.blackColor(),
81 "axisLabelFont": "Arial",
82 "axisLabelFontSize": 9,
83 "axisLabelWidth": 50,
84 "pieRadius": 0.4,
85 "enableEvents": true
86 };
87 MochiKit.Base.update(this.options, options ? options : {});
88
89 this.layout = layout;
90 this.element = MochiKit.DOM.getElement(element);
91 this.container = this.element.parentNode;
92
93 // Stuff relating to Canvas on IE support
94 this.isIE = PlotKit.Base.excanvasSupported();
95
96 if (this.isIE && !isNil(G_vmlCanvasManager)) {
97 this.IEDelay = 0.5;
98 this.maxTries = 5;
99 this.renderDelay = null;
100 this.clearDelay = null;
101 this.element = G_vmlCanvasManager.initElement(this.element);
102 }
103
104 this.height = this.element.height;
105 this.width = this.element.width;
106
107 // --- check whether everything is ok before we return
108
109 if (isNil(this.element))
110 throw "CanvasRenderer() - passed canvas is not found";
111
112 if (!this.isIE && !(PlotKit.CanvasRenderer.isSupported(this.element)))
113 throw "CanvasRenderer() - Canvas is not supported.";
114
115 if (isNil(this.container) || (this.container.nodeName.toLowerCase() != "div"))
116 throw "CanvasRenderer() - <canvas> needs to be enclosed in <div>";
117
118 // internal state
119 this.xlabels = new Array();
120 this.ylabels = new Array();
121 this.isFirstRender = true;
122
123 this.area = {
124 x: this.options.padding.left,
125 y: this.options.padding.top,
126 w: this.width - this.options.padding.left - this.options.padding.right,
127 h: this.height - this.options.padding.top - this.options.padding.bottom
128 };
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 label.style.top = (y - this.options.axisLabelFontSize) + "px";
271 label.style.left = (x - this.options.padding.left - this.options.axisTickSize) + "px";
272 label.style.textAlign = "right";
273 label.style.width = (this.options.padding.left - this.options.axisTickSize * 2) + "px";
274 MochiKit.DOM.appendChildNodes(this.container, label);
275 this.ylabels.push(label);
276 };
277
278 MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this));
279 }
280
281 context.beginPath();
282 context.moveTo(this.area.x, this.area.y);
283 context.lineTo(this.area.x, this.area.y + this.area.h);
284 context.closePath();
285 context.stroke();
286 }
287
288 if (this.options.drawXAxis) {
289 if (this.layout.xticks) {
290 var drawTick = function(tick) {
291 if (typeof(dataset) == "function") return;
292
293 var x = this.area.x + tick[0] * this.area.w;
294 var y = this.area.y + this.area.h;
295 context.beginPath();
296 context.moveTo(x, y);
297 context.lineTo(x, y + this.options.axisTickSize);
298 context.closePath();
299 context.stroke();
300
301 var label = DIV(labelStyle, tick[1]);
302 label.style.top = (y + this.options.axisTickSize) + "px";
303 label.style.left = (x - this.options.axisLabelWidth/2) + "px";
304 label.style.textAlign = "center";
305 label.style.width = this.options.axisLabelWidth + "px";
306 MochiKit.DOM.appendChildNodes(this.container, label);
307 this.xlabels.push(label);
308 };
309
310 MochiKit.Iter.forEach(this.layout.xticks, bind(drawTick, this));
311 }
312
313 context.beginPath();
314 context.moveTo(this.area.x, this.area.y + this.area.h);
315 context.lineTo(this.area.x + this.area.w, this.area.y + this.area.h);
316 context.closePath();
317 context.stroke();
318 }
319
320 context.restore();
321
322 };
323
324 PlotKit.CanvasRenderer.prototype._renderBackground = function() {
325 var context = this.element.getContext("2d");
326 context.save();
327 context.fillStyle = this.options.backgroundColor.toRGBString();
328 context.fillRect(0, 0, this.width, this.height);
329 context.restore();
330 };
331
332 PlotKit.CanvasRenderer.prototype.clear = function() {
333 if (this.isIE) {
334 // VML takes a while to start up, so we just poll every this.IEDelay
335 try {
336 if (this.clearDelay) {
337 this.clearDelay.cancel();
338 this.clearDelay = null;
339 }
340 var context = this.element.getContext("2d");
341 }
342 catch (e) {
343 this.isFirstRender = false;
344 this.clearDelay = MochiKit.Async.wait(this.IEDelay);
345 this.clearDelay.addCallback(bind(this.clear, this));
346 return;
347 }
348 }
349
350 var context = this.element.getContext("2d");
351 context.clearRect(0, 0, this.width, this.height);
352
353 MochiKit.Iter.forEach(this.xlabels, MochiKit.DOM.removeElement);
354 MochiKit.Iter.forEach(this.ylabels, MochiKit.DOM.removeElement);
355 this.xlabels = new Array();
356 this.ylabels = new Array();
357 };
358
359 // ----------------------------------------------------------------
360 // Everything below here is experimental and undocumented.
361 // ----------------------------------------------------------------
362
363 PlotKit.CanvasRenderer.prototype._initialiseEvents = function() {
364 var connect = MochiKit.Signal.connect;
365 var bind = MochiKit.Base.bind;
366 //MochiKit.Signal.registerSignals(this, ['onmouseover', 'onclick', 'onmouseout', 'onmousemove']);
367 //connect(this.element, 'onmouseover', bind(this.onmouseover, this));
368 //connect(this.element, 'onmouseout', bind(this.onmouseout, this));
369 //connect(this.element, 'onmousemove', bind(this.onmousemove, this));
370 connect(this.element, 'onclick', bind(this.onclick, this));
371 };
372
373 PlotKit.CanvasRenderer.prototype._resolveObject = function(e) {
374 // does not work in firefox
375 //var x = (e.event().offsetX - this.area.x) / this.area.w;
376 //var y = (e.event().offsetY - this.area.y) / this.area.h;
377
378 var x = (e.mouse().page.x - PlotKit.Base.findPosX(this.element) - this.area.x) / this.area.w;
379 var y = (e.mouse().page.y - PlotKit.Base.findPosY(this.element) - this.area.y) / this.area.h;
380
381 //log(x, y);
382
383 var isHit = this.layout.hitTest(x, y);
384 if (isHit)
385 return isHit;
386 return null;
387 };
388
389 PlotKit.CanvasRenderer.prototype._createEventObject = function(layoutObj, e) {
390 if (layoutObj == null) {
391 return null;
392 }
393
394 e.chart = layoutObj
395 return e;
396 };
397
398
399 PlotKit.CanvasRenderer.prototype.onclick = function(e) {
400 var layoutObject = this._resolveObject(e);
401 var eventObject = this._createEventObject(layoutObject, e);
402 if (eventObject != null)
403 MochiKit.Signal.signal(this, "onclick", eventObject);
404 };
405
406 PlotKit.CanvasRenderer.prototype.onmouseover = function(e) {
407 var layoutObject = this._resolveObject(e);
408 var eventObject = this._createEventObject(layoutObject, e);
409 if (eventObject != null)
410 signal(this, "onmouseover", eventObject);
411 };
412
413 PlotKit.CanvasRenderer.prototype.onmouseout = function(e) {
414 var layoutObject = this._resolveObject(e);
415 var eventObject = this._createEventObject(layoutObject, e);
416 if (eventObject == null)
417 signal(this, "onmouseout", e);
418 else
419 signal(this, "onmouseout", eventObject);
420
421 };
422
423 PlotKit.CanvasRenderer.prototype.onmousemove = function(e) {
424 var layoutObject = this._resolveObject(e);
425 var eventObject = this._createEventObject(layoutObject, e);
426
427 if ((layoutObject == null) && (this.event_isinside == null)) {
428 // TODO: should we emit an event anyway?
429 return;
430 }
431
432 if ((layoutObject != null) && (this.event_isinside == null))
433 signal(this, "onmouseover", eventObject);
434
435 if ((layoutObject == null) && (this.event_isinside != null))
436 signal(this, "onmouseout", eventObject);
437
438 if ((layoutObject != null) && (this.event_isinside != null))
439 signal(this, "onmousemove", eventObject);
440
441 this.event_isinside = layoutObject;
442 //log("move", x, y);
443 };
444
445 PlotKit.CanvasRenderer.isSupported = function(canvasName) {
446 var canvas = null;
447 try {
448 if (MochiKit.Base.isUndefinedOrNull(canvasName))
449 canvas = MochiKit.DOM.CANVAS({});
450 else
451 canvas = MochiKit.DOM.getElement(canvasName);
452 var context = canvas.getContext("2d");
453 }
454 catch (e) {
455 var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
456 var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
457 if ((!ie) || (ie[1] < 6) || (opera))
458 return false;
459 return true;
460 }
461 return true;
462 };
463
464 // Namespace Iniitialisation
465
466 PlotKit.Canvas = {}
467 PlotKit.Canvas.CanvasRenderer = PlotKit.CanvasRenderer;
468
469 PlotKit.Canvas.EXPORT = [
470 "CanvasRenderer"
471 ];
472
473 PlotKit.Canvas.EXPORT_OK = [
474 "CanvasRenderer"
475 ];
476
477 PlotKit.Canvas.__new__ = function() {
478 var m = MochiKit.Base;
479
480 m.nameFunctions(this);
481
482 this.EXPORT_TAGS = {
483 ":common": this.EXPORT,
484 ":all": m.concat(this.EXPORT, this.EXPORT_OK)
485 };
486 };
487
488 PlotKit.Canvas.__new__();
489 MochiKit.Base._exportSymbols(this, PlotKit.Canvas);
490