1 // -*- js-indent-level:2 -*-
2 // originally cloned from slick.dataview.js
3 // actually I ended up just adding a method here (getIdxByIdKey)
4 // so there probably are much smarter ways to achieve this
6 $.extend(true, window, {
9 UnfoldDataView: UnfoldDataView,
22 * A sample Model implementation.
23 * Provides a filtered view of the underlying data.
25 * Relies on the data item having an "id" property uniquely identifying it.
27 function UnfoldDataView(options) {
31 groupItemMetadataProvider: null,
37 var idProperty = "id"; // property holding a unique row id
38 var items = []; // data by index
39 var rows = []; // data by row
40 var idxById = {}; // indexes by id
41 var rowsById = null; // rows by id; lazy-calculated
42 var filter = null; // filter function
43 var updated = null; // updated item ids
44 var suspend = false; // suspends the recalculation
48 var refreshHints = {};
49 var prevRefreshHints = {};
51 var filteredItems = [];
53 var compiledFilterWithCaching;
57 var groupingInfoDefaults = {
60 comparer: function(a, b) { return a.value - b.value; },
63 aggregateEmpty: false,
64 aggregateCollapsed: false,
65 aggregateChildGroups: false,
67 displayTotalsRow: true
69 var groupingInfos = [];
71 var toggledGroupsByLevel = [];
72 var groupingDelimiter = ':|:';
79 var onRowCountChanged = new Slick.Event();
80 var onRowsChanged = new Slick.Event();
81 var onPagingInfoChanged = new Slick.Event();
83 options = $.extend(true, {}, defaults, options);
86 function beginUpdate() {
90 function endUpdate() {
95 function setRefreshHints(hints) {
99 function setFilterArgs(args) {
103 // the unfold addition is for spotting an index based on another key
104 // this is kind of slow for now but minimixes the changes to this mainstream code
105 function getIdxByIdKey(matching_id,matching_key) {
107 for (var i = 0, l = items.length; i < l; i++) {
108 id = items[i][matching_key];
109 if (id === undefined) { messages.warning ("ignoring record with no " + matching_key); continue; }
110 if (id == matching_id) return i;
115 function updateIdxById(startingIndex) {
116 startingIndex = startingIndex || 0;
118 for (var i = startingIndex, l = items.length; i < l; i++) {
119 id = items[i][idProperty];
120 if (id === undefined) {
121 throw "Each data element must implement a unique 'id' property";
127 function ensureIdUniqueness() {
129 for (var i = 0, l = items.length; i < l; i++) {
130 id = items[i][idProperty];
131 if (id === undefined || idxById[id] !== i) {
132 throw "Each data element must implement a unique 'id' property";
137 function getItems() {
141 function setItems(data, objectIdProperty) {
142 if (objectIdProperty !== undefined) {
143 idProperty = objectIdProperty;
145 items = filteredItems = data;
148 ensureIdUniqueness();
152 function setPagingOptions(args) {
153 if (args.pageSize != undefined) {
154 pagesize = args.pageSize;
155 pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0;
158 if (args.pageNum != undefined) {
159 pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
162 onPagingInfoChanged.notify(getPagingInfo(), null, self);
167 function getPagingInfo() {
168 var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1;
169 return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages};
172 function sort(comparer, ascending) {
174 sortComparer = comparer;
175 fastSortField = null;
176 if (ascending === false) {
179 items.sort(comparer);
180 if (ascending === false) {
189 * Provides a workaround for the extremely slow sorting in IE.
190 * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
191 * to return the value of that field and then doing a native Array.sort().
193 function fastSort(field, ascending) {
195 fastSortField = field;
197 var oldToString = Object.prototype.toString;
198 Object.prototype.toString = (typeof field == "function") ? field : function () {
201 // an extra reversal for descending sort keeps the sort stable
202 // (assuming a stable native sort implementation, which isn't true in some cases)
203 if (ascending === false) {
207 Object.prototype.toString = oldToString;
208 if (ascending === false) {
218 sort(sortComparer, sortAsc);
219 } else if (fastSortField) {
220 fastSort(fastSortField, sortAsc);
224 function setFilter(filterFn) {
226 if (options.inlineFilters) {
227 compiledFilter = compileFilter();
228 compiledFilterWithCaching = compileFilterWithCaching();
233 function getGrouping() {
234 return groupingInfos;
237 function setGrouping(groupingInfo) {
238 if (!options.groupItemMetadataProvider) {
239 options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
243 toggledGroupsByLevel = [];
244 groupingInfo = groupingInfo || [];
245 groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
247 for (var i = 0; i < groupingInfos.length; i++) {
248 var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]);
249 gi.getterIsAFn = typeof gi.getter === "function";
251 // pre-compile accumulator loops
252 gi.compiledAccumulators = [];
253 var idx = gi.aggregators.length;
255 gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
258 toggledGroupsByLevel[i] = {};
265 * @deprecated Please use {@link setGrouping}.
267 function groupBy(valueGetter, valueFormatter, sortComparer) {
268 if (valueGetter == null) {
275 formatter: valueFormatter,
276 comparer: sortComparer
281 * @deprecated Please use {@link setGrouping}.
283 function setAggregators(groupAggregators, includeCollapsed) {
284 if (!groupingInfos.length) {
285 throw new Error("At least one grouping must be specified before calling setAggregators().");
288 groupingInfos[0].aggregators = groupAggregators;
289 groupingInfos[0].aggregateCollapsed = includeCollapsed;
291 setGrouping(groupingInfos);
294 function getItemByIdx(i) {
298 function getIdxById(id) {
302 function ensureRowsByIdCache() {
305 for (var i = 0, l = rows.length; i < l; i++) {
306 rowsById[rows[i][idProperty]] = i;
311 function getRowById(id) {
312 ensureRowsByIdCache();
316 function getItemById(id) {
317 return items[idxById[id]];
320 function mapIdsToRows(idArray) {
322 ensureRowsByIdCache();
323 for (var i = 0; i < idArray.length; i++) {
324 var row = rowsById[idArray[i]];
326 rows[rows.length] = row;
332 function mapRowsToIds(rowArray) {
334 for (var i = 0; i < rowArray.length; i++) {
335 if (rowArray[i] < rows.length) {
336 ids[ids.length] = rows[rowArray[i]][idProperty];
342 function updateItem(id, item) {
343 if (idxById[id] === undefined || id !== item[idProperty]) {
344 throw "Invalid or non-matching id";
346 items[idxById[id]] = item;
354 function insertItem(insertBefore, item) {
355 items.splice(insertBefore, 0, item);
356 updateIdxById(insertBefore);
360 function addItem(item) {
362 updateIdxById(items.length - 1);
366 function deleteItem(id) {
367 var idx = idxById[id];
368 if (idx === undefined) {
372 items.splice(idx, 1);
377 function getLength() {
381 function getItem(i) {
385 function getItemMetadata(i) {
387 if (item === undefined) {
391 // overrides for grouping rows
393 return options.groupItemMetadataProvider.getGroupRowMetadata(item);
396 // overrides for totals rows
397 if (item.__groupTotals) {
398 return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
404 function expandCollapseAllGroups(level, collapse) {
406 for (var i = 0; i < groupingInfos.length; i++) {
407 toggledGroupsByLevel[i] = {};
408 groupingInfos[i].collapsed = collapse;
411 toggledGroupsByLevel[level] = {};
412 groupingInfos[level].collapsed = collapse;
418 * @param level {Number} Optional level to collapse. If not specified, applies to all levels.
420 function collapseAllGroups(level) {
421 expandCollapseAllGroups(level, true);
425 * @param level {Number} Optional level to expand. If not specified, applies to all levels.
427 function expandAllGroups(level) {
428 expandCollapseAllGroups(level, false);
431 function expandCollapseGroup(level, groupingKey, collapse) {
432 toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
437 * @param varArgs Either a Slick.Group's "groupingKey" property, or a
438 * variable argument list of grouping values denoting a unique path to the row. For
439 * example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of
440 * the 'high' setGrouping.
442 function collapseGroup(varArgs) {
443 var args = Array.prototype.slice.call(arguments);
445 if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
446 expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true);
448 expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true);
453 * @param varArgs Either a Slick.Group's "groupingKey" property, or a
454 * variable argument list of grouping values denoting a unique path to the row. For
455 * example, calling expandGroup('high', '10%') will expand the '10%' subgroup of
456 * the 'high' setGrouping.
458 function expandGroup(varArgs) {
459 var args = Array.prototype.slice.call(arguments);
461 if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
462 expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false);
464 expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false);
468 function getGroups() {
472 function extractGroups(rows, parentGroup) {
476 var groupsByVal = [];
478 var level = parentGroup ? parentGroup.level + 1 : 0;
479 var gi = groupingInfos[level];
481 for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
482 val = gi.predefinedValues[i];
483 group = groupsByVal[val];
485 group = new Slick.Group();
488 group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
489 groups[groups.length] = group;
490 groupsByVal[val] = group;
494 for (var i = 0, l = rows.length; i < l; i++) {
496 val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
497 group = groupsByVal[val];
499 group = new Slick.Group();
502 group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
503 groups[groups.length] = group;
504 groupsByVal[val] = group;
507 group.rows[group.count++] = r;
510 if (level < groupingInfos.length - 1) {
511 for (var i = 0; i < groups.length; i++) {
513 group.groups = extractGroups(group.rows, group);
517 groups.sort(groupingInfos[level].comparer);
522 // TODO: lazy totals calculation
523 function calculateGroupTotals(group) {
524 // TODO: try moving iterating over groups into compiled accumulator
525 var gi = groupingInfos[group.level];
526 var isLeafLevel = (group.level == groupingInfos.length);
527 var totals = new Slick.GroupTotals();
528 var agg, idx = gi.aggregators.length;
530 agg = gi.aggregators[idx];
532 gi.compiledAccumulators[idx].call(agg,
533 (!isLeafLevel && gi.aggregateChildGroups) ? group.groups : group.rows);
534 agg.storeResult(totals);
536 totals.group = group;
537 group.totals = totals;
540 function calculateTotals(groups, level) {
542 var gi = groupingInfos[level];
543 var idx = groups.length, g;
547 if (g.collapsed && !gi.aggregateCollapsed) {
551 // Do a depth-first aggregation so that parent setGrouping aggregators can access subgroup totals.
553 calculateTotals(g.groups, level + 1);
556 if (gi.aggregators.length && (
557 gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
558 calculateGroupTotals(g);
563 function finalizeGroups(groups, level) {
565 var gi = groupingInfos[level];
566 var groupCollapsed = gi.collapsed;
567 var toggledGroups = toggledGroupsByLevel[level];
568 var idx = groups.length, g;
571 g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
572 g.title = gi.formatter ? gi.formatter(g) : g.value;
575 finalizeGroups(g.groups, level + 1);
576 // Let the non-leaf setGrouping rows get garbage-collected.
577 // They may have been used by aggregates that go over all of the descendants,
578 // but at this point they are no longer needed.
584 function flattenGroupedRows(groups, level) {
586 var gi = groupingInfos[level];
587 var groupedRows = [], rows, gl = 0, g;
588 for (var i = 0, l = groups.length; i < l; i++) {
590 groupedRows[gl++] = g;
593 rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows;
594 for (var j = 0, jj = rows.length; j < jj; j++) {
595 groupedRows[gl++] = rows[j];
599 if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
600 groupedRows[gl++] = g.totals;
606 function getFunctionInfo(fn) {
607 var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
608 var matches = fn.toString().match(fnRegex);
610 params: matches[1].split(","),
615 function compileAccumulatorLoop(aggregator) {
616 var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
617 var fn = new Function(
619 "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
620 accumulatorInfo.params[0] + " = _items[_i]; " +
621 accumulatorInfo.body +
624 fn.displayName = fn.name = "compiledAccumulatorLoop";
628 function compileFilter() {
629 var filterInfo = getFunctionInfo(filter);
631 var filterBody = filterInfo.body
632 .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
633 .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1")
634 .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
635 "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
637 // This preserves the function template code after JS compression,
638 // so that replace() commands still work as expected.
640 //"function(_items, _args) { ",
641 "var _retval = [], _idx = 0; ",
642 "var $item$, $args$ = _args; ",
644 "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
645 "$item$ = _items[_i]; ",
651 tpl = tpl.replace(/\$filter\$/gi, filterBody);
652 tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
653 tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
655 var fn = new Function("_items,_args", tpl);
656 fn.displayName = fn.name = "compiledFilter";
660 function compileFilterWithCaching() {
661 var filterInfo = getFunctionInfo(filter);
663 var filterBody = filterInfo.body
664 .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
665 .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1")
666 .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
667 "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
669 // This preserves the function template code after JS compression,
670 // so that replace() commands still work as expected.
672 //"function(_items, _args, _cache) { ",
673 "var _retval = [], _idx = 0; ",
674 "var $item$, $args$ = _args; ",
676 "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
677 "$item$ = _items[_i]; ",
678 "if (_cache[_i]) { ",
679 "_retval[_idx++] = $item$; ",
680 "continue _coreloop; ",
687 tpl = tpl.replace(/\$filter\$/gi, filterBody);
688 tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
689 tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
691 var fn = new Function("_items,_args,_cache", tpl);
692 fn.displayName = fn.name = "compiledFilterWithCaching";
696 function uncompiledFilter(items, args) {
697 var retval = [], idx = 0;
699 for (var i = 0, ii = items.length; i < ii; i++) {
700 if (filter(items[i], args)) {
701 retval[idx++] = items[i];
708 function uncompiledFilterWithCaching(items, args, cache) {
709 var retval = [], idx = 0, item;
711 for (var i = 0, ii = items.length; i < ii; i++) {
714 retval[idx++] = item;
715 } else if (filter(item, args)) {
716 retval[idx++] = item;
724 function getFilteredAndPagedItems(items) {
726 var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
727 var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
729 if (refreshHints.isFilterNarrowing) {
730 filteredItems = batchFilter(filteredItems, filterArgs);
731 } else if (refreshHints.isFilterExpanding) {
732 filteredItems = batchFilterWithCaching(items, filterArgs, filterCache);
733 } else if (!refreshHints.isFilterUnchanged) {
734 filteredItems = batchFilter(items, filterArgs);
737 // special case: if not filtering and not paging, the resulting
738 // rows collection needs to be a copy so that changes due to sort
740 filteredItems = pagesize ? items : items.concat();
743 // get the current page
746 if (filteredItems.length < pagenum * pagesize) {
747 pagenum = Math.floor(filteredItems.length / pagesize);
749 paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
751 paged = filteredItems;
754 return {totalRows: filteredItems.length, rows: paged};
757 function getRowDiffs(rows, newRows) {
758 var item, r, eitherIsNonData, diff = [];
759 var from = 0, to = newRows.length;
761 if (refreshHints && refreshHints.ignoreDiffsBefore) {
763 Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
766 if (refreshHints && refreshHints.ignoreDiffsAfter) {
767 to = Math.min(newRows.length,
768 Math.max(0, refreshHints.ignoreDiffsAfter));
771 for (var i = from, rl = rows.length; i < to; i++) {
773 diff[diff.length] = i;
778 if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
779 item.__group !== r.__group ||
780 item.__group && !item.equals(r))
781 || (eitherIsNonData &&
782 // no good way to compare totals since they are arbitrary DTOs
783 // deep object comparison is pretty expensive
784 // always considering them 'dirty' seems easier for the time being
785 (item.__groupTotals || r.__groupTotals))
786 || item[idProperty] != r[idProperty]
787 || (updated && updated[item[idProperty]])
789 diff[diff.length] = i;
796 function recalc(_items) {
799 if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
800 refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
804 var filteredItems = getFilteredAndPagedItems(_items);
805 totalRows = filteredItems.totalRows;
806 var newRows = filteredItems.rows;
809 if (groupingInfos.length) {
810 groups = extractGroups(newRows);
812 calculateTotals(groups);
813 finalizeGroups(groups);
814 newRows = flattenGroupedRows(groups);
818 var diff = getRowDiffs(rows, newRows);
830 var countBefore = rows.length;
831 var totalRowsBefore = totalRows;
833 var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
835 // if the current page is no longer valid, go to last page and recalc
836 // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized
837 if (pagesize && totalRows < pagenum * pagesize) {
838 pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1);
839 diff = recalc(items, filter);
843 prevRefreshHints = refreshHints;
846 if (totalRowsBefore != totalRows) {
847 onPagingInfoChanged.notify(getPagingInfo(), null, self);
849 if (countBefore != rows.length) {
850 onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self);
852 if (diff.length > 0) {
853 onRowsChanged.notify({rows: diff}, null, self);
857 function syncGridSelection(grid, preserveHidden) {
859 var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());;
863 if (selectedRowIds.length > 0) {
865 var selectedRows = self.mapIdsToRows(selectedRowIds);
866 if (!preserveHidden) {
867 selectedRowIds = self.mapRowsToIds(selectedRows);
869 grid.setSelectedRows(selectedRows);
874 grid.onSelectedRowsChanged.subscribe(function(e, args) {
875 if (inHandler) { return; }
876 selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
879 this.onRowsChanged.subscribe(update);
881 this.onRowCountChanged.subscribe(update);
884 function syncGridCellCssStyles(grid, key) {
888 // since this method can be called after the cell styles have been set,
889 // get the existing ones right away
890 storeCellCssStyles(grid.getCellCssStyles(key));
892 function storeCellCssStyles(hash) {
894 for (var row in hash) {
895 var id = rows[row][idProperty];
896 hashById[id] = hash[row];
903 ensureRowsByIdCache();
905 for (var id in hashById) {
906 var row = rowsById[id];
907 if (row != undefined) {
908 newHash[row] = hashById[id];
911 grid.setCellCssStyles(key, newHash);
916 grid.onCellCssStylesChanged.subscribe(function(e, args) {
917 if (inHandler) { return; }
918 if (key != args.key) { return; }
920 storeCellCssStyles(args.hash);
924 this.onRowsChanged.subscribe(update);
926 this.onRowCountChanged.subscribe(update);
931 "beginUpdate": beginUpdate,
932 "endUpdate": endUpdate,
933 "setPagingOptions": setPagingOptions,
934 "getPagingInfo": getPagingInfo,
935 "getItems": getItems,
936 "setItems": setItems,
937 "setFilter": setFilter,
939 "fastSort": fastSort,
941 "setGrouping": setGrouping,
942 "getGrouping": getGrouping,
944 "setAggregators": setAggregators,
945 "collapseAllGroups": collapseAllGroups,
946 "expandAllGroups": expandAllGroups,
947 "collapseGroup": collapseGroup,
948 "expandGroup": expandGroup,
949 "getGroups": getGroups,
950 "getIdxById": getIdxById,
951 "getIdxByIdKey": getIdxByIdKey,
952 "getRowById": getRowById,
953 "getItemById": getItemById,
954 "getItemByIdx": getItemByIdx,
955 "mapRowsToIds": mapRowsToIds,
956 "mapIdsToRows": mapIdsToRows,
957 "setRefreshHints": setRefreshHints,
958 "setFilterArgs": setFilterArgs,
960 "updateItem": updateItem,
961 "insertItem": insertItem,
963 "deleteItem": deleteItem,
964 "syncGridSelection": syncGridSelection,
965 "syncGridCellCssStyles": syncGridCellCssStyles,
967 // data provider methods
968 "getLength": getLength,
970 "getItemMetadata": getItemMetadata,
973 "onRowCountChanged": onRowCountChanged,
974 "onRowsChanged": onRowsChanged,
975 "onPagingInfoChanged": onPagingInfoChanged
979 function AvgAggregator(field) {
982 this.init = function () {
984 this.nonNullCount_ = 0;
988 this.accumulate = function (item) {
989 var val = item[this.field_];
991 if (val != null && val !== "" && val !== NaN) {
992 this.nonNullCount_++;
993 this.sum_ += parseFloat(val);
997 this.storeResult = function (groupTotals) {
998 if (!groupTotals.avg) {
999 groupTotals.avg = {};
1001 if (this.nonNullCount_ != 0) {
1002 groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
1007 function MinAggregator(field) {
1008 this.field_ = field;
1010 this.init = function () {
1014 this.accumulate = function (item) {
1015 var val = item[this.field_];
1016 if (val != null && val !== "" && val !== NaN) {
1017 if (this.min_ == null || val < this.min_) {
1023 this.storeResult = function (groupTotals) {
1024 if (!groupTotals.min) {
1025 groupTotals.min = {};
1027 groupTotals.min[this.field_] = this.min_;
1031 function MaxAggregator(field) {
1032 this.field_ = field;
1034 this.init = function () {
1038 this.accumulate = function (item) {
1039 var val = item[this.field_];
1040 if (val != null && val !== "" && val !== NaN) {
1041 if (this.max_ == null || val > this.max_) {
1047 this.storeResult = function (groupTotals) {
1048 if (!groupTotals.max) {
1049 groupTotals.max = {};
1051 groupTotals.max[this.field_] = this.max_;
1055 function SumAggregator(field) {
1056 this.field_ = field;
1058 this.init = function () {
1062 this.accumulate = function (item) {
1063 var val = item[this.field_];
1064 if (val != null && val !== "" && val !== NaN) {
1065 this.sum_ += parseFloat(val);
1069 this.storeResult = function (groupTotals) {
1070 if (!groupTotals.sum) {
1071 groupTotals.sum = {};
1073 groupTotals.sum[this.field_] = this.sum_;
1077 // TODO: add more built-in aggregators
1078 // TODO: merge common aggregators in one to prevent needles iterating