UNPKG

react-konva-grid

Version:

Canvas grid to render large set of tabular data with virtualization.

424 lines (380 loc) 10.2 kB
// Utilities extracted from https://github.com/bvaughn/react-window import { TItemSize, IInstanceProps, IArea, ICell, TCellMetaData } from "./Grid"; type ItemType = "row" | "column"; export interface IItemMetaData { itemType: ItemType; offset: number; index: number; rowCount: number; columnCount: number; rowHeight: TItemSize; columnWidth: TItemSize; instanceProps: IInstanceProps; } export const getRowStartIndexForOffset = ({ rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }: Omit<IItemMetaData, "index" | "itemType">): number => { return findNearestItem({ itemType: "row", rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }); }; interface IRowStopIndex extends Omit<IItemMetaData, "itemType" | "index" | "offset" | "columnCount"> { startIndex: number; containerHeight: number; scrollTop: number; } export const getRowStopIndexForStartIndex = ({ startIndex, rowCount, rowHeight, columnWidth, scrollTop, containerHeight, instanceProps, }: IRowStopIndex): number => { const itemMetadata = getItemMetadata({ itemType: "row", rowHeight, columnWidth, index: startIndex, instanceProps, }); const maxOffset = scrollTop + containerHeight; let offset = itemMetadata.offset + itemMetadata.size; let stopIndex = startIndex; while (stopIndex < rowCount - 1 && offset < maxOffset) { stopIndex++; offset += getItemMetadata({ itemType: "row", rowHeight, columnWidth, index: stopIndex, instanceProps, }).size; } return stopIndex; }; export const getColumnStartIndexForOffset = ({ rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }: Omit<IItemMetaData, "index" | "itemType">): number => { return findNearestItem({ itemType: "column", rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }); }; interface IColumnStopIndex extends Omit<IItemMetaData, "itemType" | "index" | "offset" | "rowCount"> { startIndex: number; containerWidth: number; scrollLeft: number; } export const getColumnStopIndexForStartIndex = ({ startIndex, rowHeight, columnWidth, instanceProps, containerWidth, scrollLeft, columnCount, }: IColumnStopIndex): number => { const itemMetadata = getItemMetadata({ itemType: "column", index: startIndex, rowHeight, columnWidth, instanceProps, }); const maxOffset = scrollLeft + containerWidth; let offset = itemMetadata.offset + itemMetadata.size; let stopIndex = startIndex; while (stopIndex < columnCount - 1 && offset < maxOffset) { stopIndex++; offset += getItemMetadata({ itemType: "column", rowHeight, columnWidth, index: stopIndex, instanceProps, }).size; } return stopIndex; }; export const getBoundedCells = (area: IArea | null | undefined) => { const cells = new Set(); if (!area) return cells; const { top, bottom, left, right } = area; for (let i = top; i <= bottom; i++) { for (let j = left; j <= right; j++) { cells.add(cellIndentifier(i, j)); } } return cells; }; export const itemKey = ({ rowIndex, columnIndex }: ICell) => `${rowIndex}:${columnIndex}`; export const getRowOffset = ({ index, rowHeight, columnWidth, instanceProps, }: Omit<IGetItemMetadata, "itemType">): number => { return getItemMetadata({ itemType: "row", index, rowHeight, columnWidth, instanceProps, }).offset; }; export const getColumnOffset = ({ index, rowHeight, columnWidth, instanceProps, }: Omit<IGetItemMetadata, "itemType">): number => { return getItemMetadata({ itemType: "column", index, rowHeight, columnWidth, instanceProps, }).offset; }; export const getRowHeight = (index: number, instanceProps: IInstanceProps) => { return instanceProps.rowMetadataMap[index].size; }; export const getColumnWidth = ( index: number, instanceProps: IInstanceProps ) => { return instanceProps.columnMetadataMap[index].size; }; interface IGetItemMetadata extends Pick< IItemMetaData, "itemType" | "index" | "rowHeight" | "columnWidth" | "instanceProps" > {} export const getItemMetadata = ({ itemType, index, rowHeight, columnWidth, instanceProps, }: IGetItemMetadata): TCellMetaData => { let itemMetadataMap, itemSize, lastMeasuredIndex; if (itemType === "column") { itemMetadataMap = instanceProps.columnMetadataMap; itemSize = columnWidth; lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex; } else { itemMetadataMap = instanceProps.rowMetadataMap; itemSize = rowHeight; lastMeasuredIndex = instanceProps.lastMeasuredRowIndex; } if (index > lastMeasuredIndex) { let offset = 0; if (lastMeasuredIndex >= 0) { const itemMetadata = itemMetadataMap[lastMeasuredIndex]; offset = itemMetadata.offset + itemMetadata.size; } for (let i = lastMeasuredIndex + 1; i <= index; i++) { let size = itemSize(i); itemMetadataMap[i] = { offset, size, }; offset += size; } if (itemType === "column") { instanceProps.lastMeasuredColumnIndex = index; } else { instanceProps.lastMeasuredRowIndex = index; } } return itemMetadataMap[index]; }; const findNearestItem = ({ itemType, rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }: Omit<IItemMetaData, "index">): number => { let itemMetadataMap, lastMeasuredIndex; if (itemType === "column") { itemMetadataMap = instanceProps.columnMetadataMap; lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex; } else { itemMetadataMap = instanceProps.rowMetadataMap; lastMeasuredIndex = instanceProps.lastMeasuredRowIndex; } const lastMeasuredItemOffset = lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0; if (lastMeasuredItemOffset >= offset) { // If we've already measured items within this range just use a binary search as it's faster. return findNearestItemBinarySearch({ itemType, rowHeight, columnWidth, instanceProps, high: lastMeasuredIndex, low: 0, offset, }); } else { // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. // The exponential search avoids pre-computing sizes for the full set of items as a binary search would. // The overall complexity for this approach is O(log n). return findNearestItemExponentialSearch({ itemType, rowHeight, rowCount, columnCount, columnWidth, instanceProps, index: Math.max(0, lastMeasuredIndex), offset, }); } }; interface IBinarySearchArgs extends Omit<IItemMetaData, "index" | "rowCount" | "columnCount"> { high: number; low: number; } const findNearestItemBinarySearch = ({ itemType, rowHeight, columnWidth, instanceProps, high, low, offset, }: IBinarySearchArgs): number => { while (low <= high) { const middle = low + Math.floor((high - low) / 2); const currentOffset = getItemMetadata({ itemType, rowHeight, columnWidth, index: middle, instanceProps, }).offset; if (currentOffset === offset) { return middle; } else if (currentOffset < offset) { low = middle + 1; } else if (currentOffset > offset) { high = middle - 1; } } if (low > 0) { return low - 1; } else { return 0; } }; const findNearestItemExponentialSearch = ({ itemType, rowHeight, columnWidth, rowCount, columnCount, instanceProps, index, offset, }: IItemMetaData) => { const itemCount = itemType === "column" ? columnCount : rowCount; let interval = 1; while ( index < itemCount && getItemMetadata({ itemType, rowHeight, columnWidth, index, instanceProps, }).offset < offset ) { index += interval; interval *= 2; } return findNearestItemBinarySearch({ itemType, rowHeight, columnWidth, instanceProps, high: Math.min(index, itemCount - 1), low: Math.floor(index / 2), offset, }); }; export const getEstimatedTotalHeight = ( rowCount: number, estimatedRowHeight: number, instanceProps: IInstanceProps ) => { let totalSizeOfMeasuredRows = 0; let { lastMeasuredRowIndex, rowMetadataMap } = instanceProps; // Edge case check for when the number of items decreases while a scroll is in progress. // https://github.com/bvaughn/react-window/pull/138 if (lastMeasuredRowIndex >= rowCount) { lastMeasuredRowIndex = rowCount - 1; } if (lastMeasuredRowIndex >= 0) { const itemMetadata = rowMetadataMap[lastMeasuredRowIndex]; totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size; } const numUnmeasuredItems = rowCount - lastMeasuredRowIndex - 1; const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedRowHeight; return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems; }; export const getEstimatedTotalWidth = ( columnCount: number, estimatedColumnWidth: number, instanceProps: IInstanceProps ) => { let totalSizeOfMeasuredRows = 0; let { lastMeasuredColumnIndex, columnMetadataMap } = instanceProps; // Edge case check for when the number of items decreases while a scroll is in progress. // https://github.com/bvaughn/react-window/pull/138 if (lastMeasuredColumnIndex >= columnCount) { lastMeasuredColumnIndex = columnCount - 1; } if (lastMeasuredColumnIndex >= 0) { const itemMetadata = columnMetadataMap[lastMeasuredColumnIndex]; totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size; } const numUnmeasuredItems = columnCount - lastMeasuredColumnIndex - 1; const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedColumnWidth; return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems; }; /* Create a stringified cell identifier */ export const cellIndentifier = ( rowIndex: number, columnIndex: number ): string => [rowIndex, columnIndex].toString();