Results 1 to 2 of 2

Thread: Ext.ux.chart.Histogram(non-flash version)

  1. #1
    Ext User
    Join Date
    Apr 2008
    Location
    hangzhou,china
    Posts
    26

    Default Ext.ux.chart.Histogram(non-flash version)

    hi ~my friends
    i'd like to share my extension,
    there was a Ext.chart already available,but it goes along with the adobe flash.my solution is totally pure javascript(thanks to google iecanvas).so ~if you don't want use flash in application,you can use my extension instead.
    wish you have interesting with it.
    ?????????????Ext.ux.chart.Histogram????????extension,???????????
    below is the constructor code:
    ???????????
    Code:
    Ext.ns("Ext.ux.chart");
    
    /**
     * @class Ext.ux.chart ??????
     * @author cdj
     * @version beta1.4.4
     */
    
    Ext.ux.chart = Ext.extend(Ext.Component, {
        /**
         * @cfg ?????
         */
        width:300,
        /**
         * @cfg ?????
         */
        height:160,
        /**
         * @cfg ?????????????
         */
        offsets:'5,40,5,5',
        /**
         * @cfg ??css?
         */
        baseCls:'ux-chart',
        /**
         * @cfg ?????
         */
        borderWidth:1,
        /**
         * @cfg store
         */
        store:{},
        initComponent:function(){
            Ext.ux.chart.superclass.initComponent.call(this);
            this.addEvents(
                //supported event list here
            );
            this.ds = this.store;
        },
        
        onRender:function(ct,pos){
            this.preCreateSkeleton();
            this.createSkeleton(ct,pos);
            this.createXHandler();//behavior
            
            this.romCVX.init();
            this.ramCVX.init();
            this.ds.on('load',this.onLoad,this);
        },
        
        /**
         * ????????????????
         */
        preCreateSkeleton:function(){
            var _off = this.offsets.split(',');
            Ext.applyIf(this,{
                nOffset:Ext.isGecko?new Number(_off[0])+1:new Number(_off[0]),//?????????
                eOffset:new Number(_off[1]),
                sOffset:new Number(_off[2]),
                wOffset:Ext.isGecko?new Number(_off[3])+7:new Number(_off[3])
            });
            this.chartHeight = this.height + this.sOffset + this.nOffset;//??
            this.chartWidth = this.width + this.wOffset + this.eOffset;
    
        },
        
        /**
         * ???????????,???????????
         * @param ct ?????????
         * @param pos 
        */
        createSkeleton:function(ct,pos){
            var ramLayerId = Ext.id("","ui2-chart-ram-cvs-");
            var romLayerId = Ext.id("","ui2-chart-rom-cvs-");
            var labelLayerId = Ext.id("","ui2-chart-label-");//???
            var evLayerId = Ext.id("","ui2-chart-ev-");//???
            var tl = "position:absolute;left:0px;top:0px;";
            this.style += "position:relative;";
            var tpl = new Ext.XTemplate(
                '<div>',
                '<canvas id ='+romLayerId+' width = "'+this.chartWidth+'" height = "'+this.chartHeight+'" style='+tl+'background:'+this.bgColor+'></canvas>',//things that wouldn't be changed
                '<div id = "'+labelLayerId+'" style = "position:absolute;left:0px;top:0px;"></div>',//?????
                '<canvas id ='+ramLayerId+' width = "'+this.chartWidth+'" height = "'+this.chartHeight+'" style='+tl+'></canvas>',//things that would be changed in run-time
                '<div id = "'+evLayerId+'" style = "position:absolute;left:0px;top:0px;"></div>',//?????
                '</div>'
            );
            this.el = tpl.append(ct,"",true);
            if(this.baseCls){
                this.el.addClass(this.baseCls);
            }
            if(this.cls){
                this.el.addClass(this.cls);
            }
            if(this.id){
                this.el.dom.id = this.el.id = this.id;
            }
            this.evct = Ext.get(evLayerId);
            var ramCV = this.el.child("#"+ramLayerId,true);
            this.lbct = Ext.get(labelLayerId);
            var romCV = this.el.child("#"+romLayerId,true);
            if(Ext.isIE){
                ramCV = Ext.get(window.G_vmlCanvasManager.initElement(ramCV)).dom;
                romCV = Ext.get(window.G_vmlCanvasManager.initElement(romCV)).dom;
            }
            this.ramCVX = ramCV.getContext('2d');
            this.romCVX = romCV.getContext('2d');
            
        },
        
        /**
         * rom?????? override it
         */
        romLayerInitial:Ext.emptyFn,
        
        /**
         * ram?????? override it
         */
        ramLayerInitial:Ext.emptyFn,
        
        /**
         * ram????? override it
         */
        draw:Ext.emptyFn,
        
        /**
         * ram????? override it
         */
        redraw:Ext.emptyFn,
        
        createXHandler:function(){
            Ext.applyIf(this,{
                gridBgColor:this.colors.gridbg,
                gridColor:this.colors.grid,
                frameColor:this.colors.frame,
                lineColor:this.colors.line
            });
            
            this.romCVX.init = this.romLayerInitial.createDelegate(this.romCVX,[this]);
            delete this.romLayerInitial;
            
            Ext.apply(this.ramCVX,{
                init :this.ramLayerInitial.createDelegate(this.ramCVX,[this]),
                draw :this.draw.createDelegate(this.ramCVX,[this]),
                redraw:this.redraw.createDelegate(this.ramCVX,[this])
            });
            
            delete this.romLayerInitial;
            delete this.draw;
            delete this.redraw;
        },
        
        addLabel:function(conf){
            var lbf = {};
            Ext.apply(lbf,conf,{
                id:Ext.id('','ui2-monitor-label-'),
                position:[0,0],
                tag:'span'
            });
            if(typeof lbf.style != "undefined"){
                lbf.style += ";font-size:12px;white-space:nowrap";
            }else {
                lbf.style = "font-size:12px;white-space:nowrap";
            }
            if(typeof lbf.p == "undefined"){
                lbf.p = lbf.position;
            }
            //?????font-family?????????ie???????
            lbf.style += (";font-family:tahoma,arial,helvetica,sans-serif;position:absolute;left:"+lbf.p[0]+"px;top:"+lbf.p[1]+"px");
            delete lbf.p;
            delete lbf.position;
            if(lbf.vmode){
                lbf.style += ';writing-mode:tb-rl;';
                delete lbf.vmode;
            }
            
            if(typeof lbf.background != "undefined"){
                lbf.style += (";background:"+lbf.background);
            }
            var label = this.lbct.createChild(lbf,'',false);
            return label;
        },
        getStore:function(){
            return this.store;  
        },
        /**
         * override it
         */
        onLoad:Ext.emptyFn
    })
    
    /**
     * ??????
     * @author cdj
     * @usage:
     *    var histogram = new Ext.ux.chart.Histogram({
            //renderTo:cdjel,
            width:300,
            height:200,
            colSize:20,
            gapSize:0.9,
            _coloffset:30,
            offsets:'10,0,80,10',
            valueField:'value',
            nameField:'name',
            store:new Ext.data.SimpleStore({
                fields:["name","value"],
                autoLoad:true,
                url:'res.json'
            }),
            colors:{
                gridbg:'rgb(241,233,196)',
                grid:'rgb(196,196,196)',
                frame:'rgb(0,0,0)',
                line:'rgb(0,0,0)'
            }, 
            yAxis:[{
                text:'10K',
                value:10*1024
            },{
                text:'1K',
                value:1*1024
            },{
                text:'500bytes',
                value:500
            },{
                text:'100bytes',
                value:100
            },{
                text:'10bytes',
                value:10
            }]
        });
    */
    Ext.ux.chart.Histogram = Ext.extend(Ext.ux.chart, {
        xfontSize:12,
        gapScale:0.2,
        colsLeftOffset:20,//????
        xTickTpl:"",
        /**
         * @cfg ??
         */
        colors:{
            gridbg:'rgb(241,233,196)',
            grid:'rgb(196,196,196)',
            frame:'rgb(0,0,0)',
            line:'rgb(0,0,0)'
        },
        
        /**
         * rom??????
         */
        romLayerInitial:function(chart){
            chart.xScale = (chart.width - 2*chart.borderWidth)/chart.xAxisN;
            chart.yScale = (chart.height - 2*chart.borderWidth)/chart.yAxis.length;
            this.save();
            this.translate(chart.wOffset,chart.nOffset);
            this.fillStyle = chart.gridBgColor;
            this.fillRect(0,0,chart.width,chart.height);
            this.fill();
            this.strokeStyle = chart.frameColor;
            this.moveTo(0,0);
            this.lineTo(0,chart.height);
            this.lineTo(chart.width,chart.height);
            this.lineTo(chart.width,0);
            this.lineTo(0,0);
            this.stroke();
            // draw the grid and ticks
            
            this.strokeWidth = chart.borderWidth;//add by cj
            
            this.strokeStyle = chart.gridColor;
            var _p = [];// position for label
            var _s;// scale for each tick
            for(var i = 0;i < chart.yAxis.length;i++){
                _s = i*chart.yScale;
                this.moveTo(0,_s);
                this.lineTo(chart.width+5,_s);// this '5' is the small line for tick
                _p[0] = chart.chartWidth - chart.eOffset + 6; // this '4' is just for adjust the tick's position
                _p[1] = _s +3;
                chart.addLabel({
                    position:_p,
                    //vmode:true,
                    html:chart.yAxis[i].text
                })
            }
            this.stroke();
            //eof draw
        },
        ramLayerInitial:function(chart){
            this.save();
            this.translate(chart.wOffset,chart.nOffset);
            this.lineStyle = chart.lineColor;
        },
        /**
         * @override ??onload
         */
        onLoad:function(){
            this.doDataConvert();
            this.ramCVX.clearRect(0,0,10000,10000);
            if(this.evct.first()){
                var id = this.evct.id;
                Ext.destroy(this.evct);//reconstruct evct layer
                this.evct = Ext.DomHelper.append(this.el,{
                    id:id
                },true);
            }else {
                //do nothing
            }
            this.ramCVX.draw();
            Ext.ux.chart.Histogram.superclass.onLoad.call(this);
        },
        /**
         * @private ????
         * ?????column modal??????????????
         * ????????store??????????????????????
         * ???????????????????????draw???store??????
         */
        doDataConvert:function(){
            this.pd = {};//???????,data for paint 
            var sow = (this.width-this.colsLeftOffset)/this.ds.getCount();//self owned width
            
            //calculate gapWidth
            var gw = sow*this.gapScale;
            
            //calculate every ColumnWidth
            var cw = sow - gw;
            var cols = [];
            for(var i=0,cm = this.columnModal;i<cm.length;i++){
                //Ext.log(cm[i].width)
                var _tpl = new Ext.XTemplate(cm[i].tipTpl||"{"+cm[i].dataIndex+"}");
                _tpl.compile();
                var c = {
                    width:cm[i].width*cw,
                    tipTpl:_tpl,
                    dataIndex:cm[i].dataIndex,
                    color:cm[i].color || "#000"
                    //,color:"rgba()"
                };
                cols.push(c);
            }
            var _xpl = new Ext.XTemplate(this.xTickTpl);
            _xpl.compile();
            Ext.apply(this.pd,{
                gapWidth:gw,
                xTpl:_xpl,
                selfOwnedWidth:sow,
                columnsWidth:cw,
                colCfgs:cols
            })
        },
        /**
         * @override ???????
         */
        draw:function(chart){
            var pd = chart.pd;
            var cf = pd.colCfgs;
            this.beginPath();
            this.save();
            this.translate(chart.colsLeftOffset+chart.borderWidth,chart.borderWidth);
            var parseColor = Ext.ux.chart.ColorManager.parseColor.createDelegate(Ext.ux.chart.ColorManager);
            chart.ds.each(function(r,i){
                var _off = 0;//??????????
                for(var ii = 0;ii<cf.length;ii++){
                    
                    var _x = i*pd.selfOwnedWidth+_off;
                    var _y = chart.getYPixelByValue(r.get(cf[ii].dataIndex));
                    var _w = cf[ii].width;
                    var _h = chart.height-2*chart.borderWidth-_y;
                    var _clor = parseColor(cf[ii].color);
                    //begin render column
                    var gradient = this.createLinearGradient(_x,_y,_x+_w,_y);
                    gradient.addColorStop(0, _clor.s);
                    gradient.addColorStop(1, _clor.e);
                    this.fillStyle = gradient;
                    this.fillRect(_x,_y,_w,_h);
                    this.fill();
                    
                      //???
                    var m = parseInt(cf[ii].width*0.5);
                    var gradient2 = this.createLinearGradient(_x+m,_y,_x+_w,_y);
                    gradient2.addColorStop(0, 'rgba(235,245,235,0.2)');
                    gradient2.addColorStop(1, 'rgba(235,245,235,0.6)');
                    this.fillStyle = gradient2;
                    this.fillRect(_x+m,_y,_w-m,_h);
                    this.fill();
                    //eof render column
                    
                    //add tip
                    var _ev = chart.addBlankImage({
                        p:[chart.wOffset+chart.colsLeftOffset+_x,_y+chart.nOffset],
                        height:_h,
                        width:_w,
                        background:''
                    });
                    new Ext.ToolTip({
                        target: _ev,
                        title: cf[ii].title || pd.xTpl.apply(r.data),
                        width:200,
                        html: cf[ii].tipTpl.apply(r.data),
                        trackMouse:true
                    });
                    //eof add tip
                    
                    _off += _w;
                }//eof pd cols
                
                var tx= i*pd.selfOwnedWidth+0.5*pd.columnsWidth;
                //TODO ???
                if(Ext.isGecko){
                    this.save();
                    this.fillStyle = chart.lineColor;
                    this.translate(tx-parseInt(chart.xfontSize/6),chart.height+5);
                    this.rotate(90 * Math.PI / 180);
                    this.mozTextStyle = chart.xfontSize+"px simsun";
                    this.mozDrawText(pd.xTpl.apply(r.data));
                    this.restore();
                }else {
                    //ie
                    chart.addLabel({
                        html:pd.xTpl.apply(r.data),
                        p:[chart.colsLeftOffset + tx-parseInt(chart.xfontSize/6)+chart.wOffset,chart.height+chart.nOffset+5],
                        vmode:true
                    })
                }
            },this);
            this.closePath();
            this.stroke();
            this.restore();
        },
        /**
         * ??????
         */
        reconstruct:function(config){
            Ext.apply(this,config);
            //clear up rom and ram layer
            //this.ramCVX.restore();
            this.ramCVX.restore();
            //this.ramCVX.restore();
            this.ramCVX.clearRect(0,0,10000,10000);
            
            if(this.evct.first()){
                var id = this.evct.id;
                Ext.destroy(this.evct);//reconstruct evct layer
                this.evct = Ext.DomHelper.append(this.el,{
                    id:id
                },true);
            }else {
                //do nothing
            }
            this.romCVX.restore();
            this.romCVX.clearRect(0,0,10000,10000);
            
            if(this.lbct.first()){
                var id = this.lbct.id;
                Ext.destroy(this.lbct);//reconstruct evct layer
                this.lbct = Ext.DomHelper.append(this.el,{
                    id:id
                },true);
            }else {
                //do nothing
            }
            this.romCVX.init();
            this.ramCVX.init();
    
            
        },
        /**
         * ????
         */
        redraw:function(){
            this.ramCVX.clearRect(0,0,10000,10000);
            if(this.evct.first()){
                var id = this.evct.id;
                Ext.destroy(this.evct);//reconstruct evct layer
                this.evct = Ext.DomHelper.append(this.el,{
                    id:id
                },true);
            }else {
                //do nothing
            }
            this.ramCVX.draw();
        },
        /**
         * @private
         * @describe addBlankImage
         */
        addBlankImage:function(conf){
            var lbf = {};
            Ext.apply(lbf,conf,{
                position:[0,0],
                src:Ext.BLANK_IMAGE_URL,
                tag:'img',
                width:10,
                height:10
            });
            if(lbf.style){
                lbf.style += ';font-size:12px;white-space:nowrap';
            }else {
                lbf.style = 'font-size:12px;white-space:nowrap';
            }
            if(!lbf.p){
                lbf.p = lbf.position;
            }
            lbf.style += (";position:absolute;left:"+lbf.p[0]+"px;top:"+lbf.p[1]+"px;height:"+lbf.height+"px;width:"+lbf.width);
            delete lbf.p;
            delete lbf.position;
            if(lbf.vmode){
                lbf.style += ';writing-mode:tb-rl';
                delete lbf.vmode;
            }
            if(lbf.background){
                lbf.style += (";background:"+lbf.background);
            }
            var label = this.evct.createChild(lbf,'',false);
            return label;
        },
        /**
         * @private
         * @describe ??Y??????????????????
         * @returns Y???
        */
        getYPixelByValue:function(v){
            var max,min;
            for(var i = 0;i < this.yAxis.length;i++){
                if(v >= this.yAxis[i].value){
                    max = i==0?v:this.yAxis[i-1].value;
                    min = this.yAxis[i].value;
                    break;
                }else {
                    continue;
                }
            }
            var i = i-1;
            if(typeof min == 'undefined'){
                min = 0;
                max = this.yAxis[i].value;
            }
            var result;
            if(max == min){
                result = 0;
            }else{
                result = (1-(v - min)/(max - min) + i)*this.yScale;
            }
            return result;
        }
    });
    
    Ext.ux.chart.ColorManager = {
        parseColor: function(str){
            
            var result;
    
            // rgb(num,num,num)
            if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))
                return this.getColor(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));
        
            // rgba(num,num,num,num)
            if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
                return this.getColor(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4]));
                
            // rgb(num%,num%,num%)
            if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))
                return this.getColor(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
        
            // rgba(num%,num%,num%,num)
            if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
                return this.getColor(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
                
            // #a0b1c2
            if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))
                return this.getColor(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));
        
            // #fff
            if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))
                return this.getColor(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
    
            // Otherwise, we're most likely dealing with a named color.
            var name = str.trim().toLowerCase();
            if(name == 'transparent'){
                return this.getColor(255, 255, 255, 0);
            }
            return ((result = this.colors[name])) ? this.getColor(result[0], result[1], result[2]) : false;
        },
        getColor: function(r, g, b, a){
            this.rgba = ['r','g','b','a'];
            var x = 4;
            while(-1<--x){
                this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
            }
            return this.normalize();
        },
        normalize: function(){
            var limit = this.limit;
            this.r = limit(parseInt(this.r), 0, 255);
            this.g = limit(parseInt(this.g), 0, 255);
            this.b = limit(parseInt(this.b), 0, 255);
            this.a = limit(this.a, 0, 1);
            return {s:"rgba("+this.r+","+this.g+","+this.b+","+this.a+")",e:"rgba("+this.r+","+this.g+","+this.b+","+(this.a-0.5)+")"};
            //return "rgba("+this.r+","+this.g+","+this.b+","+this.a+")";
        },
        limit: function(val,minVal,maxVal){
            return Math.max(Math.min(val, maxVal), minVal);
        },
        colors : {
            aqua:[0,255,255],
            azure:[240,255,255],
            beige:[245,245,220],
            black:[0,0,0],
            blue:[0,0,255],
            brown:[165,42,42],
            cyan:[0,255,255],
            darkblue:[0,0,139],
            darkcyan:[0,139,139],
            darkgrey:[169,169,169],
            darkgreen:[0,100,0],
            darkkhaki:[189,183,107],
            darkmagenta:[139,0,139],
            darkolivegreen:[85,107,47],
            darkorange:[255,140,0],
            darkorchid:[153,50,204],
            darkred:[139,0,0],
            darksalmon:[233,150,122],
            darkviolet:[148,0,211],
            fuchsia:[255,0,255],
            gold:[255,215,0],
            green:[0,128,0],
            indigo:[75,0,130],
            khaki:[240,230,140],
            lightblue:[173,216,230],
            lightcyan:[224,255,255],
            lightgreen:[144,238,144],
            lightgrey:[211,211,211],
            lightpink:[255,182,193],
            lightyellow:[255,255,224],
            lime:[0,255,0],
            magenta:[255,0,255],
            maroon:[128,0,0],
            navy:[0,0,128],
            olive:[128,128,0],
            orange:[255,165,0],
            pink:[255,192,203],
            purple:[128,0,128],
            violet:[128,0,128],
            red:[255,0,0],
            silver:[192,192,192],
            white:[255,255,255],
            yellow:[255,255,0]
        }
    };
    Ext.reg('ux-chart-histogram', Ext.ux.chart.Histogram);
    and below is the usage example:

    Code:
    Ext.BLANK_IMAGE_URL = 'lib/ext/resources/images/default/s.gif';
    
    Ext.onReady(function(){
        //????
        //new a chart
        var histogram = new Ext.ux.chart.Histogram({
            width:700,
            height:300,
            //renderTo:cdjel,
            offsets:'10,40,80,10',
            store:new Ext.data.SimpleStore({
                fields:["name","value1","value2"],
                autoLoad:true,
                url:'res.json'
            }),
            xTickTpl:"2009 ?{name}",
            columnModal:[{
                dataIndex:'value1',
                width:0.5,
                tipTpl:"??????{value1}",
                color:'olive'
                //color:"rgba(0,0,0,1)"
                },{
                dataIndex:'value2',
                //tipTpl:"??????{value1}",
                //color:'purple',
                width:0.2
                },{
                dataIndex:'value2',
                width:0.3,
                tipTpl:"??????{value2}",
                color:'navy'
            }],
            colors:{
                gridbg:'rgb(241,233,196)',
                grid:'rgb(196,196,196)',
                frame:'rgb(0,0,0)',
                line:'rgb(0,0,0)'
            }, 
            yAxis:[{
                text:'10K',
                value:10*1024
                },{
                text:'1K',
                value:1*1024
                },{
                text:'500bytes',
                value:500
                },{
                text:'100bytes',
                value:100
                },{
                text:'10bytes',
                value:10
            }],
            listeners:{
                render:function(){
                    //Ext.log("my chart rendered");
                }
            }
        });
        
        
        var win = new Ext.Window({
            width:800,
            height:460,
            title:'????',
            items:[histogram]
        })
        win.show();
       
    })
    
    in this example,your data from the server should looked like this :
    
    Code:
    [
        ['??','1020','100'],
        ['??','300','500'],
        ['??','500','200'],
        ['??','200','400'],
        ['??','200','700'],
        ['??','300','100'],
        ['??','500','600'],
        ['??','1000','100']
    ]
    there is a screenshot and a demo in the attachment.
    the demo which is without ext lib in it,please copy a ext lib file into the right folder when you run it,it's compatible with Ext2.0 & 3.0

    notice that :
    if you wanna use this extension in ie,you must include the "google iecanvas" in your page
    Code:
    <!--[if IE]><script type="text/javascript" src="lib/mylib/excanvas.js"></script><![endif]-->
    .its size is just 27k.

    I'd appreciate comments and/or suggestions.

    sorry for my broken english,i hope you could understand what i wrote
    Attached Images Attached Images
    Attached Files Attached Files
    Last edited by cdj; 2 Aug 2009 at 7:08 PM. Reason: some grammar mistake

  2. #2

    Thumbs up Cool

    Very great code.
    Thx

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •