diff --git a/api/src/Service/SaleService.php b/api/src/Service/SaleService.php
index be0f1201a017e7275a811b22645d83b3fbcf548c..b37e15bbee3c102bd7b72c226afba68b5c886e20 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 dc28209067fcb81f257116c46c9b174a47368b6a..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..6afc7c70ef27d9069dbcf74908224fb1e19994d4
--- /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 0000000000000000000000000000000000000000..8749a047f89dc09a2cf86c11e39ffa127ddf3b02
--- /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 6fb65eb05cd2963ca515bb18e30cb5aa9d56af50..0000000000000000000000000000000000000000
--- 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 8cd5e098ab3dac8c9139d2b02c0841b8a0d0d8b7..c08b19a286b22269488ae8c8820b6bb3d0151565 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 cd3df02ede16cec66629ce3ff2f0db12cc25f52e..108710c718d026421b9f4d18d3e35dee46a10665 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 e9b56d66f7ee7fc5d44c5baeed85a2497c66afd0..31d910f6ac173c1acd666fea12a53b343cdab5b9 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 1de97f99d28b04baefadcdf7b092c00a555820f6..0d7157b52895c4c1d8f1066a435a2a0a45e9bb14 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 c1896d45fe50ff1e37f948829297659b96930b57..ae34429c527c09554cc438d93ed27f21ceb45015 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;