| 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 == "bar") { |
| 171 | this._renderBarChart(); |
| 172 | this._renderBarAxis(); |
| 173 | } |
| 174 | else if (this.layout.style == "pie") { |
| 175 | this._renderPieChart(); |
| 176 | this._renderPieAxis(); |
| 177 | } |
| 178 | else if (this.layout.style == "line") { |
| 179 | this._renderLineChart(); |
| 180 | this._renderLineAxis(); |
| 181 | } |
| 182 | }; |
| 183 | |
| 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; |
| 190 | |
| 191 | for (var i = 0; i < setCount; i++) { |
| 192 | var setName = setNames[i]; |
| 193 | var color = colorScheme[i%colorCount]; |
| 194 | context.save(); |
| 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(); |
| 200 | |
| 201 | context.lineWidth = this.options.strokeWidth; |
| 202 | var forEachFunc = function(obj) { |
| 203 | if (obj.name == setName) |
| 204 | plotFunc(context, obj); |
| 205 | }; |
| 206 | |
| 207 | MochiKit.Iter.forEach(data, bind(forEachFunc, this)); |
| 208 | context.restore(); |
| 209 | } |
| 210 | }; |
| 211 | |
| 212 | PlotKit.CanvasRenderer.prototype._renderBarChart = function() { |
| 213 | var bind = MochiKit.Base.bind; |
| 214 | |
| 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)) |
| 221 | return; |
| 222 | if (this.options.shouldFill) |
| 223 | context.fillRect(x, y, w, h); |
| 224 | if (this.options.shouldStroke) |
| 225 | context.strokeRect(x, y, w, h); |
| 226 | }; |
| 227 | this._renderBarChartWrap(this.layout.bars, bind(drawRect, this)); |
| 228 | }; |
| 229 | |
| 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; |
| 238 | |
| 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; |
| 243 | |
| 244 | // setup graphics context |
| 245 | context.save(); |
| 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(); |
| 251 | |
| 252 | context.lineWidth = this.options.strokeWidth; |
| 253 | |
| 254 | // create paths |
| 255 | var makePath = function(ctx) { |
| 256 | ctx.beginPath(); |
| 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); |
| 262 | }; |
| 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); |
| 267 | ctx.closePath(); |
| 268 | }; |
| 269 | |
| 270 | if (this.options.shouldFill) { |
| 271 | bind(makePath, this)(context); |
| 272 | context.fill(); |
| 273 | } |
| 274 | if (this.options.shouldStroke) { |
| 275 | bind(makePath, this)(context); |
| 276 | context.stroke(); |
| 277 | } |
| 278 | |
| 279 | context.restore(); |
| 280 | } |
| 281 | }; |
| 282 | |
| 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; |
| 287 | |
| 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); |
| 292 | |
| 293 | if (this.isIE) { |
| 294 | centerx = parseInt(centerx); |
| 295 | centery = parseInt(centery); |
| 296 | radius = parseInt(radius); |
| 297 | } |
| 298 | |
| 299 | |
| 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 |
| 302 | |
| 303 | for (var i = 0; i < slices.length; i++) { |
| 304 | var color = this.options.colorScheme[i%colorCount]; |
| 305 | context.save(); |
| 306 | context.fillStyle = color.toRGBString(); |
| 307 | |
| 308 | var makePath = function() { |
| 309 | context.beginPath(); |
| 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, |
| 314 | false); |
| 315 | context.lineTo(centerx, centery); |
| 316 | context.closePath(); |
| 317 | }; |
| 318 | |
| 319 | if (Math.abs(slices[i].startAngle - slices[i].endAngle) > 0.001) { |
| 320 | if (this.options.shouldFill) { |
| 321 | makePath(); |
| 322 | context.fill(); |
| 323 | } |
| 324 | |
| 325 | if (this.options.shouldStroke) { |
| 326 | makePath(); |
| 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(); |
| 332 | context.stroke(); |
| 333 | } |
| 334 | } |
| 335 | context.restore(); |
| 336 | } |
| 337 | }; |
| 338 | |
| 339 | PlotKit.CanvasRenderer.prototype._renderBarAxis = function() { |
| 340 | this._renderAxis(); |
| 341 | } |
| 342 | |
| 343 | PlotKit.CanvasRenderer.prototype._renderLineAxis = function() { |
| 344 | this._renderAxis(); |
| 345 | }; |
| 346 | |
| 347 | |
| 348 | PlotKit.CanvasRenderer.prototype._renderAxis = function() { |
| 349 | if (!this.options.drawXAxis && !this.options.drawYAxis) |
| 350 | return; |
| 351 | |
| 352 | var context = this.element.getContext("2d"); |
| 353 | |
| 354 | var labelStyle = {"style": |
| 355 | {"position": "absolute", |
| 356 | "fontSize": this.options.axisLabelFontSize + "px", |
| 357 | "zIndex": 10, |
| 358 | "color": this.options.axisLabelColor.toRGBString(), |
| 359 | "width": this.options.axisLabelWidth + "px", |
| 360 | "overflow": "hidden" |
| 361 | } |
| 362 | }; |
| 363 | |
| 364 | // axis lines |
| 365 | context.save(); |
| 366 | context.strokeStyle = this.options.axisLineColor.toRGBString(); |
| 367 | context.lineWidth = this.options.axisLineWidth; |
| 368 | |
| 369 | |
| 370 | if (this.options.drawYAxis) { |
| 371 | if (this.layout.yticks) { |
| 372 | var drawTick = function(tick) { |
| 373 | if (typeof(tick) == "function") return; |
| 374 | var x = this.area.x; |
| 375 | var y = this.area.y + tick[0] * this.area.h; |
| 376 | context.beginPath(); |
| 377 | context.moveTo(x, y); |
| 378 | context.lineTo(x - this.options.axisTickSize, y); |
| 379 | context.closePath(); |
| 380 | context.stroke(); |
| 381 | |
| 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); |
| 389 | }; |
| 390 | |
| 391 | MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this)); |
| 392 | } |
| 393 | |
| 394 | context.beginPath(); |
| 395 | context.moveTo(this.area.x, this.area.y); |
| 396 | context.lineTo(this.area.x, this.area.y + this.area.h); |
| 397 | context.closePath(); |
| 398 | context.stroke(); |
| 399 | } |
| 400 | |
| 401 | if (this.options.drawXAxis) { |
| 402 | if (this.layout.xticks) { |
| 403 | var drawTick = function(tick) { |
| 404 | if (typeof(dataset) == "function") return; |
| 405 | |
| 406 | var x = this.area.x + tick[0] * this.area.w; |
| 407 | var y = this.area.y + this.area.h; |
| 408 | context.beginPath(); |
| 409 | context.moveTo(x, y); |
| 410 | context.lineTo(x, y + this.options.axisTickSize); |
| 411 | context.closePath(); |
| 412 | context.stroke(); |
| 413 | |
| 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); |
| 421 | }; |
| 422 | |
| 423 | MochiKit.Iter.forEach(this.layout.xticks, bind(drawTick, this)); |
| 424 | } |
| 425 | |
| 426 | context.beginPath(); |
| 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); |
| 429 | context.closePath(); |
| 430 | context.stroke(); |
| 431 | } |
| 432 | |
| 433 | context.restore(); |
| 434 | |
| 435 | }; |
| 436 | |
| 437 | PlotKit.CanvasRenderer.prototype._renderPieAxis = function() { |
| 438 | if (!this.options.drawXAxis) |
| 439 | return; |
| 440 | |
| 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]; |
| 446 | } |
| 447 | |
| 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; |
| 453 | |
| 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)) |
| 457 | continue; |
| 458 | |
| 459 | |
| 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; |
| 467 | |
| 468 | var labelx = centerx + Math.sin(normalisedAngle) * (radius + 10); |
| 469 | var labely = centery - Math.cos(normalisedAngle) * (radius + 10); |
| 470 | |
| 471 | var attrib = {"position": "absolute", |
| 472 | "zIndex": 11, |
| 473 | "width": labelWidth + "px", |
| 474 | "fontSize": this.options.axisLabelFontSize + "px", |
| 475 | "overflow": "hidden", |
| 476 | "color": this.options.axisLabelColor.toHexString() |
| 477 | }; |
| 478 | |
| 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"; |
| 485 | } |
| 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"; |
| 492 | |
| 493 | } |
| 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"; |
| 500 | } |
| 501 | else { |
| 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"; |
| 507 | } |
| 508 | |
| 509 | var label = DIV({'style': attrib}, this.layout.xticks[i][1]); |
| 510 | this.xlabels.push(label); |
| 511 | MochiKit.DOM.appendChildNodes(this.container, label); |
| 512 | } |
| 513 | |
| 514 | } |
| 515 | }; |
| 516 | |
| 517 | PlotKit.CanvasRenderer.prototype._renderBackground = function() { |
| 518 | var context = this.element.getContext("2d"); |
| 519 | context.save(); |
| 520 | context.fillStyle = this.options.backgroundColor.toRGBString(); |
| 521 | context.fillRect(0, 0, this.width, this.height); |
| 522 | context.restore(); |
| 523 | }; |
| 524 | |
| 525 | PlotKit.CanvasRenderer.prototype.clear = function() { |
| 526 | if (this.isIE) { |
| 527 | // VML takes a while to start up, so we just poll every this.IEDelay |
| 528 | try { |
| 529 | if (this.clearDelay) { |
| 530 | this.clearDelay.cancel(); |
| 531 | this.clearDelay = null; |
| 532 | } |
| 533 | var context = this.element.getContext("2d"); |
| 534 | } |
| 535 | catch (e) { |
| 536 | this.isFirstRender = false; |
| 537 | this.clearDelay = MochiKit.Async.wait(this.IEDelay); |
| 538 | this.clearDelay.addCallback(bind(this.clear, this)); |
| 539 | return; |
| 540 | } |
| 541 | } |
| 542 | |
| 543 | var context = this.element.getContext("2d"); |
| 544 | context.clearRect(0, 0, this.width, this.height); |
| 545 | |
| 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(); |
| 550 | }; |
| 551 | |
| 552 | // ---------------------------------------------------------------- |
| 553 | // Everything below here is experimental and undocumented. |
| 554 | // ---------------------------------------------------------------- |
| 555 | |
| 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)); |
| 564 | }; |
| 565 | |
| 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; |
| 570 | |
| 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; |
| 573 | |
| 574 | //log(x, y); |
| 575 | |
| 576 | var isHit = this.layout.hitTest(x, y); |
| 577 | if (isHit) |
| 578 | return isHit; |
| 579 | return null; |
| 580 | }; |
| 581 | |
| 582 | PlotKit.CanvasRenderer.prototype._createEventObject = function(layoutObj, e) { |
| 583 | if (layoutObj == null) { |
| 584 | return null; |
| 585 | } |
| 586 | |
| 587 | e.chart = layoutObj |
| 588 | return e; |
| 589 | }; |
| 590 | |
| 591 | |
| 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); |
| 597 | }; |
| 598 | |
| 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); |
| 604 | }; |
| 605 | |
| 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); |
| 611 | else |
| 612 | signal(this, "onmouseout", eventObject); |
| 613 | |
| 614 | }; |
| 615 | |
| 616 | PlotKit.CanvasRenderer.prototype.onmousemove = function(e) { |
| 617 | var layoutObject = this._resolveObject(e); |
| 618 | var eventObject = this._createEventObject(layoutObject, e); |
| 619 | |
| 620 | if ((layoutObject == null) && (this.event_isinside == null)) { |
| 621 | // TODO: should we emit an event anyway? |
| 622 | return; |
| 623 | } |
| 624 | |
| 625 | if ((layoutObject != null) && (this.event_isinside == null)) |
| 626 | signal(this, "onmouseover", eventObject); |
| 627 | |
| 628 | if ((layoutObject == null) && (this.event_isinside != null)) |
| 629 | signal(this, "onmouseout", eventObject); |
| 630 | |
| 631 | if ((layoutObject != null) && (this.event_isinside != null)) |
| 632 | signal(this, "onmousemove", eventObject); |
| 633 | |
| 634 | this.event_isinside = layoutObject; |
| 635 | //log("move", x, y); |
| 636 | }; |
| 637 | |
| 638 | PlotKit.CanvasRenderer.isSupported = function(canvasName) { |
| 639 | var canvas = null; |
| 640 | try { |
| 641 | if (MochiKit.Base.isUndefinedOrNull(canvasName)) |
| 642 | canvas = MochiKit.DOM.CANVAS({}); |
| 643 | else |
| 644 | canvas = MochiKit.DOM.getElement(canvasName); |
| 645 | var context = canvas.getContext("2d"); |
| 646 | } |
| 647 | catch (e) { |
| 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)) |
| 651 | return false; |
| 652 | return true; |
| 653 | } |
| 654 | return true; |
| 655 | }; |
| 656 | |
| 657 | // Namespace Iniitialisation |
| 658 | |
| 659 | PlotKit.Canvas = {} |
| 660 | PlotKit.Canvas.CanvasRenderer = PlotKit.CanvasRenderer; |
| 661 | |
| 662 | PlotKit.Canvas.EXPORT = [ |
| 663 | "CanvasRenderer" |
| 664 | ]; |
| 665 | |
| 666 | PlotKit.Canvas.EXPORT_OK = [ |
| 667 | "CanvasRenderer" |
| 668 | ]; |
| 669 | |
| 670 | PlotKit.Canvas.__new__ = function() { |
| 671 | var m = MochiKit.Base; |
| 672 | |
| 673 | m.nameFunctions(this); |
| 674 | |
| 675 | this.EXPORT_TAGS = { |
| 676 | ":common": this.EXPORT, |
| 677 | ":all": m.concat(this.EXPORT, this.EXPORT_OK) |
| 678 | }; |
| 679 | }; |
| 680 | |
| 681 | PlotKit.Canvas.__new__(); |
| 682 | MochiKit.Base._exportSymbols(this, PlotKit.Canvas); |
| 683 | |