The source code

    /*!
     * Ext JS Library 3.3.1
     * Copyright(c) 2006-2010 Sencha Inc.
     * licensing@sencha.com
     * http://www.sencha.com/license
     */
    Ext.ns('Ext.ux.grid');

    /**
     * @class Ext.ux.grid.GroupSummary
     * @extends Ext.util.Observable
     * A GridPanel plugin that enables dynamic column calculations and a dynamically
     * updated grouped summary row.
     */
    Ext.ux.grid.GroupSummary = Ext.extend(Ext.util.Observable, {
        /**
         * @cfg {Function} summaryRenderer Renderer example:
    summaryRenderer: function(v, params, data){
        return ((v === 0 || v > 1) ? '(' + v +' Tasks)' : '(1 Task)');
    },
         *
         */
        /**
         * @cfg {String} summaryType (Optional) The type of
         * calculation to be used for the column.  For options available see
         * {@link #Calculations}.
         */

        constructor : function(config){
            Ext.apply(this, config);
            Ext.ux.grid.GroupSummary.superclass.constructor.call(this);
        },
        init : function(grid){
            this.grid = grid;
            var v = this.view = grid.getView();
            v.doGroupEnd = this.doGroupEnd.createDelegate(this);

            v.afterMethod('onColumnWidthUpdated', this.doWidth, this);
            v.afterMethod('onAllColumnWidthsUpdated', this.doAllWidths, this);
            v.afterMethod('onColumnHiddenUpdated', this.doHidden, this);
            v.afterMethod('onUpdate', this.doUpdate, this);
            v.afterMethod('onRemove', this.doRemove, this);

            if(!this.rowTpl){
                this.rowTpl = new Ext.Template(
                    '',
                    '',
                        '{cells}',
                    ''
                );
                this.rowTpl.disableFormats = true;
            }
            this.rowTpl.compile();

            if(!this.cellTpl){
                this.cellTpl = new Ext.Template(
                    '',
                    '{value}',
                    ""
                );
                this.cellTpl.disableFormats = true;
            }
            this.cellTpl.compile();
        },

        /**
         * Toggle the display of the summary row on/off
         * @param {Boolean} visible true to show the summary, false to hide the summary.
         */
        toggleSummaries : function(visible){
            var el = this.grid.getGridEl();
            if(el){
                if(visible === undefined){
                    visible = el.hasClass('x-grid-hide-summary');
                }
                el[visible ? 'removeClass' : 'addClass']('x-grid-hide-summary');
            }
        },

        renderSummary : function(o, cs){
            cs = cs || this.view.getColumnData();
            var cfg = this.grid.getColumnModel().config,
                buf = [], c, p = {}, cf, last = cs.length-1;
            for(var i = 0, len = cs.length; i < len; i++){
                c = cs[i];
                cf = cfg[i];
                p.id = c.id;
                p.style = c.style;
                p.css = i == 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
                if(cf.summaryType || cf.summaryRenderer){
                    p.value = (cf.summaryRenderer || c.renderer)(o.data[c.name], p, o);
                }else{
                    p.value = '';
                }
                if(p.value == undefined || p.value === "") p.value = " ";
                buf[buf.length] = this.cellTpl.apply(p);
            }

            return this.rowTpl.apply({
                tstyle: 'width:'+this.view.getTotalWidth()+';',
                cells: buf.join('')
            });
        },

        /**
         * @private
         * @param {Object} rs
         * @param {Object} cs
         */
        calculate : function(rs, cs){
            var data = {}, r, c, cfg = this.grid.getColumnModel().config, cf;
            for(var j = 0, jlen = rs.length; j < jlen; j++){
                r = rs[j];
                for(var i = 0, len = cs.length; i < len; i++){
                    c = cs[i];
                    cf = cfg[i];
                    if(cf.summaryType){
                        data[c.name] = Ext.ux.grid.GroupSummary.Calculations[cf.summaryType](data[c.name] || 0, r, c.name, data);
                    }
                }
            }
            return data;
        },

        doGroupEnd : function(buf, g, cs, ds, colCount){
            var data = this.calculate(g.rs, cs);
            buf.push('', this.renderSummary({data: data}, cs), '');
        },

        doWidth : function(col, w, tw){
            if(!this.isGrouped()){
                return;
            }
            var gs = this.view.getGroups(),
                len = gs.length,
                i = 0,
                s;
            for(; i < len; ++i){
                s = gs[i].childNodes[2];
                s.style.width = tw;
                s.firstChild.style.width = tw;
                s.firstChild.rows[0].childNodes[col].style.width = w;
            }
        },

        doAllWidths : function(ws, tw){
            if(!this.isGrouped()){
                return;
            }
            var gs = this.view.getGroups(),
                len = gs.length,
                i = 0,
                j,
                s,
                cells,
                wlen = ws.length;

            for(; i < len; i++){
                s = gs[i].childNodes[2];
                s.style.width = tw;
                s.firstChild.style.width = tw;
                cells = s.firstChild.rows[0].childNodes;
                for(j = 0; j < wlen; j++){
                    cells[j].style.width = ws[j];
                }
            }
        },

        doHidden : function(col, hidden, tw){
            if(!this.isGrouped()){
                return;
            }
            var gs = this.view.getGroups(),
                len = gs.length,
                i = 0,
                s,
                display = hidden ? 'none' : '';
            for(; i < len; i++){
                s = gs[i].childNodes[2];
                s.style.width = tw;
                s.firstChild.style.width = tw;
                s.firstChild.rows[0].childNodes[col].style.display = display;
            }
        },

        isGrouped : function(){
            return !Ext.isEmpty(this.grid.getStore().groupField);
        },

        // Note: requires that all (or the first) record in the
        // group share the same group value. Returns false if the group
        // could not be found.
        refreshSummary : function(groupValue){
            return this.refreshSummaryById(this.view.getGroupId(groupValue));
        },

        getSummaryNode : function(gid){
            var g = Ext.fly(gid, '_gsummary');
            if(g){
                return g.down('.x-grid3-summary-row', true);
            }
            return null;
        },

        refreshSummaryById : function(gid){
            var g = Ext.getDom(gid);
            if(!g){
                return false;
            }
            var rs = [];
            this.grid.getStore().each(function(r){
                if(r._groupId == gid){
                    rs[rs.length] = r;
                }
            });
            var cs = this.view.getColumnData(),
                data = this.calculate(rs, cs),
                markup = this.renderSummary({data: data}, cs),
                existing = this.getSummaryNode(gid);

            if(existing){
                g.removeChild(existing);
            }
            Ext.DomHelper.append(g, markup);
            return true;
        },

        doUpdate : function(ds, record){
            this.refreshSummaryById(record._groupId);
        },

        doRemove : function(ds, record, index, isUpdate){
            if(!isUpdate){
                this.refreshSummaryById(record._groupId);
            }
        },

        /**
         * Show a message in the summary row.
         *
    grid.on('afteredit', function(){
        var groupValue = 'Ext Forms: Field Anchoring';
        summary.showSummaryMsg(groupValue, 'Updating Summary...');
    });
         *
         * @param {String} groupValue
         * @param {String} msg Text to use as innerHTML for the summary row.
         */
        showSummaryMsg : function(groupValue, msg){
            var gid = this.view.getGroupId(groupValue),
                 node = this.getSummaryNode(gid);
            if(node){
                node.innerHTML = '' + msg + '';
            }
        }
    });

    //backwards compat
    Ext.grid.GroupSummary = Ext.ux.grid.GroupSummary;

    /**
     * Calculation types for summary row:
     * sum :
     * count :
     * max :
     * min :
     * average :
     *
     * Custom calculations may be implemented.  An example of
     * custom summaryType=totalCost:
    // define a custom summary function
    Ext.ux.grid.GroupSummary.Calculations['totalCost'] = function(v, record, field){
        return v + (record.data.estimate * record.data.rate);
    };
     *
     * @property Calculations
     */

    Ext.ux.grid.GroupSummary.Calculations = {
        'sum' : function(v, record, field){
            return v + (record.data[field]||0);
        },

        'count' : function(v, record, field, data){
            return data[field+'count'] ? ++data[field+'count'] : (data[field+'count'] = 1);
        },

        'max' : function(v, record, field, data){
            var v = record.data[field];
            var max = data[field+'max'] === undefined ? (data[field+'max'] = v) : data[field+'max'];
            return v > max ? (data[field+'max'] = v) : max;
        },

        'min' : function(v, record, field, data){
            var v = record.data[field];
            var min = data[field+'min'] === undefined ? (data[field+'min'] = v) : data[field+'min'];
            return v < min ? (data[field+'min'] = v) : min;
        },

        'average' : function(v, record, field, data){
            var c = data[field+'count'] ? ++data[field+'count'] : (data[field+'count'] = 1);
            var t = (data[field+'total'] = ((data[field+'total']||0) + (record.data[field]||0)));
            return t === 0 ? 0 : t / c;
        }
    };
    Ext.grid.GroupSummary.Calculations = Ext.ux.grid.GroupSummary.Calculations;

    /**
     * @class Ext.ux.grid.HybridSummary
     * @extends Ext.ux.grid.GroupSummary
     * Adds capability to specify the summary data for the group via json as illustrated here:
     *
    {
        data: [
            {
                projectId: 100,     project: 'House',
                taskId:    112, description: 'Paint',
                estimate:    6,        rate:     150,
                due:'06/24/2007'
            },
            ...
        ],

        summaryData: {
            'House': {
                description: 14, estimate: 9,
                       rate: 99, due: new Date(2009, 6, 29),
                       cost: 999
            }
        }
    }
     *
     *
     */
    Ext.ux.grid.HybridSummary = Ext.extend(Ext.ux.grid.GroupSummary, {
        /**
         * @private
         * @param {Object} rs
         * @param {Object} cs
         */
        calculate : function(rs, cs){
            var gcol = this.view.getGroupField(),
                gvalue = rs[0].data[gcol],
                gdata = this.getSummaryData(gvalue);
            return gdata || Ext.ux.grid.HybridSummary.superclass.calculate.call(this, rs, cs);
        },

        /**
         *
    grid.on('afteredit', function(){
        var groupValue = 'Ext Forms: Field Anchoring';
        summary.showSummaryMsg(groupValue, 'Updating Summary...');
        setTimeout(function(){ // simulate server call
            // HybridSummary class implements updateSummaryData
            summary.updateSummaryData(groupValue,
                // create data object based on configured dataIndex
                {description: 22, estimate: 888, rate: 888, due: new Date(), cost: 8});
        }, 2000);
    });
         *
         * @param {String} groupValue
         * @param {Object} data data object
         * @param {Boolean} skipRefresh (Optional) Defaults to false
         */
        updateSummaryData : function(groupValue, data, skipRefresh){
            var json = this.grid.getStore().reader.jsonData;
            if(!json.summaryData){
                json.summaryData = {};
            }
            json.summaryData[groupValue] = data;
            if(!skipRefresh){
                this.refreshSummary(groupValue);
            }
        },

        /**
         * Returns the summaryData for the specified groupValue or null.
         * @param {String} groupValue
         * @return {Object} summaryData
         */
        getSummaryData : function(groupValue){
            var reader = this.grid.getStore().reader,
                json = reader.jsonData,
                fields = reader.recordType.prototype.fields,
                v;

            if(json && json.summaryData){
                v = json.summaryData[groupValue];
                if(v){
                    return reader.extractValues(v, fields.items, fields.length);
                }
            }
            return null;
        }
    });

    //backwards compat
    Ext.grid.HybridSummary = Ext.ux.grid.HybridSummary;