diff --git a/app/src/components/KpiCard.tsx b/app/src/components/KpiCard.tsx index 766dc37..e5a9772 100644 --- a/app/src/components/KpiCard.tsx +++ b/app/src/components/KpiCard.tsx @@ -1,10 +1,9 @@ import { Link } from 'react-router'; import { intlLocale } from '@/i18n'; import { useEffect, useRef, useState } from 'react'; -import { animate, useReducedMotion } from 'framer-motion'; +import { animate, motion, useReducedMotion } from 'framer-motion'; import type { LucideIcon } from 'lucide-react'; import { ArrowDownRight, ArrowUpRight } from 'lucide-react'; -import { Area, AreaChart, ResponsiveContainer } from 'recharts'; import type { SemColor } from '@/data/types'; import { fmtNumber, fmtPct } from '@/lib/format'; import { cn } from '@/lib/utils'; @@ -21,6 +20,94 @@ const TINT: Record = { const EASE_OUT_QUART: [number, number, number, number] = [0.25, 1, 0.5, 1]; +const SPARK_W = 64; +const SPARK_H = 28; +const SPARK_PAD = 2; + +// Curva monotone cúbica (equivalente al type="monotone" de recharts) +function sparkLinePath(data: number[]): string { + const n = data.length; + const min = Math.min(...data); + const max = Math.max(...data); + const range = max - min || 1; + const pts = data.map((v, i) => { + const x = SPARK_PAD + (i * (SPARK_W - SPARK_PAD * 2)) / (n - 1); + const y = SPARK_H - SPARK_PAD - ((v - min) / range) * (SPARK_H - SPARK_PAD * 2); + return { x, y }; + }); + + const dx: number[] = []; + const slope: number[] = []; + for (let i = 0; i < n - 1; i++) { + dx[i] = pts[i + 1].x - pts[i].x || 1e-6; + slope[i] = (pts[i + 1].y - pts[i].y) / dx[i]; + } + const t: number[] = []; + t[0] = slope[0]; + t[n - 1] = slope[n - 2]; + for (let i = 1; i < n - 1; i++) { + t[i] = slope[i - 1] === 0 || slope[i] === 0 ? 0 : (slope[i - 1] + slope[i]) / 2; + } + for (let i = 0; i < n - 1; i++) { + const a = t[i] / slope[i]; + const b = t[i + 1] / slope[i]; + const s = a * a + b * b; + if (s > 9) { + const k = 3 / Math.sqrt(s); + t[i] = k * a * slope[i]; + t[i + 1] = k * b * slope[i]; + } + } + + let d = `M ${pts[0].x} ${pts[0].y}`; + for (let i = 0; i < n - 1; i++) { + const p0 = pts[i]; + const p1 = pts[i + 1]; + const c1x = p0.x + dx[i] / 3; + const c1y = p0.y + (t[i] * dx[i]) / 3; + const c2x = p1.x - dx[i] / 3; + const c2y = p1.y - (t[i + 1] * dx[i]) / 3; + d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p1.x} ${p1.y}`; + } + return d; +} + +function Sparkline({ data, color, reduce }: { data: number[]; color: string; reduce: boolean }) { + const line = sparkLinePath(data); + const area = `${line} L ${SPARK_W - SPARK_PAD} ${SPARK_H} L ${SPARK_PAD} ${SPARK_H} Z`; + const gid = `spark-${color.replace(/[^a-zA-Z0-9]/g, '')}`; + const spring = { delay: 0.3, duration: 0.8, ease: 'easeOut' as const }; + + return ( + + ); +} + interface KpiCardProps { icon: LucideIcon; tone: SemColor; @@ -98,22 +185,7 @@ export default function KpiCard({ {spark && spark.length > 1 && (
- - ({ i, v }))} margin={{ top: 2, bottom: 0, left: 0, right: 0 }}> - - - +
)} diff --git a/app/vite.config.ts b/app/vite.config.ts index 465e575..0006f95 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -23,13 +23,6 @@ export default defineConfig({ output: { manualChunks(id) { if (!id.includes('node_modules')) return; - if ( - id.includes('/recharts') || - id.includes('/d3-') || - id.includes('/recharts-') - ) { - return 'recharts'; - } if ( id.includes('/react/') || id.includes('/react-dom/') ||