diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts index 9bc777b88b5..b3c808d2e3f 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts @@ -48,8 +48,8 @@ import { IgxHierarchicalGridExportComponent, import { IgxHierarchicalGridComponent } from '../../grids/hierarchical-grid/public_api'; import { IgxHierarchicalRowComponent } from '../../grids/hierarchical-grid/hierarchical-row.component'; import { GridFunctions } from '../../test-utils/grid-functions.spec'; -import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent } from '../../test-utils/pivot-grid-samples.spec'; -import { IgxPivotGridComponent, PivotRowLayoutType } from '../../grids/pivot-grid/public_api'; +import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent, SALES_DATA } from '../../test-utils/pivot-grid-samples.spec'; +import { IgxPivotGridComponent, IgxPivotNumericAggregate, PivotRowLayoutType } from '../../grids/pivot-grid/public_api'; describe('Excel Exporter', () => { configureTestSuite(); @@ -1458,7 +1458,7 @@ describe('Excel Exporter', () => { let grid: IgxPivotGridComponent; beforeEach(waitForAsync(() => { - options = createExportOptions('PivotGridGridExcelExport'); + options = createExportOptions('PivotGridExcelExport'); })); it('should export pivot grid', async () => { @@ -1483,6 +1483,80 @@ describe('Excel Exporter', () => { await exportAndVerify(grid, options, actualData.exportPivotGridDataWithHeaders, false); }); + it('should export pivot grid with hierarchical row dimensions.', async () => { + fix = TestBed.createComponent(IgxPivotGridMultipleRowComponent); + fix.detectChanges(); + + grid = fix.componentInstance.pivotGrid; + fix.componentInstance.data = SALES_DATA; + fix.componentInstance.pivotConfigHierarchy = { + rows: [ + { + memberName: 'All_Srep Code Alts', + enabled: true, + width: '150px', + childLevel: { + memberName: 'SREP_CODE_ALT', + displayName: 'Srep Code Alt', + sortDirection: 1, + enabled: true, + }, + }, + { + memberName: 'All_Srep Codes', + enabled: true, + width: '150px', + childLevel: { + memberName: 'SREP_CODE', + displayName: 'Srep Code', + sortDirection: 1, + enabled: true, + }, + }, + { + memberName: 'All_Customers', + enabled: true, + width: '150px', + childLevel: { + memberName: 'CUST_CODE', + displayName: 'Customer', + sortDirection: 1, + enabled: true, + }, + } + ], + columns: [], + values: [ + { + member: 'JOBS', + aggregate: { + key: 'Count of Jobs', + aggregator: IgxPivotNumericAggregate.count, + label: 'Count of Jobs', + }, + enabled: true, + dataType: 'number', + }, + { + member: 'INV_SALES', + aggregate: { + key: 'Sum of Sales', + aggregator: IgxPivotNumericAggregate.sum, + label: 'Sum of Sales', + }, + enabled: true, + dataType: 'number', + }, + ], + filters: [], + }; + grid.pivotUI.showRowHeaders = true; + fix.detectChanges(); + await wait(300); + + await exportAndVerify(grid, options, actualData.exportPivotGridHierarchicalRowDimensions, false); + }); + it('should export hierarchical pivot grid', async () => { fix = TestBed.createComponent(IgxPivotGridTestComplexHierarchyComponent); fix.detectChanges(); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts index 6b7c9fb512f..95f41241b01 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts @@ -632,7 +632,7 @@ export class WorksheetFile implements IExcelFile { for (const currentCol of headersForLevel) { const spanLength = isVertical ? currentCol.rowSpan : currentCol.columnSpan; - if (currentCol.level === i && currentCol.headerType !== ExportHeaderType.PivotMergedHeader) { + if (currentCol.level === i) { let columnCoordinate; const column = isVertical ? this.rowIndex @@ -644,7 +644,10 @@ export class WorksheetFile implements IExcelFile { if (currentCol.headerType === ExportHeaderType.PivotRowHeader) { rowCoordinate = startValue + 1; } - const columnValue = dictionary.saveValue(currentCol.header, true, false); + + const columnValue = currentCol.headerType === ExportHeaderType.PivotMergedHeader ? + dictionary.saveValue(currentCol.field, true, true) : + dictionary.saveValue(currentCol.header, true, false); columnCoordinate = (currentCol.field === GRID_LEVEL_COL ? ExcelStrings.getExcelColumn(worksheetData.columnCount + 1) diff --git a/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts b/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts index d03e1bd864b..708212fad86 100644 --- a/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts @@ -1659,7 +1659,7 @@ export class FileContentData { public get exportPivotGridData() { this._sharedStringsData = - `count="38" uniqueCount="23">AccessoriesBikesClothingComponentsUSAUruguayBulgaria04/07/202101/06/202001/05/201905/12/202001/01/202102/19/202012/08/2021StanleyElisaLydiaDavidJohnLarryWalterUnitsSoldUnitPrice`; + `count="38" uniqueCount="23">AccessoriesBikesClothingComponentsUSAUruguayBulgaria04/07/202101/06/202001/01/202102/19/202001/05/201905/12/202012/08/2021StanleyElisaLydiaDavidJohnLarryWalterUnitsSoldUnitPrice`; this._worksheetData = ` @@ -1667,14 +1667,14 @@ export class FileContentData { topLeftCell="D1" activePane="topRight" state="frozen"/> - 14151617181920212221222122212221222122212204729385.58158683.5624928212.8151049216.0561129649.571245668.33341324018.13 `; + 14151617181920212221222122212221222122212204729385.58158683.5626928212.811049216.0541129649.5751245668.33341324018.13 `; return this.createData(); } public get exportPivotGridDataWithHeaders() { this._sharedStringsData = - `count="41" uniqueCount="26">ProductCategoryAccessoriesBikesClothingComponentsCountryUSAUruguayBulgariaDate04/07/202101/06/202001/05/201905/12/202001/01/202102/19/202012/08/2021StanleyElisaLydiaDavidJohnLarryWalterUnitsSoldUnitPrice`; + `count="41" uniqueCount="26">ProductCategoryAccessoriesBikesClothingComponentsCountryUSAUruguayBulgariaDate04/07/202101/06/202001/01/202102/19/202001/05/201905/12/202012/08/2021StanleyElisaLydiaDavidJohnLarryWalterUnitsSoldUnitPrice`; this._worksheetData = ` @@ -1682,17 +1682,17 @@ export class FileContentData { topLeftCell="D1" activePane="topRight" state="frozen"/> - 059171819202122232425242524252425242524252425161029385.582711683.56361228212.8171349216.0581429649.571545668.33461624018.13 `; + 059171819202122232425242524252425242524252425161029385.582711683.56381228212.811349216.0561429649.5771545668.33461624018.13 `; return this.createData(); } public get exportPivotGridDataHorizontal() { this._sharedStringsData = - `count="41" uniqueCount="26">ProductCategoryAccessoriesBikesClothingComponentsCountryUSAUruguayBulgariaDate04/07/202101/06/202001/05/201905/12/202001/01/202102/19/202012/08/2021StanleyElisaLydiaDavidJohnLarryWalterUnitsSoldUnitPrice`; + `count="41" uniqueCount="26">ProductCategoryAccessoriesBikesClothingComponentsCountryUSAUruguayBulgariaDate04/07/202101/06/202001/01/202102/19/202001/05/201905/12/202012/08/2021StanleyElisaLydiaDavidJohnLarryWalterUnitsSoldUnitPrice`; this._worksheetData = - `059171819202122232425242524252425242524252425161029385.582711683.56361228212.8171349216.0581429649.571545668.33461624018.13`; + `059171819202122232425242524252425242524252425161029385.582711683.56381228212.811349216.0561429649.5771545668.33461624018.13 `; return this.createData(); } @@ -1712,6 +1712,25 @@ export class FileContentData { return this.createData(); } + public get exportPivotGridHierarchicalRowDimensions() { + this._sharedStringsData = + `count="198" uniqueCount="56">All_Srep Code Alts006020024029037041047053056060All_Srep Codes538254256162183103159603604622410419421263110111220231238All_Customers45230578001555780033864VS00862CW9211800185678046621804969950535965049856800332395044928148650596475062088VS05682053968203889110609831057802VS02524JOBSINV_SALES`; + + this._worksheetData = + ` + + + + + 01232545511121733281.02999999993313253.3134158081.9535131745.1136127565.6437198731.6338140424.7539118538.6240116352.7341130413.8742130797.9343137109.544124370.7845110792.434617229.73471176455.664814418.1549118192.1650124663.1451113113.945212497.1153158532.8913113253.313313253.31141158081.9534158081.95151259310.7535131745.1136127565.64161198731.6337198731.63171140424.7538140424.75181234891.3539118538.6240116352.73191130413.8741130413.87201130797.9342130797.93211137109.543137109.5221124370.78` + + `44124370.78231110792.4345110792.4324117229.734617229.732511176455.66471176455.6626114418.154814418.15271118192.1649118192.16281124663.1450124663.14291113113.9451113113.9430112497.115212497.11311158532.8953158532.8921113253.313313253.3113113253.313313253.313113117392.734158081.9535131745.1136127565.64141158081.9534158081.95151259310.7535131745.1136127565.644112139156.3837198731.6338140424.75161198731.6337198731.63171140424.7538140424.75511234891.3539118538.6240116352.73181234891.35` + + `39118538.6240116352.73611130413.8741130413.87191130413.8741130413.87711392278.2099999999942130797.9343137109.544124370.78201130797.9342130797.93211137109.543137109.5221124370.7844124370.788113194477.8245110792.434617229.73471176455.66231110792.4345110792.4324117229.734617229.732511176455.66471176455.6691114418.154814418.1526114418.154814418.151011242855.349118192.1650124663.14271118192.1649118192.16281124663.1450124663.141111374143.9451113113.945212497.1153158532.89291113113.94` + + `51113113.9430112497.115212497.11311158532.8953158532.89 `; + + return this.createData(); + } + public get exportGridWithSummaries() { this._sharedStringsData = `count="86" uniqueCount="36">CityShippedContactTitlePTODaysGRID_LEVEL_COLBerlinSales RepresentativeMéxico D.F.OwnerLondonLuleåOrder AdministratorMannheimStrasbourgMarketing ManagerMadridMarseilleTsawassenAccounting ManagerBuenos AiresSales AgentBernSao PauloSales AssociateAachenNantesGrazSales ManagerMarketing AssistantLilletrueAssistant Sales AgentBräckeMünchenTorino`; diff --git a/projects/igniteui-angular/src/lib/services/excel/zip-verification-wrapper.spec.ts b/projects/igniteui-angular/src/lib/services/excel/zip-verification-wrapper.spec.ts index 0206a6ebbdd..94e73010040 100644 --- a/projects/igniteui-angular/src/lib/services/excel/zip-verification-wrapper.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/zip-verification-wrapper.spec.ts @@ -59,7 +59,7 @@ export class ZipWrapper { private createFilesAndFolders(obj: Object, prefix: string) { Object.keys(obj).forEach((key) => { if (ArrayBuffer.isView(obj[key])) { - this._files.set(`${prefix}${key}`, obj[key]); + this._files.set(`${prefix}${key}`, obj[key] as Uint8Array); this._filesAndFolders.push(`${prefix}${key}`); } else { const newPrefix = `${prefix}${key}/`; diff --git a/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts b/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts index 189b4290623..55abad6726b 100644 --- a/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts +++ b/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts @@ -1336,36 +1336,60 @@ export abstract class IgxBaseExporter { return; } - let startIndex = 0; - const key = keys[0]; const records = this.flatRecords.map(r => r.data); - const groupedRecords = {}; - records.forEach(obj => { - const keyValue = obj[key.name]; - if (!groupedRecords[keyValue]) { - groupedRecords[keyValue] = []; - } - groupedRecords[keyValue].push(obj); - }); + const groupedRecords = this.groupByKeys(records, keys); - if (columnGroupParent) { - const mapKeys = [...this.pivotGridKeyValueMap.keys()]; - const mapValues = [...this.pivotGridKeyValueMap.values()]; + this.createRowDimension(groupedRecords, keys, columnGroupParent); + } - for (const k of Object.keys(groupedRecords)) { - groupedRecords[k] = groupedRecords[k].filter(row => mapKeys.every(mk => Object.keys(row).includes(mk)) - && mapValues.every(mv => Object.values(row).includes(mv))); - - if (groupedRecords[k].length === 0) { - delete groupedRecords[k]; - } + private groupByKeys(items: any[], keys: any[]): any { + const group = (data: any[], groupKeys: any[]): any => { + if (groupKeys.length === 0) return data; + + const newKeys = [...groupKeys]; + const key = newKeys.shift().name; + const map = new Map(); + + for (const item of data) { + const keyValue = item[key]; + if (!map.has(keyValue)) { + map.set(keyValue, []); } + map.get(keyValue).push(item); + } + + for (const [keyValue, value] of map) { + map.set(keyValue, group(value, newKeys)); + } + + return map; + }; + + return group(items, keys); + } + + private calculateRowSpan(value: any): number { + if (value instanceof Map) { + return Array.from(value.values()).reduce( + (total, current) => total + this.calculateRowSpan(current), + 0 + ) + } else if (Array.isArray(value)) { + return value.length; } - for (const k of Object.keys(groupedRecords)) { - let groupKey = k; - const rowSpan = groupedRecords[k].length; + return 0; + } + private createRowDimension(node: any, keys: any[], columnGroupParent?: string) { + if (!(node instanceof Map)) return; + + const key = keys[0]; + const newKeys = keys.filter(k => k.level > key.level); + let startIndex = 0; + for (const k of node.keys()) { + let groupKey = k; + const rowSpan = this.calculateRowSpan(node.get(k)); const rowDimensionColumn: IColumnInfo = { columnSpan: 1, @@ -1377,32 +1401,28 @@ export abstract class IgxBaseExporter { pinnedIndex: 0, level: key.level, dataType: 'string', - headerType: groupedRecords[groupKey].length > 1 ? ExportHeaderType.MultiRowHeader : ExportHeaderType.RowHeader, + headerType: rowSpan > 1 ? ExportHeaderType.MultiRowHeader : ExportHeaderType.RowHeader, }; - if (groupKey === 'undefined') { - this.pivotGridColumns[this.pivotGridColumns.length - 1].columnSpan += 1; + + if (!groupKey) { + // if (this.pivotGridColumns?.length) + // this.pivotGridColumns[this.pivotGridColumns.length - 1].columnSpan += 1; rowDimensionColumn.headerType = ExportHeaderType.PivotMergedHeader; groupKey = columnGroupParent; } - if (columnGroupParent) { + if (key.level > 0) { rowDimensionColumn.columnGroupParent = columnGroupParent; } else { rowDimensionColumn.columnGroup = groupKey; } this.pivotGridColumns.push(rowDimensionColumn); - - if (keys.length > 1) { - if (groupKey !== columnGroupParent) { - this.pivotGridKeyValueMap.set(key.name, groupKey); - } - const newKeys = keys.filter(kdd => kdd !== key); - this.preparePivotGridColumns(newKeys, groupKey) - this.pivotGridKeyValueMap.delete(key.name); - } - startIndex += rowSpan; } + + for (const k of node.keys()) { + this.createRowDimension(node.get(k), newKeys, columnGroupParent); + } } private addLevelColumns() { diff --git a/projects/igniteui-angular/src/lib/test-utils/pivot-grid-samples.spec.ts b/projects/igniteui-angular/src/lib/test-utils/pivot-grid-samples.spec.ts index 4be80813f34..11bfd434ff6 100644 --- a/projects/igniteui-angular/src/lib/test-utils/pivot-grid-samples.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/pivot-grid-samples.spec.ts @@ -400,3 +400,153 @@ export class IgxTotalSaleAggregate { return max; }; } + +export const SALES_DATA =[ + { + "JOBS": 35, + "INV_SALES": 2497.11, + "CUST_CODE": "1057802", + "SREP_CODE": "231", + "SREP_CODE_ALT": "060" + }, + { + "JOBS": 1241, + "INV_SALES": 98731.63, + "CUST_CODE": "CW9211", + "SREP_CODE": "162", + "SREP_CODE_ALT": "024" + }, + { + "JOBS": 619, + "INV_SALES": 58532.89, + "CUST_CODE": "VS02524", + "SREP_CODE": "238", + "SREP_CODE_ALT": "060" + }, + { + "JOBS": 534, + "INV_SALES": 37109.5, + "CUST_CODE": "80033239", + "SREP_CODE": "604", + "SREP_CODE_ALT": "041" + }, + { + "JOBS": 262, + "INV_SALES": 16352.73, + "CUST_CODE": "8049699", + "SREP_CODE": "103", + "SREP_CODE_ALT": "029" + }, + { + "JOBS": 1621, + "INV_SALES": 176455.66, + "CUST_CODE": "5062088", + "SREP_CODE": "421", + "SREP_CODE_ALT": "047" + }, + { + "JOBS": 150, + "INV_SALES": 13113.94, + "CUST_CODE": "1060983", + "SREP_CODE": "220", + "SREP_CODE_ALT": "060" + }, + { + "JOBS": 400, + "INV_SALES": 24663.14, + "CUST_CODE": "2038891", + "SREP_CODE": "111", + "SREP_CODE_ALT": "056" + }, + { + "JOBS": 62, + "INV_SALES": 4418.15, + "CUST_CODE": "VS0568", + "SREP_CODE": "263", + "SREP_CODE_ALT": "053" + }, + { + "JOBS": 128, + "INV_SALES": 10792.43, + "CUST_CODE": "1486", + "SREP_CODE": "410", + "SREP_CODE_ALT": "047" + }, + { + "JOBS": 393, + "INV_SALES": 30797.93, + "CUST_CODE": "5049856", + "SREP_CODE": "603", + "SREP_CODE_ALT": "041" + }, + { + "JOBS": 458, + "INV_SALES": 24370.78, + "CUST_CODE": "5044928", + "SREP_CODE": "622", + "SREP_CODE_ALT": "041" + }, + { + "JOBS": 289, + "INV_SALES": 30413.87, + "CUST_CODE": "5053596", + "SREP_CODE": "159", + "SREP_CODE_ALT": "037" + }, + { + "JOBS": 372, + "INV_SALES": 27565.64, + "CUST_CODE": "VS00862", + "SREP_CODE": "256", + "SREP_CODE_ALT": "020" + }, + { + "JOBS": 354, + "INV_SALES": 40424.75, + "CUST_CODE": "80018567", + "SREP_CODE": "183", + "SREP_CODE_ALT": "024" + }, + { + "JOBS": 356, + "INV_SALES": 31745.11, + "CUST_CODE": "80033864", + "SREP_CODE": "256", + "SREP_CODE_ALT": "020" + }, + { + "JOBS": 910, + "INV_SALES": 58081.95, + "CUST_CODE": "80015557", + "SREP_CODE": "254", + "SREP_CODE_ALT": "020" + }, + { + "JOBS": 166, + "INV_SALES": 7229.73, + "CUST_CODE": "5059647", + "SREP_CODE": "419", + "SREP_CODE_ALT": "047" + }, + { + "JOBS": 304, + "INV_SALES": 18192.16, + "CUST_CODE": "2053968", + "SREP_CODE": "110", + "SREP_CODE_ALT": "056" + }, + { + "JOBS": 40, + "INV_SALES": 3253.31, + "CUST_CODE": "4523057", + "SREP_CODE": "538", + "SREP_CODE_ALT": "006" + }, + { + "JOBS": 332, + "INV_SALES": 18538.62, + "CUST_CODE": "8046621", + "SREP_CODE": "103", + "SREP_CODE_ALT": "029" + } +];