Files
CrazeAnalytix/components/BSRUnitsCorrelationChart.tsx
T
Christian Vidal WolfandClaude Sonnet 4.6 f1e11ca79a fix: replace Math.max(...spread) with loop to prevent call stack overflow
Math.max(...largeArray) crashes when combinedAdsData has thousands of
records. Replaced with a simple for-loop to find the max year safely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 11:03:56 +01:00

329 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
const MARKET_CONFIG: Record<string, { label: string; color: string; flag: string }> = {
'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<Props> = ({ bsrData, combinedSalesData }) => {
const [activeMarket, setActiveMarket] = useState<string>('Amazon DE');
const [bsrInverted, setBsrInverted] = useState(true);
// Derive available markets from actual data
const availableMarkets = useMemo(() => {
const bsrMarkets = new Set(bsrData.map(r => 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 topLevelBSR per week
const bsrByWeek = new Map<number, { sum: number; count: number }>();
bsrData
.filter(r => r.market === resolvedMarket && r.topLevelBSR != null)
.forEach(r => {
const e = bsrByWeek.get(r.week) ?? { sum: 0, count: 0 };
bsrByWeek.set(r.week, { sum: e.sum + r.topLevelBSR!, count: e.count + 1 });
});
// Units: sum unitsTotal per week for current year
const unitsByWeek = new Map<number, number>();
combinedSalesData
.filter(r => r.customer === resolvedMarket && r.year === currentYear && r.week)
.forEach(r => {
unitsByWeek.set(r.week, (unitsByWeek.get(r.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') : '', 'BSR Rank'];
if (name === 'Units') return [typeof value === 'number' ? value.toLocaleString('de-DE') : '', 'Units Sold'];
return [value, name];
};
return (
<div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col gap-5">
{/* Header row */}
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-[17px] font-semibold text-white tracking-wide">BSR vs Units Sold</h3>
<p className="text-[13px] text-[#64748b] mt-0.5">
Weekly correlation · {currentYear} · Top-level category BSR
</p>
</div>
<button
onClick={() => setBsrInverted(v => !v)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[#2a3040] bg-[#1f2937] text-[11px] font-semibold text-slate-300 hover:border-slate-600 hover:text-white transition-all"
>
<span className="text-base leading-none">{bsrInverted ? '↑' : '↓'}</span>
<span>BSR: {bsrInverted ? 'Lower = Top' : 'Lower = Bottom'}</span>
</button>
</div>
{/* Marketplace tabs */}
{availableMarkets.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap">
{availableMarkets.map(m => {
const conf = MARKET_CONFIG[m];
const isActive = m === resolvedMarket;
return (
<button
key={m}
onClick={() => setActiveMarket(m)}
className="px-3.5 py-1.5 rounded-lg text-[12px] font-bold uppercase tracking-wider transition-all border"
style={
isActive
? { color: conf.color, borderColor: conf.color, backgroundColor: `${conf.color}1a` }
: { color: '#64748b', borderColor: 'transparent' }
}
onMouseEnter={e => {
if (!isActive) (e.currentTarget as HTMLButtonElement).style.color = '#cbd5e1';
}}
onMouseLeave={e => {
if (!isActive) (e.currentTarget as HTMLButtonElement).style.color = '#64748b';
}}
>
{conf.flag} {conf.label}
</button>
);
})}
</div>
)}
{/* Chart */}
{chartData.length === 0 ? (
<div className="h-[260px] flex items-center justify-center text-[#475569] text-sm italic">
No data available for {config.flag} {config.label}
</div>
) : (
<div className="h-[260px] w-full">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 16, right: 54, left: 8, bottom: 4 }}>
<CartesianGrid
strokeDasharray="0"
stroke="#ffffff"
strokeOpacity={0.03}
vertical={false}
/>
<XAxis
dataKey="week"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
dy={8}
/>
{/* Left axis: BSR rank */}
<YAxis
yAxisId="bsr"
orientation="left"
reversed={bsrInverted}
domain={['auto', 'auto']}
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 10 }}
tickFormatter={formatBSR}
width={48}
label={{
value: 'BSR Rank',
angle: -90,
position: 'insideLeft',
offset: 6,
style: { fill: '#475569', fontSize: 10, fontWeight: 600 },
}}
/>
{/* Right axis: units sold */}
<YAxis
yAxisId="units"
orientation="right"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 10 }}
tickFormatter={(v: number) => 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 },
}}
/>
<Tooltip
contentStyle={TOOLTIP_STYLE}
itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#94a3b8', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#334155', strokeWidth: 1, strokeDasharray: '3 3' }}
formatter={tooltipFormatter}
/>
{/* Units — bars on right axis */}
<Bar
yAxisId="units"
dataKey="units"
name="Units"
fill={config.color}
fillOpacity={0.45}
radius={[3, 3, 0, 0]}
maxBarSize={36}
/>
{/* BSR — line on left axis */}
<Line
yAxisId="bsr"
type="monotone"
dataKey="bsr"
name="BSR"
stroke={config.color}
strokeWidth={2.5}
dot={{ r: 4, fill: CARD_BG, stroke: config.color, strokeWidth: 2 }}
activeDot={{ r: 6, strokeWidth: 0, fill: config.color }}
connectNulls
/>
</ComposedChart>
</ResponsiveContainer>
</div>
)}
{/* Legend */}
<div className="flex items-center gap-5 justify-center text-[11px] text-[#64748b]">
<div className="flex items-center gap-1.5">
<div className="w-8 h-2.5 rounded-sm" style={{ backgroundColor: config.color, opacity: 0.55 }} />
<span>Units Sold</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-6 h-0.5 rounded" style={{ backgroundColor: config.color }} />
<div className="w-2 h-2 rounded-full border-2" style={{ borderColor: config.color, backgroundColor: CARD_BG }} />
<span>BSR Rank</span>
</div>
</div>
{/* Pearson correlation note */}
<div className="flex flex-wrap items-center gap-4 px-4 py-3 rounded-xl bg-[#111827] border border-[#1e293b]">
<div className="flex items-center gap-2.5">
<span className="text-[10px] font-black uppercase tracking-widest text-[#334155]">Pearson r</span>
<span className="text-[22px] font-black tabular-nums" style={{ color: pearsonInfo.color }}>
{pearsonInfo.text}
</span>
<span className="text-[11px] font-semibold" style={{ color: pearsonInfo.color }}>
{pearsonInfo.label}
</span>
</div>
<span className="text-[10px] text-[#334155] ml-auto">
{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 BSRUnits relationship detected'
: ''}
</span>
</div>
</div>
);
};