/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/widgets/DataFrame/columns/utils.ts
741 строка
23 KB
Ken McGrady
Update copyright header to 2026 (#13491)
02 янв 2026, 16:21
Не верифицирован
02 янв 2026, 16:21
374540a
Код
Авторство
О чём код?
/** * 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. */ import { BaseGridCell, Theme as GlideTheme, GridCell, GridCellKind, GridColumn, LoadingCell, TextCell, } from "@glideapps/glide-data-grid" import { Vector } from "apache-arrow" import { merge, toString } from "lodash-es" import moment from "moment" import "moment-duration-format" import "moment-timezone" import numbro from "numbro" import { ArrowType } from "~lib/dataframes/arrowTypeUtils" import { EmotionTheme } from "~lib/theme" import { isNullOrUndefined, notNullOrUndefined } from "~lib/util/utils" /** * Interface used for defining the properties (configuration options) of a column. * These options can also be used to overwrite from user-defined column config. */ export interface BaseColumnProps { /** The id of the column. */ readonly id: string /** The name of the column from the original data. */ readonly name: string /** The display title of the column. */ readonly title: string /** The index number of the column. */ readonly indexNumber: number /** The arrow data type of the column. */ readonly arrowType: ArrowType /** If `True`, the column can be edited. */ readonly isEditable: boolean /** If `True`, the column is hidden (will not be shown). */ readonly isHidden: boolean /** If `True`, the column is a table index. */ readonly isIndex: boolean /** If `True`, the column is pinned/frozen. */ readonly isPinned: boolean /** If `True`, the column is a stretched. */ readonly isStretched: boolean /** If `True`, a value is required before the cell or row can be submitted. */ readonly isRequired?: boolean /** * If `True`, the content of the cell is allowed to be wrapped to fill the * available height of the cell. */ readonly isWrappingAllowed?: boolean /** The initial width of the column. */ readonly width?: number /** A help text that is displayed on hovering the column header. */ readonly help?: string /** * Configuration options related to the column type. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. readonly columnTypeOptions?: Record<string, any> /** The content alignment of the column. */ readonly contentAlignment?: "left" | "center" | "right" /** The default value of the column used when adding a new row. */ readonly defaultValue?: string | number | boolean /** Theme overrides for this column. */ readonly themeOverride?: Partial<GlideTheme> /** A custom icon to be displayed in the column header. */ readonly icon?: string /** The group that this column belongs to. */ readonly group?: string } /** * The interface that is implemented by any column type. */ export interface BaseColumn extends BaseColumnProps { readonly kind: string // Defines the sort mode that should be used for this column type: // default: Sorts by interpreting all values as strings. // smart: Detects if value is a number or a string and sorts accordingly. // raw: Sorts based on the actual type of the cell data value. readonly sortMode: "default" | "raw" | "smart" // An material icon identifier (":material/...") that is used // as type icon in the column menu. readonly typeIcon: string // Validate the input data for compatibility with the column type: // Either returns a boolean indicating if the data is valid or not, or // returns the corrected value. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents -- TODO: Replace 'any' with a more specific type. validateInput?(data?: any): boolean | any // Get a cell with the provided data for the column type: // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. getCell(data?: any, validate?: boolean): GridCell // Get the raw value of the given cell: // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents -- TODO: Replace 'any' with a more specific type. getCellValue(cell: GridCell): any | null } /** * A type that describes the function signature used to create a column based on * some column properties. */ export type ColumnCreator = { (props: BaseColumnProps, theme: EmotionTheme): BaseColumn readonly isEditableType: boolean } // See pydantic for inspiration: https://pydantic-docs.helpmanual.io/usage/types/#booleans const BOOLEAN_TRUE_VALUES = ["true", "t", "yes", "y", "on", "1"] const BOOLEAN_FALSE_VALUES = ["false", "f", "no", "n", "off", "0"] /** * Interface used for indicating if a cell contains an error. */ export interface ErrorCell extends TextCell { readonly isError: true readonly errorDetails: string } /** * Returns a cell with an error message. * * @param errorMsg: A short error message or the wrong value to use as display value. * @param errorDetails: The full error message to show when the user * hovers on a cell. * * @return a read-only GridCell object that can be used by glide-data-grid. */ export function getErrorCell(errorMsg: string, errorDetails = ""): ErrorCell { return { kind: GridCellKind.Text, readonly: true, allowOverlay: true, data: errorMsg, displayData: errorMsg, errorDetails: errorDetails, isError: true, style: "faded", } } /** * Returns `true` if the given cell contains an error. * This can happen if the value type is not compatible with * the given value type. */ export function isErrorCell(cell: GridCell): cell is ErrorCell { return Object.hasOwn(cell, "isError") && (cell as ErrorCell).isError } interface CellWithTooltip extends BaseGridCell { readonly tooltip: string } /** * Returns `true` if the given cell has a tooltip */ export function hasTooltip(cell: BaseGridCell): cell is CellWithTooltip { return ( Object.hasOwn(cell, "tooltip") && (cell as CellWithTooltip).tooltip !== "" ) } /** * Interface used for indicating if a cell contains no value. */ interface MissingValueCell extends BaseGridCell { readonly isMissingValue: true } /** * Returns `true` if the given cell contains no value (-> missing value). * For example, a number cell that contains null is interpreted as a missing value. */ export function isMissingValueCell( cell: BaseGridCell ): cell is MissingValueCell { return ( Object.hasOwn(cell, "isMissingValue") && (cell as MissingValueCell).isMissingValue ) } /** * Returns an empty cell. */ export function getEmptyCell(missingCell = false): LoadingCell { if (missingCell) { return { kind: GridCellKind.Loading, allowOverlay: false, isMissingValue: true, copyData: "", } as LoadingCell } return { kind: GridCellKind.Loading, allowOverlay: false, copyData: "", } as LoadingCell } /** * Returns an empty text cell. * * @param readonly: If true, returns a read-only version of the cell. * @param faded: If true, returns a faded version of the cell. * * @return a GridCell object that can be used by glide-data-grid. */ export function getTextCell(readonly: boolean, faded: boolean): TextCell { const style = faded ? "faded" : "normal" return { kind: GridCellKind.Text, data: "", displayData: "", allowOverlay: true, readonly, style, } as TextCell } /** * Converts from our BaseColumn format to the glide-data-grid compatible GridColumn. */ export function toGlideColumn(column: BaseColumn): GridColumn { return { id: column.id, title: column.title, hasMenu: false, menuIcon: "dots", themeOverride: column.themeOverride, icon: column.icon, group: column.group, // Only grow non pinned columns, it looks a bit broken otherwise: ...(column.isStretched && !column.isPinned && { grow: 1, }), ...(column.width && { width: column.width, }), } as GridColumn } /** * Merges the default column parameters with the user-defined column parameters. * * @param defaultParams - The default column parameters. * @param userParams - The user-defined column parameters. * * @returns The merged column parameters. */ export function mergeColumnParameters( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. defaultParams: Record<string, any> | undefined | null, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. userParams: Record<string, any> | undefined | null // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. ): Record<string, any> { if (isNullOrUndefined(defaultParams)) { return userParams || {} } if (isNullOrUndefined(userParams)) { return defaultParams || {} } return merge(defaultParams, userParams) } /** * Converts the given value of unknown type to an array without * the risks of any exceptions. * * @param data - The value to convert to an array. * * @returns The converted array or an empty array if the value cannot be interpreted as an array. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function toSafeArray(data: any): any[] { if (isNullOrUndefined(data)) { return [] } if (typeof data === "number" || typeof data === "boolean") { // Single number or boolean return [data] } if (typeof data === "string") { if (data === "") { // Empty string return [] } // Try to parse string to an array if (data.trim().startsWith("[") && data.trim().endsWith("]")) { // Support for JSON arrays: ["foo", 1, null, "test"] try { return JSON.parse(data) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return [data] } } else { // Support for comma-separated values: "foo,1,,test" return data.split(",") } } try { const parsedData = JSON.parse( JSON.stringify(data, (_key, value) => typeof value === "bigint" ? Number(value) : value ) ) if (!Array.isArray(parsedData)) { return [toSafeString(parsedData)] } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. return parsedData.map((value: any) => ["string", "number", "boolean", "null"].includes(typeof value) ? value : toSafeString(value) ) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return [toSafeString(data)] } } /** * Checks if the provided data used as array value is supported for editing. * * @param data - The value to inspect. * @returns True if `data` is supported for array-editing. */ export function isEditableArrayValue(data: unknown): boolean { if (typeof data === "string" || data instanceof String) { return true } if (data instanceof Vector) { data = Array.from(data) } return ( Array.isArray(data) && data.every(v => typeof v === "string" || v instanceof String) ) } /** * Efficient check to determine if a string is looks like a JSON string. * * This is only a heuristic check and does not guarantee that the string is a * valid JSON string. * * @param data - The data to check. * * @returns `true` if the data might be a JSON string. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function isMaybeJson(data: any): boolean { return data?.startsWith("{") && data.endsWith("}") } /** * Converts the given value of unknown type to a string without * the risks of any exceptions. * * @param data - The value to convert to a string. * * @return The converted string or a string showing the type of the object as fallback. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function toSafeString(data: any): string { try { try { return toString(data) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return JSON.stringify(data, (_key, value) => typeof value === "bigint" ? Number(value) : value ) } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // This is most likely an object that cannot be converted to a string // console.log converts this to `[object Object]` which we are doing here as well: return `[${typeof data}]` } } /** * Converts the given value of unknown type to a boolean without * the risks of any exceptions. * * @param value - The value to convert to a boolean. * * @return The converted boolean, null if the value is empty or undefined if the * value cannot be interpreted as a boolean. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function toSafeBoolean(value: any): boolean | null | undefined { if (isNullOrUndefined(value)) { return null } if (typeof value === "boolean") { return value } const cleanedValue = toSafeString(value).toLowerCase().trim() if (cleanedValue === "") { return null } else if (BOOLEAN_TRUE_VALUES.includes(cleanedValue)) { return true } else if (BOOLEAN_FALSE_VALUES.includes(cleanedValue)) { return false } // The value cannot be interpreted as boolean return undefined } /** * Converts the given value of unknown type to a number without * the risks of any exceptions. * * @param value - The value to convert to a number. * * @returns The converted number or null if the value is empty or undefined or NaN if the * value cannot be interpreted as a number. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function toSafeNumber(value: any): number | null { // TODO(lukasmasuch): Should this return null as replacement for NaN? if (isNullOrUndefined(value)) { return null } if (Array.isArray(value)) { return NaN } if (typeof value === "string") { if (value.trim().length === 0) { // Empty string should return null return null } try { // Try to convert string to number via numbro: // https://numbrojs.com/old-format.html#unformat const unformattedValue = numbro.unformat(value.trim()) if (notNullOrUndefined(unformattedValue)) { return unformattedValue } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // Do nothing here } } else if (value instanceof Int32Array) { // int values need to be extracted this way: return Number(value[0]) } return Number(value) } /** * Converts an array to a string representation suitable for copying. * * @param array - The array to convert. * @returns The string representation of the array. */ export function arrayToCopyValue(array?: object[] | null): string { if (isNullOrUndefined(array)) { return "" } return toSafeString( array.map((x: object) => // Replace commas with spaces since commas are used to // separate the list items. typeof x === "string" && (x as string).includes(",") ? (x as string).replace(/,/g, " ") : x ) ) } /** * Tries to convert a given value of unknown type to a JSON string without * the risks of any exceptions. * * @param value - The value to convert to a JSON string. * * @returns The converted JSON string or a string showing the type of the object as fallback. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function toJsonString(value: any): string { if (isNullOrUndefined(value)) { return "" } if (typeof value === "string") { // If the value is already a string, return it as-is return value } try { // Try to convert the value to a JSON string return JSON.stringify(value, (_key, val) => // BigInt are not supported by JSON.stringify // so we convert them to a number as fallback typeof val === "bigint" ? Number(val) : val ) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // If the value cannot be converted to a JSON string, return the stringified value return toSafeString(value) } } /** * Converts the given value of unknown type to a date without * the risks of any exceptions. * * Note: Unix timestamps are only supported in seconds. * * @param value - The value to convert to a date. * * @returns The converted date or null if the value cannot be interpreted as a date. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. export function toSafeDate(value: any): Date | null | undefined { if (isNullOrUndefined(value)) { return null } // Return the value as-is if it is already a date if (value instanceof Date) { if (!isNaN(value.getTime())) { return value } return undefined } if (typeof value === "string" && value.trim().length === 0) { // Empty string should return null return null } try { const parsedTimestamp = Number(value) if (!isNaN(parsedTimestamp)) { // Unix timestamps can be have different units. // As default, we handle the unit as second, but // if it larger than a certain threshold, we assume // a different unit. This is not 100% accurate, but // should be good enough since it is unlikely that // users are actually referring to years >= 5138. let timestampInSeconds = parsedTimestamp if (parsedTimestamp >= 10 ** 18) { // Assume that the timestamp is in nanoseconds // and adjust to seconds timestampInSeconds = parsedTimestamp / 1000 ** 3 } else if (parsedTimestamp >= 10 ** 15) { // Assume that the timestamp is in microseconds // and adjust to seconds timestampInSeconds = parsedTimestamp / 1000 ** 2 } else if (parsedTimestamp >= 10 ** 12) { // Assume that the timestamp is in milliseconds // and adjust to seconds timestampInSeconds = parsedTimestamp / 1000 } // Parse it as a unix timestamp in seconds const parsedMomentDate = moment.unix(timestampInSeconds).utc() if (parsedMomentDate.isValid()) { return parsedMomentDate.toDate() } } if (typeof value === "string") { // Try to parse string via momentJS: const parsedMomentDate = moment.utc(value) if (parsedMomentDate.isValid()) { return parsedMomentDate.toDate() } // The pasted value was not a valid date string // Try to interpret value as time string instead (HH:mm:ss) const parsedMomentTime = moment.utc(value, [ moment.HTML5_FMT.TIME_MS, // HH:mm:ss.SSS moment.HTML5_FMT.TIME_SECONDS, // HH:mm:ss moment.HTML5_FMT.TIME, // HH:mm ]) if (parsedMomentTime.isValid()) { return parsedMomentTime.toDate() } } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return undefined } // Unable to interpret this value as a date: return undefined } /** * Count the number of decimals in a number. * * @param {number} value - The number to count the decimals for. * * @returns {number} The number of decimals. */ export function countDecimals(value: number): number { if (value % 1 === 0) { return 0 } let numberStr = value.toString() if (numberStr.indexOf("e") !== -1) { // Handle scientific notation numberStr = value.toLocaleString("fullwide", { useGrouping: false, maximumFractionDigits: 20, }) } if (numberStr.indexOf(".") === -1) { // Fallback to 0 decimals, this can happen with // extremely large or small numbers return 0 } return numberStr.split(".")[1].length } /** * Truncates a number to a specified number of decimal places without rounding. * * @param {number} value - The number to be truncated. * @param {number} decimals - The number of decimal places to preserve after truncation. * * @returns {number} The truncated number. * * @example * truncateDecimals(3.14159265, 2); // returns 3.14 * truncateDecimals(123.456, 0); // returns 123 */ export function truncateDecimals(value: number, decimals: number): number { if (!Number.isFinite(value)) return value // keep NaN/±∞ untouched if (decimals <= 0) return Math.trunc(value) const factor = 10 ** decimals const shifted = value * factor // Add/subtract a relative ε that is just large enough to push // 451.999… → 452 (or −452.000… → −451.999…) before we truncate. const epsilon = Number.EPSILON * Math.abs(shifted) * 10 return Math.trunc(shifted + Math.sign(shifted) * epsilon) / factor } const LINE_BREAK_REGEX = new RegExp(/(\r\n|\n|\r)/gm) /** * Removes all line breaks from the given text. * @param text - The text to remove line breaks from. * @returns The text without line breaks. */ export function removeLineBreaks(text: string): string { if (text.indexOf("\n") !== -1) { return text.replace(LINE_BREAK_REGEX, " ") } return text } /** * Determines the correct value to display in a link cell based on the `href` and `regexPattern` parameters. * * @param href - The raw url value. * @param displayTextRegex - The regex pattern which will be applied to the `href`. If no match is found, then we return the `href`. * @returns - The string value to be displayed in the cell. * * * @example * const regex = new RegExp("https:\/\/(.*?)\.streamlit\.app") * const regex2 = new RegExp("https:\/\/roadmap\.(.*?)\.app") * getLinkDisplayValueFromRegex(regex, "https://roadmap.streamlit.app"); // returns "roadmap" * getLinkDisplayValueFromRegex(regex, "https://roadmap.streamlit.app"); // returns "streamlit" */ export function getLinkDisplayValueFromRegex( displayTextRegex: RegExp, href?: string | null ): string { if (isNullOrUndefined(href)) { return "" } try { // apply the regex pattern to display the value const patternMatch = href.match(displayTextRegex) if (patternMatch?.[1] !== undefined) { // return the first matching group // Since this might be a URI encoded value, we decode it. // Note: we replace + with %20 to correctly convert + to whitespaces. return decodeURIComponent(patternMatch[1].replace(/\+/g, "%20")) } // if the regex doesn't find a match with the url, just use the url as display value return href // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // if there was any error return the href return href } }