Skip to content

Commit

Permalink
Merge pull request #22 from ghiscoding/feature/grouping
Browse files Browse the repository at this point in the history
feat(grouping): add first draft of Grouping & Aggregators functionality
  • Loading branch information
ghiscoding authored Mar 20, 2018
2 parents 85fbe60 + c25c6bf commit a10d164
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 47 deletions.
2 changes: 2 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GridBasicComponent } from './examples/grid-basic.component';
import { GridClientSideComponent } from './examples/grid-clientside.component';
import { GridEditorComponent } from './examples/grid-editor.component';
import { GridFormatterComponent } from './examples/grid-formatter.component';
import { GridGroupingComponent } from './examples/grid-grouping.component';
import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component';
import { GridHeaderMenuComponent } from './examples/grid-headermenu.component';
import { GridLocalizationComponent } from './examples/grid-localization.component';
Expand All @@ -27,6 +28,7 @@ const routes: Routes = [
{ path: 'headermenu', component: GridHeaderMenuComponent },
{ path: 'gridgraphql', component: GridGraphqlComponent },
{ path: 'gridmenu', component: GridMenuComponent },
{ path: 'grouping', component: GridGroupingComponent },
{ path: 'localization', component: GridLocalizationComponent },
{ path: 'clientside', component: GridClientSideComponent },
{ path: 'odata', component: GridOdataComponent },
Expand Down
3 changes: 3 additions & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
<li routerLinkActive="active">
<a [routerLink]="['/swt']">13- Backend Server Custom Paging</a>
</li>
<li routerLinkActive="active">
<a [routerLink]="['/grouping']">14- Grouping &amp; Aggregator</a>
</li>
</ul>
</div>
</section>
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GridClientSideComponent } from './examples/grid-clientside.component';
import { GridEditorComponent } from './examples/grid-editor.component';
import { GridFormatterComponent } from './examples/grid-formatter.component';
import { GridGraphqlComponent } from './examples/grid-graphql.component';
import { GridGroupingComponent } from './examples/grid-grouping.component';
import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component';
import { GridHeaderMenuComponent } from './examples/grid-headermenu.component';
import { GridLocalizationComponent } from './examples/grid-localization.component';
Expand Down Expand Up @@ -63,6 +64,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
GridEditorComponent,
GridFormatterComponent,
GridGraphqlComponent,
GridGroupingComponent,
GridHeaderButtonComponent,
GridHeaderMenuComponent,
GridLocalizationComponent,
Expand Down
45 changes: 45 additions & 0 deletions src/app/examples/grid-grouping.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--The content below is only a placeholder and can be replaced.-->
<div class="container">
<h2>{{title}}</h2>
<div class="subtitle" [innerHTML]="subTitle"></div>
<div class="row col-sm-12">
<button class="btn btn-default btn-xs" (click)="loadData(500)">
500 rows
</button>
<button class="btn btn-default btn-xs" (click)="loadData(50000)">
50k rows
</button>
<button class="btn btn-default btn-xs" (click)="clearGrouping()">
Clear grouping
</button>
<button class="btn btn-default btn-xs" (click)="collapseAllGroups()">
Collapse all groups
</button>
<button class="btn btn-default btn-xs" (click)="expandAllGroups()">
Expand all groups
</button>
</div>
<hr/>
<div class="row col-sm-12">
<button class="btn btn-default btn-xs" (click)="groupByDuration()">
Group by duration &amp; sort groups by value
</button>
<button class="btn btn-default btn-xs" (click)="groupByDurationOrderByCount(false)">
Group by duration &amp; sort groups by count
</button>
</div>
<div class="row col-sm-12">
<button class="btn btn-default btn-xs" (click)="groupByDurationOrderByCount(true)">
Group by duration &amp; sort groups by count, aggregate collapsed
</button>
<button class="btn btn-default btn-xs" (click)="groupByDurationEffortDriven()">
Group by duration then effort-driven
</button>
<button class="btn btn-default btn-xs" (click)="groupByDurationEffortDrivenPercent()">
Group by duration then effort-driven then percent.
</button>
</div>
<angular-slickgrid gridId="grid2" (onDataviewCreated)="dataviewReady($event)" (onGridCreated)="gridReady($event)" [columnDefinitions]="columnDefinitions"
[gridOptions]="gridOptions" [dataset]="dataset">
</angular-slickgrid>
</div>
221 changes: 221 additions & 0 deletions src/app/examples/grid-grouping.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { Component, OnInit } from '@angular/core';
import { Column, FieldType, Formatter, Formatters, GridOption, Editors } from './../modules/angular-slickgrid';

// using external non-typed js libraries
declare var Slick: any;

// create my custom Formatter with the Formatter type
const myCustomCheckmarkFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) =>
value ? `<i class="fa fa-fire" aria-hidden="true"></i>` : '<i class="fa fa-snowflake-o" aria-hidden="true"></i>';

