Skip to content

Commit 7b70f7c

Browse files
committed
Enh(grouping): BREAKING change aggregation options and improve custom aggregation support
Add gridOption treeAggregationFns, to add custom aggregations with support for both the column menu and saveState. Refactor native aggregations to follow the same api as custom aggregations. Remove columnDef option treeAggregation and replace with separate options treeAggregationType and treeAggregationLabel Remove "custom" aggregation type, as it is redundent with specifying a custom aggregation function. Allow custom aggregation finalizers to omit setting the rendered string. Render the value (and label if provided) if a rendered string is not provided. Fix groupingShowCounts option Breaking changes are: - options previously specified on the treeAggregation object are now separate options (see docs) - aggregation entity type property may be undefined in the case of a unnamed custom aggregation function - aggregation entity previously set the initial value to 0, it now begins as undefined Closes #3302
1 parent d68dcc6 commit 7b70f7c

File tree

9 files changed

+467
-256
lines changed

9 files changed

+467
-256
lines changed

misc/tutorial/209_grouping.ngdoc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ could potentially interact with people's custom templates.
8181

8282
Tuning the way aggregations work can be done through defining a columnsProcessor that runs with higher (later)
8383
priority than the groupingColumnProcessor (so higher than 400), and that looks for grouped or aggregated columns
84-
and changes things like the treeAggregation.type, or the customTreeAggregationFinalizerFn. A 'complex' tutorial
85-
may be forthcoming that provides examples of this.
84+
and changes things like the treeAggregationFn, or the customTreeAggregationFinalizerFn. See tutorial 320 for an example.
8685

8786

