From 39bd876e4056896ad2a021216482b55611eebc1c Mon Sep 17 00:00:00 2001 From: Julien Furet Date: Thu, 12 Feb 2026 21:52:45 +0100 Subject: [PATCH 1/2] WIP --- .../components/molecules/RegionSelector.tsx | 47 ++++ pwa/app/components/organisms/Dashboard.tsx | 11 +- pwa/app/components/tabs/diagram.tsx | 215 +++++++++--------- pwa/app/components/tabs/temporal.tsx | 39 +--- 4 files changed, 159 insertions(+), 153 deletions(-) create mode 100644 pwa/app/components/molecules/RegionSelector.tsx diff --git a/pwa/app/components/molecules/RegionSelector.tsx b/pwa/app/components/molecules/RegionSelector.tsx new file mode 100644 index 0000000..71862c9 --- /dev/null +++ b/pwa/app/components/molecules/RegionSelector.tsx @@ -0,0 +1,47 @@ +'use client' + +interface RegionSelectorProps { + regions: string[] + selectedRegions: string[] + onSelectionChange: (regions: string[]) => void +} + +export default function RegionSelector({ regions, selectedRegions, onSelectionChange }: RegionSelectorProps) { + const toggleRegion = (region: string) => { + const newSelection = selectedRegions.includes(region) ? selectedRegions.filter(r => r !== region) : [...selectedRegions, region] + onSelectionChange(newSelection) + } + + if (regions.length === 0) return null + + return ( +
+ +
+ + + {regions.map(region => ( + + ))} +
+
+ ) +} diff --git a/pwa/app/components/organisms/Dashboard.tsx b/pwa/app/components/organisms/Dashboard.tsx index 8465687..6bf2c1e 100644 --- a/pwa/app/components/organisms/Dashboard.tsx +++ b/pwa/app/components/organisms/Dashboard.tsx @@ -11,8 +11,6 @@ interface DashboardProps { regions: string[] } -const tabKeys = ['temporal', 'points', 'diagram'] as const - export default function Dashboard({ regions }: DashboardProps) { const [activeTab, setActiveTab] = useState('temporal') const [sidebarOpen, setSidebarOpen] = useState(false) @@ -20,19 +18,14 @@ export default function Dashboard({ regions }: DashboardProps) { return (
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} + {sidebarOpen &&
setSidebarOpen(false)} />}
setSidebarOpen(prev => !prev)} />
{activeTab === 'temporal' && } {activeTab === 'points' && } - {activeTab === 'diagram' && } + {activeTab === 'diagram' && }
diff --git a/pwa/app/components/tabs/diagram.tsx b/pwa/app/components/tabs/diagram.tsx index c9df2e0..854079c 100644 --- a/pwa/app/components/tabs/diagram.tsx +++ b/pwa/app/components/tabs/diagram.tsx @@ -1,44 +1,60 @@ 'use client' -import { useState, useEffect } from 'react' -import { Doughnut } from 'react-chartjs-2' -import { Chart as ChartJS, ArcElement, Tooltip, Legend, TooltipItem } from 'chart.js' -import ChartDataLabels from 'chartjs-plugin-datalabels' +import { useState } from 'react' +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts' import { TAX_TYPES, YEARS, COLORS } from '../../constants' -import { getRegionDistribution, prepareChartData, RegionDistribution } from '../../services/diagram.services' +import { getRegionDistribution, RegionDistribution } from '../../services/diagram.services' import ErrorDiv from '../molecules/ErrorDiv' +import RegionSelector from '../molecules/RegionSelector' -ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels) +interface DiagramProps { + regions: string[] +} -export default function Diagram() { +export default function Diagram({ regions }: DiagramProps) { const [taxType, setTaxType] = useState('th') const [year, setYear] = useState(2019) - const [topCount, setTopCount] = useState(9) const [data, setData] = useState([]) - const [maxRegions, setMaxRegions] = useState(20) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - - // Load number of regions on mount - useEffect(() => { - const loadMaxRegions = async () => { - try { - const result = await getRegionDistribution(taxType, year) - setMaxRegions(result.length) - } catch { - // Keep default value on error - } - } - loadMaxRegions() - }, [taxType, year]) + // Regions currently selected for display (initialized from SSR props) + const [selectedRegions, setSelectedRegions] = useState(regions) const handleSubmit = async () => { + if (selectedRegions.length === 0) { + setData([]) + return + } setLoading(true) setError(null) try { const result = await getRegionDistribution(taxType, year) - setMaxRegions(result.length) - const chartData = prepareChartData(result, topCount) + + // Separate selected and non-selected regions + const selected = result.filter(r => selectedRegions.includes(r.region)) + const others = result.filter(r => !selectedRegions.includes(r.region)) + + // Calculate total for percentages + const totalAmount = result.reduce((sum, item) => sum + item.total_amount, 0) + + // Create chart data with selected regions + const chartData: RegionDistribution[] = selected.map(item => ({ + region: item.region, + total_amount: item.total_amount, + percentage: totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0, + })) + + // Add "Autres" if there are non-selected regions + if (others.length > 0) { + const othersTotal = others.reduce((sum, item) => sum + item.total_amount, 0) + const othersPercentage = totalAmount > 0 ? (othersTotal / totalAmount) * 100 : 0 + chartData.push({ + region: 'Autres', + total_amount: othersTotal, + percentage: othersPercentage, + otherRegions: others.map(item => item.region), + }) + } setData(chartData) } catch (e) { setData([]) @@ -48,73 +64,39 @@ export default function Diagram() { } } - const chartConfig = { - labels: data.map(d => d.region), - datasets: [ - { - data: data.map(d => d.total_amount), - backgroundColor: COLORS, - borderColor: '#1a1d21', - borderWidth: 2, - }, - ], + // Custom label to display percentage on slices + const renderLabel = (props: { payload?: RegionDistribution; percent?: number }) => { + const entry = props.payload + if (!entry || !entry.percentage || entry.percentage < 3) return '' + return `${entry.percentage.toFixed(1)}%` } - const chartOptions = { - responsive: true, - maintainAspectRatio: true, - aspectRatio: 1.5, - plugins: { - legend: { - position: 'right' as const, - labels: { - color: '#b0afaf', - padding: 25, - font: { size: 18 }, - boxWidth: 30, - boxHeight: 30, - }, - }, - tooltip: { - backgroundColor: '#212529', - borderColor: '#3a3f44', - borderWidth: 1, - titleFont: { size: 18 }, - bodyFont: { size: 16 }, - padding: 12, - callbacks: { - label: (ctx: TooltipItem<'doughnut'>) => { - const region = data[ctx.dataIndex] - if (!region) return '' - const amount = region.total_amount ? (region.total_amount / 1_000_000).toFixed(2) : '0.00' - const percentage = region.percentage ? region.percentage.toFixed(2) : '0.00' - - const lines = [`${region.region}`, `Volume: ${amount} M€`, `Part: ${percentage}%`] - - // If it's "Others", add the list of regions - if (region.region === 'Autres' && region.otherRegions) { - lines.push('', 'Régions incluses:') - region.otherRegions.forEach((r: string) => lines.push(` • ${r}`)) - } - - return lines - }, - }, - }, - datalabels: { - color: '#fff', - font: { - weight: 'bold' as const, - size: 24, - }, - formatter: (_value: unknown, ctx: { dataIndex: number }) => { - const region = data[ctx.dataIndex] - if (!region || !region.percentage) return '' - // Only display percentage if greater than 3% - return region.percentage >= 3 ? `${region.percentage.toFixed(1)}%` : '' - }, - }, - }, + // Custom tooltip + const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: { payload: RegionDistribution }[] }) => { + if (active && payload && payload.length && payload[0]) { + const region = payload[0].payload + const amount = region.total_amount ? (region.total_amount / 1_000_000).toFixed(2) : '0.00' + const percentage = region.percentage ? region.percentage.toFixed(2) : '0.00' + + return ( +
+