@Component({
templateUrl: './grid-grouping.component.html'
})
export class GridGroupingComponent implements OnInit {
title = 'Example 14: Grouping';
subTitle = `
<ul>
<li>Fully dynamic and interactive multi-level grouping with filtering and aggregates over 50'000 items</li>
<li>Each grouping level can have its own aggregates (over child rows, child groups, or all descendant rows)..</li>
</ul>
`;

columnDefinitions: Column[];
gridOptions: GridOption;
dataset: any[];
gridObj: any;
dataviewObj: any;
sortcol = 'title';
sortdir = 1;
percentCompleteThreshold = 0;
prevPercentCompleteThreshold = 0;

ngOnInit(): void {
this.columnDefinitions = [
{ id: 'sel', name: '#', field: 'num', width: 40, maxWidth: 70, resizable: true, selectable: false, focusable: false },
{ id: 'title', name: 'Title', field: 'title', width: 70, minWidth: 50, cssClass: 'cell-title', sortable: true, editor: Editors.text },
{ id: 'duration', name: 'Duration', field: 'duration', width: 70, sortable: true, groupTotalsFormatter: this.sumTotalsFormatter },
{ id: '%', name: '% Complete', field: 'percentComplete', width: 80, formatter: Formatters.percentCompleteBar, sortable: true, groupTotalsFormatter: this.avgTotalsFormatter },
{ id: 'start', name: 'Start', field: 'start', minWidth: 60, sortable: true, formatter: Formatters.dateIso },
{ id: 'finish', name: 'Finish', field: 'finish', minWidth: 60, sortable: true, formatter: Formatters.dateIso },
{ id: 'cost', name: 'Cost', field: 'cost', width: 90, sortable: true, groupTotalsFormatter: this.sumTotalsFormatter },
{ id: 'effort-driven', name: 'Effort Driven', width: 80, minWidth: 20, maxWidth: 80, cssClass: 'cell-effort-driven', field: 'effortDriven', formatter: Formatters.checkmark, sortable: true }
];
this.gridOptions = {
autoResize: {
containerId: 'demo-container',
sidePadding: 15
},
enableGrouping: true
};

this.loadData(500);
}

loadData(rowCount: number) {
// mock a dataset
this.dataset = [];
for (let i = 0; i < rowCount; i++) {
const randomYear = 2000 + Math.floor(Math.random() * 10);
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));
const randomPercent = Math.round(Math.random() * 100);

this.dataset[i] = {
id: 'id_' + i,
num: i,
title: 'Task ' + i,
duration: Math.round(Math.random() * 100) + '',
percentComplete: randomPercent,
percentCompleteNumber: randomPercent,
start: new Date(randomYear, randomMonth, randomDay),
finish: new Date(randomYear, (randomMonth + 1), randomDay),
cost: Math.round(Math.random() * 10000) / 100,
effortDriven: (i % 5 === 0)
};
}
}

gridReady(grid) {
this.gridObj = grid;
}

dataviewReady(dataview) {
this.dataviewObj = dataview;
}

clearGrouping() {
this.dataviewObj.setGrouping([]);
}

collapseAllGroups() {
this.dataviewObj.collapseAllGroups();
}

expandAllGroups() {
this.dataviewObj.expandAllGroups();
}

