blob: 6d624df936489aba08a82a3beba6ea43fe8cd684 [file] [log] [blame]
/*
* 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();
}
}