UNPKG

@eclipse-scout/core

Version:
511 lines (460 loc) 15.2 kB
/* * Copyright (c) 2010, 2023 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {arrays, BooleanColumn, Column, comparators, DateColumn, DateFormat, dates, EnumObject, IconColumn, Locale, NumberColumn, objects, scout, Session, Table, TableRow} from '../index'; export class TableMatrix { session: Session; locale: Locale; protected _allData: TableMatrixDataAxis[]; protected _allAxis: TableMatrixKeyAxis[]; protected _rows: TableRow[]; protected _table: Table; constructor(table: Table, session: Session) { this.session = session; this.locale = session.locale; this._allData = []; this._allAxis = []; this._rows = table.rows; this._table = table; } static DateGroup = { NONE: 0, YEAR: 256, MONTH: 257, WEEKDAY: 258, DATE: 259 } as const; static NumberGroup = { COUNT: -1, SUM: 1, AVG: 2 } as const; /** * add data axis */ addData(data: Column<any>, dataGroup: TableMatrixNumberGroup): TableMatrixDataAxis { // @ts-expect-error let dataAxis: TableMatrixDataAxis = {}, locale = this.locale; // collect all axis this._allData.push(dataAxis); // copy column for later access dataAxis.column = data; // data always is number dataAxis.format = n => locale.decimalFormat.format(n); // count, sum, avg if (dataGroup === TableMatrix.NumberGroup.COUNT) { dataAxis.norm = f => 1; dataAxis.group = array => array.length; } else if (dataGroup === TableMatrix.NumberGroup.SUM) { dataAxis.norm = f => { if (isNaN(f) || f === null || f === '') { return null; } return parseFloat(f); }; dataAxis.group = array => array.reduce((a, b) => a + b); } else if (dataGroup === TableMatrix.NumberGroup.AVG) { dataAxis.norm = f => { if (isNaN(f) || f === null || f === '') { return null; } return parseFloat(f); }; dataAxis.group = array => { let sum = array.reduce((a, b) => a + b); let count = array.reduce((a, b) => b === null ? a : a + 1, 0); if (count === 0) { return null; } return sum / count; }; } return dataAxis; } // add x or y Axis addAxis(axis: Column<any>, axisGroup: TableMatrixNumberGroup | TableMatrixDateGroup): TableMatrixKeyAxis { // @ts-expect-error let keyAxis: TableMatrixKeyAxis = [], locale = this.locale, session = this.session, getText = this.session.text.bind(this.session), emptyCell = getText('ui.EmptyCell'); // collect all axis this._allAxis.push(keyAxis); keyAxis.column = axis; // normalized string data keyAxis.normTable = []; keyAxis.sortCodeMap = {}; // add a key to the axis keyAxis.add = k => { if (keyAxis.indexOf(k) === -1) { keyAxis.push(k); } }; // default functions keyAxis.reorder = () => { keyAxis.sort((a, b) => { // make sure -empty- is at the bottom if (a === null) { return 1; } if (b === null) { return -1; } let sortCodeA = keyAxis.sortCodeMap[a], sortCodeB = keyAxis.sortCodeMap[b]; if (!objects.isNullOrUndefined(sortCodeA) || !objects.isNullOrUndefined(sortCodeB)) { return comparators.NUMERIC.compare(sortCodeA, sortCodeB); } // sort others return (a - b); }); }; keyAxis.norm = f => { if (f === null || f === '') { return null; } let index = keyAxis.normTable.indexOf(f); if (index === -1) { return keyAxis.normTable.push(f) - 1; } return index; }; keyAxis.format = n => { if (n === null) { return emptyCell; } return keyAxis.normTable[n]; }; keyAxis.deterministicKeyToKey = deterministicKey => keyAxis.norm(deterministicKey); keyAxis.keyToDeterministicKey = key => { if (key === null) { return null; } return keyAxis.format(key); }; keyAxis.normDeterministic = f => keyAxis.keyToDeterministicKey(keyAxis.norm(f)); // norm and format depends of datatype and group functionality if (axis instanceof DateColumn) { if (axisGroup === TableMatrix.DateGroup.NONE) { keyAxis.norm = f => { if (f === null || f === '') { return null; } return f.getTime(); }; keyAxis.format = n => { if (n === null) { return null; } let format = axis.format; if (format) { format = DateFormat.ensure(locale, format); } else { format = locale.dateFormat; } return format.format(new Date(n)); }; } else if (axisGroup === TableMatrix.DateGroup.YEAR) { keyAxis.norm = f => { if (f === null || f === '') { return null; } return f.getFullYear(); }; keyAxis.format = n => { if (n === null) { return emptyCell; } return String(n); }; } else if (axisGroup === TableMatrix.DateGroup.MONTH) { keyAxis.norm = f => { if (f === null || f === '') { return null; } return f.getMonth(); }; keyAxis.format = n => { if (n === null) { return emptyCell; } return locale.dateFormatSymbols.months[n]; }; } else if (axisGroup === TableMatrix.DateGroup.WEEKDAY) { keyAxis.norm = f => { if (f === null || f === '') { return null; } return (f.getDay() + 7 - locale.dateFormatSymbols.firstDayOfWeek) % 7; }; keyAxis.format = n => { if (n === null) { return emptyCell; } return locale.dateFormatSymbols.weekdaysOrdered[n]; }; } else if (axisGroup === TableMatrix.DateGroup.DATE) { keyAxis.norm = f => { if (f === null || f === '') { return null; } return dates.trunc(f).getTime(); }; keyAxis.format = n => { if (n === null) { return emptyCell; } return dates.format(new Date(n), locale, locale.dateFormatPatternDefault); }; } keyAxis.deterministicKeyToKey = (deterministicKey: number) => deterministicKey; keyAxis.keyToDeterministicKey = key => key; keyAxis.normDeterministic = f => keyAxis.norm(f); } else if (axis instanceof NumberColumn) { keyAxis.norm = f => { if (isNaN(f) || f === null || f === '') { return null; } return parseFloat(f); }; keyAxis.format = n => { if (isNaN(n) || n === null) { return emptyCell; } return axis.decimalFormat.format(n); }; keyAxis.deterministicKeyToKey = (deterministicKey: number) => deterministicKey; keyAxis.keyToDeterministicKey = key => key; keyAxis.normDeterministic = f => keyAxis.norm(f); } else if (axis instanceof BooleanColumn) { keyAxis.norm = f => { if (axis.triStateEnabled && f === null) { return -1; } if (f === true) { return 1; } return 0; }; keyAxis.format = n => { if (n === -1) { return getText('ui.BooleanColumnGroupingMixed'); } if (n === 0) { return getText('ui.BooleanColumnGroupingFalse'); } if (n === 1) { return getText('ui.BooleanColumnGroupingTrue'); } }; keyAxis.deterministicKeyToKey = (deterministicKey: number) => deterministicKey; keyAxis.keyToDeterministicKey = key => key; keyAxis.normDeterministic = f => keyAxis.norm(f); } else if (axis instanceof IconColumn) { keyAxis.isIcon = true; } else { keyAxis.reorder = () => { let comparator = comparators.TEXT; comparator.install(session); keyAxis.sort((a, b) => { // make sure -empty- is at the bottom if (a === null) { return 1; } if (b === null) { return -1; } let sortCodeA = keyAxis.sortCodeMap[a], sortCodeB = keyAxis.sortCodeMap[b]; if (!objects.isNullOrUndefined(sortCodeA) || !objects.isNullOrUndefined(sortCodeB)) { return comparators.NUMERIC.compare(sortCodeA, sortCodeB); } // sort others return comparator.compare(keyAxis.format(a), keyAxis.format(b)); }); }; } return keyAxis; } /** * @returns a cube containing the results */ calculate(): TableMatrixResult { let cube: Record<string, Array<number[] | number>> & { length?: number; getValue?(keys: number[]): number[] } = {}, length = 0; // collect data from table for (let r = 0; r < this._rows.length; r++) { let row = this._rows[r]; // collect keys of x, y axis from row let keys: number[] = []; for (let k = 0; k < this._allAxis.length; k++) { let column = this._allAxis[k].column; let key = column.cellValueOrTextForCalculation(row); let normKey = this._allAxis[k].norm(key); if (normKey !== undefined) { this._allAxis[k].add(normKey); let cell = column.cell(row); if (cell.sortCode !== null) { this._allAxis[k].sortCodeMap[normKey] = cell.sortCode; } keys.push(normKey); } } let keysString = JSON.stringify(keys); // collect values of data axis from row let values: number[] = []; for (let v = 0; v < this._allData.length; v++) { let data = this._table.cellValue(this._allData[v].column, row); let normData = this._allData[v].norm(data); if (normData !== undefined) { values.push(normData); } } // build cube if (cube[keysString]) { cube[keysString].push(values); } else { cube[keysString] = [values]; length++; } } // group values and find sum, min and max of data axis for (let v = 0; v < this._allData.length; v++) { let data = this._allData[v]; data.total = 0; data.min = null; data.max = null; for (let k in cube) { if (cube.hasOwnProperty(k)) { let allCell = cube[k], subCell: number[] = []; for (let i = 0; i < allCell.length; i++) { subCell.push(allCell[i][v]); } let newValue = this._allData[v].group(subCell); cube[k][v] = newValue; data.total += newValue; if (newValue === null) { continue; } if (newValue < data.min || data.min === null) { data.min = newValue; } if (newValue > data.max || data.min === null) { data.max = newValue; } } } // To calculate correct y axis scale data.max must not be 0. If data.max===0-> log(data.max)=-infinity if (scout.nvl(data.max, 0) === 0) { data.max = 0.1; } let f = Math.ceil(Math.log(data.max) / Math.LN10) - 1; data.max = Math.ceil(data.max / Math.pow(10, f)) * Math.pow(10, f); data.max = Math.ceil(data.max / 4) * 4; } // find dimensions and sort for x, y axis for (let k = 0; k < this._allAxis.length; k++) { let key = this._allAxis[k]; key.min = arrays.min(key); key.max = arrays.max(key); // null value should be handled as first value (in charts) if (key.indexOf(null) !== -1) { key.max = key.max + 1; } key.reorder(); } // access function used by chart cube.getValue = keys => { let keysString = JSON.stringify(keys); if (cube.hasOwnProperty(keysString)) { return cube[keysString] as number[]; } return null; }; cube.length = length; return cube as TableMatrixResult; // cast necessary because in this method cube temporary contains an Array<number | number[]>. But in the end it is reduced to only number[]. } /** * * @returns Array holding an entry for each column. Each entry consists of an array with the column at index 0 and the count at index 1. */ columnCount(filterNumberColumns?: boolean): Array<Array<Column<any> | number>> { let columns = this.columns(filterNumberColumns), colCount: Array<Array<Column<any> | any[] | number>> = [], count = 0; for (let c = 0; c < columns.length; c++) { let column = columns[c]; colCount.push([column, []]); let values = colCount[count][1] as any[]; for (let r = 0; r < this._rows.length; r++) { let row = this._rows[r]; let cellValue = column.cellValueOrTextForCalculation(row); if (values.indexOf(cellValue) === -1) { values.push(cellValue); } } colCount[count][1] = values.length; count++; } return colCount as Array<Array<Column<any> | number>>; } isEmpty(): boolean { return this._rows.length === 0 || this.columns().length === 0; } /** * @returns valid columns for table-matrix (not instance of NumberColumn and not guiOnly) * @param filterNumberColumns whether or not to filter NumberColumn, default is true */ columns(filterNumberColumns?: boolean): Column<any>[] { filterNumberColumns = scout.nvl(filterNumberColumns, true); return this._table.visibleColumns(false, true).filter(column => { if (filterNumberColumns && column instanceof NumberColumn) { return false; } return true; }); } /** * Table rows and columns are not always in a consistent state. * @returns true, if table is in a valid, consistent state */ isMatrixValid(): boolean { return this._table.rows.length === 0 || this._table.filterColumns(() => true, false).length === this._table.rows[0].cells.length; } } export type TableMatrixNumberGroup = EnumObject<typeof TableMatrix.NumberGroup>; export type TableMatrixDateGroup = EnumObject<typeof TableMatrix.DateGroup>; export type TableMatrixKeyAxis = number[] & { column: Column<any>; normTable: string[]; sortCodeMap: Record<number, number>; isIcon?: boolean; iconId?: string; min: number; max: number; format(n: number): string; keyToDeterministicKey(n: number): number | string; deterministicKeyToKey(f: string | number): number; normDeterministic(f: any): string | number; norm(f: any): number; add(k: number); reorder(): void; }; export type TableMatrixDataAxis = { column: Column<any>; total: number; min: number; max: number; format(n: number): string; norm(f: any): number; group(array: number[]): number; }; export type TableMatrixResult = Record<string, number[]> & { length: number; getValue(keys: number[]): number[] };