Nuage.tsx 10,5 ko
Newer Older
antoH's avatar
antoH a validé
"use client";

import { useEffect, useMemo, useState } from "react";
antoH's avatar
antoH a validé
import * as d3 from "d3";
import {taxes} from '@/data/taxes';
import { CommuneData } from "@/type/CommuneData";
import { Departement } from "@/type/Departement";
antoH's avatar
antoH a validé

/**
 * TODO : 
 *  - déplacer les années tous
 *  - déplacer svg
 */
export default function ScatterDepartement() {
  const [year, setYear] = useState(2022);
  const [taxe, setTaxe] = useState("cves");
  const [departements, setDepartements] = useState<Departement[]>([]);
  const [selectedDepartement, setSelectedDepartement] = useState<string>("");
  const [hovered, setHovered] = useState<string | null>(null);
  const [data, setData] = useState<CommuneData[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const [tooltip, setTooltip] = useState<{
    x: number;
    y: number;
    data: CommuneData;
  } | null>(null);

antoH's avatar
antoH a validé

  const width = 700;
  const height = 420;
  const margin = { top: 40, right: 30, bottom: 50, left: 60 };

  /**
  * Récupérer tous les départements
  */
  useEffect(() => {

    const fetchData = async () => {
      setIsLoading(true);

      try {
        const res = await fetch(`https://localhost/departements`);

        if (!res.ok) throw new Error("Erreur API");

        const apiData = await res.json();

        const formatted: Departement[] = apiData.member.map((d: any) => ({
          nom: d.nom
        }));

        setDepartements(formatted);

        if (formatted.length > 0) {
          setSelectedDepartement(formatted[0].nom);
        }

      } catch (err) {
        console.error("Erreur fetch", err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, []);

  /**
   * Récupérer les taux et le volume collectés
antoH's avatar
antoH a validé
   */

  useEffect(() => {
    if (!selectedDepartement) return;

    const fetchData = async () => {
      setIsLoading(true);

      try {
        const res = await fetch(
          `https://localhost/${taxe}?page=1&departement.nom=${selectedDepartement}&annee=${year}&order[tauxNet]=desc`
        );

        if (!res.ok) throw new Error("Erreur API data");

        const apiData = await res.json();

        console.log(apiData);
        const members = Array.isArray(apiData.member) ? apiData.member : [];

        if (members.length === 0) {
          setData([]);
          return;
        }

        const formatted: CommuneData[] = members.map((d: any) => ({

          commune: d.nomCommune,
          departement: selectedDepartement,
          year: d.annee,
          taxType: taxe,
          taxRate: d.tauxNet,
          volume: d.montantReel,
        }));


        for (let x = 0; x < 5; x++) {
          console.log(formatted[x]);
        }

        setData(formatted);


      } catch (err) {
        console.error("Erreur fetch data:", err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();

  }, [taxe, selectedDepartement, year]);

 * Couleurs
 */

  const communes = useMemo(() => {
    return Array.from(new Set(data.map(d => d.commune)));
  }, [data]);

  const colorScale = useMemo(() => {
    return d3.scaleOrdinal<string>()
      .domain(communes)
      .range(d3.schemeTableau10);
  }, [communes]);

  /**
   * Scales
antoH's avatar
antoH a validé
   */
  const xScale = useMemo(() => {
    const domain = data.length
      ? d3.extent(data, d => d.taxRate) as [number, number]
      : [0, 100]; // valeur par défaut

antoH's avatar
antoH a validé
    return d3.scaleLinear()
      .domain(domain)
antoH's avatar
antoH a validé
      .nice()
      .range([margin.left, width - margin.right]);
  }, [data]);
antoH's avatar
antoH a validé

  const yScale = useMemo(() => {
    const domain = data.length
      ? d3.extent(data, d => d.volume) as [number, number]
      : [0, 1000000]; // valeur par défaut

antoH's avatar
antoH a validé
    return d3.scaleLinear()
      .domain(domain)
antoH's avatar
antoH a validé
      .nice()
      .range([height - margin.bottom, margin.top]);
  }, [data]);


  if (!xScale || !yScale) return <p>Chargement...</p>;

antoH's avatar
antoH a validé

  const xTicks = xScale.ticks(5);
  const yTicks = yScale.ticks(5);

  return (
    <section className="w-full max-w-5xl">
antoH's avatar
antoH a validé
      <div className="rounded-xl bg-white shadow-sm p-6">
antoH's avatar
antoH a validé
        <h2 className="text-2xl font-semibold text-gray-800 mb-1">
          Relation taux d'imposition / Volume collecté
antoH's avatar
antoH a validé
        </h2>

        {/* CONTROLS */}
antoH's avatar
antoH a validé
        <div className="mb-6 flex flex-wrap gap-4">
antoH's avatar
antoH a validé
          <select
            value={taxe}
            onChange={e => setTaxe(e.target.value)}
            className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500m"
antoH's avatar
antoH a validé
          >
            {taxes.map(t => (
              <option key={t.route} value={t.route}>{t.label}</option>
            ))}
antoH's avatar
antoH a validé
          </select>

          <select
            value={year}
            onChange={e => setYear(+e.target.value)}
            className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
antoH's avatar
antoH a validé
          >
            <option value={2018}>2018</option>
            <option value={2019}>2019</option>
            <option value={2020}>2020</option>
            <option value={2021}>2021</option>
antoH's avatar
antoH a validé
            <option value={2022}>2022</option>
          </select>

          <select
            value={selectedDepartement}
            onChange={e => setSelectedDepartement(e.target.value)}
            className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
antoH's avatar
antoH a validé
          >
            {departements.map(dep => (
              <option key={dep.nom} value={dep.nom}>
                {dep.nom}
              </option>
            ))}
antoH's avatar
antoH a validé
          </select>
antoH's avatar
antoH a validé
        </div>

        {isLoading && <p className="text-gray-400">Chargement...</p>}

        {/* SVG */}
antoH's avatar
antoH a validé
        <svg width={width} height={height} className="mx-auto block">

          {!data.length && !isLoading && (
            <text
              x={width / 2}
              y={height / 2}
              textAnchor="middle"
              fill="#9ca3af"
              fontSize="16"
            >
              Aucune donnée pour cette sélection
            </text>
          )}


          {/* Label X */}
          <text
            x={width / 2}
            y={height - 10}
            textAnchor="middle"
            fontSize="12"
            fill="#374151"
          >
            Taux d'imposition (%)
          </text>

          {/* Label Y */}
          <text
            transform={`rotate(-90)`}
            x={-height / 2}
            y={20}
            textAnchor="middle"
            fontSize="12"
            fill="#374151"
          >
            Volume collecté (€)
          </text>



antoH's avatar
antoH a validé
          {yTicks.map(t => (
            <g key={t}>
              <line
                x1={margin.left - 6}
                x2={margin.left}
                y1={yScale(t)}
                y2={yScale(t)}
                stroke="#6b7280"
              />
              <text
                x={margin.left - 10}
                y={yScale(t) + 4}
                textAnchor="end"
                fontSize="11"
                fill="#374151"
              >
                {d3.format(".2s")(t)}
              </text>
            </g>

          {xTicks.map(t => (
            <g key={t}>
              <line
                x1={xScale(t)}
                x2={xScale(t)}
                y1={height - margin.bottom}
                y2={height - margin.bottom + 6}
                stroke="#6b7280"
              />
              <text
                x={xScale(t)}
                y={height - margin.bottom + 18}
                textAnchor="middle"
                fontSize="11"
                fill="#374151"
              >
                {t.toFixed(1)}
              </text>
            </g>
          ))}


antoH's avatar
antoH a validé
          {/* Axes */}
          <line
            x1={margin.left}
            x2={margin.left}
            y1={margin.top}
            y2={height - margin.bottom}
            stroke="#9ca3af"
          />
          <line
            x1={margin.left}
            x2={width - margin.right}
            y1={height - margin.bottom}
            y2={height - margin.bottom}
            stroke="#9ca3af"
          />

          {/* Points */}
          {data.map((d, i) => (
antoH's avatar
antoH a validé
            <circle
              key={i}
              cx={xScale(d.taxRate)}
              cy={yScale(d.volume)}
              r={hovered === d.commune ? 8 : 5}
              fill={colorScale(d.commune)}
              opacity={hovered && hovered !== d.commune ? 0.2 : 0.9}
              className="transition-all duration-200 cursor-pointer"

              onMouseEnter={(e) => {
                setHovered(d.commune);

                const rect = (e.target as SVGCircleElement).getBoundingClientRect();

                setTooltip({
                  x: rect.x + rect.width / 2,
                  y: rect.y,
                  data: d
                });
              }}

              onMouseLeave={() => {
                setHovered(null);
                setTooltip(null);
              }}
antoH's avatar
antoH a validé
        </svg>

        {tooltip && (
          <div
            className="fixed z-50 bg-white shadow-lg rounded-lg px-4 py-2 text-sm border border-gray-200 pointer-events-none"
            style={{
              left: tooltip.x,
              top: tooltip.y - 10,
              transform: "translate(-50%, -100%)"
            }}
          >
            <div className="font-semibold text-gray-800">
              {tooltip.data.commune}
            </div>
            <div className="text-gray-600">
              Taux : {tooltip.data.taxRate.toFixed(2)} %
            </div>
            <div className="text-gray-600">
              Volume : {d3.format(",")(tooltip.data.volume)}
            </div>
          </div>
        )}

        {/* légende */}
        <div className="mt-6 flex flex-wrap gap-4 justify-center">
          {communes.map(commune => (
            <div
              key={commune}
              onMouseEnter={() => setHovered(commune)}
              onMouseLeave={() => setHovered(null)}
              className="flex items-center gap-2 cursor-pointer transition-all"
            >
              <span
                className="w-3 h-3 rounded-sm"
                style={{ backgroundColor: colorScale(commune) }}
              />
              <span
                className={`text-sm ${hovered === commune ? "font-semibold text-gray-800" : "text-gray-500"}`} >
                {commune}
              </span>
            </div>
          ))}
        </div>

antoH's avatar
antoH a validé
      </div>
    </section>
  );
}