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 "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",
77 "axisLineColor": Color
.blackColor(),
80 "axisLabelColor": Color
.blackColor(),
81 "axisLabelFont": "Arial",
82 "axisLabelFontSize": 9,
87 MochiKit
.Base
.update(this.options
, options
? options
: {});
90 this.element
= MochiKit
.DOM
.getElement(element
);
91 this.container
= this.element
.parentNode
;
93 // Stuff relating to Canvas on IE support
94 this.isIE
= PlotKit
.Base
.excanvasSupported();
96 if (this.isIE
&& !isNil(G_vmlCanvasManager
)) {
99 this.renderDelay
= null;
100 this.clearDelay
= null;
101 this.element
= G_vmlCanvasManager
.initElement(this.element
);
104 this.height
= this.element
.height
;
105 this.width
= this.element
.width
;
107 // --- check whether everything is ok before we return
109 if (isNil(this.element
))
110 throw "CanvasRenderer() - passed canvas is not found";
112 if (!this.isIE
&& !(PlotKit
.CanvasRenderer
.isSupported(this.element
)))
113 throw "CanvasRenderer() - Canvas is not supported.";
115 if (isNil(this.container
) || (this.container
.nodeName
.toLowerCase() != "div"))
116 throw "CanvasRenderer() - <canvas> needs to be enclosed in <div>";
119 this.xlabels
= new Array();
120 this.ylabels
= new Array();
121 this.isFirstRender
= true;
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
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
== "bar") {
171 this._renderBarChart();
172 this._renderBarAxis();
174 else if (this.layout
.style
== "pie") {
175 this._renderPieChart();
176 this._renderPieAxis();
178 else if (this.layout
.style
== "line") {
179 this._renderLineChart();
180 this._renderLineAxis();
184 PlotKit
.CanvasRenderer
.prototype._renderBarChartWrap
= function(data
, plotFunc
) {
185 var context
= this.element
.getContext("2d");
186 var colorCount
= this.options
.colorScheme
.length
;
187 var colorScheme
= this.options
.colorScheme
;
188 var setNames
= MochiKit
.Base
.keys(this.layout
.datasets
);
189 var setCount
= setNames
.length
;
191 for (var i
= 0; i
< setCount
; i
++) {
192 var setName
= setNames
[i
];
193 var color
= colorScheme
[i
%colorCount
];
195 context
.fillStyle
= color
.toRGBString();
196 if (this.options
.strokeColor
)
197 context
.strokeStyle
= this.options
.strokeColor
.toRGBString();
198 else if (this.options
.strokeColorTransform
)
199 context
.strokeStyle
= color
[this.options
.strokeColorTransform
]().toRGBString();
201 context
.lineWidth
= this.options
.strokeWidth
;
202 var forEachFunc
= function(obj
) {
203 if (obj
.name
== setName
)
204 plotFunc(context
, obj
);
207 MochiKit
.Iter
.forEach(data
, bind(forEachFunc
, this));
212 PlotKit
.CanvasRenderer
.prototype._renderBarChart
= function() {
213 var bind
= MochiKit
.Base
.bind
;
215 var drawRect
= function(context
, bar
) {
216 var x
= this.area
.w
* bar
.x
+ this.area
.x
;
217 var y
= this.area
.h
* bar
.y
+ this.area
.y
;
218 var w
= this.area
.w
* bar
.w
;
219 var h
= this.area
.h
* bar
.h
;
220 if ((w
< 1) || (h
< 1))
222 if (this.options
.shouldFill
)
223 context
.fillRect(x
, y
, w
, h
);
224 if (this.options
.shouldStroke
)
225 context
.strokeRect(x
, y
, w
, h
);
227 this._renderBarChartWrap(this.layout
.bars
, bind(drawRect
, this));
230 PlotKit
.CanvasRenderer
.prototype._renderLineChart
= function() {
231 var context
= this.element
.getContext("2d");
232 var colorCount
= this.options
.colorScheme
.length
;
233 var colorScheme
= this.options
.colorScheme
;
234 var setNames
= MochiKit
.Base
.keys(this.layout
.datasets
);
235 var setCount
= setNames
.length
;
236 var bind
= MochiKit
.Base
.bind
;
237 var partial
= MochiKit
.Base
.partial
;
239 for (var i
= 0; i
< setCount
; i
++) {
240 var setName
= setNames
[i
];
241 var color
= colorScheme
[i
%colorCount
];
242 var strokeX
= this.options
.strokeColorTransform
;
244 // setup graphics context
246 context
.fillStyle
= color
.toRGBString();
247 if (this.options
.strokeColor
)
248 context
.strokeStyle
= this.options
.strokeColor
.toRGBString();
249 else if (this.options
.strokeColorTransform
)
250 context
.strokeStyle
= color
[strokeX
]().toRGBString();
252 context
.lineWidth
= this.options
.strokeWidth
;
255 var makePath
= function(ctx
) {
257 ctx
.moveTo(this.area
.x
, this.area
.y
+ this.area
.h
);
258 var addPoint
= function(ctx_
, point
) {
259 if (point
.name
== setName
)
260 ctx_
.lineTo(this.area
.w
* point
.x
+ this.area
.x
,
261 this.area
.h
* point
.y
+ this.area
.y
);
263 MochiKit
.Iter
.forEach(this.layout
.points
, partial(addPoint
, ctx
), this);
264 ctx
.lineTo(this.area
.w
+ this.area
.x
,
265 this.area
.h
+ this.area
.y
);
266 ctx
.lineTo(this.area
.x
, this.area
.y
+ this.area
.h
);
270 if (this.options
.shouldFill
) {
271 bind(makePath
, this)(context
);
274 if (this.options
.shouldStroke
) {
275 bind(makePath
, this)(context
);
283 PlotKit
.CanvasRenderer
.prototype._renderPieChart
= function() {
284 var context
= this.element
.getContext("2d");
285 var colorCount
= this.options
.colorScheme
.length
;
286 var slices
= this.layout
.slices
;
288 var centerx
= this.area
.x
+ this.area
.w
* 0.5;
289 var centery
= this.area
.y
+ this.area
.h
* 0.5;
290 var radius
= Math
.min(this.area
.w
* this.options
.pieRadius
,
291 this.area
.h
* this.options
.pieRadius
);
294 centerx
= parseInt(centerx
);
295 centery
= parseInt(centery
);
296 radius
= parseInt(radius
);
300 // NOTE NOTE!! Canvas Tag draws the circle clockwise from the y = 0, x = 1
301 // so we have to subtract 90 degrees to make it start at y = 1, x = 0
303 for (var i
= 0; i
< slices
.length
; i
++) {
304 var color
= this.options
.colorScheme
[i
%colorCount
];
306 context
.fillStyle
= color
.toRGBString();
308 var makePath
= function() {
310 context
.moveTo(centerx
, centery
);
311 context
.arc(centerx
, centery
, radius
,
312 slices
[i
].startAngle
- Math
.PI
/2,
313 slices
[i
].endAngle
- Math
.PI
/2,
315 context
.lineTo(centerx
, centery
);
319 if (Math
.abs(slices
[i
].startAngle
- slices
[i
].endAngle
) > 0.001) {
320 if (this.options
.shouldFill
) {
325 if (this.options
.shouldStroke
) {
327 context
.lineWidth
= this.options
.strokeWidth
;
328 if (this.options
.strokeColor
)
329 context
.strokeStyle
= this.options
.strokeColor
.toRGBString();
330 else if (this.options
.strokeColorTransform
)
331 context
.strokeStyle
= color
[this.options
.strokeColorTransform
]().toRGBString();
339 PlotKit
.CanvasRenderer
.prototype._renderBarAxis
= function() {
343 PlotKit
.CanvasRenderer
.prototype._renderLineAxis
= function() {
348 PlotKit
.CanvasRenderer
.prototype._renderAxis
= function() {
349 if (!this.options
.drawXAxis
&& !this.options
.drawYAxis
)
352 var context
= this.element
.getContext("2d");
354 var labelStyle
= {"style":
355 {"position": "absolute",
356 "fontSize": this.options
.axisLabelFontSize
+ "px",
358 "color": this.options
.axisLabelColor
.toRGBString(),
359 "width": this.options
.axisLabelWidth
+ "px",
366 context
.strokeStyle
= this.options
.axisLineColor
.toRGBString();
367 context
.lineWidth
= this.options
.axisLineWidth
;
370 if (this.options
.drawYAxis
) {
371 if (this.layout
.yticks
) {
372 var drawTick
= function(tick
) {
373 if (typeof(tick
) == "function") return;
375 var y
= this.area
.y
+ tick
[0] * this.area
.h
;
377 context
.moveTo(x
, y
);
378 context
.lineTo(x
- this.options
.axisTickSize
, y
);
382 var label
= DIV(labelStyle
, tick
[1]);
383 label
.style
.top
= (y
- this.options
.axisLabelFontSize
) + "px";
384 label
.style
.left
= (x
- this.options
.padding
.left
- this.options
.axisTickSize
) + "px";
385 label
.style
.textAlign
= "right";
386 label
.style
.width
= (this.options
.padding
.left
- this.options
.axisTickSize
* 2) + "px";
387 MochiKit
.DOM
.appendChildNodes(this.container
, label
);
388 this.ylabels
.push(label
);
391 MochiKit
.Iter
.forEach(this.layout
.yticks
, bind(drawTick
, this));
395 context
.moveTo(this.area
.x
, this.area
.y
);
396 context
.lineTo(this.area
.x
, this.area
.y
+ this.area
.h
);
401 if (this.options
.drawXAxis
) {
402 if (this.layout
.xticks
) {
403 var drawTick
= function(tick
) {
404 if (typeof(dataset
) == "function") return;
406 var x
= this.area
.x
+ tick
[0] * this.area
.w
;
407 var y
= this.area
.y
+ this.area
.h
;
409 context
.moveTo(x
, y
);
410 context
.lineTo(x
, y
+ this.options
.axisTickSize
);
414 var label
= DIV(labelStyle
, tick
[1]);
415 label
.style
.top
= (y
+ this.options
.axisTickSize
) + "px";
416 label
.style
.left
= (x
- this.options
.axisLabelWidth
/2) + "px";
417 label
.style
.textAlign
= "center";
418 label
.style
.width
= this.options
.axisLabelWidth
+ "px";
419 MochiKit
.DOM
.appendChildNodes(this.container
, label
);
420 this.xlabels
.push(label
);
423 MochiKit
.Iter
.forEach(this.layout
.xticks
, bind(drawTick
, this));
427 context
.moveTo(this.area
.x
, this.area
.y
+ this.area
.h
);
428 context
.lineTo(this.area
.x
+ this.area
.w
, this.area
.y
+ this.area
.h
);
437 PlotKit
.CanvasRenderer
.prototype._renderPieAxis
= function() {
438 if (!this.options
.drawXAxis
)
441 if (this.layout
.xticks
) {
442 // make a lookup dict for x->slice values
443 var lookup
= new Array();
444 for (var i
= 0; i
< this.layout
.slices
.length
; i
++) {
445 lookup
[this.layout
.slices
[i
].xval
] = this.layout
.slices
[i
];
448 var centerx
= this.area
.x
+ this.area
.w
* 0.5;
449 var centery
= this.area
.y
+ this.area
.h
* 0.5;
450 var radius
= Math
.min(this.area
.w
* this.options
.pieRadius
,
451 this.area
.h
* this.options
.pieRadius
);
452 var labelWidth
= this.options
.axisLabelWidth
;
454 for (var i
= 0; i
< this.layout
.xticks
.length
; i
++) {
455 var slice
= lookup
[this.layout
.xticks
[i
][0]];
456 if (MochiKit
.Base
.isUndefinedOrNull(slice
))
460 var angle
= (slice
.startAngle
+ slice
.endAngle
)/2;
461 // normalize the angle
462 var normalisedAngle
= angle
;
463 if (normalisedAngle
> Math
.PI
* 2)
464 normalisedAngle
= normalisedAngle
- Math
.PI
* 2;
465 else if (normalisedAngle
< 0)
466 normalisedAngle
= normalisedAngle
+ Math
.PI
* 2;
468 var labelx
= centerx
+ Math
.sin(normalisedAngle
) * (radius
+ 10);
469 var labely
= centery
- Math
.cos(normalisedAngle
) * (radius
+ 10);
471 var attrib
= {"position": "absolute",
473 "width": labelWidth
+ "px",
474 "fontSize": this.options
.axisLabelFontSize
+ "px",
475 "overflow": "hidden",
476 "color": this.options
.axisLabelColor
.toHexString()
479 if (normalisedAngle
<= Math
.PI
* 0.5) {
480 // text on top and align left
481 attrib
["textAlign"] = "left";
482 attrib
["verticalAlign"] = "top";
483 attrib
["left"] = labelx
+ "px";
484 attrib
["top"] = (labely
- this.options
.axisLabelFontSize
) + "px";
486 else if ((normalisedAngle
> Math
.PI
* 0.5) && (normalisedAngle
<= Math
.PI
)) {
487 // text on bottom and align left
488 attrib
["textAlign"] = "left";
489 attrib
["verticalAlign"] = "bottom";
490 attrib
["left"] = labelx
+ "px";
491 attrib
["top"] = labely
+ "px";
494 else if ((normalisedAngle
> Math
.PI
) && (normalisedAngle
<= Math
.PI
*1.5)) {
495 // text on bottom and align right
496 attrib
["textAlign"] = "right";
497 attrib
["verticalAlign"] = "bottom";
498 attrib
["left"] = (labelx
- labelWidth
) + "px";
499 attrib
["top"] = labely
+ "px";
502 // text on top and align right
503 attrib
["textAlign"] = "right";
504 attrib
["verticalAlign"] = "bottom";
505 attrib
["left"] = (labelx
- labelWidth
) + "px";
506 attrib
["top"] = (labely
- this.options
.axisLabelFontSize
) + "px";
509 var label
= DIV({'style': attrib
}, this.layout
.xticks
[i
][1]);
510 this.xlabels
.push(label
);
511 MochiKit
.DOM
.appendChildNodes(this.container
, label
);
517 PlotKit
.CanvasRenderer
.prototype._renderBackground
= function() {
518 var context
= this.element
.getContext("2d");
520 context
.fillStyle
= this.options
.backgroundColor
.toRGBString();
521 context
.fillRect(0, 0, this.width
, this.height
);
525 PlotKit
.CanvasRenderer
.prototype.clear
= function() {
527 // VML takes a while to start up, so we just poll every this.IEDelay
529 if (this.clearDelay
) {
530 this.clearDelay
.cancel();
531 this.clearDelay
= null;
533 var context
= this.element
.getContext("2d");
536 this.isFirstRender
= false;
537 this.clearDelay
= MochiKit
.Async
.wait(this.IEDelay
);
538 this.clearDelay
.addCallback(bind(this.clear
, this));
543 var context
= this.element
.getContext("2d");
544 context
.clearRect(0, 0, this.width
, this.height
);
546 MochiKit
.Iter
.forEach(this.xlabels
, MochiKit
.DOM
.removeElement
);
547 MochiKit
.Iter
.forEach(this.ylabels
, MochiKit
.DOM
.removeElement
);
548 this.xlabels
= new Array();
549 this.ylabels
= new Array();
552 // ----------------------------------------------------------------
553 // Everything below here is experimental and undocumented.
554 // ----------------------------------------------------------------
556 PlotKit
.CanvasRenderer
.prototype._initialiseEvents
= function() {
557 var connect
= MochiKit
.Signal
.connect
;
558 var bind
= MochiKit
.Base
.bind
;
559 //MochiKit.Signal.registerSignals(this, ['onmouseover', 'onclick', 'onmouseout', 'onmousemove']);
560 //connect(this.element, 'onmouseover', bind(this.onmouseover, this));
561 //connect(this.element, 'onmouseout', bind(this.onmouseout, this));
562 //connect(this.element, 'onmousemove', bind(this.onmousemove, this));
563 connect(this.element
, 'onclick', bind(this.onclick
, this));
566 PlotKit
.CanvasRenderer
.prototype._resolveObject
= function(e
) {
567 // does not work in firefox
568 //var x = (e.event().offsetX - this.area.x) / this.area
.w
;
569 //var y = (e.event().offsetY - this.area.y) / this.area
.h
;
571 var x
= (e
.mouse().page
.x
- PlotKit
.Base
.findPosX(this.element
) - this.area
.x
) / this.area
.w
;
572 var y
= (e
.mouse().page
.y
- PlotKit
.Base
.findPosY(this.element
) - this.area
.y
) / this.area
.h
;
576 var isHit
= this.layout
.hitTest(x
, y
);
582 PlotKit
.CanvasRenderer
.prototype._createEventObject
= function(layoutObj
, e
) {
583 if (layoutObj
== null) {
592 PlotKit
.CanvasRenderer
.prototype.onclick
= function(e
) {
593 var layoutObject
= this._resolveObject(e
);
594 var eventObject
= this._createEventObject(layoutObject
, e
);
595 if (eventObject
!= null)
596 MochiKit
.Signal
.signal(this, "onclick", eventObject
);
599 PlotKit
.CanvasRenderer
.prototype.onmouseover
= function(e
) {
600 var layoutObject
= this._resolveObject(e
);
601 var eventObject
= this._createEventObject(layoutObject
, e
);
602 if (eventObject
!= null)
603 signal(this, "onmouseover", eventObject
);
606 PlotKit
.CanvasRenderer
.prototype.onmouseout
= function(e
) {
607 var layoutObject
= this._resolveObject(e
);
608 var eventObject
= this._createEventObject(layoutObject
, e
);
609 if (eventObject
== null)
610 signal(this, "onmouseout", e
);
612 signal(this, "onmouseout", eventObject
);
616 PlotKit
.CanvasRenderer
.prototype.onmousemove
= function(e
) {
617 var layoutObject
= this._resolveObject(e
);
618 var eventObject
= this._createEventObject(layoutObject
, e
);
620 if ((layoutObject
== null) && (this.event_isinside
== null)) {
621 // TODO: should we emit an event anyway?
625 if ((layoutObject
!= null) && (this.event_isinside
== null))
626 signal(this, "onmouseover", eventObject
);
628 if ((layoutObject
== null) && (this.event_isinside
!= null))
629 signal(this, "onmouseout", eventObject
);
631 if ((layoutObject
!= null) && (this.event_isinside
!= null))
632 signal(this, "onmousemove", eventObject
);
634 this.event_isinside
= layoutObject
;
638 PlotKit
.CanvasRenderer
.isSupported
= function(canvasName
) {
641 if (MochiKit
.Base
.isUndefinedOrNull(canvasName
))
642 canvas
= MochiKit
.DOM
.CANVAS({});
644 canvas
= MochiKit
.DOM
.getElement(canvasName
);
645 var context
= canvas
.getContext("2d");
648 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
649 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
650 if ((!ie
) || (ie
[1] < 6) || (opera
))
657 // Namespace Iniitialisation
660 PlotKit
.Canvas
.CanvasRenderer
= PlotKit
.CanvasRenderer
;
662 PlotKit
.Canvas
.EXPORT
= [
666 PlotKit
.Canvas
.EXPORT_OK
= [
670 PlotKit
.Canvas
.__new__
= function() {
671 var m
= MochiKit
.Base
;
673 m
.nameFunctions(this);
676 ":common": this.EXPORT
,
677 ":all": m
.concat(this.EXPORT
, this.EXPORT_OK
)
681 PlotKit
.Canvas
.__new__();
682 MochiKit
.Base
._exportSymbols(this, PlotKit
.Canvas
);