feat(vendor): switch BSR charts to daily resolution for smoother trend lines

- Add date? field to BSRRecord (ISO string YYYY-MM-DD)
- processBSRExcel now extracts and stores the Date column as an ISO date
  (handles both Excel serial numbers and string dates)
- VendorDataView groups chart data by date (daily) when dates are available,
  falling back to weekly buckets — eliminates straight-line charts caused
  by only 2 weekly data points

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
christian.vidal
2026-03-02 18:53:45 +01:00
co-authored by Claude Sonnet 4.6
parent 702bb021f8
commit 7f5aa1b812
3 changed files with 62 additions and 29 deletions
+45 -24
View File
@@ -15,8 +15,9 @@ interface VendorDataViewProps {
bsrData: BSRRecord[]; bsrData: BSRRecord[];
} }
interface WeeklyChartPoint { interface ChartPoint {
weekLabel: string; label: string;
sortKey: string;
[key: string]: number | string | null; [key: string]: number | string | null;
} }
@@ -73,30 +74,48 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
return Array.from(m).sort(); return Array.from(m).sort();
}, [filteredData]); }, [filteredData]);
// Aggregate Data for Charts // Determine if daily resolution is available (>= half the records have a date)
const useDailyResolution = useMemo(() => {
const withDate = filteredData.filter(r => r.date).length;
return withDate > filteredData.length / 2;
}, [filteredData]);
// Aggregate Data for Charts — daily if dates available, else weekly
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => { const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
const byWeek = new Map<number, BSRRecord[]>(); // Group by date string (YYYY-MM-DD) or by week number
const byBucket = new Map<string, BSRRecord[]>();
filteredData.forEach(r => { filteredData.forEach(r => {
if (!byWeek.has(r.week)) byWeek.set(r.week, []); const key = useDailyResolution && r.date ? r.date : `W${String(r.week).padStart(2, '0')}`;
byWeek.get(r.week)!.push(r); if (!byBucket.has(key)) byBucket.set(key, []);
byBucket.get(key)!.push(r);
}); });
const sortedWeeks = Array.from(byWeek.keys()).sort((a, b) => a - b); const sortedKeys = Array.from(byBucket.keys()).sort();
const topBsrChartData: WeeklyChartPoint[] = []; const topBsrChartData: ChartPoint[] = [];
const detailBsrChartData: WeeklyChartPoint[] = []; const detailBsrChartData: ChartPoint[] = [];
const ratingChartData: WeeklyChartPoint[] = []; const ratingChartData: ChartPoint[] = [];
sortedWeeks.forEach(week => { sortedKeys.forEach(key => {
const row = byWeek.get(week)!; const rows = byBucket.get(key)!;
const weekLabel = `Week ${week}`;
const topPoint: WeeklyChartPoint = { weekLabel }; // Human-readable label
const detailPoint: WeeklyChartPoint = { weekLabel }; let label: string;
const ratingPoint: WeeklyChartPoint = { weekLabel }; if (useDailyResolution && key.includes('-')) {
// YYYY-MM-DD → "DD MMM"
const d = new Date(key + 'T12:00:00Z');
label = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
} else {
label = key; // e.g. "W08"
}
const topPoint: ChartPoint = { label, sortKey: key };
const detailPoint: ChartPoint = { label, sortKey: key };
const ratingPoint: ChartPoint = { label, sortKey: key };
activeMarkets.forEach(m => { activeMarkets.forEach(m => {
const marketRows = row.filter(r => r.market === m); const marketRows = rows.filter(r => r.market === m);
// Top BSR // Top BSR
const topBsrRows = marketRows.filter(r => r.topLevelBSR != null); const topBsrRows = marketRows.filter(r => r.topLevelBSR != null);
@@ -123,7 +142,7 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
}); });
return { topBsrChartData, detailBsrChartData, ratingChartData }; return { topBsrChartData, detailBsrChartData, ratingChartData };
}, [filteredData, activeMarkets]); }, [filteredData, activeMarkets, useDailyResolution]);
if (bsrData.length === 0) { if (bsrData.length === 0) {
return ( return (
@@ -141,6 +160,8 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
); );
} }
const resolutionLabel = useDailyResolution ? 'Daily Avg' : 'Weekly Avg';
return ( return (
<div className="space-y-6 p-4 pb-24 md:pb-4"> <div className="space-y-6 p-4 pb-24 md:pb-4">
{/* Filters */} {/* Filters */}
@@ -188,12 +209,12 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
{/* Top Level BSR Trend Chart */} {/* Top Level BSR Trend Chart */}
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4"> <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
<h3 className="text-lg font-bold text-slate-200 mb-4">Top Level BSR Trend (Weekly Avg)</h3> <h3 className="text-lg font-bold text-slate-200 mb-4">Top Level BSR Trend ({resolutionLabel})</h3>
<p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p> <p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p>
<ResponsiveContainer width="100%" height={350}> <ResponsiveContainer width="100%" height={350}>
<LineChart data={topBsrChartData}> <LineChart data={topBsrChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" /> <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="weekLabel" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" /> <XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
<YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} /> <YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
@@ -218,12 +239,12 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
{/* Detail Level BSR Trend Chart */} {/* Detail Level BSR Trend Chart */}
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4"> <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
<h3 className="text-lg font-bold text-slate-200 mb-4">Detail Level BSR Trend (Weekly Avg)</h3> <h3 className="text-lg font-bold text-slate-200 mb-4">Detail Level BSR Trend ({resolutionLabel})</h3>
<p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p> <p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p>
<ResponsiveContainer width="100%" height={350}> <ResponsiveContainer width="100%" height={350}>
<LineChart data={detailBsrChartData}> <LineChart data={detailBsrChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" /> <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="weekLabel" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" /> <XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
<YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} /> <YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
@@ -248,12 +269,12 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
{/* Average Rating Chart */} {/* Average Rating Chart */}
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4"> <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
<h3 className="text-lg font-bold text-slate-200 mb-4">Average Rating (Weekly Avg)</h3> <h3 className="text-lg font-bold text-slate-200 mb-4">Average Rating ({resolutionLabel})</h3>
<p className="text-xs text-slate-500 mb-3">Averaged across filtered ASINs per market.</p> <p className="text-xs text-slate-500 mb-3">Averaged across filtered ASINs per market.</p>
<ResponsiveContainer width="100%" height={350}> <ResponsiveContainer width="100%" height={350}>
<LineChart data={ratingChartData}> <LineChart data={ratingChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" /> <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="weekLabel" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" /> <XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
<YAxis domain={[0, 5]} tick={{ fill: '#94a3b8', fontSize: 11 }} /> <YAxis domain={[0, 5]} tick={{ fill: '#94a3b8', fontSize: 11 }} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
+13 -2
View File
@@ -682,15 +682,25 @@ export const processBSRExcel = async (fileOrBuffer: File | ArrayBuffer): Promise
// Week: try direct week column first, else derive from Date column // Week: try direct week column first, else derive from Date column
let week = 0; let week = 0;
let isoDate: string | undefined;
const weekRaw = getColumnValue(row, ['week', 'woche', 'semana', 'week number', 'weeknumber', 'Week', 'Week Number']); const weekRaw = getColumnValue(row, ['week', 'woche', 'semana', 'week number', 'weeknumber', 'Week', 'Week Number']);
if (weekRaw) { if (weekRaw) {
week = parseInt(String(weekRaw).match(/\d+/)?.[0] || '0', 10); week = parseInt(String(weekRaw).match(/\d+/)?.[0] || '0', 10);
} }
if (!week) {
const dateRaw = getColumnValue(row, ['date', 'fecha', 'datum', 'Date']); const dateRaw = getColumnValue(row, ['date', 'fecha', 'datum', 'Date']);
if (dateRaw) { if (dateRaw) {
const d = new Date(dateRaw); // Handle Excel serial date numbers
let d: Date;
const dateNum = Number(dateRaw);
if (!isNaN(dateNum) && dateNum > 1000) {
// Excel serial date: days since 1899-12-30
d = new Date((dateNum - 25569) * 86400 * 1000);
} else {
d = new Date(dateRaw);
}
if (!isNaN(d.getTime())) { if (!isNaN(d.getTime())) {
isoDate = d.toISOString().slice(0, 10); // "YYYY-MM-DD"
if (!week) {
// ISO week number // ISO week number
const tmp = new Date(d); const tmp = new Date(d);
tmp.setHours(0, 0, 0, 0); tmp.setHours(0, 0, 0, 0);
@@ -704,6 +714,7 @@ export const processBSRExcel = async (fileOrBuffer: File | ArrayBuffer): Promise
allData.push({ allData.push({
week, week,
date: isoDate,
market: String(marketRaw).trim(), market: String(marketRaw).trim(),
asin: String(asinRaw).trim(), asin: String(asinRaw).trim(),
topLevelBSR: parseIntSafe(getColumnValue(row, [ topLevelBSR: parseIntSafe(getColumnValue(row, [
+1
View File
@@ -282,6 +282,7 @@ export interface VendorCSVRow {
export interface BSRRecord { export interface BSRRecord {
week: number; week: number;
date?: string; // ISO date string (YYYY-MM-DD) when available — enables daily resolution charts
market: string; market: string;
asin: string; asin: string;
topLevelBSR: number | null; topLevelBSR: number | null;