assets/scripts/api.simile-widgets.org/timeplot/1.1/scripts/timeplot.js (361 lines of code) (raw):

/** * Timeplot * * @fileOverview Timeplot * @name Timeplot */ Timeline.Debug = SimileAjax.Debug; // timeline uses it's own debug system which is not as advanced var log = SimileAjax.Debug.log; // shorter name is easier to use /* * This function is used to implement a raw but effective OOP-like inheritance * in various Timeplot classes. */ Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; } // --------------------------------------------- /** * Create a timeplot attached to the given element and using the configuration from the given array of PlotInfos */ Timeplot.create = function(elmt, plotInfos) { return new Timeplot._Impl(elmt, plotInfos); }; /** * Create a PlotInfo configuration from the given map of params */ Timeplot.createPlotInfo = function(params) { return { id: ("id" in params) ? params.id : "p" + Math.round(Math.random() * 1000000), dataSource: ("dataSource" in params) ? params.dataSource : null, eventSource: ("eventSource" in params) ? params.eventSource : null, timeGeometry: ("timeGeometry" in params) ? params.timeGeometry : new Timeplot.DefaultTimeGeometry(), valueGeometry: ("valueGeometry" in params) ? params.valueGeometry : new Timeplot.DefaultValueGeometry(), timeZone: ("timeZone" in params) ? params.timeZone : 0, fillColor: ("fillColor" in params) ? ((params.fillColor == "string") ? new Timeplot.Color(params.fillColor) : params.fillColor) : null, fillGradient: ("fillGradient" in params) ? params.fillGradient : true, fillFrom: ("fillFrom" in params) ? params.fillFrom : Number.NEGATIVE_INFINITY, lineColor: ("lineColor" in params) ? ((params.lineColor == "string") ? new Timeplot.Color(params.lineColor) : params.lineColor) : new Timeplot.Color("#606060"), lineWidth: ("lineWidth" in params) ? params.lineWidth : 1.0, dotRadius: ("dotRadius" in params) ? params.dotRadius : 2.0, dotColor: ("dotColor" in params) ? params.dotColor : null, eventLineWidth: ("eventLineWidth" in params) ? params.eventLineWidth : 1.0, showValues: ("showValues" in params) ? params.showValues : false, roundValues: ("roundValues" in params) ? params.roundValues : true, valuesOpacity: ("valuesOpacity" in params) ? params.valuesOpacity : 75, bubbleWidth: ("bubbleWidth" in params) ? params.bubbleWidth : 300, bubbleHeight: ("bubbleHeight" in params) ? params.bubbleHeight : 200 }; }; // ------------------------------------------------------- /** * This is the implementation of the Timeplot object. * * @constructor */ Timeplot._Impl = function(elmt, plotInfos) { this._id = "t" + Math.round(Math.random() * 1000000); this._containerDiv = elmt; this._plotInfos = plotInfos; this._painters = { background: [], foreground: [] }; this._painter = null; this._active = false; this._upright = false; this._initialize(); }; Timeplot._Impl.prototype = { dispose: function() { for (var i = 0; i < this._plots.length; i++) { this._plots[i].dispose(); } this._plots = null; this._plotsInfos = null; this._containerDiv.innerHTML = ""; }, /** * Returns the main container div this timeplot is operating on. */ getElement: function() { return this._containerDiv; }, /** * Returns document this timeplot belongs to. */ getDocument: function() { return this._containerDiv.ownerDocument; }, /** * Append the given element to the timeplot DOM */ add: function(div) { this._containerDiv.appendChild(div); }, /** * Remove the given element to the timeplot DOM */ remove: function(div) { this._containerDiv.removeChild(div); }, /** * Add a painter to the timeplot */ addPainter: function(layerName, painter) { var layer = this._painters[layerName]; if (layer) { for (var i = 0; i < layer.length; i++) { if (layer[i].context._id == painter.context._id) { return; } } layer.push(painter); } }, /** * Remove a painter from the timeplot */ removePainter: function(layerName, painter) { var layer = this._painters[layerName]; if (layer) { for (var i = 0; i < layer.length; i++) { if (layer[i].context._id == painter.context._id) { layer.splice(i, 1); break; } } } }, /** * Get the width in pixels of the area occupied by the entire timeplot in the page */ getWidth: function() { return this._containerDiv.clientWidth; }, /** * Get the height in pixels of the area occupied by the entire timeplot in the page */ getHeight: function() { return this._containerDiv.clientHeight; }, /** * Get the drawing canvas associated with this timeplot */ getCanvas: function() { return this._canvas; }, /** * <p>Load the data from the given url into the given eventSource, using * the given separator to parse the columns and preprocess it before parsing * thru the optional filter function. The filter is useful for when * the data is row-oriented but the format is not compatible with the * one that Timeplot expects.</p> * * <p>Here is an example of a filter that changes dates in the form 'yyyy/mm/dd' * in the required 'yyyy-mm-dd' format: * <pre>var dataFilter = function(data) { * for (var i = 0; i < data.length; i++) { * var row = data[i]; * row[0] = row[0].replace(/\//g,"-"); * } * return data; * };</pre></p> */ loadText: function(url, separator, eventSource, filter, format) { if (this._active) { var tp = this; var fError = function(statusText, status, xmlhttp) { alert("Failed to load data xml from " + url + "\n" + statusText); tp.hideLoadingMessage(); }; var fDone = function(xmlhttp) { try { eventSource.loadText(xmlhttp.responseText, separator, url, filter, format); } catch (e) { SimileAjax.Debug.exception(e); } finally { tp.hideLoadingMessage(); } }; this.showLoadingMessage(); window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0); } }, /** * Load event data from the given url into the given eventSource, using * the Timeline XML event format. */ loadXML: function(url, eventSource) { if (this._active) { var tl = this; var fError = function(statusText, status, xmlhttp) { alert("Failed to load data xml from " + url + "\n" + statusText); tl.hideLoadingMessage(); }; var fDone = function(xmlhttp) { try { var xml = xmlhttp.responseXML; if (!xml.documentElement && xmlhttp.responseStream) { xml.load(xmlhttp.responseStream); } eventSource.loadXML(xml, url); } finally { tl.hideLoadingMessage(); } }; this.showLoadingMessage(); window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0); } }, /** * Overlay a 'div' element filled with the given text and styles to this timeplot * This is used to implement labels since canvas does not support drawing text. */ putText: function(id, text, clazz, styles) { var div = this.putDiv(id, "timeplot-div " + clazz, styles); div.innerHTML = text; return div; }, /** * Overlay a 'div' element, with the given class and the given styles to this timeplot. * This is used for labels and horizontal and vertical grids. */ putDiv: function(id, clazz, styles) { var tid = this._id + "-" + id; var div = document.getElementById(tid); if (!div) { var container = this._containerDiv.firstChild; // get the divs container div = document.createElement("div"); div.setAttribute("id",tid); container.appendChild(div); } div.setAttribute("class","timeplot-div " + clazz); div.setAttribute("className","timeplot-div " + clazz); this.placeDiv(div,styles); return div; }, /** * Associate the given map of styles to the given element. * In case such styles indicate position (left,right,top,bottom) correct them * with the padding information so that they align to the 'internal' area * of the timeplot. */ placeDiv: function(div, styles) { if (styles) { for (style in styles) { if (style == "left") { styles[style] += this._paddingX; styles[style] += "px"; } else if (style == "right") { styles[style] += this._paddingX; styles[style] += "px"; } else if (style == "top") { styles[style] += this._paddingY; styles[style] += "px"; } else if (style == "bottom") { styles[style] += this._paddingY; styles[style] += "px"; } else if (style == "width") { if (styles[style] < 0) styles[style] = 0; styles[style] += "px"; } else if (style == "height") { if (styles[style] < 0) styles[style] = 0; styles[style] += "px"; } div.style[style] = styles[style]; } } }, /** * return a {x,y} map with the location of the given element relative to the 'internal' area of the timeplot * (that is, without the container padding) */ locate: function(div) { return { x: div.offsetLeft - this._paddingX, y: div.offsetTop - this._paddingY } }, /** * Forces timeplot to re-evaluate the various value and time geometries * associated with its plot layers and repaint accordingly. This should * be invoked after the data in any of the data sources has been * modified. */ update: function() { if (this._active) { for (var i = 0; i < this._plots.length; i++) { var plot = this._plots[i]; var dataSource = plot.getDataSource(); if (dataSource) { var range = dataSource.getRange(); if (range) { plot._valueGeometry.setRange(range); plot._timeGeometry.setRange(range); } } plot.hideValues(); } this.paint(); } }, /** * Forces timeplot to re-evaluate its own geometry, clear itself and paint. * This should be used instead of paint() when you're not sure if the * geometry of the page has changed or not. */ repaint: function() { if (this._active) { this._prepareCanvas(); for (var i = 0; i < this._plots.length; i++) { var plot = this._plots[i]; if (plot._timeGeometry) plot._timeGeometry.reset(); if (plot._valueGeometry) plot._valueGeometry.reset(); } this.paint(); } }, /** * Calls all the painters that were registered to this timeplot and makes them * paint the timeplot. This should be used only when you're sure that the geometry * of the page hasn't changed. * NOTE: painting is performed by a different thread and it's safe to call this * function in bursts (as in mousemove or during window resizing */ paint: function() { if (this._active && this._painter == null) { var timeplot = this; this._painter = window.setTimeout(function() { timeplot._clearCanvas(); var run = function(action,context) { try { if (context.setTimeplot) context.setTimeplot(timeplot); action.apply(context,[]); } catch (e) { SimileAjax.Debug.exception(e); } } var background = timeplot._painters.background; for (var i = 0; i < background.length; i++) { run(background[i].action, background[i].context); } var foreground = timeplot._painters.foreground; for (var i = 0; i < foreground.length; i++) { run(foreground[i].action, foreground[i].context); } timeplot._painter = null; }, 20); } }, _clearCanvas: function() { var canvas = this.getCanvas(); var ctx = canvas.getContext('2d'); ctx.clearRect(0,0,canvas.width,canvas.height); }, _clearLabels: function() { var labels = this._containerDiv.firstChild; if (labels) this._containerDiv.removeChild(labels); labels = document.createElement("div"); this._containerDiv.appendChild(labels); }, _prepareCanvas: function() { var canvas = this.getCanvas(); // using jQuery. note we calculate the average padding; if your // padding settings are not symmetrical, the labels will be off // since they expect to be centered on the canvas. var con = SimileAjax.jQuery(this._containerDiv); this._paddingX = (parseInt(con.css('paddingLeft')) + parseInt(con.css('paddingRight'))) / 2; this._paddingY = (parseInt(con.css('paddingTop')) + parseInt(con.css('paddingBottom'))) / 2; canvas.width = this.getWidth() - (this._paddingX * 2); canvas.height = this.getHeight() - (this._paddingY * 2); var ctx = canvas.getContext('2d'); this._setUpright(ctx, canvas); ctx.globalCompositeOperation = 'source-over'; }, _setUpright: function(ctx, canvas) { // excanvas+IE requires this to be done only once, ever; actual canvas // implementations reset and require this for each call to re-layout if (!SimileAjax.Platform.browser.isIE) this._upright = false; if (!this._upright) { this._upright = true; ctx.translate(0, canvas.height); ctx.scale(1,-1); } }, _isBrowserSupported: function(canvas) { var browser = SimileAjax.Platform.browser; if ((canvas.getContext && window.getComputedStyle) || (browser.isIE && browser.majorVersion >= 6)) { return true; } else { return false; } }, _initialize: function() { // initialize the window manager (used to handle the popups) // NOTE: this is a singleton and it's safe to call multiple times SimileAjax.WindowManager.initialize(); var containerDiv = this._containerDiv; var doc = containerDiv.ownerDocument; // make sure the timeplot div has the right class containerDiv.className = "timeplot-container " + containerDiv.className; // clean it up if it contains some content while (containerDiv.firstChild) { containerDiv.removeChild(containerDiv.firstChild); } var canvas = doc.createElement("canvas"); if (this._isBrowserSupported(canvas)) { this._clearLabels(); this._canvas = canvas; canvas.className = "timeplot-canvas"; containerDiv.appendChild(canvas); if(!canvas.getContext && G_vmlCanvasManager) { canvas = G_vmlCanvasManager.initElement(this._canvas); this._canvas = canvas; } this._prepareCanvas(); // inserting copyright and link to simile var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/copyright.png"); elmtCopyright.className = "timeplot-copyright"; elmtCopyright.title = "Timeplot (c) SIMILE - http://simile.mit.edu/timeplot/"; SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://simile.mit.edu/timeplot/"; }); containerDiv.appendChild(elmtCopyright); var timeplot = this; var painter = { onAddMany: function() { timeplot.update(); }, onClear: function() { timeplot.update(); } } // creating painters this._plots = []; if (this._plotInfos) { for (var i = 0; i < this._plotInfos.length; i++) { var plot = new Timeplot.Plot(this, this._plotInfos[i]); var dataSource = plot.getDataSource(); if (dataSource) { dataSource.addListener(painter); } this.addPainter("background", { context: plot.getTimeGeometry(), action: plot.getTimeGeometry().paint }); this.addPainter("background", { context: plot.getValueGeometry(), action: plot.getValueGeometry().paint }); this.addPainter("foreground", { context: plot, action: plot.paint }); this._plots.push(plot); plot.initialize(); } } // creating loading UI var message = SimileAjax.Graphics.createMessageBubble(doc); message.containerDiv.className = "timeplot-message-container"; containerDiv.appendChild(message.containerDiv); message.contentDiv.className = "timeplot-message"; message.contentDiv.innerHTML = "<img src='" + Timeplot.urlPrefix + "images/progress-running.gif' /> Loading..."; this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; }; this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; }; this._active = true; } else { this._message = SimileAjax.Graphics.createMessageBubble(doc); this._message.containerDiv.className = "timeplot-message-container"; this._message.containerDiv.style.top = "15%"; this._message.containerDiv.style.left = "20%"; this._message.containerDiv.style.right = "20%"; this._message.containerDiv.style.minWidth = "20em"; this._message.contentDiv.className = "timeplot-message"; this._message.contentDiv.innerHTML = "We're terribly sorry, but your browser is not currently supported by <a href='http://simile.mit.edu/timeplot/'>Timeplot</a>.<br><br> We are working on supporting it in the near future but, for now, see the <a href='http://simile.mit.edu/wiki/Timeplot_Limitations'>list of currently supported browsers</a>."; this._message.containerDiv.style.display = "block"; containerDiv.appendChild(this._message.containerDiv); } } };