8887
@example
@@ -105,10 +104,10 @@ function will stop working), and writes them to the console.
105104
columnDefs: [
106105
{ name: 'name', width: '30%' },
107106
{ name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, width: '20%', cellFilter: 'mapGender' },
108-
{ name: 'age', treeAggregation: { type: uiGridGroupingConstants.aggregation.MAX }, width: '20%' },
107+
{ name: 'age', treeAggregationType: uiGridGroupingConstants.aggregation.MAX, width: '20%' },
109108
{ name: 'company', width: '25%' },
110109
{ name: 'state', grouping: { groupPriority: 0 }, sort: { priority: 0, direction: 'desc' }, width: '35%', cellTemplate: '<div><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents" title="TOOLTIP">{{COL_FIELD CUSTOM_FILTERS}}</div></div>' },
111-
{ name: 'balance', width: '25%', cellFilter: 'currency', treeAggregation: { type: uiGridGroupingConstants.aggregation.AVG }, customTreeAggregationFinalizerFn: function( aggregation ) {
110+
{ name: 'balance', width: '25%', cellFilter: 'currency', treeAggregationType: uiGridGroupingConstants.aggregation.AVG, customTreeAggregationFinalizerFn: function( aggregation ) {
112111
aggregation.rendered = aggregation.value;
113112
} }
114113
],

misc/tutorial/319_complex_trees.ngdoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ you need to be careful how you use it.
3939

4040
Aggregations are set on the columnDef or column in the format:
4141
```
42-
colDef.treeAggregation: { type: uiGridTreeViewConstants.SUM }
42+
colDef.treeAggregationType = uiGridTreeViewConstants.SUM;
4343
```
4444

4545
You can provide a custom aggregation function, but note that the aggregation is performed as a running total, so
46-
your function needs to work within that framework. Refer the documentation for `customTreeAggregationFn` under
46+
your function needs to work within that framework. Refer the documentation for `treeAggregationFn` under
4747
{@link api/ui.grid.treeBase.api:ColumnDef columnDef}.
4848

4949
You can provide a custom finalizer function, which can be used to format or otherwise complete your aggregation
@@ -68,7 +68,7 @@ that shows both the value for that row as well as the aggregated value.
6868
{ name: 'age', width: '20%' },
6969
{ name: 'company', width: '25%' },
7070
{ name: 'state', width: '35%', field: 'address.state' },
71-
{ name: 'balance', width: '25%', treeAggregation: { type: uiGridTreeViewConstants.aggregation.SUM }, cellFilter: 'currency', cellTemplate: '<div class="ui-grid-cell-contents" title="TOOLTIP"><span>{{row.entity.balance CUSTOM_FILTERS}}</span><span ng-if="row.entity[\'$$\' + col.uid]"> ({{row.entity["$$" + col.uid].value CUSTOM_FILTERS}})</span></div>' }
71+
{ name: 'balance', width: '25%', treeAggregationType: uiGridTreeViewConstants.aggregation.SUM, cellFilter: 'currency', cellTemplate: '<div class="ui-grid-cell-contents" title="TOOLTIP"><span>{{row.entity.balance CUSTOM_FILTERS}}</span><span ng-if="row.entity[\'$$\' + col.uid]"> ({{row.entity["$$" + col.uid].value CUSTOM_FILTERS}})</span></div>' }
7272
],
7373
onRegisterApi: function( gridApi ) {
7474
$scope.gridApi = gridApi;

misc/tutorial/320_complex_grouping.ngdoc

Lines changed: 155 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ children of the selected rowHeader.
1313
@example
1414
<example module="app">
1515
<file name="app.js">
16-
var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping', 'ui.grid.edit', 'ui.grid.selection' ]);
16+
var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping', 'ui.grid.edit', 'ui.grid.selection']);
1717

18-
app.controller('MainCtrl', ['$scope', '$http', '$interval', 'uiGridGroupingConstants', '$filter', function ($scope, $http, $interval, uiGridGroupingConstants, $filter ) {
18+
app.controller('MainCtrl', ['$scope', '$http', '$interval', 'uiGridGroupingConstants', '$filter', 'stats', function ($scope, $http, $interval, uiGridGroupingConstants, $filter, stats ) {
1919
var setGroupValues = function( columns, rows ) {
2020
columns.forEach( function( column ) {
2121
if ( column.grouping && column.grouping.groupPriority > -1 ){
22-
column.treeAggregation.type = uiGridGroupingConstants.aggregation.CUSTOM;
23-
column.customTreeAggregationFn = function( aggregation, fieldValue, numValue, row ) {
22+
// Put the balance next to all group labels.
23+
column.treeAggregationFn = function( aggregation, fieldValue, numValue, row ) {
2424
if ( typeof(aggregation.value) === 'undefined') {
2525
aggregation.value = 0;
2626
}
@@ -33,8 +33,6 @@ children of the selected rowHeader.
3333
aggregation.rendered = null;
3434
}
3535
};
36-
} else {
37-
delete column.customTreeAggregationFn;
3836
}
3937
});
4038
return columns;
@@ -44,20 +42,42 @@ children of the selected rowHeader.
4442
enableFiltering: true,
4543
enableGroupHeaderSelection: true,
4644
treeRowHeaderAlwaysVisible: false,
45+
treeCustomAggregations: {
46+
variance: {label: 'var: ', menuTitle: 'Agg: Var', aggregationFn: stats.aggregator.sumSquareErr, finalizerFn: stats.finalizer.variance },
47+
stdev: {label: 'stDev: ', aggregationFn: stats.aggregator.sumSquareErr, finalizerFn: stats.finalizer.stDev},
48+
mode: {label: 'mode: ', aggregationFn: stats.aggregator.mode, finalizerFn: stats.finalizer.mode },
49+
median: {label: 'median: ', aggregationFn: stats.aggregator.accumulate.numValue, finalizerFn: stats.finalizer.median }
50+
},
4751
columnDefs: [
48-
{ name: 'name', width: '30%' },
49-
{ name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, editableCellTemplate: 'ui-grid/dropdownEditor', width: '20%',
52+
{ name: 'name', width: '15%' },
53+
{ name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, editableCellTemplate: 'ui-grid/dropdownEditor', width: '10%',
5054
cellFilter: 'mapGender', editDropdownValueLabel: 'gender', editDropdownOptionsArray: [
5155
{ id: 1, gender: 'male' },
5256
{ id: 2, gender: 'female' }
5357
]
5458
},
55-
{ name: 'age', treeAggregation: { type: uiGridGroupingConstants.aggregation.MAX }, width: '20%' },
56-
{ name: 'company', width: '25%' },
57-
{ name: 'state', grouping: { groupPriority: 0 }, sort: { priority: 0, direction: 'desc' }, width: '35%', cellTemplate: '<div><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents" title="TOOLTIP">{{COL_FIELD CUSTOM_FILTERS}}</div></div>' },
58-
{ name: 'balance', width: '25%', cellFilter: 'currency', treeAggregation: { type: uiGridGroupingConstants.aggregation.AVG }, customTreeAggregationFinalizerFn: function( aggregation ) {
59-
aggregation.rendered = aggregation.value;
60-
} }
59+
{ field: 'age', treeAggregationType: uiGridGroupingConstants.aggregation.MAX, width: '10%' },
60+
{ field: 'age', displayName: 'Age (common)', customTreeAggregationFn: stats.aggregator.mode, width: '10%' },
61+
{ name: 'company', width: '15%' },
62+
{ name: 'state', grouping: { groupPriority: 0 }, sort: { priority: 0, direction: 'desc' }, width: '25%', cellTemplate: '<div><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents" title="TOOLTIP">{{COL_FIELD CUSTOM_FILTERS}}</div></div>' },
63+
{ field: 'balance', displayName: 'balance (avg)', width: '15%', cellFilter: 'currency',
64+
treeAggregationType: uiGridGroupingConstants.aggregation.AVG,
65+
customTreeAggregationFinalizerFn: function( aggregation ) {
66+
aggregation.rendered = aggregation.value;
67+
}
68+
},
69+
{ field: 'balance', displayName: 'balance (total)', width: '15%', cellFilter: 'currency',
70+
treeAggregationType: uiGridGroupingConstants.aggregation.SUM,
71+
customTreeAggregationFinalizerFn: function( aggregation ) {
72+
aggregation.rendered = aggregation.value;
73+
}
74+
},
75+
{ field: 'balance', displayName: 'balance (median)', width: '15%', cellFilter: 'currency',
76+
treeAggregationType: 'median',
77+
},
78+
{ field: 'balance', displayName: 'balance (std dev)', width: '15%', cellFilter: 'currency',
79+
treeAggregationType: 'stdev',
80+
}
6181
],
6282
onRegisterApi: function( gridApi ) {
6383
$scope.gridApi = gridApi;
@@ -109,12 +129,132 @@ children of the selected rowHeader.
109129
return input;
110130
}
111131
};
132+
})
133+
.service('stats', function(){
134+
135+
var coreAccumulate = function(aggregation, value){
136+
initAggregation(aggregation);
137+
if ( angular.isUndefined(aggregation.stats.accumulator) ){
138+
aggregation.stats.accumulator = [];
139+
}
140+
if ( !isNaN(value) ){
141+
aggregation.stats.accumulator.push(value);
142+
}
143+
};
144+
145+
var initAggregation = function(aggregation){
146+
/* To be used in conjunction with the cleanup finalizer */
147+
if (angular.isUndefined(aggregation.stats) ){
148+
aggregation.stats = {sum: 0};
149+
}
150+
};
151+
152+
var increment = function(obj, prop){
153+
/* if the property on obj is undefined, sets to 1, otherwise increments by one */
154+
if ( angular.isUndefined(obj[prop])){
155+
obj[prop] = 1;
156+
}
157+
else {
158+
obj[prop]++;
159+
}
160+
};
161+
162+
var service = {
163+
aggregator: {
164+
accumulate: {
165+
/* This is to be used with the uiGrid customTreeAggregationFn definition,
166+
* to accumulate all of the data into an array for sorting or other operations by customTreeAggregationFinalizerFn
167+
* In general this strategy is not the most efficient way to generate grouped statistics, but
168+
* sometime is the only way.
169+
*/
170+
numValue: function (aggregation, fieldValue, numValue) {
171+
return coreAccumulate(aggregation, numValue);
172+
},
173+
fieldValue: function (aggregation, fieldValue) {
174+
return coreAccumulate(aggregation, fieldValue);
175+
}
176+
},
177+
mode: function(aggregation, fieldValue){
178+
initAggregation(aggregation);
179+
var thisValue = fieldValue;
180+
if (angular.isUndefined(thisValue) || thisValue === null){
181+
thisValue = aggregation.col.grid.options.groupingNullLabel;
182+
}
183+
increment(aggregation.stats, thisValue);
184+
if ( aggregation.stats[thisValue] > aggregation.maxCount || angular.isUndefined(aggregation.maxCount) ){
185+
aggregation.maxCount = aggregation.stats[thisValue];
186+
aggregation.value = thisValue;
187+
}
188+
},
189+
sumSquareErr: function(aggregation, fieldValue, numValue){
190+
initAggregation(aggregation);
191+
if ( !isNaN(numValue) ){
192+
increment(aggregation.stats, 'count');
193+
}
194+
aggregation.stats.sum += numValue || 0;
195+
service.aggregator.accumulate.numValue(aggregation, fieldValue, numValue);
196+
}
197+
},
198+
finalizer: {
199+
cleanup: function (aggregation) {
200+
delete aggregation.stats;
201+
if ( angular.isUndefined(aggregation.rendered) ){
202+
aggregation.rendered = aggregation.value;
203+
}
204+
},
205+
median: function(aggregation){
206+
aggregation.stats.accumulator.sort();
207+
var arrLength = aggregation.stats.accumulator.length;
208+
aggregation.value = arrLength % 2 === 0 ?
209+
(aggregation.stats.accumulator[(arrLength / 2) - 1] + aggregation.stats.accumulator[(arrLength / 2)]) / 2
210+
: aggregation.stats.accumulator[(arrLength / 2) | 0];
211+
service.finalizer.cleanup(aggregation);
212+
},
213+
sumSquareErr: function(aggregation){
214+
aggregation.value = 0;
215+
if ( aggregation.count !== 0 ){
216+
var mean = aggregation.stats.sum/aggregation.stats.count,
217+
error;
218+
219+
angular.forEach(aggregation.stats.accumulator, function(value){
220+
error = value - mean;
221+
aggregation.value += error * error;
222+
});
223+
}
224+
},
225+
variance: function(aggregation){
226+
service.finalizer.sumSquareErr(aggregation);
227+
aggregation.value = aggregation.value / aggregation.stats.count;
228+
service.finalizer.cleanup(aggregation);
229+
aggregation.rendered = Math.round(aggregation.value * 100)/100;
230+
},
231+
varianceP: function(aggregation){
232+
service.finalizer.sumSquareErr(aggregation);
233+
if ( aggregation.count !== 0 ) {
234+
aggregation.value = aggregation.value / (aggregation.stats.count - 1);
235+
}
236+
service.finalizer.cleanup(aggregation);
237+
},
238+
stDev: function(aggregation){
239+
service.finalizer.variance(aggregation);
240+
aggregation.value = Math.sqrt(aggregation.value);
241+
aggregation.rendered = Math.round(aggregation.value * 100)/100;
242+
},
243+
stDevP: function(aggregation){
244+
service.finalizer.varianceP(aggregation);
245+
aggregation.value = Math.sqrt(aggregation.value);
246+
aggregation.rendered = Math.round(aggregation.value * 100)/100;
247+
}
248+
},
249+
};
250+
251+
return service;
112252
});
113253
</file>
114254

115255
<file name="index.html">
116256
<div ng-controller="MainCtrl">
117-
<div id="grid1" ui-grid="gridOptions" ui-grid-grouping ui-grid-edit ui-grid-selection class="grid"></div>
257+
<div id="grid1" ui-grid="gridOptions" ui-grid-grouping ui-grid-edit ui-grid-selection class="grid" style="width:100%;"></div>
118258
</div>
119259
</file>
120260

misc/tutorial/401_AllFeatures.ngdoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ All features are enabled to get an idea of performance
1313
<file name="app.js">
1414
var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.edit', 'ui.grid.resizeColumns', 'ui.grid.pinning', 'ui.grid.selection', 'ui.grid.moveColumns', 'ui.grid.exporter', 'ui.grid.importer', 'ui.grid.grouping']);
1515

16-
app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', 'uiGridConstants',
17-
function ($scope, $http, $timeout, $interval, uiGridConstants) {
16+
app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', 'uiGridConstants', 'uiGridGroupingConstants',
17+
function ($scope, $http, $timeout, $interval, uiGridConstants, uiGridGroupingConstants) {
1818

1919
$scope.gridOptions = {};
2020
$scope.gridOptions.data = 'myData';
@@ -35,7 +35,7 @@ All features are enabled to get an idea of performance
3535
$scope.gridOptions.columnDefs = [
3636
{ name:'id', width:50 },
3737
{ name:'name', width:100 },
38-
{ name:'age', width:100, enableCellEdit: true, aggregationType:uiGridConstants.aggregationTypes.avg },
38+
{ name:'age', width:100, enableCellEdit: true, aggregationType:uiGridConstants.aggregationTypes.avg, treeAggregationType: uiGridGroupingConstants.aggregation.AVG },
3939
{ name:'address.street', width:150, enableCellEdit: true },
4040
{ name:'address.city', width:150, enableCellEdit: true },
4141
{ name:'address.state', width:50, enableCellEdit: true },

0 commit comments

Comments
 (0)