People using ExtJs/Ext.Net/Coolite might be aware of the GroupingSummary plugin, for the GridPanel that allows you to group GridPanel rows on a particular field, and then display columns summaries for each set of grouped rows independently (see here and here for ExtJs & Ext.Net/Coolite examples respectively).

A handy plugin I must agree, but in my case, I wanted to display summaries for columns of GridPanels without any grouping. As I googled out and asked for help on Coolite forums regarding this, I came across this and this thread on ExtJs forums, as well as this thread on Coolite forums. On an initial observation, I thought of adopting either the GridTotals plugin posted by the wonderful ExtJs community member, Animal here, or the code posted by Coolite core member, vlad here.

However, on closer inspection, I thought vlad’s solution was not optimal because it added an extra record to the GridPanel’s store for the summary row, which might cause problems when you are processing the Store’s data in your code, unless you explicitly take care and ignore the summary row from your processing.

So, I decided to go with Animal’s GridTotals plugin. But when I used it in my code, I had a series of javascript errors. Confused, when I dug into the code, I noticed that Ext.Net GridPanel’s CommandColumns were causing issues. So, I had to adapt Animal’s code for use in Ext.Net GridPanel. Here’s the adpated code, which works under both ExtJs as well as Ext.Net/Coolite perfectly:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.ux.GridTotals = Ext.extend(Ext.util.Observable, {
constructor: function(config) {
config = config || {};
this.showHeaderInTotals = config.showHeaderInTotals;
this.divideRowHeightBy2 = config.divideRowHeightBy2;

Ext.ux.GridTotals.superclass.constructor.call(this, config);
},

init: function(g) {
var v = g.getView();
this.grid = g;
this.store = g.getStore();

//Need to override GridView’s findRow to not consider total’s row as normal grid row.
v.findRow = function(el) {
if (!el) {
return false;
}

if (this.fly(el).findParent(‘.x-grid-total-row’, this.rowSelectorDepth)) {
return (false);
} else {
return this.fly(el).findParent(this.rowSelector, this.rowSelectorDepth);
}
}

g.cls = (g.cls || ”) + ‘x-grid3-simple-totals’;
g.gridTotals = this;

this.store.on({
reconfigure: { fn: this.onGridReconfigure, scope: this },
add: { fn: this.updateTotals, scope: this },
remove: { fn: this.updateTotals, scope: this },
update: { fn: this.updateTotals, scope: this },
datachanged: { fn: this.updateTotals, scope: this }
});

v.onLayout = v.onLayout.createSequence(this.onLayout, this);
v.initElements = v.initElements.createSequence(this.initElements, this);
v.onAllColumnWidthsUpdated = v.onAllColumnWidthsUpdated.createSequence(this.onLayout, this);
v.onColumnWidthUpdated = v.onColumnWidthUpdated.createSequence(this.onLayout, this);
},

initElements: function() {
var v = this.grid.getView();
v.scroller.on(‘scroll’, function() {
v.totalsRow.setStyle({
left: -v.scroller.dom.scrollLeft + ‘px’
});
});
},

onLayout: function() {
this.updateTotals();
this.fixScrollerPosition();
},

fixScrollerPosition: function() {
var v = this.grid.getView();
var bottomScrollbarWidth = v.scroller.getHeight() – v.scroller.dom.clientHeight;
v.totalsRow.setStyle({
bottom: bottomScrollbarWidth + ‘px’,
width: Math.min(v.mainBody.getWidth(), v.scroller.dom.clientWidth) + ‘px’
});

//Reduce the height of the scroller to create spce for totals row to avoid overlapping.
var height = (this.divideRowHeightBy2 !== false) ? v.totalsRow.dom.clientHeight / 2 : v.totalsRow.dom.clientHeight;
v.scroller.setHeight(v.scroller.dom.clientHeight – height);
},

getTotals: function() {
var v = this.grid.getView();

var cs = v.getColumnData();
var totals = new Array(cs.length);
var store = v.grid.store;
var fields = store.recordType.prototype.fields;
var columns = v.cm.config;

for (var i = 0, l = v.grid.store.getCount(); i < l; i++) {
var rec = store.getAt(i);
for (var c = 0, nc = cs.length; c < nc; c++) {
var f = cs.name;
var t = !Ext.isEmpty(f) ? fields.get(f).type : ”;
if (columns.totalsText) {
totals = columns.totalsText;
//} else if (t.type == ‘int’ || t.type == ‘float’) {
} else if (columns.summaryType) {
var v = rec.get(f);
if (Ext.isDefined(totals)) {
switch (columns.summaryType) {
case ‘sum’:
totals += v;
break;
case ‘min’:
if (v < totals) {
totals = v;
}
break;
case ‘max’:
if (v > totals) {
totals = v;
}
break;
}
} else {
switch (columns.summaryType) {
case ‘count’:
totals = l;
break;

default:
totals = v;
break;
}
}
}
}
}

return (totals);
},

getRenderedTotals: function() {
var v = this.grid.getView();
var totals = this.getTotals();

var cs = v.getColumnData();
var store = v.grid.store;
var columns = v.cm.config;

var cells = ”, p = {};
for (var c = 0, nc = cs.length, last = nc – 1; c < nc; c++) {
if (columns.roundToPlaces) {
totals = Math.roundToPlaces(totals, columns.roundToPlaces);
}

if (this.showHeaderInTotals) {
if (Ext.isEmpty(totals)) {
totals = ‘&nbsp;’;
} else {
totals += ‘: ‘ + cs.scope.header;
}
}

var v = Ext.isDefined(totals) ? totals : ”;

if (columns.summaryType && columns.summaryRenderer) {
var renderer = columns.summaryRenderer;
if (Ext.isString(renderer)) {
renderer = Ext.util.Format[renderer];
}
totals = renderer(v, p, undefined, undefined, c, store);
}
}

return (totals);
},

updateTotals: function() {
if (!this.grid.rendered) {
return;
}

var v = this.grid.getView();

if (!v.totalsRow) {
v.mainWrap.setStyle(‘position’, ‘relative’);
v.totalsRow = v.templates.row.append(v.mainWrap, {
tstyle: ‘width:’ + v.mainBody.getWidth(),
cells: ”
}, true);
v.totalsRow.addClass(‘x-grid-total-row’);
v.totalsTr = v.totalsRow.child(‘tr’).dom;
}

var totals = this.getRenderedTotals();

var cs = v.getColumnData();

var cells = ”, p = {};
for (var c = 0, nc = cs.length, last = nc – 1; c < nc; c++) {
p.id = cs.id;
p.style = cs.style;
p.css = c == 0 ? ‘x-grid3-cell-first ‘ : (c == last ? ‘x-grid3-cell-last ‘ : ”);

cells += v.templates.cell.apply(Ext.apply({
value: totals
}, cs));
}
while (v.totalsTr.hasChildNodes()) {
v.totalsTr.removeChild(v.totalsTr.lastChild);
}
Ext.DomHelper.insertHtml(‘afterBegin’, v.totalsTr, cells);
},

onGridReconfigure: Ext.emptyFn
});
{/syntaxhighlighter}

