Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(grouping): add first draft of Grouping & Aggregators functionality #22

Merged
merged 1 commit into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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