← back to Watches

src/components/LuxuryDataVisualizations.jsx

545 lines

/**
 * LUXURY DATA VISUALIZATIONS
 * Premium chart and data visualization components
 *
 * Features:
 * - 3D price trend visualizations
 * - Interactive timeline
 * - Comparison heat maps
 * - Animated statistics
 * - Cinematic chart reveals
 */

import React, { useEffect, useRef, useMemo } from 'react';
import { motion } from 'framer-motion';
import { Line, Bar, Radar, Scatter } from 'react-chartjs-2';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  RadialLinearScale,
  ArcElement,
  Title,
  Tooltip,
  Legend,
  Filler,
} from 'chart.js';
import { AnimatedNumber } from './LuxuryInteractions';

ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  RadialLinearScale,
  ArcElement,
  Title,
  Tooltip,
  Legend,
  Filler
);

// ============================================================================
// LUXURY CHART WRAPPER with cinematic reveal
// ============================================================================
export const LuxuryChartContainer = ({ title, subtitle, children, icon, height = 400 }) => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 30, rotateX: -10 }}
      whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
      className="
        bg-white dark:bg-gray-800
        rounded-2xl shadow-xl
        overflow-hidden
        border border-gray-200 dark:border-gray-700
      "
    >
      {/* Header */}
      <div className="px-6 py-5 bg-gradient-to-r from-omega-navy to-gray-800 text-white">
        <div className="flex items-center space-x-3">
          {icon && <span className="text-2xl">{icon}</span>}
          <div>
            <h3 className="text-xl font-bold">{title}</h3>
            {subtitle && (
              <p className="text-sm text-gray-300 mt-1">{subtitle}</p>
            )}
          </div>
        </div>
      </div>

      {/* Chart content */}
      <div className="p-6" style={{ height }}>
        {children}
      </div>
    </motion.div>
  );
};

// ============================================================================
// PREMIUM LINE CHART with gradient fill
// ============================================================================
export const PremiumLineChart = ({ data, title, yAxisLabel, xAxisLabel }) => {
  const chartData = {
    labels: data.labels,
    datasets: [
      {
        label: title,
        data: data.values,
        borderColor: '#C8102E',
        backgroundColor: (context) => {
          const ctx = context.chart.ctx;
          const gradient = ctx.createLinearGradient(0, 0, 0, 400);
          gradient.addColorStop(0, 'rgba(200, 16, 46, 0.3)');
          gradient.addColorStop(0.5, 'rgba(200, 16, 46, 0.1)');
          gradient.addColorStop(1, 'rgba(200, 16, 46, 0)');
          return gradient;
        },
        borderWidth: 3,
        fill: true,
        tension: 0.4,
        pointRadius: 6,
        pointHoverRadius: 10,
        pointBackgroundColor: '#C8102E',
        pointBorderColor: '#fff',
        pointBorderWidth: 3,
        pointHoverBackgroundColor: '#FFD700',
        pointHoverBorderColor: '#C8102E',
        pointHoverBorderWidth: 3,
      },
    ],
  };

  const options = {
    responsive: true,
    maintainAspectRatio: false,
    interaction: {
      mode: 'index',
      intersect: false,
    },
    plugins: {
      legend: {
        display: false,
      },
      tooltip: {
        backgroundColor: '#1A1A2E',
        titleColor: '#FFD700',
        bodyColor: '#FFFFFF',
        borderColor: '#C8102E',
        borderWidth: 2,
        padding: 16,
        displayColors: false,
        titleFont: {
          size: 14,
          weight: 'bold',
        },
        bodyFont: {
          size: 13,
        },
        callbacks: {
          label: (context) => {
            return `$${context.parsed.y.toLocaleString()}`;
          },
        },
      },
    },
    scales: {
      x: {
        grid: {
          display: false,
        },
        ticks: {
          color: '#9CA3AF',
          font: {
            size: 11,
          },
        },
        title: {
          display: !!xAxisLabel,
          text: xAxisLabel,
          color: '#6B7280',
          font: {
            size: 12,
            weight: 'bold',
          },
        },
      },
      y: {
        grid: {
          color: 'rgba(156, 163, 175, 0.1)',
          drawBorder: false,
        },
        ticks: {
          color: '#9CA3AF',
          font: {
            size: 11,
          },
          callback: (value) => `$${(value / 1000).toFixed(0)}K`,
        },
        title: {
          display: !!yAxisLabel,
          text: yAxisLabel,
          color: '#6B7280',
          font: {
            size: 12,
            weight: 'bold',
          },
        },
      },
    },
  };

  return <Line data={chartData} options={options} />;
};

