← back to Watches
src/components/OmegaPriceHistoryChart.jsx
313 lines
import React, { useState, useEffect, useRef } from 'react';
import { Chart, registerables } from 'chart.js';
import omegaWatchesData from '../../data/omega-watches-expanded-v3.json';
Chart.register(...registerables);
function OmegaPriceHistoryChart() {
const [selectedModel, setSelectedModel] = useState('');
const [selectedYear, setSelectedYear] = useState('');
const [watches, setWatches] = useState([]);
const [availableYears, setAvailableYears] = useState([]);
const chartRef = useRef(null);
const chartInstance = useRef(null);
// Load watches data
useEffect(() => {
if (omegaWatchesData && omegaWatchesData.watches) {
const watchesList = Object.entries(omegaWatchesData.watches).map(([key, value]) => ({
id: key,
...value
}));
setWatches(watchesList);
if (watchesList.length > 0) {
setSelectedModel(watchesList[0].id);
}
}
}, []);
// Update available years when model changes
useEffect(() => {
if (selectedModel) {
const watch = watches.find(w => w.id === selectedModel);
if (watch && watch.priceHistory) {
const years = [...new Set(watch.priceHistory.map(p => p.year))].sort((a, b) => a - b);
setAvailableYears(years);
if (years.length > 0) {
setSelectedYear(years[0].toString());
}
}
}
}, [selectedModel, watches]);
// Update chart when model or year changes
useEffect(() => {
if (!selectedModel || !selectedYear || !chartRef.current) return;
const watch = watches.find(w => w.id === selectedModel);
if (!watch || !watch.priceHistory) return;
// Filter data up to selected year
const yearNum = parseInt(selectedYear);
const filteredHistory = watch.priceHistory.filter(p => p.year <= yearNum);
// Separate retail prices (new condition) and auction sales (vintage/other)
const retailPrices = filteredHistory
.filter(p => p.condition === 'new')
.map(p => ({ year: p.year, price: p.price }));
const auctionSales = filteredHistory
.filter(p => p.condition !== 'new' && (p.condition === 'vintage' || p.condition === 'used' || p.condition === 'auction'))
.map(p => ({ year: p.year, price: p.price }));
// Destroy previous chart
if (chartInstance.current) {
chartInstance.current.destroy();
}
const ctx = chartRef.current.getContext('2d');
// Prepare data for chart
const allYears = [...new Set([...retailPrices.map(p => p.year), ...auctionSales.map(p => p.year)])].sort((a, b) => a - b);
const retailData = allYears.map(year => {
const entry = retailPrices.find(p => p.year === year);
return entry ? entry.price : null;
});
const auctionData = allYears.map(year => {
const entry = auctionSales.find(p => p.year === year);
return entry ? entry.price : null;
});
// Create chart
chartInstance.current = new Chart(ctx, {
type: 'line',
data: {
labels: allYears,
datasets: [
{
label: 'Retail Price (New)',
data: retailData,
borderColor: '#2563eb',
backgroundColor: 'rgba(37, 99, 235, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 6,
pointHoverRadius: 8,
pointBackgroundColor: '#2563eb',
pointBorderColor: '#fff',
pointBorderWidth: 2,
spanGaps: false
},
{
label: 'Auction Sales (Vintage/Used)',
data: auctionData,
borderColor: '#dc2626',
backgroundColor: 'rgba(220, 38, 38, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 6,
pointHoverRadius: 8,
pointBackgroundColor: '#dc2626',
pointBorderColor: '#fff',
pointBorderWidth: 2,
spanGaps: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: {
size: 14,
weight: 'bold'
},
padding: 15,
usePointStyle: true
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleFont: {
size: 14,
weight: 'bold'
},
bodyFont: {
size: 13
},
padding: 12,
callbacks: {
label: function(context) {
if (context.parsed.y === null) {
return context.dataset.label + ': No data';
}
return context.dataset.label + ': $' + context.parsed.y.toLocaleString();
}
}
},
title: {
display: true,
text: watch.name + ' - Price History',
font: {
size: 18,
weight: 'bold'
},
padding: 20
}
},
scales: {
y: {
beginAtZero: false,
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
},
font: {
size: 12
}
},
grid: {
color: 'rgba(0, 0, 0, 0.1)'
},
title: {
display: true,
text: 'Price (USD)',
font: {
size: 14,
weight: 'bold'
}
}
},
x: {
ticks: {
font: {
size: 12
}
},
grid: {
display: false
},
title: {
display: true,
text: 'Year',
font: {
size: 14,
weight: 'bold'
}
}
}
}
}
});
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
}
};
}, [selectedModel, selectedYear, watches]);
const selectedWatch = watches.find(w => w.id === selectedModel);
return (
<div className="max-w-7xl mx-auto p-6 space-y-6">
<div className="bg-white rounded-lg shadow-lg p-6">
<h1 className="text-3xl font-bold text-gray-800 mb-6">Omega Watch Price History</h1>
{/* Dropdowns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label htmlFor="model-select" className="block text-sm font-semibold text-gray-700 mb-2">
Model
</label>
<select
id="model-select"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-800 bg-white"
>
{watches.map(watch => (
<option key={watch.id} value={watch.id}>
{watch.name} ({watch.collection})
</option>
))}
</select>
</div>
<div>
<label htmlFor="year-select" className="block text-sm font-semibold text-gray-700 mb-2">
Year (Show data up to)
</label>
<select
id="year-select"
value={selectedYear}
onChange={(e) => setSelectedYear(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-800 bg-white"
disabled={!selectedModel}
>
{availableYears.map(year => (
<option key={year} value={year.toString()}>
{year}
</option>
))}
</select>
</div>
</div>
{/* Watch Info */}
{selectedWatch && (
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<span className="text-sm text-gray-600">Collection:</span>
<p className="font-semibold text-gray-800">{selectedWatch.collection}</p>
</div>
<div>
<span className="text-sm text-gray-600">Reference:</span>
<p className="font-semibold text-gray-800">{selectedWatch.reference}</p>
</div>
<div>
<span className="text-sm text-gray-600">Year Introduced:</span>
<p className="font-semibold text-gray-800">{selectedWatch.year_introduced}</p>
</div>
</div>
{selectedWatch.description && (
<div className="mt-4">
<span className="text-sm text-gray-600">Description:</span>
<p className="text-gray-800 mt-1">{selectedWatch.description}</p>
</div>
)}
</div>
)}
{/* Chart */}
<div className="bg-white rounded-lg p-6 border border-gray-200">
<div className="h-96">
<canvas ref={chartRef}></canvas>
</div>
</div>
{/* Legend Info */}
<div className="bg-blue-50 rounded-lg p-4 mt-4">
<h3 className="font-semibold text-gray-800 mb-2">Chart Information:</h3>
<ul className="space-y-1 text-sm text-gray-700">
<li><span className="font-semibold text-blue-600">Blue Line:</span> Retail prices for new watches</li>
<li><span className="font-semibold text-red-600">Red Line:</span> Confirmed auction sales (vintage/used condition)</li>
<li className="mt-2 text-gray-600">Data shows prices up to the selected year</li>
</ul>
</div>
</div>
</div>
);
}
export default OmegaPriceHistoryChart;