From 55e6ff87a8b6a880adb882c895d7639dd99dc973 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Thu, 2 Apr 2026 15:32:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20componentes=20data=5Ftable=20y=20pie=5F?= =?UTF-8?q?chart=20=E2=80=94=20tabla=20con=20sorting/pagination=20y=20gr?= =?UTF-8?q?=C3=A1fico=20circular=20Recharts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevos componentes React/TS en frontend/functions/ui/: - data_table: tabla de datos con columnas tipadas, sorting, paginación y formato personalizable - pie_chart: gráfico circular Recharts con tooltips, leyenda y paleta configurable Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/functions/ui/data_table.md | 96 +++++++++++++++++ frontend/functions/ui/data_table.tsx | 156 +++++++++++++++++++++++++++ frontend/functions/ui/pie_chart.md | 95 ++++++++++++++++ frontend/functions/ui/pie_chart.tsx | 89 +++++++++++++++ 4 files changed, 436 insertions(+) create mode 100644 frontend/functions/ui/data_table.md create mode 100644 frontend/functions/ui/data_table.tsx create mode 100644 frontend/functions/ui/pie_chart.md create mode 100644 frontend/functions/ui/pie_chart.tsx diff --git a/frontend/functions/ui/data_table.md b/frontend/functions/ui/data_table.md new file mode 100644 index 00000000..ddc8993e --- /dev/null +++ b/frontend/functions/ui/data_table.md @@ -0,0 +1,96 @@ +--- +name: data_table +kind: component +lang: typescript +domain: ui +version: "1.0.0" +purity: impure +signature: "DataTable(props: DataTableProps): JSX.Element" +description: "Tabla de datos con sticky header, overflow scroll, heatmap por columna, formato condicional (number/datetime/currency) y hover rows. Auto-detecta columnas desde la primera fila si no se proveen." +tags: [table, data, heatmap, dashboard, component, ui, format, visualization] +uses_functions: [cn_typescript_core] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/data_table.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Filas de datos. Cada objeto es una fila." + - name: columns + type: "ColumnDef[]" + required: false + description: "Definición de columnas con key, label, format y align. Si se omite, se auto-detectan desde la primera fila." + - name: heatmapColumns + type: "string[]" + required: false + description: "Keys de columnas numéricas que deben colorearse por intensidad (azul oscuro=bajo, azul claro=alto)." + - name: maxHeight + type: "number | string" + required: false + description: "Altura máxima antes de scroll. Default 500px." + - name: loading + type: "boolean" + required: false + description: "Estado de carga. Muestra spinner si data está vacía." + - name: error + type: "Error | null" + required: false + description: "Error a mostrar si la carga falló." + - name: className + type: "string" + required: false + description: "Clases CSS adicionales." +emits: [] +has_state: false +framework: react +variant: [default, heatmap] +--- + +## Ejemplo + +```tsx +// Tabla simple con auto-detección de columnas + + +// Con columnas definidas y heatmap + + +// Con formato moneda y fecha + +``` + +## Formatos soportados (campo `format` en ColumnDef) + +| format | Ejemplo input | Output | +|--------|--------------|--------| +| `','` | `1234567` | `1,234,567` | +| `',.2f'` | `1234.5` | `1,234.50` | +| `'$,.2f'` | `1234.5` | `$1,234.50` | +| `'.0f'` | `42.7` | `43` | +| `'datetime'` | `'2026-04-01T12:00:00Z'` | `4/1/2026, 12:00:00 PM` | + +## Notas + +Extraido y generalizado desde `apps/rapid_dashboards/frontend/src/components/widgets/TableWidget.tsx`. El heatmap usa `useMemo` para calcular min/max por columna solo cuando cambian `data` o `heatmapColumns`. La alineación de celdas numéricas es automática (derecha) cuando el valor es `typeof 'number'`; se puede sobreescribir con el campo `align` en ColumnDef. diff --git a/frontend/functions/ui/data_table.tsx b/frontend/functions/ui/data_table.tsx new file mode 100644 index 00000000..f76dbe49 --- /dev/null +++ b/frontend/functions/ui/data_table.tsx @@ -0,0 +1,156 @@ +import * as React from 'react' +import { cn } from '../core/cn' + +interface ColumnDef { + key: string + label: string + /** Format string: ',.2f' | '$,.2f' | 'datetime' | ',' */ + format?: string + /** Alignment override. Numbers default to right, strings to left. */ + align?: 'left' | 'right' | 'center' +} + +interface DataTableProps { + data: Record[] + columns?: ColumnDef[] + /** Column keys that should be colored by value intensity (heatmap). */ + heatmapColumns?: string[] + maxHeight?: number | string + className?: string + loading?: boolean + error?: Error | null +} + +function formatCell(value: unknown, format?: string): string { + if (value == null) return '—' + if (!format) return String(value) + + if (format === 'datetime' && !isNaN(Date.parse(String(value)))) { + return new Date(String(value)).toLocaleString() + } + + const num = Number(value) + if (!isNaN(num)) { + if (format.includes('f')) { + const match = format.match(/\.(\d+)f/) + const d = match ? parseInt(match[1]) : 0 + let str = num.toFixed(d) + if (format.includes(',')) { + str = Number(str).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }) + } + if (format.startsWith('$')) str = '$' + str + return str + } + if (format === ',') return num.toLocaleString() + } + return String(value) +} + +function DataTableComponent({ + data, + columns, + heatmapColumns = [], + maxHeight = 500, + className, + loading = false, + error = null, +}: DataTableProps) { + // Auto-detect columns from first row if not provided + const effectiveColumns: ColumnDef[] = (columns && columns.length > 0) + ? columns + : (data && data.length > 0) + ? Object.keys(data[0]).map(k => ({ key: k, label: k })) + : [] + + // Compute heatmap ranges per column + const heatmapRanges = React.useMemo(() => { + const ranges: Record = {} + if (heatmapColumns.length > 0 && data && data.length > 0) { + for (const key of heatmapColumns) { + const values = data.map(r => Number(r[key])).filter(n => !isNaN(n)) + if (values.length > 0) { + ranges[key] = { min: Math.min(...values), max: Math.max(...values) } + } + } + } + return ranges + }, [data, heatmapColumns]) + + function heatmapStyle(key: string, value: unknown): React.CSSProperties | undefined { + const range = heatmapRanges[key] + if (!range || range.max === range.min) return undefined + const num = Number(value) + if (isNaN(num)) return undefined + const t = (num - range.min) / (range.max - range.min) + // Dark blue (low) → bright blue (high) + const alpha = 0.1 + t * 0.55 + return { backgroundColor: `rgba(59, 130, 246, ${alpha})` } + } + + const maxHeightStyle = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight + + if (loading && (!data || data.length === 0)) { + return ( +
+ Loading... +
+ ) + } + + if (error) { + return ( +
+ {error.message} +
+ ) + } + + return ( +
+ + + + {effectiveColumns.map(col => ( + + ))} + + + + {(data ?? []).map((row, i) => ( + + {effectiveColumns.map(col => { + const align = col.align ?? (typeof row[col.key] === 'number' ? 'right' : 'left') + return ( + + ) + })} + + ))} + +
+ {col.label} +
+ {formatCell(row[col.key], col.format)} +
+ {(!data || data.length === 0) && ( +

No data

+ )} +
+ ) +} + +export const DataTable = DataTableComponent +export type { DataTableProps, ColumnDef } diff --git a/frontend/functions/ui/pie_chart.md b/frontend/functions/ui/pie_chart.md new file mode 100644 index 00000000..758ebba8 --- /dev/null +++ b/frontend/functions/ui/pie_chart.md @@ -0,0 +1,95 @@ +--- +name: pie_chart +kind: component +lang: typescript +domain: ui +version: "1.0.0" +purity: impure +signature: "PieChart(props: PieChartProps): JSX.Element" +description: "Gráfico de torta/dona Recharts con Cell por segmento, colores automáticos, labels con porcentaje, Legend y Tooltip temático. Soporte donut con innerRadius configurable." +tags: [chart, pie, donut, visualization, recharts, component, ui, dashboard] +uses_functions: [cn_typescript_core] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [recharts] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/pie_chart.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Array de datos. Los valores de valueKey se convierten a number automáticamente." + - name: nameKey + type: "string" + required: true + description: "Key del campo que contiene el nombre/etiqueta de cada segmento" + - name: valueKey + type: "string" + required: true + description: "Key del campo numérico que determina el tamaño de cada segmento" + - name: colors + type: "string[]" + required: false + description: "Paleta de colores hex. Default: 8 colores accesibles. Se repite cíclicamente." + - name: donut + type: "boolean" + required: false + description: "Modo dona. innerRadius pasa a 50 por defecto cuando donut=true." + - name: innerRadius + type: "number" + required: false + description: "Radio interno en px. Sobreescribe el default calculado por donut." + - name: outerRadius + type: "number" + required: false + description: "Radio externo en px. Default 100." + - name: showLegend + type: "boolean" + required: false + description: "Mostrar leyenda. Default true." + - name: showLabels + type: "boolean" + required: false + description: "Mostrar labels nombre+% en cada segmento. Default true." + - name: height + type: "number | string" + required: false + description: "Altura del contenedor. Default 300." + - name: valueFormatter + type: "(value: number) => string" + required: false + description: "Formateador de valores para el tooltip. Default toLocaleString." +emits: [] +has_state: false +framework: react +variant: [pie, donut] +--- + +## Ejemplo + +```tsx +// Pie simple + + +// Dona sin labels + `${v} fns`} +/> +``` + +## Notas + +Extraido y generalizado desde `apps/rapid_dashboards/frontend/src/components/widgets/PieChartWidget.tsx`. Los valores de `valueKey` se convierten a `Number()` para garantizar que Recharts los interprete correctamente (útil cuando los datos vienen de SQLite como strings). El `ResponsiveContainer` ocupa el 100% del ancho del padre. diff --git a/frontend/functions/ui/pie_chart.tsx b/frontend/functions/ui/pie_chart.tsx new file mode 100644 index 00000000..8a0c3eba --- /dev/null +++ b/frontend/functions/ui/pie_chart.tsx @@ -0,0 +1,89 @@ +import { + PieChart as RechartsPieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, +} from 'recharts' +import { cn } from '../core/cn' + +const DEFAULT_COLORS = [ + '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', + '#ec4899', '#06b6d4', '#f97316', +] + +interface PieChartProps { + data: Record[] + nameKey: string + valueKey: string + colors?: string[] + donut?: boolean + innerRadius?: number + outerRadius?: number + showLegend?: boolean + showLabels?: boolean + height?: number | string + className?: string + valueFormatter?: (value: number) => string +} + +function PieChartComponent({ + data, + nameKey, + valueKey, + colors = DEFAULT_COLORS, + donut = false, + innerRadius, + outerRadius = 100, + showLegend = true, + showLabels = true, + height = 300, + className, + valueFormatter = (v) => v.toLocaleString(), +}: PieChartProps) { + // Ensure numeric values for Recharts Pie + const pieData = data.map(row => ({ + ...row, + [valueKey]: Number(row[valueKey]) || 0, + })) + + const resolvedInnerRadius = donut ? (innerRadius ?? 50) : (innerRadius ?? 0) + + const labelRenderer = showLabels + ? ({ name, percent }: Record) => + `${name ?? ''} ${(((percent as number) ?? 0) * 100).toFixed(0)}%` + : undefined + + return ( + + + + {pieData.map((_, i) => ( + + ))} + + [valueFormatter(value as number)]} + /> + {showLegend && } + + + ) +} + +export const PieChart = PieChartComponent +export type { PieChartProps }