diff geotemco/lib/simile/timeplot/scripts/geometry.js @ 0:b12c99b7c3f0

commit for previous development
author Zoe Hong <zhong@mpiwg-berlin.mpg.de>
date Mon, 19 Jan 2015 17:13:49 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geotemco/lib/simile/timeplot/scripts/geometry.js	Mon Jan 19 17:13:49 2015 +0100
@@ -0,0 +1,879 @@
+/**
+ * Geometries
+ * 
+ * @fileOverview Geometries
+ * @name Geometries
+ */
+
+/**
+ * This is the constructor for the default value geometry.
+ * A value geometry is what regulates mapping of the plot values to the screen y coordinate.
+ * If two plots share the same value geometry, they will be drawn using the same scale.
+ * If "min" and "max" parameters are not set, the geometry will stretch itself automatically
+ * so that the entire plot will be drawn without overflowing. The stretching happens also
+ * when a geometry is shared between multiple plots, the one with the biggest range will
+ * win over the others.
+ * 
+ * @constructor
+ */
+Timeplot.DefaultValueGeometry = function(params) {
+    if (!params) params = {};
+    this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
+    this._axisColor = ("axisColor" in params) ? ((typeof params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
+    this._gridColor = ("gridColor" in params) ? ((typeof params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
+    this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
+    this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "right";
+    this._gridSpacing = ("gridSpacing" in params) ? params.gridStep : 50;
+    this._gridType = ("gridType" in params) ? params.gridType : "short";
+    this._gridShortSize = ("gridShortSize" in params) ? params.gridShortSize : 10;
+    this._minValue = ("min" in params) ? params.min : null;
+    this._maxValue = ("max" in params) ? params.max : null;
+    this._linMap = {
+        direct: function(v) {
+            return v;
+        },
+        inverse: function(y) {
+            return y;
+        }
+    }
+    this._map = this._linMap;
+    this._labels = [];
+    this._grid = [];
+}
+
+Timeplot.DefaultValueGeometry.prototype = {
+
+    /**
+     * Since geometries can be reused across timeplots, we need to call this function
+     * before we can paint using this geometry.
+     */
+    setTimeplot: function(timeplot) {
+        this._timeplot = timeplot;
+        this._canvas = timeplot.getCanvas();
+        this.reset();
+    },
+
+    /**
+     * Called by all the plot layers this geometry is associated with
+     * to update the value range. Unless min/max values are specified
+     * in the parameters, the biggest value range will be used.
+     */
+    setRange: function(range) {
+        if ((this._minValue == null) || ((this._minValue != null) && (range.min < this._minValue))) {
+            this._minValue = range.min;
+        }
+        if ((this._maxValue == null) || ((this._maxValue != null) && (range.max * 1.05 > this._maxValue))) {
+            this._maxValue = range.max * 1.05; // get a little more head room to avoid hitting the ceiling
+        }
+
+        this._updateMappedValues();
+
+        if (!(this._minValue == 0 && this._maxValue == 0)) {
+            this._grid = this._calculateGrid();
+        }
+    },
+
+    /**
+     * Called after changing ranges or canvas size to reset the grid values
+     */
+    reset: function() {
+        this._clearLabels();
+        this._updateMappedValues();
+        this._grid = this._calculateGrid();
+    },
+
+    /**
+     * Map the given value to a y screen coordinate.
+     */
+    toScreen: function(value) {
+        if (this._canvas && this._maxValue) {
+            var v = value - this._minValue;
+            return this._canvas.height * (this._map.direct(v)) / this._mappedRange;
+        } else {
+            return -50;
+        }
+    },
+
+    /**
+     * Map the given y screen coordinate to a value
+     */
+    fromScreen: function(y) {
+        if (this._canvas) {
+            return this._map.inverse(this._mappedRange * y / this._canvas.height) + this._minValue;
+        } else {
+            return 0;
+        }
+    },
+
+    /**
+     * Each geometry is also a painter and paints the value grid and grid labels.
+     */
+    paint: function() {
+        if (this._timeplot) {
+            var ctx = this._canvas.getContext('2d');
+    
+            ctx.lineJoin = 'miter';
+    
+            // paint grid
+            if (this._gridColor) {        
+                var gridGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+                gridGradient.addColorStop(0, this._gridColor.toHexString());
+                gridGradient.addColorStop(0.3, this._gridColor.toHexString());
+                gridGradient.addColorStop(1, "rgba(255,255,255,0.5)");
+
+                ctx.lineWidth = this._gridLineWidth;
+                ctx.strokeStyle = gridGradient;
+    
+                for (var i = 0; i < this._grid.length; i++) {
+                    var tick = this._grid[i];
+                    var y = Math.floor(tick.y) + 0.5;
+                    if (typeof tick.label != "undefined") {
+                        if (this._axisLabelsPlacement == "left") {
+                            var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
+                                left: 4,
+                                bottom: y + 2,
+                                color: this._gridColor.toHexString(),
+                                visibility: "hidden"
+                            });
+                            this._labels.push(div);
+                        } else if (this._axisLabelsPlacement == "right") {
+                            var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
+                                right: 4,
+                                bottom: y + 2,
+                                color: this._gridColor.toHexString(),
+                                visibility: "hidden"
+                            });
+                            this._labels.push(div);
+                        }
+                        if (y + div.clientHeight < this._canvas.height + 10) {
+                            div.style.visibility = "visible"; // avoid the labels that would overflow
+                        }
+                    }
+
+                    // draw grid
+                    ctx.beginPath();
+                    if (this._gridType == "long" || tick.label == 0) {
+                        ctx.moveTo(0, y);
+                        ctx.lineTo(this._canvas.width, y);
+                    } else if (this._gridType == "short") {
+                        if (this._axisLabelsPlacement == "left") {
+                            ctx.moveTo(0, y);
+                            ctx.lineTo(this._gridShortSize, y);
+                        } else if (this._axisLabelsPlacement == "right") {
+                            ctx.moveTo(this._canvas.width, y);
+                            ctx.lineTo(this._canvas.width - this._gridShortSize, y);
+                        }                       
+                    }
+                    ctx.stroke();
+                }
+            }
+        
+            // paint axis
+            var axisGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+            axisGradient.addColorStop(0, this._axisColor.toString());
+            axisGradient.addColorStop(0.5, this._axisColor.toString());
+            axisGradient.addColorStop(1, "rgba(255,255,255,0.5)");
+            
+            ctx.lineWidth = 1;
+            ctx.strokeStyle = axisGradient;
+    
+            // left axis
+            ctx.beginPath();
+            ctx.moveTo(0,this._canvas.height);
+            ctx.lineTo(0,0);
+            ctx.stroke();
+            
+            // right axis
+            ctx.beginPath();
+            ctx.moveTo(this._canvas.width,0);
+            ctx.lineTo(this._canvas.width,this._canvas.height);
+            ctx.stroke();
+        }
+    },
+    
+    /**
+     * Removes all the labels that were added by this geometry
+     */
+    _clearLabels: function() {
+        for (var i = 0; i < this._labels.length; i++) {
+            var l = this._labels[i];
+            var parent = l.parentNode;
+            if (parent) parent.removeChild(l);
+        }
+    },
+    
+    /*
+     * This function calculates the grid spacing that it will be used 
+     * by this geometry to draw the grid in order to reduce clutter. 
+     */
+    _calculateGrid: function() {
+        var grid = [];
+        
+        if (!this._canvas || this._valueRange == 0) return grid;
+                
+        var power = 0;
+        if (this._valueRange > 1) {
+            while (Math.pow(10,power) < this._valueRange) {
+                power++;
+            }
+            power--;
+        } else {
+            while (Math.pow(10,power) > this._valueRange) {
+                power--;
+            }
+        }
+
+        var unit = Math.pow(10,power);
+        var inc = unit;
+        while (true) {
+            var dy = this.toScreen(this._minValue + inc);
+
+            while (dy < this._gridSpacing) {
+                inc += unit;
+                dy = this.toScreen(this._minValue + inc);
+            }
+
+            if (dy > 2 * this._gridSpacing) { // grids are too spaced out
+                unit /= 10;
+                inc = unit;
+            } else {
+                break;
+            }
+        }
+        
+        var v = 0;
+        var y = this.toScreen(v);
+        if (this._minValue >= 0) {
+            while (y < this._canvas.height) {
+                if (y > 0) {
+                    grid.push({ y: y, label: v });
+                }
+                v += inc;
+                y = this.toScreen(v);
+            }
+        } else if (this._maxValue <= 0) {
+            while (y > 0) {
+                if (y < this._canvas.height) {
+                    grid.push({ y: y, label: v });
+                }
+                v -= inc;
+                y = this.toScreen(v);
+            }
+        } else {
+            while (y < this._canvas.height) {
+                if (y > 0) {
+                    grid.push({ y: y, label: v });
+                }
+                v += inc;
+                y = this.toScreen(v);
+            }
+            v = -inc;
+            y = this.toScreen(v);
+            while (y > 0) {
+                if (y < this._canvas.height) {
+                    grid.push({ y: y, label: v });
+                }
+                v -= inc;
+                y = this.toScreen(v);
+            }
+        }
+        
+        return grid;
+    },
+
+    /*
+     * Update the values that are used by the paint function so that
+     * we don't have to calculate them at every repaint.
+     */
+    _updateMappedValues: function() {
+        this._valueRange = Math.abs(this._maxValue - this._minValue);
+        this._mappedRange = this._map.direct(this._valueRange);
+    }
+    
+}
+
+// --------------------------------------------------
+
+/**
+ * This is the constructor for a Logarithmic value geometry, which
+ * is useful when plots have values in different magnitudes but 
+ * exhibit similar trends and such trends want to be shown on the same
+ * plot (here a cartesian geometry would make the small magnitudes 
+ * disappear).
+ * 
+ * NOTE: this class extends Timeplot.DefaultValueGeometry and inherits
+ * all of the methods of that class. So refer to that class. 
+ * 
+ * @constructor
+ */
+Timeplot.LogarithmicValueGeometry = function(params) {
+    Timeplot.DefaultValueGeometry.apply(this, arguments);
+    this._logMap = {
+        direct: function(v) {
+            return Math.log(v + 1) / Math.log(10);
+        },
+        inverse: function(y) {
+            return Math.exp(Math.log(10) * y) - 1;
+        }
+    }
+    this._mode = "log";
+    this._map = this._logMap;
+    this._calculateGrid = this._logarithmicCalculateGrid;
+};
+
+Timeplot.LogarithmicValueGeometry.prototype._linearCalculateGrid = Timeplot.DefaultValueGeometry.prototype._calculateGrid;
+
+Object.extend(Timeplot.LogarithmicValueGeometry.prototype,Timeplot.DefaultValueGeometry.prototype);
+
+/*
+ * This function calculates the grid spacing that it will be used 
+ * by this geometry to draw the grid in order to reduce clutter. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype._logarithmicCalculateGrid = function() {
+    var grid = [];
+    
+    if (!this._canvas || this._valueRange == 0) return grid;
+
+    var v = 1;
+    var y = this.toScreen(v);
+    while (y < this._canvas.height || isNaN(y)) {
+        if (y > 0) {
+            grid.push({ y: y, label: v });
+        }
+        v *= 10;
+        y = this.toScreen(v);
+    }
+    
+    return grid;
+};
+
+/**
+ * Turn the logarithmic scaling off. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype.actLinear = function() {
+    this._mode = "lin";
+    this._map = this._linMap;
+    this._calculateGrid = this._linearCalculateGrid;
+    this.reset();
+}
+
+/**
+ * Turn the logarithmic scaling on. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype.actLogarithmic = function() {
+    this._mode = "log";
+    this._map = this._logMap;
+    this._calculateGrid = this._logarithmicCalculateGrid;
+    this.reset();
+}
+
+/**
+ * Toggle logarithmic scaling seeting it to on if off and viceversa. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype.toggle = function() {
+    if (this._mode == "log") {
+        this.actLinear();
+    } else {
+        this.actLogarithmic();
+    }
+}
+
+// -----------------------------------------------------
+
+/**
+ * This is the constructor for the default time geometry.
+ * 
+ * @constructor
+ */
+Timeplot.DefaultTimeGeometry = function(params) {
+    if (!params) params = {};
+    this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
+    this._locale = ("locale" in params) ? params.locale : "en";
+    this._timeZone = ("timeZone" in params) ? params.timeZone : SimileAjax.DateTime.getTimezone();
+    this._labeler = ("labeller" in params) ? params.labeller : null;
+    this._axisColor = ("axisColor" in params) ? ((params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
+    this._gridColor = ("gridColor" in params) ? ((params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
+    this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
+    this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "bottom";
+    this._gridStep = ("gridStep" in params) ? params.gridStep : 100;
+    this._gridStepRange = ("gridStepRange" in params) ? params.gridStepRange : 20;
+    this._min = ("min" in params) ? params.min : null;
+    this._max = ("max" in params) ? params.max : null;
+    this._timeValuePosition =("timeValuePosition" in params) ? params.timeValuePosition : "bottom";
+    this._unit = ("unit" in params) ? params.unit : SimileAjax.NativeDateUnit;
+    this._linMap = {
+        direct: function(t) {
+            return t;
+        },
+        inverse: function(x) {
+            return x;
+        }
+    }
+    this._map = this._linMap;
+    if (!this._labeler)
+        this._labeler = (this._unit && ("createLabeller" in this._unit)) ? this._unit.createLabeller(this._locale, this._timeZone) : new Timeline.GregorianDateLabeller(this._locale, this._timeZone);
+    var dateParser = this._unit.getParser("iso8601");
+    if (this._min && !this._min.getTime) {
+        this._min = dateParser(this._min);
+    }
+    if (this._max && !this._max.getTime) {
+        this._max = dateParser(this._max);
+    }
+    this._labels = [];
+    this._grid = [];
+}
+
+Timeplot.DefaultTimeGeometry.prototype = {
+
+    /**
+     * Since geometries can be reused across timeplots, we need to call this function
+     * before we can paint using this geometry.
+     */
+    setTimeplot: function(timeplot) {
+        this._timeplot = timeplot;
+        this._canvas = timeplot.getCanvas();
+        this.reset();
+    },
+
+    /**
+     * Called by all the plot layers this geometry is associated with
+     * to update the time range. Unless min/max values are specified
+     * in the parameters, the biggest range will be used.
+     */
+    setRange: function(range) {
+        if (this._min) {
+            this._earliestDate = this._min;
+        } else if (range.earliestDate && ((this._earliestDate == null) || ((this._earliestDate != null) && (range.earliestDate.getTime() < this._earliestDate.getTime())))) {
+            this._earliestDate = range.earliestDate;
+        }
+        
+        if (this._max) {
+            this._latestDate = this._max;
+        } else if (range.latestDate && ((this._latestDate == null) || ((this._latestDate != null) && (range.latestDate.getTime() > this._latestDate.getTime())))) {
+            this._latestDate = range.latestDate;
+        }
+
+        if (!this._earliestDate && !this._latestDate) {
+            this._grid = [];
+        } else {
+            this.reset(); 
+        }
+    },
+    
+    /**
+     * Called after changing ranges or canvas size to reset the grid values
+     */
+    reset: function() {
+        this._updateMappedValues();
+        if (this._canvas) this._grid = this._calculateGrid();
+    },
+    
+    /**
+     * Map the given date to a x screen coordinate.
+     */
+    toScreen: function(time) {
+        if (this._canvas && this._latestDate) {
+            var t = time - this._earliestDate.getTime();
+            var fraction = (this._mappedPeriod > 0) ? this._map.direct(t) / this._mappedPeriod : 0;
+            return this._canvas.width * fraction;
+        } else {
+            return -50;
+        } 
+    },
+
+    /**
+     * Map the given x screen coordinate to a date.
+     */
+    fromScreen: function(x) {
+        if (this._canvas) {
+            return this._map.inverse(this._mappedPeriod * x / this._canvas.width) + this._earliestDate.getTime();
+        } else {
+            return 0;
+        } 
+    },
+    
+    /**
+     * Get a period (in milliseconds) this time geometry spans.
+     */
+    getPeriod: function() {
+        return this._period;
+    },
+    
+    /**
+     * Return the labeler that has been associated with this time geometry
+     */
+    getLabeler: function() {
+        return this._labeler;
+    },
+
+    /**
+     * Return the time unit associated with this time geometry
+     */
+    getUnit: function() {
+        return this._unit;
+    },
+
+   /**
+    * Each geometry is also a painter and paints the value grid and grid labels.
+    */
+    paint: function() {
+        if (this._canvas) {
+            var unit = this._unit;
+            var ctx = this._canvas.getContext('2d');
+    
+            var gradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+    
+            ctx.strokeStyle = gradient;
+            ctx.lineWidth = this._gridLineWidth;
+            ctx.lineJoin = 'miter';
+    
+            // paint grid
+            if (this._gridColor) {        
+                gradient.addColorStop(0, this._gridColor.toString());
+                gradient.addColorStop(1, "rgba(255,255,255,0.9)");
+    
+                for (var i = 0; i < this._grid.length; i++) {
+                    var tick = this._grid[i];
+                    var x = Math.floor(tick.x) + 0.5;
+                    if (this._axisLabelsPlacement == "top") {
+                        var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
+                            left: x + 4,
+                            top: 2,
+                            visibility: "hidden"
+                        });
+                        this._labels.push(div);
+                    } else if (this._axisLabelsPlacement == "bottom") {
+                        var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
+                            left: x + 4,
+                            bottom: 2,
+                            visibility: "hidden"
+                        });
+                        this._labels.push(div);
+                    }
+                    if (x + div.clientWidth < this._canvas.width + 10) {
+                        div.style.visibility = "visible"; // avoid the labels that would overflow
+                    }
+
+                    // draw separator
+                    ctx.beginPath();
+                    ctx.moveTo(x,0);
+                    ctx.lineTo(x,this._canvas.height);
+                    ctx.stroke();
+                }
+            }
+    
+            // paint axis
+            gradient.addColorStop(0, this._axisColor.toString());
+            gradient.addColorStop(1, "rgba(255,255,255,0.5)");
+            
+            ctx.lineWidth = 1;
+            gradient.addColorStop(0, this._axisColor.toString());
+    
+            ctx.beginPath();
+            ctx.moveTo(0,0);
+            ctx.lineTo(this._canvas.width,0);
+            ctx.stroke();
+        }
+    },
+    
+    /*
+     * This function calculates the grid spacing that it will be used 
+     * by this geometry to draw the grid in order to reduce clutter. 
+     */
+    _calculateGrid: function() {
+        var grid = [];
+        
+        var time = SimileAjax.DateTime;
+        var u = this._unit;
+        var p = this._period;
+        
+        if (p == 0) return grid;
+        
+        // find the time units nearest to the time period
+        if (p > time.gregorianUnitLengths[time.MILLENNIUM]) {
+            unit = time.MILLENNIUM; 
+        } else {
+            for (var unit = time.MILLENNIUM; unit > 0; unit--) {
+                if (time.gregorianUnitLengths[unit-1] <= p && p < time.gregorianUnitLengths[unit]) {
+                    unit--;
+                    break;
+                }
+            }
+        }
+
+        var t = u.cloneValue(this._earliestDate);
+
+        do {
+            time.roundDownToInterval(t, unit, this._timeZone, 1, 0);
+            var x = this.toScreen(u.toNumber(t));
+            switch (unit) {
+                case time.SECOND:
+                  var l = t.toLocaleTimeString();
+                  break;
+                case time.MINUTE:
+                  var m = t.getMinutes();
+                  var l = t.getHours() + ":" + ((m < 10) ? "0" : "") + m;
+                  break;
+                case time.HOUR:
+                  var l = t.getHours() + ":00";
+                  break;
+                case time.DAY:
+                case time.WEEK:
+                case time.MONTH:
+                  var l = t.toLocaleDateString();
+                  break;  
+                case time.YEAR:
+                case time.DECADE:
+                case time.CENTURY:
+                case time.MILLENNIUM:
+                  var l = t.getUTCFullYear();
+                  break;
+            }
+            if (x > 0) { 
+                grid.push({ x: x, label: l });
+            }
+            time.incrementByInterval(t, unit, this._timeZone);
+        } while (t.getTime() < this._latestDate.getTime());
+        
+        return grid;
+    },
+
+    /*
+     * Clear labels generated by this time geometry.
+     */
+    _clearLabels: function() {
+        for (var i = 0; i < this._labels.length; i++) {
+            var l = this._labels[i];
+            var parent = l.parentNode;
+            if (parent) parent.removeChild(l);
+        }
+    },
+        
+    /*
+     * Update the values that are used by the paint function so that
+     * we don't have to calculate them at every repaint.
+     */
+    _updateMappedValues: function() {
+        if (this._latestDate && this._earliestDate) {
+            this._period = this._latestDate.getTime() - this._earliestDate.getTime();
+            this._mappedPeriod = this._map.direct(this._period);
+        } else {
+            this._period = 0;
+            this._mappedPeriod = 0;
+        }
+    }
+    
+}
+
+// --------------------------------------------------------------
+
+/**
+ * This is the constructor for the magnifying time geometry.
+ * Users can interact with this geometry and 'magnify' certain areas of the
+ * plot to see the plot enlarged and resolve details that would otherwise
+ * get lost or cluttered with a linear time geometry.
+ * 
+ * @constructor
+ */
+Timeplot.MagnifyingTimeGeometry = function(params) {
+    Timeplot.DefaultTimeGeometry.apply(this, arguments);
+        
+    var g = this;
+    this._MagnifyingMap = {
+        direct: function(t) {
+            if (t < g._leftTimeMargin) {
+                var x = t * g._leftRate;
+            } else if ( g._leftTimeMargin < t && t < g._rightTimeMargin ) {
+                var x = t * g._expandedRate + g._expandedTimeTranslation;
+            } else {
+                var x = t * g._rightRate + g._rightTimeTranslation;
+            }
+            return x;
+        },
+        inverse: function(x) {
+            if (x < g._leftScreenMargin) {
+                var t = x / g._leftRate;
+            } else if ( g._leftScreenMargin < x && x < g._rightScreenMargin ) {
+                var t = x / g._expandedRate + g._expandedScreenTranslation;
+            } else {
+                var t = x / g._rightRate + g._rightScreenTranslation;
+            }
+            return t;
+        }
+    }
+
+    this._mode = "lin";
+    this._map = this._linMap;
+};
+
+Object.extend(Timeplot.MagnifyingTimeGeometry.prototype,Timeplot.DefaultTimeGeometry.prototype);
+
+/**
+ * Initialize this geometry associating it with the given timeplot and 
+ * register the geometry event handlers to the timeplot so that it can
+ * interact with the user.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.initialize = function(timeplot) {
+    Timeplot.DefaultTimeGeometry.prototype.initialize.apply(this, arguments);
+
+    if (!this._lens) {
+        this._lens = this._timeplot.putDiv("lens","timeplot-lens");
+    }
+
+    var period = 1000 * 60 * 60 * 24 * 30; // a month in the magnifying lens
+
+    var geometry = this;
+    
+    var magnifyWith = function(lens) {
+        var aperture = lens.clientWidth;
+        var loc = geometry._timeplot.locate(lens);
+        geometry.setMagnifyingParams(loc.x + aperture / 2, aperture, period);
+        geometry.actMagnifying();
+        geometry._timeplot.paint();
+    }
+    
+    var canvasMouseDown = function(elmt, evt, target) {
+        geometry._canvas.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+        geometry._canvas.pressed = true;
+    }
+    
+    var canvasMouseUp = function(elmt, evt, target) {
+        geometry._canvas.pressed = false;
+        var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+        if (Timeplot.Math.isClose(coords,geometry._canvas.startCoords,5)) {
+            geometry._lens.style.display = "none";
+            geometry.actLinear();
+            geometry._timeplot.paint();
+        } else {
+            geometry._lens.style.cursor = "move";
+            magnifyWith(geometry._lens);
+        }
+    }
+
+    var canvasMouseMove = function(elmt, evt, target) {
+        if (geometry._canvas.pressed) {
+            var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+            if (coords.x < 0) coords.x = 0;
+            if (coords.x > geometry._canvas.width) coords.x = geometry._canvas.width;
+            geometry._timeplot.placeDiv(geometry._lens, {
+                left: geometry._canvas.startCoords.x,
+                width: coords.x - geometry._canvas.startCoords.x,
+                bottom: 0,
+                height: geometry._canvas.height,
+                display: "block"
+            });
+        }
+    }
+
+    var lensMouseDown = function(elmt, evt, target) {
+        geometry._lens.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);;
+        geometry._lens.pressed = true; 
+    }
+    
+    var lensMouseUp = function(elmt, evt, target) {
+        geometry._lens.pressed = false;
+    }
+    
+    var lensMouseMove = function(elmt, evt, target) {
+        if (geometry._lens.pressed) {
+            var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+            var lens = geometry._lens;
+            var left = lens.offsetLeft + coords.x - lens.startCoords.x;
+            if (left < geometry._timeplot._paddingX) left = geometry._timeplot._paddingX;
+            if (left + lens.clientWidth > geometry._canvas.width - geometry._timeplot._paddingX) left = geometry._canvas.width - lens.clientWidth + geometry._timeplot._paddingX;
+            lens.style.left = left;
+            magnifyWith(lens);
+        }
+    }
+    
+    if (!this._canvas.instrumented) {
+        SimileAjax.DOM.registerEvent(this._canvas, "mousedown", canvasMouseDown);
+        SimileAjax.DOM.registerEvent(this._canvas, "mousemove", canvasMouseMove);
+        SimileAjax.DOM.registerEvent(this._canvas, "mouseup"  , canvasMouseUp);
+        SimileAjax.DOM.registerEvent(this._canvas, "mouseup"  , lensMouseUp);
+        this._canvas.instrumented = true;
+    }
+    
+    if (!this._lens.instrumented) {
+        SimileAjax.DOM.registerEvent(this._lens, "mousedown", lensMouseDown);
+        SimileAjax.DOM.registerEvent(this._lens, "mousemove", lensMouseMove);
+        SimileAjax.DOM.registerEvent(this._lens, "mouseup"  , lensMouseUp);
+        SimileAjax.DOM.registerEvent(this._lens, "mouseup"  , canvasMouseUp);
+        this._lens.instrumented = true;
+    }
+}
+
+/**
+ * Set the Magnifying parameters. c is the location in pixels where the Magnifying
+ * center should be located in the timeplot, a is the aperture in pixel of
+ * the Magnifying and b is the time period in milliseconds that the Magnifying 
+ * should span.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.setMagnifyingParams = function(c,a,b) {
+    a = a / 2;
+    b = b / 2;
+
+    var w = this._canvas.width;
+    var d = this._period;
+
+    if (c < 0) c = 0;
+    if (c > w) c = w;
+    
+    if (c - a < 0) a = c;
+    if (c + a > w) a = w - c;
+    
+    var ct = this.fromScreen(c) - this._earliestDate.getTime();
+    if (ct - b < 0) b = ct;
+    if (ct + b > d) b = d - ct;
+
+    this._centerX = c;
+    this._centerTime = ct;
+    this._aperture = a;
+    this._aperturePeriod = b;
+    
+    this._leftScreenMargin = this._centerX - this._aperture;
+    this._rightScreenMargin = this._centerX + this._aperture;
+    this._leftTimeMargin = this._centerTime - this._aperturePeriod;
+    this._rightTimeMargin = this._centerTime + this._aperturePeriod;
+        
+    this._leftRate = (c - a) / (ct - b);
+    this._expandedRate = a / b;
+    this._rightRate = (w - c - a) / (d - ct - b);
+
+    this._expandedTimeTranslation = this._centerX - this._centerTime * this._expandedRate; 
+    this._expandedScreenTranslation = this._centerTime - this._centerX / this._expandedRate;
+    this._rightTimeTranslation = (c + a) - (ct + b) * this._rightRate;
+    this._rightScreenTranslation = (ct + b) - (c + a) / this._rightRate;
+
+    this._updateMappedValues();
+}
+
+/*
+ * Turn magnification off.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.actLinear = function() {
+    this._mode = "lin";
+    this._map = this._linMap;
+    this.reset();
+}
+
+/*
+ * Turn magnification on.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.actMagnifying = function() {
+    this._mode = "Magnifying";
+    this._map = this._MagnifyingMap;
+    this.reset();
+}
+
+/*
+ * Toggle magnification.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.toggle = function() {
+    if (this._mode == "Magnifying") {
+        this.actLinear();
+    } else {
+        this.actMagnifying();
+    }
+}
+