diff --git a/pwa/app/components/donut.jsx b/pwa/app/components/donut.jsx deleted file mode 100644 index 34d6a9ed41902a87a0fbd7a126f8e22c522df604..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/donut.tsx b/pwa/app/components/donut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2c76d5a956b1144d9361b6adfdb1a528abe41336 --- /dev/null +++ b/pwa/app/components/donut.tsx @@ -0,0 +1,242 @@ +import React, { useState, useEffect, useRef } from "react"; +import * as d3 from "d3"; + +interface ChartData { + departement: string; + value: number; +} + +const Pie_Chart = () => { + const originalData: ChartData[] = [ + { departement: "01", value: 21050 }, + { departement: "02", value: 45954 }, + { departement: "03", value: 21584 }, + { departement: "04", value: 53486 }, + { departement: "05", value: 87965 }, + { departement: "06", value: 34864 }, + { departement: "07", value: 48453 }, + { departement: "08", value: 87652 }, + { departement: "09", value: 12346 }, + { departement: "10", value: 78654 }, + { departement: "11", value: 48453 }, + { departement: "12", value: 78654 }, + { departement: "13", value: 28203 }, + { departement: "14", value: 21050 }, + { departement: "15", value: 45211 }, + { departement: "16", value: 87945 }, + { departement: "17", value: 47853 }, + { departement: "18", value: 75213 }, + { departement: "19", value: 21050 }, + { departement: "20", value: 78542 }, + { departement: "21", value: 21050 }, + { departement: "22", value: 78432 }, + { departement: "23", value: 21050 }, + { departement: "24", value: 21050 }, + { departement: "25", value: 27420 }, + { departement: "26", value: 27420 }, + { departement: "27", value: 27420 }, + { departement: "28", value: 27420 }, + { departement: "29", value: 75423 }, + { departement: "30", value: 21050 }, + { departement: "31", value: 78543 }, + { departement: "32", value: 21050 }, + { departement: "33", value: 100 }, + { departement: "34", value: 21050 }, + { departement: "35", value: 21050 }, + { departement: "36", value: 500 }, + { departement: "37", value: 24128 }, + { departement: "38", value: 24128 }, + { departement: "39", value: 2400 }, + { departement: "40", value: 24128 }, + { departement: "41", value: 7856 }, + { departement: "42", value: 2014 }, + { departement: "43", value: 3500 }, + { departement: "44", value: 24128 }, + { departement: "45", value: 2035 }, + { departement: "46", value: 150 }, + { departement: "47", value: 980 }, + { departement: "48", value: 748 }, + { departement: "49", value: 38806 }, + { departement: "50", value: 38806 }, + { departement: "51", value: 78543 }, + { departement: "52", value: 12999 }, + { departement: "53", value: 7865 }, + { departement: "54", value: 12999 }, + { departement: "55", value: 7854 }, + { departement: "56", value: 3698 }, + { departement: "57", value: 7412 }, + { departement: "58", value: 1236 }, + { departement: "59", value: 4789 }, + { departement: "60", value: 8963 }, + { departement: "61", value: 6321 }, + { departement: "62", value: 2145 }, + { departement: "63", value: 3256 }, + { departement: "64", value: 6589 }, + { departement: "65", value: 8965 }, + { departement: "66", value: 4587 }, + { departement: "67", value: 5697 }, + { departement: "68", value: 5413 }, + { departement: "69", value: 9713 }, + { departement: "70", value: 5418 }, + { departement: "71", value: 12999 }, + { departement: "72", value: 8642 }, + { departement: "73", value: 12999 }, + { departement: "74", value: 7319 }, + { departement: "75", value: 8246 }, + { departement: "76", value: 7853 }, + { departement: "77", value: 4687 }, + { departement: "78", value: 2480 }, + { departement: "79", value: 4283 }, + { departement: "80", value: 7352 }, + { departement: "81", value: 2052 }, + { departement: "82", value: 4052 }, + { departement: "83", value: 3058 }, + { departement: "84", value: 5602 }, + { departement: "85", value: 4206 }, + { departement: "86", value: 7069 }, + { departement: "87", value: 7605 }, + { departement: "88", value: 100 }, + { departement: "89", value: 200 }, + { departement: "90", value: 300 }, + { departement: "91", value: 400 }, + { departement: "92", value: 500 }, + { departement: "93", value: 600 }, + { departement: "94", value: 700 }, + { departement: "95", value: 800 }, + { departement: "971", value: 900 }, + { departement: "972", value: 1000 }, + { departement: "973", value: 1000 }, + { departement: "974", value: 1200 }, + { departement: "975", value: 1300 }, + ]; + + const chartRef = useRef(null); + const [numResults, setNumResults] = useState(10); + + useEffect(() => { + const svg = d3.select(chartRef.current); + svg.selectAll("*").remove(); + + const margin = { top: 100, right: 10, bottom: 10, left: 10 }; + const width = 400 - margin.left - margin.right; + const height = 400 - margin.top - margin.bottom; + const padding = 20; + const thicknessOnMouseOver = 10; + const radius = Math.min(width, height) / 2 - padding; + + const sortedData = [...originalData].sort((a, b) => b.value - a.value); + + const displayedData = sortedData.slice(0, numResults); + + const totalSales = d3.sum(sortedData, (d) => d.value); + + const otherData = { + departement: "Autres", + value: totalSales - d3.sum(displayedData, (d) => d.value), + }; + + const pie = d3.pie() + .value((d) => d.value); + + const arc = d3.arc() + .outerRadius(radius) + .innerRadius(radius / 2); + + const colorScale = d3.scaleOrdinal(d3.schemeCategory10); + const g = svg.append("g") + .attr("transform", `translate(${width / 2},${height / 2})`); + + const arcs = g.selectAll("arc") + .data(pie([...displayedData, otherData])) + .enter().append("g") + .attr("class", "arc"); + + arcs.append("path") + .attr("d", arc) + .attr("fill", (d) => (d.data.departement === "Autres" ? "gray" : colorScale(d.data.departement))); + + + const outerArc = d3.arc() + .innerRadius(radius * 0.9) + .outerRadius(radius * 0.9); + + arcs.append("polyline") + .attr("stroke", "black") + .attr("stroke-width", 1) + .attr("fill", "none") + .attr("points", (d) => { + const posA = arc.centroid(d); + const posB = outerArc.centroid(d); + const posC = outerArc.centroid(d); + const midangle = d.startAngle + (d.endAngle - d.startAngle) / 2; + posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); + return [posA, posB, posC]; + }); + + arcs.append("text") + .attr("transform", (d) => `translate(${outerArc.centroid(d)})`) + .attr("dy", ".35em") + .style("text-anchor", "middle") + .text((d) => { + const percentage = (d.data.value / totalSales) * 100; + return `${d.data.departement}: ${percentage.toFixed(2)}%`; + }); + svg.on("mouseover", function() { + const fillValue = this.getAttribute("fill"); + if (fillValue !== null) { + d3.select(this) + .transition() + .duration(200) + .attr("stroke", fillValue) + .attr("stroke-width", thicknessOnMouseOver); + this.parentNode.appendChild(this); + } + }) + .on("mouseout", function() { + d3.select(this) + .transition() + .attr("stroke", "none") + .attr("stroke-width", 0); + }); + const legend = svg.selectAll(".legend") + .data(pie(displayedData)) + .enter().append("g") + .attr("class", "legend") + .attr("transform", (d, i) => `translate(0,${i * 20})`); + + legend.append("rect") + .attr("x", width - 18) + .attr("width", 18) + .attr("height", 18) + .attr("fill", (d, i) => colorScale(i)); + + legend.append("text") + .attr("x", width - 24) + .attr("y", 9) + .attr("dy", ".35em") + .style("text-anchor", "end") + .text((d) => d.data.departement); + + }, [numResults]); + + return ( +
+ +
+ +
+ +
+ ); +}; + + +export default Pie_Chart; diff --git a/pwa/app/components/line_chart.tsx b/pwa/app/components/line_chart.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f52e8e9a0c0abde72bd76ab643772bb1a2d1e5be --- /dev/null +++ b/pwa/app/components/line_chart.tsx @@ -0,0 +1,65 @@ +"use strict"; +import React, { useRef, useEffect } from "react"; +import * as d3 from "d3"; + +interface DataPoint { + year: number; + value: number; +} + +function Line_Chart() { + const chartRef = useRef(null); + + const data: DataPoint[] = [ + { year: 2018, value: 21050 }, + { year: 2019, value: 28203 }, + { year: 2020, value: 27420 }, + { year: 2021, value: 24128 }, + { year: 2022, value: 38806 }, + { year: 2023, value: 12999 }, + ]; + + useEffect(() => { + const width = 640; + const height = 400; + const marginTop = 20; + const marginRight = 20; + const marginBottom = 30; + const marginLeft = 40; + + const x = d3.scaleTime() + .domain([new Date("2018-01-01"), new Date("2023-01-01")]) + .range([marginLeft, width - marginRight]); + + const y = d3.scaleLinear() + .domain([0, d3.max(data, d => d.value)]) + .range([height - marginBottom, marginTop]); + + const svg = d3.select(chartRef.current) + .attr("width", width) + .attr("height", height); + + svg.append("g") + .attr("transform", `translate(0,${height - marginBottom})`) + .call(d3.axisBottom(x).ticks(d3.timeYear.every(1)).tickFormat(d3.timeFormat("%Y"))); + + svg.append("g") + .attr("transform", `translate(${marginLeft},0)`) + .call(d3.axisLeft(y)); + + const line = d3.line() + .x(d => x(new Date(`${d.year}-01-01`))) + .y(d => y(d.value)); + + svg.append("path") + .datum(data) + .attr("fill", "none") + .attr("stroke", "steelblue") + .attr("stroke-width", 2) + .attr("d", line); + }, [data]); + + return ; +} + +export default Line_Chart; diff --git a/pwa/app/page.jsx b/pwa/app/page.jsx index bd347f89d60712c18fda4b589ea229444eb9ee03..34ab659ee0ed7ca163a9a4c819aee88afe5879a9 100644 --- a/pwa/app/page.jsx +++ b/pwa/app/page.jsx @@ -4,10 +4,17 @@ import React, {useEffect, useState} from "react"; import config from '../config.js'; import FetchService from "./help/fetch-service"; -import Donut from "./components/donut"; +import Donut from "./components/donut.tsx"; import DateInput from "./components/date-input"; import DataForm from "./components/bar-chart/form"; import BarChart from "./components/bar-chart/chart"; +import Line_Chart from "./components/line_chart.tsx"; + +export default function Page() { + const [donutValues, setDonutValues] = useState([]); + const [chartData, setChartData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const Loader = () =>
Loading...
; export default function Page() { const [donutValues, setDonutValues] = useState([]); @@ -24,22 +31,23 @@ export default function Page() { setIsLoading(false); }; - useEffect(() => { - (async () => { - const data = await FetchService.getDonutContent(); - const formattedData = data.map((x) => ({ - id: x.region, - count: x.occurences - })); - setDonutValues(formattedData); - })(); - }, []); + useEffect(() => { + (async () => { + const data = await FetchService.getDonutContent(); + const formattedData = data.map((x) => ({ + id: x.region, + count: x.occurences + })); + setDonutValues(formattedData); + })(); + }, []); return ( <>

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

+

Nombre des ventes par période

diff --git a/pwa/package-lock.json b/pwa/package-lock.json index 70344fb629df5d079dc49ae3ce14befb8a97277f..cc72fc5006faabf14b6d35b2a1a19fc91a9c758a 100644 --- a/pwa/package-lock.json +++ b/pwa/package-lock.json @@ -15,6 +15,14 @@ "@tailwindcss/forms": "^0.5.7", "@types/d3": "^7.4.3", "d3": "^7.8.5", + "next": "14.0.4", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "20.10.6", + "@types/react": "18.2.46", + "typescript": "5.3.3", "formik": "^2.4.5", "isomorphic-unfetch": "^4.0.2", "next": "^14.0.3", @@ -6992,6 +7000,42 @@ "strip-bom": "^3.0.0" } }, + "node_modules/@types/node": { + "version": "20.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", + "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.46", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.46.tgz", + "integrity": "sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -7033,6 +7077,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", "node_modules/typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", @@ -7965,6 +8019,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/pwa/package.json b/pwa/package.json index f35177cd4edfb6c6ad558e81ec488f0b98d966c8..8e5e0d4abfd13df29af7e4dfb9f82578154d410a 100644 --- a/pwa/package.json +++ b/pwa/package.json @@ -41,5 +41,10 @@ "next": "14.0.4", "react": "^18", "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "20.10.6", + "@types/react": "18.2.46", + "typescript": "5.3.3" } } diff --git a/pwa/tsconfig.json b/pwa/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..14bd9ea9c48ae458b26df8301a21b5371c47d62b --- /dev/null +++ b/pwa/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +}