/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/widgets/DataFrame/hooks/useTableSizer.ts
309 строк
11 KB
Lukas Masuch
Split `Arrow.proto` into `Table.proto` and `Dataframe.proto` (#13768)
04 фев 2026, 22:22
Не верифицирован
04 фев 2026, 22:22
dc9d8fe
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // TODO: fix incorrect hook usage and delete this lint suppression /* eslint-disable react-hooks/exhaustive-deps -- TODO: Update to match React best practices */ import { useLayoutEffect, useState } from "react" import { Size as ResizableSize } from "re-resizable" import { Dataframe as DataframeProto, streamlit } from "@streamlit/protobuf" import { calculateTableHeight, getConfiguredHeight, getConfiguredWidth, shouldUseContainerWidth, shouldUseContentHeight, shouldUseContentWidth, shouldUseStretchHeight, } from "~lib/components/widgets/DataFrame/dimensionUtils" import { notNullOrUndefined } from "~lib/util/utils" import { CustomGridTheme } from "./useCustomTheme" export type AutoSizerReturn = { // The minimum height that the data grid can be resized to minHeight: number // The maximum height of the data grid can be resized to maxHeight: number // The minimum width of the data grid can be resized to minWidth: number // The maximum width of the data grid can be resized to maxWidth: number // The row height of the data grid rowHeight: number // The current (or initial) size of the data grid resizableSize: ResizableSize // A callback function to change the size of the data grid. setResizableSize: React.Dispatch<React.SetStateAction<ResizableSize>> } /** * A custom React hook that manages all aspects related to the size of the table. * * @param element - The DataframeProto element * @param numRows - The number of rows in the table * @param usesGroupRow - Whether the table uses a group row to display multiple column headers. * @param containerWidth - The width of the surrounding container * @param fullscreenContainerHeight - The height of the surrounding container (fullscreen mode) * @param isFullScreen - Whether the table is in fullscreen mode * @param widthConfig - The width configuration of the table * @param heightConfig - The height configuration of the table * @param measuredContainerHeight - The measured height of the container for height="stretch" * @param isInRoot - Whether the table is in the root container * * @returns The row height, min/max height & width, and the current size of the resizable container. */ function useTableSizer( element: DataframeProto, gridTheme: CustomGridTheme, numRows: number, usesGroupRow: boolean, containerWidth: number, fullscreenContainerHeight?: number, isFullScreen?: boolean, widthConfig?: streamlit.IWidthConfig | null, heightConfig?: streamlit.IHeightConfig | null, measuredContainerHeight?: number, isInRoot?: boolean ): AutoSizerReturn { const rowHeight = element.rowHeight ?? gridTheme.defaultRowHeight // Group row + column header row const numHeaderRows = usesGroupRow ? 2 : 1 const numTrailingRows = element.editingMode === DataframeProto.EditingMode.DYNAMIC || element.editingMode === DataframeProto.EditingMode.ADD_ONLY ? 1 : 0 // Calculate the height of the table based on the number of rows: const totalDataRows = numRows + numTrailingRows const calculatedHeight = calculateTableHeight({ numRows: totalDataRows, rowHeight, theme: gridTheme, numHeaderRows, }) /** * Minimum height logic: * * Base minimum (all modes): header + 1 row + borders (applies even to user-configured height) * * Stretch height mode (height="stretch") additionally: * - 0 rows → 1-row minimum * - 1-3 rows → match actual number of rows * - 4+ rows → 3-row minimum */ const configuredHeight = getConfiguredHeight(heightConfig) // Stretch height styling does not work in the root container without an // enclosing fixed-height container. const useStretchHeight = shouldUseStretchHeight(heightConfig, isInRoot) const useContentHeight = shouldUseContentHeight(heightConfig) // Determine the minimum number of rows to display const minRows = useStretchHeight ? Math.max(1, Math.min(numRows, 3)) // Stretch: match rows (1-3 range) : 1 // All other modes: 1-row minimum const minHeight = calculateTableHeight({ numRows: minRows, rowHeight, theme: gridTheme, }) // Maximum height cap for content mode (10,000 pixels) const MAX_CONTENT_HEIGHT = 10000 // Ensure maxHeight is at least minHeight // E.g., with 0 rows: calculatedHeight might be just header+borders, // but we enforce minHeight to maintain table structure (at least 1 row visible) let maxHeight = Math.max(calculatedHeight, minHeight) // The reason why we have initial height is that the table itself is // resizable by the user. So, it starts with initial height but can be // resized between min and max height. let initialHeight = Math.min(maxHeight, gridTheme.defaultTableHeight) if ( useStretchHeight && measuredContainerHeight && measuredContainerHeight > 0 ) { // height="stretch" - for stretch mode, we want the dataframe to participate // in flexbox layout, so we don't set a fixed height. Instead, we let the // container handle the height through CSS. // We still need to set a reasonable maxHeight for the resizable container. maxHeight = Math.max(measuredContainerHeight, maxHeight) // Don't override initialHeight - let it use the default behavior } else if (useContentHeight) { // height="content" - show all rows, but cap at MAX_CONTENT_HEIGHT (10,000px) // Calculate the approximate number of rows that would result in a 10,000px table const maxRowsForHeightCap = Math.ceil(MAX_CONTENT_HEIGHT / rowHeight) const effectiveRows = Math.min(totalDataRows, maxRowsForHeightCap) // Add 1px to account for GlideDataGrid's internal rendering/spacing // Without this, scrollbars appear unnecessarily when all rows should fit const contentHeight = calculateTableHeight({ numRows: effectiveRows, rowHeight, theme: gridTheme, numHeaderRows, }) + 1 initialHeight = Math.max(contentHeight, minHeight) maxHeight = Math.max(initialHeight, maxHeight) } else if (configuredHeight) { // User has explicitly configured a height (integer value) // Respect their choice, but enforce minimum (minHeight = 1 row when not stretch) initialHeight = Math.max(configuredHeight, minHeight) maxHeight = Math.max(initialHeight, maxHeight) } // else: height="auto" (default) - use the default behavior (show at most 10 rows) if (fullscreenContainerHeight) { // If we are in fullscreen mode, the height and maxHeight should not exceed // the fullscreen container height. initialHeight = Math.min(initialHeight, fullscreenContainerHeight) maxHeight = Math.min(maxHeight, fullscreenContainerHeight) if (!configuredHeight) { // If no explicit height is set, set height to max height (fullscreen mode) initialHeight = maxHeight } } // Min width for the resizable table container: // Based on one column at minimum width + borders const minWidth = gridTheme.minColumnWidth + 2 * gridTheme.tableBorderWidth // The available width should be at least the minimum table width // to prevent "maximum update depth exceeded" error. The reason // is that the container width can be -1 in some edge cases // caused by the resize observer in the Block component. // This can trigger the "maximum update depth exceeded" error // within the grid component. const availableWidth = Math.max(containerWidth, minWidth) // The initial width of the data grid. // If not set, the data grid will be auto adapted to its content. // The reason why we have initial width is that the data grid itself // is resizable by the user. It starts with initial width but can be // resized between min and max width. let initialWidth: number | undefined // The maximum width of the data grid can be resized to. let maxWidth = availableWidth const useContainerWidth = shouldUseContainerWidth(widthConfig) const configuredWidth = getConfiguredWidth(widthConfig) const useContentWidth = shouldUseContentWidth(widthConfig) if (useContainerWidth) { // If user has set use_container_width or width="stretch", // use the full container (available) width. initialWidth = availableWidth } else if (configuredWidth) { // The user has explicitly configured a width // use it but keep between the MIN_TABLE_WIDTH // and the available width. initialWidth = Math.min( Math.max(configuredWidth, minWidth), availableWidth ) // Make sure that the max width we configure is between the user // configured width and the available (container) width. maxWidth = Math.min(Math.max(configuredWidth, maxWidth), availableWidth) } else if (useContentWidth) { // width="content" - let the table auto-size to its content initialWidth = undefined } const [resizableSize, setResizableSize] = useState<ResizableSize>({ // If user hasn't specified a width via `width` or `use_container_width`, // we configure the table to 100%. Which will cause the data grid to // calculate the best size on the content and use that. width: initialWidth || "100%", height: useStretchHeight ? "100%" : initialHeight, }) useLayoutEffect(() => { // This prevents weird table resizing behavior if the container width // changes and the table uses the full container width. if (useContainerWidth && resizableSize.width === "100%") { setResizableSize(prev => ({ ...prev, width: availableWidth, })) } }, [availableWidth]) // Reset the width if the element width parameter was changed: useLayoutEffect(() => { setResizableSize(prev => ({ ...prev, width: initialWidth || "100%", })) }, [initialWidth]) // Reset the height if the element height parameter was changed or // if the number of rows changes (e.g. via add_rows): useLayoutEffect(() => { setResizableSize(prev => ({ ...prev, height: useStretchHeight ? "100%" : initialHeight, })) }, [ initialHeight, numRows, measuredContainerHeight, useStretchHeight, isInRoot, ]) // Change sizing if the fullscreen mode is activated or deactivated: useLayoutEffect(() => { if (isFullScreen) { const stretchColumns: boolean = useContainerWidth || (notNullOrUndefined(configuredWidth) && configuredWidth > 0) setResizableSize({ width: stretchColumns ? maxWidth : "100%", height: maxHeight, }) } else { setResizableSize({ width: initialWidth || "100%", height: initialHeight, }) } }, [isFullScreen]) return { minHeight, maxHeight, minWidth, maxWidth, rowHeight, resizableSize, setResizableSize, } } export default useTableSizer