diff --git a/backend/configs/config.json b/backend/configs/config.json index f9349960..d3d9b694 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -41,9 +41,12 @@ "resource": "inventaire_id", "layerType": "symbol", "columns": { + "geom": "gps.merge().centroid()", + "id": "_id", "for": "for", "cod": "cod", - "geom": "gps.merge().centroid()", + "taille_placette": "20^2*pi()/10000", + "projet": "trim('A Kob Ale')", "biomass_volume": "ind.sum(0.0673 * (dens_bois.value * (dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10)^2 * haut)^0.976 if etat = 1) / (20^2 * pi() / 10000) / 1000", "tree_density": "count(1 if ind.etat = 1) / (20^2 * pi() / 10000)", "tree_pop": "count(1 if ind.etat = 1)", @@ -52,14 +55,14 @@ "epf_tree_density": "count(1 if ind.etat = 1) / (20^2 * pi() / 10000) / 100", "epf_necromass_pied": "ind.sum(0.0673 * (dens_bois_mort.dens_bois * (dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10)^2 * haut)^0.976 if etat = 2)", "epf_necromass_sol": "ind.sum(3.1415926535 / 3 * ((dhp_b / 2)^2 + ((dhp_e / 2)^2 + dhp_b / 2 + dhp_e / 2) * longueur * dens_bois_mort.dens_bois) if etat = 3)", - "epf_deadWood": "((ind.sum(0.0673 * (dens_bois_mort.dens_bois * (dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10)^2 * haut)^0.976 if etat = 2) + ind.sum(3.1415926535 / 3 * ((dhp_b / 2)^2 + ((dhp_e / 2)^2 + dhp_b / 2 + dhp_e / 2) * longueur * dens_bois_mort.dens_bois) if etat = 3)) / ind.sum(0.0673 * (dens_bois.value * (dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10)^2 * haut)^0.976 if etat = 1)) * 10", + "epf_deadWood": "((ind.sum(0.0673 * (dens_bois_mort.dens_bois * (dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10)^2 * haut)^0.976 if etat = 2) + ind.sum(pi() / 3 * ((dhp_b / 2)^2 + ((dhp_e / 2)^2 + dhp_b / 2 + dhp_e / 2) * longueur * dens_bois_mort.dens_bois) if etat = 3)) / ind.sum(0.0673 * (dens_bois.value * (dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10)^2 * haut)^0.976 if etat = 1)) * 10", "epf_tree_diversity": "shannon(ind.ess_arb) / 5 * 10", "epf_spatial_distribution": "ind.categorical_gini(zon) * 10", "epf_diameter_distribution": "ind.gini(dhp1 + dhp2 + dhp3 + dhp4 + dhp5 + dhp6 + dhp7 + dhp8 + dhp9 + dhp10) * 10", "epf_vertical_distribution": "gini(ind.haut) * 10", "epf_dominant_height": "ind.avg(haut if haut > percentile(haut, 80)) * 10 / 40", "epf_microhabitats": "ind.list(dmh).flatten().list_unique() * 10 / 16", - "soil_structure": "sum(ep1*int(not1) + ep2*int(not2) + ep3*int(not3) + ep4*int(not4) + ep5*int(not5)) / sum(ep1 + ep2 + ep3 + ep4 + ep5) * 2", + "soil_structure": "sum(ep1*int(not1) + ep2*int(not2) + ep3*int(not3) + ep4*int(not4) + ep5*int(not5)) / (sum(ep1 + ep2 + ep3 + ep4 + ep5) if sum(ep1 + ep2 + ep3 + ep4 + ep5) > 0 else 1) * 2", "soil_composition": "count(1)", "ero_rainfall_and_wind": "meteo.concat_ws('-', pluv, vent)", "ero_couv_slope_and_cover": "inventaire_external.concat_ws('-', pent, couv)", @@ -91,6 +94,7 @@ "groupby": ["admi2"], "columns": { "geom": "centroid(merge(point(socio_eco_gps.long, socio_eco_gps.latit)))", + "household_nb": "count(_id)", "population": "sum(hab)", "energyType": "count(ener)", "boughtWoodEnergy": "count(1)", @@ -101,7 +105,7 @@ "otherEnergy": "count(1)", "woodEnergyConsumption": "count(1)", "woodEnergyNeeds": "count(1)", - "woodCollectionTime:": "count(1)", + "woodCollectionTime": "count(1)", "timberNeeds": "count(1)", "foodDiversity": "count(1)", "autoConsumtionNeeds": "count(1)", diff --git a/backend/requirements.txt b/backend/requirements.txt index 966113ba..91848379 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ asgiref==3.8.1 -coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@0.3.0#subdirectory=coordo-py +coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@arnaudfnr/fix-null-values-in-formulas#subdirectory=coordo-py Django>=4.2.27 djangorestframework==3.16.0 djangorestframework_simplejwt==5.5.1 diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 7d921573..20128456 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -22,7 +22,7 @@ "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "coordo": "github:dataforgoodfr/Coordonnees#0.3.0", + "coordo": "github:dataforgoodfr/Coordonnees#arnaudfnr/fix-null-values-in-formulas", "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -4120,7 +4120,7 @@ }, "node_modules/coordo": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/dataforgoodfr/Coordonnees.git#9231c239c3877f1175fdd373f69b60f4e869ec2b", + "resolved": "git+ssh://git@github.com/dataforgoodfr/Coordonnees.git#337be638e156e1dc536b35bb4741747fa9708566", "license": "ISC", "dependencies": { "maplibre-gl": "^5.16.0" diff --git a/webapp/package.json b/webapp/package.json index 53b704d0..ac683f11 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -39,7 +39,7 @@ "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "coordo": "github:dataforgoodfr/Coordonnees#0.3.0", + "coordo": "github:dataforgoodfr/Coordonnees#arnaudfnr/fix-null-values-in-formulas", "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", diff --git a/webapp/src/app/styles/all4trees.css b/webapp/src/app/styles/all4trees.css index 0116b857..82d5c054 100644 --- a/webapp/src/app/styles/all4trees.css +++ b/webapp/src/app/styles/all4trees.css @@ -36,11 +36,11 @@ /* ======================================================================== COULEURS AJUSTEES - POUR UNE MEILLEURE UI ======================================================================== */ - --custom-color-green-dark: #1a2420; + --custom-color-green-dark: #102017; --custom-color-green-main: #007a55; --custom-color-green-pastel: #d0fae5; --custom-color-grey-light: #e5e7eb; - --custom-color-blue-dark: #192637; + --custom-color-blue-dark: #192430; --custom-color-blue-light: #52a4ff; --custom-color-socio-eco: #1447e6; /* Same as in socio-eco-icon */ diff --git a/webapp/src/app/styles/globals.css b/webapp/src/app/styles/globals.css index f936bfa7..53b1f34f 100644 --- a/webapp/src/app/styles/globals.css +++ b/webapp/src/app/styles/globals.css @@ -44,7 +44,6 @@ overflow: hidden !important; color: var(--popover-foreground) !important; background: var(--popover) !important; - border: 1px solid var(--border) !important; border-radius: var(--radius) !important; box-shadow: 0 40px 120px rgba(0, 0, 0, 0.25) !important; } diff --git a/webapp/src/app/styles/index.css b/webapp/src/app/styles/index.css index 8943ea69..d0b35e4e 100644 --- a/webapp/src/app/styles/index.css +++ b/webapp/src/app/styles/index.css @@ -55,11 +55,12 @@ --input: #ffffff; --ring: var(--a4t-color-vert-kelly); - --chart-1: var(--a4t-color-bleu); - --chart-2: var(--a4t-color-vert-de-gris); - --chart-3: var(--a4t-color-vert-kelly); - --chart-4: var(--a4t-color-citrouille); - --chart-5: var(--a4t-color-jaune-vert); + --chart-1: var(--a4t-color-vert-kelly); + --chart-2: var(--a4t-color-citrouille); + --chart-3: var(--a4t-color-bleu); + --chart-4: #895bf5; + --chart-5: #f04646; + --chart-6: var(--a4t-color-onyx); --sidebar: var(--a4t-color-alabaster); --sidebar-foreground: var(--a4t-color-alabaster); @@ -75,7 +76,7 @@ --background: var(--custom-color-green-dark); --foreground: var(--a4t-color-alabaster); - --card: var(--a4t-color-nuit); + --card: var(--custom-color-blue-dark); --card-foreground: var(--a4t-color-alabaster); --popover: var(--a4t-color-nuit); diff --git a/webapp/src/features/charts/biodiversity/chart-forest-potential.tsx b/webapp/src/features/charts/biodiversity/chart-forest-potential.tsx index 5929075d..ae23a3eb 100644 --- a/webapp/src/features/charts/biodiversity/chart-forest-potential.tsx +++ b/webapp/src/features/charts/biodiversity/chart-forest-potential.tsx @@ -1,7 +1,6 @@ -import type { FC } from "react"; - import { useTranslation } from "@i18n"; +import type { ChartComponentType } from "../components/chart-component"; import { ChartRadarWithBenefAndControl } from "../components/radar-benef-control"; type Data = { @@ -20,10 +19,9 @@ type ChartForestPotentialProps = { temoin?: Data; }; -export const ChartForestPotential: FC = ({ - benef, - temoin, -}) => { +export const ChartForestPotential: ChartComponentType< + ChartForestPotentialProps +> = ({ benef, temoin }) => { const { t } = useTranslation("translations"); const chartData: Array<{ @@ -99,3 +97,5 @@ export const ChartForestPotential: FC = ({ /> ); }; + +ChartForestPotential.isChartComponent = true; diff --git a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx index 0bee77fd..a63829bb 100644 --- a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx +++ b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx @@ -1,10 +1,10 @@ import type { LayerMetadata } from "node_modules/coordo/coordo-ts/src/types"; -import type { FC } from "react"; import { useTranslation } from "@shared/i18n"; -import { findCategoricalLabel } from "@shared/lib/utils"; +import { findCategoricalLabel, precise } from "@shared/lib/utils"; import type { ChartConfig } from "@shared/ui/chart"; +import type { ChartComponentType } from "../components/chart-component"; import { PieChartCategorical } from "../components/pie-chart-categorical"; type PieChartProps = { @@ -12,42 +12,62 @@ type PieChartProps = { metadata: LayerMetadata; }; -export const ChartRelativeAbundance: FC = ({ +export const ChartRelativeAbundance: ChartComponentType = ({ data, metadata, }) => { const { t } = useTranslation("translations"); - const chartData = data.map((element, index) => ({ - fill: `var(--chart-${(index % 4) + 1})`, - name: element[0], - value: element[1], - })); + const smallCategoriesSum = Number( + precise( + data + .filter(([_, value]) => value < 5) + .reduce((acc, [_, value]) => acc + value, 0), + ), + ); + const chartData = data + .filter(([name, value]) => name !== "0" && (data.length < 6 || value >= 5)) + .map((element, index) => ({ + fill: `var(--chart-${(index % 5) + 1})`, + name: element[0], + value: element[1], + })); let chartConfig: ChartConfig = {}; - data.forEach((element) => { + chartData.forEach((element) => { chartConfig = { ...chartConfig, - [element[0]]: { + [element.name]: { label: - findCategoricalLabel(metadata, "ess_arb", element[0]) || - t( - "indicators.biodiversity.sections.treeDiversity.relativeAbundance.other", - ), + findCategoricalLabel(metadata, "ess_arb", element.name) || + element.name, + }, + other: { + label: t( + "indicators.biodiversity.sections.treeDiversity.relativeAbundance.other", + ), }, }; }); + if (data.length >= 6 && smallCategoriesSum > 0) { + chartData.push({ + fill: `var(--chart-6)`, + name: "other", + value: smallCategoriesSum, + }); + } + return ( ); }; + +ChartRelativeAbundance.isChartComponent = true; diff --git a/webapp/src/features/charts/components/bar-chart-benef-control.tsx b/webapp/src/features/charts/components/bar-chart-benef-control.tsx index 7ee074ed..12f2533f 100644 --- a/webapp/src/features/charts/components/bar-chart-benef-control.tsx +++ b/webapp/src/features/charts/components/bar-chart-benef-control.tsx @@ -1,8 +1,6 @@ -import type { FC } from "react"; -import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { useTranslation } from "@shared/i18n"; -import { Card, CardContent, CardHeader, CardTitle } from "@shared/ui/card"; import { type ChartConfig, ChartContainer, @@ -10,18 +8,23 @@ import { ChartTooltipContent, } from "@shared/ui/chart"; +import type { ChartComponentType } from "./chart-component"; +import { ChartComponent } from "./chart-component"; + type BarChartProps = { title: string; chartData: Array<{ indicator: string; benef: unknown; temoin?: unknown }>; legendLabel: string; withTemoin?: boolean; + layout?: { chartHeight: number; chartXAxisHeight: number }; }; -export const BarCharWithBenefAndControl: FC = ({ +export const BarCharWithBenefAndControl: ChartComponentType = ({ title, chartData, legendLabel, - withTemoin, + withTemoin = false, + layout = { chartHeight: 80, chartXAxisHeight: 90 }, }) => { const { t } = useTranslation("translations"); const chartConfig = { @@ -30,53 +33,82 @@ export const BarCharWithBenefAndControl: FC = ({ label: `(${t("indicators.common.beneficiary")}) ` + legendLabel, }, temoin: { - color: "var(--chart-2)", + color: "var(--chart-3)", label: `(${t("indicators.common.control")}) ` + legendLabel, }, } satisfies ChartConfig; return ( - - - {title} - - - + + - - - value.slice(0, 4)} - tickLine={false} - tickMargin={10} - /> - } - cursor={false} - /> + + + `${value}%`} + tickLine={false} + tickMargin={-2} + width={24} + /> + } + cursor={false} + /> + + + {withTemoin && ( - - {withTemoin && ( - - )} - - - - + )} + + + ); }; + +BarCharWithBenefAndControl.isChartComponent = true; + +const renderXAxisTick = ({ + x, + y, + payload, +}: { + x: number; + y: number; + payload: { value: string | number }; +}) => ( + + {payload.value} + +); diff --git a/webapp/src/features/charts/components/chart-component.tsx b/webapp/src/features/charts/components/chart-component.tsx new file mode 100644 index 00000000..8ba82c92 --- /dev/null +++ b/webapp/src/features/charts/components/chart-component.tsx @@ -0,0 +1,46 @@ +import type { PropsWithChildren } from "react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@ui/card"; + +type ChartComponentProps = PropsWithChildren<{ + title?: string; + description?: string; + className?: string; +}>; + +export type MarkedComponent

= React.ComponentType

& { + isChartComponent?: true; +}; + +export type ChartComponentType

= React.FC

& { + isChartComponent?: true; +}; + +export const ChartComponent: ChartComponentType = ({ + title, + description, + className, + children, +}) => { + return ( + + {(title || description) && ( + + {title ? {title} : null} + {description ? ( + {description} + ) : null} + + )} + {children} + + ); +}; + +ChartComponent.isChartComponent = true; diff --git a/webapp/src/features/charts/components/pie-chart-categorical.tsx b/webapp/src/features/charts/components/pie-chart-categorical.tsx index 1d3c8f72..b353c06e 100644 --- a/webapp/src/features/charts/components/pie-chart-categorical.tsx +++ b/webapp/src/features/charts/components/pie-chart-categorical.tsx @@ -1,14 +1,11 @@ -import type { FC } from "react"; +import type { JSX } from "react"; import { Pie, PieChart, type PieLabel } from "recharts"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@ui/card"; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@ui/chart"; +import { ChartContainer, ChartTooltip } from "@ui/chart"; + +import { lineBreakLabel } from "../utils"; +import type { ChartComponentType } from "./chart-component"; +import { ChartComponent } from "./chart-component"; export const description = "A pie chart with a label"; @@ -18,37 +15,117 @@ export type PieChartCategoricalProps = { chartConfig: any; description?: string; withLabel?: PieLabel; + unit?: string; + innerRadius?: number; + outerRadius?: number; + paddingAngle?: number; + minAngle?: number; + labelLine?: boolean; +}; + +export const PieChartCategorical: ChartComponentType< + PieChartCategoricalProps +> = ({ chartData, chartConfig, title, description, withLabel, unit }) => { + const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + const name = chartConfig[payload[0].name]?.label || payload[0].name; + const tooltip = `${name}: ${payload[0].value}${unit}`; + return ( +

+

{tooltip}

+
+ ); + } + return null; + }; + return ( + + + + } /> + renderLabel({ ...props, chartConfig }) + : withLabel + } + labelLine={false} + nameKey="name" + outerRadius={140} + /> + + + + ); }; -export const PieChartCategorical: FC = ({ - chartData, +PieChartCategorical.isChartComponent = true; + +export const renderLabel = ({ + payload, + cx, + cy, + midAngle, + outerRadius, chartConfig, - title, - description, - withLabel, -}) => { + linebreak = 13, +}: { + payload: any; + cx: number; + cy: number; + midAngle: number; + outerRadius: number; + chartConfig: any; + linebreak: number; +}): JSX.Element => { + const label = chartConfig[payload.name]?.label || payload.name; + const RADIAN = Math.PI / 180; + const angle = -midAngle * RADIAN; + const lineStartX = cx + outerRadius * Math.cos(angle); + const lineStartY = cy + outerRadius * Math.sin(angle); + const lineMidX = cx + (outerRadius + 10) * Math.cos(angle); + const lineMidY = cy + (outerRadius + 10) * Math.sin(angle); + const labelX = lineMidX + (lineMidX > cx ? 14 : -14); + const labelY = lineMidY; + const textAnchor = lineMidX > cx ? "start" : "end"; + const lines = lineBreakLabel(label, linebreak); + return ( - - - {title} - {description} - - - - - } /> - - - - - + + + + {lines.map((line, index) => ( + + key={index} + x={labelX} + > + {line} + + ))} + + ); }; diff --git a/webapp/src/features/charts/components/radar-benef-control.tsx b/webapp/src/features/charts/components/radar-benef-control.tsx index c1ac5077..f97287e0 100644 --- a/webapp/src/features/charts/components/radar-benef-control.tsx +++ b/webapp/src/features/charts/components/radar-benef-control.tsx @@ -1,4 +1,3 @@ -import type { FC } from "react"; import { PolarAngleAxis, PolarGrid, @@ -9,7 +8,6 @@ import { import { useTranslation } from "@shared/i18n"; -import { Card, CardContent, CardDescription, CardHeader } from "@ui/card"; import { type ChartConfig, ChartContainer, @@ -20,7 +18,9 @@ import { } from "@ui/chart"; import { RADAR_CONFIG } from "../constants"; -import { renderPolarAngleTick } from "./render-polar-angle-tick"; +import { lineBreakLabel } from "../utils"; +import type { ChartComponentType } from "./chart-component"; +import { ChartComponent } from "./chart-component"; type ChartRadarWithBenefAndControlProps = { title: string; @@ -28,7 +28,7 @@ type ChartRadarWithBenefAndControlProps = { chartData: Array<{ indicator: string; benef: unknown; temoin?: unknown }>; }; -export const ChartRadarWithBenefAndControl: FC< +export const ChartRadarWithBenefAndControl: ChartComponentType< ChartRadarWithBenefAndControlProps > = ({ chartData, title, withTemoin }) => { const { t } = useTranslation("translations"); @@ -39,59 +39,84 @@ export const ChartRadarWithBenefAndControl: FC< label: t("indicators.common.beneficiary"), }, temoin: { - color: "var(--chart-4)", + color: "var(--chart-3)", label: t("indicators.common.control"), }, }; return ( - - - {title} - - - + + - - } - cursor={true} - /> - - - - - {withTemoin && ( + } + cursor={true} + /> + + + + + {withTemoin && ( + <> - )} - } - /> - - - - + + } + /> + + )} + + + + ); +}; + +ChartRadarWithBenefAndControl.isChartComponent = true; + +const renderPolarAngleTick = ({ payload, x, y, textAnchor }: any) => { + const label = String(payload?.value ?? ""); + const lines = lineBreakLabel(label); + + return ( + + {lines.map((line, index) => ( + + key={index} + x={x} + > + {line} + + ))} + ); }; diff --git a/webapp/src/features/charts/components/render-polar-angle-tick.tsx b/webapp/src/features/charts/components/render-polar-angle-tick.tsx deleted file mode 100644 index d77f7f91..00000000 --- a/webapp/src/features/charts/components/render-polar-angle-tick.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { lineBreakLabel } from "../utils"; - -export const renderPolarAngleTick = ({ payload, x, y, textAnchor }: any) => { - const label = String(payload?.value ?? ""); - const lines = lineBreakLabel(label); - - return ( - - {lines.map((line, index) => ( - - key={index} - x={x} - > - {line} - - ))} - - ); -}; diff --git a/webapp/src/features/charts/socio-eco/chart-beneficial-practices.tsx b/webapp/src/features/charts/socio-eco/chart-beneficial-practices.tsx index fb9cb072..b87a04d9 100644 --- a/webapp/src/features/charts/socio-eco/chart-beneficial-practices.tsx +++ b/webapp/src/features/charts/socio-eco/chart-beneficial-practices.tsx @@ -1,8 +1,7 @@ -import type { FC } from "react"; - import { useTranslation } from "@shared/i18n"; import { BarCharWithBenefAndControl } from "../components/bar-chart-benef-control"; +import type { ChartComponentType } from "../components/chart-component"; type Data = { defense: number; @@ -16,10 +15,9 @@ type ChartBeneficialPracticesProps = { temoin?: Data; }; -export const ChartBeneficialPractices: FC = ({ - benef, - temoin, -}) => { +export const ChartBeneficialPractices: ChartComponentType< + ChartBeneficialPracticesProps +> = ({ benef, temoin }) => { const { t } = useTranslation("translations"); const chartData = [ @@ -56,6 +54,7 @@ export const ChartBeneficialPractices: FC = ({ return ( = ({ /> ); }; + +ChartBeneficialPractices.isChartComponent = true; diff --git a/webapp/src/features/charts/socio-eco/chart-energy-sources.tsx b/webapp/src/features/charts/socio-eco/chart-energy-sources.tsx index 007cc9b2..d0b3c6e8 100644 --- a/webapp/src/features/charts/socio-eco/chart-energy-sources.tsx +++ b/webapp/src/features/charts/socio-eco/chart-energy-sources.tsx @@ -1,8 +1,7 @@ -import type { FC } from "react"; - import { useTranslation } from "@shared/i18n"; import { BarCharWithBenefAndControl } from "../components/bar-chart-benef-control"; +import type { ChartComponentType } from "../components/chart-component"; type Data = { collectedWood: number; @@ -19,10 +18,9 @@ type ChartEnergySourcesProps = { temoin?: Data; }; -export const ChartEnergySources: FC = ({ - benef, - temoin, -}) => { +export const ChartEnergySources: ChartComponentType< + ChartEnergySourcesProps +> = ({ benef, temoin }) => { const { t } = useTranslation("translations"); const chartData = [ @@ -80,3 +78,5 @@ export const ChartEnergySources: FC = ({ /> ); }; + +ChartEnergySources.isChartComponent = true; diff --git a/webapp/src/features/charts/socio-eco/chart-food-diversity.tsx b/webapp/src/features/charts/socio-eco/chart-food-diversity.tsx index 0ed49661..a1a619bc 100644 --- a/webapp/src/features/charts/socio-eco/chart-food-diversity.tsx +++ b/webapp/src/features/charts/socio-eco/chart-food-diversity.tsx @@ -1,8 +1,7 @@ -import type { FC } from "react"; - import { useTranslation } from "@shared/i18n"; import { BarCharWithBenefAndControl } from "../components/bar-chart-benef-control"; +import type { ChartComponentType } from "../components/chart-component"; type Data = { cereals: number; @@ -20,10 +19,9 @@ type ChartFoodDiversityProps = { temoin?: Data; }; -export const ChartFoodDiversity: FC = ({ - benef, - temoin, -}) => { +export const ChartFoodDiversity: ChartComponentType< + ChartFoodDiversityProps +> = ({ benef, temoin }) => { const { t } = useTranslation("translations"); const chartData = [ @@ -78,3 +76,5 @@ export const ChartFoodDiversity: FC = ({ /> ); }; + +ChartFoodDiversity.isChartComponent = true; diff --git a/webapp/src/features/charts/socio-eco/chart-living-perception.tsx b/webapp/src/features/charts/socio-eco/chart-living-perception.tsx index 46003552..e8a0e981 100644 --- a/webapp/src/features/charts/socio-eco/chart-living-perception.tsx +++ b/webapp/src/features/charts/socio-eco/chart-living-perception.tsx @@ -1,10 +1,8 @@ -import type { FC } from "react"; - import { useTranslation } from "@shared/i18n"; import type { ChartConfig } from "@shared/ui/chart"; +import type { ChartComponentType } from "../components/chart-component"; import { PieChartCategorical } from "../components/pie-chart-categorical"; -import { lineBreakLabel } from "../utils"; type PieChartProps = { data: { @@ -16,11 +14,13 @@ type PieChartProps = { }; }; -export const ChartLivingPerception: FC = ({ data }) => { +export const ChartLivingPerception: ChartComponentType = ({ + data, +}) => { const { t } = useTranslation("translations"); const chartData = [ { - fill: "var(--chart-4)", + fill: "var(--chart-1)", name: "improvement", value: data.improvement, }, @@ -30,17 +30,17 @@ export const ChartLivingPerception: FC = ({ data }) => { value: data.stable, }, { - fill: "var(--chart-2)", + fill: "var(--chart-5)", name: "regression", value: data.regression, }, { - fill: "var(--chart-5)", + fill: "var(--chart-6)", name: "refuse", value: data.refuse, }, { - fill: "var(--chart-1)", + fill: "var(--chart-2)", name: "dontKnow", value: data.dontKnow, }, @@ -75,44 +75,10 @@ export const ChartLivingPerception: FC = ({ data }) => { chartConfig={chartConfig} chartData={chartData} title={t("indicators.socioEco.sections.economy.livingPerception.title")} - withLabel={/*renderLabel(chartConfig)*/ false} + unit="%" + withLabel /> ); }; -// TODO: Would be nice to display labels correctly but there is not enough space in chart card. -//@ts-expect-error -const _renderLabel = - (chartConfig: any) => - ({ - payload, - ...props - }: { - payload: { name: string; value: number }; - [key: string]: any; - }) => { - const name = payload.name as keyof typeof chartConfig; - const label = `${chartConfig[name]?.label || payload.name}: ${payload.value}%`; - const lines = lineBreakLabel(label); - - return ( - - {lines.map((line, index) => ( - - key={index} - x={props.x} - > - {line} - - ))} - - ); - }; +ChartLivingPerception.isChartComponent = true; diff --git a/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx b/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx index c564d544..9d9b7ed8 100644 --- a/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx +++ b/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx @@ -1,9 +1,11 @@ -import type { FC } from "react"; - import { useTranslation } from "@shared/i18n"; import type { ChartConfig } from "@shared/ui/chart"; -import { PieChartCategorical } from "../components/pie-chart-categorical"; +import type { ChartComponentType } from "../components/chart-component"; +import { + PieChartCategorical, + renderLabel, +} from "../components/pie-chart-categorical"; type PieChartProps = { data: { @@ -14,26 +16,28 @@ type PieChartProps = { }; }; -export const ChartTimberNeeds: FC = ({ data }) => { +export const ChartTimberNeeds: ChartComponentType = ({ + data, +}) => { const { t } = useTranslation("translations"); const chartData = [ { - fill: "var(--chart-4)", + fill: "var(--chart-1)", name: "easyToMeet", value: data.easyToMeet, }, { - fill: "var(--chart-3)", + fill: "var(--chart-2)", name: "moderateToMeet", value: data.moderateToMeet, }, { - fill: "var(--chart-2)", + fill: "var(--chart-5)", name: "difficultToMeet", value: data.difficultToMeet, }, { - fill: "var(--chart-1)", + fill: "var(--chart-6)", name: "dontKnow", value: data.dontKnow, }, @@ -59,7 +63,12 @@ export const ChartTimberNeeds: FC = ({ data }) => { chartConfig={chartConfig} chartData={chartData} title={t("indicators.socioEco.sections.wood.timberNeeds.title")} - withLabel={/*renderLabel(chartConfig)*/ false} + unit="%" + withLabel={(props) => + renderLabel({ ...props, chartConfig, linebreak: 18 }) + } /> ); }; + +ChartTimberNeeds.isChartComponent = true; diff --git a/webapp/src/features/charts/socio-eco/chart-wood-energy-needs.tsx b/webapp/src/features/charts/socio-eco/chart-wood-energy-needs.tsx index fe113105..bcaa2b64 100644 --- a/webapp/src/features/charts/socio-eco/chart-wood-energy-needs.tsx +++ b/webapp/src/features/charts/socio-eco/chart-wood-energy-needs.tsx @@ -1,9 +1,11 @@ -import type { FC } from "react"; - import { useTranslation } from "@shared/i18n"; import type { ChartConfig } from "@shared/ui/chart"; -import { PieChartCategorical } from "../components/pie-chart-categorical"; +import type { ChartComponentType } from "../components/chart-component"; +import { + PieChartCategorical, + renderLabel, +} from "../components/pie-chart-categorical"; type PieChartProps = { data: { @@ -14,26 +16,28 @@ type PieChartProps = { }; }; -export const ChartWoodEnergyNeeds: FC = ({ data }) => { +export const ChartWoodEnergyNeeds: ChartComponentType = ({ + data, +}) => { const { t } = useTranslation("translations"); const chartData = [ { - fill: "var(--chart-4)", + fill: "var(--chart-1)", name: "easyToMeet", value: data.easyToMeet, }, { - fill: "var(--chart-3)", + fill: "var(--chart-2)", name: "moderateToMeet", value: data.moderateToMeet, }, { - fill: "var(--chart-2)", + fill: "var(--chart-5)", name: "difficultToMeet", value: data.difficultToMeet, }, { - fill: "var(--chart-1)", + fill: "var(--chart-6)", name: "dontKnow", value: data.dontKnow, }, @@ -58,8 +62,13 @@ export const ChartWoodEnergyNeeds: FC = ({ data }) => { + renderLabel({ ...props, chartConfig, linebreak: 18 }) + } /> ); }; + +ChartWoodEnergyNeeds.isChartComponent = true; diff --git a/webapp/src/features/charts/soil/ui/chart-aquatic-erosion.tsx b/webapp/src/features/charts/soil/ui/chart-aquatic-erosion.tsx index 46926067..5d66fe18 100644 --- a/webapp/src/features/charts/soil/ui/chart-aquatic-erosion.tsx +++ b/webapp/src/features/charts/soil/ui/chart-aquatic-erosion.tsx @@ -1,4 +1,4 @@ -import type { FC } from "react"; +import type { ChartComponentType } from "@features/charts/components/chart-component"; import { useTranslation } from "@shared/i18n"; @@ -17,10 +17,9 @@ type ChartAquaticErosionProps = { temoin?: Data; }; -export const ChartAquaticErosion: FC = ({ - benef, - temoin, -}) => { +export const ChartAquaticErosion: ChartComponentType< + ChartAquaticErosionProps +> = ({ benef, temoin }) => { const { t } = useTranslation("translations"); const chartData: Array<{ @@ -63,3 +62,5 @@ export const ChartAquaticErosion: FC = ({ /> ); }; + +ChartAquaticErosion.isChartComponent = true; diff --git a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx b/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx index 98bf13e5..595a74a2 100644 --- a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx +++ b/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx @@ -1,15 +1,18 @@ import type { Data as PlotlyData } from "plotly.js"; -import type { FC } from "react"; import Plot from "react-plotly.js"; +import { + ChartComponent, + type ChartComponentType, +} from "@features/charts/components/chart-component"; + import { useTranslation } from "@shared/i18n"; -import { Card, CardContent, CardHeader, CardTitle } from "@shared/ui/card"; import { SUNBURST_LAYOUT } from "../config"; import { buildNodeColors, buildSunburstNodes } from "../lib/sunburst"; import type { PieChartProps, SunburstTrace } from "../types"; -export const ChartTaxonAbundance: FC = ({ +export const ChartTaxonAbundance: ChartComponentType = ({ data, metadata, dataType, @@ -20,7 +23,11 @@ export const ChartTaxonAbundance: FC = ({ let sunburstData: PlotlyData[] = []; const cardHeight = hasTaxonData ? "min-h-105" : "min-h-40"; if (hasTaxonData) { - const nodes = buildSunburstNodes(dataEntries, metadata, dataType); + // Remove taxon entries with "Aucun" value + const filteredDataEntries = dataEntries.filter( + ([key]) => key.trim() !== "0", + ); + const nodes = buildSunburstNodes(filteredDataEntries, metadata, dataType); const nodeColors = buildNodeColors(nodes); sunburstData = [ @@ -39,13 +46,14 @@ export const ChartTaxonAbundance: FC = ({ ] as unknown as PlotlyData[]; } return ( - - - {t("indicators.common.abundance")} - - + +
{hasTaxonData && ( = ({ )} {!hasTaxonData && (
{t("indicators.common.noData")}
)} - - +
+
); }; + +ChartTaxonAbundance.isChartComponent = true; diff --git a/webapp/src/features/charts/soil/ui/chart-wind-erosion.tsx b/webapp/src/features/charts/soil/ui/chart-wind-erosion.tsx index 8e92e32b..4b4f0092 100644 --- a/webapp/src/features/charts/soil/ui/chart-wind-erosion.tsx +++ b/webapp/src/features/charts/soil/ui/chart-wind-erosion.tsx @@ -1,4 +1,4 @@ -import type { FC } from "react"; +import type { ChartComponentType } from "@features/charts/components/chart-component"; import { useTranslation } from "@shared/i18n"; @@ -15,7 +15,7 @@ type ChartWindErosionProps = { temoin?: Data; }; -export const ChartWindErosion: FC = ({ +export const ChartWindErosion: ChartComponentType = ({ benef, temoin, }) => { @@ -51,3 +51,5 @@ export const ChartWindErosion: FC = ({ /> ); }; + +ChartWindErosion.isChartComponent = true; diff --git a/webapp/src/features/charts/utils.ts b/webapp/src/features/charts/utils.ts index f0d78ec6..d9acb6cf 100644 --- a/webapp/src/features/charts/utils.ts +++ b/webapp/src/features/charts/utils.ts @@ -1,3 +1,28 @@ +import React, { type ReactNode } from "react"; + +import type { MarkedComponent } from "./components/chart-component"; + +// Check if node or one of its children has been marked as Chart Component +export const isChartElement = (node: ReactNode): boolean => { + if (!React.isValidElement(node)) { + return false; + } + + const element = node as React.ReactElement<{ children?: ReactNode }>; + const type = element.type as MarkedComponent; + + if (type.isChartComponent) { + return true; + } + + const children = element.props.children; + if (!children) { + return false; + } + + return React.Children.toArray(children).some(isChartElement); +}; + export const lineBreakLabel = (label: string, maxChars = 14) => { const words = label.split(" "); const lines: string[] = []; diff --git a/webapp/src/features/indicators/biodiversity/format-data.ts b/webapp/src/features/indicators/biodiversity/format-data.ts index 9daed1be..de64b884 100644 --- a/webapp/src/features/indicators/biodiversity/format-data.ts +++ b/webapp/src/features/indicators/biodiversity/format-data.ts @@ -70,21 +70,12 @@ export const useFormatBiodiversityData = (data: BiodiversityData) => { verticalDistribution: safeData.epf_vertical_distribution, }, }, - // replace hardcoded value when data will be available - indicatorSpecies: { - abundanceTaxon1: 43, - abundanceTaxon2: 56, - abundanceTaxon3: 33, - speciesRichnessTaxon1: 47, - speciesRichnessTaxon2: 23, - speciesRichnessTaxon3: 24, - }, treeDiversity: { relative_abundance: formatRelativeAbundance( data.relative_abundance, data.tree_pop, ), - speciesRichness: data.richness, + speciesRichness: formatWithUnit(safeData.richness, UNITS.essenceCount), tree_pop: data.tree_pop, }, }; diff --git a/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx b/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx index aaa1841f..db6e1454 100644 --- a/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx +++ b/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx @@ -1,5 +1,5 @@ import type { LayerMetadata } from "coordo"; -import { HeartPulseIcon, InfoIcon } from "lucide-react"; +import { Gem, TreePine, Trees } from "lucide-react"; import { ChartForestPotential } from "@features/charts/biodiversity/chart-forest-potential"; import { ChartRelativeAbundance } from "@features/charts/biodiversity/chart-relative-abundance"; @@ -25,11 +25,12 @@ export const useBiodiversityIndicatorElements = ( <> } value={data.biomass.volume} /> } + iconStart={} value={data.biomass.density} /> @@ -46,7 +47,7 @@ export const useBiodiversityIndicatorElements = ( dataName={t( "indicators.biodiversity.sections.treeDiversity.speciesRichness", )} - iconStart={} + iconStart={} value={data.treeDiversity.speciesRichness} /> = ({ } return ( -
+
{iconStart}

{dataName}

diff --git a/webapp/src/features/indicators/components/indicator-section.tsx b/webapp/src/features/indicators/components/indicator-section.tsx index 2e462ab2..dc0320b9 100644 --- a/webapp/src/features/indicators/components/indicator-section.tsx +++ b/webapp/src/features/indicators/components/indicator-section.tsx @@ -1,22 +1,78 @@ import type { FC, PropsWithChildren, ReactNode } from "react"; +import React from "react"; + +import { isChartElement } from "@features/charts/utils"; + +import { Card, CardContent } from "@shared/ui/card"; type IndicatorSectionProps = PropsWithChildren<{ title: string; iconStart?: ReactNode; }>; +// Put a node and all its children in the same array +const flattenChildren = (node: ReactNode): ReactNode[] => { + if (!React.isValidElement(node)) { + return [node]; + } + + if (node.type === React.Fragment) { + const element = node as React.ReactElement<{ children?: ReactNode }>; + return React.Children.toArray(element.props.children).flatMap( + flattenChildren, + ); + } + + return [node]; +}; + export const IndicatorSection: FC = ({ title, iconStart, children, }) => { + const childrenArray = flattenChildren(children); + + // Divide indicator elements in 2 categories + // valueIndicators will be displayed inside a Card Component to improve visibility + // chartIndicators are already put (individually) inside a Card. + const [valueIndicators, chartIndicators] = childrenArray.reduce< + [ReactNode[], ReactNode[]] + >( + ([values, charts], child) => { + return isChartElement(child) + ? [values, [...charts, child]] + : [[...values, child], charts]; + }, + [[], []], + ); + return (
{iconStart} -
{title}
+
{title}
- {children} + + {valueIndicators.length > 0 && ( + + {valueIndicators} + + )} + + {chartIndicators.length > 0 && ( +
+ {chartIndicators.map((child, index) => ( +
+ key={`chart-indicator-${index}`} + > + {child} +
+ ))} +
+ )}
); }; diff --git a/webapp/src/features/indicators/economy/format-data.ts b/webapp/src/features/indicators/economy/format-data.ts index 3190457a..b0272742 100644 --- a/webapp/src/features/indicators/economy/format-data.ts +++ b/webapp/src/features/indicators/economy/format-data.ts @@ -31,11 +31,11 @@ export const useFormatEconomicData = (data: EconomicData) => { assetsIndex: `${safeData.estateIndex}/10 (±${1})`, incomeSourceNb: `${1} (±${1})`, livingConditionsPerception: { - dontKnow: 2, - improvement: 1, - refuse: 1, - regression: 5, - stable: 3, + dontKnow: 4, + improvement: 28, + refuse: 3, + regression: 38, + stable: 27, }, nbAdditionalIncomes: 1, revenueChange: `${-5} % (±${1})`, @@ -44,10 +44,10 @@ export const useFormatEconomicData = (data: EconomicData) => { }, governance: { beneficialPractices: { - defense: 4, - improvedHousehold: 6, - rna: 3, - treePlanting: 8, + defense: 71, + improvedHousehold: 57, + rna: 100, + treePlanting: 100, }, conflictIndex: `${6}/10 (±${1})`, }, diff --git a/webapp/src/features/indicators/economy/use-economic-indicator-elements.tsx b/webapp/src/features/indicators/economy/use-economic-indicator-elements.tsx index cc3e3185..343e9053 100644 --- a/webapp/src/features/indicators/economy/use-economic-indicator-elements.tsx +++ b/webapp/src/features/indicators/economy/use-economic-indicator-elements.tsx @@ -1,8 +1,11 @@ +import { ChartLine, HandCoins, Scroll } from "lucide-react"; + import { ChartBeneficialPractices } from "@features/charts/socio-eco/chart-beneficial-practices"; import { ChartLivingPerception } from "@features/charts/socio-eco/chart-living-perception"; import { useTranslation } from "@i18n"; +import { ICON_SIZE } from "../components/constants"; import { IndicatorRawValue } from "../components/indicator-raw-value"; import type { UseIndicatorReturnType } from "../components/types"; import { useFormatEconomicData } from "./format-data"; @@ -20,14 +23,17 @@ export const useEconomicIndicatorElements = ( <> } value={data.economy.incomeSourceNb} /> } value={data.economy.revenueChange} /> } value={data.economy.assetsIndex} /> + {/* Not implemented yet, waiting for All4Trees to better define this indicator. + /> */} diff --git a/webapp/src/features/indicators/social/format-data.ts b/webapp/src/features/indicators/social/format-data.ts index 9e63ddb2..7d4b2712 100644 --- a/webapp/src/features/indicators/social/format-data.ts +++ b/webapp/src/features/indicators/social/format-data.ts @@ -10,7 +10,6 @@ import { import type { SocialData } from "./types"; const indicatorKeys: NumericKeys[] = [ - "population", "collectedWoodEnergy", "boughtWoodEnergy", "coalEnergy", @@ -56,28 +55,31 @@ export const useFormatSocialData = (data: SocialData) => { leanPeriod: formatWithUnit(3, UNITS.monthPerYear), }, wood: { - collectionTime: safeData.woodCollectionTime, + collectionTime: formatWithUnit( + safeData.woodCollectionTime, + UNITS.minPerHouseholdPerDay, + ), energyConsumption: formatWithUnit(0, UNITS.m3PerHabPerYear), energyNeeds: { - difficultToMeet: 3, - dontKnow: 2, - easyToMeet: 4, - moderateToMeet: 7, + difficultToMeet: 48, + dontKnow: 4, + easyToMeet: 10, + moderateToMeet: 38, }, energySources: { - animalWaste: 4, - boughtWood: 10, - coal: 5, - collectedWood: 18, - gas: 14, - organicWaste: 3, - other: 1, + animalWaste: 14, + boughtWood: 14, + coal: 57, + collectedWood: 100, + gas: 71, + organicWaste: 86, + other: 57, }, timberNeeds: { - difficultToMeet: 2, - dontKnow: 1, - easyToMeet: 4, - moderateToMeet: 8, + difficultToMeet: 38, + dontKnow: 6, + easyToMeet: 16, + moderateToMeet: 40, }, }, }; diff --git a/webapp/src/features/indicators/social/types.ts b/webapp/src/features/indicators/social/types.ts index 4b059904..7a9510f9 100644 --- a/webapp/src/features/indicators/social/types.ts +++ b/webapp/src/features/indicators/social/types.ts @@ -1,6 +1,4 @@ export type SocialData = { - admi2: string; - population: number; collectedWoodEnergy: number; boughtWoodEnergy: number; coalEnergy: number; diff --git a/webapp/src/features/indicators/social/use-social-indicator-elements.tsx b/webapp/src/features/indicators/social/use-social-indicator-elements.tsx index 2173a4b9..ac431686 100644 --- a/webapp/src/features/indicators/social/use-social-indicator-elements.tsx +++ b/webapp/src/features/indicators/social/use-social-indicator-elements.tsx @@ -1,3 +1,5 @@ +import { Calendar, Hourglass, Soup, Wheat, Zap } from "lucide-react"; + import { ChartEnergySources, ChartWoodEnergyNeeds, @@ -9,6 +11,7 @@ import type { UseIndicatorReturnType } from "@features/indicators/components/typ import { useTranslation } from "@i18n"; +import { ICON_SIZE } from "../components/constants"; import { useFormatSocialData } from "./format-data"; import type { SocialData } from "./types"; @@ -25,11 +28,13 @@ export const useSocialIndicatorElements = ( } value={data.wood.energyConsumption} /> } value={data.wood.collectionTime} /> @@ -44,15 +49,18 @@ export const useSocialIndicatorElements = ( <> } value={data.food.foodDiversityScore} /> } value={data.food.autoConsumptionNeeds} /> } value={data.food.leanPeriod} /> diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index 3e9597ea..cc662554 100644 --- a/webapp/src/features/indicators/soil/format-data.ts +++ b/webapp/src/features/indicators/soil/format-data.ts @@ -12,7 +12,6 @@ import type { SoilData } from "./types"; const indicatorsToPreciseWithFallBack: NumericKeys[] = [ "soil_structure", - "soil_composition", "soil_fauna_density", "surface_fauna_density", ] as const; diff --git a/webapp/src/features/indicators/soil/types.ts b/webapp/src/features/indicators/soil/types.ts index b4822a44..1f7bca0f 100644 --- a/webapp/src/features/indicators/soil/types.ts +++ b/webapp/src/features/indicators/soil/types.ts @@ -4,7 +4,6 @@ export type SoilData = { soil_structure: number; - soil_composition: number; ero_rainfall: number; ero_wind: number; ero_soil_cover: number; diff --git a/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx b/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx index da52010a..704e1d44 100644 --- a/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx +++ b/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx @@ -1,4 +1,5 @@ import type { LayerMetadata } from "coordo"; +import { Bug, Gem, Sprout } from "lucide-react"; import { ChartAquaticErosion } from "@features/charts/soil/ui/chart-aquatic-erosion"; import { ChartTaxonAbundance } from "@features/charts/soil/ui/chart-taxon-abundance"; @@ -8,6 +9,7 @@ import { IndicatorRawValue } from "@features/indicators/components/indicator-raw import { useTranslation } from "@i18n"; +import { ICON_SIZE } from "../components/constants"; import { useFormatSoilData } from "./format-data"; import type { SoilData } from "./types"; @@ -23,12 +25,9 @@ export const useSoilIndicatorElements = ( { children: ( <> - } value={data.soil_structure} /> @@ -69,10 +68,12 @@ export const useSoilIndicatorElements = ( <> } value={data.soil_fauna_density} /> } value={data.soil_fauna_diversity} /> } value={data.surface_fauna_density} /> } value={data.surface_fauna_diversity} /> { return t("indicators.units.speciesCount", { count: parseInt(formattedValue, 10), }); + case UNITS.essenceCount: + return t("indicators.units.essenceCount", { + count: parseInt(formattedValue, 10), + }); case UNITS.tonPerHectare: return t("indicators.units.tonPerHectare", { value }); case UNITS.m3PerHabPerYear: @@ -50,6 +56,8 @@ export const useFormatterWithUnit = () => { return t("indicators.units.monthPerYear", { value }); case UNITS.percentFoodRequirements: return t("indicators.units.percentFoodRequirements", { value }); + case UNITS.minPerHouseholdPerDay: + return t("indicators.units.minPerHhPerDay", { value }); default: return null; } diff --git a/webapp/src/features/popup/components/indicator-popup-header.tsx b/webapp/src/features/popup/components/indicator-popup-header.tsx index 510a90d6..28d0ab2e 100644 --- a/webapp/src/features/popup/components/indicator-popup-header.tsx +++ b/webapp/src/features/popup/components/indicator-popup-header.tsx @@ -1,6 +1,11 @@ -import { XIcon } from "lucide-react"; +import { Calendar, XIcon } from "lucide-react"; import type { FC, ReactNode } from "react"; +import { + ICON_SIZE, + ICON_SIZE_HEADER, +} from "@features/indicators/components/constants"; + import { cn } from "@shared/lib/utils"; import { Alert, @@ -10,12 +15,11 @@ import { } from "@shared/ui/alert"; import { Button } from "@shared/ui/button"; -import { ICON_SIZE_HEADER } from "../../indicators/components/constants"; - type IndicatorPopupHeaderProps = { icon: ReactNode; title: string; subtitle?: string; + date?: string; onCrossClick: () => void; }; @@ -23,19 +27,31 @@ export const IndicatorPopupHeader: FC = ({ icon, title, subtitle, + date, onCrossClick, }) => { return ( {icon} {title} - {subtitle && ( - - {subtitle} + {(subtitle || date) && ( + + {subtitle && {subtitle}} + + {date && ( +
+ +

{date}

+
+ )}
)} diff --git a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx index 445cb4a8..31bb1627 100644 --- a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx +++ b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx @@ -39,13 +39,8 @@ export const ForestInventoryPopupContent: FC< const biodiversityElements = useBiodiversityIndicatorElements(data, metadata); const soilElements = useSoilIndicatorElements(data, metadata); - const title = t("popup.forestInventory", { - code: data.cod, - label: - findCategoricalLabel(metadata, "for", data.for) || t("popup.undefined"), - }); - - const subtitles = { + const title = t("popup.forestInventory.title", { id: data.id }); + const tabs = { [TABS.BIODIVERSITY]: t("indicators.biodiversity.title"), [TABS.SOIL]: t("indicators.soil.title"), }; @@ -53,9 +48,13 @@ export const ForestInventoryPopupContent: FC< return (
} onCrossClick={onClose} - subtitle={formatDate(new Date())} + subtitle={ + findCategoricalLabel(metadata, "for", data.for) || + t("popup.undefined") + } title={title} /> @@ -65,11 +64,11 @@ export const ForestInventoryPopupContent: FC< options={[ { id: TABS.BIODIVERSITY, - label: subtitles[TABS.BIODIVERSITY], + label: tabs[TABS.BIODIVERSITY], }, { id: TABS.SOIL, - label: subtitles[TABS.SOIL], + label: tabs[TABS.SOIL], }, ]} value={selectedTab} diff --git a/webapp/src/features/popup/forest-inventory/types.ts b/webapp/src/features/popup/forest-inventory/types.ts index cf554b39..16b9fd10 100644 --- a/webapp/src/features/popup/forest-inventory/types.ts +++ b/webapp/src/features/popup/forest-inventory/types.ts @@ -2,7 +2,10 @@ import type { BiodiversityData } from "@features/indicators/biodiversity/types"; import type { SoilData } from "@features/indicators/soil/types"; export type ForestInventoryData = { + id: string; for: string; cod: number; + projet: string; + taille_placette: number; } & SoilData & BiodiversityData; diff --git a/webapp/src/features/popup/renderPopup.tsx b/webapp/src/features/popup/renderPopup.tsx index e8a40d97..c8f0e8ec 100644 --- a/webapp/src/features/popup/renderPopup.tsx +++ b/webapp/src/features/popup/renderPopup.tsx @@ -8,7 +8,7 @@ export const DEFAULT_POPUP_CONFIG: PopupOptions = { closeButton: false, closeOnClick: true, closeOnMove: false, - maxWidth: "500px", + maxWidth: "560px", }; export function getRenderPopupLayer( @@ -24,7 +24,7 @@ export function getRenderPopupLayer( const root = createRoot(container); root.render( root.unmount()} diff --git a/webapp/src/features/popup/socio-eco/popup-socio-eco.tsx b/webapp/src/features/popup/socio-eco/popup-socio-eco.tsx index cc3b23fe..ab17d642 100644 --- a/webapp/src/features/popup/socio-eco/popup-socio-eco.tsx +++ b/webapp/src/features/popup/socio-eco/popup-socio-eco.tsx @@ -57,11 +57,10 @@ export const SocioEcoIndicator: FC = ({ const socialElements = useSocialIndicatorElements(data); const economicElements = useEconomicIndicatorElements(data); - const title = t("popup.socioEco", { + const title = t("popup.socioEco.title", { village: exctractVillageName(metadata, data), }); - - const subtitles = { + const tabs = { [TABS.RESOURCES]: t("indicators.resources.title"), [TABS.ECONOMY]: t("indicators.economy.title"), }; @@ -69,23 +68,26 @@ export const SocioEcoIndicator: FC = ({ return (
} onCrossClick={onClose} - subtitle={formatDate(new Date())} + subtitle={t("popup.socioEco.subtitleCount", { + count: data.household_nb, + })} title={title} /> setSelectedTab(value as TabKind)} options={[ { id: TABS.RESOURCES, - label: subtitles[TABS.RESOURCES], + label: tabs[TABS.RESOURCES], }, { id: TABS.ECONOMY, - label: subtitles[TABS.ECONOMY], + label: tabs[TABS.ECONOMY], }, ]} value={selectedTab} diff --git a/webapp/src/features/popup/socio-eco/types.ts b/webapp/src/features/popup/socio-eco/types.ts index af29fa2b..8cf78cd9 100644 --- a/webapp/src/features/popup/socio-eco/types.ts +++ b/webapp/src/features/popup/socio-eco/types.ts @@ -2,6 +2,8 @@ import type { EconomicData } from "@features/indicators/economy/types"; import type { SocialData } from "@features/indicators/social/types"; export type SocioEcoData = { + household_nb: number; + population: number; admi2: string; } & SocialData & EconomicData; diff --git a/webapp/src/shared/i18n/translations/en/translations.json b/webapp/src/shared/i18n/translations/en/translations.json index 41bbc85a..05eb27a4 100644 --- a/webapp/src/shared/i18n/translations/en/translations.json +++ b/webapp/src/shared/i18n/translations/en/translations.json @@ -92,7 +92,7 @@ "title": "Tree Diversity" } }, - "title": "Biodiversity Data" + "title": "Biodiversity" }, "common": { "abundance": "Relative abundance", @@ -104,10 +104,10 @@ "speciesRichness": "Species richness" }, "economy": { - "title": "Economy and governance" + "title": "Local Residents" }, "resources": { - "title": "Households resources" + "title": "Resources & Territories" }, "socioEco": { "sections": { @@ -218,25 +218,34 @@ }, "unknownTaxon": "Unknown taxon" }, - "title": "Soil data" + "title": "Soil" }, "units": { + "essenceCount": "{{ count }} species inventoried", "individualPerCubicMeter": "{{ value }} ind/m³", "individualPerHectare": "{{ value }} ind/ha", "individualPerTrap": "{{ value }} ind/trap", "m3PerHabPerYear": "{{ value }} m³/inhab/year", + "minPerHhPerDay": "{{ value }} min/household/day", "monthPerYear": "{{ value }} month/year", "percentFoodRequirements": "{{ value }} % of food needs", - "speciesCount": "", - "speciesCount_one": "{{ count }} species inventoried", - "speciesCount_other": "{{ count }} species inventoried", + "speciesCount": "{{ count }} species inventoried", "tonPerHectare": "{{ value }} t/ha" } }, "popup": { - "forestInventory": "Plot n°{{ code }} in {{ label }} forest", + "forestInventory": { + "date": "Site survey date : {{ date }}", + "title": "Plot n°{{ id }}" + }, "seed": "Plot n°{{ id}}", - "socioEco": "Village {{ village }}", + "socioEco": { + "date": "Survey date: {{ date }}", + "subtitleCount": "", + "subtitleCount_one": "{{ count }} household surveyed", + "subtitleCount_other": "{{ count }} households surveyed", + "title": "{{ village }}" + }, "undefined": "not found" }, "seed": { diff --git a/webapp/src/shared/i18n/translations/fr/translations.json b/webapp/src/shared/i18n/translations/fr/translations.json index e0bd7aef..132330a8 100644 --- a/webapp/src/shared/i18n/translations/fr/translations.json +++ b/webapp/src/shared/i18n/translations/fr/translations.json @@ -92,7 +92,7 @@ "title": "Diversité arborée" } }, - "title": "Données biodiversité" + "title": "Biodiversité" }, "common": { "abundance": "Abondance relative", @@ -104,10 +104,10 @@ "speciesRichness": "Richesse spécifique" }, "economy": { - "title": "Modèle économique et gouvernance" + "title": "Population locale" }, "resources": { - "title": "Ressources des ménages" + "title": "Ressources et territoires" }, "socioEco": { "sections": { @@ -126,7 +126,7 @@ "title": "Modèle économique" }, "food": { - "autoConso": "Satsifaction des besoins en auto-consommation", + "autoConso": "Satisfaction des besoins en auto-consommation", "diversity": { "cereals": "Céréales", "eggs": "Œufs", @@ -176,7 +176,7 @@ "difficultToMeet": "Difficile à satisfaire", "dontKnow": "Ne sait pas", "easyToMeet": "Facile à satisfaire", - "moderateToMeet": "Modéré à satisfaire" + "moderateToMeet": "Moyennement facile à satisfaire" }, "timberNeeds": { "title": "Satisfaction des besoins en bois d’œuvre" @@ -218,13 +218,17 @@ }, "unknownTaxon": "Taxon inconnu" }, - "title": "Données sol" + "title": "Sol" }, "units": { + "essenceCount": "", + "essenceCount_one": "{{ count }} essence inventoriée", + "essenceCount_other": "{{ count }} essences inventoriées", "individualPerCubicMeter": "{{ value }} ind/m³", "individualPerHectare": "{{ value }} ind/ha", "individualPerTrap": "{{ value }} ind/piège", "m3PerHabPerYear": "{{ value }} m³/hab/an", + "minPerHhPerDay": "{{ value }} min/ménage/jour", "monthPerYear": "{{ value }} mois/an", "percentFoodRequirements": "{{ value }} % des besoins alimentaires", "speciesCount": "", @@ -234,9 +238,18 @@ } }, "popup": { - "forestInventory": "Placette n°{{ code }} dans la forêt {{ label }}", + "forestInventory": { + "date": "Date relevé: {{ date }}", + "title": "Placette n°{{ id }}" + }, "seed": "Placette n°{{ id}}", - "socioEco": "Village {{ village }}", + "socioEco": { + "date": "Date relevé: {{ date }}", + "subtitleCount": "", + "subtitleCount_one": "{{ count }} ménage enquêté", + "subtitleCount_other": "{{ count }} ménages enquêtés", + "title": "{{ village }}" + }, "undefined": "non trouvée" }, "seed": { diff --git a/webapp/src/shared/lib/palette.ts b/webapp/src/shared/lib/palette.ts index 80a5e334..ff7b4bc6 100644 --- a/webapp/src/shared/lib/palette.ts +++ b/webapp/src/shared/lib/palette.ts @@ -7,9 +7,10 @@ const getCssVarColor = (name: string, fallback: string) => { }; export const getChartPalette = () => [ - getCssVarColor("--chart-1", "#091c2d"), - getCssVarColor("--chart-2", "#f8f8f8"), - getCssVarColor("--chart-3", "#26a65d"), - getCssVarColor("--chart-4", "#f5911f"), - getCssVarColor("--chart-5", "#f5f27b"), + getCssVarColor("--chart-1", "#97cf17"), + getCssVarColor("--chart-2", "#f98038"), + getCssVarColor("--chart-3", "#2d6db4"), + getCssVarColor("--chart-4", "#895bf5"), + getCssVarColor("--chart-5", "#f04646"), + getCssVarColor("--chart-6", "#424242"), ]; diff --git a/webapp/src/shared/lib/utils.ts b/webapp/src/shared/lib/utils.ts index 013177c1..06bc3f27 100644 --- a/webapp/src/shared/lib/utils.ts +++ b/webapp/src/shared/lib/utils.ts @@ -29,6 +29,7 @@ export function findCategoricalLabel( fieldName: string, fieldValue: any, ): string | undefined { + // Searching field category in main resource schema const resourceLabel = metadata?.resource?.schema?.fields .find((f) => f.name === fieldName) ?.categories?.find((c) => c.value === fieldValue)?.label; @@ -37,6 +38,7 @@ export function findCategoricalLabel( return resourceLabel; } + // Searching field category in main resource's references' schemas return metadata?.references ?.find((ref) => ref.schema.fields