# ET-PFM-009: Custom Charts (SVG + Canvas)

**Versión:** 1.0.0
**Fecha:** 2026-01-25
**Epic:** OQI-008 - Portfolio Manager
**Componente:** Frontend Components (SVG + Canvas)
**Estado:** ✅ Implementado (documentación retroactiva)
**Prioridad:** P3

---

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | ET-PFM-009 |
| **Tipo** | Especificación Técnica |
| **Epic** | OQI-008 |
| **US Relacionada** | US-PFM-009 (Ver Charts de Portafolio Personalizados) |
| **Componentes** | `AllocationChart.tsx` (138 líneas), `PerformanceChart.tsx` (312 líneas) |
| **Tecnologías** | SVG (geometría), Canvas API (rendering) |
| **Complejidad** | Media (matemáticas de geometría + Canvas API) |

---

## 1. Descripción General

Este módulo implementa **2 custom charts** para visualización de portafolio sin dependencias externas de librerías de gráficos:

### 1.1 AllocationChart (SVG Donut Chart)
- **Tecnología:** SVG (path elements)
- **Tipo:** Donut chart
- **Propósito:** Visualizar distribución de activos en portafolio
- **Features:** Hover tooltips, colores por asset, leyenda

### 1.2 PerformanceChart (Canvas Line Chart)
- **Tecnología:** Canvas 2D API
- **Tipo:** Line chart con área rellena
- **Propósito:** Mostrar evolución del valor del portafolio en el tiempo
- **Features:** Hover crosshair, multi-period, grid lines, gradient fill

---

## 2. AllocationChart (SVG Donut Chart)

### 2.1 Arquitectura SVG

```
┌────────────────────────────────────────────────────────┐
│         AllocationChart Architecture (SVG)             │
├────────────────────────────────────────────────────────┤
│                                                         │
│  Allocations Data:                                      │
│  [                                                      │
│    { asset: 'BTC', value: 5000, currentPercent: 50 },  │
│    { asset: 'ETH', value: 3000, currentPercent: 30 },  │
│    { asset: 'SOL', value: 2000, currentPercent: 20 }   │
│  ]                                                      │
│         │                                               │
│         v                                               │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Step 1: Calculate Angles                       │  │
│  │  BTC: 0° to 180° (50% of 360°)                  │  │
│  │  ETH: 180° to 288° (30% of 360°)                │  │
│  │  SOL: 288° to 360° (20% of 360°)                │  │
│  └──────────────┬───────────────────────────────────┘  │
│                 │                                       │
│                 v                                       │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Step 2: Convert to Radians                     │  │
│  │  angleRad = (angleDeg × π) / 180                │  │
│  └──────────────┬───────────────────────────────────┘  │
│                 │                                       │
│                 v                                       │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Step 3: Calculate Arc Points                   │  │
│  │  x = centerX + radius × cos(angle)              │  │
│  │  y = centerY + radius × sin(angle)              │  │
│  │                                                  │  │
│  │  Outer arc: radius = 100px                      │  │
│  │  Inner arc: radius = 60px (donut hole)          │  │
│  └──────────────┬───────────────────────────────────┘  │
│                 │                                       │
│                 v                                       │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Step 4: Generate SVG Path Data                 │  │
│  │  M x1Inner y1Inner   (Move to inner start)      │  │
│  │  L x1 y1             (Line to outer start)      │  │
│  │  A r r 0 largeArc 1 x2 y2  (Outer arc)          │  │
│  │  L x2Inner y2Inner   (Line to inner end)        │  │
│  │  A rInner rInner 0 largeArc 0 x1Inner y1Inner   │  │
│  │  Z                   (Close path)               │  │
│  └──────────────┬───────────────────────────────────┘  │
│                 │                                       │
│                 v                                       │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Step 5: Render SVG <path> Elements             │  │
│  │  <path d="M 60 100 L ..." fill="#F7931A" />     │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
└────────────────────────────────────────────────────────┘
```

### 2.2 Código Matemático

