diff --git a/.gitignore b/.gitignore index b2f649cfdb182392e35be5e4ec06044e0d9581df..9fe32dd506a77eba09c592b7902a71e62c8b83a7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ !/helm/api-platform/charts/.gitignore .idea/* + +pwa/coverage/ + diff --git a/pwa/app/components/donut-chart/donut.jsx b/pwa/app/components/donut-chart/donut.jsx index 6afc7c70ef27d9069dbcf74908224fb1e19994d4..b006fb04039b6de7d3fbe950398b70e13fd58caf 100644 --- a/pwa/app/components/donut-chart/donut.jsx +++ b/pwa/app/components/donut-chart/donut.jsx @@ -1,127 +1,91 @@ -"use client"; - -import React, {useEffect, useRef} from "react"; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSpinner } from "@fortawesome/free-solid-svg-icons"; +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 {[{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; +export default function DonutChart({ data }) { + const ref = useRef(); + const svgWidth = 600; + const svgHeight = 600; + const width = 400; + const height = 400; + const radius = Math.min(width, height) / 2; + const innerRadius = radius * 0.6; + const outerRadius = radius * 0.9; + const hoverOuterRadius = outerRadius + 20; - useEffect(() => { - if (data === undefined || data.length === 0) { - return; - } + useEffect(() => { + if (!data || data.length === 0) { + return; + } - const rawPieData = d3.pie().value((x) => x.occurrences)(data); + const totalOccurrences = data.reduce((acc, item) => acc + item.occurrences, 0); + const minPercentage = 1; + let otherData = { region: "Autres", occurrences: 0, details: [] }; - let pieData = []; - const aggregate = { - data: { - region: "aggregate", - occurrences: 0 - }, - index: rawPieData.length, - value: 0, - padAngle: 0, - startAngle: 0, - endAngle: Math.PI * 2 - }; + let newData = data.filter(d => { + let percentage = (d.occurrences / totalOccurrences) * 100; + if (percentage < minPercentage) { + otherData.occurrences += d.occurrences; + otherData.details.push(`${d.region} (${percentage.toFixed(2)}%)`); + return false; + } + return true; + }); - 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); + if (otherData.occurrences > 0) { + newData.push(otherData); + } - const arc = d3 - .arc() - .innerRadius(radius * 0.6) - .outerRadius(radius) + const pieData = d3.pie().value(d => d.occurrences)(newData); + const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius); + const hoverArc = d3.arc().innerRadius(innerRadius).outerRadius(hoverOuterRadius); - const svg = d3.select(ref.current) - .attr("width", width) - .attr("height", height) - .append("g") - .attr("transform", `translate(${width / 2}, ${height / 2})`); + const svg = d3.select(ref.current) + .attr("width", svgWidth) + .attr("height", svgHeight) + .append("g") + .attr("transform", `translate(${svgWidth / 2}, ${svgHeight / 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.selectAll("path") + .data(pieData) + .enter() + .append("path") + .attr("d", arc) + .attr("fill", (d, i) => d3.schemeCategory10[i % 10]) + .on("mouseover", function(event, d) { + d3.select(this).transition().attr("d", hoverArc); - 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); + if (d.data.region === "Autres") { + svg.append("text") + .attr("class", "details-text") + .attr("x", 0) + .attr("y", 0) + .attr("text-anchor", "middle") + .selectAll("tspan") + .data(otherData.details) + .enter() + .append("tspan") + .attr("x", 0) + .attr("dy", "1.2em") + .text(detail => detail); + } else { + const percentage = ((d.data.occurrences / totalOccurrences) * 100).toFixed(2); + const text = `${d.data.region}: ${percentage}%`; - svg.select(".tooltip").remove(); - }); + svg.append("text") + .attr("class", "tooltip") + .attr("x", 0) + .attr("y", 20) + .attr("text-anchor", "middle") + .text(text); + } + }) + .on("mouseout", function() { + d3.select(this).transition().attr("d", arc); + svg.selectAll(".details-text").remove(); + svg.selectAll(".tooltip").remove(); + }); }, [data]); - return ( -
- {/*isLoading && ( -
- -
- )*/} - -
- ); + return ; } diff --git a/pwa/app/page.jsx b/pwa/app/page.jsx index e5838a94ed1d76add7c8c9c66e0514844066628e..6931f00f3bff798b6189da01841826e2bd3b4bda 100644 --- a/pwa/app/page.jsx +++ b/pwa/app/page.jsx @@ -102,7 +102,7 @@ export default function Page() { )} -
+

Répartition des ventes par régions

{(isLoading || isLoadingDonut)? ( diff --git a/pwa/app/style/style.css b/pwa/app/style/style.css index 2e1b648bd0d72c7bd9ddc0274311bc072a6fda53..ee81acfd6c926006c4aec7b5ec96afb903b014fa 100644 --- a/pwa/app/style/style.css +++ b/pwa/app/style/style.css @@ -18,7 +18,10 @@ .chart-container { margin: 20px; } - +.donut-container { + margin: 10px; + width: 90%; +} .form-container { display: flex; flex-direction: row;