From f2c7626fb653069e339ae7108440e2a174126286 Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Wed, 1 Jul 2026 12:58:17 +0200 Subject: [PATCH 1/9] Fix(indicators): Fix taxon abundances --- backend/configs/config.json | 8 +++- .../biodiversity/chart-relative-abundance.tsx | 23 ++++------- webapp/src/features/charts/soil/types.ts | 2 + .../charts/soil/ui/chart-taxon-abundance.tsx | 5 +++ .../indicators/biodiversity/format-data.ts | 20 +++------ .../features/indicators/soil/format-data.ts | 29 ++++++++++++- webapp/src/features/indicators/soil/types.ts | 4 ++ webapp/src/features/indicators/utils.ts | 41 +++++++++++-------- webapp/src/shared/lib/utils.ts | 3 +- 9 files changed, 85 insertions(+), 50 deletions(-) diff --git a/backend/configs/config.json b/backend/configs/config.json index 1745a353..f592a8d8 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -76,10 +76,14 @@ "soil_eros_water_infiltration": "600 * 300 / ( (epoch(interval(beer1)) + epoch(interval(beer2)) + epoch(interval(beer3)) + epoch(interval(beer4)) + epoch(interval(beer5)) + epoch(interval(beer6)) + epoch(interval(beer7)) + epoch(interval(beer8)) + epoch(interval(beer9)) + epoch(interval(beer10)) ) / 10 * pi() * 9^2) if beer1 != '' else 0", "soil_fauna_density": "sum(tsbf_001.tsbf_no) / tsbf_samp", "soil_fauna_diversity": "list_unique(list(nullif(tsbf_001.concat_ws('-', tsbf_tax1, tsbf_tax2, tsbf_tax3), '')))", - "soil_fauna_abundance": "list_aggregate(list(tsbf_001.concat_ws('-', tsbf_tax1, tsbf_tax2, tsbf_tax3)), 'histogram')", + "soil_fauna_abundance": "list_aggregate(list(nullif(tsbf_001.concat_ws('-', tsbf_tax1, tsbf_tax2, tsbf_tax3), '')), 'histogram')", + "soil_fauna_abundance_pop": "list_concat(list(tsbf_001.concat_ws(':', concat_ws('-',tsbf_tax1, tsbf_tax2, tsbf_tax3), tsbf_no) if tsbf_001.tsbf_no > 0))", + "soil_fauna_total_pop": "int(sum(tsbf_001.tsbf_no))", "soil_surface_fauna_density": "(sum(barba_001.barbA_no) + sum(barbb_001.barbB_no) + sum(barbc_001.barbC_no) + sum(barbd_001.barbD_no)) / barb_samp", "soil_surface_fauna_diversity": "list_unique(list_concat(list(nullif(barba_001.concat_ws('-', barbA_tax1, barbA_tax2, barbA_tax3), '')), list(nullif(barbb_001.concat_ws('-', barbB_tax1, barbB_tax2, barbB_tax3), '')), list(nullif(barbc_001.concat_ws('-', barbC_tax1, barbC_tax2, barbC_tax3), '')), list(nullif(barbd_001.concat_ws('-', barbD_tax1, barbD_tax2, barbD_tax3), ''))))", - "soil_surface_fauna_abundance": "list_aggregate(list_concat(list(barba_001.concat_ws('-', barbA_tax1, barbA_tax2, barbA_tax3)), list(barbb_001.concat_ws('-', barbB_tax1, barbB_tax2, barbB_tax3)), list(barbc_001.concat_ws('-', barbC_tax1, barbC_tax2, barbC_tax3)), list(barbd_001.concat_ws('-', barbD_tax1, barbD_tax2, barbD_tax3))), 'histogram')" + "soil_surface_fauna_abundance": "list_aggregate(list_concat(list(nullif(barba_001.concat_ws('-', barbA_tax1, barbA_tax2, barbA_tax3), '')), list(nullif(barbb_001.concat_ws('-', barbB_tax1, barbB_tax2, barbB_tax3), '')), list(nullif(barbc_001.concat_ws('-', barbC_tax1, barbC_tax2, barbC_tax3), '')), list(nullif(barbd_001.concat_ws('-', barbD_tax1, barbD_tax2, barbD_tax3), ''))), 'histogram')", + "soil_surface_fauna_abundance_pop": "list_concat(list(barba_001.concat_ws(':', concat_ws('-',barbA_tax1, barbA_tax2, barbA_tax3), barbA_no) if barba_001.barbA_no > 0), list(barbb_001.concat_ws(':', concat_ws('-',barbB_tax1, barbB_tax2, barbB_tax3), barbB_no) if barbb_001.barbB_no > 0), list(barbc_001.concat_ws(':', concat_ws('-',barbC_tax1, barbC_tax2, barbC_tax3), barbC_no) if barbc_001.barbC_no > 0), list(barbd_001.concat_ws(':', concat_ws('-',barbD_tax1, barbD_tax2, barbD_tax3), barbD_no) if barbd_001.barbD_no > 0))", + "soil_surface_fauna_total_pop": "int(sum(barba_001.barbA_no) + sum(barbb_001.barbB_no) + sum(barbc_001.barbC_no) + sum(barbd_001.barbD_no))" }, "popup": { "trigger": "click" diff --git a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx index 9a7ddc7a..5f06761e 100644 --- a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx +++ b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx @@ -2,7 +2,6 @@ import { findLabelInExternalData } from "@features/indicators/utils"; import type { ExternalData } from "@features/popup/forest-inventory/types"; import { useTranslation } from "@shared/i18n"; -import { precise } from "@shared/lib/utils"; import type { ChartConfig } from "@shared/ui/chart"; import type { ChartComponentType } from "../components/chart-component"; @@ -10,7 +9,7 @@ import { PieChartCategorical } from "../components/pie-chart-categorical"; type PieChartProps = { project: string; - data: [string, number][]; + data: Record; externalData: ExternalData; }; @@ -20,19 +19,15 @@ export const ChartRelativeAbundance: ChartComponentType = ({ externalData, }) => { const { t } = useTranslation("all4trees"); - 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) => ({ + const smallCategoriesSum = Object.values(data) + .filter((value) => value < 5) + .reduce((acc, value) => acc + value, 0); + const chartData = Object.entries(data) + .filter(([name, value]) => name !== "0" && (Object.keys(data).length < 6 || value >= 5)) + .map(([name, value], index) => ({ fill: `var(--chart-${(index % 5) + 1})`, - name: element[0], - value: element[1], + name, + value, })); let chartConfig: ChartConfig = {}; diff --git a/webapp/src/features/charts/soil/types.ts b/webapp/src/features/charts/soil/types.ts index 581102a3..666a2a62 100644 --- a/webapp/src/features/charts/soil/types.ts +++ b/webapp/src/features/charts/soil/types.ts @@ -17,6 +17,8 @@ export type SunburstTrace = { type: "sunburst"; branchvalues: "total"; extendsunburstcolorway: boolean; + hoverinfo?: string; + hovertext?: string[]; ids: string[]; labels: string[]; parents: string[]; 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 a63f049f..a20f214a 100644 --- a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx +++ b/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx @@ -32,11 +32,16 @@ export const ChartTaxonAbundance: ChartComponentType = ({ ); const nodes = buildSunburstNodes(filteredDataEntries, metadata, project); const nodeColors = buildNodeColors(nodes); + const hoverText = nodes.map( + (node) => `${node.label}
${node.value.toFixed(2)} %`, + ); sunburstData = [ { branchvalues: "total", extendsunburstcolorway: true, + hoverinfo: "text", + hovertext: hoverText, ids: nodes.map((n) => n.id), labels: nodes.map((n) => n.label), leaf: { opacity: 0.4 }, diff --git a/webapp/src/features/indicators/biodiversity/format-data.ts b/webapp/src/features/indicators/biodiversity/format-data.ts index 817e9557..85a1b56e 100644 --- a/webapp/src/features/indicators/biodiversity/format-data.ts +++ b/webapp/src/features/indicators/biodiversity/format-data.ts @@ -1,15 +1,16 @@ import { useTranslation } from "react-i18next"; import { + convertDictToPercentage, preciseNumericIndicators, UNITS, useFormatterWithUnit, } from "@features/indicators/utils"; -import { precise } from "@shared/lib/utils"; import type { NumericKeys } from "@shared/types"; import type { BiodiversityData } from "./types"; +import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; const indicatorKeys: NumericKeys[] = [ "biomass_volume", @@ -25,22 +26,10 @@ const indicatorKeys: NumericKeys[] = [ "bio_idx_microhabitats", ]; -const formatRelativeAbundance = ( - relativeAbundance: BiodiversityData["relative_abundance"], - treePop: number, -) => - Object.entries(relativeAbundance).map( - ([key, value]) => - [key, Number(precise((Number(value) * 100) / treePop))] as [ - string, - number, - ], - ); - /** * Return data in a convenient way for UI rendering, handling units and fixing */ -export const useFormatBiodiversityData = (data: BiodiversityData) => { +export const useFormatBiodiversityData = (data: ForestInventoryData) => { const { t } = useTranslation("common"); const { formatWithUnit } = useFormatterWithUnit(); @@ -59,9 +48,10 @@ export const useFormatBiodiversityData = (data: BiodiversityData) => { volume: formatWithUnit(safeData.biomass_volume, UNITS.tonPerHectare), }, treeDiversity: { - relative_abundance: formatRelativeAbundance( + relative_abundance: convertDictToPercentage( data.relative_abundance, data.tree_pop, + "0", ), speciesRichness: formatWithUnit(safeData.richness, UNITS.essenceCount), tree_pop: data.tree_pop, diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index a30677d1..8eca4fa8 100644 --- a/webapp/src/features/indicators/soil/format-data.ts +++ b/webapp/src/features/indicators/soil/format-data.ts @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { computeScore, + convertDictToPercentage, preciseNumericIndicators, UNITS, useFormatterWithUnit, @@ -11,6 +12,7 @@ import type { ForestInventoryData } from "@features/popup/forest-inventory"; import type { NumericKeys } from "@shared/types"; import type { SoilData } from "./types"; +import type { ForestInventoryData } from "@features/popup/forest-inventory"; const indicatorsToPreciseWithFallBack: NumericKeys[] = [ "soil_structure_idx", @@ -25,6 +27,21 @@ const indicatorsToPreciseWithoutFallBack: NumericKeys[] = [ "soil_eros_water_infiltration", ] as const; +function formatTaxonAbundance(abundancePop: string[], abundanceTotal: number) { + if (!abundanceTotal) { + return {}; + } + + let abundancePopRecord: Record = {}; + abundancePop.forEach((value) => { + const [ taxon, count ] = value.split(':'); + const currentCount = abundancePopRecord[taxon] || 0; + abundancePopRecord[taxon] = currentCount + parseInt(count, 10); + }); + + return convertDictToPercentage(abundancePopRecord, abundanceTotal, "0"); +} + /** * Return data in a convenient way for UI rendering, handling units and fixing */ @@ -51,12 +68,22 @@ export const useFormatSoilData = (data: ForestInventoryData) => { indicatorsToPreciseWithFallBack, t("dataManagement.noData"), ); + + safeData.soil_surface_fauna_abundance = convertDictToPercentage(safeData.soil_surface_fauna_abundance, Object.values(safeData.soil_surface_fauna_abundance || {}).reduce((a, b) => a + b, 0), "0") /*formatTaxonAbundance( + safeData.soil_surface_fauna_abundance_pop, + safeData.soil_surface_fauna_total_pop + )*/ + + safeData.soil_fauna_abundance = formatTaxonAbundance( + safeData.soil_fauna_abundance_pop, + safeData.soil_fauna_total_pop + ) return { ...safeData, soil_eros_rainfall: Number(soil_eros_rainfall_and_wind.split("-")[0]), soil_eros_stability: soil_eros_stability, - soil_eros_water_infiltration: computeScore(soil_eros_water_infiltration), + soil_eros_water_infiltration: computeScore(Number(soil_eros_water_infiltration)), soil_eros_wind: Number(soil_eros_rainfall_and_wind.split("-")[1]), soil_fauna_abundance: safeData.soil_fauna_abundance, soil_fauna_density: formatWithUnit( diff --git a/webapp/src/features/indicators/soil/types.ts b/webapp/src/features/indicators/soil/types.ts index 04416841..4e9d0899 100644 --- a/webapp/src/features/indicators/soil/types.ts +++ b/webapp/src/features/indicators/soil/types.ts @@ -12,7 +12,11 @@ export type SoilData = { soil_fauna_density: number; soil_fauna_diversity: number; soil_fauna_abundance: any; + soil_fauna_abundance_pop: string[]; + soil_fauna_total_pop: number; soil_surface_fauna_density: number; soil_surface_fauna_diversity: number; soil_surface_fauna_abundance: any; + soil_surface_fauna_abundance_pop: string[]; + soil_surface_fauna_total_pop: number; }; diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index 5efb824b..e3ff2fdb 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -27,45 +27,48 @@ export type Unit = keyof typeof UNITS; * Return a function that appends the correct internationalized unit based on the `unit` input. */ export const useFormatterWithUnit = () => { - const { t } = useTranslation("all4trees"); + const { t } = useTranslation(["common", "all4trees"]); function formatWithUnit( value: number | string | null | undefined, unit: Unit, ): string | null { - if (value == null) { - return null; + const noDataStr = t("dataManagement.noData", { ns: "common" }); + if (value == null || value === noDataStr) { + return noDataStr; } const formattedValue = typeof value === "number" ? precise(value) : value; switch (unit) { case UNITS.individualPerCubicMeter: - return t("indicators.units.individualPerCubicMeter", { value }); + return t("indicators.units.individualPerCubicMeter", { value, ns: "all4trees" }); case UNITS.individualPerHectare: - return t("indicators.units.individualPerHectare", { value }); + return t("indicators.units.individualPerHectare", { value, ns: "all4trees" }); case UNITS.individualPerTrap: - return t("indicators.units.individualPerTrap", { value }); + return t("indicators.units.individualPerTrap", { value, ns: "all4trees" }); case UNITS.speciesCount: return t("indicators.units.speciesCount", { count: parseInt(formattedValue, 10), + ns: "all4trees" }); case UNITS.essenceCount: return t("indicators.units.essenceCount", { count: parseInt(formattedValue, 10), + ns: "all4trees" }); case UNITS.tonPerHectare: - return t("indicators.units.tonPerHectare", { value }); + return t("indicators.units.tonPerHectare", { value, ns: "all4trees" }); case UNITS.m3PerHabPerYear: - return t("indicators.units.m3PerHabPerYear", { value }); + return t("indicators.units.m3PerHabPerYear", { value, ns: "all4trees" }); case UNITS.monthPerYear: - return t("indicators.units.monthPerYear", { value }); + return t("indicators.units.monthPerYear", { value, ns: "all4trees" }); case UNITS.percentFoodRequirements: - return t("indicators.units.percentFoodRequirements", { value }); + return t("indicators.units.percentFoodRequirements", { value, ns: "all4trees" }); case UNITS.minPerHouseholdPerDay: - return t("indicators.units.minPerHhPerDay", { value }); + return t("indicators.units.minPerHhPerDay", { value, ns: "all4trees" }); default: - return null; + return noDataStr; } } @@ -79,11 +82,12 @@ export function preciseNumericIndicators>( ): T { return Object.fromEntries( Object.entries(data).map(([key, value]) => [ - key, - indicatorKeys.includes(key as (typeof indicatorKeys)[number]) - ? precise(Number(value), defaultValue) - : (value ?? defaultValue), - ]), + key, + indicatorKeys.includes(key as (typeof indicatorKeys)[number]) + ? precise(Number(value), defaultValue) + : value, // Keep the original value if it's not in the list of indicator keys + ] + ), ) as T; } @@ -92,6 +96,9 @@ export function convertDictToPercentage( total: number, defaultValue: string, ): Record { + if (!data) { + return {}; + } return Object.fromEntries( Object.entries(data).map(([key, value]) => [ key, diff --git a/webapp/src/shared/lib/utils.ts b/webapp/src/shared/lib/utils.ts index 438e42ec..31c2c6cc 100644 --- a/webapp/src/shared/lib/utils.ts +++ b/webapp/src/shared/lib/utils.ts @@ -8,7 +8,8 @@ export function cn(...inputs: ClassValue[]) { } export function precise(value?: number | null, defaultValue: string = "0") { - if (!value || Number.isNaN(value)) { + // !value is true is value === 0. + if (value === null || value === undefined || Number.isNaN(value)) { return defaultValue; } if (value > 999) { From ff6983234741b2609523f4d33c51dcf4c8abbb75 Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Wed, 1 Jul 2026 13:00:21 +0200 Subject: [PATCH 2/9] Fix lint --- .../biodiversity/chart-relative-abundance.tsx | 5 ++- .../indicators/biodiversity/format-data.ts | 2 +- .../features/indicators/soil/format-data.ts | 24 ++++++---- webapp/src/features/indicators/utils.ts | 44 ++++++++++++------- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx index 5f06761e..5d4e56ad 100644 --- a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx +++ b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx @@ -23,7 +23,10 @@ export const ChartRelativeAbundance: ChartComponentType = ({ .filter((value) => value < 5) .reduce((acc, value) => acc + value, 0); const chartData = Object.entries(data) - .filter(([name, value]) => name !== "0" && (Object.keys(data).length < 6 || value >= 5)) + .filter( + ([name, value]) => + name !== "0" && (Object.keys(data).length < 6 || value >= 5), + ) .map(([name, value], index) => ({ fill: `var(--chart-${(index % 5) + 1})`, name, diff --git a/webapp/src/features/indicators/biodiversity/format-data.ts b/webapp/src/features/indicators/biodiversity/format-data.ts index 85a1b56e..86430208 100644 --- a/webapp/src/features/indicators/biodiversity/format-data.ts +++ b/webapp/src/features/indicators/biodiversity/format-data.ts @@ -6,11 +6,11 @@ import { UNITS, useFormatterWithUnit, } from "@features/indicators/utils"; +import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; import type { NumericKeys } from "@shared/types"; import type { BiodiversityData } from "./types"; -import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; const indicatorKeys: NumericKeys[] = [ "biomass_volume", diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index 8eca4fa8..f6c238ee 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 { ForestInventoryData } from "@features/popup/forest-inventory"; import type { NumericKeys } from "@shared/types"; import type { SoilData } from "./types"; -import type { ForestInventoryData } from "@features/popup/forest-inventory"; const indicatorsToPreciseWithFallBack: NumericKeys[] = [ "soil_structure_idx", @@ -32,9 +31,9 @@ function formatTaxonAbundance(abundancePop: string[], abundanceTotal: number) { return {}; } - let abundancePopRecord: Record = {}; + const abundancePopRecord: Record = {}; abundancePop.forEach((value) => { - const [ taxon, count ] = value.split(':'); + const [taxon, count] = value.split(":"); const currentCount = abundancePopRecord[taxon] || 0; abundancePopRecord[taxon] = currentCount + parseInt(count, 10); }); @@ -68,22 +67,31 @@ export const useFormatSoilData = (data: ForestInventoryData) => { indicatorsToPreciseWithFallBack, t("dataManagement.noData"), ); - - safeData.soil_surface_fauna_abundance = convertDictToPercentage(safeData.soil_surface_fauna_abundance, Object.values(safeData.soil_surface_fauna_abundance || {}).reduce((a, b) => a + b, 0), "0") /*formatTaxonAbundance( + + safeData.soil_surface_fauna_abundance = convertDictToPercentage( + safeData.soil_surface_fauna_abundance, + Object.values(safeData.soil_surface_fauna_abundance || {}).reduce( + (a, b) => a + b, + 0, + ), + "0", + ); /*formatTaxonAbundance( safeData.soil_surface_fauna_abundance_pop, safeData.soil_surface_fauna_total_pop )*/ safeData.soil_fauna_abundance = formatTaxonAbundance( safeData.soil_fauna_abundance_pop, - safeData.soil_fauna_total_pop - ) + safeData.soil_fauna_total_pop, + ); return { ...safeData, soil_eros_rainfall: Number(soil_eros_rainfall_and_wind.split("-")[0]), soil_eros_stability: soil_eros_stability, - soil_eros_water_infiltration: computeScore(Number(soil_eros_water_infiltration)), + soil_eros_water_infiltration: computeScore( + Number(soil_eros_water_infiltration), + ), soil_eros_wind: Number(soil_eros_rainfall_and_wind.split("-")[1]), soil_fauna_abundance: safeData.soil_fauna_abundance, soil_fauna_density: formatWithUnit( diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index e3ff2fdb..51643aab 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -42,31 +42,46 @@ export const useFormatterWithUnit = () => { switch (unit) { case UNITS.individualPerCubicMeter: - return t("indicators.units.individualPerCubicMeter", { value, ns: "all4trees" }); + return t("indicators.units.individualPerCubicMeter", { + ns: "all4trees", + value, + }); case UNITS.individualPerHectare: - return t("indicators.units.individualPerHectare", { value, ns: "all4trees" }); + return t("indicators.units.individualPerHectare", { + ns: "all4trees", + value, + }); case UNITS.individualPerTrap: - return t("indicators.units.individualPerTrap", { value, ns: "all4trees" }); + return t("indicators.units.individualPerTrap", { + ns: "all4trees", + value, + }); case UNITS.speciesCount: return t("indicators.units.speciesCount", { count: parseInt(formattedValue, 10), - ns: "all4trees" + ns: "all4trees", }); case UNITS.essenceCount: return t("indicators.units.essenceCount", { count: parseInt(formattedValue, 10), - ns: "all4trees" + ns: "all4trees", }); case UNITS.tonPerHectare: - return t("indicators.units.tonPerHectare", { value, ns: "all4trees" }); + return t("indicators.units.tonPerHectare", { ns: "all4trees", value }); case UNITS.m3PerHabPerYear: - return t("indicators.units.m3PerHabPerYear", { value, ns: "all4trees" }); + return t("indicators.units.m3PerHabPerYear", { + ns: "all4trees", + value, + }); case UNITS.monthPerYear: - return t("indicators.units.monthPerYear", { value, ns: "all4trees" }); + return t("indicators.units.monthPerYear", { ns: "all4trees", value }); case UNITS.percentFoodRequirements: - return t("indicators.units.percentFoodRequirements", { value, ns: "all4trees" }); + return t("indicators.units.percentFoodRequirements", { + ns: "all4trees", + value, + }); case UNITS.minPerHouseholdPerDay: - return t("indicators.units.minPerHhPerDay", { value, ns: "all4trees" }); + return t("indicators.units.minPerHhPerDay", { ns: "all4trees", value }); default: return noDataStr; } @@ -82,12 +97,11 @@ export function preciseNumericIndicators>( ): T { return Object.fromEntries( Object.entries(data).map(([key, value]) => [ - key, - indicatorKeys.includes(key as (typeof indicatorKeys)[number]) - ? precise(Number(value), defaultValue) + key, + indicatorKeys.includes(key as (typeof indicatorKeys)[number]) + ? precise(Number(value), defaultValue) : value, // Keep the original value if it's not in the list of indicator keys - ] - ), + ]), ) as T; } From c69935ee18be2d925cecea74d276d23a55ccc3f9 Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Thu, 2 Jul 2026 22:03:33 +0200 Subject: [PATCH 3/9] Fix soil structure idx --- backend/configs/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/configs/config.json b/backend/configs/config.json index f592a8d8..e0b55566 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -67,7 +67,7 @@ "bio_idx_vertical_distribution": "gini(adu.hgt_perp) * 10", "bio_idx_dominant_height": "adu.avg(hgt_perp if hgt_perp > percentile(hgt_perp, 80)) * 10 / 40", "bio_idx_microhabitats": "adu.list(trem).flatten().list_unique() * 10 / trem_group_max", - "soil_structure_idx": "(thk1*float(not1) + thk2*float(not2) + thk3*float(not3) + thk4*float(not4) + thk5*float(not5)) / (thk1 + thk2 + thk3 + thk4 + thk5) * 2", + "soil_structure_idx": "(thk1*float(not1) + thk2*float(not2) + thk3*float(not3) + thk4*float(not4) + thk5*float(not5)) / (thk1 + thk2 + thk3 + thk4 + thk5)", "soil_composition": "count(1)", "soil_eros_rainfall_and_wind": "for_weath.concat_ws('-', rain, wind)", "soil_eros_slope": "int(10 - slop if slop < 10 else 0)", From b5a1515234236ab442d07ca71a2b140ef880cb35 Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Tue, 7 Jul 2026 10:18:59 +0200 Subject: [PATCH 4/9] Remove soil composition indicator --- backend/configs/config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/configs/config.json b/backend/configs/config.json index e0b55566..21f3312a 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -68,7 +68,6 @@ "bio_idx_dominant_height": "adu.avg(hgt_perp if hgt_perp > percentile(hgt_perp, 80)) * 10 / 40", "bio_idx_microhabitats": "adu.list(trem).flatten().list_unique() * 10 / trem_group_max", "soil_structure_idx": "(thk1*float(not1) + thk2*float(not2) + thk3*float(not3) + thk4*float(not4) + thk5*float(not5)) / (thk1 + thk2 + thk3 + thk4 + thk5)", - "soil_composition": "count(1)", "soil_eros_rainfall_and_wind": "for_weath.concat_ws('-', rain, wind)", "soil_eros_slope": "int(10 - slop if slop < 10 else 0)", "soil_eros_cover": "veg", From 5c6d3ec9ebcee4be517c6c41c29c2ad37521341d Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Tue, 7 Jul 2026 19:47:48 +0200 Subject: [PATCH 5/9] Fix cathalijne comments --- backend/configs/config.json | 25 ++++++----- .../features/indicators/soil/format-data.ts | 41 +++++++------------ webapp/src/features/indicators/utils.ts | 2 +- .../popup-forest-inventory.tsx | 2 +- 4 files changed, 28 insertions(+), 42 deletions(-) diff --git a/backend/configs/config.json b/backend/configs/config.json index 21f3312a..3be69a68 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -51,6 +51,7 @@ "geom": "gps.merge().centroid()", "for": "loc2", "cod": "cod", + "isPublic": "conf", "ecos": "ecos", "taille_placette": "20^2*pi()/10000", "project": "proj", @@ -65,21 +66,22 @@ "bio_idx_spatial_distribution": "adu.categorical_gini(area) * 10", "bio_idx_diametric_distribution": "adu.gini(meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10) * 10", "bio_idx_vertical_distribution": "gini(adu.hgt_perp) * 10", - "bio_idx_dominant_height": "adu.avg(hgt_perp if hgt_perp > percentile(hgt_perp, 80)) * 10 / 40", - "bio_idx_microhabitats": "adu.list(trem).flatten().list_unique() * 10 / trem_group_max", + "percentile_80": "adu.list(percentile(hgt_perp, 80))", + "bio_idx_dominant_height": "adu.avg(hgt_perp if hgt_perp >= percentile(hgt_perp, 80)) * 10 / 40", + "bio_idx_microhabitats": "adu.list(trem).flatten().filter( x -> x != 'None').list_unique() * 10 / trem_group_max", "soil_structure_idx": "(thk1*float(not1) + thk2*float(not2) + thk3*float(not3) + thk4*float(not4) + thk5*float(not5)) / (thk1 + thk2 + thk3 + thk4 + thk5)", "soil_eros_rainfall_and_wind": "for_weath.concat_ws('-', rain, wind)", "soil_eros_slope": "int(10 - slop if slop < 10 else 0)", "soil_eros_cover": "veg", - "soil_eros_stability": "(int(slak1) + int(slak2) + int(slak3)) / 3", - "soil_eros_water_infiltration": "600 * 300 / ( (epoch(interval(beer1)) + epoch(interval(beer2)) + epoch(interval(beer3)) + epoch(interval(beer4)) + epoch(interval(beer5)) + epoch(interval(beer6)) + epoch(interval(beer7)) + epoch(interval(beer8)) + epoch(interval(beer9)) + epoch(interval(beer10)) ) / 10 * pi() * 9^2) if beer1 != '' else 0", + "soil_eros_stability": "((int(slak1) + int(slak2) + int(slak3) ) / 3 if slak1 != '' )", + "soil_eros_water_infiltration": "600 * 300 / ( (epoch(interval(beer1)) + ( epoch(interval(beer2)) if beer2!='' ) + ( epoch(interval(beer3)) if beer3!='' ) + ( epoch(interval(beer4)) if beer4!='' ) + ( epoch(interval(beer5)) if beer5!='' ) + ( epoch(interval(beer6)) if beer6!='' ) + ( epoch(interval(beer7)) if beer7!='' ) + ( epoch(interval(beer8)) if beer8!='' ) + ( epoch(interval(beer9)) if beer9!='' ) + ( epoch(interval(beer10)) if beer10!='' ) ) / non_null_count(beer1, beer2, beer3, beer4, beer5, beer6, beer7, beer8, beer9, beer10) * pi() * 9^2) if beer1 != ''", "soil_fauna_density": "sum(tsbf_001.tsbf_no) / tsbf_samp", - "soil_fauna_diversity": "list_unique(list(nullif(tsbf_001.concat_ws('-', tsbf_tax1, tsbf_tax2, tsbf_tax3), '')))", + "soil_fauna_diversity": "count(tsbf_001.tsbf_tax1)", "soil_fauna_abundance": "list_aggregate(list(nullif(tsbf_001.concat_ws('-', tsbf_tax1, tsbf_tax2, tsbf_tax3), '')), 'histogram')", "soil_fauna_abundance_pop": "list_concat(list(tsbf_001.concat_ws(':', concat_ws('-',tsbf_tax1, tsbf_tax2, tsbf_tax3), tsbf_no) if tsbf_001.tsbf_no > 0))", "soil_fauna_total_pop": "int(sum(tsbf_001.tsbf_no))", "soil_surface_fauna_density": "(sum(barba_001.barbA_no) + sum(barbb_001.barbB_no) + sum(barbc_001.barbC_no) + sum(barbd_001.barbD_no)) / barb_samp", - "soil_surface_fauna_diversity": "list_unique(list_concat(list(nullif(barba_001.concat_ws('-', barbA_tax1, barbA_tax2, barbA_tax3), '')), list(nullif(barbb_001.concat_ws('-', barbB_tax1, barbB_tax2, barbB_tax3), '')), list(nullif(barbc_001.concat_ws('-', barbC_tax1, barbC_tax2, barbC_tax3), '')), list(nullif(barbd_001.concat_ws('-', barbD_tax1, barbD_tax2, barbD_tax3), ''))))", + "soil_surface_fauna_diversity": "(count(barba_001.barbA_no) + count(barbb_001.barbB_no) + count(barbc_001.barbC_no) + count(barbd_001.barbD_no) ) / 4", "soil_surface_fauna_abundance": "list_aggregate(list_concat(list(nullif(barba_001.concat_ws('-', barbA_tax1, barbA_tax2, barbA_tax3), '')), list(nullif(barbb_001.concat_ws('-', barbB_tax1, barbB_tax2, barbB_tax3), '')), list(nullif(barbc_001.concat_ws('-', barbC_tax1, barbC_tax2, barbC_tax3), '')), list(nullif(barbd_001.concat_ws('-', barbD_tax1, barbD_tax2, barbD_tax3), ''))), 'histogram')", "soil_surface_fauna_abundance_pop": "list_concat(list(barba_001.concat_ws(':', concat_ws('-',barbA_tax1, barbA_tax2, barbA_tax3), barbA_no) if barba_001.barbA_no > 0), list(barbb_001.concat_ws(':', concat_ws('-',barbB_tax1, barbB_tax2, barbB_tax3), barbB_no) if barbb_001.barbB_no > 0), list(barbc_001.concat_ws(':', concat_ws('-',barbC_tax1, barbC_tax2, barbC_tax3), barbC_no) if barbc_001.barbC_no > 0), list(barbd_001.concat_ws(':', concat_ws('-',barbD_tax1, barbD_tax2, barbD_tax3), barbD_no) if barbd_001.barbD_no > 0))", "soil_surface_fauna_total_pop": "int(sum(barba_001.barbA_no) + sum(barbb_001.barbB_no) + sum(barbc_001.barbC_no) + sum(barbd_001.barbD_no))" @@ -107,7 +109,7 @@ "population": "sum(res)", "fuel_sources": "list_aggregate(flatten(list(ener)), 'histogram')", "wood_fuel_cons": "sum( 12 * ( ifnull(fw_qty * enq_fw.fw_vol * fw_freq, 0) + ifnull(coal_qty * enq_coal.coal_kg * coal_freq * enq_coal.coal_fw, 0) / enq_coal.coal_dens )) / sum(res)", - "firewood_collec_time": "ifnull(avg(coll_freq * coll_dur)/30, 0)", + "firewood_collec_time": "avg(ifnull(coll_freq, 0) * ifnull(coll_dur, 0) / 30)", "firewood_satis": "histogram(wf_satis if int(wf_satis) > 0)", "firewood_satis_tot": "count(1 if int(wf_satis) > 0)", "firewood_satis1": "count(1 if wf_satis = '1') / count(1 if int(wf_satis) > 0) * 100", @@ -125,16 +127,13 @@ "lean_period": "avg(len(lean) if lean_yn = 1)", "food_diversity": "list_aggregate(flatten(list(food_cons)), 'histogram')", "food_diversity_score": "avg(list_unique(food_cons) / int(food_cons_max) * 10)", - "food_self_suff_score": "avg(list_unique(food_self) / list_unique(food_cons) * 100)", + "food_self_suff_score": "avg(ifnull(list_unique(food_self), 0) / list_unique(food_cons) * 100)", "living_cond_perception": "histogram(cond_evol)", "revenue_change": "avg(0 - inc_dif if inc_evol = '2' else ifnull(inc_dif, 0))", - "assets_idx": "10 / int(ass_tot_max) * avg( ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + list_unique(ass) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + int( land_own ) + int( land_titl ) + int( land_size ) ) if ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + list_unique(ass) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + int( land_own ) + int( land_titl ) + int( land_size ) ) <= int(ass_tot_max) )", + "assets_idx_raw": "avg( ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + ifnull(list_unique(ass),0) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + ( int( land_own ) + int( land_titl ) + int( land_size ) if int(land_own) > 0 else 0) ) if ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + list_unique(ass) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + int( land_own ) + int( land_titl ) + int( land_size ) ) <= int(ass_tot_max) )", + "assets_idx": "10 / int(ass_tot_max) * avg( ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + ifnull(list_unique(ass),0) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + ( int( land_own ) + int( land_titl ) + int( land_size ) if int(land_own) > 0 else 0) ) if ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + list_unique(ass) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + int( land_own ) + int( land_titl ) + int( land_size ) ) <= int(ass_tot_max) )", "nb_income_sources" : "avg(int(inc_no))", "nb_additional_incomes": "'N/A'", - "sector_benef": "'N/A'", - "sector_support_amount": "'N/A'", - "nb_improved_skills": "'N/A'", - "stak_typ": "'N/A'", "benef_pract1": "count(1 if int(pract1) = 2) / count(1) * 100", "benef_pract2": "count(1 if int(pract2) = 2) / count(1) * 100", "benef_pract3": "count(1 if int(pract3) = 2) / count(1) * 100", diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index f6c238ee..967cb2dd 100644 --- a/webapp/src/features/indicators/soil/format-data.ts +++ b/webapp/src/features/indicators/soil/format-data.ts @@ -17,13 +17,8 @@ const indicatorsToPreciseWithFallBack: NumericKeys[] = [ "soil_structure_idx", "soil_fauna_density", "soil_surface_fauna_density", -] as const; - -// Radar indicators does not need a fallback value other than 0. -// Only 2 of them needs precision, the others are manually set in the form as integer between 0 and 10. -const indicatorsToPreciseWithoutFallBack: NumericKeys[] = [ - "soil_eros_stability", "soil_eros_water_infiltration", + "soil_eros_stability", ] as const; function formatTaxonAbundance(abundancePop: string[], abundanceTotal: number) { @@ -48,12 +43,6 @@ export const useFormatSoilData = (data: ForestInventoryData) => { const { t } = useTranslation("common"); const { formatWithUnit } = useFormatterWithUnit(); - const { soil_eros_stability, soil_eros_water_infiltration } = - preciseNumericIndicators( - data, - indicatorsToPreciseWithoutFallBack, - ); - const { soil_structure_idx, soil_fauna_diversity, @@ -61,6 +50,8 @@ export const useFormatSoilData = (data: ForestInventoryData) => { soil_surface_fauna_diversity, soil_surface_fauna_density, soil_eros_rainfall_and_wind, + soil_eros_stability, + soil_eros_water_infiltration, ...safeData } = preciseNumericIndicators( data, @@ -68,17 +59,10 @@ export const useFormatSoilData = (data: ForestInventoryData) => { t("dataManagement.noData"), ); - safeData.soil_surface_fauna_abundance = convertDictToPercentage( - safeData.soil_surface_fauna_abundance, - Object.values(safeData.soil_surface_fauna_abundance || {}).reduce( - (a, b) => a + b, - 0, - ), - "0", - ); /*formatTaxonAbundance( + safeData.soil_surface_fauna_abundance = formatTaxonAbundance( safeData.soil_surface_fauna_abundance_pop, - safeData.soil_surface_fauna_total_pop - )*/ + safeData.soil_surface_fauna_total_pop, + ); safeData.soil_fauna_abundance = formatTaxonAbundance( safeData.soil_fauna_abundance_pop, @@ -89,9 +73,10 @@ export const useFormatSoilData = (data: ForestInventoryData) => { ...safeData, soil_eros_rainfall: Number(soil_eros_rainfall_and_wind.split("-")[0]), soil_eros_stability: soil_eros_stability, - soil_eros_water_infiltration: computeScore( - Number(soil_eros_water_infiltration), - ), + soil_eros_water_infiltration: + String(soil_eros_water_infiltration) === t("dataManagement.noData") + ? soil_eros_water_infiltration + : computeScore(soil_eros_water_infiltration), soil_eros_wind: Number(soil_eros_rainfall_and_wind.split("-")[1]), soil_fauna_abundance: safeData.soil_fauna_abundance, soil_fauna_density: formatWithUnit( @@ -102,8 +87,10 @@ export const useFormatSoilData = (data: ForestInventoryData) => { soil_fauna_diversity, UNITS.speciesCount, ), - // Temporary fix due to coordo bug - soil_structure_idx: `${soil_structure_idx}/10`, + soil_structure_idx: + String(soil_structure_idx) === t("dataManagement.noData") + ? soil_structure_idx + : `${soil_structure_idx}/10`, surface_fauna_abundance: safeData.soil_surface_fauna_abundance, surface_fauna_density: formatWithUnit( soil_surface_fauna_density, diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index 51643aab..c3822ff7 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -99,7 +99,7 @@ export function preciseNumericIndicators>( Object.entries(data).map(([key, value]) => [ key, indicatorKeys.includes(key as (typeof indicatorKeys)[number]) - ? precise(Number(value), defaultValue) + ? precise(value, defaultValue) : value, // Keep the original value if it's not in the list of indicator keys ]), ) as T; 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 1eed5c83..a032d10b 100644 --- a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx +++ b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx @@ -43,7 +43,7 @@ export const ForestInventoryPopupContent: FC< }; const title = t("popup.forestInventory.title", { - id: data.id, + id: data.cod, ns: "all4trees", }); From 693d37aaed5affd4c5e3e7528440bddccdebc4e1 Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Wed, 8 Jul 2026 09:45:56 +0200 Subject: [PATCH 6/9] Fix sunburst colors --- webapp/package-lock.json | 7 +++ webapp/package.json | 1 + webapp/src/app/styles/all4trees.css | 8 +-- .../src/features/charts/soil/lib/sunburst.ts | 58 +++++++++---------- webapp/src/features/charts/soil/types.ts | 3 +- .../charts/soil/ui/chart-taxon-abundance.tsx | 4 +- webapp/src/shared/lib/palette.ts | 2 - 7 files changed, 42 insertions(+), 41 deletions(-) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index afa5ddb9..7f597633 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -23,6 +23,7 @@ "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "color2k": "^2.0.4", "coordo": "github:dataforgoodfr/Coordonnees#0.6.0", "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", @@ -4559,6 +4560,12 @@ "integrity": "sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==", "license": "Unlicense" }, + "node_modules/color2k": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.4.tgz", + "integrity": "sha512-OXAPGFRNeLFnUfqDtloYdxkwsJoIdXe28+bjbpJiPqyei2HPa3VHmMCWa0Qe62+U4Ftf9Hj7hRssOkxz7WiWbg==", + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", diff --git a/webapp/package.json b/webapp/package.json index c81d006d..29ff587f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -40,6 +40,7 @@ "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "color2k": "^2.0.4", "coordo": "github:dataforgoodfr/Coordonnees#0.6.0", "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", diff --git a/webapp/src/app/styles/all4trees.css b/webapp/src/app/styles/all4trees.css index a9a79085..993e44a2 100644 --- a/webapp/src/app/styles/all4trees.css +++ b/webapp/src/app/styles/all4trees.css @@ -97,10 +97,10 @@ --ring: var(--a4t-color-vert-kelly); --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-2: var(--a4t-color-bleu); + --chart-3: var(--a4t-color-citrouille); + --chart-4: var(--a4t-color-vert-de-gris); + --chart-5: #895bf5; --chart-6: var(--a4t-color-onyx); --sidebar: var(--a4t-color-alabaster); diff --git a/webapp/src/features/charts/soil/lib/sunburst.ts b/webapp/src/features/charts/soil/lib/sunburst.ts index 3a5f3b57..04684fbf 100644 --- a/webapp/src/features/charts/soil/lib/sunburst.ts +++ b/webapp/src/features/charts/soil/lib/sunburst.ts @@ -1,3 +1,5 @@ +import { darken, lighten } from "color2k"; + import type { ExternalData } from "@features/popup/forest-inventory/types"; import { getChartPalette } from "@shared/lib/palette"; @@ -10,59 +12,51 @@ export function buildSunburstNodes( metadata: ExternalData, project: string, ): SunburstNode[] { - const nodes: SunburstNode[] = []; - const seen = new Set(); - + const nodes = new Map(); for (const [key, value] of dataEntries) { const parts = key.split("-"); - nodes.push({ + nodes.set(key, { + depth: parts.length - 1, id: key, label: formatTaxonLevelLabel(key, metadata, project), parent: parts.slice(0, -1).join("-") || "", value, }); - seen.add(key); - for (let i = 0; i < parts.length - 1; i++) { - const parentPath = parts.slice(0, i + 1).join("-"); - if (!seen.has(parentPath)) { - nodes.push({ + for (let i = 1; i < parts.length; i++) { + const parentPath = parts.slice(0, i).join("-"); + const parentNode = nodes.get(parentPath); + if (parentNode) { + parentNode.value += value; + nodes.set(parentPath, parentNode); + } else { + nodes.set(parentPath, { + depth: parentPath.split("-").length - 1, id: parentPath, label: formatTaxonLevelLabel(parentPath, metadata, project), - parent: parts.slice(0, i).join("-") || "", - value: 0, + parent: parts.slice(0, i - 1).join("-") || "", + value: value, }); - seen.add(parentPath); } } } - const nodeMap = new Map(nodes.map((n) => [n.id, n])); - nodes.forEach((node) => { - if (node.parent) { - const parent = nodeMap.get(node.parent); - if (parent) parent.value += node.value; - } - }); - - return nodes; + return Array.from(nodes.values()); } export const getLevelPalettes = () => { const palette = getChartPalette(); - - return [ - palette.slice(0, 3), - [palette[3], palette[4], palette[0]], - [palette[1], palette[2], palette[3]], - ]; + const lvl1Palette = palette.map((color) => darken(color, 0.1)); + const lvl3Palette = palette.map((color) => lighten(color, 0.1)); + return [lvl1Palette, palette, lvl3Palette]; }; export function buildNodeColors(nodes: SunburstNode[]) { const palettes = getLevelPalettes(); - return nodes.map((node, index) => { - const depth = node.id.split("-").length; - const palette = palettes[depth - 1] ?? palettes[0]; - return palette[index % palette.length]; - }); + return nodes + .sort((a, b) => a.depth - b.depth) + .map((node, index) => { + const palette = palettes[node.depth]; + return palette[index % palette.length]; + }); } diff --git a/webapp/src/features/charts/soil/types.ts b/webapp/src/features/charts/soil/types.ts index 666a2a62..c9b68d3d 100644 --- a/webapp/src/features/charts/soil/types.ts +++ b/webapp/src/features/charts/soil/types.ts @@ -11,6 +11,7 @@ export type SunburstNode = { label: string; parent: string; value: number; + depth: number; }; export type SunburstTrace = { @@ -23,7 +24,7 @@ export type SunburstTrace = { labels: string[]; parents: string[]; values: number[]; - marker: { colors: string[]; line: { width: number } }; + marker: { colors: string[]; line: { width: number; color: string } }; leaf: { opacity: number }; sunburstcolorway: string[]; }; 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 a20f214a..96c817d7 100644 --- a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx +++ b/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx @@ -44,8 +44,8 @@ export const ChartTaxonAbundance: ChartComponentType = ({ hovertext: hoverText, ids: nodes.map((n) => n.id), labels: nodes.map((n) => n.label), - leaf: { opacity: 0.4 }, - marker: { colors: nodeColors, line: { width: 2 } }, + leaf: { opacity: 1 }, + marker: { colors: nodeColors, line: { color: "#fff", width: 2 } }, parents: nodes.map((n) => n.parent), sunburstcolorway: nodeColors, type: "sunburst", diff --git a/webapp/src/shared/lib/palette.ts b/webapp/src/shared/lib/palette.ts index ff7b4bc6..8411113e 100644 --- a/webapp/src/shared/lib/palette.ts +++ b/webapp/src/shared/lib/palette.ts @@ -11,6 +11,4 @@ export const getChartPalette = () => [ getCssVarColor("--chart-2", "#f98038"), getCssVarColor("--chart-3", "#2d6db4"), getCssVarColor("--chart-4", "#895bf5"), - getCssVarColor("--chart-5", "#f04646"), - getCssVarColor("--chart-6", "#424242"), ]; From 07d594dade47cd912dce2d6087791ae51e13842b Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Thu, 9 Jul 2026 13:01:56 +0200 Subject: [PATCH 7/9] Fix spatial distribution + minor bugs --- backend/configs/config.json | 7 ++++--- backend/requirements.txt | 2 +- webapp/src/features/indicators/soil/format-data.ts | 8 +++++--- webapp/src/shared/i18n/translations/en/all4trees.json | 2 +- webapp/src/shared/i18n/translations/fr/all4trees.json | 6 +++--- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/configs/config.json b/backend/configs/config.json index 3be69a68..3153b75e 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -60,13 +60,14 @@ "tree_density": "count(1 if adu.cond = 1) / (20^2 * pi() / 10000)", "richness": "count(unique(adu.adu_sp))", "relative_abundance": "histogram(adu.adu_sp)", + "deadwoord": "adu.count(1 if cond=2 or cond=3)", "bio_idx_tree_density": "count(1 if adu.cond = 1) / (20^2 * pi() / 10000) / 100", - "bio_idx_deadWood": "((adu.sum(0.0673 * (for_dw.dens * (meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10)^2 * hgt_perp)^0.976 if cond = 2) + adu.sum(pi() / 3 * ((meas_b / 2)^2 + ((meas_e / 2)^2 + meas_b / 2 + meas_e / 2) * lght * for_dw.dens) if cond = 3)) / adu.sum(0.0673 * (for_sp.dens * (meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10)^2 * hgt_perp)^0.976 if cond = 1)) * 10", + "bio_idx_deadWood": "((adu.sum(0.0673 * (for_dw.dens * (meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10)^2 * hgt_perp)^0.976 if cond = 2) + adu.sum(pi() / 3 * ((meas_b / 2)^2 + ((meas_e / 2)^2 + meas_b / 2 + meas_e / 2) * lght * for_dw.dens) if cond = 3)) / adu.sum(0.0673 * (for_sp.dens * (meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10)^2 * hgt_perp)^0.976 if cond = 1)) / 30 * 10", "bio_idx_tree_diversity": "shannon(adu.adu_sp) / 5 * 10", - "bio_idx_spatial_distribution": "adu.categorical_gini(area) * 10", + "test_gini": "adu.gini(int(area) if cond = 1) * 10", + "bio_idx_spatial_distribution": "gini_arr(list_concat(list(adu.count(1 if cond=1 and area=1)), list(adu.count(1 if cond=1 and area=2)), list(adu.count(1 if cond=1 and area=3)), list(adu.count(1 if cond=1 and area=4)))) * 10", "bio_idx_diametric_distribution": "adu.gini(meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10) * 10", "bio_idx_vertical_distribution": "gini(adu.hgt_perp) * 10", - "percentile_80": "adu.list(percentile(hgt_perp, 80))", "bio_idx_dominant_height": "adu.avg(hgt_perp if hgt_perp >= percentile(hgt_perp, 80)) * 10 / 40", "bio_idx_microhabitats": "adu.list(trem).flatten().filter( x -> x != 'None').list_unique() * 10 / trem_group_max", "soil_structure_idx": "(thk1*float(not1) + thk2*float(not2) + thk3*float(not3) + thk4*float(not4) + thk5*float(not5)) / (thk1 + thk2 + thk3 + thk4 + thk5)", diff --git a/backend/requirements.txt b/backend/requirements.txt index 20bd8149..b0c6c66b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.8.1 chardet==7.4.3 -coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@0.6.2#subdirectory=coordo-py +coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@test-0.7.1#subdirectory=coordo-py Django>=4.2.27 djangorestframework==3.16.0 djangorestframework_simplejwt==5.5.1 diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index 967cb2dd..eb60ef17 100644 --- a/webapp/src/features/indicators/soil/format-data.ts +++ b/webapp/src/features/indicators/soil/format-data.ts @@ -28,9 +28,11 @@ function formatTaxonAbundance(abundancePop: string[], abundanceTotal: number) { const abundancePopRecord: Record = {}; abundancePop.forEach((value) => { - const [taxon, count] = value.split(":"); - const currentCount = abundancePopRecord[taxon] || 0; - abundancePopRecord[taxon] = currentCount + parseInt(count, 10); + if (value) { + const [taxon, count] = value.split(":"); + const currentCount = abundancePopRecord[taxon] || 0; + abundancePopRecord[taxon] = currentCount + parseInt(count, 10); + } }); return convertDictToPercentage(abundancePopRecord, abundanceTotal, "0"); diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index 96e52fbf..d21447f6 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -203,7 +203,7 @@ "minPerHhPerDay": "{{ value }} min/household/day", "monthPerYear": "{{ value }} month/year", "percentFoodRequirements": "{{ value }} % of food needs", - "speciesCount": "{{ count }} species inventoried", + "speciesCount": "{{ count }} species per trap", "tonPerHectare": "{{ value }} t/ha" } }, diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index 8c331e87..eb7185e0 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -118,7 +118,7 @@ "governance": { "beneficialPractices": { "legend": "des ménages ont adopté cette pratique", - "pract1": "Mise en défense", + "pract1": "Plantation", "pract2": "Cultures en courbes de niveau", "pract3": "Association de cultures", "pract4": "Rotation des cultures", @@ -206,8 +206,8 @@ "monthPerYear": "{{ value }} mois/an", "percentFoodRequirements": "{{ value }} % des besoins alimentaires", "speciesCount": "", - "speciesCount_one": "{{ count }} espèce inventoriée", - "speciesCount_other": "{{ count }} espèces inventoriées", + "speciesCount_one": "{{ count }} espèce par piège", + "speciesCount_other": "{{ count }} espèces par piège", "tonPerHectare": "{{ value }} t/ha" } }, From 8d71ea5ba942d30cac77809e3070676ae77670e4 Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Thu, 9 Jul 2026 13:32:51 +0200 Subject: [PATCH 8/9] Add ecosystem to popup header --- backend/configs/config.json | 8 ++++---- webapp/src/features/indicators/utils.ts | 14 ++++++++++---- .../popup/components/indicator-popup-header.tsx | 6 ++++++ .../forest-inventory/popup-forest-inventory.tsx | 14 ++++++++++++-- .../src/features/popup/forest-inventory/types.ts | 6 ++++-- .../src/shared/i18n/translations/en/all4trees.json | 3 ++- .../src/shared/i18n/translations/fr/all4trees.json | 3 ++- 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/backend/configs/config.json b/backend/configs/config.json index 3153b75e..915beb3b 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -49,11 +49,11 @@ "columns": { "id": "_id", "geom": "gps.merge().centroid()", - "for": "loc2", + "for": "int(loc2)", "cod": "cod", - "isPublic": "conf", - "ecos": "ecos", - "taille_placette": "20^2*pi()/10000", + "isPublic": "int(conf)", + "ecos": "int(ecos)", + "plot_size": "20^2*pi()/10000", "project": "proj", "tree_pop": "count(1 if adu.cond = 1)", "biomass_volume": "adu.sum(0.0673 * (for_sp.dens * (meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10)^2 * hgt_perp)^0.976 if cond = 1) / (20^2 * pi() / 10000) / 1000", diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index c3822ff7..66ea998c 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -180,10 +180,16 @@ export function findLabelInExternalData( // Find the record matching all criteria: project, list_name, and name const record = resourceData.find( - (item: LabelData) => - item?.proj?.trim() === project.trim() && - item?.list_name?.trim() === fieldName.trim() && - item?.name === fieldValue, + (item: LabelData) => { + if (typeof (item.name) != typeof (fieldValue)) { + console.warn(`Checking field values with different types ! resourceName=${resourceName} fieldName=${fieldName} fieldValue type=${typeof(fieldValue)}; item.name type= ${typeof(item.name)}`) + } + return ( + item.proj?.trim() === project.trim() && + item.list_name?.trim() === fieldName.trim() && + item.name === fieldValue + ) + } ); return record?.label; diff --git a/webapp/src/features/popup/components/indicator-popup-header.tsx b/webapp/src/features/popup/components/indicator-popup-header.tsx index 30d93141..d6604f2c 100644 --- a/webapp/src/features/popup/components/indicator-popup-header.tsx +++ b/webapp/src/features/popup/components/indicator-popup-header.tsx @@ -20,6 +20,7 @@ export type IndicatorPopupHeaderProps = { title: string; subtitle?: string; date?: string; + ecos?: string, onClose: () => void; toggleShiftSize: () => void; }; @@ -54,6 +55,7 @@ export const IndicatorPopupHeader: FC = ({ title, subtitle, date, + ecos, onClose, toggleShiftSize, }) => { @@ -79,6 +81,10 @@ export const IndicatorPopupHeader: FC = ({

{date}

)} + + {ecos && ( +

{ecos}

+ )} )} 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 a032d10b..c63a823f 100644 --- a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx +++ b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx @@ -53,8 +53,17 @@ export const ForestInventoryPopupContent: FC< "for_label", data.project, "loc2", - Number(data.for), - ) || t("dataManagement.undefined", { ns: "common" }); + data.for, + ) || t("common:dataManagement.undefined"); + + const ecos = `${t("all4trees:popup.forestInventory.ecos")}: ${ + findLabelInExternalData( + externalData, + "for_label", + data.project, + "ecos", + data.ecos + ) || t("dataManagement.undefined", { ns: "common" })}`; const biodiversityElements = useBiodiversityIndicatorElements( data, @@ -71,6 +80,7 @@ export const ForestInventoryPopupContent: FC< icon={} subtitle={subtitle} title={title} + ecos={ecos} {...headerProps} /> diff --git a/webapp/src/features/popup/forest-inventory/types.ts b/webapp/src/features/popup/forest-inventory/types.ts index 252caa7b..8441591b 100644 --- a/webapp/src/features/popup/forest-inventory/types.ts +++ b/webapp/src/features/popup/forest-inventory/types.ts @@ -3,10 +3,12 @@ import type { SoilData } from "@features/indicators/soil/types"; export type ForestInventoryData = { id: string; - for: string; + for: number; cod: number; project: string; - taille_placette: number; + isPublic: boolean; + ecos: number; + plot_size: number; } & SoilData & BiodiversityData; diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index d21447f6..1006b93f 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -215,7 +215,8 @@ "popup": { "forestInventory": { "date": "Site survey date : {{ date }}", - "title": "Plot n°{{ id }}" + "title": "Plot n°{{ id }}", + "ecos": "Ecosystem" }, "socioEco": { "date": "Survey date: {{ date }}", diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index eb7185e0..25e48735 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -219,7 +219,8 @@ "popup": { "forestInventory": { "date": "Date relevé: {{ date }}", - "title": "Placette n°{{ id }}" + "title": "Placette n°{{ id }}", + "ecos": "Ecosystème" }, "socioEco": { "date": "Date relevé: {{ date }}", From 853df9b494709f705be9036e6e1d9340006d5edb Mon Sep 17 00:00:00 2001 From: Arnaud Fournier Date: Thu, 9 Jul 2026 15:21:41 +0200 Subject: [PATCH 9/9] Fix lint --- webapp/src/features/indicators/utils.ts | 22 +++++++++---------- .../components/indicator-popup-header.tsx | 6 ++--- .../popup-forest-inventory.tsx | 13 ++++++----- .../i18n/translations/en/all4trees.json | 4 ++-- .../i18n/translations/fr/all4trees.json | 4 ++-- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index 66ea998c..e1151c12 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -179,18 +179,18 @@ export function findLabelInExternalData( } // Find the record matching all criteria: project, list_name, and name - const record = resourceData.find( - (item: LabelData) => { - if (typeof (item.name) != typeof (fieldValue)) { - console.warn(`Checking field values with different types ! resourceName=${resourceName} fieldName=${fieldName} fieldValue type=${typeof(fieldValue)}; item.name type= ${typeof(item.name)}`) - } - return ( - item.proj?.trim() === project.trim() && - item.list_name?.trim() === fieldName.trim() && - item.name === fieldValue - ) + const record = resourceData.find((item: LabelData) => { + if (typeof item.name !== typeof fieldValue) { + console.warn( + `Checking field values with different types ! resourceName=${resourceName} fieldName=${fieldName} fieldValue type=${typeof fieldValue}; item.name type= ${typeof item.name}`, + ); } - ); + return ( + item.proj?.trim() === project.trim() && + item.list_name?.trim() === fieldName.trim() && + item.name === fieldValue + ); + }); return record?.label; } diff --git a/webapp/src/features/popup/components/indicator-popup-header.tsx b/webapp/src/features/popup/components/indicator-popup-header.tsx index d6604f2c..e5f87abe 100644 --- a/webapp/src/features/popup/components/indicator-popup-header.tsx +++ b/webapp/src/features/popup/components/indicator-popup-header.tsx @@ -20,7 +20,7 @@ export type IndicatorPopupHeaderProps = { title: string; subtitle?: string; date?: string; - ecos?: string, + ecos?: string; onClose: () => void; toggleShiftSize: () => void; }; @@ -82,9 +82,7 @@ export const IndicatorPopupHeader: FC = ({ )} - {ecos && ( -

{ecos}

- )} + {ecos &&

{ecos}

} )} 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 c63a823f..58e3704d 100644 --- a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx +++ b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx @@ -59,11 +59,12 @@ export const ForestInventoryPopupContent: FC< const ecos = `${t("all4trees:popup.forestInventory.ecos")}: ${ findLabelInExternalData( externalData, - "for_label", - data.project, - "ecos", - data.ecos - ) || t("dataManagement.undefined", { ns: "common" })}`; + "for_label", + data.project, + "ecos", + data.ecos, + ) || t("dataManagement.undefined", { ns: "common" }) + }`; const biodiversityElements = useBiodiversityIndicatorElements( data, @@ -77,10 +78,10 @@ export const ForestInventoryPopupContent: FC< date: formatDate(new Date()), ns: "all4trees", })} + ecos={ecos} icon={} subtitle={subtitle} title={title} - ecos={ecos} {...headerProps} /> diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index 1006b93f..a91856ce 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -215,8 +215,8 @@ "popup": { "forestInventory": { "date": "Site survey date : {{ date }}", - "title": "Plot n°{{ id }}", - "ecos": "Ecosystem" + "ecos": "Ecosystem", + "title": "Plot n°{{ id }}" }, "socioEco": { "date": "Survey date: {{ date }}", diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index 25e48735..80dc30d6 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -219,8 +219,8 @@ "popup": { "forestInventory": { "date": "Date relevé: {{ date }}", - "title": "Placette n°{{ id }}", - "ecos": "Ecosystème" + "ecos": "Ecosystème", + "title": "Placette n°{{ id }}" }, "socioEco": { "date": "Date relevé: {{ date }}",