chore(frontend): comprehensive TypeScript quality improvements (#37625)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-02-06 16:16:57 -05:00
committed by GitHub
parent e9ae212c1c
commit fc5506e466
441 changed files with 14136 additions and 9956 deletions

View File

@@ -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: [
{

View File

@@ -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;

View File

@@ -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'};

View File

@@ -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)}

View File

@@ -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,