I have made a couple of fixes and enhancements to the Animal’s plugin.

  1. As said earlier, it was adapted for Ext.Net GridPanel, by taking the CommandColumns into consideration.
  2. In my case, I needed to maintain dynamic running totals for the columns. Thus I needed to update the column summaries when data in the GridPanel’s store changed. So, I have registered additional listeners for the GridPanel’s store to upate column summaries as you can see here:

    {syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }this.store.on({
    reconfigure: { fn: this.onGridReconfigure, scope: this },
    add: { fn: this.updateTotals, scope: v },
    remove: { fn: this.updateTotals, scope: v },
    update: { fn: this.updateTotals, scope: v },
    datachanged: { fn: this.updateTotals, scope: v }
    });{/syntaxhighlighter} 

  3. For integer and float columns, I needed to display column summaries as sum of values for the rows. However, for other columns (String etc.), I needed to display static text as the summary. So, I enhanced the Plugin to support this. Now, you can specify any text as totalsText custom config option for any GridPanel column like below:
    <ext:Column DataIndex="fullName" Header="Passenger">
    	<CustomConfig>
    		<ext:ConfigItem Name="totalsText" Value="Total" Mode="Value" />
    	</CustomConfig>
    </ext:Column>

    If this config option is specified for a column, the corresponding value for the totalsText column config option would be used in the summary row.

  4. A very important point to remember about this plugin is that it displays summaries for only int or float columns, and it is important to specify the data type for the Store field explicitly for which you want column summaries as the sum of the column values. See the example below:
    <ext:RecordField Name="tax1" Type="Float" DefaultValue="0" />
    <ext:RecordField Name="tax2" Type="Float" DefaultValue="0" />

This plugin natively supports only sum of column values for summaries (or the static totalsText option as specified above). But it should not be too difficult adapting it to support average or any other other column summary calculation. If anyone needs help doing so, please let me know.

And the last important point, do not forget to also include the following css in the page somewhere:

{syntaxhighlighter brush: css;fontsize: 100; first-line: 1; }.x-grid3-simple-totals .x-grid3-row-last {
margin-bottom: 21px;
}

.x-grid3-simple-totals .x-grid-total-row {
position: absolute;
left: 0;
bottom: 15px;
background: #F9F9F9 url(../../resources/images/default/grid/grid3-hrow.gif);
}

.x-grid3-simple-totals .x-grid-total-row td {
border-left: 1px solid #EEEEEE;
border-right: 1px solid #D0D0D0;
padding-left: 0px;
padding-right: 0px;
}{/syntaxhighlighter}

My last couple of blog entires (including this one) have been adaptations of other’s code, rather than code written from the scratch myself. I again fully acknowledge Animal’s code that I have adapted for my needs.

Here’s a demonstration of this plugin:

 

UPDATE:

  • Sep 16, 2010 – Reversed the order of checking of field data types and presence of totalsText for a column to give more preference to totalsText if specified.
  • May 27, 2011 – Updated code to support better rendering of column totals.
    Added live sample for the demo.

The complete code including the html file for the above sample and js and css resource files for the plugin are attached below.