import React, { useState, useMemo } from 'react'; import { ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts'; import { BSRRecord, CombinedKPIs } from '../types'; interface Props { bsrData: BSRRecord[]; combinedSalesData: CombinedKPIs[]; } const MARKETS = ['Amazon DE', 'Amazon ES', 'Amazon FR', 'Amazon IT', 'Amazon UK'] as const; // BSR Excel stores market as short codes ('DE', 'IT', โ€ฆ); normalize to full names const BSR_MARKET_NORMALIZE: Record = { 'DE': 'Amazon DE', 'ES': 'Amazon ES', 'FR': 'Amazon FR', 'IT': 'Amazon IT', 'UK': 'Amazon UK', }; const normalizeBsrMarket = (m: string) => BSR_MARKET_NORMALIZE[m.toUpperCase()] ?? m; const MARKET_CONFIG: Record = { 'Amazon DE': { label: 'DE', color: '#f37526', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, 'Amazon ES': { label: 'ES', color: '#2acbd6', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, 'Amazon FR': { label: 'FR', color: '#7950f2', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, 'Amazon IT': { label: 'IT', color: '#10b981', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, 'Amazon UK': { label: 'UK', color: '#f59e0b', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, }; function pearsonR(x: number[], y: number[]): number | null { const n = x.length; if (n < 2 || n !== y.length) return null; const meanX = x.reduce((a, b) => a + b, 0) / n; const meanY = y.reduce((a, b) => a + b, 0) / n; const num = x.reduce((s, xi, i) => s + (xi - meanX) * (y[i] - meanY), 0); const den = Math.sqrt( x.reduce((s, xi) => s + (xi - meanX) ** 2, 0) * y.reduce((s, yi) => s + (yi - meanY) ** 2, 0) ); return den === 0 ? null : num / den; } const formatBSR = (v: number) => v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1).replace(/\.0$/, '')}M` : v >= 1_000 ? `${Math.round(v / 1_000)}K` : String(v); const CARD_BG = '#161b24'; const TOOLTIP_STYLE = { backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '10px', color: '#fff', fontSize: '12px', }; export const BSRUnitsCorrelationChart: React.FC = ({ bsrData, combinedSalesData }) => { const [activeMarket, setActiveMarket] = useState('Amazon DE'); const [bsrInverted, setBsrInverted] = useState(true); // Derive available markets from actual data const availableMarkets = useMemo(() => { const bsrMarkets = new Set(bsrData.map(r => normalizeBsrMarket(r.market))); const salesMarkets = new Set(combinedSalesData.map(r => r.customer)); return MARKETS.filter(m => bsrMarkets.has(m) || salesMarkets.has(m)); }, [bsrData, combinedSalesData]); // Ensure activeMarket is always valid when data changes const resolvedMarket = availableMarkets.includes(activeMarket as any) ? activeMarket : (availableMarkets[0] ?? 'Amazon DE'); // Current year from sales data โ€” use reduce to avoid call stack overflow with large arrays const currentYear = useMemo(() => { let max = 0; for (const r of combinedSalesData) { if (r.year && r.year > max) max = r.year; } return max > 0 ? max : new Date().getFullYear(); }, [combinedSalesData]); // Per-market chart data: BSR avg + units sum aligned by week const { chartData, pearsonCorrelation } = useMemo(() => { // BSR: average detailLevelBSR per week (falls back to topLevelBSR if detail is unavailable) // Coerce week to number to guard against string values at runtime const bsrByWeek = new Map(); const bsrRecordsForMarket = bsrData.filter(r => normalizeBsrMarket(r.market) === resolvedMarket); bsrRecordsForMarket .filter(r => r.detailLevelBSR != null || r.topLevelBSR != null) .forEach(r => { const bsr = r.detailLevelBSR ?? r.topLevelBSR!; const week = Number(r.week); const e = bsrByWeek.get(week) ?? { sum: 0, count: 0 }; bsrByWeek.set(week, { sum: e.sum + bsr, count: e.count + 1 }); }); // Units: sum unitsTotal per week for current year const unitsByWeek = new Map(); combinedSalesData .filter(r => r.customer === resolvedMarket && r.year === currentYear && r.week) .forEach(r => { const week = Number(r.week); unitsByWeek.set(week, (unitsByWeek.get(week) ?? 0) + r.unitsTotal); }); const allWeeks = Array.from( new Set([...bsrByWeek.keys(), ...unitsByWeek.keys()]) ).sort((a, b) => a - b); const chartData = allWeeks.map(week => { const bsrEntry = bsrByWeek.get(week); return { week: `W${String(week).padStart(2, '0')}`, weekNum: week, bsr: bsrEntry ? Math.round(bsrEntry.sum / bsrEntry.count) : null, units: unitsByWeek.get(week) ?? null, }; }); // Pearson only for weeks where both values exist const paired = chartData.filter(d => d.bsr !== null && d.units !== null); const r = pearsonR( paired.map(d => d.bsr as number), paired.map(d => d.units as number) ); return { chartData, pearsonCorrelation: r }; }, [resolvedMarket, bsrData, combinedSalesData, currentYear]); const config = MARKET_CONFIG[resolvedMarket] ?? { label: '?', color: '#94a3b8', flag: '๐ŸŒ' }; const pearsonInfo = useMemo(() => { if (pearsonCorrelation === null) return { text: 'โ€“', label: 'Insufficient data', color: '#475569' }; const abs = Math.abs(pearsonCorrelation); const sign = pearsonCorrelation < 0 ? 'negative' : 'positive'; const strength = abs >= 0.7 ? 'Strong' : abs >= 0.4 ? 'Moderate' : 'Weak'; // Negative r is GOOD: lower BSR (better rank) โ†’ more units const baseColor = pearsonCorrelation < 0 ? (abs >= 0.7 ? '#10b981' : abs >= 0.4 ? '#6ee7b7' : '#94a3b8') : (abs >= 0.7 ? '#ef4444' : abs >= 0.4 ? '#fca5a5' : '#94a3b8'); return { text: pearsonCorrelation.toFixed(2), label: `${strength} ${sign}`, color: baseColor, }; }, [pearsonCorrelation]); const tooltipFormatter = (value: unknown, name: string) => { if (name === 'BSR') return [typeof value === 'number' ? value.toLocaleString('de-DE') : 'โ€“', 'Detail BSR']; if (name === 'Units') return [typeof value === 'number' ? value.toLocaleString('de-DE') : 'โ€“', 'Units Sold']; return [value, name]; }; return (
{/* Header row */}

BSR vs Units Sold

Weekly correlation ยท {currentYear} ยท Detail category BSR

{/* Marketplace tabs */} {availableMarkets.length > 0 && (
{availableMarkets.map(m => { const conf = MARKET_CONFIG[m]; const isActive = m === resolvedMarket; return ( ); })}
)} {/* Chart */} {chartData.length === 0 ? (
No data available for {config.flag} {config.label}
) : (
{/* Left axis: BSR rank */} {/* Right axis: units sold */} v >= 1_000 ? `${Math.round(v / 1_000)}K` : String(v)} width={48} label={{ value: 'Units Sold', angle: 90, position: 'insideRight', offset: 8, style: { fill: '#475569', fontSize: 10, fontWeight: 600 }, }} /> {/* Units โ€” bars on right axis */} {/* BSR โ€” line on left axis */}
)} {/* Legend */}
Units Sold
BSR Rank
{/* Pearson correlation note */}
Pearson r {pearsonInfo.text} {pearsonInfo.label}
{pearsonCorrelation !== null && pearsonCorrelation < -0.3 ? 'โ†“ Rank improvement correlates with โ†‘ Units sold' : pearsonCorrelation !== null && pearsonCorrelation > 0.3 ? 'โ†‘ Rank number correlates with โ†‘ Units โ€” unusual' : pearsonCorrelation !== null ? 'No strong BSRโ€“Units relationship detected' : ''}
); };