function Chart(id, instrumentId, exchangeId, quoteSource, allowScroll) {
    if(!ChartCache.works()) {
        throw '[Chart] charts are not supported';    
    }
    if(ChartCache.get(id) !== null && typeof console != 'undefined') {
        console.warn('[Chart] chart with id \''+id+'\' already exists!');
    }
    ChartCache.set(id,this);
    
    this._isiPhone = false;
    
    /**
    * element id
    */
    this._id = id;
    
    /**
    * instrument and source 
    */
    this._instrumentId  = instrumentId;
    this._exchangeId    = exchangeId;
    this._quoteSource   = quoteSource;
    
    /**
    *  size
    */
    this._size = [250,150];
    
    /**
    * style
    */
    this._style = new ChartStyle();

    /**
    * benchmark settings
    */
    this._referenceSpan = null;
    
    /**
    * charts
    */
    this._charts = [];
    this._focusedChart = null;
    
    /**
    * current span 
    */
    this._requestedSpan = null;
    this._span = null;
    this._buffer = null;
    
    /**
    * canvas
    */
    this._canvas = null;
    this._context = null;
    
    /**
    * slider
    */
    this._slider = null;
    this._sliderIndicator = null;
    
    /**
    * loading indicator
    */
    this._load = null;
    this._loading = false;
    
    /**
    * action lock
    */
    this._locked = true;
    this._inputLocked = false;
    
    /**
    * link
    */
    this._link = null;
    
    /**
    * flags
    */
    this._moving = false;
    
    /**
    * sources
    */
    this._sources = [];
    this._defaultSource = null;
    
    //==========================================================
    // destructor
    //==========================================================
    this._delete = function() {
        var e = document.getElementById(id);
        e.removeChild(this._canvas);
        e.removeChild(this._load);
        if(this._slider) {
            e.removeChild(this._slider);
        }        
        
        for(var i=0;i<this._sources.length;i++) {
            this._sources[i].source.release(this._sources[i].subscription);
        }
        
        var bg;
        for(var i=0;i<this._charts.length;i++) {
            bg = document.getElementById(this._id + '_chart_bg_' + i);
            if(bg) {
                e.removeChild(bg);
            }
        }
    }
    
    //==========================================================
    // constructor
    //==========================================================
    // fetch element
    var e = document.getElementById(id);
    if(!e) {
        throw "[Chart] element with id '"+id+"' does not exist!"
    }
    e.innerHTML = '';
    e.style.position = 'relative';
    if(typeof e == 'undefined' || e === null) {
        return null;
    }
    
    var w = e.offsetWidth, h = e.offsetHeight;
    if(w && h) {
        this._size[0] = w;
        this._size[1] = h;
    }                     
    
    // create canvas
    var c       = document.createElement('canvas');
    c.width     = this._size[0];
    c.height    = this._size[1];
    c.id        = id+'_canvas';
    c.style.position = 'absolute';
    e.appendChild(c);
    
    if(typeof G_vmlCanvasManager !== 'undefined') {
        G_vmlCanvasManager.initElement(c);
    }
    
    this._context = c.getContext("2d");
    this._canvas = c;
    this._context.font = "12px 'arial'";
    this._canvas.style.cursor = 'pointer';
   
   // create loading indicator
    var l                   = document.createElement('div');
    l.style.backgroundColor = "#dddddd";
    l.style.backgroundImage = "url('"+ChartSource.prototype._baseURL + "load.gif"+"')";
    l.style.backgroundRepeat= "no-repeat";
    l.style.backgroundPosition="center center";
    l.style.opacity         = 0.4;
    l.style.filter          = "alpha(opacity = 40)";
    l.style.width           = "100%";
    l.style.height          = "100%";
    l.style.position        = 'absolute';
    l.style.display         = 'none';

    e.appendChild(l);
    this._load              = l;
    
    // create simple slider
    var s                   = document.createElement('div');
    s.id = id+'_slider';
    s.style.height          = '30px';
    s.style.width           = '300px';
    s.style.position        = 'absolute';
    s.style.top             = ((this._size[1]/2)-15)+'px';
    s.style.left            = ((this._size[0]/2)-150)+'px';
    s.style.border          = '1px solid black';
    s.style.display         = 'none';
    
    var si                  = document.createElement('div');
    si.style.position        = 'relative';
    si.style.left            = '50px';
    si.style.width           = '50px';
    si.style.height          = '30px';
    si.style.backgroundColor = 'black';
    si.style.opacity        = 0.5;
    s.appendChild(si);
    e.appendChild(s);
    
    this._slider = s;
    this._sliderIndicator = si;
    
    // set event handlers
    if(allowScroll !== false) {
        c.style.cursor = 'pointer';
        var me = this;
        var f = function(e) { me.handleEvent(e); return false; };
        
        function addEvent(e, name, f) {
            if(e.addEventListener) {
                e.addEventListener(name, f, false);
            } else {
                e.attachEvent('on'+name, f);
            }
        }
        
        if(navigator.appVersion.indexOf('iPhone') != -1) {
            this._isiPhone = true;
        
            addEvent(c,'touchmove',f);
            addEvent(c,'touchstart',f);
            addEvent(c,'touchend',f);
            addEvent(s,'touchmove',f);
            addEvent(s,'touchstart',f);
            addEvent(s,'touchend',f);
            addEvent(si,'touchmove',f);
            addEvent(si,'touchstart',f);
            addEvent(si,'touchend',f);
            addEvent(l,'touchmove',f);
            addEvent(l,'touchstart',f);
            addEvent(l,'touchend',f);
        } else {
            addEvent(c,'contextmenu',f);
            addEvent(c,'mousedown',f);
            addEvent(c,'mouseup',f);
            addEvent(c,'mousemove',f);
            addEvent(c,'mouseout',f);
            addEvent(c,'mouseover',f);
            
            addEvent(l,'contextmenu',f);
            addEvent(l,'mousedown',f);
            addEvent(l,'mouseup',f);
            addEvent(l,'mousemove',f);
            addEvent(l,'mouseout',f);
            addEvent(l,'mouseover',f);
        }
        
        if (window.addEventListener) {
            c.addEventListener('DOMMouseScroll', f, false);
            l.addEventListener('DOMMouseScroll', f, false);
        }
        addEvent(c,'mousewheel',f);
        addEvent(l,'mousewheel',f);
    }
    
    //==========================================================
    // internal functions
    //========================================================== 
    /**
    * set size
    */
    this._setSize = function(x,y) {
        this._size = [x,y];
        
        this._slider.style.top  = ((this._size[1]/2)-15)+'px';
        this._slider.style.left = ((this._size[0]/2)-150)+'px';
        
        this._canvas.width = this._size[0];
        this._canvas.height = this._size[1];
        
        var e = document.getElementById(id);
        e.style.width   = this._size[0] + 'px';
        e.style.height  = this._size[1] + 'px';
        
        this.setInterval(this._detectInterval());
        this.redraw();
    }
    
    /**
    * update chart layout
    */
    this._updateLayout = function() {
        var y = 0;
        var b = null;
        for(var i=0;i<this._charts.length;i++) {
            b = document.getElementById(this._id + '_chart_bg_' + i);
            if(b) {
                b.style.height = this._charts[i]._size + 'px';
                b.style.top = y + 'px';
            }
            this._charts[i]._position = y;
            y += this._charts[i]._size;
        }    
        if(y != this._size[1]) {
            this._setSize(this._size[0],y);
        }
    }
    
    /**
    * update scrolling offset
    */
    this._updateOffset = function(visible, left, right) {
        if(this._defaultSource.offset === null) {
            if(visible) {
                var n = ((this._defaultSource.span[1].index-this._defaultSource.span[0].index)+this._defaultSource.offsetL+this._defaultSource.offsetR);
                var tw = (this._size[0]-this._style.offset)/n;
                this._defaultSource.offsetR = Math.ceil(this._style.offset/tw);
            } else {
                this._defaultSource.offsetR = 0;
            }
            this._defaultSource.offsetL = 0;
            this._defaultSource.offset = 0;
        } else {
            if(this._defaultSource.offsetR != 0 && right) {
                this._defaultSource.offsetR = 0;
            } 
            if(this._defaultSource.offsetL != 0 && left) {
                this._defaultSource.offsetL = 0;
            }
        }
    }
    
    /**
    * toggle move state
    */
    this._toggleMove = function(mode) {
        var old = this._moving;
        this._moving = mode;
        if(old != mode) {
            this.redraw();
        }
    }
    
    /**
    * set cursor
    */
    this._setCursor = function(cursor) {
        if(cursor == '') {
            cursor = 'pointer';
        }
        this._canvas.style.cursor = cursor;    
    }
    
    /**
    * focus a value
    */
    this._focus = function(point,load) {
        if(typeof load == 'undefined') {
            load = true;
        }
        
        var w = (this._defaultSource.span[1].index - this._defaultSource.span[0].index) +this._defaultSource.offsetL + this._defaultSource.offsetR;
        w /= 2;
        w = w|0;
        
        var begin = mv(point,-w);
        var end = mv(point,w);
        var offsetL = w - (point.index - begin.index);
        var offsetR = w - (end.index - point.index);
        
        if(load && offsetL > 0) {
            this._toggleLoading(true);
            
            var z = this._interval <= ESpans.DAY ? ESpans.DAY : ESpans.MONTH;
            this._defaultSource.source.requestData(begin.timestamp-z, end.timestamp+z, this._interval, 
                function() {
                    me._toggleLoading(false);
                    
                    me._focus(point,false);
                }
            );
            return;        
        }
        
        this._defaultSource.span = [begin, end];
        this._defaultSource.offsetL = offsetL;
        this._defaultSource.offsetR = offsetR;
        
        this._updateSources();
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i].OnDataUpdated();
        }
        this.redraw();
    }
    
    /**
    * set span direct
    */  
    this._adjustSpan = function(begin, end) {
        if(!begin || !end || end.index <= begin.index) {
            return;
        }
        
        var oldSpan = this._defaultSource.span;
        var or = this._defaultSource.offsetR/(this._defaultSource.span[1].index - this._defaultSource.span[0].index);
            
        this._defaultSource.span = [begin, end];
        this.setInterval(this._detectInterval());
        this._updateOffset(!this._defaultSource.span[1].next,begin!=oldSpan[0],end!=oldSpan[1]);
        
        if(!end.next && oldSpan[1] == end) {
            this._defaultSource.offsetR = or*(this._defaultSource.span[1].index - this._defaultSource.span[0].index);
        }
        
        this._updateSources();
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i].OnDataUpdated();
        }
        this.redraw();
    }
    
    /**
    * detect optimal interval
    */
    this._detectInterval = function(span) {
        if(typeof span == 'undefined') {
            if(this._loading || !this._defaultSource.span) {
                return this._interval;
            }
            span = ((this._defaultSource.span[1].index-this._defaultSource.span[0].index)+this._defaultSource.offsetL+this._defaultSource.offsetR)*this._interval;
        }
        
        var intervals = [ESpans.MONTH, ESpans.WEEK, ESpans.DAY, ESpans.HOUR, ESpans.THIRTY_MINUTES, ESpans.FIFTEEN_MINUTES, ESpans.FIVE_MINUTES, ESpans.MINUTE];
        var w = this._style.pointSpacing + 1;
        for(var c=0;c<this._charts.length;c++) {
            if(this._charts[c]._type == 'candle' || this._charts[c]._type == 'ohlc') {
                w = this._style.candleSpacing + 6;
                break;
            }
        }
        var n = 0;
        for(var i = intervals.length;i>=0;i--) {
            n = span/intervals[i];
            if(this._size[0]/n >= w) {
                return intervals[i];
            }
        }   
        return ESpans.MONTH;         
    }
    
    this._updateReferenceSpan = function(source) {
        if(!this._defaultSource.span) {
            return;            
        }
                                       
        if(typeof source !== 'undefined') {
            if(!source.span || !source.buffer) {
                return;
            }
            
            var value = source.buffer.findTimestamp(this._referenceSpan);
            if(value) {
                source.relative = value.close;
            } else {
                source.relative = 1;
            }
            return;    
        }
        
        for(var j=0;j<this._sources.length;j++) {
            source = this._sources[j];
            
            this._updateReferenceSpan(source);
        }        
    }
    
    this._updateSources = function() {
        if(!this._defaultSource.span) {
            return;            
        }
        
        var begin   = this._defaultSource.span[0];
        var end     = this._defaultSource.span[1];
        var i       = this._defaultSource.buffer.interval;
        
        var me = this;
        var source  = null, res = false;
        for(var j=0;j<this._sources.length;j++) {
            source = this._sources[j];
            if(source == this._defaultSource) {
                continue;
            }
            
            var s = source;
            
            var f =     function(a,b,c,l) {
                            if(!s.buffer || s.buffer.interval != c) {
                                s.buffer   = s.source.getBuffer(c);
                            }
                            if(s.buffer) {
                                s.span = s.buffer.findSpan(begin.timestamp, end.timestamp);
                                me._updateReferenceSpan(s);
                            } else {
                                s.span = null;
                                s.relative = 1;
                            }
                            if(l) {
                                for(var i=0;i<me._charts.length;i++) {
                                    me._charts[i].OnDataUpdated();
                                }
                                me.redraw();
                            }
                        }
                        
            res = source.source.requestData(begin.timestamp,end.timestamp,i,f,true);
        }    
    }
    
    this._removeSource = function(source) {
        for(var i=0;i<this._sources.length;i++) {
            if(this._sources[i] == source) {
                this._sources[i].source.release(this._sources[i].subscription);
                this._sources.splice(i,1);    
                return;
            }
        }
    }
    
    this._getSource = function(instrumentId, exchangeId, quoteSource) {
        if(instrumentId instanceof ChartSource) {
            chartSource = instrumentId;     
        } else {
            for(var i=0;i<this._sources.length;i++) {
                if(this._sources[i].instrumentId == instrumentId && 
                    this._sources[i].exchangeId == exchangeId && this._sources[i].quoteSource == quoteSource) {
                    return this._sources[i];
                }
            }
            chartSource = ChartSourceCache.get(instrumentId, exchangeId, quoteSource);
        }
        var me = this;
        var f = function(newTick) { me.OnPush(newTick); };
        
        var source = {};
        source.source = chartSource;
        source.buffer = null;
        source.span = null;
        source.subscription = f;
        source.offset = 0;
        source.offsetL = 0;
        source.offsetR = 0;
        source.relative = 0;
        source.source.acquire(f);
        this._sources.push(source);
        
        if(this._defaultSource) {
            this._updateSources();
        }
        
        return source;
    }

    this._getContext = function() {
        return this._context;    
    }
    
    this._isLoading = function() {
        return this._loading;
    }
    
    this._toggleLoading = function(mode) {
        if(mode) {
            this._loading = true;
            this._load.style.display = 'block';
        } else {
            this._loading = false;
            this._load.style.display = 'none';
        }
    }
    
    //==========================================================
    // public functions
    //==========================================================
    /**
    * set active source
    */
    this.setSource = function(source) {
        if(this._loading || source == this._defaultSource.source) {
            return;
        }
        
        this._instrumentId  = source._instrumentId;
        this._exchangeId    = source._exchangeId;
        this._quoteSource   = source._quoteSource;
    
        var setSpan = false, start, end, interval;
        if(this._defaultSource && this._defaultSource.span) {
            setSpan     = true;
            start       = this._defaultSource.span[0].timestamp;
            end         = this._defaultSource.span[1].timestamp;
            interval    = this._interval;
            
            var now = ((new Date()).getTime())/1000;
            if(!this._defaultSource.span[0].prev || !this._defaultSource.span[0].prev.prev) {
                start = Math.floor(now / ESpans.DAY)*ESpans.DAY    
            }
            if(!this._defaultSource.span[1].next) {
                end = now;    
            }
        }
        
        this._removeSource(this._defaultSource);
        
        this._defaultSource = null;
        this._defaultSource = this._getSource(source);
        
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i]._source = this._defaultSource;
            this._charts[i].OnSourceChanged();
        }
        
        if(setSpan) {
            this.setSpan(start,end,interval);
        }
    }
    
    /**
    * set instrument
    */
    this.setInstrument = function(instrumentId, exchangeId, quoteSource) {
        this._instrumentId  = instrumentId;
        this._exchangeId    = exchangeId;
        this._quoteSource   = quoteSource;
    
        this.setSource(ChartSourceCache.get(instrumentId, exchangeId, quoteSource));
    }
    
    /**
    * get active source
    */
    this.getSource = function() {
        return this._defaultSource.source;
    }
    
    /**
    * lock input
    */
    this.setLocked = function(flag) {
        if(this._link) {
            this._setCursor('pointer');
        }
        this._inputLocked = flag;
    }
    
    this.isLocked = function() {
        return this._inputLocked;
    }
    
    /**
    * link
    */
    this.setLink = function(link) {
        if(this._inputLocked) {
            this._setCursor('pointer');
        }
        this._link = link;
    }
    
    this.getLink = function() {
        return this._link;
    }
    
    /**
    * set size
    */
    this.setSize = function(x,y) {
        var s = y/this._size[1];
        
        var y = 0;
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i]._size *= s;
        }
        
        this._setSize(x,y);
        this._updateLayout();
        
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i]._updateStyle();
        }
        
        this.redraw();
    }
    
    /**
    * set style
    */
    this.setStyle = function(style) {
        this._style = style;
        this._sliderIndicator.style.backgroundColor = style.sliderColor;
        this._slider.style.border = '1px solid '+style.sliderColor;
        
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i]._updateStyle();
        }
        
        this.redraw();
    };
    
    /**
    * zoom in/out
    */
    this.zoom = function(l,r, load) {
        if(this._loading || !this._defaultSource.span) {
            return;
        }
        
        if(typeof load == 'undefined') {
            load = true;
        }
        
        var begin = this._defaultSource.span[0];
        var end = this._defaultSource.span[1];
        
        // load new data
        var prev = mv(begin,l);
        if(load && l < 0 && (prev.index - begin.index) > l) {
            this._toggleLoading(true);
            
            var z = this._interval <= ESpans.DAY ? ESpans.DAY : ESpans.MONTH;
            this._defaultSource.source.requestData(begin.timestamp-z, end.timestamp+z, this._interval, 
                function() {
                    me._toggleLoading(false);
                    
                    me.zoom(l,r,false);
                }
            );
            return;        
        }
        
        // switch from intraday to history
        if(l < 0) {
            if(!begin.prev && !end.next && this._defaultSource.buffer.interval < ESpans.DAY) {
                this.setSpanSimple(ESpans.MONTH);
                return;    
            }
        // switch from history to intraday
        } else if(l > 0){
            if(!end.next && end.index - begin.index <= 2 && this._defaultSource.buffer.interval >= ESpans.DAY) {
                this.setSpanSimple(ESpans.DAY);
                return;    
            }
        }
        
        if(this._defaultSource.offsetR > 0 && r < 0) {
            this._defaultSource.offsetR += r;
            if(this._defaultSource.offsetR < 0) {
                r = this._defaultSource.offsetR;
                this._defaultSource.offsetR = 0;
            } else {
                r = 0;
            }
        }

        if(this._defaultSource.offsetL > 0 && l > 0) {
            this._defaultSource.offsetL -= l;
            if(this._defaultSource.offsetL < 0) {
                l = -this._defaultSource.offsetL;
                this._defaultSource.offsetL = 0;
            } else {
                l = 0;
            }
        }
        
        begin = mv(begin,l);
        end = mv(end,r);
        if(begin.index < end.index) {
            var or = this._defaultSource.offsetR/(this._defaultSource.span[1].index - this._defaultSource.span[0].index);
            
            var l2 = begin.index - this._defaultSource.span[0].index;
            var r2 = end.index - this._defaultSource.span[1].index;
            this._defaultSource.offsetL -= l-l2;
            this._defaultSource.offsetR += r-r2;
            
            this._defaultSource.span = [begin, end];
            
            this.setInterval(this._detectInterval());
            
            this._updateSources();
            this._updateOffset(!this._defaultSource.span[1].next,l2!=0,r2!=0);
            if(r == 0) {
                this._defaultSource.offsetR = or*(this._defaultSource.span[1].index - this._defaultSource.span[0].index);
            }
            for(var i=0;i<this._charts.length;i++) {
                this._charts[i].OnDataUpdated();
            }
            this.redraw();
        } 
    }
    
    /**
    * scroll sideways
    */
    this.scroll = function(s, load) {
        if(this._loading || !this._defaultSource.span) {
            return;
        }
        
        if(typeof load == 'undefined') {
            var old = this._defaultSource.offset;
            this._defaultSource.offset += s;
            if(((old+s)|0) == (old|0)) {
                this.redraw();
                return;
            }
            load = true;
        }

        s = (this._defaultSource.offset|0);
        
        if(s != 0) {
            var l = s, r = s;
            
            if(this._defaultSource.offsetR > 0 && s < 0) {
                this._defaultSource.offsetR += s;
                if(this._defaultSource.offsetR < 0) {
                    r = this._defaultSource.offsetR;
                    this._defaultSource.offsetR = 0;
                } else {
                    r = 0;
                }
            }
            
            if(this._defaultSource.offsetL > 0 && s > 0) {
                this._defaultSource.offsetL -= s;
                if(this._defaultSource.offsetL < 0) {
                    l = -this._defaultSource.offsetL;
                    this._defaultSource.offsetL = 0;
                } else {
                    l = 0;
                }
            }
            
            var begin = mv(this._defaultSource.span[0],l);
            var end = mv(this._defaultSource.span[1],r);
            
            if(load && ((s < 0 && !begin.prev) || (s > 0 && !end.next))) {
                var me = this;
                
                this._toggleLoading(true);
                
                var res = this._defaultSource.source.requestData(begin.timestamp+s*this._interval, end.timestamp+s*this._interval, this._interval, 
                    function(s,e,i,l) {
                        me._toggleLoading(false);
                        if(l) {
                            me.scroll(s,false);
                        }
                    }
                );
                if(res) {
                    return;
                } else {
                    this._toggleLoading(false);
                }
            }
            
            var l2 = begin.index - this._defaultSource.span[0].index;
            var r2 = end.index - this._defaultSource.span[1].index;
            this._defaultSource.offsetL -= l-l2;
            this._defaultSource.offsetR += r-r2;
            
            if(l != 0 || r != 0) {
                this._defaultSource.offset -= s;
                
                this._defaultSource.span = [begin, end];
                
                this._updateSources();
                this._updateOffset(!this._defaultSource.span[1].next,l2!=0,r2!=0);
                for(var i=0;i<this._charts.length;i++) {
                    this._charts[i].OnDataUpdated();
                }
            }
            
        }
        
        this.redraw();
    }
    
    /**
    * set reference span
    */
    this.setReferenceSpan = function(span) {
        if(span < this._interval) {
            return;
        }
        
        this._referenceSpan = ((new Date()).getTime()/1000)-span;    
        this._updateReferenceSpan();
        
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i].OnDataUpdated();
        }
        
        this.redraw();
    }
    
    /**
    * set span simple
    */
    this.setSpanSimple = function(span, interval,offset) {
        var now     = ((new Date()).getTime()/1000)|0;    
        var end     = Math.ceil(now / ESpans.DAY)*ESpans.DAY;
        var start   = Math.ceil((now - span)/ ESpans.DAY)*ESpans.DAY + 1;
        
        this.setSpan(start,end,interval,offset);
    }
    
    /**
    * set interval
    */
    this.setInterval = function(i) {
        if(this._loading || i == this._interval) {
            return;
        }
        
        var buffer  = this._defaultSource.buffer;
        var begin   = this._defaultSource.span[0], end = this._defaultSource.span[1];
        var n = end.index - begin.index;
        
        if(i > this._interval) {
            do {
                buffer = buffer.parent;
                begin = begin.parent;
                end = end.parent;
            } while(end && begin && buffer && buffer.interval != i);
        } else {
            do {
                buffer = buffer.child;   
                begin = begin.begin;
                end = end.end;
            } while(end && begin && buffer && buffer.interval != i);
        }
        
        if(end && begin && buffer && begin.index < end.index) {
            if((i <= ESpans.DAY && this._interval >= ESpans.DAY) ||
                (i >= ESpans.DAY && this._interval <= ESpans.DAY)) {
                this._defaultSource.offset = null;                
            } else {
                var f = (end.index - begin.index)/n;
                if(this._defaultSource.offset) {
                    this._defaultSource.offset *= f;
                }
                if(this._defaultSource.offsetR) {
                    this._defaultSource.offsetR *= f;
                }
                if(this._defaultSource.offsetL) {
                    this._defaultSource.offsetL *= f;
                }
            }
            
            this._interval = i;
            
            this._defaultSource.buffer = buffer;
            this._defaultSource.span = [begin, end];
            
            this._updateSources();
            this._updateOffset(!this._defaultSource.span[1].next,false,false);
            for(var i=0;i<this._charts.length;i++) {
                this._charts[i].OnDataUpdated();
            }
            this.redraw();
        }
    }
    
    /**
    * set span
    */
    this.setSpan = function(start, end, interval, offset) {
        if(this._isLoading()) {
            return;
        }
        
        // check that range is valid
        if(end < start) {
            throw "invalid range specified!";
        }
        
        var newInterval = interval;
        if(typeof interval == 'undefined' || interval === null) {
            newInterval = this._detectInterval(end-start);
        }
        this._interval = newInterval;
        
        offset = typeof offset != "undefined" ? offset : null;
        this._defaultSource.offset = offset;
        this._defaultSource.offsetL = 0;
        this._defaultSource.offsetR = 0;
        
        this._toggleLoading(true);
        
        this._requestedSpan = [start, end];
        var me = this;
        
        if(newInterval < ESpans.DAY) {
            var span = this._defaultSource.source.getAvailableSpan();
            if(span && start > span[1] && span[1] - span[0] > 0) {
                start = Math.floor(span[1]/ESpans.DAY)*ESpans.DAY;
            }
        }
                    
        this._defaultSource.source.requestData(start, end, newInterval, function(s,e,i) {me.OnDataAvailable(s,e,i);});
    };
    
    /**
    * redraw
    */
    this._drawing = false;
    
    this._redraw = function() {
        var c = this._context;
        c.clearRect(0,0,this._size[0],this._size[1]);    
        
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i]._draw(this._moving);
        }
        
        this._drawing = false;
    }
    
    this.redraw = function() {
        if(this._drawing || this._scrolls.length > 0) {
            return;
        }
        this._drawing = true;
        
        setTimeout('ChartCache.get(\''+this._id+'\')._redraw()', 0);
    }
    
    /**
    * create background element
    */
    this._addBackground = function(h,y,isNavBar) {
        var isFirst = false;
        var e = document.getElementById(this._id);
        var b = document.createElement('div');
        b.id = this._id + '_chart_bg_' + (this._charts.length-1);
        if(!isNavBar) {
            if(this._style.backgroundColor) {
                b.style.backgroundColor = this._style.backgroundColor;
            }
            if(this._charts[this._charts.length-1]._showBackgroundImage && this._style.backgroundImage) {
                b.style.backgroundImage = "url('"+this._style.backgroundImage+"')";
                b.style.backgroundRepeat= "no-repeat";
                b.style.backgroundPosition="center center";
            }
        } else {
            if(this._style.navBarBackgroundColor) {
                b.style.backgroundColor = this._style.navBarBackgroundColor;
            }
        }
        b.style.zIndex = 0;
        b.style.width = '100%';
        b.style.height = h + 'px';
        b.style.position = 'absolute';
        b.style.left = '0px';
        b.style.top = y + 'px';
        e.insertBefore(b, e.firstChild)
    }
    
    /**
    * add navbar
    */
    this.addNavBar = function(h) {
        var y = 0;
        for(var i=0;i<this._charts.length;i++) {
            y += this._charts[i]._size;
        }
        
        var w = this._size[0];
        if(typeof h != 'number') {
            if(this._charts.length == 0 && this._size[1] > 0) {
                h = this._size[1];
            } else {
                h = 100;
            }
        }
        
        var chart = new NavBar(this, this._defaultSource);
        chart._size = h;
        chart._position = y;
        this._charts.push(chart);
        
        this._addBackground(h,y,true);
        this._updateLayout();
        
        this.redraw();
        
        return chart;    
    }
    
    /**
    * add an indicator chart
    */ 
    this.addVolumeChart = function(h) {
        var chart = this.addChart(h,false);
        chart.showData(3);
        chart.showLabels(false,true);
        chart.showMarkers(false);
        chart.showInformation(false);
        chart.showCursor(false);
        
        return chart;
    }
    
    /**
    * add an indicator chart
    */ 
    this.addIndicatorChart = function(h) {
        var chart = this.addChart(h);
        chart.showData(2);
        chart.showLabels(false,true);
        chart.showCurrentValue(false);
        chart.showInformation(false);
        chart.showCursor(false);
        return chart;
    }
    
    /**
    * add a chart
    */
    this.addChart = function(h,showBackgroundImage) {
        var y = 0;
        for(var i=0;i<this._charts.length;i++) {
            y += this._charts[i]._size;
        }
        
        var w = this._size[0];
        if(typeof h != 'number') {
            if(this._charts.length == 0 && this._size[1] > 0) {
                h = this._size[1];
            } else {
                h = 100;
            }
        }
        
        var chart = new XYChart(this, this._defaultSource);
        chart._size = h;
        chart._position = y;
        chart._showBackgroundImage = typeof showBackgroundImage == 'undefined'||showBackgroundImage?true:false;
        
        if(this._style.yAxisMargins[0] + this._style.yAxisMargins[1] > h*0.5) {
            var m = this._style.yLabelFontSize/2;
            if(h*0.5 < m*2) {
                m = 0;
            }
            chart.setYAxisMargins(m,m);        
        }
        
        chart.setType(this._style.chartType);
        this._charts.push(chart);
        
        this._addBackground(h,y);
        this._updateLayout();
        
        this.redraw();  
        
        return chart;    
    }
    
    /**
    * remove a chart by index
    */
    this.removeChart = function(index) {
        if(index < 0 || index >= this._charts.length) {
            return;
        }
        
        var b = document.getElementById(this._id + '_chart_bg_' + index);
        if(b) {
            var e = document.getElementById(this._id).removeChild(b);
        }
        
        this._charts.splice(index, 1);
        for(var i=index;i<this._charts.length;i++) {
            b = document.getElementById(this._id + '_chart_bg_' + (i+1));
            if(b) {
                b.id = this._id + '_chart_bg_' + i;    
            }
        }        
        this._updateLayout();
        
        this.redraw();
    }
    
    /**
    * move a chart
    */
    this.moveChart = function(index, index2) {
        if(index < 0 || index2 < 0 || index >= this._charts.length || index2 >= this._charts.length) {
            return;
        }
        // swap backgrounds
        var b = document.getElementById(this._id + '_chart_bg_' + index);
        var b2 = document.getElementById(this._id + '_chart_bg_' + index2);
        
        b2.id = this._id + '_chart_bg_temp';
        b.id = this._id + '_chart_bg_' + index2;
        b2.id = this._id + '_chart_bg_' + index;
        
        // swap charts
        var c1 = this._charts[index];   
        this._charts[index] = this._charts[index2];
        this._charts[index2] = c1;
        
        this._updateLayout();
        
        this.redraw();
    }
    
    /**
    * get number of active charts
    */
    this.getNumCharts = function() {
        return this._charts.length;
    }
    
    /**
    * get chart
    */
    this.getChart = function(i) {
        if(i<0 || i>=this._charts.length) {
            return null;
        }
        return this._charts[i];
    }
    
    /**
    * save image
    */
    this.saveImage = function(callback) {
        if(this._isLoading()) {
            return;
        }
        
        this._toggleLoading(true);
        
        var xrequest = null;
        if (typeof XMLHttpRequest != 'undefined') {
            xrequest = new XMLHttpRequest();
        } else if (window.ActiveXObject) {
            if (navigator.userAgent.toLowerCase().indexOf("msie 5") != -1) {
                xrequest = new ActiveXObject("Microsoft.XMLHTTP");
            } else {
                xrequest = new ActiveXObject("Msxml2.XMLHTTP");
            }
        }
        
        var data = this._canvas.toDataURL('image/png');
        
        xrequest.open("POST",ChartSource.prototype._baseURL + 'download.php',true);
        
        var me = this;
        xrequest.onreadystatechange = function() {
            if(xrequest.readyState == 4) {
                if(callback) {
                    callback(ChartSource.prototype._baseURL + 'download.php?id=' + xrequest.responseText);
                }
                
                me._toggleLoading(false);
            }
        };
        
        var color, image;
        var info = '{';
        info += '"data": "' + data + '", "charts": [';
        for(var i=0;i<this._charts.length;i++) {
            if(i!=0) {
                info += ',';
            }
            if(this._charts[i] instanceof NavBar ) {
                color = this._style.navBarBackgroundColor!==null?this._style.navBarBackgroundColor:'';
                image = '';
            } else {
                color = this._style.backgroundColor!==null?this._style.backgroundColor:'';
                image = this._charts[i]._showBackgroundImage && this._style.backgroundImage!==null?this._style.backgroundImage:'';
            }
            info += '{"position":'+this._charts[i]._position+', "size":'+this._charts[i]._size+', "backgroundColor":"' + color + '", "backgroundImage":"'+image+'"}';
        }
        info += ']}';
        
        xrequest.send(info);    
    };
    
    //==========================================================
    // event handling
    //==========================================================
    /**
    * called on push
    */
    this.OnPush = function(newTick) {
        // no valid span selected
        if(!this._defaultSource || !this._defaultSource.span) {
            if(!this._loading) {
                var buffer = this._defaultSource.source.getBuffer(60);
                this._defaultSource.span = [buffer.begin,buffer.end];
                this._defaultSource.buffer = buffer;
            } else {
                return;
            }
        }
        
        // if old end is visible, advance
        if(newTick && this._defaultSource.span[1].next == this._defaultSource.buffer.end) {
            this._defaultSource.span[1] = this._defaultSource.span[1].next;
        }
        
        // update charts if end is visible
        this._updateOffset(!this._defaultSource.span[1].next,false,false);
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i].OnDataUpdated();
        }    
        this.redraw();
    }
    
    /**
    * 
    */
    this.OnDataAvailable = function(start, end, interval) {
        if(start === false || end === false || interval === false || end-start == 0) {
            this._toggleLoading(false);
            
            if(interval !== false && this._interval < ESpans.DAY) {
                var span = this._defaultSource.source.getAvailableSpan();
                if(span[1] - span[0] > 0) {
                    var start = Math.floor(span[1]/ESpans.DAY)*ESpans.DAY;
                    this.setSpan(start, start+ESpans.DAY);
                } else {
                    var b = this._defaultSource.source.getBuffer(ESpans.DAY);
                    if(!b || b.size == 1) {     
                        this.setSpanSimple(ESpans.WEEK);    
                    }
                }
            } else {
                this.redraw();
            }
            return;
        }
        
        // detect interval
        interval        = interval<ESpans.DAY?60:ESpans.DAY;
        var buffer      = this._defaultSource.source.getBuffer(interval);
        var realSpan    = buffer.findSpan(start,end);
        var newInterval        = this._detectInterval(((realSpan[1].index-realSpan[0].index)+1)*interval);
        if(newInterval < ESpans.DAY && interval >= ESpans.DAY) {
            var buffer      = this._defaultSource.source.getBuffer(newInterval);
            if(buffer && buffer.begin && buffer.begin.timestamp <= realSpan[0].timestamp) {
                interval = newInterval;
            }
        } else {
            interval = newInterval;
        }
        
        // fetch buffer
        this._defaultSource.buffer    = this._defaultSource.source.getBuffer(interval);
        this._defaultSource.span      = this._defaultSource.buffer.findSpan(start, end);
        
        if(this._referenceSpan === null || this._referenceSpan <= interval) {
            var prevClose = this._defaultSource.source.getPreviousClose();
            if((this._requestedSpan[1] - this._requestedSpan[0]) == ESpans.DAY && prevClose) {
                this._referenceSpan = prevClose.timestamp;
            } else {
                this._referenceSpan = start;    
            }
            this._updateReferenceSpan();
        } else {
            this._updateReferenceSpan(this._defaultSource);
        }
        
        this._interval = interval;
        
        this.setInterval(this._detectInterval());

        this._updateSources();
        this._updateOffset(!this._defaultSource.span[1].next,true,true);
        for(var i=0;i<this._charts.length;i++) {
            this._charts[i].OnDataUpdated();
        }
        
        this._toggleLoading(false);
        
        this.redraw();
    }
    
    /**
    * zoom
    */
    this._updateSlider = function() {
        var w = this._slider.offsetWidth;
        
        this._sliderIndicator.style.width = (((this._defaultSource.span[1].index - this._defaultSource.span[0].index)/this._defaultSource.buffer.size)*w) + 'px';
        this._sliderIndicator.style.left = ((this._defaultSource.span[0].index/this._defaultSource.buffer.size)*w)+ 'px';        
    }
    
    this._scrolls       = [];
    
    /**
    * handle events
    */
    this.handleEvent = function(e) {
        if(this._inputLocked) {
            var button = e.which ? e.which : e.button;
            if(e.type == 'mousedown' && button == 1 && this._link) {
                document.location = this._link;
            }
            if(e.type != 'mousemove' && e.type != 'mouseout' && e.type != 'mouseover') {
                return;    
            }
        }
        
        // find target
        var newFocus = false;
        if(e.type.indexOf('touch') != -1) {
            
            switch(e.type) {
                case 'touchstart':
                    
                if(this._scrolls.length == 0) {
                    this._slider.style.display = 'block';
                    this._updateSlider();
                }
                
                var s = null;
                for(var i=0;i<e.changedTouches.length;i++) {
                    s =  {  id: e.changedTouches[i].identifier, 
                            x: e.changedTouches[i].pageX, y: e.changedTouches[i].pageY,
                            xs: 0, ys: 0};
                            
                    this._scrolls.push(s);
                }
                
                break;
            case 'touchend':
                if(this._scrolls.length > 0) {
                    for(var j=0;j<e.changedTouches.length;j++) {
                        
                        for(var i=0;i<this._scrolls.length;i++) {
                            if(this._scrolls[i].id == e.changedTouches[j].identifier) {
                                this._scrolls.splice(i,i+1);
                                break;
                            }
                        }
                        
                    }
                     
                    if(this._scrolls.length == 0) {
                        this._slider.style.display = 'none';
                        this.redraw();
                    } 
                }
                break;
            case 'touchmove':
                var n = ((this._defaultSource.span[1].index-this._defaultSource.span[0].index)+this._defaultSource.offsetL+this._defaultSource.offsetR);
                var tw = this._size[0]/n;
                    
                var t = null;
                for(var j=0;j<e.changedTouches.length;j++) {
                    for(var i=0;i<this._scrolls.length;i++) {
                        t = this._scrolls[i];
                        if(t.id == e.changedTouches[j].identifier) {
                            t.xs += e.changedTouches[j].pageX - t.x;
                            t.ys += e.changedTouches[j].pageY - t.y;
                            
                            t.x = e.changedTouches[j].pageX;
                            t.y = e.changedTouches[j].pageY;
                        }
                    }
                }
                // zoom
                if(this._scrolls.length == 2) {
                    var left = this._scrolls[0];
                    var right = this._scrolls[1];
                    if(left.x > right.x) {
                        var tmp = right;
                        right = left;
                        left = tmp;
                    }
                    this.zoom(-(left.xs/tw),-(right.xs/tw));
                    left.xs = 0;
                    left.ys = 0;
                    right.xs = 0;
                    right.ys = 0;
                // scroll
                } else if(this._scrolls.length == 1) {
                    var touch = this._scrolls[0];
                    this.scroll(-(touch.xs/tw));
                    touch.xs = 0;
                    touch.ys = 0;
                }
                
                this._updateSlider();
                
                break;
            }  
                   
        } else if(!this._isiPhone){
            var layerX, layerY;
            if(e.layerX && e.layerY) {
                layerX = e.layerX;
                layerY = e.layerY;
            } else {
                layerX = e.offsetX;
                layerY = e.offsetY;
            }
        
            if(e.type == 'mouseover') {
                return;
            } else if(e.type == 'mouseout') {
                // ignore when displaying loading element
                if(this._isLoading()) {
                    return;
                }
                
                if(this._focusedChart!==null) {
                    this._charts[this._focusedChart].handleEvent({type:'mouseout', pageX: e.pageX, pageY: e.pageY, layerX: e.layerX, layerY: e.layerY});    
                }
                this._focusedChart = null;
                
                if(e.target == this._canvas) {
                    this._locked = true;
                    this._setCursor('default');
                }
                return;
            }
            
            if(this._locked) {
                if(e.type == 'mousemove') {
                    this._setCursor('');
                    this._locked = false;
                } else {
                    return;
                }
            }
            
            var p, s;
            for(var i=0;i<this._charts.length;i++) {
                p = this._charts[i]._position;
                s = this._charts[i]._size;
                if(layerY >= p && layerY <= p + s) {
                    newFocus = i;
                    break;
                }
            }
         
            if(newFocus !== this._focusedChart) {
                if(this._focusedChart !== null) {     
                    this._charts[this._focusedChart].handleEvent({type:'mouseout'}, e.which ? e.which : e.button, layerX, layerY);     
                }
                this._charts[newFocus].handleEvent({type:'mouseover'}, e.which ? e.which : e.button, layerX, layerY);  
                this._focusedChart = newFocus;   
            }
            if(newFocus!==false) {
                this._charts[newFocus].handleEvent(e, e.which ? e.which : e.button, layerX, layerY); 
            }
        }
            
        // stop propagation of this event
        e.cancelBubble = true;
        if(e.stopPropagation) {
            e.stopPropagation();
        }
        if (e.preventDefault) {
            e.preventDefault();
        }
        e.returnValue = false;

        return false;
    }
    
    // add source
    this._defaultSource = this._getSource(instrumentId, exchangeId, quoteSource);
    this._setCursor('default');
};