diff --git a/backend/configs/config.json b/backend/configs/config.json index 9681148e..ccc06874 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -76,10 +76,50 @@ "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" + } + }, + { + "id": "inventaire_bio", + "type": "datapackage", + "path": "../catalog/inventaire_bio", + "resource": "inv_bio", + "layerType": "symbol", + "cluster": { + "radius": 50, + "maxZoom": 50, + "steps": [10, 20], + "colors": ["#99D6C2", "#009966", "#006B47"] + }, + "columns": { + "geom": "start_point.merge().centroid()", + "id": "_id", + "conf": "conf", + "proj": "proj", + "ecos": "ecos", + "region": "loc1", + "forest": "loc2", + "start_date": "coh", + "taxon": "'N/A'", + "type": "meth", + "samp_area": "inv_samp", + "samp_unit": "inv_unit", + "density": "int(sum(inv_001.no)) / inv_samp", + "dens_unit": "dens_unit", + "richness": "list_unique(list(nullif(inv_001.concat_ws('-', tax1, tax2, tax3), '')))", + "taxons_relative_abundance": "list_aggregate(list(nullif(inv_001.concat_ws('-', tax1, tax2, tax3), '')), 'histogram')", + "pop_by_taxon": "list_concat(list(inv_001.concat_ws(':', concat_ws('-',tax1, tax2, tax3), no) if inv_001.no > 0))", + "total_pop": "int(sum(inv_001.no))" }, "popup": { "trigger": "click" diff --git a/backend/requirements.txt b/backend/requirements.txt index 9d742fb9..0bb1dc19 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.6.0#subdirectory=coordo-py +coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@0.6.2#subdirectory=coordo-py Django>=4.2.27 djangorestframework==3.16.0 djangorestframework_simplejwt==5.5.1 diff --git a/webapp/src/app/providers/map-provider-all4trees.tsx b/webapp/src/app/providers/map-provider-all4trees.tsx index b31f2a29..b0b4aeed 100644 --- a/webapp/src/app/providers/map-provider-all4trees.tsx +++ b/webapp/src/app/providers/map-provider-all4trees.tsx @@ -5,7 +5,6 @@ import { renderAnchor, renderLayerRow } from "@features/controls/layer-control"; import { API_URL } from "@shared/api/client"; import { - type Category, MapContext, } from "@shared/contexts/map-context-all4trees"; import { useLocalStorage } from "@shared/hooks/use-local-storage"; @@ -30,7 +29,6 @@ type MapProviderAll4TreesProps = { export function MapProviderAll4Trees({ children }: MapProviderAll4TreesProps) { const [isReady, setIsReady] = useState(false); const mapApiRef = useRef | null>(null); - const [forests, setForests] = useState([]); const [mapSettings, setMapSettings] = useLocalStorage( "d4g:map-settings:all4trees", DEFAULT_MAP_SETTINGS, @@ -51,14 +49,6 @@ export function MapProviderAll4Trees({ children }: MapProviderAll4TreesProps) { const handleReady = () => { setIsReady(true); - // biome-ignore lint/suspicious/noExplicitAny : - const metadata: any = mapApiRef.current?.getLayerMetadata("inventaire"); - const forestField = metadata?.schema?.fields?.find( - (f: { name: string }) => f.name === "for", - ); - if (forestField?.categories) { - setForests(forestField.categories); - } // On first mount, sync the map state with the local storage state of "categories-filters" syncInitialCategoriesFilters({ @@ -100,12 +90,10 @@ export function MapProviderAll4Trees({ children }: MapProviderAll4TreesProps) { diff --git a/webapp/src/features/categories-filters/use-categories-config.tsx b/webapp/src/features/categories-filters/use-categories-config.tsx index 672b92b8..c078d653 100644 --- a/webapp/src/features/categories-filters/use-categories-config.tsx +++ b/webapp/src/features/categories-filters/use-categories-config.tsx @@ -20,15 +20,15 @@ export const useCategoriesConfig = (): { // ----- Actions ----- - const actionInventary: CategoryGroupItem = { + const actionInventory: CategoryGroupItem = { icon: ( ), - identifier: CATEGORY_IDENTIFIERS.ACTION_INVENTARY, - label: t("filters.categories.actions.forestInventary"), + identifier: CATEGORY_IDENTIFIERS.ACTION_INVENTORY, + label: t("filters.categories.actions.forestInventory"), }; const actionDiversity: CategoryGroupItem = { @@ -111,7 +111,7 @@ export const useCategoriesConfig = (): { }; return { - actions: [actionInventary, actionDiversity, actionSocioEco], + actions: [actionInventory, actionDiversity, actionSocioEco], data: [dataGround, dataSatellite, dataModel], system: [ systemMangroveHigh, diff --git a/webapp/src/features/categories-filters/use-categories-filters.ts b/webapp/src/features/categories-filters/use-categories-filters.ts index eb8782dc..fd84654b 100644 --- a/webapp/src/features/categories-filters/use-categories-filters.ts +++ b/webapp/src/features/categories-filters/use-categories-filters.ts @@ -12,7 +12,8 @@ export const useCategoriesFilters = () => { const [categoriesFilters, setCategoriesFilters] = useLocalStorage("categories-filters", { [CATEGORY_IDENTIFIERS.ACTION_DIVERSITY]: true, - [CATEGORY_IDENTIFIERS.ACTION_INVENTARY]: true, + [CATEGORY_IDENTIFIERS.ACTION_INVENTORY]: true, + [CATEGORY_IDENTIFIERS.ACTION_BIO]: true, [CATEGORY_IDENTIFIERS.ACTION_SOCIO]: true, [CATEGORY_IDENTIFIERS.DATA_GROUND]: true, [CATEGORY_IDENTIFIERS.DATA_MODEL]: true, diff --git a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx index f0fc491f..5962ecc8 100644 --- a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx +++ b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx @@ -1,13 +1,13 @@ import { useTranslation } from "@shared/i18n"; import type { LayerMetadata } from "@shared/lib/coordo"; -import { findCategoricalLabel, precise } from "@shared/lib/utils"; +import { findCategoricalLabel } 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 = { - data: [string, number][]; + data: Record; metadata: LayerMetadata; }; @@ -16,19 +16,18 @@ export const ChartRelativeAbundance: ChartComponentType = ({ metadata, }) => { 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/ui/chart-taxon-abundance.tsx b/webapp/src/features/charts/components/chart-taxon-abundance.tsx similarity index 85% rename from webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx rename to webapp/src/features/charts/components/chart-taxon-abundance.tsx index 091e0daa..6255e2d7 100644 --- a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx +++ b/webapp/src/features/charts/components/chart-taxon-abundance.tsx @@ -8,14 +8,14 @@ import { import { useTranslation } from "@shared/i18n"; -import { SUNBURST_LAYOUT } from "../config"; -import { buildNodeColors, buildSunburstNodes } from "../lib/sunburst"; -import type { PieChartProps, SunburstTrace } from "../types"; +import { SUNBURST_LAYOUT } from "../soil/config"; +import { buildNodeColors, buildSunburstNodes } from "./lib/sunburst"; +import type { PieChartProps, SunburstTrace } from "../soil/types"; export const ChartTaxonAbundance: ChartComponentType = ({ data, metadata, - dataType, + dataType = null, }) => { const { t } = useTranslation(["common", "all4trees"]); const dataEntries = Object.entries(data); @@ -29,11 +29,16 @@ export const ChartTaxonAbundance: ChartComponentType = ({ ); const nodes = buildSunburstNodes(filteredDataEntries, metadata, dataType); 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/charts/soil/lib/sunburst.ts b/webapp/src/features/charts/components/lib/sunburst.ts similarity index 95% rename from webapp/src/features/charts/soil/lib/sunburst.ts rename to webapp/src/features/charts/components/lib/sunburst.ts index 0d75fc4f..3ca6a221 100644 --- a/webapp/src/features/charts/soil/lib/sunburst.ts +++ b/webapp/src/features/charts/components/lib/sunburst.ts @@ -1,13 +1,13 @@ import type { LayerMetadata } from "@shared/lib/coordo"; import { getChartPalette } from "@shared/lib/palette"; -import type { SunburstNode } from "../types"; +import type { SunburstNode } from "../../soil/types"; import { formatTaxonLevelLabel } from "./taxon"; export function buildSunburstNodes( dataEntries: [string, number][], metadata: LayerMetadata, - dataType: "tsbf" | "barbA", + dataType: "tsbf" | "barbA" | null, ): SunburstNode[] { const nodes: SunburstNode[] = []; const seen = new Set(); diff --git a/webapp/src/features/charts/soil/lib/taxon.ts b/webapp/src/features/charts/components/lib/taxon.ts similarity index 61% rename from webapp/src/features/charts/soil/lib/taxon.ts rename to webapp/src/features/charts/components/lib/taxon.ts index 4d2a0fbb..620a4432 100644 --- a/webapp/src/features/charts/soil/lib/taxon.ts +++ b/webapp/src/features/charts/components/lib/taxon.ts @@ -1,25 +1,29 @@ import type { LayerMetadata } from "@shared/lib/coordo"; import { findCategoricalLabel } from "@shared/lib/utils"; +export function getDataTypePrefix(dataType: "tsbf" | "barbA" | null): string { + return dataType ? `${dataType}_` : ''; +} + export function getTaxonLabels( element: string, metadata: LayerMetadata, - dataType: "tsbf" | "barbA", + dataType: "tsbf" | "barbA" | null, ): [string, string, string] { const [taxon1, taxon2, taxon3] = element.split("-"); const taxon1Label = - findCategoricalLabel(metadata, `${dataType}_tax1`, taxon1) || taxon1; + findCategoricalLabel(metadata, `${getDataTypePrefix(dataType)}tax1`, taxon1) || taxon1; const taxon2Label = - findCategoricalLabel(metadata, `${dataType}_tax2`, taxon2) || taxon2; + findCategoricalLabel(metadata, `${getDataTypePrefix(dataType)}tax2`, taxon2) || taxon2; const taxon3Label = - findCategoricalLabel(metadata, `${dataType}_tax3`, taxon3) || taxon3; + findCategoricalLabel(metadata, `${getDataTypePrefix(dataType)}tax3`, taxon3) || taxon3; return [taxon1Label, taxon2Label, taxon3Label]; } export function formatTaxonLevelLabel( element: string, metadata: LayerMetadata, - dataType: "tsbf" | "barbA", + dataType: "tsbf" | "barbA" | null, ): string { const [taxon1Label, taxon2Label, taxon3Label] = getTaxonLabels( element, diff --git a/webapp/src/features/charts/soil/types.ts b/webapp/src/features/charts/soil/types.ts index ff8af292..35ed3b44 100644 --- a/webapp/src/features/charts/soil/types.ts +++ b/webapp/src/features/charts/soil/types.ts @@ -3,7 +3,7 @@ import type { LayerMetadata } from "@shared/lib/coordo"; export type PieChartProps = { data: Record; metadata: LayerMetadata; - dataType: "tsbf" | "barbA"; + dataType?: "tsbf" | "barbA"; }; export type SunburstNode = { @@ -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/controls/layer-control.tsx b/webapp/src/features/controls/layer-control.tsx index 6ced8f24..4789ac60 100644 --- a/webapp/src/features/controls/layer-control.tsx +++ b/webapp/src/features/controls/layer-control.tsx @@ -54,9 +54,9 @@ function useLayerConfig(layerId: string) { /> ), }; - case LAYERS.INVENTARY: + case LAYERS.INVENTORY_FOR: return { - label: t("filters.categories.actions.forestInventary") as string, + label: t("filters.categories.actions.forestInventory") as string, renderIcon: (checked?: boolean) => ( [] = [ + "samp_area", + "density", + "richness", + "total_pop", +]; + +/** + * Return data in a convenient way for UI rendering, handling units and fixing + */ +export const useFormatBioInventoryData = (data: BioInventoryData, metadata: LayerMetadata) => { + const { t } = useTranslation("common"); + + const safeData = preciseNumericIndicators( + data, + indicatorKeys, + t("dataManagement.noData"), + ); + + safeData.taxons_relative_abundance = formatTaxonAbundance( + safeData.pop_by_taxon, + safeData.total_pop, + ); + + return { + taxon: 'Lémuriens', // findCategoricalLabel(metadata, "tax", safeData.taxon) || safeData.taxon + type: findCategoricalLabel(metadata, "meth", safeData.type.toString()) || safeData.type, + area: `${safeData.samp_area} ${findCategoricalLabel(metadata, "inv_unit", safeData.samp_unit) || t("dataManagement.noUnit")}`, + density: `${safeData.density} ${safeData.dens_unit || t("dataManagement.noUnit")}`, + richness: safeData.richness, + relative_abundance: safeData.taxons_relative_abundance, + total_population: safeData.total_pop, + }; +}; + +export type FormattedData = ReturnType; diff --git a/webapp/src/features/indicators/bio-inventory/index.ts b/webapp/src/features/indicators/bio-inventory/index.ts new file mode 100644 index 00000000..04683400 --- /dev/null +++ b/webapp/src/features/indicators/bio-inventory/index.ts @@ -0,0 +1 @@ +export { useBioInventoryIndicatorElements as useBiodiversityIndicatorElements } from "./use-bioinventory-indicator-elements"; diff --git a/webapp/src/features/indicators/bio-inventory/use-bioinventory-indicator-elements.tsx b/webapp/src/features/indicators/bio-inventory/use-bioinventory-indicator-elements.tsx new file mode 100644 index 00000000..5bb48601 --- /dev/null +++ b/webapp/src/features/indicators/bio-inventory/use-bioinventory-indicator-elements.tsx @@ -0,0 +1,60 @@ +import { PawPrint, SearchAlert, Gem } from "lucide-react"; + +import type { LayerMetadata } from "@shared/lib/coordo"; +import { useTranslation } from "@i18n"; + +import { ICON_SIZE } from "../components/constants"; +import { IndicatorRawValue } from "../components/indicator-raw-value"; +import type { UseIndicatorReturnType } from "../components/types"; +import { useFormatBioInventoryData } from "./format-data"; +import type { BioInventoryData } from "@features/popup/bio-inventory/types"; +import { ChartTaxonAbundance } from "@features/charts/components/chart-taxon-abundance"; + +export const useBioInventoryIndicatorElements = ( + rawData: BioInventoryData, + metadata: LayerMetadata, +): UseIndicatorReturnType => { + const { t } = useTranslation("all4trees"); + const data = useFormatBioInventoryData(rawData, metadata); + + return [ + { + children: ( + <> + } + value={data.taxon} + /> + } + value={data.type} + /> + } + value={data.area} + /> + } + value={data.density} + /> + } + value={data.richness} + /> + + + ), + identifier: "bio", + title: t("indicators.bioinventory.title"), + type: "section", + }, + ]; +}; diff --git a/webapp/src/features/indicators/biodiversity/format-data.ts b/webapp/src/features/indicators/biodiversity/format-data.ts index 817e9557..86430208 100644 --- a/webapp/src/features/indicators/biodiversity/format-data.ts +++ b/webapp/src/features/indicators/biodiversity/format-data.ts @@ -1,12 +1,13 @@ import { useTranslation } from "react-i18next"; import { + convertDictToPercentage, preciseNumericIndicators, UNITS, useFormatterWithUnit, } from "@features/indicators/utils"; +import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; -import { precise } from "@shared/lib/utils"; import type { NumericKeys } from "@shared/types"; import type { BiodiversityData } from "./types"; @@ -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/biodiversity/use-biodiversity-indicator-elements.tsx b/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx index 7f4bc190..7aa19694 100644 --- a/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx +++ b/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx @@ -2,6 +2,7 @@ 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"; +import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; import type { LayerMetadata } from "@shared/lib/coordo"; import { useTranslation } from "@i18n"; @@ -10,10 +11,9 @@ import { ICON_SIZE } from "../components/constants"; import { IndicatorRawValue } from "../components/indicator-raw-value"; import type { UseIndicatorReturnType } from "../components/types"; import { useFormatBiodiversityData } from "./format-data"; -import type { BiodiversityData } from "./types"; export const useBiodiversityIndicatorElements = ( - rawData: BiodiversityData, + rawData: ForestInventoryData, metadata: LayerMetadata, ): UseIndicatorReturnType => { const { t } = useTranslation("all4trees"); diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index dcbb2ea8..bd0b0524 100644 --- a/webapp/src/features/indicators/soil/format-data.ts +++ b/webapp/src/features/indicators/soil/format-data.ts @@ -2,10 +2,13 @@ import { useTranslation } from "react-i18next"; import { computeScore, + convertDictToPercentage, + formatTaxonAbundance, preciseNumericIndicators, UNITS, useFormatterWithUnit, } from "@features/indicators/utils"; +import type { ForestInventoryData } from "@features/popup/forest-inventory"; import type { NumericKeys } from "@shared/types"; @@ -27,7 +30,7 @@ const indicatorsToPreciseWithoutFallBack: NumericKeys[] = [ /** * Return data in a convenient way for UI rendering, handling units and fixing */ -export const useFormatSoilData = (data: SoilData) => { +export const useFormatSoilData = (data: ForestInventoryData) => { const { t } = useTranslation("common"); const { formatWithUnit } = useFormatterWithUnit(); @@ -51,11 +54,30 @@ export const useFormatSoilData = (data: SoilData) => { 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/soil/use-soil-indicator-elements.tsx b/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx index 486dabdb..8966edda 100644 --- a/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx +++ b/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx @@ -1,20 +1,20 @@ 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"; +import { ChartTaxonAbundance } from "@features/charts/components/chart-taxon-abundance"; import { ChartWindErosion } from "@features/charts/soil/ui/chart-wind-erosion"; import type { UseIndicatorReturnType } from "@features/indicators//components/types"; import { IndicatorRawValue } from "@features/indicators/components/indicator-raw-value"; +import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; import type { LayerMetadata } from "@shared/lib/coordo"; import { useTranslation } from "@i18n"; import { ICON_SIZE } from "../components/constants"; import { useFormatSoilData } from "./format-data"; -import type { SoilData } from "./types"; export const useSoilIndicatorElements = ( - rawData: SoilData, + rawData: ForestInventoryData, metadata: LayerMetadata, ): UseIndicatorReturnType => { const { t } = useTranslation("all4trees"); diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index 04d0b768..31d314e2 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -21,45 +21,63 @@ 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", { + ns: "all4trees", + value, + }); case UNITS.individualPerHectare: - return t("indicators.units.individualPerHectare", { value }); + return t("indicators.units.individualPerHectare", { + ns: "all4trees", + value, + }); case UNITS.individualPerTrap: - return t("indicators.units.individualPerTrap", { value }); + return t("indicators.units.individualPerTrap", { + ns: "all4trees", + value, + }); 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", { ns: "all4trees", value }); case UNITS.m3PerHabPerYear: - return t("indicators.units.m3PerHabPerYear", { value }); + return t("indicators.units.m3PerHabPerYear", { + ns: "all4trees", + value, + }); case UNITS.monthPerYear: - return t("indicators.units.monthPerYear", { value }); + return t("indicators.units.monthPerYear", { ns: "all4trees", value }); case UNITS.percentFoodRequirements: - return t("indicators.units.percentFoodRequirements", { value }); + return t("indicators.units.percentFoodRequirements", { + ns: "all4trees", + value, + }); case UNITS.minPerHouseholdPerDay: - return t("indicators.units.minPerHhPerDay", { value }); + return t("indicators.units.minPerHhPerDay", { ns: "all4trees", value }); default: - return null; + return noDataStr; } } @@ -76,7 +94,7 @@ export function preciseNumericIndicators>( key, indicatorKeys.includes(key as (typeof indicatorKeys)[number]) ? precise(Number(value), defaultValue) - : (value ?? defaultValue), + : value, // Keep the original value if it's not in the list of indicator keys ]), ) as T; } @@ -86,6 +104,9 @@ export function convertDictToPercentage( total: number, defaultValue: string, ): Record { + if (!data) { + return {}; + } return Object.fromEntries( Object.entries(data).map(([key, value]) => [ key, @@ -94,6 +115,30 @@ export function convertDictToPercentage( ); } +/* + Format taxon relative abundance by converting the sring array containing data like 'taxon:pop' by: + 1. Sum all pop corresponding to same taxon + 2. Convert the pop to percentage of total population + 3. Return a record with taxon as key and percentage as value + Example: + Input: ['taxon1:10', 'taxon2:20', 'taxon1:30'], totalPopulation = 60 + Output: { taxon1: 66.67, taxon2: 33.33 } +*/ +export function formatTaxonAbundance(abundancePop: string[], abundanceTotal: number) { + if (!abundanceTotal) { + return {}; + } + + const 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"); +} + /* Compute score based on the scale define in for_score external data (see backed/catalog/inventaire_for) Ideally should be conmputed in the config.json but for now we can't do it. diff --git a/webapp/src/features/popup/bio-inventory/index.ts b/webapp/src/features/popup/bio-inventory/index.ts new file mode 100644 index 00000000..56a09ba3 --- /dev/null +++ b/webapp/src/features/popup/bio-inventory/index.ts @@ -0,0 +1,2 @@ +export { BioInventoryPopupContent } from "./popup-bio-inventory"; +export type { BioInventoryData } from "./types"; diff --git a/webapp/src/features/popup/bio-inventory/popup-bio-inventory.tsx b/webapp/src/features/popup/bio-inventory/popup-bio-inventory.tsx new file mode 100644 index 00000000..83ea5e62 --- /dev/null +++ b/webapp/src/features/popup/bio-inventory/popup-bio-inventory.tsx @@ -0,0 +1,53 @@ +import { cx } from "class-variance-authority"; +import { Leaf } from "lucide-react"; +import { type FC } from "react"; + +import { ICON_SIZE_HEADER } from "@features/indicators/components/constants"; +import { IndicatorElements } from "@features/indicators/components/indicator-elements"; +import { IndicatorScrollContainer } from "@features/indicators/components/indicator-scroll-container"; +import { IndicatorPopupHeader } from "@features/popup/components/indicator-popup-header"; + +import { findCategoricalLabel, formatDate } from "@shared/lib/utils"; +import { useTranslation } from "@i18n"; + +import type { RenderPopupProps } from "../renderPopup"; +import type { BioInventoryData } from "./types"; +import { useBioInventoryIndicatorElements } from "@features/indicators/bio-inventory/use-bioinventory-indicator-elements"; + +type BioInventoryPopupContentProps = RenderPopupProps; + +export const BioInventoryPopupContent: FC< + BioInventoryPopupContentProps +> = ({ data, metadata, className, ...headerProps }) => { + const { t } = useTranslation(["common", "all4trees"]); + + const biodiversityElements = useBioInventoryIndicatorElements(data, metadata); + + console.log("BioInventoryPopupContent data", data, "metadata", metadata, "biodiversityElements", biodiversityElements); + const title = t("popup.bioInventory.title", { + id: data.id, + ns: "all4trees", + }); + + return ( +
+ } + subtitle={ + findCategoricalLabel(metadata, "loc2", data.forest) || + t("dataManagement.undefined", { ns: "common" }) + } + title={title} + {...headerProps} + /> + + + + +
+ ); +}; diff --git a/webapp/src/features/popup/bio-inventory/types.ts b/webapp/src/features/popup/bio-inventory/types.ts new file mode 100644 index 00000000..833706c0 --- /dev/null +++ b/webapp/src/features/popup/bio-inventory/types.ts @@ -0,0 +1,19 @@ +export type BioInventoryData = { + id: number; + conf: boolean; + proj: string; + ecos: number; + region: number; + forest: number; + start_date: Date; + taxon: string; + type: number; + samp_area: number; + samp_unit: number; + density: number; + dens_unit: string; + richness: number; + taxons_relative_abundance: Record; + pop_by_taxon: string[]; + total_pop: number; +} diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index b8e056f3..a742a5bc 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -59,7 +59,7 @@ export default function DashboardPage() { const loadDashboardData = async () => { try { - const dashboardData = await api.getDashboardData(LAYERS.INVENTARY); + const dashboardData = await api.getDashboardData(LAYERS.INVENTORY_FOR); setData(dashboardData); setChartData(dashboardData[selectedYear]?.beneficiary ?? {}); } catch (error) { diff --git a/webapp/src/shared/api/categories-filters.ts b/webapp/src/shared/api/categories-filters.ts index bc231f5d..8685b635 100644 --- a/webapp/src/shared/api/categories-filters.ts +++ b/webapp/src/shared/api/categories-filters.ts @@ -5,11 +5,12 @@ import { LAYERS } from "./layers"; */ export const CATEGORY_IDENTIFIERS = { ACTION_DIVERSITY: "action-tree-diversity", - ACTION_INVENTARY: `action-forest_${LAYERS.INVENTARY}`, - ACTION_SOCIO: `action-socio-eco_${LAYERS.ENQUETE}`, + ACTION_INVENTORY: `action::${LAYERS.INVENTORY_FOR}`, + ACTION_SOCIO: `action::${LAYERS.ENQUETE}`, + ACTION_BIO: `action::${LAYERS.INVENTORY_BIO}`, DATA_GROUND: "data-ground", DATA_MODEL: "data-model", - DATA_SATELLITE: `data_${LAYERS.SATELLITE}`, + DATA_SATELLITE: `data::${LAYERS.SATELLITE}`, SYSTEM_FOREST_PRIMARY: "system-forest-primary", SYSTEM_FOREST_SECONDARY: "system-forest-secondary", SYSTEM_MANGROVE_HIGH: "system-mangrove-high", @@ -21,7 +22,7 @@ export const CATEGORY_IDENTIFIERS = { * Return null if it can't retrieve the layer id. */ export const parseLayerId = (name: string) => { - const parts = name.split("_"); + const parts = name.split("::"); return parts.length > 1 ? parts[1] : null; }; diff --git a/webapp/src/shared/api/layers.ts b/webapp/src/shared/api/layers.ts index 468b83c4..de0fc41d 100644 --- a/webapp/src/shared/api/layers.ts +++ b/webapp/src/shared/api/layers.ts @@ -1,7 +1,8 @@ export const LAYERS = { BOUNDARIES: "boundaries", ENQUETE: "enquete", - INVENTARY: "inventaire_for", + INVENTORY_FOR: "inventaire_for", + INVENTORY_BIO: "inventaire_bio", SATELLITE: "satellite", SEED: "seed", SEED_POINT: "seed_point", diff --git a/webapp/src/shared/contexts/map-context-all4trees.ts b/webapp/src/shared/contexts/map-context-all4trees.ts index 5c16b5dd..f248e46a 100644 --- a/webapp/src/shared/contexts/map-context-all4trees.ts +++ b/webapp/src/shared/contexts/map-context-all4trees.ts @@ -3,17 +3,12 @@ import { createContext, type RefCallback, type RefObject } from "react"; import type { CategoriesFiltersState } from "@shared/api/categories-filters"; import type { SetValue } from "@shared/hooks/use-local-storage"; import type { createMap } from "@shared/lib/coordo"; - -export type Category = { value: string; label: string }; - export interface MapContextType { categoriesFilters: CategoriesFiltersState; - forests: Category[]; isReady: boolean; mapApiRef: RefObject | null>; mapContainerRef: RefCallback; setCategoriesFilters: (setter: SetValue) => void; - setForests: (forests: Category[]) => void; setIsReady: (isReady: boolean) => void; } diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index 96e52fbf..d230fb2c 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -7,7 +7,7 @@ "filters": { "categories": { "actions": { - "forestInventary": "Forest inventory", + "forestInventory": "Forest inventory", "socioEco": "Socio-economic", "title": "Actions taken", "treeDiversity": "Tree diversity" @@ -223,6 +223,10 @@ "subtitleCount_one": "{{ count }} household surveyed", "subtitleCount_other": "{{ count }} households surveyed", "title": "{{ village }}" + }, + "bioInventory": { + "date": "Survey date: {{ date }}", + "title": "Plot n°{{ id }}" } } -} +} \ No newline at end of file diff --git a/webapp/src/shared/i18n/translations/en/common.json b/webapp/src/shared/i18n/translations/en/common.json index 7b8c2c4d..c156edc8 100644 --- a/webapp/src/shared/i18n/translations/en/common.json +++ b/webapp/src/shared/i18n/translations/en/common.json @@ -19,7 +19,8 @@ }, "dataManagement": { "noData": "No data available", - "undefined": "not found" + "undefined": "not found", + "noUnit": "Unit undefined" }, "header": { "colorModeSelector": { diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index 8c331e87..5988d712 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -7,7 +7,7 @@ "filters": { "categories": { "actions": { - "forestInventary": "Inventaire forestier", + "forestInventory": "Inventaire forestier", "socioEco": "Socio-éco", "title": "Actions menées", "treeDiversity": "Diversité arborée" @@ -48,7 +48,6 @@ "other": "Autre", "title": "Abondance relative par essence" }, - "speciesRichness": "Richesse spécifique", "title": "Diversité arborée" }, "tropicalBiodivIndex": { @@ -66,6 +65,12 @@ }, "title": "Biodiversité" }, + "bioinventory": { + "title": "Inventaire de faune", + "taxon": "Taxon cible", + "type": "Type de dispositif", + "sampArea": "Effort d'échantillonnage" + }, "common": { "abundance": "Abondance relative", "beneficiary": "Bénef.", @@ -227,6 +232,10 @@ "subtitleCount_one": "{{ count }} ménage enquêté", "subtitleCount_other": "{{ count }} ménages enquêtés", "title": "{{ village }}" + }, + "bioInventory": { + "date": "Date relevé: {{ date }}", + "title": "Placette n°{{ id }}" } } -} +} \ No newline at end of file diff --git a/webapp/src/shared/i18n/translations/fr/common.json b/webapp/src/shared/i18n/translations/fr/common.json index 92c67e41..8a6b9412 100644 --- a/webapp/src/shared/i18n/translations/fr/common.json +++ b/webapp/src/shared/i18n/translations/fr/common.json @@ -19,7 +19,8 @@ }, "dataManagement": { "noData": "Données non disponibles", - "undefined": "non trouvée" + "undefined": "non trouvée", + "noUnit": "Unité non définie" }, "header": { "colorModeSelector": { diff --git a/webapp/src/shared/lib/utils.ts b/webapp/src/shared/lib/utils.ts index 0fa24d05..91e80e7a 100644 --- a/webapp/src/shared/lib/utils.ts +++ b/webapp/src/shared/lib/utils.ts @@ -9,7 +9,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) { diff --git a/webapp/src/widgets/map/map-all4trees.tsx b/webapp/src/widgets/map/map-all4trees.tsx index 0f509822..6a800492 100644 --- a/webapp/src/widgets/map/map-all4trees.tsx +++ b/webapp/src/widgets/map/map-all4trees.tsx @@ -21,9 +21,10 @@ import pictoInventaire from "./assets/inventaire-icon.svg"; import pictoSocioEco from "./assets/socio-eco-icon.svg"; import { MapBase } from "./map-base"; import { getIconSize } from "./utils"; +import { BioInventoryPopupContent, type BioInventoryData } from "@features/popup/bio-inventory"; export const MapAll4Trees: FC = () => { - const { isReady, mapApiRef, forests, mapContainerRef } = useMap(); + const { isReady, mapApiRef, mapContainerRef } = useMap(); const [isMaximizedPopupSize, setIsMaximizedPopupSize] = useState(false); useEffect(() => { @@ -33,7 +34,7 @@ export const MapAll4Trees: FC = () => { mapApiRef.current.setLayerSymbol({ iconSize: getIconSize({}), - layerId: LAYERS.INVENTARY, + layerId: LAYERS.INVENTORY_FOR, svg: pictoInventaire, }); @@ -43,10 +44,16 @@ export const MapAll4Trees: FC = () => { svg: pictoSocioEco, }); - // Set the popup for the "inventaire" layer + mapApiRef.current.setLayerSymbol({ + iconSize: getIconSize({}), + layerId: LAYERS.INVENTORY_BIO, + svg: pictoInventaire, + }); + + // Set the popup for the "inventaire_for" layer mapApiRef.current.setLayerPopup({ centerOnClick: true, - layerId: LAYERS.INVENTARY, + layerId: LAYERS.INVENTORY_FOR, popupConfig: DEFAULT_POPUP_CONFIG, renderCallback: getRenderPopupLayer({ Element: ForestInventoryPopupContent, @@ -67,6 +74,18 @@ export const MapAll4Trees: FC = () => { trigger: "click", }); + // Set the popup for the "inventaire_bio" layer + mapApiRef.current.setLayerPopup({ + centerOnClick: true, + layerId: LAYERS.INVENTORY_BIO, + popupConfig: DEFAULT_POPUP_CONFIG, + renderCallback: getRenderPopupLayer({ + Element: BioInventoryPopupContent, + toggleShiftSize, + }), + trigger: "click", + }); + // Set the popup for the Seed data layer mapApiRef.current.setLayerPopup({ centerOnClick: true, @@ -80,45 +99,11 @@ export const MapAll4Trees: FC = () => { }); }, [isReady, mapApiRef]); - const filterByForest = (forestId: string) => { - mapApiRef.current?.setLayerFilters({ - filters: { args: [{ property: "for" }, forestId], op: "=" }, - layerId: "inventaire", - }); - }; - - const resetFilter = () => { - mapApiRef.current?.setLayerFilters({ - filters: null, - layerId: "inventaire", - }); - }; - return ( - {forests.length > 0 && ( -
- {forests.map((forest) => ( - - ))} - -
- )} -
+ /> ); };