5 Provides HTML Canvas Renderer. This is supported under:
10 - IE 6 (via VML Emulation)
12 It uses DIVs for labels.
16 Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
17 For use under the BSD license. <http://www.liquidx.net/plotkit>
20 // --------------------------------------------------------------------
21 // Check required components
22 // --------------------------------------------------------------------
25 if ((typeof(PlotKit
.Base
) == 'undefined') ||
26 (typeof(PlotKit
.Layout
) == 'undefined'))
32 throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.{Base,Layout}"
36 // ------------------------------------------------------------------------
37 // Defines the renderer class
38 // ------------------------------------------------------------------------
40 if (typeof(PlotKit
.CanvasRenderer
) == 'undefined') {
41 PlotKit
.CanvasRenderer
= {};
44 PlotKit
.CanvasRenderer
.NAME
= "PlotKit.CanvasRenderer";
45 PlotKit
.CanvasRenderer
.VERSION
= PlotKit
.VERSION
;
47 PlotKit
.CanvasRenderer
.__repr__
= function() {
48 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
51 PlotKit
.CanvasRenderer
.toString
= function() {
52 return this.__repr__();
55 PlotKit
.CanvasRenderer
= function(element
, layout
, options
) {
56 if (arguments
.length
> 0)
57 this.__init__(element
, layout
, options
);
60 PlotKit
.CanvasRenderer
.prototype.__init__
= function(element
, layout
, options
) {
61 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
62 var Color
= MochiKit
.Color
.Color
;
66 "drawBackground": true,
67 "backgroundColor": Color
.whiteColor(),
68 "colorScheme": PlotKit
.Base
.palette(PlotKit
.Base
.baseColors()[0]),
69 "strokeColor": Color
.whiteColor(),
70 "strokeColorTransform": "asStrokeColor",
76 "axisLineColor": Color
.blackColor(),
79 "axisLabelColor": Color
.blackColor(),
80 "axisLabelFont": "Arial",
81 "axisLabelFontSize": 9,
86 MochiKit
.Base
.update(this.options
, options
? options
: {});
89 this.element
= MochiKit
.DOM
.getElement(element
);
90 this.container
= this.element
.parentNode
;
92 // Stuff relating to Canvas on IE support
93 this.isIE
= PlotKit
.Base
.excanvasSupported();
95 if (this.isIE
&& !isNil(G_vmlCanvasManager
)) {
98 this.renderDelay
= null;
99 this.clearDelay
= null;
100 this.element
= G_vmlCanvasManager
.initElement(this.element
);
103 this.height
= this.element
.height
;
104 this.width
= this.element
.width
;
106 // --- check whether everything is ok before we return
108 if (isNil(this.element
))
109 throw "CanvasRenderer() - passed canvas is not found";
111 if (!this.isIE
&& !(PlotKit
.CanvasRenderer
.isSupported(this.element
)))
112 throw "CanvasRenderer() - Canvas is not supported.";
114 if (isNil(this.container
) || (this.container
.nodeName
.toLowerCase() != "div"))
115 throw "CanvasRenderer() - <canvas> needs to be enclosed in <div>";
118 this.xlabels
= new Array();
119 this.ylabels
= new Array();
120 this.isFirstRender
= true;
123 x
: this.options
.yAxisLabelWidth
+ 2 * this.options
.axisTickSize
,
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
;
130 MochiKit
.DOM
.updateNodeAttributes(this.container
,
131 {"style":{ "position": "relative", "width": this.width
+ "px"}});
133 // load event system if we have Signals
134 /* Disabled until we have a proper implementation
136 this.event_isinside = null;
137 if (MochiKit.Signal && this.options.enableEvents) {
138 this._initialiseEvents();
142 // still experimental
147 PlotKit
.CanvasRenderer
.prototype.render
= function() {
149 // VML takes a while to start up, so we just poll every this.IEDelay
151 if (this.renderDelay
) {
152 this.renderDelay
.cancel();
153 this.renderDelay
= null;
155 var context
= this.element
.getContext("2d");
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));
167 if (this.options
.drawBackground
)
168 this._renderBackground();
170 if (this.layout
.style
== "line") {
171 this._renderLineChart();
172 this._renderLineAxis();
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
;
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
;
190 // setup graphics context
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();
198 context
.lineWidth
= this.options
.strokeWidth
;
201 var makePath
= function(ctx
) {
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
);
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
);
216 if (this.options
.shouldFill
) {
217 bind(makePath
, this)(context
);
220 if (this.options
.shouldStroke
) {
221 bind(makePath
, this)(context
);
230 PlotKit
.CanvasRenderer
.prototype._renderLineAxis
= function() {
235 PlotKit
.CanvasRenderer
.prototype._renderAxis
= function() {
236 if (!this.options
.drawXAxis
&& !this.options
.drawYAxis
)
239 var context
= this.element
.getContext("2d");
241 var labelStyle
= {"style":
242 {"position": "absolute",
243 "fontSize": this.options
.axisLabelFontSize
+ "px",
245 "color": this.options
.axisLabelColor
.toRGBString(),
246 "width": this.options
.axisLabelWidth
+ "px",
253 context
.strokeStyle
= this.options
.axisLineColor
.toRGBString();
254 context
.lineWidth
= this.options
.axisLineWidth
;
257 if (this.options
.drawYAxis
) {
258 if (this.layout
.yticks
) {
259 var drawTick
= function(tick
) {
260 if (typeof(tick
) == "function") return;
262 var y
= this.area
.y
+ tick
[0] * this.area
.h
;
264 context
.moveTo(x
, y
);
265 context
.lineTo(x
- this.options
.axisTickSize
, y
);
269 var label
= DIV(labelStyle
, tick
[1]);
270 var top
= (y
- this.options
.axisLabelFontSize
/ 2);
271 if (top
< 0) top
= 0;
273 if (top
+ this.options
.axisLabelFontSize
+ 3 > this.height
) {
274 label
.style
.bottom
= "0px";
276 label
.style
.top
= top
+ "px";
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
);
285 MochiKit
.Iter
.forEach(this.layout
.yticks
, bind(drawTick
, this));
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";
300 context
.moveTo(this.area
.x
, this.area
.y
);
301 context
.lineTo(this.area
.x
, this.area
.y
+ this.area
.h
);
306 if (this.options
.drawXAxis
) {
307 if (this.layout
.xticks
) {
308 var drawTick
= function(tick
) {
309 if (typeof(dataset
) == "function") return;
311 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
312 var y
= this.area
.y
+ this.area
.h
;
314 context
.moveTo(x
, y
);
315 context
.lineTo(x
, y
+ this.options
.axisTickSize
);
319 var label
= DIV(labelStyle
, tick
[1]);
320 label
.style
.textAlign
= "center";
321 label
.style
.bottom
= "0px";
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";
330 label
.style
.textAlign
= "left";
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
);
339 MochiKit
.Iter
.forEach(this.layout
.xticks
, bind(drawTick
, this));
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
);
353 PlotKit
.CanvasRenderer
.prototype._renderBackground
= function() {
354 var context
= this.element
.getContext("2d");
356 context
.fillStyle
= this.options
.backgroundColor
.toRGBString();
357 context
.fillRect(0, 0, this.width
, this.height
);
361 PlotKit
.CanvasRenderer
.prototype.clear
= function() {
363 // VML takes a while to start up, so we just poll every this.IEDelay
365 if (this.clearDelay
) {
366 this.clearDelay
.cancel();
367 this.clearDelay
= null;
369 var context
= this.element
.getContext("2d");
372 this.isFirstRender
= false;
373 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
374 this.clearDelay
.addCallback(bind(this.clear
, this));
379 var context
= this.element
.getContext("2d");
380 context
.clearRect(0, 0, this.width
, this.height
);
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();
388 // ----------------------------------------------------------------
389 // Everything below here is experimental and undocumented.
390 // ----------------------------------------------------------------
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));
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
;
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
;
412 var isHit
= this.layout
.hitTest(x
, y
);
418 PlotKit
.CanvasRenderer
.prototype._createEventObject
= function(layoutObj
, e
) {
419 if (layoutObj
== null) {
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
);
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
);
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
);
448 signal(this, "onmouseout", eventObject
);
452 PlotKit
.CanvasRenderer
.prototype.onmousemove
= function(e
) {
453 var layoutObject
= this._resolveObject(e
);
454 var eventObject
= this._createEventObject(layoutObject
, e
);
456 if ((layoutObject
== null) && (this.event_isinside
== null)) {
457 // TODO: should we emit an event anyway?
461 if ((layoutObject
!= null) && (this.event_isinside
== null))
462 signal(this, "onmouseover", eventObject
);
464 if ((layoutObject
== null) && (this.event_isinside
!= null))
465 signal(this, "onmouseout", eventObject
);
467 if ((layoutObject
!= null) && (this.event_isinside
!= null))
468 signal(this, "onmousemove", eventObject
);
470 this.event_isinside
= layoutObject
;
474 PlotKit
.CanvasRenderer
.isSupported
= function(canvasName
) {
477 if (MochiKit
.Base
.isUndefinedOrNull(canvasName
))
478 canvas
= MochiKit
.DOM
.CANVAS({});
480 canvas
= MochiKit
.DOM
.getElement(canvasName
);
481 var context
= canvas
.getContext("2d");
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
))
493 // Namespace Iniitialisation
496 PlotKit
.Canvas
.CanvasRenderer
= PlotKit
.CanvasRenderer
;
498 PlotKit
.Canvas
.EXPORT
= [
502 PlotKit
.Canvas
.EXPORT_OK
= [
506 PlotKit
.Canvas
.__new__
= function() {
507 var m
= MochiKit
.Base
;
509 m
.nameFunctions(this);
512 ":common": this.EXPORT
,
513 ":all": m
.concat(this.EXPORT
, this.EXPORT_OK
)
517 PlotKit
.Canvas
.__new__();
518 MochiKit
.Base
._exportSymbols(this, PlotKit
.Canvas
);