Skip to content

Commit

Permalink
Add multi-level grouping to DataView.
Browse files Browse the repository at this point in the history
Based on the original pull request (#522) by ghiscoding.

Deprecated DataVIew APIs (will continue to work):
- .groupBy()
- .setAggregators()

New DataView APIs:
- .getGrouping()
- .setGrouping(groupingInfo)
- .setGrouping([groupingInfo1, groupingInfo2, ...])
- .collapseAllGroups()
- .collapseAllGroups(level)
- .expandAllGroups()
- .expandAllGroups(level)
- .collapseGroup(groupingKey)
- .collapseGroup(level1value, level2value, ...)
- .expandGroup(groupingKey)
- .expandGroup(level1value, level2value, ...)

Grouping info options (for use in .setGrouping() calls):
- getter
- formatter
- comparer
- aggregators
- aggregateCollapsed
- aggregateChildGroups
- collapsed

New Group fields:
- level
- groups
- groupingKey

Also fixed 0-handling in default aggregators.
  • Loading branch information
mleibman committed Feb 27, 2013
1 parent 1af8606 commit 90964ce
Show file tree
Hide file tree
Showing 5 changed files with 402 additions and 173 deletions.
239 changes: 147 additions & 92 deletions examples/example-grouping.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@
<link rel="stylesheet" href="examples.css" type="text/css"/>
<link rel="stylesheet" href="../controls/slick.columnpicker.css" type="text/css"/>
<style>
.cell-title {
.cell-effort-driven {
text-align: center;
}

.slick-group-title[level='0'] {
font-weight: bold;
}

.cell-effort-driven {
text-align: center;
.slick-group-title[level='1'] {
text-decoration: underline;
}

.slick-group-title[level='2'] {
font-style: italic;
}
</style>
</head>
Expand All @@ -28,7 +36,7 @@
<div id="pager" style="width:100%;height:20px;"></div>
</div>

<div class="options-panel">
<div class="options-panel" style="width:450px;">
<b>Options:</b>
<hr/>
<div style="padding:6px;">
Expand All @@ -38,28 +46,37 @@
<div style="width:100px;display:inline-block;" id="pcSlider"></div>
</div>
<br/><br/>
<button onclick="loadData(50)">50 rows</button>
<button onclick="loadData(50000)">50k rows</button>
<button onclick="loadData(500000)">500k rows</button>
<hr/>
<button onclick="clearGrouping()">Clear grouping</button>
<button onclick="dataView.setGrouping([])">Clear grouping</button>
<br/>
<button onclick="groupByDuration()">Group by duration & sort groups by value</button>
<br/>
<button onclick="groupByDurationOrderByCount()">Group by duration & sort groups by count</button>
<button onclick="groupByDurationOrderByCount(false)">Group by duration & sort groups by count</button>
<br/>
<button onclick="groupByDurationOrderByCountGroupCollapsed()">Group by duration & sort groups by count, group
<button onclick="groupByDurationOrderByCount(true)">Group by duration & sort groups by count, aggregate
collapsed
</button>
<br/>
<br/>
<button onclick="collapseAllGroups()">Collapse all groups</button>
<button onclick="groupByDurationEffortDriven()">Group by duration then effort-driven</button>
<br/>
<button onclick="expandAllGroups()">Expand all groups</button>
<button onclick="groupByDurationEffortDrivenPercent()">Group by duration then effort-driven then percent.</button>
<br/>
<br/>
<button onclick="dataView.collapseAllGroups()">Collapse all groups</button>
<br/>
<button onclick="dataView.expandAllGroups()">Expand all groups</button>
<br/>
</div>
<hr/>
<h2>Demonstrates:</h2>
<ul>
<li>
Fully dynamic and interactive grouping with filtering and aggregates over <b>50'000</b> items<br>
Fully dynamic and interactive multi-level grouping with filtering and aggregates over <b>50'000</b> items<br>
Each grouping level can have its own aggregates (over child rows, child groups, or all descendant rows).<br>
Personally, this is just the coolest slickest thing I've ever seen done with DHTML grids!
</li>
</ul>
Expand Down Expand Up @@ -91,11 +108,12 @@ <h2>Demonstrates:</h2>
var data = [];
var columns = [
{id: "sel", name: "#", field: "num", cssClass: "cell-selection", width: 40, resizable: false, selectable: false, focusable: false },
{id: "title", name: "Title", field: "title", width: 120, minWidth: 120, cssClass: "cell-title", sortable: true, editor: Slick.Editors.Text},
{id: "duration", name: "Duration", field: "duration", sortable: true},
{id: "title", name: "Title", field: "title", width: 70, minWidth: 50, cssClass: "cell-title", sortable: true, editor: Slick.Editors.Text},
{id: "duration", name: "Duration", field: "duration", width: 70, sortable: true, groupTotalsFormatter: sumTotalsFormatter},
{id: "%", name: "% Complete", field: "percentComplete", width: 80, formatter: Slick.Formatters.PercentCompleteBar, sortable: true, groupTotalsFormatter: avgTotalsFormatter},
{id: "start", name: "Start", field: "start", minWidth: 60, sortable: true},
{id: "finish", name: "Finish", field: "finish", minWidth: 60, sortable: true},
{id: "cost", name: "Cost", field: "cost", width: 90, sortable: true, groupTotalsFormatter: sumTotalsFormatter},
{id: "effort-driven", name: "Effort Driven", width: 80, minWidth: 20, maxWidth: 80, cssClass: "cell-effort-driven", field: "effortDriven", formatter: Slick.Formatters.Checkmark, sortable: true}
];

Expand All @@ -110,7 +128,19 @@ <h2>Demonstrates:</h2>
var prevPercentCompleteThreshold = 0;

function avgTotalsFormatter(totals, columnDef) {
return "avg: " + Math.round(totals.avg[columnDef.field]) + "%";
var val = totals.avg && totals.avg[columnDef.field];
if (val != null) {
return "avg: " + Math.round(val) + "%";
}
return "";
}

function sumTotalsFormatter(totals, columnDef) {
var val = totals.sum && totals.sum[columnDef.field];
if (val != null) {
return "total: " + ((Math.round(parseFloat(val)*100)/100));
}
return "";
}

function myFilter(item, args) {
Expand All @@ -126,95 +156,132 @@ <h2>Demonstrates:</h2>
return (x == y ? 0 : (x > y ? 1 : -1));
}

function collapseAllGroups() {
dataView.beginUpdate();
for (var i = 0; i < dataView.getGroups().length; i++) {
dataView.collapseGroup(dataView.getGroups()[i].value);
}
dataView.endUpdate();
}

function expandAllGroups() {
dataView.beginUpdate();
for (var i = 0; i < dataView.getGroups().length; i++) {
dataView.expandGroup(dataView.getGroups()[i].value);
}
dataView.endUpdate();
function groupByDuration() {
dataView.setGrouping({
getter: "duration",
formatter: function (g) {
return "Duration: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
},
aggregators: [
new Slick.Data.Aggregators.Avg("percentComplete"),
new Slick.Data.Aggregators.Sum("cost")
],
aggregateCollapsed: false
});
}

function clearGrouping() {
dataView.groupBy(null);
function groupByDurationOrderByCount(aggregateCollapsed) {
dataView.setGrouping({
getter: "duration",
formatter: function (g) {
return "Duration: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
},
comparer: function (a, b) {
return a.count - b.count;
},
aggregators: [
new Slick.Data.Aggregators.Avg("percentComplete"),
new Slick.Data.Aggregators.Sum("cost")
],
aggregateCollapsed: aggregateCollapsed
});
}

function groupByDuration() {
dataView.groupBy(
"duration",
function (g) {
function groupByDurationEffortDriven() {
dataView.setGrouping([
{
getter: "duration",
formatter :function (g) {
return "Duration: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
},
function (a, b) {
return a.value - b.value;
}
);
dataView.setAggregators([
new Slick.Data.Aggregators.Avg("percentComplete")
], false);
}

function groupByDurationOrderByCount() {
dataView.groupBy(
"duration",
function (g) {
return "Duration: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
aggregators: [
new Slick.Data.Aggregators.Sum("duration"),
new Slick.Data.Aggregators.Sum("cost")
],
aggregateCollapsed: true
},
{
getter: "effortDriven",
formatter :function (g) {
return "Effort-Driven: " + (g.value ? "True" : "False") + " <span style='color:green'>(" + g.count + " items)</span>";
},
function (a, b) {
return a.count - b.count;
}
);
dataView.setAggregators([
new Slick.Data.Aggregators.Avg("percentComplete")
], false);
aggregators: [
new Slick.Data.Aggregators.Avg("percentComplete"),
new Slick.Data.Aggregators.Sum("cost")
],
collapsed: true
}
]);
}

function groupByDurationOrderByCountGroupCollapsed() {
dataView.groupBy(
"duration",
function (g) {
function groupByDurationEffortDrivenPercent() {
dataView.setGrouping([
{
getter: "duration",
formatter: function (g) {
return "Duration: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
},
function (a, b) {
return a.count - b.count;
}
);
dataView.setAggregators([
new Slick.Data.Aggregators.Avg("percentComplete")
], true);
aggregators: [
new Slick.Data.Aggregators.Sum("duration"),
new Slick.Data.Aggregators.Sum("cost")
],
aggregateCollapsed: true
},
{
getter: "effortDriven",
formatter: function (g) {
return "Effort-Driven: " + (g.value ? "True" : "False") + " <span style='color:green'>(" + g.count + " items)</span>";
},
aggregators :[
new Slick.Data.Aggregators.Sum("duration"),
new Slick.Data.Aggregators.Sum("cost")
]
},
{
getter: "percentComplete",
formatter: function (g) {
return "% Complete: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
},
aggregators: [
new Slick.Data.Aggregators.Avg("percentComplete")
],
aggregateCollapsed: true,
collapsed: true
}
]);
}

$(".grid-header .ui-icon")
.addClass("ui-state-default ui-corner-all")
.mouseover(function (e) {
$(e.target).addClass("ui-state-hover")
})
.mouseout(function (e) {
$(e.target).removeClass("ui-state-hover")
});

$(function () {
function loadData(count) {
var someDates = ["01/01/2009", "02/02/2009", "03/03/2009"];
data = [];
// prepare the data
for (var i = 0; i < 50000; i++) {
for (var i = 0; i < count; i++) {
var d = (data[i] = {});

d["id"] = "id_" + i;
d["num"] = i;
d["title"] = "Task " + i;
d["duration"] = Math.round(Math.random() * 14);
d["percentComplete"] = Math.round(Math.random() * 100);
d["start"] = "01/01/2009";
d["finish"] = "01/05/2009";
d["start"] = someDates[ Math.floor((Math.random()*2)) ];
d["finish"] = someDates[ Math.floor((Math.random()*2)) ];
d["cost"] = Math.round(Math.random() * 10000) / 100;
d["effortDriven"] = (i % 5 == 0);
}
dataView.setItems(data);
}


$(".grid-header .ui-icon")
.addClass("ui-state-default ui-corner-all")
.mouseover(function (e) {
$(e.target).addClass("ui-state-hover")
})
.mouseout(function (e) {
$(e.target).removeClass("ui-state-hover")
});

$(function () {
var groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
dataView = new Slick.Data.DataView({
groupItemMetadataProvider: groupItemMetadataProvider,
Expand Down Expand Up @@ -309,24 +376,12 @@ <h2>Demonstrates:</h2>

// initialize the model after all the events have been hooked up
dataView.beginUpdate();
dataView.setItems(data);
dataView.setFilter(myFilter);
dataView.setFilterArgs({
percentComplete: percentCompleteThreshold
});
dataView.groupBy(
"duration",
function (g) {
return "Duration: " + g.value + " <span style='color:green'>(" + g.count + " items)</span>";
},
function (a, b) {
return a.value - b.value;
}
);
dataView.setAggregators([
new Slick.Data.Aggregators.Avg("percentComplete")
], false);
dataView.collapseGroup(0);
loadData(50);
groupByDuration();
dataView.endUpdate();

$("#gridContainer").resizable();
Expand Down
30 changes: 29 additions & 1 deletion slick.core.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,13 @@
*/
function Group() {
this.__group = true;
this.__updated = false;

/**
* Grouping level, starting with 0.
* @property level
* @type {Number}
*/
this.level = 0;

/***
* Number of rows in the group.
Expand Down Expand Up @@ -307,6 +313,28 @@
* @type {GroupTotals}
*/
this.totals = null;

/**
* Rows that are part of the group.
* @property rows
* @type {Array}
*/
this.rows = null;

/**
* Sub-groups that are part of the group.
* @property groups
* @type {Array}
*/
this.groups = null;

/**
* A unique key used to identify the group. This key can be used in calls to DataView
* collapseGroup() or expandGroup().
* @property groupingKey
* @type {Object}
*/
this.groupingKey = null;
}

Group.prototype = new NonDataItem();
Expand Down
Loading

0 comments on commit 90964ce

Please sign in to comment.