```typescript
// SVG donut chart geometry
const radius = size / 2;           // 100px (outer radius)
const innerRadius = radius * 0.6;  // 60px (inner radius for donut hole)
const centerX = radius;
const centerY = radius;

let currentAngle = -90; // Start from top (12 o'clock)

const segments = allocations.map((alloc) => {
  // Calculate angles
  const angle = (alloc.currentPercent / 100) * 360;
  const startAngle = currentAngle;
  const endAngle = currentAngle + angle;
  currentAngle = endAngle;

  // Convert to radians
  const startRad = (startAngle * Math.PI) / 180;
  const endRad = (endAngle * Math.PI) / 180;

  // Calculate arc points (polar to cartesian)
  // Outer arc
  const x1 = centerX + radius * Math.cos(startRad);
  const y1 = centerY + radius * Math.sin(startRad);
  const x2 = centerX + radius * Math.cos(endRad);
  const y2 = centerY + radius * Math.sin(endRad);

  // Inner arc (donut hole)
  const x1Inner = centerX + innerRadius * Math.cos(startRad);
  const y1Inner = centerY + innerRadius * Math.sin(startRad);
  const x2Inner = centerX + innerRadius * Math.cos(endRad);
  const y2Inner = centerY + innerRadius * Math.sin(endRad);

  // Large arc flag (>180°)
  const largeArc = angle > 180 ? 1 : 0;

  // SVG Path data
  const pathData = [
    `M ${x1Inner} ${y1Inner}`,  // Move to inner start
    `L ${x1} ${y1}`,            // Line to outer start
    `A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2}`,  // Outer arc (clockwise)
    `L ${x2Inner} ${y2Inner}`,  // Line to inner end
    `A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x1Inner} ${y1Inner}`,  // Inner arc (counter-clockwise)
    'Z',  // Close path
  ].join(' ');

  return { ...alloc, pathData, color: getAssetColor(alloc.asset) };
});
```

**Polar to Cartesian Conversion:**
```
x = centerX + radius × cos(θ)
y = centerY + radius × sin(θ)
```

### 2.3 Props Interface

```typescript
interface AllocationChartProps {
  allocations: PortfolioAllocation[];
  size?: number;  // Default: 200px
}

interface PortfolioAllocation {
  asset: string;           // 'BTC', 'ETH', 'SOL'
  value: number;           // Dollar value
  currentPercent: number;  // Percentage of portfolio (0-100)
}
```

### 2.4 Asset Color Palette

```typescript
const ASSET_COLORS: Record<string, string> = {
  BTC: '#F7931A',   // Bitcoin orange
  ETH: '#627EEA',   // Ethereum blue
  USDT: '#26A17B',  // Tether green
  SOL: '#9945FF',   // Solana purple
  LINK: '#2A5ADA',  // Chainlink blue
  AVAX: '#E84142',  // Avalanche red
  ADA: '#0033AD',   // Cardano blue
  DOT: '#E6007A',   // Polkadot pink
  MATIC: '#8247E5', // Polygon purple
  default: '#6B7280', // Gray fallback
};
```

### 2.5 SVG Rendering

```tsx
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
  {segments.map((segment, index) => (
    <path
      key={index}
      d={segment.pathData}
      fill={segment.color}
      className="transition-opacity hover:opacity-80 cursor-pointer"
    >
      <title>
        {segment.asset}: {segment.currentPercent.toFixed(1)}% ($
        {segment.value.toLocaleString()})
      </title>
    </path>
  ))}

  {/* Center text */}
  <text x={centerX} y={centerY - 10} textAnchor="middle" className="text-xs text-gray-500">
    Valor Total
  </text>
  <text x={centerX} y={centerY + 15} textAnchor="middle" className="text-lg font-bold">
    ${totalValue.toLocaleString()}
  </text>
</svg>
```

### 2.6 Legend

```tsx
<div className="flex flex-wrap justify-center gap-3 mt-4">
  {allocations.map((alloc) => (
    <div key={alloc.asset} className="flex items-center gap-2">
      <div
        className="w-3 h-3 rounded-full"
        style={{ backgroundColor: getAssetColor(alloc.asset) }}
      />
      <span className="text-sm text-gray-600">
        {alloc.asset} ({alloc.currentPercent.toFixed(1)}%)
      </span>
    </div>
  ))}
</div>
```

---

## 3. PerformanceChart (Canvas Line Chart)

### 3.1 Arquitectura Canvas

