Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions webapp/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"!@pages/**",
"!@widgets/**",
"!@features/**",
"!@entities/**",
"!@shared/**",
"!@lib/**",
"!@i18n",
Expand All @@ -31,6 +32,8 @@
":BLANK_LINE:",
"@features/**",
":BLANK_LINE:",
"@entities/**",
":BLANK_LINE:",
"@shared/**",
"@lib/**",
"@i18n",
Expand Down
12 changes: 11 additions & 1 deletion webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.13.0",
"react-spinners": "^0.17.0",
"recharts": "^2.15.4"
"recharts": "^2.15.4",
"zod": "^4.4.3"
},
"devDependencies": {
"@biomejs/biome": "^2.4.4",
Expand Down
5 changes: 2 additions & 3 deletions webapp/src/app/providers/ApiProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { type ReactNode, useMemo } from "react";

import { createApiClient } from "@features/api/client";
import { useAuth } from "@features/auth/useAuth";

import { createApiClient } from "@shared/api/client";
import { ApiContext } from "@shared/contexts/ApiContext";
import { ApiContext } from "@features/contexts/ApiContext";
Comment thread
severo marked this conversation as resolved.

interface ApiProviderProps {
children: ReactNode;
Expand Down
22 changes: 22 additions & 0 deletions webapp/src/entities/dashboard/epf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// > les epf regroupent tous les indicateurs indirects de biodiversité ("Tropical Biodiversity Index")
// > epf = eco-potentialité forestière, l'ancien nom de l'indice de biodiversité tropicale
// TODO: rename epf to something more meaningful?

import * as z from "zod";

import { ValueAndErrorSchema } from "@entities/dashboard/generic";

export const EpfDataSchema = z.object({
epf_deadWood: ValueAndErrorSchema,
epf_diameter_distribution: ValueAndErrorSchema,
epf_dominant_height: ValueAndErrorSchema,
epf_microhabitats: ValueAndErrorSchema,
epf_necromass_pied: ValueAndErrorSchema, // needed?

@arnaudfnr arnaudfnr Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed indeed. I added them in the config.json to help me construct the (very long) formulat for 'epf_deadwoord' but I removed them in #162

epf_necromass_sol: ValueAndErrorSchema, // needed?
epf_spatial_distribution: ValueAndErrorSchema,
epf_tree_density: ValueAndErrorSchema,
epf_tree_diversity: ValueAndErrorSchema,
epf_vertical_distribution: ValueAndErrorSchema,
});

export type EpfData = z.infer<typeof EpfDataSchema>;
30 changes: 30 additions & 0 deletions webapp/src/entities/dashboard/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as z from "zod";

export const ValueAndErrorSchema = z
.object({
error: z.number().nullable(),
value: z.number().nullable(),
})
.default(() => ({
error: null,
value: null,
}));

const DictionaryDataSchema = z.record(z.string(), ValueAndErrorSchema);

export type DictionaryData = z.infer<typeof DictionaryDataSchema>;

const MIN_YEAR = 1900;
const MAX_YEAR = 2100;
const YearSchema = z.coerce.number().int().min(MIN_YEAR).max(MAX_YEAR);

const YearDataSchema = z.object({
beneficiary: DictionaryDataSchema,
control: DictionaryDataSchema,
});

export type YearData = z.infer<typeof YearDataSchema>;

export const DashboardDataSchema = z.record(YearSchema, YearDataSchema);

export type DashboardData = z.infer<typeof DashboardDataSchema>;
20 changes: 20 additions & 0 deletions webapp/src/features/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
type DashboardData,
DashboardDataSchema,
} from "@entities/dashboard/generic";

import { fetchJSONWithAuth } from "@shared/api/client";

export const createApiClient = (authToken: string | null) => ({
// Bases
fetchDashboardData: async (layerId: string): Promise<DashboardData> => {
const json = await fetchJSONWithAuth(
`/maps/dashboard/${layerId}`,
{},
authToken,
);
return DashboardDataSchema.parse(json);
},
});

export type ApiClient = ReturnType<typeof createApiClient>;
20 changes: 17 additions & 3 deletions webapp/src/features/charts/biodiversity/chart-forest-potential.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useTranslation } from "@i18n";
import type { ChartComponentType } from "@features/charts/components/chart-component";
import { ChartRadarWithBenefAndControl } from "@features/charts/components/radar-benef-control";

