mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
chore(frontend): comprehensive TypeScript quality improvements (#37625)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
} from 'react';
|
||||
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
import { t } from '@apache-superset/core';
|
||||
import {
|
||||
@@ -501,7 +505,7 @@ export default function PivotTableChart(props: PivotTableProps) {
|
||||
|
||||
const toggleFilter = useCallback(
|
||||
(
|
||||
e: MouseEvent,
|
||||
e: ReactMouseEvent,
|
||||
value: string,
|
||||
filters: FilterType,
|
||||
pivotData: Record<string, any>,
|
||||
@@ -598,10 +602,10 @@ export default function PivotTableChart(props: PivotTableProps) {
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(
|
||||
e: MouseEvent,
|
||||
colKey: (string | number | boolean)[] | undefined,
|
||||
rowKey: (string | number | boolean)[] | undefined,
|
||||
dataPoint: { [key: string]: string },
|
||||
e: ReactMouseEvent,
|
||||
colKey?: string[],
|
||||
rowKey?: string[],
|
||||
dataPoint?: { [key: string]: string },
|
||||
) => {
|
||||
if (onContextMenu) {
|
||||
e.preventDefault();
|
||||
@@ -611,7 +615,7 @@ export default function PivotTableChart(props: PivotTableProps) {
|
||||
colKey.forEach((val, i) => {
|
||||
const col = cols[i];
|
||||
const formatter = dateFormatters[col];
|
||||
const formattedVal = formatter?.(val as number) || String(val);
|
||||
const formattedVal = formatter?.(Number(val)) || String(val);
|
||||
if (i > 0) {
|
||||
drillToDetailFilters.push({
|
||||
col,
|
||||
@@ -627,7 +631,7 @@ export default function PivotTableChart(props: PivotTableProps) {
|
||||
rowKey.forEach((val, i) => {
|
||||
const col = rows[i];
|
||||
const formatter = dateFormatters[col];
|
||||
const formattedVal = formatter?.(val as number) || String(val);
|
||||
const formattedVal = formatter?.(Number(val)) || String(val);
|
||||
drillToDetailFilters.push({
|
||||
col,
|
||||
op: '==',
|
||||
@@ -639,7 +643,9 @@ export default function PivotTableChart(props: PivotTableProps) {
|
||||
}
|
||||
onContextMenu(e.clientX, e.clientY, {
|
||||
drillToDetail: drillToDetailFilters,
|
||||
crossFilter: getCrossFilterDataMask(dataPoint),
|
||||
crossFilter: dataPoint
|
||||
? getCrossFilterDataMask(dataPoint)
|
||||
: undefined,
|
||||
drillBy: dataPoint && {
|
||||
filters: [
|
||||
{
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
import { PureComponent } from 'react';
|
||||
import { TableRenderer } from './TableRenderers';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
class PivotTable extends PureComponent {
|
||||
type PivotTableProps = ComponentProps<typeof TableRenderer>;
|
||||
|
||||
class PivotTable extends PureComponent<PivotTableProps> {
|
||||
render() {
|
||||
return <TableRenderer {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
PivotTable.propTypes = TableRenderer.propTypes;
|
||||
PivotTable.defaultProps = TableRenderer.defaultProps;
|
||||
|
||||
export default PivotTable;
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { css, styled } from '@apache-superset/core/ui';
|
||||
|
||||
export const Styles = styled.div`
|
||||
export const Styles = styled.div<{ isDashboardEditMode: boolean }>`
|
||||
${({ theme, isDashboardEditMode }) => css`
|
||||
table.pvtTable {
|
||||
position: ${isDashboardEditMode ? 'inherit' : 'relative'};
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Component } from 'react';
|
||||
import { Component, ReactNode, MouseEvent } from 'react';
|
||||
import { safeHtmlSpan } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -27,7 +27,106 @@ import { FaSortUp as FaSortAsc } from '@react-icons/all-files/fa/FaSortUp';
|
||||
import { PivotData, flatKey } from './utilities';
|
||||
import { Styles } from './Styles';
|
||||
|
||||
const parseLabel = value => {
|
||||
interface CellColorFormatter {
|
||||
column: string;
|
||||
getColorFromValue(value: unknown): string | undefined;
|
||||
}
|
||||
|
||||
type ClickCallback = (
|
||||
e: MouseEvent,
|
||||
value: unknown,
|
||||
filters: Record<string, string>,
|
||||
pivotData: InstanceType<typeof PivotData>,
|
||||
) => void;
|
||||
|
||||
type HeaderClickCallback = (
|
||||
e: MouseEvent,
|
||||
value: string,
|
||||
filters: Record<string, string>,
|
||||
pivotData: InstanceType<typeof PivotData>,
|
||||
isSubtotal: boolean,
|
||||
isGrandTotal: boolean,
|
||||
) => void;
|
||||
|
||||
interface TableOptions {
|
||||
rowTotals?: boolean;
|
||||
colTotals?: boolean;
|
||||
rowSubTotals?: boolean;
|
||||
colSubTotals?: boolean;
|
||||
clickCallback?: ClickCallback;
|
||||
clickColumnHeaderCallback?: HeaderClickCallback;
|
||||
clickRowHeaderCallback?: HeaderClickCallback;
|
||||
highlightHeaderCellsOnHover?: boolean;
|
||||
omittedHighlightHeaderGroups?: string[];
|
||||
highlightedHeaderCells?: Record<string, unknown[]>;
|
||||
cellColorFormatters?: Record<string, CellColorFormatter[]>;
|
||||
dateFormatters?: Record<string, ((val: unknown) => string) | undefined>;
|
||||
}
|
||||
|
||||
interface SubtotalDisplay {
|
||||
displayOnTop: boolean;
|
||||
enabled?: boolean;
|
||||
hideOnExpand: boolean;
|
||||
}
|
||||
|
||||
interface SubtotalOptions {
|
||||
arrowCollapsed?: ReactNode;
|
||||
arrowExpanded?: ReactNode;
|
||||
colSubtotalDisplay?: Partial<SubtotalDisplay>;
|
||||
rowSubtotalDisplay?: Partial<SubtotalDisplay>;
|
||||
}
|
||||
|
||||
interface TableRendererProps {
|
||||
cols: string[];
|
||||
rows: string[];
|
||||
aggregatorName: string;
|
||||
tableOptions: TableOptions;
|
||||
subtotalOptions?: SubtotalOptions;
|
||||
namesMapping?: Record<string, string>;
|
||||
onContextMenu: (
|
||||
e: MouseEvent,
|
||||
colKey?: string[],
|
||||
rowKey?: string[],
|
||||
filters?: Record<string, string>,
|
||||
) => void;
|
||||
allowRenderHtml?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface TableRendererState {
|
||||
collapsedRows: Record<string, boolean>;
|
||||
collapsedCols: Record<string, boolean>;
|
||||
sortingOrder: string[];
|
||||
activeSortColumn?: number | null;
|
||||
}
|
||||
|
||||
interface PivotSettings {
|
||||
pivotData: InstanceType<typeof PivotData>;
|
||||
colAttrs: string[];
|
||||
rowAttrs: string[];
|
||||
colKeys: string[][];
|
||||
rowKeys: string[][];
|
||||
rowTotals: boolean;
|
||||
colTotals: boolean;
|
||||
arrowCollapsed: ReactNode;
|
||||
arrowExpanded: ReactNode;
|
||||
colSubtotalDisplay: SubtotalDisplay;
|
||||
rowSubtotalDisplay: SubtotalDisplay;
|
||||
cellCallbacks: Record<string, Record<string, (e: MouseEvent) => void>>;
|
||||
rowTotalCallbacks: Record<string, (e: MouseEvent) => void>;
|
||||
colTotalCallbacks: Record<string, (e: MouseEvent) => void>;
|
||||
grandTotalCallback: ((e: MouseEvent) => void) | null;
|
||||
namesMapping: Record<string, string>;
|
||||
allowRenderHtml?: boolean;
|
||||
visibleRowKeys?: string[][];
|
||||
visibleColKeys?: string[][];
|
||||
maxRowVisible?: number;
|
||||
maxColVisible?: number;
|
||||
rowAttrSpans?: number[][];
|
||||
colAttrSpans?: number[][];
|
||||
}
|
||||
|
||||
const parseLabel = (value: unknown): string | number => {
|
||||
if (typeof value === 'string') {
|
||||
if (value === 'metric') return t('metric');
|
||||
return value;
|
||||
@@ -38,21 +137,21 @@ const parseLabel = value => {
|
||||
return String(value);
|
||||
};
|
||||
|
||||
function displayCell(value, allowRenderHtml) {
|
||||
function displayCell(value: unknown, allowRenderHtml?: boolean): ReactNode {
|
||||
if (allowRenderHtml && typeof value === 'string') {
|
||||
return safeHtmlSpan(value);
|
||||
}
|
||||
return parseLabel(value);
|
||||
}
|
||||
function displayHeaderCell(
|
||||
needToggle,
|
||||
ArrowIcon,
|
||||
onArrowClick,
|
||||
value,
|
||||
namesMapping,
|
||||
allowRenderHtml,
|
||||
) {
|
||||
const name = namesMapping[value] || value;
|
||||
needToggle: boolean,
|
||||
ArrowIcon: ReactNode,
|
||||
onArrowClick: ((e: MouseEvent<HTMLSpanElement>) => void) | null,
|
||||
value: unknown,
|
||||
namesMapping: Record<string, string>,
|
||||
allowRenderHtml?: boolean,
|
||||
): ReactNode {
|
||||
const name = namesMapping[String(value)] || value;
|
||||
const parsedLabel = parseLabel(name);
|
||||
const labelContent =
|
||||
allowRenderHtml && typeof parsedLabel === 'string'
|
||||
@@ -62,9 +161,9 @@ function displayHeaderCell(
|
||||
<span className="toggle-wrapper">
|
||||
<span
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
tabIndex={0}
|
||||
className="toggle"
|
||||
onClick={onArrowClick}
|
||||
onClick={onArrowClick || undefined}
|
||||
>
|
||||
{ArrowIcon}
|
||||
</span>
|
||||
@@ -75,7 +174,16 @@ function displayHeaderCell(
|
||||
);
|
||||
}
|
||||
|
||||
function sortHierarchicalObject(obj, objSort, rowPartialOnTop) {
|
||||
interface HierarchicalNode {
|
||||
currentVal?: number;
|
||||
[key: string]: HierarchicalNode | number | undefined;
|
||||
}
|
||||
|
||||
function sortHierarchicalObject(
|
||||
obj: Record<string, HierarchicalNode>,
|
||||
objSort: string,
|
||||
rowPartialOnTop: boolean | undefined,
|
||||
): Map<string, unknown> {
|
||||
// Performs a recursive sort of nested object structures. Sorts objects based on
|
||||
// their currentVal property. The function preserves the hierarchical structure
|
||||
// while sorting each level according to the specified criteria.
|
||||
@@ -93,11 +201,18 @@ function sortHierarchicalObject(obj, objSort, rowPartialOnTop) {
|
||||
return objSort === 'asc' ? valA - valB : valB - valA;
|
||||
});
|
||||
|
||||
const result = new Map();
|
||||
const result = new Map<string, unknown>();
|
||||
sortedKeys.forEach(key => {
|
||||
const value = obj[key];
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
result.set(key, sortHierarchicalObject(value, objSort, rowPartialOnTop));
|
||||
result.set(
|
||||
key,
|
||||
sortHierarchicalObject(
|
||||
value as Record<string, HierarchicalNode>,
|
||||
objSort,
|
||||
rowPartialOnTop,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
result.set(key, value);
|
||||
}
|
||||
@@ -106,21 +221,21 @@ function sortHierarchicalObject(obj, objSort, rowPartialOnTop) {
|
||||
}
|
||||
|
||||
function convertToArray(
|
||||
obj,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
parentKeys = [],
|
||||
result = [],
|
||||
obj: Map<string, unknown>,
|
||||
rowEnabled: boolean | undefined,
|
||||
rowPartialOnTop: boolean | undefined,
|
||||
maxRowIndex: number,
|
||||
parentKeys: string[] = [],
|
||||
result: string[][] = [],
|
||||
flag = false,
|
||||
) {
|
||||
): string[][] {
|
||||
// Recursively flattens a hierarchical Map structure into an array of key paths.
|
||||
// Handles different rendering scenarios based on row grouping configurations and
|
||||
// depth limitations. The function supports complex hierarchy flattening with
|
||||
let updatedFlag = flag;
|
||||
|
||||
const keys = Array.from(obj.keys());
|
||||
const getValue = key => obj.get(key);
|
||||
const getValue = (key: string) => obj.get(key);
|
||||
|
||||
keys.forEach(key => {
|
||||
if (key === 'currentVal') {
|
||||
@@ -131,9 +246,9 @@ function convertToArray(
|
||||
result.push(parentKeys.length > 0 ? [...parentKeys, key] : [key]);
|
||||
updatedFlag = true;
|
||||
}
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
convertToArray(
|
||||
value,
|
||||
value as Map<string, unknown>,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
@@ -157,8 +272,18 @@ function convertToArray(
|
||||
return result;
|
||||
}
|
||||
|
||||
export class TableRenderer extends Component {
|
||||
constructor(props) {
|
||||
export class TableRenderer extends Component<
|
||||
TableRendererProps,
|
||||
TableRendererState
|
||||
> {
|
||||
sortCache: Map<string, string[][]>;
|
||||
cachedProps: TableRendererProps | null;
|
||||
cachedBasePivotSettings: PivotSettings | null;
|
||||
|
||||
static propTypes: Record<string, unknown>;
|
||||
static defaultProps: Record<string, unknown>;
|
||||
|
||||
constructor(props: TableRendererProps) {
|
||||
super(props);
|
||||
|
||||
// We need state to record which entries are collapsed and which aren't.
|
||||
@@ -166,18 +291,20 @@ export class TableRenderer extends Component {
|
||||
// should be collapsed.
|
||||
this.state = { collapsedRows: {}, collapsedCols: {}, sortingOrder: [] };
|
||||
this.sortCache = new Map();
|
||||
this.cachedProps = null;
|
||||
this.cachedBasePivotSettings = null;
|
||||
this.clickHeaderHandler = this.clickHeaderHandler.bind(this);
|
||||
this.clickHandler = this.clickHandler.bind(this);
|
||||
}
|
||||
|
||||
getBasePivotSettings() {
|
||||
getBasePivotSettings(): PivotSettings {
|
||||
// One-time extraction of pivot settings that we'll use throughout the render.
|
||||
|
||||
const { props } = this;
|
||||
const colAttrs = props.cols;
|
||||
const rowAttrs = props.rows;
|
||||
|
||||
const tableOptions = {
|
||||
const tableOptions: TableOptions = {
|
||||
rowTotals: true,
|
||||
colTotals: true,
|
||||
...props.tableOptions,
|
||||
@@ -186,27 +313,30 @@ export class TableRenderer extends Component {
|
||||
const colTotals = tableOptions.colTotals || rowAttrs.length === 0;
|
||||
|
||||
const namesMapping = props.namesMapping || {};
|
||||
const subtotalOptions = {
|
||||
const subtotalOptions: Required<
|
||||
Pick<SubtotalOptions, 'arrowCollapsed' | 'arrowExpanded'>
|
||||
> &
|
||||
SubtotalOptions = {
|
||||
arrowCollapsed: '\u25B2',
|
||||
arrowExpanded: '\u25BC',
|
||||
...props.subtotalOptions,
|
||||
};
|
||||
|
||||
const colSubtotalDisplay = {
|
||||
const colSubtotalDisplay: SubtotalDisplay = {
|
||||
displayOnTop: false,
|
||||
enabled: tableOptions.colSubTotals,
|
||||
hideOnExpand: false,
|
||||
...subtotalOptions.colSubtotalDisplay,
|
||||
};
|
||||
|
||||
const rowSubtotalDisplay = {
|
||||
const rowSubtotalDisplay: SubtotalDisplay = {
|
||||
displayOnTop: false,
|
||||
enabled: tableOptions.rowSubTotals,
|
||||
hideOnExpand: false,
|
||||
...subtotalOptions.rowSubtotalDisplay,
|
||||
};
|
||||
|
||||
const pivotData = new PivotData(props, {
|
||||
const pivotData = new PivotData(props as Record<string, unknown>, {
|
||||
rowEnabled: rowSubtotalDisplay.enabled,
|
||||
colEnabled: colSubtotalDisplay.enabled,
|
||||
rowPartialOnTop: rowSubtotalDisplay.displayOnTop,
|
||||
@@ -217,10 +347,13 @@ export class TableRenderer extends Component {
|
||||
|
||||
// Also pre-calculate all the callbacks for cells, etc... This is nice to have to
|
||||
// avoid re-calculations of the call-backs on cell expansions, etc...
|
||||
const cellCallbacks = {};
|
||||
const rowTotalCallbacks = {};
|
||||
const colTotalCallbacks = {};
|
||||
let grandTotalCallback = null;
|
||||
const cellCallbacks: Record<
|
||||
string,
|
||||
Record<string, (e: MouseEvent) => void>
|
||||
> = {};
|
||||
const rowTotalCallbacks: Record<string, (e: MouseEvent) => void> = {};
|
||||
const colTotalCallbacks: Record<string, (e: MouseEvent) => void> = {};
|
||||
let grandTotalCallback: ((e: MouseEvent) => void) | null = null;
|
||||
if (tableOptions.clickCallback) {
|
||||
rowKeys.forEach(rowKey => {
|
||||
const flatRowKey = flatKey(rowKey);
|
||||
@@ -281,11 +414,15 @@ export class TableRenderer extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
clickHandler(pivotData, rowValues, colValues) {
|
||||
clickHandler(
|
||||
pivotData: InstanceType<typeof PivotData>,
|
||||
rowValues: string[],
|
||||
colValues: string[],
|
||||
) {
|
||||
const colAttrs = this.props.cols;
|
||||
const rowAttrs = this.props.rows;
|
||||
const value = pivotData.getAggregator(rowValues, colValues).value();
|
||||
const filters = {};
|
||||
const filters: Record<string, string> = {};
|
||||
const colLimit = Math.min(colAttrs.length, colValues.length);
|
||||
for (let i = 0; i < colLimit; i += 1) {
|
||||
const attr = colAttrs[i];
|
||||
@@ -300,26 +437,26 @@ export class TableRenderer extends Component {
|
||||
filters[attr] = rowValues[i];
|
||||
}
|
||||
}
|
||||
return e =>
|
||||
this.props.tableOptions.clickCallback(e, value, filters, pivotData);
|
||||
const { clickCallback } = this.props.tableOptions;
|
||||
return (e: MouseEvent) => clickCallback?.(e, value, filters, pivotData);
|
||||
}
|
||||
|
||||
clickHeaderHandler(
|
||||
pivotData,
|
||||
values,
|
||||
attrs,
|
||||
attrIdx,
|
||||
callback,
|
||||
pivotData: InstanceType<typeof PivotData>,
|
||||
values: string[],
|
||||
attrs: string[],
|
||||
attrIdx: number,
|
||||
callback: HeaderClickCallback | undefined,
|
||||
isSubtotal = false,
|
||||
isGrandTotal = false,
|
||||
) {
|
||||
const filters = {};
|
||||
const filters: Record<string, string> = {};
|
||||
for (let i = 0; i <= attrIdx; i += 1) {
|
||||
const attr = attrs[i];
|
||||
filters[attr] = values[i];
|
||||
}
|
||||
return e =>
|
||||
callback(
|
||||
return (e: MouseEvent) =>
|
||||
callback?.(
|
||||
e,
|
||||
values[attrIdx],
|
||||
filters,
|
||||
@@ -329,15 +466,17 @@ export class TableRenderer extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
collapseAttr(rowOrCol, attrIdx, allKeys) {
|
||||
return e => {
|
||||
collapseAttr(rowOrCol: boolean, attrIdx: number, allKeys: string[][]) {
|
||||
return (e: MouseEvent<HTMLSpanElement>) => {
|
||||
// Collapse an entire attribute.
|
||||
e.stopPropagation();
|
||||
const keyLen = attrIdx + 1;
|
||||
const collapsed = allKeys.filter(k => k.length === keyLen).map(flatKey);
|
||||
const collapsed = allKeys
|
||||
.filter((k: string[]) => k.length === keyLen)
|
||||
.map(flatKey);
|
||||
|
||||
const updates = {};
|
||||
collapsed.forEach(k => {
|
||||
const updates: Record<string, boolean> = {};
|
||||
collapsed.forEach((k: string) => {
|
||||
updates[k] = true;
|
||||
});
|
||||
|
||||
@@ -353,13 +492,13 @@ export class TableRenderer extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
expandAttr(rowOrCol, attrIdx, allKeys) {
|
||||
return e => {
|
||||
expandAttr(rowOrCol: boolean, attrIdx: number, allKeys: string[][]) {
|
||||
return (e: MouseEvent<HTMLSpanElement>) => {
|
||||
// Expand an entire attribute. This implicitly implies expanding all of the
|
||||
// parents as well. It's a bit inefficient but ah well...
|
||||
e.stopPropagation();
|
||||
const updates = {};
|
||||
allKeys.forEach(k => {
|
||||
const updates: Record<string, boolean> = {};
|
||||
allKeys.forEach((k: string[]) => {
|
||||
for (let i = 0; i <= attrIdx; i += 1) {
|
||||
updates[flatKey(k.slice(0, i + 1))] = false;
|
||||
}
|
||||
@@ -377,8 +516,8 @@ export class TableRenderer extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
toggleRowKey(flatRowKey) {
|
||||
return e => {
|
||||
toggleRowKey(flatRowKey: string) {
|
||||
return (e: MouseEvent<HTMLSpanElement>) => {
|
||||
e.stopPropagation();
|
||||
this.setState(state => ({
|
||||
collapsedRows: {
|
||||
@@ -389,8 +528,8 @@ export class TableRenderer extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
toggleColKey(flatColKey) {
|
||||
return e => {
|
||||
toggleColKey(flatColKey: string) {
|
||||
return (e: MouseEvent<HTMLSpanElement>) => {
|
||||
e.stopPropagation();
|
||||
this.setState(state => ({
|
||||
collapsedCols: {
|
||||
@@ -401,7 +540,7 @@ export class TableRenderer extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
calcAttrSpans(attrArr, numAttrs) {
|
||||
calcAttrSpans(attrArr: string[][], numAttrs: number) {
|
||||
// Given an array of attribute values (i.e. each element is another array with
|
||||
// the value at every level), compute the spans for every attribute value at
|
||||
// every level. The return value is a nested array of the same shape. It has
|
||||
@@ -410,7 +549,7 @@ export class TableRenderer extends Component {
|
||||
const spans = [];
|
||||
// Index of the last new value
|
||||
const li = Array(numAttrs).map(() => 0);
|
||||
let lv = Array(numAttrs).map(() => null);
|
||||
let lv: (string | null)[] = Array(numAttrs).map(() => null);
|
||||
for (let i = 0; i < attrArr.length; i += 1) {
|
||||
// Keep increasing span values as long as the last keys are the same. For
|
||||
// the rest, record spans of 1. Update the indices too.
|
||||
@@ -434,12 +573,16 @@ export class TableRenderer extends Component {
|
||||
return spans;
|
||||
}
|
||||
|
||||
getAggregatedData(pivotData, visibleColName, rowPartialOnTop) {
|
||||
getAggregatedData(
|
||||
pivotData: InstanceType<typeof PivotData>,
|
||||
visibleColName: string[],
|
||||
rowPartialOnTop: boolean | undefined,
|
||||
) {
|
||||
// Transforms flat row keys into a hierarchical group structure where each level
|
||||
// represents a grouping dimension. For each row key path, it calculates the
|
||||
// aggregated value for the specified column and builds a nested object that
|
||||
// preserves the hierarchy while storing aggregation values at each level.
|
||||
const groups = {};
|
||||
const groups: Record<string, HierarchicalNode> = {};
|
||||
const rows = pivotData.rowKeys;
|
||||
rows.forEach(rowKey => {
|
||||
const aggValue =
|
||||
@@ -448,13 +591,17 @@ export class TableRenderer extends Component {
|
||||
if (rowPartialOnTop) {
|
||||
const parent = rowKey
|
||||
.slice(0, -1)
|
||||
.reduce((acc, key) => (acc[key] ??= {}), groups);
|
||||
parent[rowKey.at(-1)] = { currentVal: aggValue };
|
||||
.reduce(
|
||||
(acc: Record<string, HierarchicalNode>, key: string) =>
|
||||
(acc[key] ??= {}) as Record<string, HierarchicalNode>,
|
||||
groups,
|
||||
);
|
||||
parent[rowKey.at(-1)!] = { currentVal: aggValue as number };
|
||||
} else {
|
||||
rowKey.reduce((acc, key) => {
|
||||
rowKey.reduce((acc: Record<string, HierarchicalNode>, key: string) => {
|
||||
acc[key] = acc[key] || { currentVal: 0 };
|
||||
acc[key].currentVal = aggValue;
|
||||
return acc[key];
|
||||
(acc[key] as HierarchicalNode).currentVal = aggValue as number;
|
||||
return acc[key] as Record<string, HierarchicalNode>;
|
||||
}, groups);
|
||||
}
|
||||
});
|
||||
@@ -462,11 +609,11 @@ export class TableRenderer extends Component {
|
||||
}
|
||||
|
||||
sortAndCacheData(
|
||||
groups,
|
||||
sortOrder,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
groups: Record<string, HierarchicalNode>,
|
||||
sortOrder: string,
|
||||
rowEnabled: boolean | undefined,
|
||||
rowPartialOnTop: boolean | undefined,
|
||||
maxRowIndex: number,
|
||||
) {
|
||||
// Processes hierarchical data by first sorting it according to the specified order
|
||||
// and then converting the sorted structure into a flat array format. This function
|
||||
@@ -485,7 +632,12 @@ export class TableRenderer extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex) {
|
||||
sortData(
|
||||
columnIndex: number,
|
||||
visibleColKeys: string[][],
|
||||
pivotData: InstanceType<typeof PivotData>,
|
||||
maxRowIndex: number,
|
||||
) {
|
||||
// Handles column sorting with direction toggling (asc/desc) and implements
|
||||
// caching mechanism to avoid redundant sorting operations. When sorting the same
|
||||
// column multiple times, it cycles through sorting directions. Uses composite
|
||||
@@ -500,7 +652,10 @@ export class TableRenderer extends Component {
|
||||
newDirection = sortingOrder[columnIndex] === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
const { rowEnabled, rowPartialOnTop } = pivotData.subtotals;
|
||||
const { rowEnabled, rowPartialOnTop } = pivotData.subtotals as {
|
||||
rowEnabled?: boolean;
|
||||
rowPartialOnTop?: boolean;
|
||||
};
|
||||
newSortingOrder[columnIndex] = newDirection;
|
||||
|
||||
const cacheKey = `${columnIndex}-${visibleColKeys.length}-${rowEnabled}-${rowPartialOnTop}-${newDirection}`;
|
||||
@@ -525,8 +680,8 @@ export class TableRenderer extends Component {
|
||||
newRowKeys = sortedRowKeys;
|
||||
}
|
||||
this.cachedBasePivotSettings = {
|
||||
...this.cachedBasePivotSettings,
|
||||
rowKeys: newRowKeys,
|
||||
...this.cachedBasePivotSettings!,
|
||||
rowKeys: newRowKeys!,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -536,7 +691,11 @@ export class TableRenderer extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
renderColHeaderRow(attrName, attrIdx, pivotSettings) {
|
||||
renderColHeaderRow(
|
||||
attrName: string,
|
||||
attrIdx: number,
|
||||
pivotSettings: PivotSettings,
|
||||
) {
|
||||
// Render a single row in the column header at the top of the pivot table.
|
||||
|
||||
const {
|
||||
@@ -561,6 +720,10 @@ export class TableRenderer extends Component {
|
||||
dateFormatters,
|
||||
} = this.props.tableOptions;
|
||||
|
||||
if (!visibleColKeys || !colAttrSpans) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spaceCell =
|
||||
attrIdx === 0 && rowAttrs.length !== 0 ? (
|
||||
<th
|
||||
@@ -572,15 +735,15 @@ export class TableRenderer extends Component {
|
||||
) : null;
|
||||
|
||||
const needToggle =
|
||||
colSubtotalDisplay.enabled && attrIdx !== colAttrs.length - 1;
|
||||
colSubtotalDisplay.enabled === true && attrIdx !== colAttrs.length - 1;
|
||||
let arrowClickHandle = null;
|
||||
let subArrow = null;
|
||||
if (needToggle) {
|
||||
arrowClickHandle =
|
||||
attrIdx + 1 < maxColVisible
|
||||
attrIdx + 1 < maxColVisible!
|
||||
? this.collapseAttr(false, attrIdx, colKeys)
|
||||
: this.expandAttr(false, attrIdx, colKeys);
|
||||
subArrow = attrIdx + 1 < maxColVisible ? arrowExpanded : arrowCollapsed;
|
||||
subArrow = attrIdx + 1 < maxColVisible! ? arrowExpanded : arrowCollapsed;
|
||||
}
|
||||
const attrNameCell = (
|
||||
<th key="label" className="pvtAxisLabel">
|
||||
@@ -600,7 +763,7 @@ export class TableRenderer extends Component {
|
||||
// Iterate through columns. Jump over duplicate values.
|
||||
let i = 0;
|
||||
while (i < visibleColKeys.length) {
|
||||
let handleContextMenu;
|
||||
let handleContextMenu: ((e: MouseEvent) => void) | undefined;
|
||||
const colKey = visibleColKeys[i];
|
||||
const colSpan = attrIdx < colKey.length ? colAttrSpans[i][attrIdx] : 1;
|
||||
let colLabelClass = 'pvtColLabel';
|
||||
@@ -609,7 +772,7 @@ export class TableRenderer extends Component {
|
||||
if (highlightHeaderCellsOnHover) {
|
||||
colLabelClass += ' hoverable';
|
||||
}
|
||||
handleContextMenu = e =>
|
||||
handleContextMenu = (e: MouseEvent) =>
|
||||
this.props.onContextMenu(e, colKey, undefined, {
|
||||
[attrName]: colKey[attrIdx],
|
||||
});
|
||||
@@ -621,14 +784,15 @@ export class TableRenderer extends Component {
|
||||
) {
|
||||
colLabelClass += ' active';
|
||||
}
|
||||
const { maxRowVisible: maxRowIndex, maxColVisible } = pivotSettings;
|
||||
const visibleSortIcon = maxColVisible - 1 === attrIdx;
|
||||
const columnName = colKey[maxColVisible - 1];
|
||||
const maxRowIndex = pivotSettings.maxRowVisible!;
|
||||
const mColVisible = pivotSettings.maxColVisible!;
|
||||
const visibleSortIcon = mColVisible - 1 === attrIdx;
|
||||
const columnName = colKey[mColVisible - 1];
|
||||
|
||||
const rowSpan = 1 + (attrIdx === colAttrs.length - 1 ? rowIncrSpan : 0);
|
||||
const flatColKey = flatKey(colKey.slice(0, attrIdx + 1));
|
||||
const onArrowClick = needToggle ? this.toggleColKey(flatColKey) : null;
|
||||
const getSortIcon = key => {
|
||||
const getSortIcon = (key: number) => {
|
||||
const { activeSortColumn, sortingOrder } = this.state;
|
||||
|
||||
if (activeSortColumn !== key) {
|
||||
@@ -651,11 +815,7 @@ export class TableRenderer extends Component {
|
||||
);
|
||||
};
|
||||
const headerCellFormattedValue =
|
||||
dateFormatters &&
|
||||
dateFormatters[attrName] &&
|
||||
typeof dateFormatters[attrName] === 'function'
|
||||
? dateFormatters[attrName](colKey[attrIdx])
|
||||
: colKey[attrIdx];
|
||||
dateFormatters?.[attrName]?.(colKey[attrIdx]) ?? colKey[attrIdx];
|
||||
attrValueCells.push(
|
||||
<th
|
||||
className={colLabelClass}
|
||||
@@ -753,7 +913,7 @@ export class TableRenderer extends Component {
|
||||
return <tr key={`colAttr-${attrIdx}`}>{cells}</tr>;
|
||||
}
|
||||
|
||||
renderRowHeaderRow(pivotSettings) {
|
||||
renderRowHeaderRow(pivotSettings: PivotSettings) {
|
||||
// Render just the attribute names of the rows (the actual attribute values
|
||||
// will show up in the individual rows).
|
||||
|
||||
@@ -773,15 +933,15 @@ export class TableRenderer extends Component {
|
||||
<tr key="rowHdr">
|
||||
{rowAttrs.map((r, i) => {
|
||||
const needLabelToggle =
|
||||
rowSubtotalDisplay.enabled && i !== rowAttrs.length - 1;
|
||||
rowSubtotalDisplay.enabled === true && i !== rowAttrs.length - 1;
|
||||
let arrowClickHandle = null;
|
||||
let subArrow = null;
|
||||
if (needLabelToggle) {
|
||||
arrowClickHandle =
|
||||
i + 1 < maxRowVisible
|
||||
i + 1 < maxRowVisible!
|
||||
? this.collapseAttr(true, i, rowKeys)
|
||||
: this.expandAttr(true, i, rowKeys);
|
||||
subArrow = i + 1 < maxRowVisible ? arrowExpanded : arrowCollapsed;
|
||||
subArrow = i + 1 < maxRowVisible! ? arrowExpanded : arrowCollapsed;
|
||||
}
|
||||
return (
|
||||
<th className="pvtAxisLabel" key={`rowAttr-${i}`}>
|
||||
@@ -820,7 +980,11 @@ export class TableRenderer extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderTableRow(rowKey, rowIdx, pivotSettings) {
|
||||
renderTableRow(
|
||||
rowKey: string[],
|
||||
rowIdx: number,
|
||||
pivotSettings: PivotSettings,
|
||||
) {
|
||||
// Render a single row in the pivot table.
|
||||
|
||||
const {
|
||||
@@ -849,14 +1013,14 @@ export class TableRenderer extends Component {
|
||||
const flatRowKey = flatKey(rowKey);
|
||||
|
||||
const colIncrSpan = colAttrs.length !== 0 ? 1 : 0;
|
||||
const attrValueCells = rowKey.map((r, i) => {
|
||||
let handleContextMenu;
|
||||
const attrValueCells = rowKey.map((r: string, i: number) => {
|
||||
let handleContextMenu: ((e: MouseEvent) => void) | undefined;
|
||||
let valueCellClassName = 'pvtRowLabel';
|
||||
if (!omittedHighlightHeaderGroups.includes(rowAttrs[i])) {
|
||||
if (highlightHeaderCellsOnHover) {
|
||||
valueCellClassName += ' hoverable';
|
||||
}
|
||||
handleContextMenu = e =>
|
||||
handleContextMenu = (e: MouseEvent) =>
|
||||
this.props.onContextMenu(e, undefined, rowKey, {
|
||||
[rowAttrs[i]]: r,
|
||||
});
|
||||
@@ -868,20 +1032,18 @@ export class TableRenderer extends Component {
|
||||
) {
|
||||
valueCellClassName += ' active';
|
||||
}
|
||||
const rowSpan = rowAttrSpans[rowIdx][i];
|
||||
const rowSpan = rowAttrSpans![rowIdx][i];
|
||||
if (rowSpan > 0) {
|
||||
const flatRowKey = flatKey(rowKey.slice(0, i + 1));
|
||||
const colSpan = 1 + (i === rowAttrs.length - 1 ? colIncrSpan : 0);
|
||||
const needRowToggle =
|
||||
rowSubtotalDisplay.enabled && i !== rowAttrs.length - 1;
|
||||
rowSubtotalDisplay.enabled === true && i !== rowAttrs.length - 1;
|
||||
const onArrowClick = needRowToggle
|
||||
? this.toggleRowKey(flatRowKey)
|
||||
: null;
|
||||
|
||||
const headerCellFormattedValue =
|
||||
dateFormatters && dateFormatters[rowAttrs[i]]
|
||||
? dateFormatters[rowAttrs[i]](r)
|
||||
: r;
|
||||
dateFormatters?.[rowAttrs[i]]?.(r) ?? r;
|
||||
return (
|
||||
<th
|
||||
key={`rowKeyLabel-${i}`}
|
||||
@@ -935,14 +1097,18 @@ export class TableRenderer extends Component {
|
||||
</th>
|
||||
) : null;
|
||||
|
||||
if (!visibleColKeys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowClickHandlers = cellCallbacks[flatRowKey] || {};
|
||||
const valueCells = visibleColKeys.map(colKey => {
|
||||
const valueCells = visibleColKeys.map((colKey: string[]) => {
|
||||
const flatColKey = flatKey(colKey);
|
||||
const agg = pivotData.getAggregator(rowKey, colKey);
|
||||
const aggValue = agg.value();
|
||||
|
||||
const keys = [...rowKey, ...colKey];
|
||||
let backgroundColor;
|
||||
let backgroundColor: string | undefined;
|
||||
if (cellColorFormatters) {
|
||||
Object.values(cellColorFormatters).forEach(cellColorFormatter => {
|
||||
if (Array.isArray(cellColorFormatter)) {
|
||||
@@ -1008,7 +1174,7 @@ export class TableRenderer extends Component {
|
||||
return <tr key={`keyRow-${flatRowKey}`}>{rowCells}</tr>;
|
||||
}
|
||||
|
||||
renderTotalsRow(pivotSettings) {
|
||||
renderTotalsRow(pivotSettings: PivotSettings) {
|
||||
// Render the final totals rows that has the totals for all the columns.
|
||||
|
||||
const {
|
||||
@@ -1021,6 +1187,10 @@ export class TableRenderer extends Component {
|
||||
grandTotalCallback,
|
||||
} = pivotSettings;
|
||||
|
||||
if (!visibleColKeys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalLabelCell = (
|
||||
<th
|
||||
key="label"
|
||||
@@ -1043,7 +1213,7 @@ export class TableRenderer extends Component {
|
||||
</th>
|
||||
);
|
||||
|
||||
const totalValueCells = visibleColKeys.map(colKey => {
|
||||
const totalValueCells = visibleColKeys.map((colKey: string[]) => {
|
||||
const flatColKey = flatKey(colKey);
|
||||
const agg = pivotData.getAggregator([], colKey);
|
||||
const aggValue = agg.value();
|
||||
@@ -1071,7 +1241,7 @@ export class TableRenderer extends Component {
|
||||
role="gridcell"
|
||||
key="total"
|
||||
className="pvtGrandTotal pvtRowTotal"
|
||||
onClick={grandTotalCallback}
|
||||
onClick={grandTotalCallback || undefined}
|
||||
onContextMenu={e => this.props.onContextMenu(e, undefined, undefined)}
|
||||
>
|
||||
{displayCell(agg.format(aggValue, agg), this.props.allowRenderHtml)}
|
||||
@@ -1088,11 +1258,18 @@ export class TableRenderer extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
visibleKeys(keys, collapsed, numAttrs, subtotalDisplay) {
|
||||
visibleKeys(
|
||||
keys: string[][],
|
||||
collapsed: Record<string, boolean>,
|
||||
numAttrs: number,
|
||||
subtotalDisplay: SubtotalDisplay,
|
||||
) {
|
||||
return keys.filter(
|
||||
key =>
|
||||
(key: string[]) =>
|
||||
// Is the key hidden by one of its parents?
|
||||
!key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) &&
|
||||
!key.some(
|
||||
(_k: string, j: number) => collapsed[flatKey(key.slice(0, j))],
|
||||
) &&
|
||||
// Leaf key.
|
||||
(key.length === numAttrs ||
|
||||
// Children hidden. Must show total.
|
||||
@@ -1113,11 +1290,14 @@ export class TableRenderer extends Component {
|
||||
render() {
|
||||
if (this.cachedProps !== this.props) {
|
||||
this.sortCache.clear();
|
||||
this.state.sortingOrder = [];
|
||||
this.state.activeSortColumn = null;
|
||||
// Reset sort state without using setState to avoid re-render during render.
|
||||
// This is safe because the state is being synchronized with new props.
|
||||
(this.state as TableRendererState).sortingOrder = [];
|
||||
(this.state as TableRendererState).activeSortColumn = null;
|
||||
this.cachedProps = this.props;
|
||||
this.cachedBasePivotSettings = this.getBasePivotSettings();
|
||||
}
|
||||
const basePivotSettings = this.cachedBasePivotSettings!;
|
||||
const {
|
||||
colAttrs,
|
||||
rowAttrs,
|
||||
@@ -1127,7 +1307,7 @@ export class TableRenderer extends Component {
|
||||
rowSubtotalDisplay,
|
||||
colSubtotalDisplay,
|
||||
allowRenderHtml,
|
||||
} = this.cachedBasePivotSettings;
|
||||
} = basePivotSettings;
|
||||
|
||||
// Need to account for exclusions to compute the effective row
|
||||
// and column keys.
|
||||
@@ -1144,28 +1324,28 @@ export class TableRenderer extends Component {
|
||||
colSubtotalDisplay,
|
||||
);
|
||||
|
||||
const pivotSettings = {
|
||||
const pivotSettings: PivotSettings = {
|
||||
visibleRowKeys,
|
||||
maxRowVisible: Math.max(...visibleRowKeys.map(k => k.length)),
|
||||
maxRowVisible: Math.max(...visibleRowKeys.map((k: string[]) => k.length)),
|
||||
visibleColKeys,
|
||||
maxColVisible: Math.max(...visibleColKeys.map(k => k.length)),
|
||||
maxColVisible: Math.max(...visibleColKeys.map((k: string[]) => k.length)),
|
||||
rowAttrSpans: this.calcAttrSpans(visibleRowKeys, rowAttrs.length),
|
||||
colAttrSpans: this.calcAttrSpans(visibleColKeys, colAttrs.length),
|
||||
allowRenderHtml,
|
||||
...this.cachedBasePivotSettings,
|
||||
...basePivotSettings,
|
||||
};
|
||||
|
||||
return (
|
||||
<Styles isDashboardEditMode={this.isDashboardEditMode()}>
|
||||
<table className="pvtTable" role="grid">
|
||||
<thead>
|
||||
{colAttrs.map((c, j) =>
|
||||
{colAttrs.map((c: string, j: number) =>
|
||||
this.renderColHeaderRow(c, j, pivotSettings),
|
||||
)}
|
||||
{rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleRowKeys.map((r, i) =>
|
||||
{visibleRowKeys.map((r: string[], i: number) =>
|
||||
this.renderTableRow(r, i, pivotSettings),
|
||||
)}
|
||||
{colTotals && this.renderTotalsRow(pivotSettings)}
|
||||
@@ -20,7 +20,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@apache-superset/core/ui';
|
||||
|
||||
const addSeparators = function (nStr, thousandsSep, decimalSep) {
|
||||
type SortFunction = (
|
||||
a: string | number | null,
|
||||
b: string | number | null,
|
||||
) => number;
|
||||
type Formatter = (x: number) => string;
|
||||
type PivotRecord = Record<string, string | number | boolean>;
|
||||
|
||||
interface NumberFormatOptions {
|
||||
digitsAfterDecimal?: number;
|
||||
scaler?: number;
|
||||
thousandsSep?: string;
|
||||
decimalSep?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
interface Aggregator {
|
||||
push(record: PivotRecord): void;
|
||||
value(): string | number | null;
|
||||
format(x: string | number | null, agg?: Aggregator): string;
|
||||
numInputs?: number;
|
||||
getCurrencies?(): string[];
|
||||
isSubtotal?: boolean;
|
||||
isRowSubtotal?: boolean;
|
||||
isColSubtotal?: boolean;
|
||||
}
|
||||
|
||||
interface SubtotalOptions {
|
||||
rowEnabled?: boolean;
|
||||
colEnabled?: boolean;
|
||||
rowPartialOnTop?: boolean;
|
||||
colPartialOnTop?: boolean;
|
||||
}
|
||||
|
||||
const addSeparators = function (
|
||||
nStr: string,
|
||||
thousandsSep: string,
|
||||
decimalSep: string,
|
||||
): string {
|
||||
const x = String(nStr).split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimalSep + x[1] : '';
|
||||
@@ -31,7 +69,7 @@ const addSeparators = function (nStr, thousandsSep, decimalSep) {
|
||||
return x1 + x2;
|
||||
};
|
||||
|
||||
const numberFormat = function (optsIn) {
|
||||
const numberFormat = function (optsIn?: NumberFormatOptions): Formatter {
|
||||
const defaults = {
|
||||
digitsAfterDecimal: 2,
|
||||
scaler: 1,
|
||||
@@ -41,7 +79,7 @@ const numberFormat = function (optsIn) {
|
||||
suffix: '',
|
||||
};
|
||||
const opts = { ...defaults, ...optsIn };
|
||||
return function (x) {
|
||||
return function (x: number): string {
|
||||
if (Number.isNaN(x) || !Number.isFinite(x)) {
|
||||
return '';
|
||||
}
|
||||
@@ -57,7 +95,7 @@ const numberFormat = function (optsIn) {
|
||||
const rx = /(\d+)|(\D+)/g;
|
||||
const rd = /\d/;
|
||||
const rz = /^0/;
|
||||
const naturalSort = (as, bs) => {
|
||||
const naturalSort: SortFunction = (as, bs) => {
|
||||
// nulls first
|
||||
if (bs !== null && as === null) {
|
||||
return -1;
|
||||
@@ -114,56 +152,68 @@ const naturalSort = (as, bs) => {
|
||||
}
|
||||
|
||||
// special treatment for strings containing digits
|
||||
a = a.match(rx);
|
||||
b = b.match(rx);
|
||||
while (a.length && b.length) {
|
||||
const a1 = a.shift();
|
||||
const b1 = b.shift();
|
||||
const aArr = a.match(rx)!;
|
||||
const bArr = b.match(rx)!;
|
||||
while (aArr.length && bArr.length) {
|
||||
const a1 = aArr.shift()!;
|
||||
const b1 = bArr.shift()!;
|
||||
if (a1 !== b1) {
|
||||
if (rd.test(a1) && rd.test(b1)) {
|
||||
return a1.replace(rz, '.0') - b1.replace(rz, '.0');
|
||||
return Number(a1.replace(rz, '.0')) - Number(b1.replace(rz, '.0'));
|
||||
}
|
||||
return a1 > b1 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
return a.length - b.length;
|
||||
return aArr.length - bArr.length;
|
||||
};
|
||||
|
||||
const sortAs = function (order) {
|
||||
const mapping = {};
|
||||
const sortAs = function (order: (string | number)[]): SortFunction {
|
||||
const mapping: Record<string | number, number> = {};
|
||||
|
||||
// sort lowercased keys similarly
|
||||
const lMapping = {};
|
||||
order.forEach((element, i) => {
|
||||
const lMapping: Record<string, number> = {};
|
||||
order.forEach((element: string | number, i: number) => {
|
||||
mapping[element] = i;
|
||||
if (typeof element === 'string') {
|
||||
lMapping[element.toLowerCase()] = i;
|
||||
}
|
||||
});
|
||||
return function (a, b) {
|
||||
if (a in mapping && b in mapping) {
|
||||
return mapping[a] - mapping[b];
|
||||
return function (
|
||||
a: string | number | null,
|
||||
b: string | number | null,
|
||||
): number {
|
||||
const aKey = a !== null ? String(a) : '';
|
||||
const bKey = b !== null ? String(b) : '';
|
||||
if (aKey in mapping && bKey in mapping) {
|
||||
return mapping[aKey] - mapping[bKey];
|
||||
}
|
||||
if (a in mapping) {
|
||||
if (aKey in mapping) {
|
||||
return -1;
|
||||
}
|
||||
if (b in mapping) {
|
||||
if (bKey in mapping) {
|
||||
return 1;
|
||||
}
|
||||
if (a in lMapping && b in lMapping) {
|
||||
return lMapping[a] - lMapping[b];
|
||||
if (aKey in lMapping && bKey in lMapping) {
|
||||
return lMapping[aKey] - lMapping[bKey];
|
||||
}
|
||||
if (a in lMapping) {
|
||||
if (aKey in lMapping) {
|
||||
return -1;
|
||||
}
|
||||
if (b in lMapping) {
|
||||
if (bKey in lMapping) {
|
||||
return 1;
|
||||
}
|
||||
return naturalSort(a, b);
|
||||
};
|
||||
};
|
||||
|
||||
const getSort = function (sorters, attr) {
|
||||
const getSort = function (
|
||||
sorters:
|
||||
| ((attr: string) => SortFunction | undefined)
|
||||
| Record<string, SortFunction>
|
||||
| null
|
||||
| undefined,
|
||||
attr: string,
|
||||
): SortFunction {
|
||||
if (sorters) {
|
||||
if (typeof sorters === 'function') {
|
||||
const sort = sorters(attr);
|
||||
@@ -186,13 +236,16 @@ const usFmtPct = numberFormat({
|
||||
suffix: '%',
|
||||
});
|
||||
|
||||
const fmtNonString = formatter => (x, aggregator) =>
|
||||
typeof x === 'string' ? x : formatter(x, aggregator);
|
||||
const fmtNonString =
|
||||
(formatter: Formatter) =>
|
||||
(x: string | number | null): string =>
|
||||
typeof x === 'string' ? x : formatter(x as number);
|
||||
|
||||
/*
|
||||
* Aggregators track currencies via push() and expose them via getCurrencies()
|
||||
* for per-cell currency detection in AUTO mode.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const baseAggregatorTemplates = {
|
||||
count(formatter = usFmtInt) {
|
||||
return () =>
|
||||
@@ -210,18 +263,23 @@ const baseAggregatorTemplates = {
|
||||
};
|
||||
},
|
||||
|
||||
uniques(fn, formatter = usFmtInt) {
|
||||
return function ([attr]) {
|
||||
uniques(fn: (uniq: any[]) => any, formatter = usFmtInt) {
|
||||
return function ([attr]: string[]) {
|
||||
return function () {
|
||||
return {
|
||||
uniq: [],
|
||||
currencySet: new Set(),
|
||||
push(record) {
|
||||
uniq: [] as any[],
|
||||
currencySet: new Set<string>(),
|
||||
push(record: PivotRecord) {
|
||||
if (!Array.from(this.uniq).includes(record[attr])) {
|
||||
this.uniq.push(record[attr]);
|
||||
}
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
@@ -238,19 +296,24 @@ const baseAggregatorTemplates = {
|
||||
},
|
||||
|
||||
sum(formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function ([attr]: string[]) {
|
||||
return function () {
|
||||
return {
|
||||
sum: 0,
|
||||
currencySet: new Set(),
|
||||
push(record) {
|
||||
sum: 0 as any,
|
||||
currencySet: new Set<string>(),
|
||||
push(record: PivotRecord) {
|
||||
if (Number.isNaN(Number(record[attr]))) {
|
||||
this.sum = record[attr];
|
||||
} else {
|
||||
this.sum += parseFloat(record[attr]);
|
||||
this.sum += parseFloat(String(record[attr]));
|
||||
}
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
@@ -266,17 +329,17 @@ const baseAggregatorTemplates = {
|
||||
};
|
||||
},
|
||||
|
||||
extremes(mode, formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function (data) {
|
||||
extremes(mode: string, formatter = usFmt) {
|
||||
return function ([attr]: string[]) {
|
||||
return function (data: any) {
|
||||
return {
|
||||
val: null,
|
||||
currencySet: new Set(),
|
||||
val: null as any,
|
||||
currencySet: new Set<string>(),
|
||||
sorter: getSort(
|
||||
typeof data !== 'undefined' ? data.sorters : null,
|
||||
attr,
|
||||
),
|
||||
push(record) {
|
||||
push(record: PivotRecord) {
|
||||
const x = record[attr];
|
||||
if (['min', 'max'].includes(mode)) {
|
||||
const coercedValue = Number(x);
|
||||
@@ -288,24 +351,36 @@ const baseAggregatorTemplates = {
|
||||
? x
|
||||
: this.val;
|
||||
} else {
|
||||
this.val = Math[mode](
|
||||
const mathFn = mode === 'min' ? Math.min : Math.max;
|
||||
this.val = mathFn(
|
||||
coercedValue,
|
||||
this.val !== null ? this.val : coercedValue,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
mode === 'first' &&
|
||||
this.sorter(x, this.val !== null ? this.val : x) <= 0
|
||||
this.sorter(
|
||||
x as any,
|
||||
this.val !== null ? this.val : (x as any),
|
||||
) <= 0
|
||||
) {
|
||||
this.val = x;
|
||||
} else if (
|
||||
mode === 'last' &&
|
||||
this.sorter(x, this.val !== null ? this.val : x) >= 0
|
||||
this.sorter(
|
||||
x as any,
|
||||
this.val !== null ? this.val : (x as any),
|
||||
) >= 0
|
||||
) {
|
||||
this.val = x;
|
||||
}
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
@@ -314,7 +389,7 @@ const baseAggregatorTemplates = {
|
||||
getCurrencies() {
|
||||
return Array.from(this.currencySet);
|
||||
},
|
||||
format(x) {
|
||||
format(x: any) {
|
||||
if (typeof x === 'number') {
|
||||
return formatter(x);
|
||||
}
|
||||
@@ -326,24 +401,29 @@ const baseAggregatorTemplates = {
|
||||
};
|
||||
},
|
||||
|
||||
quantile(q, formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
quantile(q: number, formatter = usFmt) {
|
||||
return function ([attr]: string[]) {
|
||||
return function () {
|
||||
return {
|
||||
vals: [],
|
||||
strMap: {},
|
||||
currencySet: new Set(),
|
||||
push(record) {
|
||||
vals: [] as number[],
|
||||
strMap: {} as Record<string, number>,
|
||||
currencySet: new Set<string>(),
|
||||
push(record: PivotRecord) {
|
||||
const val = record[attr];
|
||||
const x = Number(val);
|
||||
|
||||
if (Number.isNaN(x)) {
|
||||
this.strMap[val] = (this.strMap[val] || 0) + 1;
|
||||
this.strMap[String(val)] = (this.strMap[String(val)] || 0) + 1;
|
||||
} else {
|
||||
this.vals.push(x);
|
||||
}
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
@@ -355,16 +435,18 @@ const baseAggregatorTemplates = {
|
||||
}
|
||||
|
||||
if (Object.keys(this.strMap).length) {
|
||||
const values = Object.values(this.strMap).sort((a, b) => a - b);
|
||||
const values = (Object.values(this.strMap) as number[]).sort(
|
||||
(a: number, b: number) => a - b,
|
||||
);
|
||||
const middle = Math.floor(values.length / 2);
|
||||
|
||||
const keys = Object.keys(this.strMap);
|
||||
return keys.length % 2 !== 0
|
||||
? keys[middle]
|
||||
: (keys[middle - 1] + keys[middle]) / 2;
|
||||
: (Number(keys[middle - 1]) + Number(keys[middle])) / 2;
|
||||
}
|
||||
|
||||
this.vals.sort((a, b) => a - b);
|
||||
this.vals.sort((a: number, b: number) => a - b);
|
||||
const i = (this.vals.length - 1) * q;
|
||||
return (this.vals[Math.floor(i)] + this.vals[Math.ceil(i)]) / 2.0;
|
||||
},
|
||||
@@ -379,21 +461,28 @@ const baseAggregatorTemplates = {
|
||||
},
|
||||
|
||||
runningStat(mode = 'mean', ddof = 1, formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function ([attr]: string[]) {
|
||||
return function () {
|
||||
return {
|
||||
n: 0.0,
|
||||
m: 0.0,
|
||||
s: 0.0,
|
||||
strValue: null,
|
||||
currencySet: new Set(),
|
||||
push(record) {
|
||||
strValue: null as string | null,
|
||||
currencySet: new Set<string>(),
|
||||
push(record: PivotRecord) {
|
||||
const x = Number(record[attr]);
|
||||
if (Number.isNaN(x)) {
|
||||
this.strValue =
|
||||
typeof record[attr] === 'string' ? record[attr] : this.strValue;
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
typeof record[attr] === 'string'
|
||||
? (record[attr] as string)
|
||||
: this.strValue;
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -404,8 +493,13 @@ const baseAggregatorTemplates = {
|
||||
const mNew = this.m + (x - this.m) / this.n;
|
||||
this.s += (x - this.m) * (x - mNew);
|
||||
this.m = mNew;
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
@@ -442,21 +536,26 @@ const baseAggregatorTemplates = {
|
||||
},
|
||||
|
||||
sumOverSum(formatter = usFmt) {
|
||||
return function ([num, denom]) {
|
||||
return function ([num, denom]: string[]) {
|
||||
return function () {
|
||||
return {
|
||||
sumNum: 0,
|
||||
sumDenom: 0,
|
||||
currencySet: new Set(),
|
||||
push(record) {
|
||||
currencySet: new Set<string>(),
|
||||
push(record: PivotRecord) {
|
||||
if (!Number.isNaN(Number(record[num]))) {
|
||||
this.sumNum += parseFloat(record[num]);
|
||||
this.sumNum += parseFloat(String(record[num]));
|
||||
}
|
||||
if (!Number.isNaN(Number(record[denom]))) {
|
||||
this.sumDenom += parseFloat(record[denom]);
|
||||
this.sumDenom += parseFloat(String(record[denom]));
|
||||
}
|
||||
if (record.__currencyColumn && record[record.__currencyColumn]) {
|
||||
this.currencySet.add(record[record.__currencyColumn]);
|
||||
if (
|
||||
record.__currencyColumn &&
|
||||
record[record.__currencyColumn as string]
|
||||
) {
|
||||
this.currencySet.add(
|
||||
String(record[record.__currencyColumn as string]),
|
||||
);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
@@ -473,15 +572,19 @@ const baseAggregatorTemplates = {
|
||||
};
|
||||
},
|
||||
|
||||
fractionOf(wrapped, type = 'total', formatter = usFmtPct) {
|
||||
return (...x) =>
|
||||
function (data, rowKey, colKey) {
|
||||
fractionOf(
|
||||
wrapped: (...args: any[]) => any,
|
||||
type = 'total',
|
||||
formatter = usFmtPct,
|
||||
) {
|
||||
return (...x: any[]) =>
|
||||
function (data: any, rowKey: any, colKey: any) {
|
||||
return {
|
||||
selector: { total: [[], []], row: [rowKey, []], col: [[], colKey] }[
|
||||
type
|
||||
],
|
||||
inner: wrapped(...Array.from(x || []))(data, rowKey, colKey),
|
||||
push(record) {
|
||||
push(record: PivotRecord) {
|
||||
this.inner.push(record);
|
||||
},
|
||||
format: fmtNonString(formatter),
|
||||
@@ -504,36 +607,40 @@ const baseAggregatorTemplates = {
|
||||
};
|
||||
},
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
const extendedAggregatorTemplates = {
|
||||
countUnique(f) {
|
||||
return baseAggregatorTemplates.uniques(x => x.length, f);
|
||||
countUnique(f?: Formatter) {
|
||||
return baseAggregatorTemplates.uniques((x: unknown[]) => x.length, f);
|
||||
},
|
||||
listUnique(s, f) {
|
||||
return baseAggregatorTemplates.uniques(x => x.join(s), f || (x => x));
|
||||
listUnique(s: string, f?: Formatter) {
|
||||
return baseAggregatorTemplates.uniques(
|
||||
(x: unknown[]) => x.join(s),
|
||||
f || (((x: unknown) => x) as unknown as Formatter),
|
||||
);
|
||||
},
|
||||
max(f) {
|
||||
max(f?: Formatter) {
|
||||
return baseAggregatorTemplates.extremes('max', f);
|
||||
},
|
||||
min(f) {
|
||||
min(f?: Formatter) {
|
||||
return baseAggregatorTemplates.extremes('min', f);
|
||||
},
|
||||
first(f) {
|
||||
first(f?: Formatter) {
|
||||
return baseAggregatorTemplates.extremes('first', f);
|
||||
},
|
||||
last(f) {
|
||||
last(f?: Formatter) {
|
||||
return baseAggregatorTemplates.extremes('last', f);
|
||||
},
|
||||
median(f) {
|
||||
median(f?: Formatter) {
|
||||
return baseAggregatorTemplates.quantile(0.5, f);
|
||||
},
|
||||
average(f) {
|
||||
average(f?: Formatter) {
|
||||
return baseAggregatorTemplates.runningStat('mean', 1, f);
|
||||
},
|
||||
var(ddof, f) {
|
||||
var(ddof: number, f?: Formatter) {
|
||||
return baseAggregatorTemplates.runningStat('var', ddof, f);
|
||||
},
|
||||
stdev(ddof, f) {
|
||||
stdev(ddof: number, f?: Formatter) {
|
||||
return baseAggregatorTemplates.runningStat('stdev', ddof, f);
|
||||
},
|
||||
};
|
||||
@@ -603,63 +710,92 @@ const mthNamesEn = [
|
||||
'Dec',
|
||||
];
|
||||
const dayNamesEn = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const zeroPad = number => `0${number}`.substr(-2, 2); // eslint-disable-line no-magic-numbers
|
||||
const zeroPad = (number: number): string => `0${number}`.substr(-2, 2); // eslint-disable-line no-magic-numbers
|
||||
|
||||
const derivers = {
|
||||
bin(col, binWidth) {
|
||||
return record => record[col] - (record[col] % binWidth);
|
||||
bin(col: string, binWidth: number) {
|
||||
return (record: PivotRecord) =>
|
||||
(record[col] as number) - ((record[col] as number) % binWidth);
|
||||
},
|
||||
dateFormat(
|
||||
col,
|
||||
formatString,
|
||||
col: string,
|
||||
formatString: string,
|
||||
utcOutput = false,
|
||||
mthNames = mthNamesEn,
|
||||
dayNames = dayNamesEn,
|
||||
) {
|
||||
const utc = utcOutput ? 'UTC' : '';
|
||||
return function (record) {
|
||||
const date = new Date(Date.parse(record[col]));
|
||||
if (Number.isNaN(date)) {
|
||||
return function (record: PivotRecord) {
|
||||
const date = new Date(Date.parse(String(record[col])));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
return formatString.replace(/%(.)/g, function (m, p) {
|
||||
switch (p) {
|
||||
case 'y':
|
||||
return date[`get${utc}FullYear`]();
|
||||
case 'm':
|
||||
return zeroPad(date[`get${utc}Month`]() + 1);
|
||||
case 'n':
|
||||
return mthNames[date[`get${utc}Month`]()];
|
||||
case 'd':
|
||||
return zeroPad(date[`get${utc}Date`]());
|
||||
case 'w':
|
||||
return dayNames[date[`get${utc}Day`]()];
|
||||
case 'x':
|
||||
return date[`get${utc}Day`]();
|
||||
case 'H':
|
||||
return zeroPad(date[`get${utc}Hours`]());
|
||||
case 'M':
|
||||
return zeroPad(date[`get${utc}Minutes`]());
|
||||
case 'S':
|
||||
return zeroPad(date[`get${utc}Seconds`]());
|
||||
default:
|
||||
return `%${p}`;
|
||||
}
|
||||
});
|
||||
return formatString.replace(
|
||||
/%(.)/g,
|
||||
function (m: string, p: string): string {
|
||||
switch (p) {
|
||||
case 'y':
|
||||
return String(date[`get${utc}FullYear`]());
|
||||
case 'm':
|
||||
return zeroPad(date[`get${utc}Month`]() + 1);
|
||||
case 'n':
|
||||
return mthNames[date[`get${utc}Month`]()];
|
||||
case 'd':
|
||||
return zeroPad(date[`get${utc}Date`]());
|
||||
case 'w':
|
||||
return dayNames[date[`get${utc}Day`]()];
|
||||
case 'x':
|
||||
return String(date[`get${utc}Day`]());
|
||||
case 'H':
|
||||
return zeroPad(date[`get${utc}Hours`]());
|
||||
case 'M':
|
||||
return zeroPad(date[`get${utc}Minutes`]());
|
||||
case 'S':
|
||||
return zeroPad(date[`get${utc}Seconds`]());
|
||||
default:
|
||||
return `%${p}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Given an array of attribute values, convert to a key that
|
||||
// can be used in objects.
|
||||
const flatKey = attrVals => attrVals.join(String.fromCharCode(0));
|
||||
const flatKey = (attrVals: string[]): string =>
|
||||
attrVals.join(String.fromCharCode(0));
|
||||
|
||||
/*
|
||||
Data Model class
|
||||
*/
|
||||
|
||||
class PivotData {
|
||||
constructor(inputProps = {}, subtotals = {}) {
|
||||
props: Record<string, unknown>;
|
||||
aggregator: (...args: unknown[]) => Aggregator;
|
||||
formattedAggregators:
|
||||
| Record<string, Record<string, (...args: unknown[]) => Aggregator>>
|
||||
| false;
|
||||
tree: Record<string, Record<string, Aggregator>>;
|
||||
rowKeys: string[][];
|
||||
colKeys: string[][];
|
||||
rowTotals: Record<string, Aggregator>;
|
||||
colTotals: Record<string, Aggregator>;
|
||||
allTotal: Aggregator;
|
||||
subtotals: SubtotalOptions;
|
||||
sorted: boolean;
|
||||
|
||||
static forEachRecord: (
|
||||
input: unknown,
|
||||
processRecord: (record: PivotRecord) => void,
|
||||
) => void;
|
||||
static defaultProps: Record<string, unknown>;
|
||||
static propTypes: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
inputProps: Record<string, unknown> = {},
|
||||
subtotals: SubtotalOptions = {},
|
||||
) {
|
||||
this.props = { ...PivotData.defaultProps, ...inputProps };
|
||||
this.processRecord = this.processRecord.bind(this);
|
||||
PropTypes.checkPropTypes(
|
||||
@@ -669,23 +805,38 @@ class PivotData {
|
||||
'PivotData',
|
||||
);
|
||||
|
||||
this.aggregator = this.props
|
||||
.aggregatorsFactory(this.props.defaultFormatter)
|
||||
[this.props.aggregatorName](this.props.vals);
|
||||
this.formattedAggregators =
|
||||
this.props.customFormatters &&
|
||||
Object.entries(this.props.customFormatters).reduce(
|
||||
(acc, [key, columnFormatter]) => {
|
||||
acc[key] = {};
|
||||
Object.entries(columnFormatter).forEach(([column, formatter]) => {
|
||||
acc[key][column] = this.props
|
||||
.aggregatorsFactory(formatter)
|
||||
[this.props.aggregatorName](this.props.vals);
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const aggregatorsFactory = this.props.aggregatorsFactory as (
|
||||
fmt: unknown,
|
||||
) => Record<string, (vals: unknown) => (...args: unknown[]) => Aggregator>;
|
||||
const aggregatorName = this.props.aggregatorName as string;
|
||||
const vals = this.props.vals as string[];
|
||||
this.aggregator = aggregatorsFactory(this.props.defaultFormatter)[
|
||||
aggregatorName
|
||||
](vals);
|
||||
this.formattedAggregators = this.props.customFormatters
|
||||
? Object.entries(
|
||||
this.props.customFormatters as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>,
|
||||
).reduce(
|
||||
(
|
||||
acc: Record<
|
||||
string,
|
||||
Record<string, (...args: unknown[]) => Aggregator>
|
||||
>,
|
||||
[key, columnFormatter],
|
||||
) => {
|
||||
acc[key] = {};
|
||||
Object.entries(columnFormatter).forEach(([column, formatter]) => {
|
||||
acc[key][column] =
|
||||
aggregatorsFactory(formatter)[aggregatorName](vals);
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: false;
|
||||
this.tree = {};
|
||||
this.rowKeys = [];
|
||||
this.colKeys = [];
|
||||
@@ -699,29 +850,36 @@ class PivotData {
|
||||
PivotData.forEachRecord(this.props.data, this.processRecord);
|
||||
}
|
||||
|
||||
getFormattedAggregator(record, totalsKeys) {
|
||||
getFormattedAggregator(record: PivotRecord, totalsKeys?: string[]) {
|
||||
if (!this.formattedAggregators) {
|
||||
return this.aggregator;
|
||||
}
|
||||
const fmtAggs = this.formattedAggregators;
|
||||
const [groupName, groupValue] =
|
||||
Object.entries(record).find(
|
||||
([name, value]) =>
|
||||
this.formattedAggregators[name] &&
|
||||
this.formattedAggregators[name][value],
|
||||
([name, value]) => fmtAggs[name] && fmtAggs[name][String(value)],
|
||||
) || [];
|
||||
if (
|
||||
!groupName ||
|
||||
!groupValue ||
|
||||
(totalsKeys && !totalsKeys.includes(groupValue))
|
||||
(totalsKeys && !totalsKeys.includes(String(groupValue)))
|
||||
) {
|
||||
return this.aggregator;
|
||||
}
|
||||
return this.formattedAggregators[groupName][groupValue] || this.aggregator;
|
||||
return fmtAggs[groupName][String(groupValue)] || this.aggregator;
|
||||
}
|
||||
|
||||
arrSort(attrs, partialOnTop, reverse = false) {
|
||||
const sortersArr = attrs.map(a => getSort(this.props.sorters, a));
|
||||
return function (a, b) {
|
||||
arrSort(attrs: string[], partialOnTop: boolean | undefined, reverse = false) {
|
||||
const sortersArr = attrs.map(a =>
|
||||
getSort(
|
||||
this.props.sorters as
|
||||
| ((attr: string) => SortFunction | undefined)
|
||||
| Record<string, SortFunction>
|
||||
| null,
|
||||
a,
|
||||
),
|
||||
);
|
||||
return function (a: string[], b: string[]) {
|
||||
const limit = Math.min(a.length, b.length);
|
||||
for (let i = 0; i < limit; i += 1) {
|
||||
const sorter = sortersArr[i];
|
||||
@@ -734,14 +892,16 @@ class PivotData {
|
||||
};
|
||||
}
|
||||
|
||||
sortKeys() {
|
||||
sortKeys(): void {
|
||||
if (!this.sorted) {
|
||||
this.sorted = true;
|
||||
const v = (r, c) => this.getAggregator(r, c).value();
|
||||
const rows = this.props.rows as string[];
|
||||
const cols = this.props.cols as string[];
|
||||
const v = (r: string[], c: string[]) => this.getAggregator(r, c).value();
|
||||
switch (this.props.rowOrder) {
|
||||
case 'key_z_to_a':
|
||||
this.rowKeys.sort(
|
||||
this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop, true),
|
||||
this.arrSort(rows, this.subtotals.rowPartialOnTop, true),
|
||||
);
|
||||
break;
|
||||
case 'value_a_to_z':
|
||||
@@ -751,14 +911,12 @@ class PivotData {
|
||||
this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, [])));
|
||||
break;
|
||||
default:
|
||||
this.rowKeys.sort(
|
||||
this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop),
|
||||
);
|
||||
this.rowKeys.sort(this.arrSort(rows, this.subtotals.rowPartialOnTop));
|
||||
}
|
||||
switch (this.props.colOrder) {
|
||||
case 'key_z_to_a':
|
||||
this.colKeys.sort(
|
||||
this.arrSort(this.props.cols, this.subtotals.colPartialOnTop, true),
|
||||
this.arrSort(cols, this.subtotals.colPartialOnTop, true),
|
||||
);
|
||||
break;
|
||||
case 'value_a_to_z':
|
||||
@@ -768,32 +926,30 @@ class PivotData {
|
||||
this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b)));
|
||||
break;
|
||||
default:
|
||||
this.colKeys.sort(
|
||||
this.arrSort(this.props.cols, this.subtotals.colPartialOnTop),
|
||||
);
|
||||
this.colKeys.sort(this.arrSort(cols, this.subtotals.colPartialOnTop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getColKeys() {
|
||||
getColKeys(): string[][] {
|
||||
this.sortKeys();
|
||||
return this.colKeys;
|
||||
}
|
||||
|
||||
getRowKeys() {
|
||||
getRowKeys(): string[][] {
|
||||
this.sortKeys();
|
||||
return this.rowKeys;
|
||||
}
|
||||
|
||||
processRecord(record) {
|
||||
processRecord(record: PivotRecord): void {
|
||||
// this code is called in a tight loop
|
||||
const colKey = [];
|
||||
const rowKey = [];
|
||||
this.props.cols.forEach(col => {
|
||||
colKey.push(col in record ? record[col] : 'null');
|
||||
const colKey: string[] = [];
|
||||
const rowKey: string[] = [];
|
||||
(this.props.cols as string[]).forEach((col: string) => {
|
||||
colKey.push(col in record ? String(record[col]) : 'null');
|
||||
});
|
||||
this.props.rows.forEach(row => {
|
||||
rowKey.push(row in record ? record[row] : 'null');
|
||||
(this.props.rows as string[]).forEach((row: string) => {
|
||||
rowKey.push(row in record ? String(record[row]) : 'null');
|
||||
});
|
||||
|
||||
this.allTotal.push(record);
|
||||
@@ -860,7 +1016,7 @@ class PivotData {
|
||||
}
|
||||
}
|
||||
|
||||
getAggregator(rowKey, colKey) {
|
||||
getAggregator(rowKey: string[], colKey: string[]): Aggregator {
|
||||
let agg;
|
||||
const flatRowKey = flatKey(rowKey);
|
||||
const flatColKey = flatKey(colKey);
|
||||
@@ -887,7 +1043,10 @@ class PivotData {
|
||||
}
|
||||
|
||||
// can handle arrays or jQuery selections of tables
|
||||
PivotData.forEachRecord = function (input, processRecord) {
|
||||
PivotData.forEachRecord = function (
|
||||
input: unknown,
|
||||
processRecord: (record: PivotRecord) => void,
|
||||
) {
|
||||
if (Array.isArray(input)) {
|
||||
// array of objects
|
||||
return input.map(record => processRecord(record));
|
||||
@@ -933,6 +1092,14 @@ PivotData.propTypes = {
|
||||
]),
|
||||
};
|
||||
|
||||
export type {
|
||||
SortFunction,
|
||||
Formatter,
|
||||
PivotRecord,
|
||||
Aggregator,
|
||||
SubtotalOptions,
|
||||
};
|
||||
|
||||
export {
|
||||
aggregatorTemplates,
|
||||
aggregators,
|
||||
Reference in New Issue
Block a user