// ============================================================================
// COMPARISON HEAT MAP
// ============================================================================
export const ComparisonHeatMap = ({ watches }) => {
  const heatmapData = useMemo(() => {
    return watches.map(watch => {
      const first = watch.priceHistory[0].price;
      const last = watch.priceHistory[watch.priceHistory.length - 1].price;
      const appreciation = ((last - first) / first) * 100;

      return {
        name: watch.model,
        appreciation,
        currentPrice: last,
        series: watch.series,
      };
    }).sort((a, b) => b.appreciation - a.appreciation);
  }, [watches]);

  const getHeatColor = (appreciation) => {
    if (appreciation >= 100) return 'from-green-600 to-green-700';
    if (appreciation >= 50) return 'from-green-500 to-green-600';
    if (appreciation >= 25) return 'from-yellow-500 to-green-500';
    if (appreciation >= 0) return 'from-yellow-400 to-yellow-500';
    return 'from-red-500 to-red-600';
  };

  return (
    <div className="space-y-2">
      {heatmapData.map((watch, index) => (
        <motion.div
          key={watch.name}
          initial={{ opacity: 0, x: -20 }}
          animate={{ opacity: 1, x: 0 }}
          transition={{ delay: index * 0.05 }}
          className="relative overflow-hidden rounded-lg group"
        >
          {/* Background bar */}
          <motion.div
            initial={{ scaleX: 0 }}
            whileInView={{ scaleX: 1 }}
            viewport={{ once: true }}
            transition={{ duration: 0.8, delay: index * 0.05, ease: 'easeOut' }}
            className={`
              absolute inset-0 bg-gradient-to-r ${getHeatColor(watch.appreciation)}
              origin-left
            `}
            style={{ opacity: 0.15 }}
          />

          {/* Content */}
          <div className="relative flex items-center justify-between p-4 hover:bg-white/5 transition-colors">
            <div className="flex items-center space-x-4 flex-1 min-w-0">
              <span className="text-sm font-bold text-gray-500 dark:text-gray-400 w-8 text-center">
                #{index + 1}
              </span>
              <div className="flex-1 min-w-0">
                <h4 className="font-semibold text-gray-800 dark:text-white truncate group-hover:text-omega-red transition-colors">
                  {watch.name}
                </h4>
                <p className="text-xs text-gray-500 dark:text-gray-400">
                  {watch.series} • ${watch.currentPrice.toLocaleString()}
                </p>
              </div>
            </div>

            <div className="flex items-center space-x-3">
              <AnimatedNumber
                value={watch.appreciation}
                prefix="+"
                suffix="%"
                decimals={2}
                duration={1000 + index * 50}
              />
              <div className={`px-3 py-1 rounded-full text-xs font-bold ${
                watch.appreciation >= 50 ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' :
                watch.appreciation >= 0 ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' :
                'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
              }`}>
                {watch.appreciation >= 50 ? 'Excellent' :
                 watch.appreciation >= 25 ? 'Very Good' :
                 watch.appreciation >= 0 ? 'Good' : 'Fair'}
              </div>
            </div>
          </div>
        </motion.div>
      ))}
    </div>
  );
};

// ============================================================================
// INTERACTIVE OMEGA TIMELINE
// ============================================================================
export const OmegaTimeline = ({ watches }) => {
  const timelineEvents = useMemo(() => {
    const events = watches.map(watch => ({
      year: watch.yearIntroduced,
      model: watch.model,
      series: watch.series,
      reference: watch.reference,
    }));

    return events.sort((a, b) => a.year - b.year);
  }, [watches]);

  const decades = useMemo(() => {
    const years = timelineEvents.map(e => e.year);
    const minYear = Math.min(...years);
    const maxYear = Math.max(...years);
    const result = [];

    for (let year = Math.floor(minYear / 10) * 10; year <= maxYear; year += 10) {
      result.push(year);
    }

    return result;
  }, [timelineEvents]);

  return (
    <div className="relative py-8">
      {/* Timeline line */}
      <div className="absolute left-1/2 top-0 bottom-0 w-1 bg-gradient-to-b from-omega-red via-omega-gold to-omega-red" />

      {/* Events */}
      <div className="space-y-12">
        {timelineEvents.map((event, index) => {
          const isLeft = index % 2 === 0;

          return (
            <motion.div
              key={`${event.year}-${event.reference}`}
              initial={{ opacity: 0, x: isLeft ? -50 : 50 }}
              whileInView={{ opacity: 1, x: 0 }}
              viewport={{ once: true, margin: '-100px' }}
              transition={{ duration: 0.6, delay: index * 0.1 }}
              className={`flex items-center ${isLeft ? 'flex-row' : 'flex-row-reverse'}`}
            >
              {/* Content card */}
              <div className={`w-5/12 ${isLeft ? 'text-right pr-8' : 'text-left pl-8'}`}>
                <motion.div
                  whileHover={{ scale: 1.05, rotateY: 5 }}
                  className="
                    bg-white dark:bg-gray-800
                    rounded-xl shadow-lg p-6
                    border-2 border-transparent
                    hover:border-omega-red
                    transition-all duration-300
                    cursor-pointer
                  "
                >
                  <span className="text-sm font-bold text-omega-red">{event.year}</span>
                  <h4 className="text-lg font-bold text-gray-800 dark:text-white mt-1">
                    {event.model}
                  </h4>
                  <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
                    {event.series}
                  </p>
                  <p className="text-xs text-gray-500 dark:text-gray-500 mt-2 font-mono">
                    {event.reference}
                  </p>
                </motion.div>
              </div>

              {/* Timeline dot */}
              <motion.div
                initial={{ scale: 0 }}
                whileInView={{ scale: 1 }}
                viewport={{ once: true }}
                transition={{ duration: 0.3, delay: index * 0.1 + 0.3 }}
                className="
                  relative z-10
                  w-6 h-6 rounded-full
                  bg-omega-red
                  border-4 border-white dark:border-gray-900
                  shadow-lg
                "
              >
                <motion.div
                  animate={{ scale: [1, 1.5, 1] }}
                  transition={{
                    duration: 2,
                    repeat: Infinity,
                    delay: index * 0.2,
                  }}
                  className="absolute inset-0 rounded-full bg-omega-red/30"
                />
              </motion.div>

              {/* Empty space on other side */}
              <div className="w-5/12" />
            </motion.div>
          );
        })}
      </div>
    </div>
  );
};

