summaryrefslogtreecommitdiffstats
path: root/slick.dataview.js
diff options
context:
space:
mode:
authormleibman <michael.leibman@gmail.com>2010-12-30 04:28:27 -0800
committermleibman <michael.leibman@gmail.com>2010-12-30 04:28:27 -0800
commitcdb6193ef7fb4528952fe25ee11f324f6092c2b0 (patch)
treec3251f0deb82cd7c26567563db6008ab77791bea /slick.dataview.js
parent1f2db649c0c0e0909e5f4eefb2804bc9daf76246 (diff)
downloadSlickGrid-cdb6193ef7fb4528952fe25ee11f324f6092c2b0.zip
SlickGrid-cdb6193ef7fb4528952fe25ee11f324f6092c2b0.tar.gz
SlickGrid-cdb6193ef7fb4528952fe25ee11f324f6092c2b0.tar.bz2
This is likely unfinished and buggy, but it is waaay too cool not to check in now!
Added Slick.NonDataRow and deriving Slick.Group and Slick.GroupTotals classes that the grid recognizes. Added grouping and aggregation support to the DataView. DataView.groupBy() takes in a grouping value getter (either a property name or a function), a grouping value formatter, and an optional sort comparer. The sort comparer is pretty slick. Unlike most other implementations, SlickGrid's grouping does not force you into any particular order. It can take your data just the way it is ordered and simply break it into groups. Essentially, you can end up with multiple groups with the same value in different places. If you do specify the grouping sort, however, you get a more traditional ordering where the items are first ordered by the groups and, then, ordered by whatever you normally sort by withing the groups. DataView now also supports custom aggregators and group expanding/collapsing. Several reference aggregators are included in Slick.Data. Totals row cells are formatted using the "groupTotalsFormatter" specified on the column definition. To maintain a consistent API, I've decided to remove the "fastSort" DataView method. For IE8 and lower, it may be better to override Array.prototype.sort with a custom quicksort or mergesort implementation. Overriding the Object.toString works great, but it requires a different way of specifying the sort. Also for consistency's sake, I've removed the DataView.rows collection. DataView now exposes GetItem() and GetLength() and can be used as a databinding source directly.
Diffstat (limited to 'slick.dataview.js')
-rw-r--r--slick.dataview.js332
1 files changed, 270 insertions, 62 deletions
diff --git a/slick.dataview.js b/slick.dataview.js
index 4069c85..a7732c6 100644
--- a/slick.dataview.js
+++ b/slick.dataview.js
@@ -1,4 +1,18 @@
(function($) {
+ $.extend(true, window, {
+ Slick: {
+ Data: {
+ DataView: DataView,
+ Aggregators: {
+ Avg: AvgAggregator,
+ Min: MinAggregator,
+ Max: MaxAggregator
+ }
+ }
+ }
+ });
+
+
/***
* A sample Model implementation.
* Provides a filtered view of the underlying data.
@@ -17,9 +31,13 @@
var filter = null; // filter function
var updated = null; // updated item ids
var suspend = false; // suspends the recalculation
- var sortAsc = true;
- var sortComparer = null;
- var fastSortField = null;
+ var sortComparer;
+ var groupingGetter;
+ var groupingFormatter;
+ var groupingComparer;
+ var collapsedGroups = {};
+ var aggregators;
+ var aggregateCollapsed = false;
var pagesize = 0;
var pagenum = 0;
@@ -77,43 +95,31 @@
return {pageSize:pagesize, pageNum:pagenum, totalRows:totalRows};
}
- function sort(comparer, ascending) {
- sortAsc = ascending;
- sortComparer = comparer;
- fastSortField = null;
- if (ascending === false) items.reverse();
- items.sort(comparer);
- if (ascending === false) items.reverse();
- refreshIdxById();
- refresh();
+ function getCombinedComparer(groupingCmp, cmp) {
+ if (!groupingCmp) {
+ return cmp;
+ }
+ else {
+ return function combinedComparer(a ,b) {
+ return groupingCmp(groupingGetter(a), groupingGetter(b)) || (cmp && cmp(a, b)) || 0;
+ }
+ }
}
- /***
- * Provides a workaround for the extremely slow sorting in IE.
- * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
- * to return the value of that field and then doing a native Array.sort().
- */
- function fastSort(field, ascending) {
- sortAsc = ascending;
- fastSortField = field;
- sortComparer = null;
- var oldToString = Object.prototype.toString;
- Object.prototype.toString = (typeof field == "function")?field:function() { return this[field] };
- // an extra reversal for descending sort keeps the sort stable
- // (assuming a stable native sort implementation, which isn't true in some cases)
- if (ascending === false) items.reverse();
- items.sort();
- Object.prototype.toString = oldToString;
- if (ascending === false) items.reverse();
- refreshIdxById();
- refresh();
+ function sort(comparer) {
+ sortComparer = comparer;
+ var combinedComparer = getCombinedComparer(groupingComparer, comparer);
+ if (combinedComparer) {
+ items.sort(combinedComparer);
+ refreshIdxById();
+ refresh();
+ }
}
function reSort() {
- if (sortComparer)
- sort(sortComparer,sortAsc);
- else if (fastSortField)
- fastSort(fastSortField,sortAsc);
+ if (groupingGetter || sortComparer) {
+ sort(sortComparer);
+ }
}
function setFilter(filterFn) {
@@ -121,6 +127,21 @@
refresh();
}
+ function groupBy(valueGetter, valueFormatter, sortComparer) {
+ groupingGetter = valueGetter;
+ groupingFormatter = valueFormatter;
+ groupingComparer = sortComparer;
+ collapsedGroups = {};
+ reSort();
+ refresh();
+ }
+
+ function setAggregators(groupAggregators, includeCollapsed) {
+ aggregators = groupAggregators;
+ aggregateCollapsed = includeCollapsed !== undefined ? includeCollapsed : aggregateCollapsed;
+ refresh();
+ }
+
function getItemByIdx(i) {
return items[i];
}
@@ -182,42 +203,154 @@
return rows[i];
}
+ function collapseGroup(groupingValue) {
+ collapsedGroups[groupingValue] = true;
+ refresh();
+ }
+
+ function expandGroup(groupingValue) {
+ delete collapsedGroups[groupingValue];
+ refresh();
+ }
+
+ function getGroupingValue(item) {
+ if (typeof groupingGetter === "function") {
+ return groupingGetter(item);
+ }
+ else {
+ return item[groupingGetter];
+ }
+ }
+
+ function getGroups(rows) {
+ var group;
+ var val;
+ var groups = [];
+
+ for (var i = 0, l = rows.length; i < l; i++) {
+ val = getGroupingValue(rows[i]);
+ if (!group || group.value !== val) {
+ if (group) {
+ group.end = i - 1;
+ group.count = group.end - group.start + 1;
+ group.title = groupingFormatter ? groupingFormatter(group) : group.value;
+ }
+
+ group = new Slick.Group();
+ group.value = val;
+ group.start = i;
+ group.collapsed = (val in collapsedGroups);
+ groups.push(group);
+ }
+ }
+ if (group) {
+ group.end = rows.length - 1;
+ group.count = group.end - group.start + 1;
+ group.title = groupingFormatter ? groupingFormatter(group) : group.value;
+ }
+
+ return groups;
+ }
+
+
+ function flattenGroupedRows(groups, rows) {
+ var groupedRows = [], gl = 0, idx, totals;
+ for (var i = 0, l = groups.length; i < l; i++) {
+ var g = groups[i];
+ groupedRows[gl++] = g;
+
+ if (aggregators) {
+ idx = aggregators.length;
+ while (idx--) {
+ aggregators[idx].init();
+ }
+ }
+
+ for (var j = g.start; j <= g.end; j++) {
+ if (aggregators) {
+ idx = aggregators.length;
+ while (idx--) {
+ aggregators[idx].accumulate(rows[j]);
+ }
+ }
+ if (!g.collapsed) {
+ groupedRows[gl++] = rows[j];
+ }
+ }
+
+ if (aggregators && (!g.collapsed || aggregateCollapsed)) {
+ totals = new Slick.GroupTotals();
+ idx = aggregators.length;
+ while (idx--) {
+ aggregators[idx].storeResult(totals);
+ }
+ groupedRows[gl++] = totals;
+ }
+ }
+ return groupedRows;
+ }
+
function recalc(_items, _rows, _filter, _updated) {
var diff = [];
- var items = _items, rows = _rows, filter = _filter, updated = _updated; // cache as local vars
rowsById = null;
// go over all items remapping them to rows on the fly
// while keeping track of the differences and updating indexes
- var rl = rows.length;
- var currentRowIndex = 0;
- var currentPageIndex = 0;
+ var pageStartRow = pagesize * pagenum;
+ var pageEndRow = pageStartRow + pagesize;
var item,id;
-
- for (var i = 0, il = items.length; i < il; ++i) {
- item = items[i];
-
- if (!filter || filter(item)) {
- id = item[idProperty];
-
- if (!pagesize || (currentRowIndex >= pagesize * pagenum && currentRowIndex < pagesize * (pagenum + 1))) {
- if (currentPageIndex >= rl || id != rows[currentPageIndex][idProperty] || (updated && updated[id])) {
- diff[diff.length] = currentPageIndex;
- rows[currentPageIndex] = item;
+ var newRows = [];
+ var il = _items.length;
+ var rl = _rows.length;
+ var itemIdx = 0, rowIdx = 0;
+
+ // filter the data and get the current page if paging
+ if (_filter) {
+ for (var i = 0; i < il; ++i) {
+ item = _items[i];
+ if (_filter(item)) {
+ if (!pagesize || (itemIdx >= pageStartRow && itemIdx < pageEndRow)) {
+ newRows[rowIdx] = item;
+ rowIdx++;
}
-
- currentPageIndex++;
+ itemIdx++;
}
+ }
+ }
+ else {
+ newRows = _items.concat();
+ itemIdx = il;
+ }
- currentRowIndex++;
+ if (groupingGetter != null) {
+ var groups = getGroups(newRows);
+ if (groups.length) {
+ newRows = flattenGroupedRows(groups, newRows);
}
}
- if (rl > currentPageIndex)
- rows.splice(currentPageIndex, rl - currentPageIndex);
+ for (var i = 0, nrl = newRows.length, r; i < nrl; i++) {
+ item = newRows[i];
+ r = _rows[i];
+
+ if (i >= rl ||
+ (groupingGetter &&
+ (item instanceof Slick.Group !== r instanceof Slick.Group ||
+ (item instanceof Slick.Group && !item.equals(r)))) ||
+ (aggregators &&
+ // no good way to compare totals since they are arbitrary DTOs
+ // deep object comparison is pretty expensive
+ // always considering them 'dirty' seems easier for the time being
+ (item instanceof Slick.GroupTotals || r instanceof Slick.GroupTotals)) ||
+ item[idProperty] != r[idProperty] ||
+ (_updated && _updated[item[idProperty]])) {
+ diff[diff.length] = i;
+ }
+ }
- totalRows = currentRowIndex;
+ rows = newRows;
+ totalRows = itemIdx;
return diff;
}
@@ -246,9 +379,6 @@
return {
- // properties
- "rows": rows, // note: neither the array or the data in it should be modified directly
-
// methods
"beginUpdate": beginUpdate,
"endUpdate": endUpdate,
@@ -258,8 +388,11 @@
"setItems": setItems,
"setFilter": setFilter,
"sort": sort,
- "fastSort": fastSort,
"reSort": reSort,
+ "groupBy": groupBy,
+ "setAggregators": setAggregators,
+ "collapseGroup": collapseGroup,
+ "expandGroup": expandGroup,
"getIdxById": getIdxById,
"getRowById": getRowById,
"getItemById": getItemById,
@@ -280,6 +413,81 @@
};
}
- // Slick.Data.DataView
- $.extend(true, window, { Slick: { Data: { DataView: DataView }}});
+
+
+
+ function AvgAggregator(field) {
+ this.init = function() {
+ this.count = 0;
+ this.nonNullCount = 0;
+ this.sum = 0;
+ };
+
+ this.accumulate = function(item) {
+ var val = item[field];
+ this.count++;
+ if (val != null && val != NaN) {
+ this.nonNullCount++;
+ this.sum += parseFloat(val);
+ }
+ };
+
+ this.storeResult = function(groupTotals) {
+ if (!groupTotals.avg) {
+ groupTotals.avg = {};
+ }
+ if (this.nonNullCount != 0) {
+ groupTotals.avg[field] = this.sum / this.nonNullCount;
+ }
+ };
+ }
+
+
+ function MinAggregator(field) {
+ this.init = function() {
+ this.min = null;
+ };
+
+ this.accumulate = function(item) {
+ var val = item[field];
+ if (val != null && val != NaN) {
+ if (this.min == null ||val < this.min) {
+ this.min = val;
+ }
+ }
+ };
+
+ this.storeResult = function(groupTotals) {
+ if (!groupTotals.min) {
+ groupTotals.min = {};
+ }
+ groupTotals.min[field] = this.min;
+ }
+ }
+
+ function MaxAggregator(field) {
+ this.init = function() {
+ this.max = null;
+ };
+
+ this.accumulate = function(item) {
+ var val = item[field];
+ if (val != null && val != NaN) {
+ if (this.max == null ||val > this.max) {
+ this.max = val;
+ }
+ }
+ };
+
+ this.storeResult = function(groupTotals) {
+ if (!groupTotals.max) {
+ groupTotals.max = {};
+ }
+ groupTotals.max[field] = this.max;
+ }
+ }
+
+ // TODO: add more built-in aggregators
+ // TODO: merge common aggregators in one to prevent needles iterating
+
})(jQuery); \ No newline at end of file