```
┌──────────────────────────────────────────────────────────┐
│      PerformanceChart Architecture (Canvas API)          │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  Performance Data:                                        │
│  [                                                        │
│    { date: '2026-01-01', value: 10000, pnl: 0 },         │
│    { date: '2026-01-02', value: 10150, pnl: 150 },       │
│    { date: '2026-01-03', value: 10200, pnl: 200 },       │
│    ...                                                    │
│  ]                                                        │
│         │                                                 │
│         v                                                 │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 1: Setup Canvas (High DPI)                  │  │
│  │  dpr = window.devicePixelRatio (2x on Retina)     │  │
│  │  canvas.width = rect.width × dpr                  │  │
│  │  ctx.scale(dpr, dpr)                               │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 v                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 2: Calculate Min/Max & Scaling              │  │
│  │  minValue = min(values) × 0.995                   │  │
│  │  maxValue = max(values) × 1.005                   │  │
│  │  valueRange = maxValue - minValue                 │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 v                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 3: Map Data to Canvas Coordinates           │  │
│  │  x = padding.left + (i / (len - 1)) × chartWidth  │  │
│  │  y = padding.top + innerHeight -                  │  │
│  │      ((value - min) / range) × innerHeight        │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 v                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 4: Draw Grid Lines (horizontal)             │  │
│  │  for i in 0..5:                                   │  │
│  │    y = padding.top + (i / 5) × innerHeight        │  │
│  │    ctx.moveTo(padding.left, y)                    │  │
│  │    ctx.lineTo(width - padding.right, y)           │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 v                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 5: Draw Gradient Fill (area under line)     │  │
│  │  ctx.beginPath()                                  │  │
│  │  ctx.moveTo(points[0].x, chartHeight - padding)   │  │
│  │  for point in points: ctx.lineTo(point.x, point.y)│  │
│  │  ctx.lineTo(lastX, chartHeight - padding)         │  │
│  │  ctx.closePath()                                  │  │
│  │  ctx.fill() with rgba color                       │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 v                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 6: Draw Line Path                           │  │
│  │  ctx.moveTo(points[0].x, points[0].y)             │  │
│  │  for point in points: ctx.lineTo(point.x, point.y)│  │
│  │  ctx.stroke() with green/red color                │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 v                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Step 7: Draw X-axis Labels (dates)               │  │
│  │  Step 8: Draw Hover Point + Crosshair             │  │
│  └────────────────────────────────────────────────────┘  │
│                                                           │
└──────────────────────────────────────────────────────────┘
```

### 3.2 Canvas Setup (High DPI)

```typescript
const drawChart = () => {
  const canvas = canvasRef.current;
  const ctx = canvas.getContext('2d');

  // High DPI support (Retina displays)
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  ctx.scale(dpr, dpr);

  const width = rect.width;
  const height = rect.height;
  const padding = { top: 20, right: 20, bottom: 30, left: 60 };

  // Clear canvas
  ctx.clearRect(0, 0, width, height);
};
```

**Why DPR scaling?**
- Retina displays have devicePixelRatio = 2 (or higher)
- Without scaling, canvas looks blurry on high-DPI screens
- Scale canvas by DPR, then scale context back for crisp rendering

### 3.3 Coordinate Mapping

```typescript
// Get data range
const values = data.map((d) => d.value);
const minValue = Math.min(...values) * 0.995;  // Add 0.5% padding
const maxValue = Math.max(...values) * 1.005;
const valueRange = maxValue - minValue;

// Map data points to canvas coordinates
const points = data.map((d, i) => ({
  x: padding.left + (i / (data.length - 1)) * chartWidth,
  y: padding.top + innerHeight - ((d.value - minValue) / valueRange) * innerHeight,
  data: d,
}));
```

**Coordinate System:**
- Canvas Y-axis: 0 at top, increases downward
- Chart Y-axis: 0 at bottom, increases upward
- Formula inverts Y: `innerHeight - ((value - min) / range) × height`

### 3.4 Drawing Grid Lines

```typescript
ctx.strokeStyle = 'rgba(156, 163, 175, 0.2)';
ctx.lineWidth = 1;

const gridLines = 5;
for (let i = 0; i <= gridLines; i++) {
  const y = padding.top + (i / gridLines) * innerHeight;

  // Horizontal grid line
  ctx.beginPath();
  ctx.moveTo(padding.left, y);
  ctx.lineTo(width - padding.right, y);
  ctx.stroke();

  // Y-axis label
  const value = maxValue - (i / gridLines) * valueRange;
  ctx.fillStyle = '#9CA3AF';
  ctx.font = '11px system-ui';
  ctx.textAlign = 'right';
  ctx.fillText(
    `$${value.toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
    padding.left - 8,
    y + 4
  );
}
```

### 3.5 Drawing Gradient Fill

```typescript
const isPositive = data[data.length - 1].value >= data[0].value;
const fillColor = isPositive ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)';