import type { EpfData } from "@entities/dashboard/epf";

import type { ChartComponentType } from "../components/chart-component";
import { ChartRadarWithBenefAndControl } from "../components/radar-benef-control";
import { useTranslation } from "@i18n";

export type ChartForestPotentialData = {
density: number;
Expand All @@ -19,6 +21,18 @@ type ChartForestPotentialProps = {
temoin?: ChartForestPotentialData;
};

export function fromEpfData(data: EpfData): ChartForestPotentialData {
return {
deadWood: data.epf_deadWood.value ?? 0,
density: data.epf_tree_density.value ?? 0,
diameterDistribution: data.epf_diameter_distribution.value ?? 0,
diversity: data.epf_tree_diversity.value ?? 0,
dominantHeight: data.epf_dominant_height.value ?? 0,
microHabitat: data.epf_microhabitats.value ?? 0,
spatialDistribution: data.epf_spatial_distribution.value ?? 0,
verticalDistribution: data.epf_vertical_distribution.value ?? 0,
};
}
export const ChartForestPotential: ChartComponentType<
ChartForestPotentialProps
> = ({ benef, temoin }) => {
Expand Down
11 changes: 9 additions & 2 deletions webapp/src/features/charts/soil/lib/sunburst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,18 @@ export function buildSunburstNodes(
return nodes;
}

export const getLevelPalettes = () => {
type ThreeColorsPalette = [string, string, string];
type ThreePalettes = [
ThreeColorsPalette,
ThreeColorsPalette,
ThreeColorsPalette,
];

export const getLevelPalettes = (): ThreePalettes => {
const palette = getChartPalette();

return [
palette.slice(0, 3),
[palette[0], palette[1], palette[2]],
[palette[3], palette[4], palette[0]],
[palette[1], palette[2], palette[3]],
];
Expand Down
35 changes: 9 additions & 26 deletions webapp/src/features/charts/soil/lib/taxon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,18 @@ import type { LayerMetadata } from "coordo";

import { findCategoricalLabel } from "@shared/lib/utils";

export function getTaxonLabels(
element: string,
metadata: LayerMetadata,
dataType: "tsbf" | "barbA",
): [string, string, string] {
const [taxon1, taxon2, taxon3] = element.split("-");
const taxon1Label =
findCategoricalLabel(metadata, `tax1_${dataType}`, taxon1) || taxon1;
const taxon2Label =
findCategoricalLabel(metadata, `tax2_${dataType}`, taxon2) || taxon2;
const taxon3Label =
findCategoricalLabel(metadata, `tax3_${dataType}`, taxon3) || taxon3;
return [taxon1Label, taxon2Label, taxon3Label];
}

export function formatTaxonLevelLabel(
element: string,
metadata: LayerMetadata,
dataType: "tsbf" | "barbA",
): string {
const [taxon1Label, taxon2Label, taxon3Label] = getTaxonLabels(
element,
metadata,
dataType,
);
const parts = element.split("-");
return parts.length === 1
? taxon1Label
: parts.length === 2
? taxon2Label
: taxon3Label;
// 'taxon1 = element' is for typescript... taxon1 should never be undefined, but typescript doesn't know that
const [taxon1 = element, taxon2, taxon3] = element.split("-");
if (taxon2 === undefined) {
return findCategoricalLabel(metadata, `tax1_${dataType}`, taxon1) || taxon1;
}
if (taxon3 === undefined) {
return findCategoricalLabel(metadata, `tax2_${dataType}`, taxon2) || taxon2;
}
return findCategoricalLabel(metadata, `tax3_${dataType}`, taxon3) || taxon3;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext } from "react";

import type { ApiClient } from "@shared/api/client";
import type { ApiClient } from "@features/api/client";

export const ApiContext = createContext<ApiClient | undefined>(undefined);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from "react";

import { ApiContext } from "@shared/contexts/ApiContext";
import { ApiContext } from "@features/contexts/ApiContext";

export function useApi() {
const context = useContext(ApiContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type ForestInventoryPopupContentProps = RenderPopupProps<ForestInventoryData>;

type TabKind = "biodiversity" | "soil";

const TABS: Record<string, TabKind> = {
const TABS = {
BIODIVERSITY: "biodiversity",
SOIL: "soil",
} as const;
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/features/popup/socio-eco/popup-socio-eco.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type SocioEcoIndicatorProps = RenderPopupProps<SocioEcoData>;

type TabKind = "resources" | "economy";

const TABS: Record<string, TabKind> = {
const TABS = {
ECONOMY: "economy",
RESOURCES: "resources",
} as const;
Expand Down
13 changes: 3 additions & 10 deletions webapp/src/shared/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const API_URL =
import.meta.env.VITE_API_URL || "http://localhost:8000/api";

export const fetchWithAuth = async (
const fetchWithAuth = async (
endpoint: string,
options: RequestInit = {},
authToken: string | null,
Expand Down Expand Up @@ -37,12 +37,5 @@ export const fetchJSONWithAuth = async (
endpoint: string,
options: RequestInit = {},
authToken: string | null,
) => (await fetchWithAuth(endpoint, options, authToken)).json();

export const createApiClient = (authToken: string | null) => ({
// Bases
getDashboardData: (layerId: string) =>
fetchJSONWithAuth(`/maps/dashboard/${layerId}`, {}, authToken),
});

export type ApiClient = ReturnType<typeof createApiClient>;
): Promise<unknown> =>
(await fetchWithAuth(endpoint, options, authToken)).json();
6 changes: 6 additions & 0 deletions webapp/src/shared/i18n/translations/en/all4trees.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"title": "Error while loading data",
"unknownMessage": "An unknown error occurred. Please try again later."
},
"header": {
"catalog": "Charts catalog",
"greeting": "Hello {{username}}! 👋",
"temporaryBeneficiary": "⚠ Beneficiary",
"temporaryFilter": "⚠ Filter"
},
"select": {
"year": "Year"
}
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/shared/i18n/translations/fr/all4trees.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"title": "Erreur lors du chargement des données",
"unknownMessage": "Une erreur inconnue s'est produite. Veuillez réessayer plus tard."
},
"header": {
"catalog": "Catalogue des graphiques",
"greeting": "Bonjour {{username}} ! 👋",
"temporaryBeneficiary": "⚠ Bénéficiaire",
"temporaryFilter": "⚠ Filtre"
},
"select": {
"year": "Année"
}
Expand Down
9 changes: 8 additions & 1 deletion webapp/src/shared/lib/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ const getCssVarColor = (name: string, fallback: string) => {
);
};

export const getChartPalette = () => [
export const getChartPalette = (): [
string,
string,
string,
string,
string,
string,
] => [
getCssVarColor("--chart-1", "#97cf17"),
getCssVarColor("--chart-2", "#f98038"),
getCssVarColor("--chart-3", "#2d6db4"),
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/shared/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export function cn(...inputs: ClassValue[]) {

export function precise(value?: number | null) {
if (!value || Number.isNaN(value)) {
// TODO: missing or erroneous values should not be considered as 0, but rather as null or undefined.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some cases Cathaline asked us to put "0" for empty values, in other cases "N/A".
Check hiw data is formatted here
It is certainly not the best way to do it, but that's why this method returns "0"

// when displayed, they should be shown as "N/A" or "No data", or the point could be omitted from the chart.
return "0";
}
if (value > 999) {
Expand Down
4 changes: 4 additions & 0 deletions webapp/src/shared/ui/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ const ChartTooltipContent = React.forwardRef<
{
/* Force a space between item label and value*/ "\xa0" +
item.value.toLocaleString()
// ^ TODO: why not using CSS to add a space between the label and the value?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is generated automatically when importing shadcn components, see Shadcn Doc. We might want to tweak this code, but as long as it is not strictly necessary, I would prefer to keep it as is
(Same for every component in shared/ui)

// ^ TODO: pass a prop to format the item value (e.g.: no more than 2 decimals, or other formatting rules)
// ^ TODO: use the current locale to format the item value (e.g.: 1,000.00 in en-US, 1 000,00 in fr-FR, etc.)
// ^ TODO: add the unit?
}
</span>
)}
Expand Down
Loading
Loading