| /* |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit, ViewChild } from '@angular/core'; |
| |
| import { filter, maxBy, minBy, orderBy, sumBy } from 'lodash'; |
| import { NzTableComponent, NzTableSortOrder } from 'ng-zorro-antd/table'; |
| import { utils, writeFile, WorkSheet } from 'xlsx'; |
| |
| import { TableData, Visualization, VISUALIZATION } from '@zeppelin/visualization'; |
| |
| type ColType = 'string' | 'date' | 'number'; |
| type AggregationType = 'count' | 'sum' | 'min' | 'max' | 'avg'; |
| |
| class FilterOption { |
| sort: 'desc' | 'asc' | '' = ''; |
| type: ColType = 'string'; |
| visible = true; |
| pinned?: string; |
| term = ''; |
| width: string | '*' = '*'; |
| aggregation: AggregationType | null = null; |
| aggregationValue: number | null = null; |
| } |
| |
| function typeCoercion(value: string, type: ColType): string | number | Date { |
| switch (type) { |
| case 'number': |
| const num = Number.parseFloat(value); |
| return Number.isNaN(num) ? value : num; |
| case 'date': |
| const date = new Date(value); |
| return Number.isNaN(date.valueOf()) ? value : date; |
| default: |
| return value; |
| } |
| } |
| |
| @Component({ |
| selector: 'zeppelin-visualization-table-visualization', |
| templateUrl: './table-visualization.component.html', |
| styleUrls: ['./table-visualization.component.less'], |
| changeDetection: ChangeDetectionStrategy.OnPush |
| }) |
| export class TableVisualizationComponent implements OnInit { |
| tableData?: TableData; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| rows: any[] = []; |
| columns: string[] = []; |
| colOptions = new Map<string, FilterOption>(); |
| types: ColType[] = ['string', 'number', 'date']; |
| aggregations: AggregationType[] = ['count', 'sum', 'min', 'max', 'avg']; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| @ViewChild(NzTableComponent, { static: false }) nzTable!: NzTableComponent<any>; |
| |
| exportFile(type: 'csv' | 'xlsx', all = true) { |
| const wb = utils.book_new(); |
| let ws: WorkSheet; |
| if (all) { |
| ws = utils.json_to_sheet(this.rows); |
| } else { |
| ws = utils.json_to_sheet([...this.nzTable.data]); |
| } |
| utils.book_append_sheet(wb, ws, 'Sheet1'); |
| writeFile(wb, `export.${type}`); |
| } |
| |
| onChangeType(type: ColType, col: string) { |
| this.getColOptionOrThrow(col).type = type; |
| this.filterRows(); |
| this.aggregate(); |
| } |
| |
| onChangeAggregation(aggregation: AggregationType, col: string) { |
| const opt = this.getColOptionOrThrow(col); |
| opt.aggregation = opt.aggregation === aggregation ? null : aggregation; |
| this.aggregate(); |
| } |
| |
| onSearch(): void { |
| this.filterRows(); |
| } |
| |
| onSortChange(col: string, type: NzTableSortOrder): void { |
| const opt = this.getColOptionOrThrow(col); |
| this.colOptions.delete(col); |
| if (type) { |
| opt.sort = type === 'descend' ? 'desc' : 'asc'; |
| } else { |
| opt.sort = ''; |
| } |
| this.colOptions.set(col, opt); |
| this.filterRows(); |
| } |
| |
| onTermChange(col: string, term: string) { |
| this.getColOptionOrThrow(col).term = term; |
| } |
| |
| getColOptionOrThrow(col: string): FilterOption { |
| const opt = this.colOptions.get(col); |
| if (!opt) { |
| throw new Error('Column option should have been initialized'); |
| } |
| return opt; |
| } |
| |
| aggregate() { |
| if (!this.tableData) { |
| throw new Error('tableData is not defined'); |
| } |
| const tableData = this.tableData; |
| this.colOptions.forEach((opt, key) => { |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const numValue = (row: any) => { |
| const value = typeCoercion(row[key], opt.type); |
| if (typeof value === 'number') { |
| return value; |
| } |
| if (value instanceof Date) { |
| return value.valueOf(); |
| } |
| return value; |
| }; |
| const getSum = () => |
| sumBy(tableData.rows, row => { |
| const value = typeCoercion(row[key], 'number'); |
| return typeof value === 'number' ? value : 0; |
| }); |
| |
| switch (opt.aggregation) { |
| case 'sum': |
| opt.aggregationValue = getSum(); |
| break; |
| case 'avg': |
| opt.aggregationValue = getSum() / tableData.rows.length; |
| break; |
| case 'count': |
| opt.aggregationValue = tableData.rows.length; |
| break; |
| case 'max': |
| opt.aggregationValue = maxBy(tableData.rows, numValue)[key]; |
| break; |
| case 'min': |
| opt.aggregationValue = minBy(tableData.rows, numValue)[key]; |
| break; |
| default: |
| opt.aggregationValue = null; |
| } |
| }); |
| } |
| |
| filterRows() { |
| if (!this.tableData) { |
| throw new Error('tableData is not defined'); |
| } |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const sortKeys: any[] = []; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const sortTypes: any[] = []; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const terms: any[] = []; |
| this.colOptions.forEach((value, key) => { |
| if (value.sort) { |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| sortKeys.push((row: any) => typeCoercion(row[key], value.type)); |
| sortTypes.push(value.sort); |
| } |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| terms.push((row: any) => String(row[key]).search(value.term) !== -1); |
| }); |
| this.rows = filter(this.tableData.rows, row => terms.every(term => term(row))); |
| this.rows = orderBy(this.rows, sortKeys, sortTypes); |
| this.cdr.markForCheck(); |
| } |
| |
| constructor( |
| @Inject(VISUALIZATION) public visualization: Visualization, |
| private cdr: ChangeDetectorRef |
| ) {} |
| |
| ngOnInit() {} |
| |
| render() { |
| this.tableData = this.visualization.transformed; |
| this.columns = this.tableData!.columns; |
| this.rows = [...this.tableData!.rows]; |
| this.columns.forEach(col => { |
| this.colOptions.set(col, new FilterOption()); |
| }); |
| this.filterRows(); |
| } |
| } |