// Draw area fill
ctx.beginPath();
ctx.moveTo(points[0].x, chartHeight - padding.bottom);  // Start at bottom-left
points.forEach((p) => ctx.lineTo(p.x, p.y));  // Follow line
ctx.lineTo(points[points.length - 1].x, chartHeight - padding.bottom);  // Back to bottom
ctx.closePath();
ctx.fillStyle = fillColor;
ctx.fill();
```

### 3.6 Drawing Line

```typescript
const lineColor = isPositive ? '#10B981' : '#EF4444';

ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
points.forEach((p) => ctx.lineTo(p.x, p.y));
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.stroke();
```

### 3.7 Hover Interactions

```typescript
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
  const canvas = canvasRef.current;
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;

  // Map mouse X to data index
  const padding = { left: 60, right: 20 };
  const chartWidth = rect.width - padding.left - padding.right;
  const relativeX = (x - padding.left) / chartWidth;
  const idx = Math.round(relativeX * (data.length - 1));

  // Clamp to valid range
  const point = data[Math.max(0, Math.min(idx, data.length - 1))];
  setHoveredPoint(point);
};
```

### 3.8 Drawing Hover Crosshair

```typescript
if (hoveredPoint) {
  const idx = data.findIndex((d) => d.date === hoveredPoint.date);
  const point = points[idx];

  // Draw circle at point
  ctx.beginPath();
  ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
  ctx.fillStyle = lineColor;
  ctx.fill();
  ctx.strokeStyle = '#fff';
  ctx.lineWidth = 2;
  ctx.stroke();

  // Draw vertical dashed line
  ctx.beginPath();
  ctx.moveTo(point.x, padding.top);
  ctx.lineTo(point.x, chartHeight - padding.bottom);
  ctx.strokeStyle = 'rgba(156, 163, 175, 0.5)';
  ctx.lineWidth = 1;
  ctx.setLineDash([4, 4]);
  ctx.stroke();
  ctx.setLineDash([]);
}
```

### 3.9 Props Interface

```typescript
interface PerformanceChartProps {
  portfolioId: string;
  height?: number;  // Default: 300px
}

interface PerformanceDataPoint {
  date: string;         // '2026-01-25'
  value: number;        // Portfolio value
  pnl: number;          // Profit/Loss
  pnlPercent: number;   // P&L percentage
  change: number;       // Daily change
  changePercent: number; // Daily change %
}

type PerformancePeriod = 'week' | 'month' | '3months' | 'year' | 'all';
```

### 3.10 Period Selector

```tsx
const PERIOD_OPTIONS = [
  { value: 'week', label: '7D' },
  { value: 'month', label: '1M' },
  { value: '3months', label: '3M' },
  { value: 'year', label: '1A' },
  { value: 'all', label: 'Todo' },
];

<div className="flex gap-1">
  {PERIOD_OPTIONS.map((option) => (
    <button
      onClick={() => setPeriod(option.value)}
      className={period === option.value ? 'bg-blue-600' : 'bg-gray-100'}
    >
      {option.label}
    </button>
  ))}
</div>
```

---

## 4. Comparación: SVG vs Canvas

| Feature | SVG (AllocationChart) | Canvas (PerformanceChart) |
|---------|----------------------|---------------------------|
| **Rendering** | DOM elements (declarative) | Pixel-based (imperative) |
| **Performance** | Good for < 100 elements | Excellent for thousands of points |
| **Interactivity** | Native (hover, click) | Manual (mouse coords → data) |
| **Scaling** | Vector (infinite zoom) | Raster (needs redraw on resize) |
| **Animations** | CSS transitions | Manual redraw |
| **Accessibility** | Better (DOM structure) | Requires ARIA labels |
| **Use Case** | Static/semi-static shapes | Dynamic/real-time data |

**Why SVG for AllocationChart?**
- Small number of elements (5-10 segments)
- Native hover tooltips (`<title>`)
- Crisp vector rendering at any size

**Why Canvas for PerformanceChart?**
- Many data points (30-365)
- Better performance for redraws on hover
- Pixel-perfect control over rendering

---

## 5. Uso e Integración

### 5.1 AllocationChart

```tsx
import AllocationChart from '@/modules/portfolio/components/AllocationChart';

