import type { ColumnFormatType, OutputColumnMetadata } from "@internal/tsql"; import { Hash } from "lucide-react"; import { useMemo } from "react"; import type { BigNumberAggregationType, BigNumberConfiguration, } from "~/components/metrics/QueryWidget"; import { createValueFormatter } from "~/utils/columnFormat"; import { AnimatedNumber } from "../AnimatedNumber"; import { ChartBlankState } from "./ChartBlankState"; import { Spinner } from "../Spinner"; interface BigNumberCardProps { rows: Record[]; columns: OutputColumnMetadata[]; config: BigNumberConfiguration; isLoading?: boolean; } /** * Extracts numeric values from a specific column across all rows, * optionally sorting them first. */ function extractColumnValues( rows: Record[], column: string, sortDirection?: "asc" | "desc" ): number[] { const values: number[] = []; const sortedRows = sortDirection ? [...rows].sort((a, b) => { const aVal = toNumber(a[column]); const bVal = toNumber(b[column]); return sortDirection === "asc" ? aVal - bVal : bVal - aVal; }) : rows; for (const row of sortedRows) { const val = row[column]; if (typeof val === "number") { values.push(val); } else if (typeof val === "string") { const parsed = parseFloat(val); if (!isNaN(parsed)) { values.push(parsed); } } } return values; } function toNumber(value: unknown): number { if (typeof value === "number") return value; if (typeof value === "string") { const parsed = parseFloat(value); return isNaN(parsed) ? 0 : parsed; } return 0; } /** * Aggregate an array of numbers using the specified aggregation function */ function aggregateValues(values: number[], aggregation: BigNumberAggregationType): number { if (values.length === 0) return 0; switch (aggregation) { case "sum": return values.reduce((a, b) => a + b, 0); case "avg": return values.reduce((a, b) => a + b, 0) / values.length; case "count": return values.length; case "min": return Math.min(...values); case "max": return Math.max(...values); case "first": return values[0]; case "last": return values[values.length - 1]; } } /** * Computes the display value and unit suffix for abbreviated display. * Returns the divided-down number (e.g. 1.5 for 1500) and the suffix (e.g. "K"), * along with the appropriate decimal places for formatting. */ function abbreviateValue(value: number): { displayValue: number; unitSuffix?: string; decimalPlaces: number; } { if (Math.abs(value) >= 1_000_000_000) { const v = value / 1_000_000_000; return { displayValue: v, unitSuffix: "B", decimalPlaces: v % 1 === 0 ? 0 : 1 }; } if (Math.abs(value) >= 1_000_000) { const v = value / 1_000_000; return { displayValue: v, unitSuffix: "M", decimalPlaces: v % 1 === 0 ? 0 : 1 }; } if (Math.abs(value) >= 1_000) { const v = value / 1_000; return { displayValue: v, unitSuffix: "K", decimalPlaces: v % 1 === 0 ? 0 : 1 }; } return { displayValue: value, decimalPlaces: getDecimalPlaces(value) }; } /** * Determines decimal places for plain (non-abbreviated) display. */ function getDecimalPlaces(value: number): number { if (Number.isInteger(value)) return 0; const abs = Math.abs(value); if (abs >= 100) return 0; if (abs >= 10) return 1; if (abs >= 1) return 2; if (abs >= 0.01) return 3; return 4; } export function BigNumberCard({ rows, columns, config, isLoading = false }: BigNumberCardProps) { const { column, aggregation, sortDirection, abbreviate = true, prefix, suffix } = config; const result = useMemo(() => { if (rows.length === 0) return null; const values = extractColumnValues(rows, column, sortDirection); if (values.length === 0) return null; return aggregateValues(values, aggregation); }, [rows, column, aggregation, sortDirection]); // Look up column format for format-aware display const columnValueFormatter = useMemo(() => { const columnMeta = columns.find((c) => c.name === column); const formatType = (columnMeta?.format ?? columnMeta?.customRenderType) as | ColumnFormatType | undefined; return createValueFormatter(formatType); }, [columns, column]); if (isLoading) { return (
); } if (result === null) { return ; } // Use format-aware formatter when available if (columnValueFormatter) { return (
{prefix && {prefix}} {columnValueFormatter(result)} {suffix && {suffix}}
); } const { displayValue, unitSuffix, decimalPlaces } = abbreviate ? abbreviateValue(result) : { displayValue: result, unitSuffix: undefined, decimalPlaces: getDecimalPlaces(result) }; return (
{prefix && {prefix}} {(unitSuffix || suffix) && ( {unitSuffix} {unitSuffix && suffix ? " " : ""} {suffix} )}
); }