forked from mengyxu/noob-components
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
4.9 KiB
162 lines
4.9 KiB
/** |
|
* usePretextColumnWidths |
|
* |
|
* Computes flex-based column width parameters using pretext text measurement. |
|
* - flexBasis: mean (μ) of measured widths + padding |
|
* - flexGrow/flexShrink: derived from variance (σ²) |
|
* - minWidth: max(μ - 2*σ, 50) + padding |
|
* - maxWidth: 300px |
|
* |
|
* User can override any parameter via column definition. |
|
*/ |
|
import { computed, type Ref } from "vue"; |
|
import { measureShrinkWrapWidth } from "./measureText"; |
|
import type { ListTableColumn } from "./types"; |
|
|
|
const DEFAULT_FONT = "14px Inter, sans-serif"; |
|
const DEFAULT_HEADER_FONT = "bold 14px Inter, sans-serif"; |
|
const DEFAULT_PADDING = 16; |
|
const CELL_PADDING = DEFAULT_PADDING * 2; // left + right |
|
const MAX_WIDTH = 300; |
|
const MIN_BASE_WIDTH = 50; |
|
|
|
export interface ColumnFlexConfig { |
|
key: string; |
|
flexBasis: number; |
|
flexGrow: number; |
|
flexShrink: number; |
|
minWidth: number; |
|
maxWidth: number; |
|
measuredMean: number; |
|
measuredVariance: number; |
|
measuredSampleCount: number; |
|
} |
|
|
|
/** |
|
* Sample rows evenly distributed across the dataset. |
|
* Takes first, last, and evenly spaced rows in between. |
|
*/ |
|
function sampleRows<T>(data: T[], sampleSize: number): T[] { |
|
if (data.length <= sampleSize) return data; |
|
const result: T[] = []; |
|
const step = (data.length - 1) / (sampleSize - 1); |
|
for (let i = 0; i < sampleSize; i++) { |
|
result.push(data[Math.round(i * step)]); |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Compute mean and variance of an array of numbers |
|
*/ |
|
function computeStats(widths: number[]): { mean: number; variance: number } { |
|
if (widths.length === 0) return { mean: 0, variance: 0 }; |
|
|
|
const mean = widths.reduce((sum, w) => sum + w, 0) / widths.length; |
|
const variance = |
|
widths.reduce((sum, w) => sum + Math.pow(w - mean, 2), 0) / widths.length; |
|
|
|
return { mean, variance }; |
|
} |
|
|
|
/** |
|
* Derive flexGrow/flexShrink from variance score |
|
* varianceScore = min(1, σ / (μ * 0.5)) |
|
* flex = 0.1 + varianceScore * 1.9 → range [0.1, 2.0] |
|
*/ |
|
function varianceToFlex(mean: number, variance: number): number { |
|
if (mean <= 0) return 0.1; |
|
const stdDev = Math.sqrt(variance); |
|
const varianceScore = Math.min(1, stdDev / (mean * 0.5)); |
|
return 0.1 + varianceScore * 1.9; |
|
} |
|
|
|
export function usePretextColumnWidths<T>( |
|
data: Ref<T[]>, |
|
columns: Ref<ListTableColumn<T>[]>, |
|
containerWidth: Ref<number>, |
|
options?: { |
|
font?: string; |
|
headerFont?: string; |
|
sampleSize?: number; |
|
} |
|
) { |
|
const font = options?.font ?? DEFAULT_FONT; |
|
const headerFont = options?.headerFont ?? DEFAULT_HEADER_FONT; |
|
const sampleSize = options?.sampleSize ?? 100; |
|
|
|
const computedConfigs = computed<ColumnFlexConfig[]>(() => { |
|
if (!columns.value.length) return []; |
|
|
|
const sampled = sampleRows(data.value, sampleSize); |
|
const configs: ColumnFlexConfig[] = []; |
|
|
|
for (const col of columns.value) { |
|
const colKey = String(col.key); |
|
|
|
// User overrides (convert string values to numbers) |
|
const userFlexGrow = col.flexGrow; |
|
const userFlexShrink = col.flexShrink; |
|
const userFlexBasis = |
|
col.flexBasis !== undefined && col.flexBasis !== "auto" |
|
? Number(col.flexBasis) |
|
: undefined; |
|
const userMinWidth = |
|
col.minWidth !== undefined ? Number(col.minWidth) : undefined; |
|
const userMaxWidth = |
|
col.maxWidth !== undefined ? Number(col.maxWidth) : undefined; |
|
|
|
// Measure header width |
|
const headerText = col.name || col.i18n || colKey; |
|
const headerWidth = measureShrinkWrapWidth(headerText, headerFont) + CELL_PADDING; |
|
|
|
// Measure sampled cell widths |
|
const cellWidths: number[] = []; |
|
for (const row of sampled) { |
|
const rawValue = (row as any)[col.dataKey || colKey]; |
|
const cellText = rawValue == null ? "" : String(rawValue); |
|
if (cellText) { |
|
const w = measureShrinkWrapWidth(cellText, font) + CELL_PADDING; |
|
cellWidths.push(w); |
|
} |
|
} |
|
|
|
// Include header in stats |
|
const allWidths = [headerWidth, ...cellWidths]; |
|
const { mean, variance } = computeStats(allWidths); |
|
|
|
// Derived values |
|
const flexBasis = userFlexBasis ?? mean; |
|
const minWidth = userMinWidth |
|
? Number(userMinWidth) |
|
: Math.max(mean - 2 * Math.sqrt(variance), MIN_BASE_WIDTH) + CELL_PADDING; |
|
const maxWidth = userMaxWidth ? Number(userMaxWidth) : MAX_WIDTH; |
|
const flexGrow = userFlexGrow ?? varianceToFlex(mean, variance); |
|
const flexShrink = userFlexShrink ?? varianceToFlex(mean, variance); |
|
|
|
configs.push({ |
|
key: colKey, |
|
flexBasis, |
|
flexGrow, |
|
flexShrink, |
|
minWidth, |
|
maxWidth, |
|
measuredMean: mean, |
|
measuredVariance: variance, |
|
measuredSampleCount: allWidths.length, |
|
}); |
|
} |
|
|
|
return configs; |
|
}); |
|
|
|
// Total flex basis (sum of all flexBasis values) |
|
const totalFlexBasis = computed(() => |
|
computedConfigs.value.reduce((sum, c) => sum + c.flexBasis, 0) |
|
); |
|
|
|
return { |
|
computedConfigs, |
|
totalFlexBasis, |
|
}; |
|
}
|
|
|