const allocations = [
  { asset: 'BTC', value: 5000, currentPercent: 50 },
  { asset: 'ETH', value: 3000, currentPercent: 30 },
  { asset: 'SOL', value: 2000, currentPercent: 20 },
];

<AllocationChart allocations={allocations} size={250} />
```

### 5.2 PerformanceChart

```tsx
import PerformanceChart from '@/modules/portfolio/components/PerformanceChart';

<PerformanceChart portfolioId="portfolio-123" height={350} />
```

### 5.3 Combined Dashboard

```tsx
function PortfolioDashboard({ portfolioId }: { portfolioId: string }) {
  const { data: portfolio } = usePortfolio(portfolioId);

  return (
    <div className="grid grid-cols-2 gap-6">
      {/* Allocation Donut */}
      <div>
        <h3>Distribución de Activos</h3>
        <AllocationChart
          allocations={portfolio.allocations}
          size={300}
        />
      </div>

      {/* Performance Line Chart */}
      <div>
        <PerformanceChart
          portfolioId={portfolioId}
          height={300}
        />
      </div>
    </div>
  );
}
```

---

## 6. Mejoras Futuras

### 6.1 AllocationChart Enhancements
- **Interactive selection:** Click segment → drill down
- **Animated transitions:** Smooth arc transitions on data change
- **Multiple rings:** Nested donuts for sub-categories
- **Exploded segments:** Separate selected segment

### 6.2 PerformanceChart Enhancements
- **Zoom/Pan:** Pinch to zoom, drag to pan
- **Multiple series:** Compare multiple portfolios
- **Annotations:** Mark important events (deposits, withdrawals)
- **Export:** Download chart as PNG/SVG
- **Real-time updates:** WebSocket integration for live data

### 6.3 Additional Chart Types
- **Bar Chart (Canvas):** Monthly returns comparison
- **Heatmap (SVG):** Asset correlation matrix
- **Scatter Plot (Canvas):** Risk vs Return
- **Candlestick (Canvas):** Historical trades

---

## 7. Performance Optimization

### 7.1 Canvas Optimizations

```typescript
// 1. Debounce mouse move events
const debouncedMouseMove = useMemo(
  () => debounce(handleMouseMove, 16),  // ~60fps
  []
);

// 2. Only redraw on state change
useEffect(() => {
  if (data.length > 0) {
    drawChart();
  }
}, [data, hoveredPoint]);

// 3. Use requestAnimationFrame for smooth animations
const animateChart = () => {
  drawChart();
  requestAnimationFrame(animateChart);
};
```

### 7.2 SVG Optimizations

```typescript
// 1. useMemo for expensive calculations
const segments = useMemo(() => {
  return calculateSegments(allocations);
}, [allocations]);

// 2. CSS instead of re-rendering
<path className="transition-opacity hover:opacity-80" />
```

---

## 8. Testing

### Manual Test Cases

1. **AllocationChart:**
   - Single asset (100%) → Full circle
   - Two equal assets (50% each) → Two semicircles
   - Many small assets → Visible slices
   - Hover → Tooltip shows correct data
   - Resize → SVG scales properly

2. **PerformanceChart:**
   - Positive performance → Green line/fill
   - Negative performance → Red line/fill
   - Hover → Crosshair + tooltip
   - Period change → Data updates
   - Resize → Canvas redraws correctly
   - High DPI → No blur on Retina

---

## 9. Referencias

- **AllocationChart:** `apps/frontend/src/modules/portfolio/components/AllocationChart.tsx` (138 líneas)
- **PerformanceChart:** `apps/frontend/src/modules/portfolio/components/PerformanceChart.tsx` (312 líneas)
- **Service:** `apps/frontend/src/services/portfolio.service.ts`
- **US:** `US-PFM-009-custom-charts.md`
- **MDN Canvas API:** https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
- **MDN SVG Paths:** https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths

---

**Última actualización:** 2026-01-25
**Responsable:** Frontend Lead

