Skip to content

Commit

Permalink
feat: pivot sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
islxyqwe committed Oct 17, 2023
1 parent 81009e9 commit 73e796d
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 83 deletions.
37 changes: 30 additions & 7 deletions packages/graphic-walker/src/components/pivotTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ const PivotTable: React.FC<PivotTableProps> = function PivotTableComponent(props

const vizStore = useVizStore();
const enableCollapse = !!vizStore;
const { allFields, viewMeasures } = vizStore;
const { allFields, viewMeasures, sort, sortedEncoding } = vizStore;
const tableCollapsedHeaderMap = vizStore?.tableCollapsedHeaderMap ?? emptyMap;
const { rows, columns } = draggableFieldState;
const { defaultAggregated, folds } = visualConfig;
const { showTableSummary } = layout
const { defaultAggregated, folds } = visualConfig;
const { showTableSummary } = layout;
const aggData = useRef<IRow[]>([]);
const [topTreeHeaderRowNum, setTopTreeHeaderRowNum] = useState<number>(0);

Expand Down Expand Up @@ -104,7 +104,21 @@ const PivotTable: React.FC<PivotTableProps> = function PivotTableComponent(props
const generateNewTable = () => {
appRef.current?.updateRenderStatus('rendering');
setIsLoading(true);
buildPivotTableService(dimsInRow, dimsInColumn, data, aggData.current, Array.from(tableCollapsedHeaderMap.keys()), showTableSummary)
buildPivotTableService(
dimsInRow,
dimsInColumn,
data,
aggData.current,
Array.from(tableCollapsedHeaderMap.keys()),
showTableSummary,
sort !== 'none' && sortedEncoding !== 'none'
? {
fid: sortedEncoding === 'column' ? `${measInRow[0].fid}_${measInRow[0].aggName}` : `${measInColumn[0].fid}_${measInColumn[0].aggName}`,
mode: sortedEncoding,
type: sort,
}
: undefined
)
.then((data) => {
const { lt, tt, metric } = data;
unstable_batchedUpdates(() => {
Expand Down Expand Up @@ -145,7 +159,16 @@ const PivotTable: React.FC<PivotTableProps> = function PivotTableComponent(props
setIsLoading(true);
appRef.current?.updateRenderStatus('computing');
const groupbyPromises: Promise<IRow[]>[] = groupbyCombList.map((dimComb) => {
const workflow = toWorkflow(vizStore.viewFilters, vizStore.allFields, dimComb, vizStore.viewMeasures, defaultAggregated, vizStore.sort, folds ?? [], vizStore.limit > 0 ? vizStore.limit : undefined);
const workflow = toWorkflow(
vizStore.viewFilters,
vizStore.allFields,
dimComb,
vizStore.viewMeasures,
defaultAggregated,
vizStore.sort,
folds ?? [],
vizStore.limit > 0 ? vizStore.limit : undefined
);
return dataQuery(computation, workflow, vizStore.limit > 0 ? vizStore.limit : undefined)
.then((res) => fold2(res, defaultAggregated, allFields, viewMeasures, dimComb, folds))
.catch((err) => {
Expand Down Expand Up @@ -212,7 +235,7 @@ const PivotTable: React.FC<PivotTableProps> = function PivotTableComponent(props
data={leftTree}
dimsInRow={dimsInRow}
measInRow={measInRow}
onHeaderCollapse={n => vizStore?.updateTableCollapsedHeader(n)}
onHeaderCollapse={(n) => vizStore?.updateTableCollapsedHeader(n)}
enableCollapse={enableCollapse}
/>
)}
Expand All @@ -223,7 +246,7 @@ const PivotTable: React.FC<PivotTableProps> = function PivotTableComponent(props
data={topTree}
dimsInCol={dimsInColumn}
measInCol={measInColumn}
onHeaderCollapse={n => vizStore?.updateTableCollapsedHeader(n)}
onHeaderCollapse={(n) => vizStore?.updateTableCollapsedHeader(n)}
onTopTreeHeaderRowNumChange={(num) => setTopTreeHeaderRowNum(num)}
enableCollapse={enableCollapse}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IAggregator } from "../../interfaces";
export interface INestNode {
key: string | number;
value: string | number;
sort: string | number;
uniqueKey: string;
fieldKey: string;
children: INestNode[];
Expand Down
119 changes: 73 additions & 46 deletions packages/graphic-walker/src/components/pivotTable/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { IRow } from "../../interfaces";
import { INestNode } from "./inteface";
import { IRow } from '../../interfaces';
import { INestNode } from './inteface';

const key_prefix = 'nk_';

function insertNode (tree: INestNode, layerKeys: string[], nodeData: IRow, depth: number, collapsedKeyList: string[]) {
function insertNode(
tree: INestNode,
layerKeys: string[],
nodeData: IRow,
depth: number,
collapsedKeyList: string[],
sort?: {
fid: string;
type: 'ascending' | 'descending';
}
) {
if (depth >= layerKeys.length) {
// tree.key = nodeData[layerKeys[depth]];
return;
return;
}
const key = nodeData[layerKeys[depth]];
const uniqueKey = `${tree.uniqueKey}__${key}`;
Expand All @@ -16,75 +26,88 @@ function insertNode (tree: INestNode, layerKeys: string[], nodeData: IRow, depth
child = {
key,
value: key,
uniqueKey: uniqueKey,
sort: depth === layerKeys.length - 1 && sort ? nodeData[sort.fid] : key,
uniqueKey: uniqueKey,
fieldKey: layerKeys[depth],
children: [],
path: [...tree.path, {key: layerKeys[depth], value: key}],
path: [...tree.path, { key: layerKeys[depth], value: key }],
height: layerKeys.length - depth - 1,
isCollapsed: false,
}
};
if (collapsedKeyList.includes(tree.uniqueKey)) {
tree.isCollapsed = true;
}
tree.children.splice(binarySearchIndex(tree.children, child.key), 0, child);
const reverse = depth === layerKeys.length - 1 && sort?.type === 'descending';
tree.children.splice(binarySearchIndex(tree.children, child.sort, reverse), 0, child);
}
insertNode(child, layerKeys, nodeData, depth + 1, collapsedKeyList);

insertNode(child, layerKeys, nodeData, depth + 1, collapsedKeyList, sort);
}

// Custom binary search function to find appropriate index for insertion.
function binarySearchIndex(arr: INestNode[], keyVal: string | number): number {
let start = 0, end = arr.length - 1;
function binarySearchIndex(arr: INestNode[], keyVal: string | number, reverse = false): number {
let start = 0,
end = arr.length - 1;

while (start <= end) {
let middle = Math.floor((start + end) / 2);
let middleVal = arr[middle].key;
let middleVal = arr[middle].sort;
if (typeof middleVal === 'number' && typeof keyVal === 'number') {
if (middleVal < keyVal) start = middle + 1;
if (reverse !== middleVal < keyVal) start = middle + 1;
else end = middle - 1;
} else {
let cmp = String(middleVal).localeCompare(String(keyVal));
if (cmp < 0) start = middle + 1;
if (reverse !== cmp < 0) start = middle + 1;
else end = middle - 1;
}
}
return start;
}

const ROOT_KEY = '__root';
const TOTAL_KEY = '__total'
const TOTAL_KEY = '__total';

function insertSummaryNode (node: INestNode): void {
function insertSummaryNode(node: INestNode): void {
if (node.children.length > 0) {
node.children.push({
key: TOTAL_KEY,
value: 'total',
sort: '',
fieldKey: node.children[0].fieldKey,
uniqueKey: `${node.uniqueKey}${TOTAL_KEY}`,
children: [],
path: [],
height: node.children[0].height,
isCollapsed: true,
});
for (let i = 0; i < node.children.length - 1; i ++) {
for (let i = 0; i < node.children.length - 1; i++) {
insertSummaryNode(node.children[i]);
}
}
};
}

export function buildNestTree (layerKeys: string[], data: IRow[], collapsedKeyList: string[], showSummary: boolean): INestNode {
export function buildNestTree(
layerKeys: string[],
data: IRow[],
collapsedKeyList: string[],
showSummary: boolean,
sort?: {
fid: string;
type: 'ascending' | 'descending';
}
): INestNode {
const tree: INestNode = {
key: ROOT_KEY,
value: 'root',
fieldKey: 'root',
sort: '',
uniqueKey: ROOT_KEY,
children: [],
path: [],
height: layerKeys.length,
isCollapsed: false,
};
for (let row of data) {
insertNode(tree, layerKeys, row, 0, collapsedKeyList);
insertNode(tree, layerKeys, row, 0, collapsedKeyList, sort);
}
if (showSummary) {
insertSummaryNode(tree);
Expand All @@ -96,28 +119,28 @@ class NodeIterator {
public tree: INestNode;
public nodeStack: INestNode[] = [];
public current: INestNode | null = null;
constructor (tree: INestNode) {
constructor(tree: INestNode) {
this.tree = tree;
}
public first () {
let node = this.tree
public first() {
let node = this.tree;
this.nodeStack = [node];
while (node.children.length > 0 && !node.isCollapsed) {
this.nodeStack.push(node.children[0])
node = node.children[0]
this.nodeStack.push(node.children[0]);
node = node.children[0];
}
this.current = node;
return this.current;
}
public next (): INestNode | null {
public next(): INestNode | null {
let cursorMoved = false;
let counter = 0
let counter = 0;
while (this.nodeStack.length > 1) {
counter++
counter++;
if (counter > 100) break;
let node = this.nodeStack[this.nodeStack.length - 1];
let parent = this.nodeStack[this.nodeStack.length - 2];
let nodeIndex = parent.children.findIndex(n => n.key === node!.key);
let nodeIndex = parent.children.findIndex((n) => n.key === node!.key);
if (nodeIndex === -1) break;
if (cursorMoved) {
if (node.children.length > 0 && !node.isCollapsed) {
Expand All @@ -129,8 +152,8 @@ class NodeIterator {
} else {
if (nodeIndex < parent.children.length - 1) {
this.nodeStack.pop();
this.nodeStack.push(parent.children[nodeIndex + 1])
cursorMoved = true
this.nodeStack.push(parent.children[nodeIndex + 1]);
cursorMoved = true;
continue;
}
if (nodeIndex >= parent.children.length - 1) {
Expand All @@ -146,15 +169,17 @@ class NodeIterator {
}
return this.current;
}
public predicates (): { key: string; value: string | number }[] {
return this.nodeStack.filter(node => node.key !== ROOT_KEY).map(node => ({
key: node.fieldKey,
value: node.value
}))
public predicates(): { key: string; value: string | number }[] {
return this.nodeStack
.filter((node) => node.key !== ROOT_KEY)
.map((node) => ({
key: node.fieldKey,
value: node.value,
}));
}
}

export function buildMetricTableFromNestTree (leftTree: INestNode, topTree: INestNode, data: IRow[]): (IRow | null)[][] {
export function buildMetricTableFromNestTree(leftTree: INestNode, topTree: INestNode, data: IRow[]): (IRow | null)[][] {
const mat: any[][] = [];
const iteLeft = new NodeIterator(leftTree);
const iteTop = new NodeIterator(topTree);
Expand All @@ -163,26 +188,28 @@ export function buildMetricTableFromNestTree (leftTree: INestNode, topTree: INes
const vec: any[] = [];
iteTop.first();
while (iteTop.current !== null) {
const predicates = iteLeft.predicates().concat(iteTop.predicates()).filter((ele) => ele.value !== "total");
const matchedRows = data.filter(r => predicates.every(pre => r[pre.key] === pre.value));
const predicates = iteLeft
.predicates()
.concat(iteTop.predicates())
.filter((ele) => ele.value !== 'total');
const matchedRows = data.filter((r) => predicates.every((pre) => r[pre.key] === pre.value));
if (matchedRows.length > 0) {
// If multiple rows are matched, then find the most matched one (the row with smallest number of keys)
vec.push(matchedRows.reduce((a, b) => Object.keys(a).length < Object.keys(b).length ? a : b));
vec.push(matchedRows.reduce((a, b) => (Object.keys(a).length < Object.keys(b).length ? a : b)));
} else {
vec.push(undefined);
}
iteTop.next();
}
mat.push(vec)
mat.push(vec);
iteLeft.next();
}
return mat;
}

export function getAllChildrenSize (node: INestNode, depth: number): number {
export function getAllChildrenSize(node: INestNode, depth: number): number {
if (depth === 0) {
return node.children.length;
}
return node.children.reduce((acc, child) => acc + getAllChildrenSize(child, depth + 1), 0)

}
return node.children.reduce((acc, child) => acc + getAllChildrenSize(child, depth + 1), 0);
}
10 changes: 8 additions & 2 deletions packages/graphic-walker/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export const buildPivotTableService = async (dimsInRow: IViewField[],
allData: IRow[],
aggData: IRow[],
collapsedKeyList: string[],
showTableSummary: boolean
showTableSummary: boolean,
sort?: {
fid: string,
type: 'ascending' | 'descending',
mode: 'row' | 'column',
}
): Promise<{lt: INestNode, tt: INestNode, metric: (IRow | null)[][]}> => {
const worker = new BuildMetricTableWorker();
try {
Expand All @@ -161,7 +166,8 @@ export const buildPivotTableService = async (dimsInRow: IViewField[],
allData,
aggData,
collapsedKeyList,
showTableSummary
showTableSummary,
sort,
});
return res;
} catch (error) {
Expand Down
11 changes: 11 additions & 0 deletions packages/graphic-walker/src/store/visualSpecStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ export class VizSpecStore {
return 'none';
}

get sortedEncoding() {
const { rows, columns } = this;
if (rows.length && !rows.find((x) => x.analyticType === 'measure')) {
return 'row';
}
if (columns.length && !columns.find((x) => x.analyticType === 'measure')) {
return 'column';
}
return 'none';
}

get allFields() {
return [...this.dimensions, ...this.measures];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { buildPivotTable } from "./buildPivotTable"
* @param {MessageEvent<{ dimsInRow: import('../interfaces').IViewField[]; dimsInColumn: import('../interfaces').IViewField[]; allData: import('../interfaces').IRow[]; aggData: import('../interfaces').IRow[]; collapsedKeyList: string[]; showTableSummary: boolean }>} e
*/
const main = e => {
const { dimsInRow, dimsInColumn, allData, aggData, collapsedKeyList, showTableSummary } = e.data;
const { dimsInRow, dimsInColumn, allData, aggData, collapsedKeyList, showTableSummary, sort } = e.data;
try {
const ans = buildPivotTable(dimsInRow, dimsInColumn, allData, aggData, collapsedKeyList, showTableSummary);
const ans = buildPivotTable(dimsInRow, dimsInColumn, allData, aggData, collapsedKeyList, showTableSummary, sort);
self.postMessage(ans);
} catch (error) {
self.postMessage({ error: error.message });
Expand Down
Loading

0 comments on commit 73e796d

Please sign in to comment.