/timeplot/scripts/timeplot.js
JavaScript | 543 lines | 361 code | 63 blank | 119 comment | 67 complexity | 1761e4c0191684592112cdf07ea37bca MD5 | raw file
1/** 2 * Timeplot 3 * 4 * @fileOverview Timeplot 5 * @name Timeplot 6 */ 7 8Timeline.Debug = SimileAjax.Debug; // timeline uses it's own debug system which is not as advanced 9var log = SimileAjax.Debug.log; // shorter name is easier to use 10 11/* 12 * This function is used to implement a raw but effective OOP-like inheritance 13 * in various Timeplot classes. 14 */ 15Object.extend = function(destination, source) { 16 for (var property in source) { 17 destination[property] = source[property]; 18 } 19 return destination; 20} 21 22// --------------------------------------------- 23 24/** 25 * Create a timeplot attached to the given element and using the configuration from the given array of PlotInfos 26 */ 27Timeplot.create = function(elmt, plotInfos) { 28 return new Timeplot._Impl(elmt, plotInfos); 29}; 30 31/** 32 * Create a PlotInfo configuration from the given map of params 33 */ 34Timeplot.createPlotInfo = function(params) { 35 return { 36 id: ("id" in params) ? params.id : "p" + Math.round(Math.random() * 1000000), 37 dataSource: ("dataSource" in params) ? params.dataSource : null, 38 eventSource: ("eventSource" in params) ? params.eventSource : null, 39 timeGeometry: ("timeGeometry" in params) ? params.timeGeometry : new Timeplot.DefaultTimeGeometry(), 40 valueGeometry: ("valueGeometry" in params) ? params.valueGeometry : new Timeplot.DefaultValueGeometry(), 41 timeZone: ("timeZone" in params) ? params.timeZone : 0, 42 fillColor: ("fillColor" in params) ? ((params.fillColor == "string") ? new Timeplot.Color(params.fillColor) : params.fillColor) : null, 43 fillGradient: ("fillGradient" in params) ? params.fillGradient : true, 44 fillFrom: ("fillFrom" in params) ? params.fillFrom : Number.NEGATIVE_INFINITY, 45 lineColor: ("lineColor" in params) ? ((params.lineColor == "string") ? new Timeplot.Color(params.lineColor) : params.lineColor) : new Timeplot.Color("#606060"), 46 lineWidth: ("lineWidth" in params) ? params.lineWidth : 1.0, 47 dotRadius: ("dotRadius" in params) ? params.dotRadius : 2.0, 48 dotColor: ("dotColor" in params) ? params.dotColor : null, 49 eventLineWidth: ("eventLineWidth" in params) ? params.eventLineWidth : 1.0, 50 showValues: ("showValues" in params) ? params.showValues : false, 51 roundValues: ("roundValues" in params) ? params.roundValues : true, 52 valuesOpacity: ("valuesOpacity" in params) ? params.valuesOpacity : 75, 53 bubbleWidth: ("bubbleWidth" in params) ? params.bubbleWidth : 300, 54 bubbleHeight: ("bubbleHeight" in params) ? params.bubbleHeight : 200 55 }; 56}; 57 58// ------------------------------------------------------- 59 60/** 61 * This is the implementation of the Timeplot object. 62 * 63 * @constructor 64 */ 65Timeplot._Impl = function(elmt, plotInfos) { 66 this._id = "t" + Math.round(Math.random() * 1000000); 67 this._containerDiv = elmt; 68 this._plotInfos = plotInfos; 69 this._painters = { 70 background: [], 71 foreground: [] 72 }; 73 this._painter = null; 74 this._active = false; 75 this._upright = false; 76 this._initialize(); 77}; 78 79Timeplot._Impl.prototype = { 80 81 dispose: function() { 82 for (var i = 0; i < this._plots.length; i++) { 83 this._plots[i].dispose(); 84 } 85 this._plots = null; 86 this._plotsInfos = null; 87 this._containerDiv.innerHTML = ""; 88 }, 89 90 /** 91 * Returns the main container div this timeplot is operating on. 92 */ 93 getElement: function() { 94 return this._containerDiv; 95 }, 96 97 /** 98 * Returns document this timeplot belongs to. 99 */ 100 getDocument: function() { 101 return this._containerDiv.ownerDocument; 102 }, 103 104 /** 105 * Append the given element to the timeplot DOM 106 */ 107 add: function(div) { 108 this._containerDiv.appendChild(div); 109 }, 110 111 /** 112 * Remove the given element to the timeplot DOM 113 */ 114 remove: function(div) { 115 this._containerDiv.removeChild(div); 116 }, 117 118 /** 119 * Add a painter to the timeplot 120 */ 121 addPainter: function(layerName, painter) { 122 var layer = this._painters[layerName]; 123 if (layer) { 124 for (var i = 0; i < layer.length; i++) { 125 if (layer[i].context._id == painter.context._id) { 126 return; 127 } 128 } 129 layer.push(painter); 130 } 131 }, 132 133 /** 134 * Remove a painter from the timeplot 135 */ 136 removePainter: function(layerName, painter) { 137 var layer = this._painters[layerName]; 138 if (layer) { 139 for (var i = 0; i < layer.length; i++) { 140 if (layer[i].context._id == painter.context._id) { 141 layer.splice(i, 1); 142 break; 143 } 144 } 145 } 146 }, 147 148 /** 149 * Get the width in pixels of the area occupied by the entire timeplot in the page 150 */ 151 getWidth: function() { 152 return this._containerDiv.clientWidth; 153 }, 154 155 /** 156 * Get the height in pixels of the area occupied by the entire timeplot in the page 157 */ 158 getHeight: function() { 159 return this._containerDiv.clientHeight; 160 }, 161 162 /** 163 * Get the drawing canvas associated with this timeplot 164 */ 165 getCanvas: function() { 166 return this._canvas; 167 }, 168 169 /** 170 * <p>Load the data from the given url into the given eventSource, using 171 * the given separator to parse the columns and preprocess it before parsing 172 * thru the optional filter function. The filter is useful for when 173 * the data is row-oriented but the format is not compatible with the 174 * one that Timeplot expects.</p> 175 * 176 * <p>Here is an example of a filter that changes dates in the form 'yyyy/mm/dd' 177 * in the required 'yyyy-mm-dd' format: 178 * <pre>var dataFilter = function(data) { 179 * for (var i = 0; i < data.length; i++) { 180 * var row = data[i]; 181 * row[0] = row[0].replace(/\//g,"-"); 182 * } 183 * return data; 184 * };</pre></p> 185 */ 186 loadText: function(url, separator, eventSource, filter, format) { 187 if (this._active) { 188 var tp = this; 189 190 var fError = function(statusText, status, xmlhttp) { 191 alert("Failed to load data xml from " + url + "\n" + statusText); 192 tp.hideLoadingMessage(); 193 }; 194 195 var fDone = function(xmlhttp) { 196 try { 197 eventSource.loadText(xmlhttp.responseText, separator, url, filter, format); 198 } catch (e) { 199 SimileAjax.Debug.exception(e); 200 } finally { 201 tp.hideLoadingMessage(); 202 } 203 }; 204 205 this.showLoadingMessage(); 206 window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0); 207 } 208 }, 209 210 /** 211 * Load event data from the given url into the given eventSource, using 212 * the Timeline XML event format. 213 */ 214 loadXML: function(url, eventSource) { 215 if (this._active) { 216 var tl = this; 217 218 var fError = function(statusText, status, xmlhttp) { 219 alert("Failed to load data xml from " + url + "\n" + statusText); 220 tl.hideLoadingMessage(); 221 }; 222 223 var fDone = function(xmlhttp) { 224 try { 225 var xml = xmlhttp.responseXML; 226 if (!xml.documentElement && xmlhttp.responseStream) { 227 xml.load(xmlhttp.responseStream); 228 } 229 eventSource.loadXML(xml, url); 230 } finally { 231 tl.hideLoadingMessage(); 232 } 233 }; 234 235 this.showLoadingMessage(); 236 window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0); 237 } 238 }, 239 240 /** 241 * Overlay a 'div' element filled with the given text and styles to this timeplot 242 * This is used to implement labels since canvas does not support drawing text. 243 */ 244 putText: function(id, text, clazz, styles) { 245 var div = this.putDiv(id, "timeplot-div " + clazz, styles); 246 div.innerHTML = text; 247 return div; 248 }, 249 250 /** 251 * Overlay a 'div' element, with the given class and the given styles to this timeplot. 252 * This is used for labels and horizontal and vertical grids. 253 */ 254 putDiv: function(id, clazz, styles) { 255 var tid = this._id + "-" + id; 256 var div = document.getElementById(tid); 257 if (!div) { 258 var container = this._containerDiv.firstChild; // get the divs container 259 div = document.createElement("div"); 260 div.setAttribute("id",tid); 261 container.appendChild(div); 262 } 263 div.setAttribute("class","timeplot-div " + clazz); 264 div.setAttribute("className","timeplot-div " + clazz); 265 this.placeDiv(div,styles); 266 return div; 267 }, 268 269 /** 270 * Associate the given map of styles to the given element. 271 * In case such styles indicate position (left,right,top,bottom) correct them 272 * with the padding information so that they align to the 'internal' area 273 * of the timeplot. 274 */ 275 placeDiv: function(div, styles) { 276 if (styles) { 277 for (style in styles) { 278 if (style == "left") { 279 styles[style] += this._paddingX; 280 styles[style] += "px"; 281 } else if (style == "right") { 282 styles[style] += this._paddingX; 283 styles[style] += "px"; 284 } else if (style == "top") { 285 styles[style] += this._paddingY; 286 styles[style] += "px"; 287 } else if (style == "bottom") { 288 styles[style] += this._paddingY; 289 styles[style] += "px"; 290 } else if (style == "width") { 291 if (styles[style] < 0) styles[style] = 0; 292 styles[style] += "px"; 293 } else if (style == "height") { 294 if (styles[style] < 0) styles[style] = 0; 295 styles[style] += "px"; 296 } 297 div.style[style] = styles[style]; 298 } 299 } 300 }, 301 302 /** 303 * return a {x,y} map with the location of the given element relative to the 'internal' area of the timeplot 304 * (that is, without the container padding) 305 */ 306 locate: function(div) { 307 return { 308 x: div.offsetLeft - this._paddingX, 309 y: div.offsetTop - this._paddingY 310 } 311 }, 312 313 /** 314 * Forces timeplot to re-evaluate the various value and time geometries 315 * associated with its plot layers and repaint accordingly. This should 316 * be invoked after the data in any of the data sources has been 317 * modified. 318 */ 319 update: function() { 320 if (this._active) { 321 for (var i = 0; i < this._plots.length; i++) { 322 var plot = this._plots[i]; 323 var dataSource = plot.getDataSource(); 324 if (dataSource) { 325 var range = dataSource.getRange(); 326 if (range) { 327 plot._valueGeometry.setRange(range); 328 plot._timeGeometry.setRange(range); 329 } 330 } 331 plot.hideValues(); 332 } 333 this.paint(); 334 } 335 }, 336 337 /** 338 * Forces timeplot to re-evaluate its own geometry, clear itself and paint. 339 * This should be used instead of paint() when you're not sure if the 340 * geometry of the page has changed or not. 341 */ 342 repaint: function() { 343 if (this._active) { 344 this._prepareCanvas(); 345 for (var i = 0; i < this._plots.length; i++) { 346 var plot = this._plots[i]; 347 if (plot._timeGeometry) plot._timeGeometry.reset(); 348 if (plot._valueGeometry) plot._valueGeometry.reset(); 349 } 350 this.paint(); 351 } 352 }, 353 354 /** 355 * Calls all the painters that were registered to this timeplot and makes them 356 * paint the timeplot. This should be used only when you're sure that the geometry 357 * of the page hasn't changed. 358 * NOTE: painting is performed by a different thread and it's safe to call this 359 * function in bursts (as in mousemove or during window resizing 360 */ 361 paint: function() { 362 if (this._active && this._painter == null) { 363 var timeplot = this; 364 this._painter = window.setTimeout(function() { 365 timeplot._clearCanvas(); 366 367 var run = function(action,context) { 368 try { 369 if (context.setTimeplot) context.setTimeplot(timeplot); 370 action.apply(context,[]); 371 } catch (e) { 372 SimileAjax.Debug.exception(e); 373 } 374 } 375 376 var background = timeplot._painters.background; 377 for (var i = 0; i < background.length; i++) { 378 run(background[i].action, background[i].context); 379 } 380 var foreground = timeplot._painters.foreground; 381 for (var i = 0; i < foreground.length; i++) { 382 run(foreground[i].action, foreground[i].context); 383 } 384 385 timeplot._painter = null; 386 }, 20); 387 } 388 }, 389 390 _clearCanvas: function() { 391 var canvas = this.getCanvas(); 392 var ctx = canvas.getContext('2d'); 393 ctx.clearRect(0,0,canvas.width,canvas.height); 394 }, 395 396 _clearLabels: function() { 397 var labels = this._containerDiv.firstChild; 398 if (labels) this._containerDiv.removeChild(labels); 399 labels = document.createElement("div"); 400 this._containerDiv.appendChild(labels); 401 }, 402 403 _prepareCanvas: function() { 404 var canvas = this.getCanvas(); 405 406 // using jQuery. note we calculate the average padding; if your 407 // padding settings are not symmetrical, the labels will be off 408 // since they expect to be centered on the canvas. 409 var con = SimileAjax.jQuery(this._containerDiv); 410 this._paddingX = (parseInt(con.css('paddingLeft')) + 411 parseInt(con.css('paddingRight'))) / 2; 412 this._paddingY = (parseInt(con.css('paddingTop')) + 413 parseInt(con.css('paddingBottom'))) / 2; 414 415 canvas.width = this.getWidth() - (this._paddingX * 2); 416 canvas.height = this.getHeight() - (this._paddingY * 2); 417 418 var ctx = canvas.getContext('2d'); 419 this._setUpright(ctx, canvas); 420 ctx.globalCompositeOperation = 'source-over'; 421 }, 422 423 _setUpright: function(ctx, canvas) { 424 // excanvas+IE requires this to be done only once, ever; actual canvas 425 // implementations reset and require this for each call to re-layout 426 if (!SimileAjax.Platform.browser.isIE) this._upright = false; 427 if (!this._upright) { 428 this._upright = true; 429 ctx.translate(0, canvas.height); 430 ctx.scale(1,-1); 431 } 432 }, 433 434 _isBrowserSupported: function(canvas) { 435 var browser = SimileAjax.Platform.browser; 436 if ((canvas.getContext && window.getComputedStyle) || 437 (browser.isIE && browser.majorVersion >= 6)) { 438 return true; 439 } else { 440 return false; 441 } 442 }, 443 444 _initialize: function() { 445 446 // initialize the window manager (used to handle the popups) 447 // NOTE: this is a singleton and it's safe to call multiple times 448 SimileAjax.WindowManager.initialize(); 449 450 var containerDiv = this._containerDiv; 451 var doc = containerDiv.ownerDocument; 452 453 // make sure the timeplot div has the right class 454 containerDiv.className = "timeplot-container " + containerDiv.className; 455 456 // clean it up if it contains some content 457 while (containerDiv.firstChild) { 458 containerDiv.removeChild(containerDiv.firstChild); 459 } 460 461 var canvas = doc.createElement("canvas"); 462 463 if (this._isBrowserSupported(canvas)) { 464 this._clearLabels(); 465 466 this._canvas = canvas; 467 canvas.className = "timeplot-canvas"; 468 containerDiv.appendChild(canvas); 469 if(!canvas.getContext && G_vmlCanvasManager) { 470 canvas = G_vmlCanvasManager.initElement(this._canvas); 471 this._canvas = canvas; 472 } 473 this._prepareCanvas(); 474 475 // inserting copyright and link to simile 476 var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/copyright.png"); 477 elmtCopyright.className = "timeplot-copyright"; 478 elmtCopyright.title = "SIMILE Timeplot - http://www.simile-widgets.organ/timeplot/"; 479 SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://www.simile-widgets.organ/timeplot/"; }); 480 containerDiv.appendChild(elmtCopyright); 481 482 var timeplot = this; 483 var painter = { 484 onAddMany: function() { timeplot.update(); }, 485 onClear: function() { timeplot.update(); } 486 } 487 488 // creating painters 489 this._plots = []; 490 if (this._plotInfos) { 491 for (var i = 0; i < this._plotInfos.length; i++) { 492 var plot = new Timeplot.Plot(this, this._plotInfos[i]); 493 var dataSource = plot.getDataSource(); 494 if (dataSource) { 495 dataSource.addListener(painter); 496 } 497 this.addPainter("background", { 498 context: plot.getTimeGeometry(), 499 action: plot.getTimeGeometry().paint 500 }); 501 this.addPainter("background", { 502 context: plot.getValueGeometry(), 503 action: plot.getValueGeometry().paint 504 }); 505 this.addPainter("foreground", { 506 context: plot, 507 action: plot.paint 508 }); 509 this._plots.push(plot); 510 plot.initialize(); 511 } 512 } 513 514 // creating loading UI 515 var message = SimileAjax.Graphics.createMessageBubble(doc); 516 message.containerDiv.className = "timeplot-message-container"; 517 containerDiv.appendChild(message.containerDiv); 518 519 message.contentDiv.className = "timeplot-message"; 520 message.contentDiv.innerHTML = "<img src='" + Timeplot.urlPrefix + "images/progress-running.gif' /> Loading..."; 521 522 this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; }; 523 this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; }; 524 525 this._active = true; 526 527 } else { 528 529 this._message = SimileAjax.Graphics.createMessageBubble(doc); 530 this._message.containerDiv.className = "timeplot-message-container"; 531 this._message.containerDiv.style.top = "15%"; 532 this._message.containerDiv.style.left = "20%"; 533 this._message.containerDiv.style.right = "20%"; 534 this._message.containerDiv.style.minWidth = "20em"; 535 this._message.contentDiv.className = "timeplot-message"; 536 this._message.contentDiv.innerHTML = "We're terribly sorry, but your browser is not currently supported by <a href='http://www.simile-widgets.org/timeplot/'>Timeplot</a>."; 537 this._message.containerDiv.style.display = "block"; 538 539 containerDiv.appendChild(this._message.containerDiv); 540 541 } 542 } 543};