From 8bc6ec844fb3560c2a595b5bb191a2be8c403fc3 Mon Sep 17 00:00:00 2001 From: firdaous elhalafi Date: Sat, 6 Jan 2024 19:40:39 +0100 Subject: [PATCH] =?UTF-8?q?recuperation=20des=20donn=C3=A9es=20de=20l'api?= =?UTF-8?q?=20pour=20le=20donut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/Service/SaleService.php | 2 - pwa/app/components/date-input.jsx | 7 - pwa/app/components/donut-chart/donut.jsx | 127 ++++++++++++++++++ pwa/app/components/donut-chart/year-input.jsx | 32 +++++ pwa/app/components/donut.jsx | 94 ------------- pwa/app/components/timeseries/timeseries.jsx | 31 +++-- pwa/app/globals.css | 12 ++ pwa/app/help/fetch-service.js | 11 +- pwa/app/page.jsx | 89 ++++++------ pwa/config.js | 5 + 10 files changed, 251 insertions(+), 159 deletions(-) delete mode 100644 pwa/app/components/date-input.jsx create mode 100644 pwa/app/components/donut-chart/donut.jsx create mode 100644 pwa/app/components/donut-chart/year-input.jsx delete mode 100644 pwa/app/components/donut.jsx diff --git a/api/src/Service/SaleService.php b/api/src/Service/SaleService.php index be0f120..b37e15b 100755 --- a/api/src/Service/SaleService.php +++ b/api/src/Service/SaleService.php @@ -55,8 +55,6 @@ class SaleService $result = $queryBuilder ->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'AVG(CASE WHEN s.surface <> 0 THEN s.amount / s.surface ELSE 0 END) as average_price') - #->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'SUM(s.amount) as total_price', 'SUM(s.surface) as total_surface') - #->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'AVG(s.amount) as total_price', 'AVG(s.surface) as total_surface') ->from(Sale::class, 's') ->groupBy('year, month') ->orderBy('year, month') diff --git a/pwa/app/components/date-input.jsx b/pwa/app/components/date-input.jsx deleted file mode 100644 index dc28209..0000000 --- a/pwa/app/components/date-input.jsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function DateInput() { - return ( -
- -
- ); -} diff --git a/pwa/app/components/donut-chart/donut.jsx b/pwa/app/components/donut-chart/donut.jsx new file mode 100644 index 0000000..6afc7c7 --- /dev/null +++ b/pwa/app/components/donut-chart/donut.jsx @@ -0,0 +1,127 @@ +"use client"; + +import React, {useEffect, useRef} from "react"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; +import * as d3 from "d3"; + +const defaultColorScheme = [ + "#1e64af", "#419bd2", "#69beeb", "#d7b428", "#e19196", + "#a5236e", "#eb1e4b", "#f06937", "#f5dc50", "#2d969b", + "#335c6c", "#ff7d2d", "#fac846", "#a0c382", "#5f9b8c", + "#325a69", "#fff5af", "#e1a03c", "#a0282d", "#550a0f" +]; + +/** + * @param {object} props + * @param {[{region: string, occurrences: number}]} props.data + */ +export default function Donut({data}) { + const ref = useRef(); + const width = 300; + const height = 300; + const padding = 20; + const thicknessOnMouseOver = 10; + const radius = Math.min(width, height) / 2 - padding; + const deathAngle = 0.2; + + useEffect(() => { + if (data === undefined || data.length === 0) { + return; + } + + const rawPieData = d3.pie().value((x) => x.occurrences)(data); + + let pieData = []; + const aggregate = { + data: { + region: "aggregate", + occurrences: 0 + }, + index: rawPieData.length, + value: 0, + padAngle: 0, + startAngle: 0, + endAngle: Math.PI * 2 + }; + + let lastEndAngle = 0; + for (const d of rawPieData) { + if (d.endAngle > lastEndAngle) { + lastEndAngle = d.endAngle; + } + // if d angle is less than deathAngle, we don't show it in final donut + if (d.endAngle - d.startAngle < deathAngle) { + aggregate.startAngle = lastEndAngle; + aggregate.endAngle -= d.endAngle - d.startAngle; + aggregate.value += d.value; + aggregate.data.occurrences += d.data.occurrences; + } else { + pieData.push(d); + } + } + pieData.push(aggregate); + + const arc = d3 + .arc() + .innerRadius(radius * 0.6) + .outerRadius(radius) + + const svg = d3.select(ref.current) + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", `translate(${width / 2}, ${height / 2})`); + + svg.selectAll("path") + .data(pieData) + .enter() + .append("path") + .attr("d", arc) + .attr("fill", (x, i) => x.data.region === "aggregate" ? "#dddddd" : defaultColorScheme[i % data.length]) + .on("mouseover", function (event, d) { + const percentage = (d.value / aggregate.value).toFixed(2); + const text = `${d.data.region}: ${percentage}%`; + d3.select(this) + .transition() + .duration(200) + .attr("stroke", this.attributes.fill.nodeValue) + .attr("stroke-width", thicknessOnMouseOver); + + svg.append("text") + .attr("class", "tooltip") + .attr("text-anchor", "middle") + .attr("dy",-radius) + .text(text) + .attr("transform", `translate(${width / 2 - 100}, ${height / 2 - 50})`); + this.parentNode.appendChild(this); + }) + .on("mouseout", function () { + d3.select(this) + .transition() + .attr("stroke", "none") + .attr("stroke-width", 0); + + svg.select(".tooltip").remove(); + }); + + }, [data]); + + return ( +
+ {/*isLoading && ( +
+ +
+ )*/} + +
+ ); +} diff --git a/pwa/app/components/donut-chart/year-input.jsx b/pwa/app/components/donut-chart/year-input.jsx new file mode 100644 index 0000000..8749a04 --- /dev/null +++ b/pwa/app/components/donut-chart/year-input.jsx @@ -0,0 +1,32 @@ +import React, {useState} from 'react'; +import constants from '../../../config.js'; + +function YearInput({ onDateChange }) { + const [selectedYear, setSelectedYear] = useState(`${constants.initialDonutChartYear.selectedYear}`); + + const handleYearChange = (event) => { + setSelectedYear(event.target.value); + }; + + const handleButtonClick = () => { + onDateChange(selectedYear); + }; + + return ( +
+ + +
+ ); +} + +export default YearInput; diff --git a/pwa/app/components/donut.jsx b/pwa/app/components/donut.jsx deleted file mode 100644 index 6fb65eb..0000000 --- a/pwa/app/components/donut.jsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client"; - -import React, {useEffect, useRef} from "react"; -import * as d3 from "d3"; - -const defaultColorScheme = [ - "#1e64af", "#419bd2", "#69beeb", "#d7b428", "#e19196", - "#a5236e", "#eb1e4b", "#f06937", "#f5dc50", "#2d969b", - "#335c6c", "#ff7d2d", "#fac846", "#a0c382", "#5f9b8c", - "#325a69", "#fff5af", "#e1a03c", "#a0282d", "#550a0f" -]; - -/** - * @param {object} props - * @param {[{id: string, count: number}]} props.data - */ -export default function Donut({data}) { - const ref = useRef(); - const width = 250; - const height = 250; - const padding = 20; - const thicknessOnMouseOver = 10; - const radius = Math.min(width, height) / 2 - padding; - const deathAngle = 0.2; - - useEffect(() => { - const rawPieData = d3.pie().value((x) => x.count)(data); - - let pieData = []; - const aggregate = { - data: { - id: "aggregate", - count: 0 - }, - index: rawPieData.length, - value: 0, - padAngle: 0, - startAngle: 0, - endAngle: Math.PI * 2 - }; - - let lastEndAngle = 0; - for (const d of rawPieData) { - if (d.endAngle > lastEndAngle) { - lastEndAngle = d.endAngle; - } - // if d angle is less than deathAngle, we don't show it in final donut - if (d.endAngle - d.startAngle < deathAngle) { - aggregate.startAngle = lastEndAngle; - aggregate.endAngle -= d.endAngle - d.startAngle; - aggregate.value += d.value; - aggregate.data.count += d.data.count; - } else { - pieData.push(d); - } - } - pieData.push(aggregate); - - const arc = d3 - .arc() - .innerRadius(radius * 0.6) - .outerRadius(radius) - - d3.select(ref.current) - .attr("width", width) - .attr("height", height) - .append("g") - .attr("transform", `translate(${width/2}, ${height/2})`) - .selectAll("path") - .data(pieData) - .enter() - .append("path") - .attr("d", arc) - .attr("fill", (x, i) => x.data.id === "aggregate" ? "#dddddd" : defaultColorScheme[i % data.length]) - .on("mouseover", function() { - d3.select(this) - .transition() - .duration(200) - .attr("stroke", this.attributes.fill.nodeValue) - .attr("stroke-width", thicknessOnMouseOver); - this.parentNode.appendChild(this); - }) - .on("mouseout", function() { - d3.select(this) - .transition() - .attr("stroke", "none") - .attr("stroke-width", 0); - }); - }, [data]); - - return ( - - ); -} diff --git a/pwa/app/components/timeseries/timeseries.jsx b/pwa/app/components/timeseries/timeseries.jsx index 8cd5e09..c08b19a 100644 --- a/pwa/app/components/timeseries/timeseries.jsx +++ b/pwa/app/components/timeseries/timeseries.jsx @@ -7,18 +7,15 @@ function Timeseries({ data }) { const chartRef = useRef(null); const isLoading = data.length === 0; - const width = 940; - const height = 400; - const marginTop = 20; - const marginRight = 20; - const marginBottom = 30; - const marginLeft = 60; + const drawTimeseries = () => + { + const width = 940; + const height = 400; + const marginTop = 20; + const marginRight = 20; + const marginBottom = 30; + const marginLeft = 60; - - useEffect(() => { - if (data.length === 0) { - return; - } const x = d3.scaleTime() .domain(d3.extent(data, d => new Date(d.year, d.month - 1))) .range([marginLeft, width - marginRight]); @@ -27,7 +24,6 @@ function Timeseries({ data }) { .domain([d3.min(data, d => d.averagePrice), d3.max(data, d => d.averagePrice)]) .range([height - marginBottom, marginTop]); - const svg = d3.select(chartRef.current) .attr("width", width) .attr("height", height); @@ -50,12 +46,19 @@ function Timeseries({ data }) { .attr("stroke", "steelblue") .attr("stroke-width", 2) .attr("d", line); + } + + useEffect(() => { + if (data && data.length > 0) { + drawTimeseries(); + } + }, [data]); return (
- {isLoading && ( + {/*isLoading && (
- )} + )*/}
); diff --git a/pwa/app/globals.css b/pwa/app/globals.css index cd3df02..108710c 100644 --- a/pwa/app/globals.css +++ b/pwa/app/globals.css @@ -10,3 +10,15 @@ html { margin: 0; font-family: sans-serif; } + +.donut-card { + width: 500px !important; + height: 500px; +} + +.donut-container { + display: flex; + align-items: center; + justify-content: center; + height: 70%; +} diff --git a/pwa/app/help/fetch-service.js b/pwa/app/help/fetch-service.js index e9b56d6..31d910f 100644 --- a/pwa/app/help/fetch-service.js +++ b/pwa/app/help/fetch-service.js @@ -3,7 +3,14 @@ import constants from '../../config.js'; * @param {number} year */ async function getDonutContent(year) { - return new Promise((resolve, reject) => { + const url = `${constants.config.apiUrl}/donut-chart/${year}`; + return fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + /* return new Promise((resolve, reject) => { resolve( [ "Auvergne-Rhône-Alpes", @@ -27,7 +34,7 @@ async function getDonutContent(year) { "Nouvelle Calédonie" ].map((x) => ({region: x, occurences: Math.round(Math.random() * 300)})) ); - }); + });*/ } const getTimesSeries = () => { diff --git a/pwa/app/page.jsx b/pwa/app/page.jsx index 1de97f9..0d7157b 100644 --- a/pwa/app/page.jsx +++ b/pwa/app/page.jsx @@ -1,78 +1,77 @@ "use client"; import React, {useEffect, useState} from "react"; -import constants from '../config.js'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; +import constants from '../config.js'; import FetchService from "./help/fetch-service"; -import Donut from "./components/donut.jsx"; -import DateInput from "./components/date-input"; + +import Donut from "./components/donut-chart/donut.jsx"; +import YearInput from "./components/donut-chart/year-input.jsx"; import DataForm from "./components/bar-chart/form"; import BarChart from "./components/bar-chart/chart"; import Timeseries from "./components/timeseries/timeseries.jsx"; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSpinner } from "@fortawesome/free-solid-svg-icons"; + export default function Page() { const [donutValues, setDonutValues] = useState([]); const [chartData, setChartData] = useState([]); - const [lineData, setLineData] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [timeseries, setTimeseries] = useState([]); + + const [isLoadingBar, setIsLoadingBar] = useState(false); + const [isLoadingDonut, setIsLoadingDonut] = useState(false); + const [isLoadingTimeseries, setIsLoadingTimesseries] = useState(false); const handleFormSubmit = async (formData) => { - setIsLoading(true); + setIsLoadingBar(true); const response = await FetchService.fetchBarChartData(formData); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const data = await response.json(); setChartData(data); - setIsLoading(false); + setIsLoadingBar(false); }; - const loadInitialBarChartData = async () => { - setIsLoading(true); - try { - const formData = { - startDate: `${constants.initialBarChartFormData.startDate}`, - endDate: `${constants.initialBarChartFormData.endDate}`, - granularity: `${constants.initialBarChartFormData.granularity}` - }; - const response = await FetchService.fetchBarChartData(formData); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - setChartData(data); - } catch (error) { - console.error('Error fetching bar chart data:', error); + const handleYearSubmit = async (year) => { + setIsLoadingDonut(true); + const response = await FetchService.getDonutContent(year); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); } - setIsLoading(false); - }; + const data = await response.json(); + setDonutValues(data); + setIsLoadingDonut(false); + } useEffect(() => { (async () => { // bar chart - loadInitialBarChartData(); + const formData = { + startDate: `${constants.initialBarChartFormData.startDate}`, + endDate: `${constants.initialBarChartFormData.endDate}`, + granularity: `${constants.initialBarChartFormData.granularity}` + }; + await handleFormSubmit(formData); // time series try { + setIsLoadingTimesseries(true); const response = await FetchService.getTimesSeries(); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const data = await response.json(); - setLineData(data); + setTimeseries(data); + setIsLoadingTimesseries(false); } catch (error) { console.error('Error fetching time series data:', error); } // donut - const data = await FetchService.getDonutContent(); - const formattedData = data.map((x) => ({ - id: x.region, - count: x.occurences - })); - setDonutValues(formattedData); + await handleYearSubmit(2020); + })(); }, []); @@ -81,21 +80,31 @@ export default function Page() {

L’évolution du prix de vente moyen du mètre carré

- + {isLoadingTimeseries? ( + + ): ( + + )}

Nombre des ventes par période

- {isLoading? ( + {isLoadingBar? ( ) : ( )}
-
+

Répartition des ventes par régions

- - + + {isLoadingDonut? ( + + ):( +
+ +
+ )}
diff --git a/pwa/config.js b/pwa/config.js index c1896d4..ae34429 100644 --- a/pwa/config.js +++ b/pwa/config.js @@ -8,9 +8,14 @@ const initialBarChartFormData = { granularity: 'month', }; +const initialDonutChartYear = { + selectedYear: 2020 +} + const constants = { initialBarChartFormData, config, + initialDonutChartYear }; export default constants; -- GitLab