// ============================================================================
// RADIAL PERFORMANCE CHART
// ============================================================================
export const RadialPerformanceChart = ({ watch }) => {
  const specs = watch.specifications;

  const data = {
    labels: [
      'Value Appreciation',
      'Brand Heritage',
      'Build Quality',
      'Movement',
      'Rarity',
      'Condition',
    ],
    datasets: [
      {
        label: watch.model,
        data: [
          Math.min(((watch.priceHistory[watch.priceHistory.length - 1].price / watch.priceHistory[0].price - 1) * 100), 100),
          85, // Brand heritage (Omega is premium)
          90, // Build quality
          specs?.movement?.includes('Co-Axial') ? 95 : 80,
          70, // Rarity (example)
          90, // Condition (example - could be from data)
        ],
        backgroundColor: 'rgba(200, 16, 46, 0.2)',
        borderColor: '#C8102E',
        borderWidth: 3,
        pointBackgroundColor: '#C8102E',
        pointBorderColor: '#fff',
        pointHoverBackgroundColor: '#FFD700',
        pointHoverBorderColor: '#C8102E',
        pointRadius: 5,
        pointHoverRadius: 8,
      },
    ],
  };

  const options = {
    responsive: true,
    maintainAspectRatio: false,
    scales: {
      r: {
        min: 0,
        max: 100,
        ticks: {
          stepSize: 20,
          backdropColor: 'transparent',
          color: '#9CA3AF',
          font: {
            size: 10,
          },
        },
        grid: {
          color: 'rgba(156, 163, 175, 0.2)',
        },
        pointLabels: {
          color: '#6B7280',
          font: {
            size: 12,
            weight: 'bold',
          },
        },
      },
    },
    plugins: {
      legend: {
        display: false,
      },
      tooltip: {
        backgroundColor: '#1A1A2E',
        titleColor: '#FFD700',
        bodyColor: '#FFFFFF',
        borderColor: '#C8102E',
        borderWidth: 2,
        padding: 12,
        callbacks: {
          label: (context) => {
            return `${context.label}: ${context.parsed.r.toFixed(0)}%`;
          },
        },
      },
    },
  };

  return <Radar data={data} options={options} />;
};

// ============================================================================
// ANIMATED STAT CARD
// ============================================================================
export const AnimatedStatCard = ({ title, value, prefix = '', suffix = '', icon, trend, color = 'omega-red' }) => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      whileHover={{ y: -5, boxShadow: '0 20px 40px rgba(0,0,0,0.1)' }}
      className="
        bg-white dark:bg-gray-800
        rounded-2xl shadow-lg p-6
        border border-gray-200 dark:border-gray-700
        relative overflow-hidden
      "
    >
      {/* Background gradient */}
      <div className={`absolute top-0 right-0 w-32 h-32 bg-${color}/5 rounded-full blur-3xl`} />

      <div className="relative z-10">
        <div className="flex items-start justify-between mb-4">
          <span className="text-sm font-medium text-gray-600 dark:text-gray-400">
            {title}
          </span>
          {icon && (
            <div className={`p-3 bg-${color}/10 rounded-xl`}>
              <span className="text-2xl">{icon}</span>
            </div>
          )}
        </div>

        <div className="flex items-baseline space-x-2">
          <span className={`text-4xl font-bold text-${color}`}>
            <AnimatedNumber value={value} prefix={prefix} suffix={suffix} duration={1500} />
          </span>
          {trend && (
            <span className={`text-sm font-semibold ${
              trend > 0 ? 'text-green-600' : 'text-red-600'
            }`}>
              {trend > 0 ? '↑' : '↓'} {Math.abs(trend)}%
            </span>
          )}
        </div>
      </div>
    </motion.div>
  );
};

export default {
  LuxuryChartContainer,
  PremiumLineChart,
  ComparisonHeatMap,
  OmegaTimeline,
  RadialPerformanceChart,
  AnimatedStatCard,
};