{region.region}

+

Volume: {amount} M€

+

Part: {percentage}%

+ {region.region === 'Autres' && region.otherRegions && region.otherRegions.length > 0 && ( + <> +

Régions incluses:

+ {region.otherRegions.map((r: string) => ( +

+ • {r} +

+ ))} + + )} +
+ ) + } + return null } return ( @@ -152,21 +134,6 @@ export default function Diagram() { -
- - -
- - {regions.length > 0 && ( -
- -
- - - {regions.map(region => ( - - ))} -
-
- )} + {error && } -- GitLab From 4061e9fce45339e052dcf7d196356187c2218739 Mon Sep 17 00:00:00 2001 From: Julien Furet Date: Thu, 12 Feb 2026 22:45:53 +0100 Subject: [PATCH 2/2] [FIX] - Diagram to SVG --- pwa/app/components/tabs/diagram.tsx | 182 ++++++++++++++++++--------- pwa/app/services/diagram.services.ts | 2 + 2 files changed, 124 insertions(+), 60 deletions(-) diff --git a/pwa/app/components/tabs/diagram.tsx b/pwa/app/components/tabs/diagram.tsx index 854079c..7485ff3 100644 --- a/pwa/app/components/tabs/diagram.tsx +++ b/pwa/app/components/tabs/diagram.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts' import { TAX_TYPES, YEARS, COLORS } from '../../constants' import { getRegionDistribution, RegionDistribution } from '../../services/diagram.services' @@ -12,57 +12,104 @@ interface DiagramProps { } export default function Diagram({ regions }: DiagramProps) { - const [taxType, setTaxType] = useState('th') - const [year, setYear] = useState(2019) + const [taxType, setTaxType] = useState('') + const [year, setYear] = useState(null) const [data, setData] = useState([]) + const [rawData, setRawData] = useState([]) // Store fetched data const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - // Regions currently selected for display (initialized from SSR props) - const [selectedRegions, setSelectedRegions] = useState(regions) + // Regions currently selected for display (all selected by default) + const [selectedRegions, setSelectedRegions] = useState([]) + const [isInitialized, setIsInitialized] = useState(false) + + // Initialize selectedRegions with all regions on first render + useEffect(() => { + if (!isInitialized && regions.length > 0) { + setSelectedRegions(regions) + setIsInitialized(true) + } + }, [regions, isInitialized]) + + // Fetch data only when tax type or year changes + useEffect(() => { + const fetchData = async () => { + if (!taxType || !year) { + setRawData([]) + setData([]) + return + } + setLoading(true) + setError(null) + try { + const result = await getRegionDistribution(taxType, year) + setRawData(result) + } catch (e) { + setRawData([]) + setData([]) + setError(e instanceof Error ? e.message : 'Une erreur est survenue') + } finally { + setLoading(false) + } + } - const handleSubmit = async () => { - if (selectedRegions.length === 0) { + fetchData() + }, [taxType, year]) + + // Filter and process data when selectedRegions or rawData changes + useEffect(() => { + if (rawData.length === 0 || selectedRegions.length === 0) { setData([]) return } - setLoading(true) - setError(null) - try { - const result = await getRegionDistribution(taxType, year) - - // Separate selected and non-selected regions - const selected = result.filter(r => selectedRegions.includes(r.region)) - const others = result.filter(r => !selectedRegions.includes(r.region)) - - // Calculate total for percentages - const totalAmount = result.reduce((sum, item) => sum + item.total_amount, 0) - - // Create chart data with selected regions - const chartData: RegionDistribution[] = selected.map(item => ({ - region: item.region, - total_amount: item.total_amount, - percentage: totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0, - })) - - // Add "Autres" if there are non-selected regions - if (others.length > 0) { - const othersTotal = others.reduce((sum, item) => sum + item.total_amount, 0) - const othersPercentage = totalAmount > 0 ? (othersTotal / totalAmount) * 100 : 0 - chartData.push({ - region: 'Autres', - total_amount: othersTotal, - percentage: othersPercentage, - otherRegions: others.map(item => item.region), + + // Calculate total for percentages + const totalAmount = rawData.reduce((sum, item) => sum + item.total_amount, 0) + + // Separate selected and non-selected regions + const selected = rawData.filter(r => selectedRegions.includes(r.region)) + const notSelected = rawData.filter(r => !selectedRegions.includes(r.region)) + + // Filter selected regions: keep only those >= 0.5%, others go to "Autres" + const significantRegions: RegionDistribution[] = [] + const smallRegions: RegionDistribution[] = [] + + selected.forEach(item => { + const percentage = totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0 + if (percentage >= 0.5) { + significantRegions.push({ + region: item.region, + total_amount: item.total_amount, + percentage: percentage, }) + } else { + smallRegions.push(item) } - setData(chartData) - } catch (e) { - setData([]) - setError(e instanceof Error ? e.message : 'Une erreur est survenue') - } finally { - setLoading(false) + }) + + // Combine small selected regions with non-selected regions for "Autres" + const othersRegions = [...smallRegions, ...notSelected] + + // Add "Autres" if there are regions to group + if (othersRegions.length > 0) { + const othersTotal = othersRegions.reduce((sum, item) => sum + item.total_amount, 0) + const othersPercentage = totalAmount > 0 ? (othersTotal / totalAmount) * 100 : 0 + significantRegions.push({ + region: 'Autres', + total_amount: othersTotal, + percentage: othersPercentage, + otherRegionsSmall: smallRegions.map(item => ({ + name: item.region, + percentage: totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0, + })), + otherRegionsNotSelected: notSelected.map(item => ({ + name: item.region, + percentage: totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0, + })), + }) } - } + + setData(significantRegions) + }, [rawData, selectedRegions]) // Custom label to display percentage on slices const renderLabel = (props: { payload?: RegionDistribution; percent?: number }) => { @@ -83,14 +130,28 @@ export default function Diagram({ regions }: DiagramProps) {

{region.region}

Volume: {amount} M€

Part: {percentage}%

- {region.region === 'Autres' && region.otherRegions && region.otherRegions.length > 0 && ( + {region.region === 'Autres' && ( <> -

Régions incluses:

- {region.otherRegions.map((r: string) => ( -

- • {r} -

- ))} + {region.otherRegionsSmall && region.otherRegionsSmall.length > 0 && ( + <> +

Régions sélectionnées (< 0.5%):

+ {region.otherRegionsSmall.map((r: { name: string; percentage: number }) => ( +

+ • {r.name} ({r.percentage < 0.01 ? '<0.01%' : `${r.percentage.toFixed(2)}%`}) +

+ ))} + + )} + {region.otherRegionsNotSelected && region.otherRegionsNotSelected.length > 0 && ( + <> +

Régions non sélectionnées:

+ {region.otherRegionsNotSelected.map((r: { name: string; percentage: number }) => ( +

+ • {r.name} ({r.percentage < 0.01 ? '<0.01%' : `${r.percentage.toFixed(2)}%`}) +

+ ))} + + )} )} @@ -111,6 +172,7 @@ export default function Diagram({ regions }: DiagramProps) { onChange={e => setTaxType(e.target.value)} className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm" > + {TAX_TYPES.map(t => (