avgTotalsFormatter(totals, columnDef) {
const val = totals.avg && totals.avg[columnDef.field];
if (val != null) {
return 'avg: ' + Math.round(val) + '%';
}
return '';
}
sumTotalsFormatter(totals, columnDef) {
const val = totals.sum && totals.sum[columnDef.field];
if (val != null) {
return 'total: ' + ((Math.round(parseFloat(val) * 100) / 100));
}
return '';
}
myFilter(item, args) {
return item['percentComplete'] >= args.percentComplete;
}
percentCompleteSort(a, b) {
return a['percentComplete'] - b['percentComplete'];
}
comparer(a: any, b: any) {
const x = a[this.sortcol], y = b[this.sortcol];
return (x === y ? 0 : (x > y ? 1 : -1));
}
groupByDuration() {
this.dataviewObj.setGrouping({
getter: 'duration',
formatter: (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,
lazyTotalsCalculation: true
});
}
groupByDurationOrderByCount(aggregateCollapsed) {
this.dataviewObj.setGrouping({
getter: 'duration',
formatter: (g) => {
return `Duration: ${g.value} <span style="color:green">(${g.count} items)</span>`;
},
comparer: (a, b) => {
return a.count - b.count;
},
aggregators: [
new Slick.Data.Aggregators.Avg('percentComplete'),
new Slick.Data.Aggregators.Sum('cost')
],
aggregateCollapsed,
lazyTotalsCalculation: true
});
}
groupByDurationEffortDriven() {
this.dataviewObj.setGrouping([
{
getter: 'duration',
formatter: (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,
lazyTotalsCalculation: true
},
{
getter: 'effortDriven',
formatter: (g) => {
return `Effort-Driven: ${(g.value ? 'True' : 'False')} <span style="color:green">(${g.count} items)</span>`;
},
aggregators: [
new Slick.Data.Aggregators.Avg('percentComplete'),
new Slick.Data.Aggregators.Sum('cost')
],
collapsed: true,
lazyTotalsCalculation: true
}
]);
}
groupByDurationEffortDrivenPercent() {
this.dataviewObj.setGrouping([
{
getter: 'duration',
formatter: (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,
lazyTotalsCalculation: true
},
{
getter: 'effortDriven',
formatter: (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')
],
lazyTotalsCalculation: true
},
{
getter: 'percentComplete',
formatter: (g) => {
return `% Complete: ${g.value} <span style="color:green">(${g.count} items)</span>`;
},
aggregators: [
new Slick.Data.Aggregators.Avg('percentComplete')
],
aggregateCollapsed: true,
collapsed: true,
lazyTotalsCalculation: true
}
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'slickgrid/slick.core';
import 'slickgrid/slick.dataview';
import 'slickgrid/slick.grid';
import 'slickgrid/slick.dataview';
import 'slickgrid/slick.groupitemmetadataprovider.js';
import 'slickgrid/controls/slick.columnpicker';
import 'slickgrid/controls/slick.gridmenu';
import 'slickgrid/controls/slick.pager';
Expand Down Expand Up @@ -60,6 +61,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
gridHeightString: string;
gridWidthString: string;
groupingDefinition: any = {};
groupItemMetadataProvider: any;
showPagination = false;
isGridInitialized = false;

Expand Down Expand Up @@ -150,11 +152,23 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
this.gridOptions = this.mergeGridOptions(this.gridOptions);
this.createBackendApiInternalPostProcessCallback(this.gridOptions);

this._dataView = new Slick.Data.DataView();
if (this.gridOptions.enableGrouping) {
this.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
this.sharedService.groupItemMetadataProvider = this.groupItemMetadataProvider;
this._dataView = new Slick.Data.DataView({
groupItemMetadataProvider: this.groupItemMetadataProvider,
inlineFilters: true
});
} else {
this._dataView = new Slick.Data.DataView();
}
this.controlAndPluginService.createPluginBeforeGridCreation(this._columnDefinitions, this.gridOptions);
this.grid = new Slick.Grid(`#${this.gridId}`, this._dataView, this._columnDefinitions, this.gridOptions);

this.controlAndPluginService.attachDifferentControlOrPlugins(this.grid, this._columnDefinitions, this.gridOptions, this._dataView);
// pass all necessary options to the shared service
this.sharedService.init(this.grid, this._dataView, this.gridOptions, this._columnDefinitions);

this.controlAndPluginService.attachDifferentControlOrPlugins();
this.attachDifferentHooks(this.grid, this.gridOptions, this._dataView);

// emit the Grid & DataView object to make them available in parent component
Expand All @@ -166,9 +180,6 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
this._dataView.setItems(this._dataset, this.gridOptions.datasetIdPropertyName);
this._dataView.endUpdate();

// pass all necessary options to the shared service
this.sharedService.init(this.grid, this._dataView, this.gridOptions, this._columnDefinitions);

// attach resize ONLY after the dataView is ready
this.attachResizeHook(this.grid, this.gridOptions);

Expand Down
6 changes: 5 additions & 1 deletion src/app/modules/angular-slickgrid/models/column.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ColumnFilter } from './columnFilter.interface';
import { Editor } from './editor.interface';
import { FieldType } from './fieldType';
import { Formatter } from './formatter.interface';
import { GroupFormatter } from './groupFormatter.interface';
import { HeaderButtonItem } from './headerButtonItem.interface';
import { HeaderMenuItem } from './headerMenuItem.interface';
import { OnEventArgs } from './onEventArgs.interface';
Expand Down Expand Up @@ -73,9 +74,12 @@ export interface Column {
/** are we allowed to focus on the column? */
focusable?: boolean;

/** Custom Sorter function that can be provided to the column */
/** Formatter function that can be used to change and format certain column(s) in the grid */
formatter?: Formatter;

/** Group Totals Formatter function that can be used to add grouping totals in the grid */
groupTotalsFormatter?: GroupFormatter;

/** Options that can be provide to the Header Menu Plugin */
header?: {
/** list of Buttons to show in the header */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export interface GridOption {
/** Do we want to enable the Export to File? (if Yes, it will show up in the Grid Menu) */
enableExport?: boolean;

/** Defaults to false, do we want to enable the Grouping & Aggregator? */
enableGrouping?: boolean;

/** Defaults to false, which leads to all Formatters of the grid being evaluated on export. You can also override a column by changing the propery on the column itself */
exportWithFormatter?: boolean;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Column } from './column.interface';

export type GroupFormatter = (totals: any, columnDef: Column) => string;
Loading

0 comments on commit a10d164

Please sign in to comment.