@1771technologies/lytenyte-pro
Version:
Blazingly fast headless React data grid with 100s of features.
154 lines (153 loc) • 7.39 kB
JavaScript
import { useMemo, useRef } from "react";
import { computeField } from "@1771technologies/lytenyte-core/internal";
import {} from "@1771technologies/lytenyte-shared";
import { pivotPaths } from "./auxiliary-functions/pivot-paths.js";
import { applyReferenceColumn } from "./auxiliary-functions/apply-reference-column.js";
import { equal, itemsWithIdToMap } from "@1771technologies/js-utils";
import { measureText } from "@1771technologies/dom-utils";
export function usePivotColumns(pivotMode, pivotControlled, model, leafs, filtered, processor) {
const measures = model?.measures;
const columns = model?.columns;
const prevMeasuresRef = useRef(measures);
const prevColumnsRef = useRef(columns);
const pivotControlledImmediateRef = useRef(pivotControlled.state);
pivotControlledImmediateRef.current = pivotControlled.state;
const pivotColumns = useMemo(() => {
if (!pivotMode)
return null;
// At this point we should check if we need to replace the pivot state
const prevMeasures = prevMeasuresRef.current;
const prevColumns = prevColumnsRef.current;
// If the measures or columns have changed, then the pivot state should be reset.
if (!equal(prevMeasures, measures) || !equal(prevColumns, columns)) {
pivotControlled.setState((prev) => ({
...prev,
columnState: { ordering: [], pinning: {}, resizing: {} },
columnGroupState: {},
}));
pivotControlledImmediateRef.current = {
...pivotControlledImmediateRef.current,
columnState: { ordering: [], pinning: {}, resizing: {} },
columnGroupState: {},
};
prevColumnsRef.current = columns;
prevMeasuresRef.current = measures;
}
if (!measures?.length && !columns?.length)
return [];
// There are only measures, hence each measure should become a column.
if (!columns?.length) {
return measures.map((x) => {
const column = applyReferenceColumn({
id: x.dim.id,
field: x.dim.id,
name: x.dim.name,
}, x.dim);
return column;
});
}
const paths = pivotPaths(filtered, leafs, columns, measures, model?.colLabelFilter);
const lookup = Object.fromEntries((measures ?? []).map((x) => [x.dim.id, x]));
const cols = paths.map((path) => {
const partsRaw = path.split("-->");
const parts = partsRaw.map((x) => (x === "ln__blank__" ? "(blank)" : x));
if (parts.length === 1) {
return { id: path };
}
const measureId = parts.at(-1);
const measureRef = lookup[measureId]?.dim;
// Pop the last part as this is the aggregation value
parts.pop();
let name = measureId === "ln__noop" || measures?.length === 1 ? parts.at(-1) : measureId;
if (name === "ln__grand_total")
name = "Grand Total";
if (parts[0] === "ln__grand_total" && measures && measures.length > 1)
name = "Grand Total " + measureId;
if (name === "ln__total")
name = "Total";
const group = (parts.length === 1 && measures?.length === 1
? undefined
: !measures?.length || measures.length === 1
? parts.slice(0, -1)
: parts)?.map((x) => (x === "ln__total" ? "Total" : x));
partsRaw.pop();
const label = measures.length > 1 ? (measureRef?.name ?? name) : name;
const minWidth = measureText(label);
const column = applyReferenceColumn({
id: path,
name: label,
widthMin: minWidth?.width,
groupPath: group?.map((x) => {
if (x === "ln__grand_total")
return "Grand Total";
if (x === "ln__total")
return "Total";
return x;
}),
groupVisibility: path.includes("ln__total") || path.includes("ln__grand_total") ? "always" : "open",
field: ({ row }) => {
// If the value is a group then we can simply grab the aggregated value.
if (row.kind === "branch" || row.kind === "aggregated")
return row.data[path];
// Pivots do not have leafs displayed. So here we do something interesting. We return true if the
// row should be kept for this pivot, otherwise false. This is effectively a leaf row filter for pivots.
// We can then aggregate these.
for (let i = 0; i < columns.length; i++) {
const c = columns[i];
const field = c.field ?? c.id;
const value = field ? computeField(field, row) : false;
const match = partsRaw[i];
// This is a total columns. Totals will always be one shorter than than the path
if (i >= partsRaw.length)
return true;
const isMatch = match.startsWith("ln") ||
String(value) === match ||
(value == null && match === "ln__blank__");
if (!isMatch)
return false;
}
return true;
},
}, measureRef);
return column;
});
return cols;
}, [pivotMode, measures, columns, filtered, leafs, model?.colLabelFilter, pivotControlled]);
const pivotColumnsWithState = useMemo(() => {
if (!pivotColumns)
return null;
void pivotControlled.pivotColumnState;
const state = pivotControlledImmediateRef.current.columnState;
const byId = itemsWithIdToMap(pivotColumns);
const ordering = state.ordering.filter((x) => byId.has(x));
const withBlanks = pivotColumns.map((x) => (ordering.includes(x.id) ? null : x));
let orderPos = 0;
for (let i = 0; i < withBlanks.length; i++) {
if (withBlanks[i])
continue;
const id = state.ordering[orderPos];
const column = byId.get(id);
withBlanks[i] = column;
orderPos++;
}
Object.entries(state.resizing).forEach(([id, value]) => {
const column = byId.get(id);
if (!column)
return;
Object.assign(column, { width: value });
});
Object.entries(state.pinning).forEach(([id, value]) => {
const column = byId.get(id);
if (!column)
return;
Object.assign(column, { pin: value });
});
return withBlanks;
}, [pivotColumns, pivotControlled.pivotColumnState]);
const processedColumns = useMemo(() => {
if (!processor || !pivotColumnsWithState)
return pivotColumnsWithState;
return processor(pivotColumnsWithState);
}, [pivotColumnsWithState, processor]);
return processedColumns;
}