feat(@fn_library): extract 2 components + improve 2 from Claude Design export
From: sources/frontend_designs/Ads Analytics Dashboard _standalone_.html New components: - funnel_chart_ts_ui — visualización de funnel de conversión con barras degradadas y tasa entre etapas como Badge semántico. - heatmap_grid_ts_ui — matriz rows × cols con intensidad color-mix sobre el primary color. Genérica (day×hour, cohort, correlation...). Improvements: - alert_ts_ui v1.1.0 — añadidas variantes semánticas success, warning, info (antes: solo default y destructive). - data_table_ts_ui v1.1.0 — prop opcional density: compact | cozy | roomy. No rompe API existente (default 'cozy' = comportamiento previo). Barrel frontend/functions/ui/index.ts actualizado con los dos nuevos exports y el type DataTableDensity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,10 @@ name: alert
|
||||
kind: component
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "Alert(props: { variant?: 'default' | 'destructive' }): JSX.Element"
|
||||
description: "Alerta accesible con variantes default y destructive. Mantine Alert con slots para título, descripción y acción."
|
||||
signature: "Alert(props: { variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info' }): JSX.Element"
|
||||
description: "Alerta accesible con 5 variantes semánticas (default, destructive, success, warning, info). Mantine Alert con slots para título, descripción y acción."
|
||||
tags: [alert, feedback, component, ui, notification, mantine]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -21,13 +21,13 @@ test_file_path: ""
|
||||
file_path: "frontend/functions/ui/alert.tsx"
|
||||
props:
|
||||
- name: variant
|
||||
type: "'default' | 'destructive'"
|
||||
type: "'default' | 'destructive' | 'success' | 'warning' | 'info'"
|
||||
required: false
|
||||
description: "Variante visual"
|
||||
description: "Variante visual semántica."
|
||||
emits: []
|
||||
has_state: false
|
||||
framework: react
|
||||
variant: [default, destructive]
|
||||
variant: [default, destructive, success, warning, info]
|
||||
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
|
||||
source_license: "MIT"
|
||||
source_file: "frontend/src/components/ui/alert.tsx"
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import { Alert as MantineAlert, Box, Text } from '@mantine/core'
|
||||
|
||||
type AlertVariant = 'default' | 'destructive'
|
||||
type AlertVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'
|
||||
|
||||
const variantColorMap: Record<AlertVariant, string | undefined> = {
|
||||
default: undefined,
|
||||
destructive: 'red',
|
||||
success: 'green',
|
||||
warning: 'yellow',
|
||||
info: 'blue',
|
||||
}
|
||||
|
||||
function Alert({
|
||||
|
||||
@@ -3,10 +3,10 @@ name: data_table
|
||||
kind: component
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
version: "1.1.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."
|
||||
description: "Tabla de datos con sticky header, overflow scroll, heatmap por columna, formato condicional (number/datetime/currency), hover rows y densidad configurable (compact/cozy/roomy). Auto-detecta columnas desde la primera fila si no se proveen."
|
||||
tags: [table, data, heatmap, dashboard, component, ui, format, visualization]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -44,6 +44,10 @@ props:
|
||||
type: "Error | null"
|
||||
required: false
|
||||
description: "Error a mostrar si la carga falló."
|
||||
- name: density
|
||||
type: "'compact' | 'cozy' | 'roomy'"
|
||||
required: false
|
||||
description: "Padding vertical y horizontal de filas. compact=4/xs, cozy=6/sm (default), roomy=10/md."
|
||||
emits: []
|
||||
has_state: false
|
||||
framework: react
|
||||
|
||||
@@ -10,6 +10,8 @@ interface ColumnDef {
|
||||
align?: 'left' | 'right' | 'center'
|
||||
}
|
||||
|
||||
type DataTableDensity = 'compact' | 'cozy' | 'roomy'
|
||||
|
||||
interface DataTableProps {
|
||||
data: Record<string, unknown>[]
|
||||
columns?: ColumnDef[]
|
||||
@@ -18,6 +20,14 @@ interface DataTableProps {
|
||||
maxHeight?: number | string
|
||||
loading?: boolean
|
||||
error?: Error | null
|
||||
/** Row padding preset. compact=4/8, cozy=6/12 (default), roomy=10/16. */
|
||||
density?: DataTableDensity
|
||||
}
|
||||
|
||||
const DENSITY_MAP: Record<DataTableDensity, { py: number; px: 'xs' | 'sm' | 'md' }> = {
|
||||
compact: { py: 4, px: 'xs' },
|
||||
cozy: { py: 6, px: 'sm' },
|
||||
roomy: { py: 10, px: 'md' },
|
||||
}
|
||||
|
||||
function formatCell(value: unknown, format?: string): string {
|
||||
@@ -52,7 +62,9 @@ function DataTableComponent({
|
||||
maxHeight = 500,
|
||||
loading = false,
|
||||
error = null,
|
||||
density = 'cozy',
|
||||
}: DataTableProps) {
|
||||
const pad = DENSITY_MAP[density]
|
||||
// Auto-detect columns from first row if not provided
|
||||
const effectiveColumns: ColumnDef[] = (columns && columns.length > 0)
|
||||
? columns
|
||||
@@ -113,8 +125,8 @@ function DataTableComponent({
|
||||
fw={500}
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
py={6}
|
||||
px="sm"
|
||||
py={pad.py}
|
||||
px={pad.px}
|
||||
>
|
||||
{col.label}
|
||||
</Table.Th>
|
||||
@@ -131,8 +143,8 @@ function DataTableComponent({
|
||||
key={col.key}
|
||||
style={{ textAlign: align, fontFamily: 'var(--mantine-font-family-monospace)', ...heatmapStyle(col.key, row[col.key]) }}
|
||||
fz="xs"
|
||||
py={6}
|
||||
px="sm"
|
||||
py={pad.py}
|
||||
px={pad.px}
|
||||
>
|
||||
{formatCell(row[col.key], col.format)}
|
||||
</Table.Td>
|
||||
@@ -152,4 +164,4 @@ function DataTableComponent({
|
||||
}
|
||||
|
||||
export const DataTable = DataTableComponent
|
||||
export type { DataTableProps, ColumnDef }
|
||||
export type { DataTableProps, ColumnDef, DataTableDensity }
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: funnel_chart
|
||||
kind: component
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "FunnelChart(props: FunnelChartProps): JSX.Element"
|
||||
description: "Visualiza un funnel de conversión con barras degradadas, valores formateados y tasa de conversión entre etapas como Badge semántico. Genérico — acepta cualquier array {stage,value}."
|
||||
tags: [funnel, conversion, dashboard, component, ui, chart, acquisition, analytics]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["@mantine/core", react]
|
||||
output: "Componente FunnelChart que renderiza un funnel vertical de conversión con porcentajes entre etapas."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "frontend/functions/ui/funnel_chart.tsx"
|
||||
props:
|
||||
- name: data
|
||||
type: "FunnelStage[]"
|
||||
required: true
|
||||
description: "Etapas del funnel. Cada etapa con { stage: string, value: number }."
|
||||
- name: valueFormatter
|
||||
type: "(value: number) => string"
|
||||
required: false
|
||||
description: "Formateador del valor absoluto. Default: compact (K/M/B)."
|
||||
- name: showConversion
|
||||
type: "boolean"
|
||||
required: false
|
||||
description: "Mostrar tasa de conversión entre etapas como Badge. Default true."
|
||||
- name: barHeight
|
||||
type: "number"
|
||||
required: false
|
||||
description: "Alto de cada barra en px. Default 28."
|
||||
- name: goodThreshold
|
||||
type: "number"
|
||||
required: false
|
||||
description: "Tasa de conversión (%) por encima de la cual el Badge es 'success'. Default 30."
|
||||
- name: warnThreshold
|
||||
type: "number"
|
||||
required: false
|
||||
description: "Tasa de conversión (%) por encima de la cual el Badge es 'info' (por debajo → 'warning'). Default 5."
|
||||
- name: barLabel
|
||||
type: "(stage: FunnelStage, pctOfMax: number) => string"
|
||||
required: false
|
||||
description: "Texto opcional dentro de cada barra."
|
||||
- name: barColor
|
||||
type: "string"
|
||||
required: false
|
||||
description: "Color o gradient CSS a aplicar a las barras. Default: gradient indigo→cyan."
|
||||
emits: []
|
||||
has_state: false
|
||||
framework: react
|
||||
variant: [default]
|
||||
source_repo: "claude.ai/design"
|
||||
source_license: ""
|
||||
source_file: "sources/frontend_designs/Ads Analytics Dashboard _standalone_.html"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```tsx
|
||||
import { FunnelChart } from '@fn_library'
|
||||
|
||||
const funnel = [
|
||||
{ stage: 'Impressions', value: 12_400_000 },
|
||||
{ stage: 'Clicks', value: 214_807 },
|
||||
{ stage: 'Sessions', value: 186_904 },
|
||||
{ stage: 'Add to cart', value: 24_113 },
|
||||
{ stage: 'Checkout', value: 9_642 },
|
||||
{ stage: 'Conversions', value: 4_812 },
|
||||
]
|
||||
|
||||
// Básico — conversión automática entre etapas
|
||||
<FunnelChart data={funnel} />
|
||||
|
||||
// Moneda + altura mayor
|
||||
<FunnelChart
|
||||
data={funnel}
|
||||
valueFormatter={(n) => '$' + n.toLocaleString()}
|
||||
barHeight={36}
|
||||
/>
|
||||
|
||||
// Umbrales custom de conversión (ej. funnel de lead gen donde >10% ya es bueno)
|
||||
<FunnelChart
|
||||
data={leadFunnel}
|
||||
goodThreshold={10}
|
||||
warnThreshold={2}
|
||||
/>
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- La barra se dimensiona como porcentaje del valor máximo (primera etapa por defecto).
|
||||
- La tasa de conversión se calcula contra la **etapa anterior**, no contra el máximo.
|
||||
- Extraído de un export de Claude Design (Ads Analytics Dashboard). El `AdsFunnel` original estaba hardcodeado con gradient indigo→cyan; aquí se generaliza con `barColor` y `barLabel`.
|
||||
- Los tres colores semánticos del Badge (`success`/`info`/`warning`) son configurables vía `goodThreshold` y `warnThreshold`.
|
||||
@@ -0,0 +1,106 @@
|
||||
import * as React from 'react'
|
||||
import { Stack, Group, Text, Box } from '@mantine/core'
|
||||
import { Badge } from './badge'
|
||||
|
||||
interface FunnelStage {
|
||||
stage: string
|
||||
value: number
|
||||
}
|
||||
|
||||
interface FunnelChartProps {
|
||||
data: FunnelStage[]
|
||||
/** Format function for the absolute value shown next to each stage. */
|
||||
valueFormatter?: (value: number) => string
|
||||
/** Show the inter-stage conversion rate as a badge. Default true. */
|
||||
showConversion?: boolean
|
||||
/** Height of each bar. Default 28. */
|
||||
barHeight?: number
|
||||
/** Conversion rate (%) above which the badge is `success`. Default 30. */
|
||||
goodThreshold?: number
|
||||
/** Conversion rate (%) above which the badge is `info` (below → `warning`). Default 5. */
|
||||
warnThreshold?: number
|
||||
/** Label shown inside each bar. `(stage, pctOfMax) => string`. Default empty. */
|
||||
barLabel?: (stage: FunnelStage, pctOfMax: number) => string
|
||||
/** Override the default gradient. Pass a single color or CSS gradient string. */
|
||||
barColor?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
function defaultFormatter(n: number): string {
|
||||
const abs = Math.abs(n)
|
||||
if (abs >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, '') + 'B'
|
||||
if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
|
||||
if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'K'
|
||||
return String(Math.round(n))
|
||||
}
|
||||
|
||||
const DEFAULT_BAR_GRADIENT =
|
||||
'linear-gradient(90deg, var(--mantine-primary-color-filled), color-mix(in oklab, var(--mantine-color-cyan-4) 70%, var(--mantine-primary-color-filled)))'
|
||||
|
||||
function FunnelChart({
|
||||
data,
|
||||
valueFormatter = defaultFormatter,
|
||||
showConversion = true,
|
||||
barHeight = 28,
|
||||
goodThreshold = 30,
|
||||
warnThreshold = 5,
|
||||
barLabel,
|
||||
barColor,
|
||||
className,
|
||||
}: FunnelChartProps) {
|
||||
if (!data || data.length === 0) return null
|
||||
|
||||
const max = Math.max(...data.map(d => d.value))
|
||||
|
||||
return (
|
||||
<Stack gap="xs" className={className}>
|
||||
{data.map((s, i) => {
|
||||
const pct = max === 0 ? 0 : s.value / max
|
||||
const convRate = i > 0 ? (s.value / (data[i - 1]!.value || 1)) * 100 : 100
|
||||
const conversionVariant =
|
||||
convRate > goodThreshold ? 'success' : convRate > warnThreshold ? 'info' : 'warning'
|
||||
const label = barLabel?.(s, pct)
|
||||
|
||||
return (
|
||||
<div key={s.stage}>
|
||||
<Group justify="space-between" mb={4}>
|
||||
<Text size="xs" c="dimmed">
|
||||
{s.stage}
|
||||
</Text>
|
||||
<Group gap={8}>
|
||||
<Text size="xs" fw={600}>
|
||||
{valueFormatter(s.value)}
|
||||
</Text>
|
||||
{showConversion && i > 0 && (
|
||||
<Badge size="sm" variant={conversionVariant}>
|
||||
{convRate.toFixed(1)}%
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
<Box
|
||||
style={{
|
||||
height: barHeight,
|
||||
width: `${pct * 100}%`,
|
||||
background: barColor ?? DEFAULT_BAR_GRADIENT,
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: 'white',
|
||||
transition: 'width 400ms ease',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { FunnelChart }
|
||||
export type { FunnelChartProps, FunnelStage }
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: heatmap_grid
|
||||
kind: component
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "HeatmapGrid(props: HeatmapGridProps): JSX.Element"
|
||||
description: "Matriz rows × columns con intensidad de color proporcional al valor. Genérico — casos típicos: day×hour, cohort retention, matriz de correlación, heatmap geográfico."
|
||||
tags: [heatmap, matrix, dashboard, component, ui, chart, retention, cohort]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["@mantine/core", react]
|
||||
output: "Componente HeatmapGrid que renderiza una tabla de celdas coloreadas por intensidad con labels formateados."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "frontend/functions/ui/heatmap_grid.tsx"
|
||||
props:
|
||||
- name: rows
|
||||
type: "Record<string, unknown>[]"
|
||||
required: true
|
||||
description: "Filas de datos. Cada objeto contiene el label de fila (bajo rowKey) y los valores (bajo las keys de columns)."
|
||||
- name: rowKey
|
||||
type: "string"
|
||||
required: true
|
||||
description: "Key en cada row donde está el label de la fila (ej. 'day', 'cohort')."
|
||||
- name: columns
|
||||
type: "HeatmapColumn[]"
|
||||
required: true
|
||||
description: "Columnas a visualizar, en orden. Cada una: { key, label? }."
|
||||
- name: valueFormatter
|
||||
type: "(v: number) => string"
|
||||
required: false
|
||||
description: "Formateador del valor numérico. Default: int o 2 decimales."
|
||||
- name: rowLabelFormatter
|
||||
type: "(row: Record<string, unknown>) => string"
|
||||
required: false
|
||||
description: "Formatear el label de fila. Default: String(row[rowKey])."
|
||||
- name: tooltip
|
||||
type: "(row, column, value) => string"
|
||||
required: false
|
||||
description: "Generador del tooltip (title HTML nativo)."
|
||||
- name: cellLabelThreshold
|
||||
type: "number"
|
||||
required: false
|
||||
description: "Mostrar el valor dentro de la celda solo si |v| ≥ threshold. Default: siempre mostrar."
|
||||
- name: cellSize
|
||||
type: "number"
|
||||
required: false
|
||||
description: "Tamaño de cada celda en px. Default 22."
|
||||
- name: colLabelEvery
|
||||
type: "number"
|
||||
required: false
|
||||
description: "Mostrar label de columna cada N columnas (grids densos). Default 1."
|
||||
- name: intensityRange
|
||||
type: "[number, number]"
|
||||
required: false
|
||||
description: "Min/max % del baseColor aplicado vía color-mix. Default [6, 84]."
|
||||
- name: baseColor
|
||||
type: "string"
|
||||
required: false
|
||||
description: "Color base. Default 'var(--mantine-primary-color-filled)'."
|
||||
emits: []
|
||||
has_state: false
|
||||
framework: react
|
||||
variant: [default]
|
||||
source_repo: "claude.ai/design"
|
||||
source_license: ""
|
||||
source_file: "sources/frontend_designs/Ads Analytics Dashboard _standalone_.html"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```tsx
|
||||
import { HeatmapGrid } from '@fn_library'
|
||||
|
||||
// CTR por día × hora (caso del export original)
|
||||
const hours = Array.from({ length: 24 }, (_, i) => ({ key: 'h' + i, label: String(i).padStart(2, '0') }))
|
||||
const rows = [
|
||||
{ day: 'Mon', h0: 0.8, h1: 0.7, /* ... */ h23: 1.4 },
|
||||
{ day: 'Tue', h0: 0.9, h1: 0.8, /* ... */ h23: 1.3 },
|
||||
// ...
|
||||
]
|
||||
|
||||
<HeatmapGrid
|
||||
rows={rows}
|
||||
rowKey="day"
|
||||
columns={hours}
|
||||
colLabelEvery={2}
|
||||
valueFormatter={(v) => v.toFixed(2)}
|
||||
cellLabelThreshold={1.7}
|
||||
tooltip={(r, c, v) => `${r.day} ${c.label}:00 — CTR ${v}%`}
|
||||
/>
|
||||
|
||||
// Cohort retention (semana 0-12)
|
||||
const cohortCols = Array.from({ length: 13 }, (_, i) => ({ key: 'w' + i, label: 'W' + i }))
|
||||
|
||||
<HeatmapGrid
|
||||
rows={cohorts}
|
||||
rowKey="cohort"
|
||||
columns={cohortCols}
|
||||
valueFormatter={(v) => v.toFixed(0) + '%'}
|
||||
intensityRange={[10, 90]}
|
||||
/>
|
||||
|
||||
// Matriz de correlación
|
||||
<HeatmapGrid
|
||||
rows={correlationMatrix}
|
||||
rowKey="variable"
|
||||
columns={variables.map(v => ({ key: v, label: v }))}
|
||||
valueFormatter={(v) => v.toFixed(2)}
|
||||
cellSize={40}
|
||||
baseColor="var(--mantine-color-cyan-6)"
|
||||
/>
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- El color de cada celda es `color-mix(in oklab, baseColor N%, transparent)` donde N interpola linealmente entre `intensityRange[0]` y `intensityRange[1]` según el valor normalizado en `[min, max]`.
|
||||
- El color del texto en la celda cambia automáticamente a blanco si `v > mid` (mid-point del rango) para contraste.
|
||||
- Para grids densos (ej. 24 horas × 7 días) usar `colLabelEvery={2}` o mayor para que los headers no se solapen.
|
||||
- Extraído de un export de Claude Design (Ads Analytics Dashboard). El `Heatmap` original hardcodeaba `h0..h23`; aquí se generaliza aceptando cualquier array de columnas.
|
||||
@@ -0,0 +1,153 @@
|
||||
import * as React from 'react'
|
||||
import { Box } from '@mantine/core'
|
||||
|
||||
interface HeatmapColumn {
|
||||
key: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
interface HeatmapGridProps {
|
||||
/** Row data. Each row is an object that contains values under the column keys and the row label under `rowKey`. */
|
||||
rows: Record<string, unknown>[]
|
||||
/** Key in each row that holds the row label (e.g. 'day', 'cohort'). */
|
||||
rowKey: string
|
||||
/** Columns to visualize, in order. Each entry has the value key and an optional display label. */
|
||||
columns: HeatmapColumn[]
|
||||
/** Formatter for numeric values inside cells and tooltip. */
|
||||
valueFormatter?: (v: number) => string
|
||||
/** Formatter for row labels (left column). Defaults to String(row[rowKey]). */
|
||||
rowLabelFormatter?: (row: Record<string, unknown>) => string
|
||||
/** Tooltip text generator (native HTML title). */
|
||||
tooltip?: (row: Record<string, unknown>, column: HeatmapColumn, value: number) => string
|
||||
/** Render the numeric label inside the cell only if |value| ≥ threshold. Default: always render. */
|
||||
cellLabelThreshold?: number
|
||||
/** Cell size in px. Default 22. */
|
||||
cellSize?: number
|
||||
/** Render a column header label every N columns (keeps the grid compact for hourly grids). Default 1. */
|
||||
colLabelEvery?: number
|
||||
/** Min/Max percent of primary color applied via color-mix. Default [6, 84]. */
|
||||
intensityRange?: [number, number]
|
||||
/** Override the base color. Default is `var(--mantine-primary-color-filled)`. */
|
||||
baseColor?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
function defaultValueFormatter(v: number): string {
|
||||
return Number.isInteger(v) ? String(v) : v.toFixed(2)
|
||||
}
|
||||
|
||||
function HeatmapGrid({
|
||||
rows,
|
||||
rowKey,
|
||||
columns,
|
||||
valueFormatter = defaultValueFormatter,
|
||||
rowLabelFormatter,
|
||||
tooltip,
|
||||
cellLabelThreshold,
|
||||
cellSize = 22,
|
||||
colLabelEvery = 1,
|
||||
intensityRange = [6, 84],
|
||||
baseColor = 'var(--mantine-primary-color-filled)',
|
||||
className,
|
||||
}: HeatmapGridProps) {
|
||||
const { min, max, mid } = React.useMemo(() => {
|
||||
const vals: number[] = []
|
||||
for (const row of rows) {
|
||||
for (const col of columns) {
|
||||
const n = Number(row[col.key])
|
||||
if (!isNaN(n)) vals.push(n)
|
||||
}
|
||||
}
|
||||
if (vals.length === 0) return { min: 0, max: 0, mid: 0 }
|
||||
const mn = Math.min(...vals)
|
||||
const mx = Math.max(...vals)
|
||||
return { min: mn, max: mx, mid: (mn + mx) / 2 }
|
||||
}, [rows, columns])
|
||||
|
||||
const [lo, hi] = intensityRange
|
||||
|
||||
function cellBg(v: number): string {
|
||||
if (max === min) return `color-mix(in oklab, ${baseColor} ${lo}%, transparent)`
|
||||
const t = (v - min) / (max - min)
|
||||
const pct = lo + t * (hi - lo)
|
||||
return `color-mix(in oklab, ${baseColor} ${pct.toFixed(0)}%, transparent)`
|
||||
}
|
||||
|
||||
return (
|
||||
<Box style={{ overflow: 'auto' }} className={className}>
|
||||
<table style={{ borderCollapse: 'separate', borderSpacing: 2, width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
{columns.map((col, i) => {
|
||||
if (colLabelEvery > 1 && i % colLabelEvery !== 0) return <th key={col.key} />
|
||||
const span = Math.min(colLabelEvery, columns.length - i)
|
||||
return (
|
||||
<th
|
||||
key={col.key}
|
||||
colSpan={span}
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
fontWeight: 500,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{col.label ?? col.key}
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, rowIdx) => {
|
||||
const label = rowLabelFormatter ? rowLabelFormatter(row) : String(row[rowKey] ?? '')
|
||||
return (
|
||||
<tr key={String(row[rowKey] ?? rowIdx)}>
|
||||
<td
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
paddingRight: 8,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</td>
|
||||
{columns.map(col => {
|
||||
const raw = Number(row[col.key])
|
||||
const v = isNaN(raw) ? 0 : raw
|
||||
const showLabel =
|
||||
cellLabelThreshold == null ? true : Math.abs(v) >= cellLabelThreshold
|
||||
const title = tooltip
|
||||
? tooltip(row, col, v)
|
||||
: `${label} · ${col.label ?? col.key} — ${valueFormatter(v)}`
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
title={title}
|
||||
style={{
|
||||
width: cellSize,
|
||||
height: cellSize,
|
||||
background: cellBg(v),
|
||||
borderRadius: 3,
|
||||
fontSize: 9,
|
||||
textAlign: 'center',
|
||||
color: v > mid ? 'white' : 'var(--mantine-color-dimmed)',
|
||||
}}
|
||||
>
|
||||
{showLabel ? valueFormatter(v) : ''}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export { HeatmapGrid }
|
||||
export type { HeatmapGridProps, HeatmapColumn }
|
||||
@@ -36,7 +36,11 @@ export type { Series } from './chart_container'
|
||||
|
||||
// Data
|
||||
export { DataTable } from './data_table'
|
||||
export type { DataTableProps, ColumnDef } from './data_table'
|
||||
export type { DataTableProps, ColumnDef, DataTableDensity } from './data_table'
|
||||
export { FunnelChart } from './funnel_chart'
|
||||
export type { FunnelChartProps, FunnelStage } from './funnel_chart'
|
||||
export { HeatmapGrid } from './heatmap_grid'
|
||||
export type { HeatmapGridProps, HeatmapColumn } from './heatmap_grid'
|
||||
|
||||
// Mantine Provider
|
||||
export { FnMantineProvider } from './mantine_provider'
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user