Angular由一个bug说起之十六:Angular 透视表Pivot Table
在现代前端开发中,数据分析与可视化 是提升用户体验和决策效率的重要手段。在数据分析和可视化场景中,透视表(Pivot Table)是极为常用的组件。它能够灵活地对原始数据进行分组、汇总和多维展示。
本文将以一个基于 Angular 框架 的项目为例,逐步演示如何从最基础的数据表格出发,逐步构建出功能强大、结构清晰、性能优良的透视表组件。
V1:基础数据表格
首先,我们展示最原始的数据表格。该表格直接将数据源中的每一条记录以行的形式展现,便于查看原始数据内容。这种方式适用于数据量较小、结构简单的场景。但通常情况下一个表格会比较长。
V2:嵌套分组表格
嵌套分组是常见的需求。这样可以缩短列表长度。还能通过展开/收起分组提升数据可读性。适合层级结构明显的数据。
在V1表格基础上实现了按class分组的嵌套展示。这里通过展开/收起分组,可以清晰的看到每个班级的学生分数情况。
<table mat-table [dataSource]=\"dataSource\" class=\"score-table score-table-nested mat-elevation-z1\"> <ng-container matColumnDef=\"group\"> <td mat-cell *matCellDef=\"let group\" [attr.colspan]=\"displayedColumns.length\" (click)=\"toggleScoreExpand(group.className)\" class=\"expandable-row\" style=\"cursor:pointer;\"> {{ group.className }} <span class=\"inline-arrow\">{{ expandedScores.has(group.className) ? \'↓\' : \'→\' }}</span> <div class=\"arrow\"> ... </div> </td> </ng-container> <ng-container matColumnDef=\"student\"> <th mat-header-cell *matHeaderCellDef>Class</th> <td mat-cell *matCellDef=\"let row\">{{ row.student }}</td> </ng-container> <ng-container matColumnDef=\"year\"> <th mat-header-cell *matHeaderCellDef>Year</th> <td mat-cell *matCellDef=\"let row\">{{ row.year }}</td> </ng-container> <ng-container matColumnDef=\"score\"> <th mat-header-cell *matHeaderCellDef>Score</th> <td mat-cell *matCellDef=\"let row\">{{ row.score }}</td> </ng-container> <ng-container matColumnDef=\"level\"> <th mat-header-cell *matHeaderCellDef>Level</th> <td mat-cell *matCellDef=\"let row\">{{ row.level }}</td> </ng-container> <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr> <tr mat-row *matRowDef=\"let row; columns: [\'group\']; when: isGroup\"></tr> <tr mat-row *matRowDef=\"let row; columns: displayedColumns; when: isDetail\"></tr> </table> <mat-paginator [pageSize]=\"10\" [pageSizeOptions]=\"[10]\" showFirstLastButtons [hidePageSize]=\"true\"></mat-paginator>
export class ScoreGroupedTableComponent implements OnInit { @ViewChild(MatPaginator) paginator!: MatPaginator; @Input() data: any[] = []; expandedScores = new Set<string>(); displayedColumns: string[] = [\'student\', \'year\', \'score\', \'level\']; groupedData: any[] = []; dataSource = new MatTableDataSource<any>(); ngOnInit() { this.updateGroupedData(); setTimeout(() => { if (this.dataSource && this.paginator) { this.dataSource.paginator = this.paginator; } }); } toggleScoreExpand(className: string) { if (this.expandedScores.has(className)) { this.expandedScores.delete(className); } else { this.expandedScores.add(className); } this.updateGroupedData(); } isGroup = (_: number, item: any) => item.isGroup === true; isDetail = (_: number, item: any) => !item.isGroup; updateGroupedData() { if (!this.data) { this.groupedData = []; return; } // Group by className const groups: any[] = []; const grouped = this.data.reduce((acc: Record<string, any[]>, row: any) => { acc[row.className] = acc[row.className] || []; acc[row.className].push(row); return acc; }, {} as Record<string, any[]>); Object.keys(grouped).forEach(className => { groups.push({ isGroup: true, className }); if (this.expandedScores.has(className)) { groups.push(...grouped[className].map((row: any) => ({ ...row, isGroup: false }))); } }); this.groupedData = groups; this.dataSource.data = this.groupedData; if (this.paginator) { this.dataSource.paginator = this.paginator; } } }
V3:分组嵌套透视表格
这时候在V2的基础上,我们可以对数据进行透视,例如,将 Year作为一级表头,统计每年学生的分数形成横向对比减少数据行数。实现更清晰的结构分区和更强的数据对比能力。
旨在应对高复杂度、多维度、层级嵌套的数据分析需求。
亮点功能:
•表头支持多级嵌套,视觉层次分明;
•支持任意维度组合;
数据驱动渲染:
•所有表头与单元格均由数据动态生成;
•支持缺失值处理与默认占位符(如 ‘-’);
•可扩展为支持多指标(如 test、rank、score 等)展示。
适用场景:
•需要按维度进行横向对比,进行复杂的交叉分析
•数据维度较多,需清晰展示结构;
<table mat-table [dataSource]=\"dataSource\" class=\"score-table score-table-nested mat-elevation-z1\"> <ng-container matColumnDef=\"group\"> <td mat-cell *matCellDef=\"let group\" [attr.colspan]=\"displayedColumns.length\" (click)=\"toggleScoreExpand(group.className)\" class=\"expandable-row\" style=\"cursor:pointer;\"> {{ group.className }} <span class=\"inline-arrow\">{{ expandedScores.has(group.className) ? \'↓\' : \'→\' }}</span> <div class=\"arrow\"> ... </div> </td> </ng-container> <ng-container matColumnDef=\"student\"> <th mat-header-cell *matHeaderCellDef rowspan=\"2\">Class</th> <td mat-cell *matCellDef=\"let row\">{{ row.student }}</td> </ng-container> @for (year of yearColumnsGroup; track $index) { <ng-container [matColumnDef]=\"\'yearGroup_\' + year\"> <th mat-header-cell *matHeaderCellDef colspan=\"2\">{{ year }}</th> </ng-container> } @for (year of yearColumnsGroup; track $index) { <ng-container [matColumnDef]=\"\'score_\' + year\"> <th mat-header-cell *matHeaderCellDef class=\"section-border-left\">score</th> <td mat-cell *matCellDef=\"let row\" class=\"section-border-left\">{{ row[\'score_\' + year] }}</td> </ng-container> <ng-container [matColumnDef]=\"\'level_\' + year\"> <th mat-header-cell *matHeaderCellDef>level</th> <td mat-cell *matCellDef=\"let row\">{{ row[\'level_\' + year] }}</td> </ng-container> } <tr mat-header-row *matHeaderRowDef=\"headerRowDef1\"></tr> <tr mat-header-row *matHeaderRowDef=\"yearColumns\"></tr> <tr mat-row *matRowDef=\"let row; columns: [\'group\']; when: isGroup\"></tr> <tr mat-row *matRowDef=\"let row; columns: displayedColumns; when: isDetail\"></tr> </table> <mat-paginator [pageSize]=\"10\" [pageSizeOptions]=\"[10]\" showFirstLastButtons [hidePageSize]=\"true\"></mat-paginator>
export class ScorePivotTableComponent implements OnInit { @ViewChild(MatPaginator) paginator!: MatPaginator; @Input() data: any[] = []; expandedScores = new Set<string>(); displayedColumns: string[] = []; yearColumns: string[] = []; yearColumnsGroup: string[] = []; headerRowDef1: string[] = []; groupedData: any[] = []; dataSource = new MatTableDataSource<any>(); ngOnInit() { this.updateGroupedData(); setTimeout(() => { if (this.dataSource && this.paginator) { this.dataSource.paginator = this.paginator; } }); } toggleScoreExpand(className: string) { this.expandedScores.has(className) ? this.expandedScores.delete(className) : this.expandedScores.add(className); this.updateGroupedData(); } isGroup = (_: number, item: any) => item.isGroup === true; isDetail = (_: number, item: any) => !item.isGroup; updateGroupedData() { if (!this.data) { this.groupedData = []; return; } const years = Array.from(new Set(this.data.map(row => row.year))).sort(); this.yearColumnsGroup = years; this.yearColumns = years.flatMap(year => [`score_${year}`, `level_${year}`]); this.displayedColumns = [\'student\', ...this.yearColumns]; this.headerRowDef1 = [\'student\', ...years.map(y => \'yearGroup_\' + y)]; const grouped = this.data.reduce((acc: Record<string, any[]>, row: any) => { (acc[row.className] ||= []).push(row); return acc; }, {} as Record<string, any[]>); const groups: any[] = []; for (const className in grouped) { groups.push({ isGroup: true, className }); if (this.expandedScores.has(className)) { const students = Array.from(new Set(grouped[className].map((row: any) => row.student))); for (const student of students) { const studentRow: any = { isGroup: false, student }; for (const year of years) { const found = grouped[className].find((row: any) => row.student === student && row.year === year); studentRow[`score_${year}`] = found?.score ?? \'\'; studentRow[`level_${year}`] = found?.level ?? \'\'; } groups.push(studentRow); } } } this.groupedData = groups; this.dataSource.data = this.groupedData; if (this.paginator) { this.dataSource.paginator = this.paginator; } } }
优化建议
• 数据结构设计:建议在后端或服务层预处理数据,减少前端计算压力;
• 组件解耦:将透视逻辑封装为服务,提升复用性;
• 交互增强:可加入字段拖拽、动态维度选择、导出 Excel 等功能;
• 可视化联动:结合图表库实现图表与表格联动展示。
总结
通过以上三个阶段的实践,我们可以看到 Angular 在实现透视表和复杂表格方面的强大能力。无论是基础表格、单维透视、多维分组,还是嵌套分组与多维透视的结合,都可以通过合理的数据结构设计和模板渲染轻松实现。希望本文能为你在实际项目中实现和优化透视表提供参考和启发!