diff --git a/Dashboards/assets/img/diploicia_canescens.JPG b/Dashboards/assets/img/diploicia_canescens.jpg similarity index 100% rename from Dashboards/assets/img/diploicia_canescens.JPG rename to Dashboards/assets/img/diploicia_canescens.jpg diff --git a/Dashboards/assets/img/melanelixia_glabratula.JPG b/Dashboards/assets/img/melanelixia_glabratula.jpg similarity index 100% rename from Dashboards/assets/img/melanelixia_glabratula.JPG rename to Dashboards/assets/img/melanelixia_glabratula.jpg diff --git a/Dashboards/assets/lichen_species_ecology.csv b/Dashboards/assets/lichen_species_ecology.csv index 979eff2..669c61d 100644 --- a/Dashboards/assets/lichen_species_ecology.csv +++ b/Dashboards/assets/lichen_species_ecology.csv @@ -1,39 +1,39 @@ -id;Taxon;pH;aridity;eutrophication;poleotolerance;thallus;picture -7;Amandinea punctata/Lecidella elaeochroma;neutrophilous;xerophilous;mesotrophic;resistant;crustose;amandinea_punctata.jpg -28;Anaptychia ciliaris;neutrophilous;mesophilous;mesotrophic;intermediate;fruticose;anaptychia_ciliaris.jpg -30;Autre lichen crustacé;unknown;unknown;unknown;intermediate;crustose;lichen_crustace.jpg -20;Autre lichen foliacé;unknown;unknown;unknown;intermediate;foliose;lichen_foliace.jpg -36;Autre lichen fruticuleux;unknown;unknown;unknown;intermediate;fruticose;lichen_crustace.jpg -2;Candelaria concolor;neutrophilous;xerophilous;eutrophic;resistant;foliose;candelaria_concolor.jpg -21;Candelariella sp.;neutrophilous;mesophilous;eutrophic;resistant;crustose;candelariella.jpg -35;Diploicia canescens;basophilous;mesophilous;mesotrophic;intermediate;crustose;diploicia_canescens.jpg -8;Evernia prunastri;acidophilous;mesophilous;oligotrophic;intermediate;fruticose;evernia_prunastri.jpg -3;Flavoparmelia caperata/soredians;neutrophilous;mesophilous;oligotrophic;intermediate;foliose;flavoparmelia_caperata.jpg -24;Hyperphyscia adglutinata;basophilous;xerophilous;eutrophic;resistant;foliose;hyperphyscia_adglutinata.jpg -22;Hypogymnia physodes/tubulosa;acidophilous;mesophilous;oligotrophic;intermediate;foliose;hypogymnia_physodes.jpg -4;Hypotrachyna afrorevoluta/revoluta;acidophilous;mesophilous;oligotrophic;intermediate;foliose;hypotrachyna_afrevoluta.jpg -9;Lecanora sp.;unknown;unknown;unknown;intermediate;crustose;lecanora.jpg -5;Lichen crustacé à aspect poudreux;acidophilous;unknown;unknown;intermediate;crustose;crustaces_poudreux.jpg -38;Lichen crustacé à lirelles;unknown;unknown;unknown;intermediate;crustose;crustaces_lirelles.jpg -10;Melanelixia glabratula/Melanohalea exasperatula;neutrophilous;mesophilous;mesotrophic;resistant;foliose;melanelixia_glabratula.jpg -25;Melanohalea exasperata;neutrophilous;xerophilous;mesotrophic;intermediate;foliose;melanohalea_exasperata.jpg -33;Parmelia saxatilis;acidophilous;mesophilous;oligotrophic;intermediate;foliose;parmelia_saxatilis.jpg -11;Parmelia sulcata;acidophilous;mesophilous;oligotrophic;resistant;foliose;parmelia_sulcata.jpg -23;Parmelina tiliacea/pastillifera;neutrophilous;hygrophilous;mesotrophic;intermediate;foliose;parmelina_tiliacea.jpg -31;Parmotrema perlatum/reticulatum;neutrophilous;mesophilous;oligotrophic;intermediate;foliose;parmotrema_perlatum.jpg -29;Pertusaria pertusa;acidophilous;mesophilous;oligotrophic;intermediate;crustose;pertusaria_pertusa.jpg -12;Phaeophyscia orbicularis;neutrophilous;xerophilous;eutrophic;resistant;foliose;phaeophyscia_orbicularis.jpg -13;Physcia adscendens/tenella;basophilous;xerophilous;eutrophic;resistant;foliose;physcia_adscendens.jpg -19;Physcia aipolia/stellaris;neutrophilous;mesophilous;eutrophic;resistant;foliose;physcia_aipolia.jpg -16;Physcia leptalea;neutrophilous;xerophilous;mesotrophic;intermediate;foliose;physcia_leptalea.jpg -18;Physconia distorta;neutrophilous;xerophilous;eutrophic;resistant;foliose;physconia_distorta.jpg -14;Physconia grisea;neutrophilous;mesophilous;eutrophic;resistant;foliose;physconia_grisea.jpg -15;Pleurosticta acetabulum;neutrophilous;xerophilous;mesotrophic;intermediate;foliose;pleurosticta_acetabulum.jpg -37;Polycauliona polycarpa;neutrophilous;xerophilous;mesotrophic;intermediate;foliose;polycauliona_polycarpa.jpg -27;Pseudevernia furfuracea;acidophilous;xerophilous;oligotrophic;intermediate;fruticose;pseudevernia_furfuracea.jpg -17;Punctelia sp.;neutrophilous;mesophilous;oligotrophic;resistant;foliose;punctelia_sp_.jpg -6;Ramalina farinacea;neutrophilous;hygrophilous;oligotrophic;intermediate;fruticose;ramalina_farinacea.jpg -32;Ramalina fastigiata;neutrophilous;mesophilous;oligotrophic;intermediate;fruticose;ramalina_fastigiata.jpg -26;Ramalina fraxinea;neutrophilous;mesophilous;mesotrophic;sensitive;fruticose;ramalina_fraxinea.jpg -34;Usnea sp.;acidophilous;hygrophilous;oligotrophic;sensitive;fruticose;usnea.jpg -1;Xanthoria parietina;neutrophilous;xerophilous;eutrophic;resistant;foliose;xanthoria_parietina.jpg +id;Taxon;pH;aridity;eutrophication;poleotolerance;thallus;rarity;picture +8;Evernia prunastri;acidophilous;mesophilous;oligotrophic;intermediate;fruticuleux;commun;evernia_prunastri.jpg +3;Flavoparmelia caperata/soredians;neutrophilous;mesophilous;oligotrophic;intermediate;foliacé;commun;flavoparmelia_caperata.jpg +9;Lecanora sp.;unknown;unknown;unknown;intermediate;crustacé;commun;lecanora.jpg +5;Lichen crustacé à aspect poudreux;acidophilous;unknown;unknown;intermediate;crustacé;commun;crustaces_poudreux.jpg +10;Melanelixia glabratula/Melanohalea exasperatula;neutrophilous;mesophilous;mesotrophic;resistant;foliacé;commun;melanelixia_glabratula.jpg +11;Parmelia sulcata;acidophilous;mesophilous;oligotrophic;resistant;foliacé;commun;parmelia_sulcata.jpg +14;Physconia grisea;neutrophilous;mesophilous;eutrophic;resistant;foliacé;commun;physconia_grisea.jpg +17;Punctelia sp.;neutrophilous;mesophilous;oligotrophic;resistant;foliacé;commun;punctelia_sp_.jpg +35;Diploicia canescens;basophilous;mesophilous;mesotrophic;intermediate;crustacé;commun près des côtes;diploicia_canescens.jpg +30;Autre lichen crustacé;unknown;unknown;unknown;intermediate;crustacé;indéterminé;lichen_crustace.jpg +20;Autre lichen foliacé;unknown;unknown;unknown;intermediate;foliacé;indéterminé;lichen_foliace.jpg +36;Autre lichen fruticuleux;unknown;unknown;unknown;intermediate;fruticuleux;indéterminé;lichen_crustace.jpg +22;Hypogymnia physodes/tubulosa;acidophilous;mesophilous;oligotrophic;intermediate;foliacé;peu commun;hypogymnia_physodes.jpg +4;Hypotrachyna afrorevoluta/revoluta;acidophilous;mesophilous;oligotrophic;intermediate;foliacé;peu commun;hypotrachyna_afrevoluta.jpg +38;Lichen crustacé à lirelles;unknown;unknown;unknown;intermediate;crustacé;peu commun;crustaces_lirelles.jpg +33;Parmelia saxatilis;acidophilous;mesophilous;oligotrophic;intermediate;foliacé;peu commun;parmelia_saxatilis.jpg +23;Parmelina tiliacea/pastillifera;neutrophilous;hygrophilous;mesotrophic;intermediate;foliacé;peu commun;parmelina_tiliacea.jpg +31;Parmotrema perlatum/reticulatum;neutrophilous;mesophilous;oligotrophic;intermediate;foliacé;peu commun;parmotrema_perlatum.jpg +19;Physcia aipolia/stellaris;neutrophilous;mesophilous;eutrophic;resistant;foliacé;peu commun;physcia_aipolia.jpg +18;Physconia distorta;neutrophilous;xerophilous;eutrophic;resistant;foliacé;peu commun;physconia_distorta.jpg +15;Pleurosticta acetabulum;neutrophilous;xerophilous;mesotrophic;intermediate;foliacé;peu commun;pleurosticta_acetabulum.jpg +37;Polycauliona polycarpa;neutrophilous;xerophilous;mesotrophic;intermediate;foliacé;peu commun;polycauliona_polycarpa.jpg +6;Ramalina farinacea;neutrophilous;hygrophilous;oligotrophic;intermediate;fruticuleux;peu commun;ramalina_farinacea.jpg +32;Ramalina fastigiata;neutrophilous;mesophilous;oligotrophic;intermediate;fruticuleux;peu commun;ramalina_fastigiata.jpg +16;Physcia leptalea;neutrophilous;xerophilous;mesotrophic;intermediate;foliacé;rare;physcia_leptalea.jpg +28;Anaptychia ciliaris;neutrophilous;mesophilous;mesotrophic;intermediate;fruticuleux;rare;anaptychia_ciliaris.jpg +25;Melanohalea exasperata;neutrophilous;xerophilous;mesotrophic;intermediate;foliacé;rare;melanohalea_exasperata.jpg +29;Pertusaria pertusa;acidophilous;mesophilous;oligotrophic;intermediate;crustacé;rare;pertusaria_pertusa.jpg +27;Pseudevernia furfuracea;acidophilous;xerophilous;oligotrophic;intermediate;fruticuleux;rare;pseudevernia_furfuracea.jpg +26;Ramalina fraxinea;neutrophilous;mesophilous;mesotrophic;sensitive;fruticuleux;rare;ramalina_fraxinea.jpg +34;Usnea sp.;acidophilous;hygrophilous;oligotrophic;sensitive;fruticuleux;rare;usnea.jpg +7;Amandinea punctata/Lecidella elaeochroma;neutrophilous;xerophilous;mesotrophic;resistant;crustacé;très commun;amandinea_punctata.jpg +2;Candelaria concolor;neutrophilous;xerophilous;eutrophic;resistant;foliacé;très commun;candelaria_concolor.jpg +21;Candelariella sp.;neutrophilous;mesophilous;eutrophic;resistant;crustacé;très commun;candelariella.jpg +24;Hyperphyscia adglutinata;basophilous;xerophilous;eutrophic;resistant;foliacé;très commun;hyperphyscia_adglutinata.jpg +12;Phaeophyscia orbicularis;neutrophilous;xerophilous;eutrophic;resistant;foliacé;très commun;phaeophyscia_orbicularis.jpg +13;Physcia adscendens/tenella;basophilous;xerophilous;eutrophic;resistant;foliacé;très commun;physcia_adscendens.jpg +1;Xanthoria parietina;neutrophilous;xerophilous;eutrophic;resistant;foliacé;très commun;xanthoria_parietina.jpg diff --git a/Dashboards/assets/styles.css b/Dashboards/assets/styles.css deleted file mode 100644 index 2dda30a..0000000 --- a/Dashboards/assets/styles.css +++ /dev/null @@ -1,9 +0,0 @@ -.graph-title { - text-align: left; - margin: 0; - padding: 5px; -} - -.info-icon { - font-size: 15px; -} diff --git a/Dashboards/charts.py b/Dashboards/charts.py index 3cf8458..7a15018 100644 --- a/Dashboards/charts.py +++ b/Dashboards/charts.py @@ -20,22 +20,22 @@ def blank_figure(): return fig def create_map(filtered_df, selected_map_column, zoom, center): - fig_map = px.scatter_mapbox( + fig_map = px.scatter_map( filtered_df, lat="localisation_lat", lon="localisation_long", color=selected_map_column, hover_name="date_obs", hover_data=["localisation_lat", "localisation_long"], - mapbox_style="open-street-map", + map_style="open-street-map", color_discrete_map=MAP_SETTINGS[selected_map_column]["color_map"], ) fig_map.update_layout( PLOTLY_LAYOUT, margin=dict(l=0, r=0, t=0, b=0), - mapbox_zoom=zoom, - mapbox_center=center, + map_zoom=zoom, + map_center=center, legend=dict( x=0.02, # Position the legend on the map y=0.02, diff --git a/Dashboards/constants.py b/Dashboards/constants.py index ecbae77..99019ca 100644 --- a/Dashboards/constants.py +++ b/Dashboards/constants.py @@ -1,25 +1,9 @@ BODY_FONT_FAMILY = '"Source Sans Pro", sans-serif' -PLOTLY_FONT_COLOR = "#495057" # Grey - -# Constants for color palettes, font families, etc. -# BASE_COLOR_PALETTE = [ -# "#387CA6", -# "#1C6C8C", -# "#3887A6", -# "#ADCCD9", -# "#F2F2F2" -# ] +PLOTLY_FONT_COLOR = "#868e96" # Grey # Generated with https://omatsuri.app/color-shades-generator - BASE_COLOR_PALETTE = [ - # "#333D43", - # "#37444C", - # "#3A4C58", - # "#3C5665", - # "#3D6176", - # "#3C6D8C", "#387CA6", "#4A86AB", "#608FAD", @@ -30,12 +14,6 @@ ] PASTEL_COLOR_PALETTE = [ - # "#c1c4c6", - # "#c3c6c9", - # "#c3c9cc", - # "#c4ccd0", - # "#c4cfd5", - # "#c4d3dc", "#c3d7e4", "#c8dae5", "#cfdde6", @@ -76,6 +54,19 @@ "title": "VDL", "color_map": {'<25': 'red', '25-50': 'orange', '50-75': 'yellow', '>75': 'green'} }, + "deg_pollution_acid_cat": { + "title": "Dégré pollution acide", + "color_map": {'0-25%': 'red', '25-50%': 'orange', '50-75%': 'yellow', '75-100%': 'green'} + }, + "deg_pollution_azote_cat": { + "title": "Dégré pollution azote", + "color_map": {'0-25%': 'green', '25-50%': 'yellow', '50-75%': 'orange', '75-100%': 'red'} + }, + "deg_artif_cat": + { + "title": "Dégré artificialisation", + "color_map": {'0-25%': 'green', '25-50%': 'yellow', '50-75%': 'orange', '75-100%': 'red'} + }, "selected_species_present": { "title": "Espèce sélectionnée présente", @@ -84,6 +75,22 @@ } +# Constants for common styles +FLEX_COLUMNS_CONTAINER_STYLE = {"display": "flex", "gap": "16px"} +GRID_STYLE = {"gutter": "md", "align": "stretch"} +CARD_STYLE = { + "pt":"xs", + "pb": "cs", + "shadow": "sm", + "withBorder": True + } + +MAP_STYLE = { + "withBorder": True, + "p": 0, +} + + # Define the plotly style for hover labels PLOTLY_HOVER_STYLE = { "font": dict( @@ -93,7 +100,10 @@ # Define the plotly layout for all plots PLOTLY_LAYOUT = { - "font": dict(family=BODY_FONT_FAMILY, color=PLOTLY_FONT_COLOR), + "font": dict(family=BODY_FONT_FAMILY, + color="grey", + #color=PLOTLY_FONT_COLOR + ), "template": "plotly_white", "margin": dict(l=0, r=0, t=10, b=10), "barcornerradius": "30%", @@ -101,3 +111,17 @@ "plot_bgcolor": "rgba(0, 0, 0, 0)", # Transparent plot background "paper_bgcolor": "rgba(0, 0, 0, 0)", # Transparent paper background } + +TRANSLATIONS_EN_FR = { + "unknown": "inconnu", + "acidophilous": "acidophile", + "neutrophilous": "neutrophile", + "basophilous": "basophile", + "oligotrophic": "oligotrophe", + "mesotrophic": "mésotrophe", + "eutrophic": "eutrophe", + "sensitive": "sensible", + "intermediate": "intermédiaire", + "tolerant": "tolérant", + "resistant": "résistant", +} diff --git a/Dashboards/dashboard.py b/Dashboards/dashboard.py index ba82fa3..1fed9a3 100644 --- a/Dashboards/dashboard.py +++ b/Dashboards/dashboard.py @@ -2,7 +2,7 @@ import os import dash_mantine_components as dmc -from dash import Dash, _dash_renderer, html, dcc, Output, Input, callback +from dash import Dash, _dash_renderer, html, dcc, Output, Input, callback, no_update from dash.dependencies import State from dash.exceptions import PreventUpdate from dash_iconify import DashIconify @@ -11,7 +11,7 @@ from Dashboards.my_data.datasets import get_useful_data from Dashboards.my_data.computed_datasets import merge_tables, calc_degrees_pollution, calc_vdl, count_lichen, count_lichen_per_species, count_species_per_observation, count_lichen_per_lichen_id, group_lichen_by_observation_and_thallus from Dashboards.charts import blank_figure, create_map, create_hist1_nb_species, create_hist2_vdl, create_hist3, create_pie_thallus, create_hist4, create_gauge_chart -from Dashboards.constants import MAP_SETTINGS, BASE_COLOR_PALETTE, BODY_FONT_FAMILY, POSITIVE_GAUGE_COLOR_PALETTE, NEGATIVE_GAUGE_COLOR_PALETTE +from Dashboards.constants import MAP_SETTINGS, BASE_COLOR_PALETTE, BODY_FONT_FAMILY, POSITIVE_GAUGE_COLOR_PALETTE, NEGATIVE_GAUGE_COLOR_PALETTE, TRANSLATIONS_EN_FR, GRID_STYLE, CARD_STYLE, MAP_STYLE, FLEX_COLUMNS_CONTAINER_STYLE _dash_renderer._set_react_version("18.2.0") @@ -48,11 +48,29 @@ # Initialize a blank figure to show during loading blank_fig = blank_figure() -# Initialize the selections +# Initialize the options and selections date_range = [merged_observation_df["date_obs"].min(), datetime.now().date()] map_column_selected = list(MAP_SETTINGS.keys())[0] +# Convert DataFrame to list of dictionaries +species_options = [{"value": str(row["species_id"]), "label": row["name"]} for _, row in merged_lichen_species_df.sort_values(by="name").iterrows()] +species_id_selected = species_options[0]["value"] # Default to the first species ID + +# Callback to reset the date range +@callback( + Output('date-picker-range', 'value'), + Input('reset-date-button', 'n_clicks'), + State('date-picker-range', 'minDate'), + State('date-picker-range', 'maxDate'), + State('date-picker-range', 'value') +) +def reset_date_range(n_clicks, min_date, max_date, date_range): + if n_clicks is None or date_range == [min_date, max_date]: + raise PreventUpdate + + return [min_date, max_date] + # First callback to update the dashboard based on date and map selection @callback( Output('map-nb_species-vdl', 'figure'), @@ -71,9 +89,9 @@ def update_dashboard_map(date_range, map_column_selected, relayoutData): filtered_observation_df = merged_observation_df[(merged_observation_df['date_obs'] >= start_date) & (merged_observation_df['date_obs'] <= end_date)] - if relayoutData and "mapbox.zoom" in relayoutData and "mapbox.center" in relayoutData: - current_zoom = relayoutData["mapbox.zoom"] - current_center = relayoutData["mapbox.center"] + if relayoutData and "map.zoom" in relayoutData and "map.center" in relayoutData: + current_zoom = relayoutData["map.zoom"] + current_center = relayoutData["map.center"] else: current_zoom = 4.8 current_center = {"lat": filtered_observation_df['localisation_lat'].mean() + 0.5, "lon": filtered_observation_df['localisation_long'].mean()} @@ -158,11 +176,19 @@ def update_dashboard_observation(clickData, date_range): @callback( Output(component_id='map-species_present', component_property='figure'), Output(component_id='hist4-species', component_property='figure'), - Output(component_id='lichen-image', component_property='src'), + Output(component_id='species-name', component_property='children'), + Output(component_id='species-image', component_property='src'), + Output(component_id='acid-badge', component_property='children'), + Output(component_id='eutro-badge', component_property='children'), + Output(component_id='poleo-badge', component_property='children'), + Output(component_id='species-thallus', component_property='children'), + Output(component_id='species-rarity', component_property='children'), Input(component_id='species-dropdown', component_property='value'), State('map-species_present', 'relayoutData') ) def update_dashboard2(species_id_selected, relayoutData): + if isinstance(species_id_selected, str): + species_id_selected = int(species_id_selected) hist4_species = create_hist4(nb_lichen_per_species_df, species_id_selected) @@ -179,381 +205,394 @@ def update_dashboard2(species_id_selected, relayoutData): current_zoom = 4.8 current_center = {"lat": observation_with_selected_species_col_df['localisation_lat'].mean() + 0.5, "lon": observation_with_selected_species_col_df['localisation_long'].mean()} - fig_map = create_map(observation_with_selected_species_col_df, 'selected_species_present', current_zoom, current_center) - lichen_img = merged_lichen_species_df[merged_lichen_species_df['species_id'] == species_id_selected]['picture'].iloc[0] - lichen_img_path = os.path.join(lichen_img_dir, lichen_img) + # Filter on the selected species + species_selected = merged_lichen_species_df[merged_lichen_species_df['species_id'] == species_id_selected].iloc[0] + + species_name = species_selected['name'] + species_img = species_selected['picture'] + species_img_path = os.path.join(lichen_img_dir, species_img) + + species_acid = species_selected['pH'] + species_eutro = species_selected['eutrophication'] + species_poleo = species_selected['poleotolerance'] + species_thallus = species_selected['thallus'] + species_rarity = species_selected['rarity'] + + # Translate with the dictionary + species_acid = TRANSLATIONS_EN_FR.get(species_acid, species_acid) + species_eutro = TRANSLATIONS_EN_FR.get(species_eutro, species_eutro) + species_poleo = TRANSLATIONS_EN_FR.get(species_poleo, species_poleo) + species_thallus = TRANSLATIONS_EN_FR.get(species_thallus, species_thallus) - return fig_map, hist4_species, lichen_img_path + # Pluralize the thallus + if not species_thallus.endswith("x") or species_thallus.endswith("s"): + species_thallus += "s" + return fig_map, hist4_species, species_name, species_img_path, species_acid, species_eutro, species_poleo, species_thallus, species_rarity + + +# Reusable component for title and tooltip def title_and_tooltip(title, tooltip_text): - return html.Div( - style={"display": "flex", "align-items": "center"}, + return dmc.Group( children=[ - dmc.Title(title, order=4, className="graph-title"), + dmc.Title(title, order=4), dmc.Tooltip( - label=tooltip_text, - position="top", - withArrow=True, children=DashIconify( icon="material-symbols:info-outline", - className="info-icon", + height=15, ), + label=tooltip_text, + withArrow=True, + position="top", ), ], + align="center", + gap=2, ) - -# Create options for the user species dropdown, sorted by name -species_options = [ - {"label": row["name"], "value": row["species_id"]} - for _, row in nb_lichen_per_species_df.sort_values(by="name").iterrows() -] -species_id_selected = species_options[0]['value'] # Default to the first species ID - - -# Layout for the sites (observations) -sites_layout = [ - # Divider for the 2 columns - html.Div( - style={"display": "flex", "gap": "10px"}, +# Reusable component for gauge cards +def gauge_card(title, tooltip_text, graph_id, max_height="200px"): + return dmc.Card( children=[ - # Divider for the first column with map and gauge - html.Div( - style={"flex": "5"}, - children=[ - # Divider for the map title and selector - html.Div( - style={"display": "flex", - "align-items": "center", "gap": "10px", }, - children=[ - dmc.Title( - "Carte des observations", - order=4, - className="graph-title", - style={"padding": "0px"}, - ), - # Selector for the map column - dmc.SegmentedControl( - id="map-column-select", - value=list(MAP_SETTINGS.keys())[0], - data=[ - {"label": MAP_SETTINGS[col] - ["title"], "value": col} - for col in ["nb_species_cat", "VDL_cat"] - ], - transitionDuration=500, - ), - ], - ), - html.Div( - children=[ - dmc.Card( - children=[ - dcc.Graph( - id="map-nb_species-vdl", - figure=blank_fig, - config={ - "displaylogo": False, # Remove plotly logo - }, - ), - ], - withBorder=True, - shadow="sm", - # Remove padding between the card and the map - style={"padding": "0"}, - ), - ], - ), - # Divider for the gauge charts, with 3 columns each - html.Div( - style={"display": "flex", "gap": "10px", - "padding-top": "10px"}, - children=[ - html.Div( - style={"flex": "1"}, - children=[ - dmc.Card( - children=[ - title_and_tooltip( - title="% Espèces toxitolérantes", - tooltip_text="Pourcentage d'espèces toxitolérantes sur le site sélectionné", - ), - dcc.Graph( - id="gauge-chart1-artif", - figure=blank_fig, - style={"height": "100px"}, - config={ - "displayModeBar": False, # Remove plotly tool bar - }, - ), - ], - withBorder=True, - shadow="sm", - # Reduce padding between the card and the gauge - style={ - "padding-top": "5px", "padding-left": "5px", "padding-right": "5px"}, - ), - ], - ), - html.Div( - style={"flex": "1"}, - children=[ - dmc.Card( - children=[ - title_and_tooltip( - title="% Espèces eutophes", - tooltip_text="Pourcentage d'espèces eutrophes sur le site sélectionné" - ), - dcc.Graph( - id="gauge-chart3-azote", - figure=blank_fig, - style={"height": "100px"}, - config={ - "displayModeBar": False, # Remove plotly tool bar - }, - ), - ], - withBorder=True, - shadow="sm", - # Reduce padding between the card and the gauge - style={ - "padding-top": "5px", "padding-left": "5px", "padding-right": "5px"}, - ), - ], - ), - html.Div( - style={"flex": "1"}, - children=[ - dmc.Card( - children=[ - title_and_tooltip( - title="% Espèces acidophiles", - tooltip_text="Pourcentage d'espèces acidophiles sur le site sélectionné" - ), - dcc.Graph( - id="gauge-chart2-acide", - figure=blank_fig, - style={"height": "100px"}, - config={ - "displayModeBar": False, # Remove plotly tool bar - }, - ), - ], - withBorder=True, - shadow="sm", - # Reduce padding between the card and the gauge - style={ - "padding-top": "5px", "padding-left": "5px", "padding-right": "5px"}, - ), - ], - ), - ], - ), - ], + title_and_tooltip(title, tooltip_text), + dcc.Graph( + id=graph_id, + figure=blank_fig, + style={ + "height": "100px", + "width": "100%", + }, + config={"displayModeBar": False}, ), - # Divider for the second column with histograms - html.Div( - style={"flex": "5", - "padding": "5px", - }, - children=[ - dmc.Grid( - gutter="md", - align="stretch", - children=[ - # Column for hist1 - dmc.GridCol( - span=6, - children=[ - title_and_tooltip( - title="Distribution du nombre d'espèces", - tooltip_text="Distribution du nombre d'espèces par site. Si vous cliquez sur un site sur la carte, son nombre d'espèce sera affiché en trait pointillé rouge." - ), - dmc.Card( - dcc.Graph( - id="hist1-nb_species", - figure=blank_fig, - style={"height": "300px"}, - config={ - "displaylogo": False, # Remove plotly logo - }, - ), - withBorder=True, - shadow="sm", - ) - ], - ), - # Column for hist2 - dmc.GridCol( - span=6, - children=[ - title_and_tooltip( - title="Distribution de VDL", - tooltip_text="Distribution des valeurs de Diversité Lichénique (VDL) sur l'ensemble des sites. Si vous cliquez sur un site sur la carte, sa VDL sera affichée en trait pointillé rouge." - ), - dcc.Graph( - id="hist2-vdl", - figure=blank_fig, - style={"height": "300px"}, - config={ - "displaylogo": False, # Remove plotly logo - }, - ), - ], - ), - # Column for hist3 - dmc.GridCol( - span=8, - children=[ - dmc.Card( - children=[ - title_and_tooltip( - title="Espèces observées sur le site sélectionné", - tooltip_text="Distribution des espèces observées sur le site sélectionné" - ), - dcc.Graph( - id="hist3-species", - figure=blank_fig, - style={"height": "300px"}, - config={ - "displaylogo": False, # Remove plotly logo - }, - ), - ], - withBorder=True, - shadow="sm", - ), - ], - ), - # Column for pie chart - dmc.GridCol( - span=4, - children=[ - title_and_tooltip( - title="Morphologie du site sélectionné", - tooltip_text="Distribution des thalles sur le site sélectionné" - ), - dcc.Graph( - id="pie-thallus", - figure=blank_fig, - style={"height": "250px"}, - config={ - "displaylogo": False, # Remove plotly logo - }, - ), - ], - ), - ], - ) - ], + ], + style={ + "display": "flex", + "flexDirection": "column", + "justifyContent": "space-between", + "flexGrow": 1, + "maxHeight": max_height + }, + **CARD_STYLE + ) + +# Reusable component for histogram cards +def histogram_card(title, tooltip_text, graph_id, height="330px"): + return dmc.Card( + children=[ + title_and_tooltip(title, tooltip_text), + dcc.Graph( + id=graph_id, + figure=blank_fig, + style={"height": height}, + config={"displaylogo": False}, ), ], - ), -] + **CARD_STYLE + ) -# Layout for the "Espèces" tab -species_layout = html.Div( # Divider for 2 columns - style={"display": "flex", "gap": "20px"}, +# Layout for the sites (observations) +sites_layout = html.Div( + style=FLEX_COLUMNS_CONTAINER_STYLE, children=[ - # Divider for the first column with selector and map + # First column with map and gauge html.Div( - style={"flex": "5"}, + style={"flex-grow": "1", "flex-basis": "50%"}, children=[ - html.Div( + dmc.Group( [ - html.Label( - "Sélectionner une espèce:", - style={ - "margin-right": "10px", - }, + DashIconify(icon="mdi:calendar", width=26, height=26), + dmc.DatePicker( + id="date-picker-range", + minDate=merged_observation_df["date_obs"].min(), + maxDate=datetime.now().date(), + type="range", + value=[ + merged_observation_df["date_obs"].min(), + datetime.now().date(), + ], + valueFormat="DD/MM/YYYY", + w=170, ), - dcc.Dropdown( - id="species-dropdown", - options=species_options, - value=species_id_selected, - clearable=False, - style={"width": "400px"}, + dmc.Button( + id="reset-date-button", + children="✖", + variant="outline", + color="red", + size="xs", ), ], - style={ - "display": "flex", - "align-items": "center", - "justify-content": "left", - "margin-left": "20px", - }, - ), - dmc.Title( - "Carte de présence de l'espèce sélectionnée", - order=4, - className="graph-title", - style={"padding": "0px"}, + align="center", + gap="xs", + mb="xs", ), - html.Div( + dmc.Card( children=[ - dmc.Card( + dmc.Title("Carte des observations", order=4), + dmc.SegmentedControl( + id="map-column-select", + value=list(MAP_SETTINGS.keys())[0], + data=[ + {"label": MAP_SETTINGS[col]["title"], "value": col} + for col in ["nb_species_cat", "VDL_cat", "deg_pollution_acid_cat", "deg_pollution_azote_cat", "deg_artif_cat"] + ], + transitionDuration=600, + transitionTimingFunction="ease-in-out", + mb="xs" + ), + html.Div( children=[ - dcc.Graph( - id="map-species_present", - figure=blank_fig, - config={ - "displaylogo": False, # Remove plotly logo - }, + dmc.Card( + children=[ + dcc.Graph( + id="map-nb_species-vdl", + figure=blank_fig, + style={"height": "469px"}, + config={"displaylogo": False}, + ), + ], + **MAP_STYLE ), ], - withBorder=True, - shadow="sm", - # Remove padding between the card and the map - style={"padding": "0"}, ), ], + **CARD_STYLE, + mb="md", ), + dmc.Grid( + **GRID_STYLE, + children=[ + dmc.GridCol( + gauge_card( + "% Espèces toxitolérantes", + "Pourcentage d'espèces toxitolérantes sur le site sélectionné", + "gauge-chart1-artif", + ), + span=4, + style={"display": "flex", + "flexDirection": "column"} + ), + dmc.GridCol( + gauge_card( + "% Espèces eutrophes", + "Pourcentage d'espèces eutrophes sur le site sélectionné", + "gauge-chart3-azote", + ), + span=4, + style={"display": "flex", + "flexDirection": "column"} + ), + dmc.GridCol( + gauge_card( + "% Espèces acidophiles", + "Pourcentage d'espèces acidophiles sur le site sélectionné", + "gauge-chart2-acide", + ), + span=4, + style={"display": "flex", + "flexDirection": "column"} + ), + ], + ) ], ), - # Divider for the second column + # Second column with histograms html.Div( - style={"flex": "5"}, + style={"flex-grow": "1", "flex-basis": "50%"}, children=[ dmc.Grid( - gutter="md", - align="stretch", + **GRID_STYLE, children=[ dmc.GridCol( - span=8, + span=6, children=[ - dmc.Card([ - dmc.Title("Carte d'identité de l'espèce sélectionnée", order=4, className="graph-title"), - dmc.Image( - id="lichen-image", - radius="md", - src=None, - h=150, - fallbackSrc="https://placehold.co/600x400?text=No%20image%20found", - ), - ], - withBorder=True, - ) + histogram_card( + "Distribution du nombre d'espèces", + "Distribution du nombre d'espèces par site. Si vous cliquez sur un site sur la carte, son nombre d'espèce sera affiché en trait pointillé rouge.", + "hist1-nb_species", + ), ], ), dmc.GridCol( - span=10, + span=6, children=[ - title_and_tooltip( - title="Espèces les plus observées", - tooltip_text="Distribution des espèces observées sur l'ensemble des sites" + histogram_card( + "Distribution de VDL", + "Distribution des valeurs de Diversité Lichénique (VDL) sur l'ensemble des sites. Si vous cliquez sur un site sur la carte, sa VDL sera affichée en trait pointillé rouge.", + "hist2-vdl", + ), + ], + ), + dmc.GridCol( + span=7, + children=[ + histogram_card( + "Espèces observées sur le site sélectionné", + "Distribution des espèces observées sur le site sélectionné", + "hist3-species", ), - dcc.Graph( - id="hist4-species", - figure=blank_fig, - config={ - "displaylogo": False, # Remove plotly logo - }, + ], + ), + dmc.GridCol( + span=5, + children=[ + histogram_card( + "Morphologie du site sélectionné", + "Distribution des thalles sur le site sélectionné", + "pie-thallus", ), ], ), + ], + ), + ], + ), + ], +) + +urls = [ + "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png", + "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png", + "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png", +] + +images = [dmc.Image(radius="sm", src=url) for url in urls] + +species_card = dmc.Card( + children=[ + dmc.CardSection( + children=[ + dmc.Text(id="species-name", size="lg"), + ], + withBorder=True, + inheritPadding=True, + py="xs", + ), + dmc.Text( + children=[ + "Ce lichen fait partie de la famille des ", + dmc.Text( + id="species-thallus", + c="blue", + style={"display": "inline"}, + ), + " et est classé comme ", + dmc.Text( + id="species-rarity", + c="blue", + style={"display": "inline"}, + ), + ".", + ], + mt="sm", + c="dimmed", + size="sm", + ), + dmc.CardSection( + dmc.Image( + id="species-image", + mt="sm", + src=None, + fallbackSrc="https://placehold.co/600x400?text=No%20image%20found", + ), + ), + dmc.CardSection( + children=[ + dmc.Stack( + [ + dmc.Group( + [ + "Acidité", + dmc.Badge(id="acid-badge", variant="light"), + ] + ), + dmc.Group( + [ + "Eutrophisation", + dmc.Badge(id="eutro-badge", variant="light"), + ] + ), + dmc.Group( + [ + "Poléotolérance", + dmc.Badge(id="poleo-badge", variant="light"), + ] + ), + ], + align="left", + gap="md", + ), + ], + inheritPadding=True, + mt="sm", + pb="md", + ), + ], + withBorder=True, + shadow="sm", + radius="md", + maw=300, + miw=250, +) +# Layout for the "Espèces" tab +species_layout = html.Div( + children=[ + dmc.Select( + id="species-dropdown", + label="Espèce", + description="Sélectionnez une espèce pour afficher les informations", + value=species_id_selected, + data=species_options, + clearable=False, + allowDeselect=False, + searchable=True, + w=400, + ), + dmc.Space(h=10), + html.Div( + style=FLEX_COLUMNS_CONTAINER_STYLE, + children=[ + html.Div(species_card), + html.Div( + style={"flex-basis": "40%", "flex-grow": "1"}, + children=[ + dmc.Card( + children=[ + title_and_tooltip( + title="Carte de présence de l'espèce sélectionnée", + tooltip_text="Carte de présence de l'espèce sélectionnée" + ), + dmc.Card( + children=[ + dcc.Graph( + id="map-species_present", + figure=blank_fig, + config={ + "displaylogo": False, # Remove plotly logo + }, + style={"height": "578px"}, + ), + ], + **MAP_STYLE, + mt="xs", + ), + ], + **CARD_STYLE, + ), + ], + ), + html.Div( + style={"flex-basis": "40%", "flex-grow": "1"}, + children=[ + histogram_card( + "Espèces les plus observées", + "Distribution des espèces observées sur l'ensemble des sites", + "hist4-species", + height="590px", + ), ], ), ], @@ -565,13 +604,18 @@ def title_and_tooltip(title, tooltip_text): # Toggle to switch between light and dark theme theme_toggle = dmc.ActionIcon( [ - dmc.Paper(DashIconify(icon="radix-icons:sun", width=25), darkHidden=True), + dmc.Paper(DashIconify(icon="radix-icons:sun", width=25), darkHidden=True), dmc.Paper(DashIconify(icon="radix-icons:moon", width=25), lightHidden=True), ], variant="transparent", id="color-scheme-toggle", size="lg", - ms="auto", + style={ + "position": "fixed", + "top": "20px", + "right": "26px", + "zIndex": 1000, + } ) @@ -608,29 +652,10 @@ def switch_theme(_, theme): ) -sidebar = dmc.Box( - children=[ - theme_toggle, - dmc.DatePicker( - id="date-picker-range", - label="Sélectionner une plage de dates", - minDate=merged_observation_df["date_obs"].min(), - maxDate=datetime.now().date(), - type="range", - value=[ - merged_observation_df["date_obs"].min(), - datetime.now().date(), - ], - valueFormat="DD/MM/YYYY", - w=110, # width - ), - ], - style={"width": "120px", "padding": "10px"}, -) - dashboards_layout = dmc.Box( children=[ dmc.Accordion( + id="accordion", disableChevronRotation=True, chevronPosition="left", variant="contained", @@ -639,32 +664,31 @@ def switch_theme(_, theme): dmc.AccordionItem( children=[ dmc.AccordionControl( - dmc.Group( - children=[ - DashIconify( - icon="tabler:map-pin", - height=20, - color=BASE_COLOR_PALETTE[0] - ), - dmc.Title("Sites", order=3), - dmc.Tooltip( - label="Cliquez sur un site pour découvrir ce que les lichens peuvent nous apprendre", - position="right", - withArrow=True, - children=DashIconify( - icon="material-symbols:info-outline", - className="info-icon", - ) - ), - ], - align="center", - ), - ), - dmc.AccordionPanel(sites_layout), - ], - value="sites", - ), - dmc.AccordionItem( + [ + dmc.Group( + children=[ + DashIconify( + icon="tabler:map-pin", + height=25, + color=BASE_COLOR_PALETTE[0] + ), + dmc.Tooltip( + label="Cliquez sur un site pour découvrir ce que les lichens peuvent nous apprendre", + position="right", + withArrow=True, + children=dmc.Title( + "Sites", order=3) + ), + ], + align="center", + ), + ], + ), + dmc.AccordionPanel(sites_layout), + ], + value="sites", + ), + dmc.AccordionItem( children=[ dmc.AccordionControl( dmc.Group( @@ -699,9 +723,8 @@ def switch_theme(_, theme): children=[ dmc.Group( children=[ - sidebar, - dmc.Divider(orientation="vertical"), dashboards_layout, + theme_toggle, ], align="start", ) diff --git a/Dashboards/my_data/computed_datasets.py b/Dashboards/my_data/computed_datasets.py index 594e142..d6f93f2 100644 --- a/Dashboards/my_data/computed_datasets.py +++ b/Dashboards/my_data/computed_datasets.py @@ -109,7 +109,7 @@ def unique_lichen_name(nb_lichen_per_lichen_id_df): -# Count the number of lichen (species) for each observation +# Count the number of lichen species for each observation def count_species_per_observation(lichen_df, observation_df): # Count the number of different lichen (=lines in the df) per observation count_species_per_observation_df = lichen_df['observation_id'].value_counts().to_frame().rename(columns={"count":"nb_species"}) @@ -200,6 +200,7 @@ def calc_degrees_pollution(merged_table_with_nb_lichen_df, lichen_df, merged_lic # Calculate the ratio of resistant nb_lichen to total nb_lichen merged_df['deg_artif'] = merged_df['nb_lichen_resistant'] / merged_df['nb_lichen'] + # Add a categorical column based on the number of lichen # Calculate the degree of acid pollution merged_df['deg_pollution_acid'] = merged_df['nb_lichen_acid'] / merged_df['nb_lichen'] @@ -207,4 +208,8 @@ def calc_degrees_pollution(merged_table_with_nb_lichen_df, lichen_df, merged_lic # Calculate the degree of nitrogen (azote in french) pollution merged_df['deg_pollution_azote'] = merged_df['nb_lichen_eutrophic'] / merged_df['nb_lichen'] + # Add categorical columns + for col in ['deg_artif', 'deg_pollution_acid', 'deg_pollution_azote']: + merged_df[col + '_cat'] = pd.cut(merged_df[col], bins=[-0.1, 0.25, 0.5, 0.75, np.inf], labels=["0-25%", "25-50%", "50-75%", "75-100%"]) + return merged_df diff --git a/poetry.lock b/poetry.lock index b819e05..92823a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "anyio" @@ -2164,13 +2164,13 @@ type = ["mypy (>=1.8)"] [[package]] name = "plotly" -version = "5.22.0" +version = "5.24.1" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.22.0-py3-none-any.whl", hash = "sha256:68fc1901f098daeb233cc3dd44ec9dc31fb3ca4f4e53189344199c43496ed006"}, - {file = "plotly-5.22.0.tar.gz", hash = "sha256:859fdadbd86b5770ae2466e542b761b247d1c6b49daed765b95bb8c7063e7469"}, + {file = "plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089"}, + {file = "plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae"}, ] [package.dependencies]