diff --git a/api-platform/api/src/Entity/Commune.php b/api-platform/api/src/Entity/Commune.php index ee12563ad4dd3b21fbb41589b4c07166be2b28cd..8c5ba42b9a942cfd900b5e521945aaf246878cc3 100644 --- a/api-platform/api/src/Entity/Commune.php +++ b/api-platform/api/src/Entity/Commune.php @@ -9,6 +9,7 @@ use App\Repository\CommuneRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: CommuneRepository::class)] #[ApiResource( @@ -17,6 +18,9 @@ use Doctrine\ORM\Mapping as ORM; new GetCollection() ], paginationClientEnabled: true, + normalizationContext: [ + 'groups' => ['read'] + ], )] class Commune { @@ -29,6 +33,7 @@ class Commune private ?int $code = null; #[ORM\Column(length: 255)] + #[Groups(['read'])] private ?string $nom = null; #[ORM\ManyToOne()] diff --git a/api-platform/api/src/Entity/Taxe.php b/api-platform/api/src/Entity/Taxe.php index 3653e67618ecd356febcf170d6fb9d8e54c0777c..508554c9e4c3803717d7fe4a848b614787c9195c 100644 --- a/api-platform/api/src/Entity/Taxe.php +++ b/api-platform/api/src/Entity/Taxe.php @@ -10,6 +10,7 @@ use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; use App\Repository\TaxeRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: TaxeRepository::class)] #[ApiResource( @@ -19,6 +20,9 @@ use Doctrine\ORM\Mapping as ORM; ], paginationClientEnabled: true, paginationClientItemsPerPage: true, + normalizationContext: [ + 'groups' => ['read'] + ], )] #[ApiFilter(SearchFilter::class, properties: [ 'type' => 'exact', @@ -32,23 +36,29 @@ class Taxe #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] + #[Groups('read')] private ?int $id = null; #[ORM\ManyToOne(cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] + #[Groups('read')] private ?Commune $commune = null; #[ORM\Column] + #[Groups('read')] private ?int $annee = null; #[ORM\ManyToOne()] #[ORM\JoinColumn(nullable: false)] + #[Groups('read')] private ?TypeTaxe $type = null; #[ORM\Column] + #[Groups('read')] private ?float $taux = null; #[ORM\Column] + #[Groups('read')] private ?float $volume = null; public function getId(): ?int diff --git a/front/package-lock.json b/front/package-lock.json index 2a16d488f270014b7fc9d752e13e4081e3809b3b..5762d300532c84542745507c7b561e491454e529 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -13,10 +13,12 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.1.18", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", "recharts": "^2.15.4", @@ -1591,6 +1593,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", @@ -3987,6 +3995,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -7419,6 +7439,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", diff --git a/front/package.json b/front/package.json index eb767ef1a6caf7d59261c33dc86f6e14ae4e9359..8286e4cebfa2f97c19f24cee208750f0ae1cd566 100644 --- a/front/package.json +++ b/front/package.json @@ -15,16 +15,18 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.1.18", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", "recharts": "^2.15.4", "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7", "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0" }, "devDependencies": { diff --git a/front/src/components/ScatterPlot.tsx b/front/src/components/ScatterPlot.tsx index 845b0691b434c41c636425c4a9cbccbee8bb5c92..1580a39898eab5128f6165da695e85f8be68c748 100644 --- a/front/src/components/ScatterPlot.tsx +++ b/front/src/components/ScatterPlot.tsx @@ -1,33 +1,55 @@ -import { - ScatterChart, - Scatter, - XAxis, - YAxis, - Tooltip, - CartesianGrid, -} from 'recharts'; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from '@/components/ui/chart'; +import { Chart as ChartJS, LinearScale, PointElement, Tooltip as ChartJSTooltip, Legend } from 'chart.js'; +import type { ChartData, ChartOptions } from 'chart.js'; +import { Scatter } from 'react-chartjs-2'; type Point = { commune: string; taux: number; volume: number }; export default function ScatterPlot({ data }: { data: Point[] }) { - const config = { - Communes: { label: 'Communes', color: '#0B5FFF' }, - } as const; + + ChartJS.register(LinearScale, PointElement, ChartJSTooltip, Legend); + + const chartData: ChartData<'scatter'> = { + datasets: [ + { + label: 'Communes', + data: data.map((p) => ({ x: p.taux, y: p.volume, commune: p.commune } as any)), + backgroundColor: 'var(--color-Communes)', + pointRadius: 4, + }, + ], + }; + + const options: ChartOptions<'scatter'> = { + responsive: true, + maintainAspectRatio: false, + animation: false, + scales: { + x: { + type: 'linear', + title: { display: true, text: 'Taux (%)' }, + }, + y: { + title: { display: true, text: 'Volume (€)' }, + }, + }, + plugins: { + tooltip: { + callbacks: { + title: () => '', + label: (ctx) => { + const r: any = ctx.raw; + const commune = r?.commune ?? ''; + return `${commune} — Taux: ${r.x}%, Volume: €${r.y}`; + }, + }, + }, + legend: { display: false }, + }, + }; return ( - - - - - - } /> - - - +
+ +
); } diff --git a/front/src/components/ui/select.tsx b/front/src/components/ui/select.tsx index 2ca1df8d11067aa34b75f04fbc8eb330ef53521a..d549a86bb6c694961bcdfb9786c30159c289e43c 100644 --- a/front/src/components/ui/select.tsx +++ b/front/src/components/ui/select.tsx @@ -88,6 +88,8 @@ const SelectContent = React.forwardRef< position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" )} + + style={{ maxHeight: '50vh' }} > {children} diff --git a/front/src/pages/Dashboard.tsx b/front/src/pages/Dashboard.tsx index 33a7a3ce9238d01816aac9da5391975e07b659d5..42462f2dce7643ef6452586c7a828c41e63bd1a0 100644 --- a/front/src/pages/Dashboard.tsx +++ b/front/src/pages/Dashboard.tsx @@ -215,6 +215,8 @@ export default function DashboardPage() { // Scatter: transform API data const scatterData = useMemo(() => { return rawScatterData.map((t: any) => { + console.log(t); + let name = "Commune"; if (t.commune && typeof t.commune === "object" && t.commune.nom) { name = t.commune.nom; @@ -222,6 +224,7 @@ export default function DashboardPage() { // Try to extract ID from IRI and match with mock if real name not available const parts = t.commune.split("/"); const id = parts[parts.length - 1]; + name = `Commune ${id}`; }