PageRenderTime 128ms CodeModel.GetById 60ms app.highlight 37ms RepoModel.GetById 23ms app.codeStats 0ms

/timeplot/scripts/timeplot.js

http://showslow.googlecode.com/
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};