@eclipse-scout/core
Version:
Eclipse Scout runtime
511 lines (460 loc) • 15.2 kB
text/typescript
/*
* 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[] };