diff --git a/.gitignore b/.gitignore index 368d112..50c2cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,8 @@ __pycache__/ venv/ .venv/ Local/ -HTML +HTML/ +static/ # Ignorer données Fiches (adapté à ton projet) Instructions.md diff --git a/README.md b/README.md index 1e2bdfa..4fb2eba 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ fabnum-dev/ │ │ └── README.md # Documentation du module │ └── visualisations/ # Visualisations graphiques │ ├── interface.py # Interface des visualisations +│ ├── graphes.py # Gestion des graphes à visualiser │ └── README.md # Documentation du module ├── components/ # Composants d'interface réutilisables │ ├── sidebar.py # Barre latérale de navigation diff --git a/app/analyse/interface.py b/app/analyse/interface.py index 54a5ab5..b8a68c2 100644 --- a/app/analyse/interface.py +++ b/app/analyse/interface.py @@ -37,7 +37,7 @@ def selectionner_niveaux(): niveau_depart = st.selectbox("Niveau de départ", niveau_choix, key="analyse_niveau_depart") if niveau_depart == valeur_defaut: return None, None - + niveau_depart_int = inverse_niveau_labels[niveau_depart] niveaux_arrivee_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart_int] niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles @@ -45,7 +45,7 @@ def selectionner_niveaux(): analyse_niveau_arrivee = st.selectbox("Niveau d'arrivée", niveaux_arrivee_choix, key="analyse_niveau_arrivee") if analyse_niveau_arrivee == valeur_defaut: return niveau_depart_int, None - + niveau_arrivee_int = inverse_niveau_labels[analyse_niveau_arrivee] return niveau_depart_int, niveau_arrivee_int @@ -66,7 +66,7 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee): minerais_nodes, key="analyse_minerais" ) - + return minerais_selection @@ -78,16 +78,16 @@ def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee): depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart] arrivee_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_arrivee] - noeuds_depart = st.multiselect("Filtrer par noeuds de départ (optionnel)", - sorted(depart_nodes), + noeuds_depart = st.multiselect("Filtrer par noeuds de départ (optionnel)", + sorted(depart_nodes), key="analyse_noeuds_depart") - noeuds_arrivee = st.multiselect("Filtrer par noeuds d'arrivée (optionnel)", - sorted(arrivee_nodes), + noeuds_arrivee = st.multiselect("Filtrer par noeuds d'arrivée (optionnel)", + sorted(arrivee_nodes), key="analyse_noeuds_arrivee") noeuds_depart = noeuds_depart if noeuds_depart else None noeuds_arrivee = noeuds_arrivee if noeuds_arrivee else None - + return noeuds_depart, noeuds_arrivee @@ -96,51 +96,62 @@ def configurer_filtres_vulnerabilite(): st.markdown("---") st.markdown("## Sélection des filtres pour identifier les vulnérabilités") - filtrer_ics = st.checkbox("Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", + filtrer_ics = st.checkbox("Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", key="analyse_filtrer_ics") - filtrer_ivc = st.checkbox("Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", + filtrer_ivc = st.checkbox("Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", key="analyse_filtrer_ivc") - filtrer_ihh = st.checkbox("Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", + filtrer_ihh = st.checkbox("Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", key="analyse_filtrer_ihh") ihh_type = "Pays" if filtrer_ihh: - ihh_type = st.radio("Appliquer le filtre IHH sur :", - ["Pays", "Acteurs"], - horizontal=True, + ihh_type = st.radio("Appliquer le filtre IHH sur :", + ["Pays", "Acteurs"], + horizontal=True, key="analyse_ihh_type") - filtrer_isg = st.checkbox("Filtrer les chemins contenant un pays instable (ISG ≥ 60)", + filtrer_isg = st.checkbox("Filtrer les chemins contenant un pays instable (ISG ≥ 60)", key="analyse_filtrer_isg") - logique_filtrage = st.radio("Logique de filtrage", - ["OU", "ET"], - horizontal=True, + logique_filtrage = st.radio("Logique de filtrage", + ["OU", "ET"], + horizontal=True, key="analyse_logique_filtrage") - + return filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage def interface_analyse(G_temp): + st.markdown("# Analyse du graphe") + with st.expander("Comment utiliser cet onglet ?", expanded=False): + st.markdown(""" + 1. Sélectionnez le niveau de départ (produit final, composant ou minerai) + 2. Choisissez le niveau d'arrivée souhaité + 3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel) + 4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents + 5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse + 6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe + """) + st.markdown("---") + try: - st.markdown("# Analyse") - + # Préparation du graphe G_temp, niveaux_temp = preparer_graphe(G_temp) - + # Sélection des niveaux niveau_depart, niveau_arrivee = selectionner_niveaux() if niveau_depart is None or niveau_arrivee is None: return - + # Sélection des minerais si nécessaire minerais_selection = selectionner_minerais(G_temp, niveau_depart, niveau_arrivee) - + # Sélection fine des noeuds noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart, niveau_arrivee) - + # Configuration des filtres de vulnérabilité filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage = configurer_filtres_vulnerabilite() - + # Lancement de l'analyse st.markdown("---") if st.button("Lancer l'analyse", type="primary", key="analyse_lancer"): diff --git a/app/fiches/generer.py b/app/fiches/generer.py index 5d6bc60..a13a045 100644 --- a/app/fiches/generer.py +++ b/app/fiches/generer.py @@ -5,6 +5,8 @@ import markdown from bs4 import BeautifulSoup from latex2mathml.converter import convert as latex_to_mathml from .utils.fiche_utils import render_fiche_markdown +import pypandoc +import streamlit as st from .utils.dynamic import ( build_dynamic_sections, @@ -113,13 +115,31 @@ def generer_fiche(md_source, dossier, nom_fichier, seuils): elif type_fiche == "minerai": md_source = build_minerai_sections(md_source) - contenu_md = render_fiche_markdown(md_source, seuils) + contenu_md = render_fiche_markdown(md_source, seuils, license_path="assets/licence.md") md_path = os.path.join("Fiches", dossier, nom_fichier) os.makedirs(os.path.dirname(md_path), exist_ok=True) with open(md_path, "w", encoding="utf-8") as f: f.write(contenu_md) + # Génération automatique du PDF + pdf_dir = os.path.join("static", "Fiches", dossier) + os.makedirs(pdf_dir, exist_ok=True) + + # Construire le chemin PDF correspondant (même nom que .md, mais .pdf) + nom_pdf = os.path.splitext(nom_fichier)[0] + ".pdf" + pdf_path = os.path.join(pdf_dir, nom_pdf) + + try: + pypandoc.convert_file( + md_path, + to="pdf", + outputfile=pdf_path, + extra_args=["--pdf-engine=xelatex", "-V", "geometry:margin=2cm"] + ) + except Exception as e: + st.error(f"[ERREUR] Génération PDF échouée pour {md_path}: {e}") + html_output = rendu_html(contenu_md) html_dir = os.path.join("HTML", dossier) diff --git a/app/fiches/interface.py b/app/fiches/interface.py index 38973e3..aeaf6b2 100644 --- a/app/fiches/interface.py +++ b/app/fiches/interface.py @@ -16,8 +16,21 @@ from .utils.fiche_utils import load_seuils, doit_regenerer_fiche from .generer import generer_fiche def interface_fiches(): - st.markdown("# Affichage des fiches") - st.markdown("Sélectionner d'abord l'opération que vous souhaitez examiner et ensuite choisisez la fiche à lire.") + st.markdown("# Découverte des fiches") + with st.expander("Comment utiliser cet onglet ?", expanded=False): + st.markdown(""" + 1. Parcourez la liste des fiches disponibles par catégorie + 2. Sélectionnez une fiche pour afficher son contenu complet + 3. Consultez les données détaillées, graphiques et analyses supplémentaires + 4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées + + Les catégories sont les suivantes : + * Assemblage : opération d'assemblage des produits finaux à partir des composants + * Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition + * Criticités : indices utilisés pour identifier et évaluer les vulnérabilités + * Fabrication : opération de fabrication des composants à partir de minerais + * Minerai : description et opérations d'extraction et de traitement des minerais + """) st.markdown("---") if "fiches_arbo" not in st.session_state: @@ -29,7 +42,7 @@ def interface_fiches(): return dossiers = sorted(arbo.keys(), key=lambda x: x.lower()) - dossier_choisi = st.selectbox("Choisissez un dossier", ["-- Sélectionner un dossier --"] + dossiers) + dossier_choisi = st.selectbox("Choisissez une catégorie de fiches", ["-- Sélectionner un dossier --"] + dossiers) if dossier_choisi and dossier_choisi != "-- Sélectionner un dossier --": fiches = arbo.get(dossier_choisi, []) diff --git a/app/fiches/utils/fiche_utils.py b/app/fiches/utils/fiche_utils.py index 7a6892a..a20e8d5 100644 --- a/app/fiches/utils/fiche_utils.py +++ b/app/fiches/utils/fiche_utils.py @@ -38,10 +38,11 @@ def _migrate_metadata(meta: Dict) -> Dict: return meta -def render_fiche_markdown(md_text: str, seuils: Dict) -> str: - """Renvoie la fiche rendue (Markdown) : - – placeholders Jinja2 remplacés ({{ … }}) - – table seuils injectée via dict 'seuils'. +def render_fiche_markdown(md_text: str, seuils: Dict, license_path: str = "assets/licence.md") -> str: + """Renvoie la fiche rendue (Markdown) : + – placeholders Jinja2 remplacés ({{ … }}) + – table seuils injectée via dict 'seuils'. + - licence ajoutée après le tableau de version et avant le premier titre de niveau 2 """ post = frontmatter.loads(md_text) meta = _migrate_metadata(dict(post.metadata)) @@ -64,6 +65,24 @@ def render_fiche_markdown(md_text: str, seuils: Dict) -> str: {rendered_body}""" + # Charger le contenu de la licence + try: + license_content = pathlib.Path(license_path).read_text(encoding="utf-8") + + # Insérer la licence après le tableau de version et avant le premier titre h2 + # Trouver la position du premier titre h2 + h2_match = re.search(r"^## ", rendered_body, flags=re.M) + + if h2_match: + h2_position = h2_match.start() + rendered_body = f"{rendered_body[:h2_position]}\n\n{license_content}\n\n{rendered_body[h2_position:]}" + else: + # S'il n'y a pas de titre h2, ajouter la licence à la fin + rendered_body = f"{rendered_body}\n\n{license_content}" + except Exception as e: + # En cas d'erreur lors de la lecture du fichier de licence, continuer sans l'ajouter + pass + return rendered_body diff --git a/app/personnalisation/interface.py b/app/personnalisation/interface.py index 4a93d92..2993cb9 100644 --- a/app/personnalisation/interface.py +++ b/app/personnalisation/interface.py @@ -6,16 +6,17 @@ from .modification import modifier_produit from .import_export import importer_exporter_graph def interface_personnalisation(G): - st.markdown(""" -# Personnalisation des produits finaux - -Dans cette section, vous pouvez ajouter des produits finaux qui ne sont pas présents dans la liste, -par exemple des produits que vous concevez vous-même. - -Vous pouvez aussi enregistrer ou recharger vos modifications. - ---- -""") + st.markdown("# Personnalisation des produits finaux") + with st.expander("Comment utiliser cet onglet ?", expanded=False): + st.markdown(""" + 1. Cliquez sur "Ajouter un produit final" pour créer un nouveau produit + 2. Donnez un nom à votre produit + 3. Sélectionnez une opération d'assemblage appropriée (si pertinent) + 4. Choisissez les composants qui constituent votre produit dans la liste proposée + 5. Sauvegardez votre configuration pour une réutilisation future + 6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés + """) + st.markdown("---") G = ajouter_produit(G) G = modifier_produit(G) diff --git a/app/visualisations/graphes.py b/app/visualisations/graphes.py new file mode 100644 index 0000000..2bf2aad --- /dev/null +++ b/app/visualisations/graphes.py @@ -0,0 +1,178 @@ +import streamlit as st +import altair as alt +import numpy as np +from collections import Counter +import pandas as pd + + +def afficher_graphique_altair(df): + ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction'] + categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()] + for cat in categories: + st.markdown(f"### {cat}") + df_cat = df[df['categorie'] == cat].copy() + + coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1))) + counts = Counter(coord_pairs) + + offset_x = [] + offset_y = {} + seen = Counter() + for pair in coord_pairs: + rank = seen[pair] + seen[pair] += 1 + if counts[pair] > 1: + angle = rank * 1.5 + radius = 0.8 + 0.4 * rank + offset_x.append(radius * np.cos(angle)) + offset_y[pair] = radius * np.sin(angle) + else: + offset_x.append(0) + offset_y[pair] = 0 + + df_cat['ihh_pays'] += offset_x + df_cat['ihh_acteurs'] += [offset_y[p] for p in coord_pairs] + df_cat['ihh_pays_text'] = df_cat['ihh_pays'] + 0.5 + df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5 + + base = alt.Chart(df_cat).encode( + x=alt.X('ihh_pays:Q', title='IHH Pays (%)'), + y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'), + size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), + color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])) + ) + + points = base.mark_circle(opacity=0.6) + lines = alt.Chart(df_cat).mark_rule(strokeWidth=0.5, color='gray').encode( + x='ihh_pays:Q', x2='ihh_pays_text:Q', + y='ihh_acteurs:Q', y2='ihh_acteurs_text:Q' + ) + + labels = alt.Chart(df_cat).mark_text( + align='left', dx=3, dy=-3, fontSize=8, font='Arial', angle=335 + ).encode( + x='ihh_pays_text:Q', + y='ihh_acteurs_text:Q', + text='nom:N' + ) + + hline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15)) + hline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25)) + hline_100 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='white').encode(y=alt.datum(100)) + vline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15)) + vline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25)) + vline_100 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='white').encode(x=alt.datum(100)) + + chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties( + width=500, + height=400, + title=f"Concentration et criticité – {cat}" + ).interactive() + + st.altair_chart(chart, use_container_width=True) + + +def creer_graphes(donnees): + if not donnees: + st.warning("Aucune donnée à afficher.") + return + + try: + df = pd.DataFrame(donnees) + df['ivc_cat'] = df['ivc'].apply(lambda x: 1 if x <= 15 else (2 if x <= 30 else 3)) + + from collections import Counter + coord_pairs = list(zip(df['ihh_extraction'].round(1), df['ihh_reserves'].round(1))) + counts = Counter(coord_pairs) + + offset_x, offset_y = [], {} + seen = Counter() + for pair in coord_pairs: + rank = seen[pair] + seen[pair] += 1 + if counts[pair] > 1: + angle = rank * 1.5 + radius = 0.8 + 0.4 * rank + offset_x.append(radius * np.cos(angle)) + offset_y[pair] = radius * np.sin(angle) + else: + offset_x.append(0) + offset_y[pair] = 0 + + df['ihh_extraction'] += offset_x + df['ihh_reserves'] += [offset_y[p] for p in coord_pairs] + df['ihh_extraction_text'] = df['ihh_extraction'] + 0.5 + df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5 + + base = alt.Chart(df).encode( + x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'), + y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'), + size=alt.Size('ivc_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), + color=alt.Color('ivc_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])), + tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q'] + ) + + points = base.mark_circle(opacity=0.6) + lines = alt.Chart(df).mark_rule(strokeWidth=0.5, color='gray').encode( + x='ihh_extraction:Q', x2='ihh_extraction_text:Q', + y='ihh_reserves:Q', y2='ihh_reserves_text:Q' + ) + + labels = alt.Chart(df).mark_text( + align='left', dx=10, dy=-10, fontSize=10, font='Arial', angle=335 + ).encode( + x='ihh_extraction_text:Q', + y='ihh_reserves_text:Q', + text='nom:N' + ) + + hline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15)) + hline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25)) + hline_100 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(100)) + vline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15)) + vline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25)) + vline_100 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(100)) + + chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties( + width=600, + height=500, + title="Concentration des ressources critiques vs vulnérabilité IVC" + ).interactive() + + st.altair_chart(chart, use_container_width=True) + + except Exception as e: + st.error(f"Erreur lors de la création du graphique : {e}") + + +def lancer_visualisation_ihh_criticite(graph): + try: + import networkx as nx + from utils.graph_utils import recuperer_donnees + + niveaux = nx.get_node_attributes(graph, "niveau") + noeuds = [n for n, v in niveaux.items() if v == "10" and "Reserves" not in n] + noeuds.sort() + + df = recuperer_donnees(graph, noeuds) + if df.empty: + st.warning("Aucune donnée à visualiser.") + else: + afficher_graphique_altair(df) + except Exception as e: + st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}") + + +def lancer_visualisation_ihh_ivc(graph): + try: + from utils.graph_utils import recuperer_donnees_2 + noeuds_niveau_2 = [ + n for n, data in graph.nodes(data=True) + if data.get("niveau") == "2" and "ivc" in data + ] + if not noeuds_niveau_2: + return + data = recuperer_donnees_2(graph, noeuds_niveau_2) + creer_graphes(data) + except Exception as e: + st.error(f"Erreur dans la visualisation IHH vs IVC : {e}") diff --git a/app/visualisations/interface.py b/app/visualisations/interface.py index 6c74ac1..266dff7 100644 --- a/app/visualisations/interface.py +++ b/app/visualisations/interface.py @@ -1,185 +1,31 @@ import streamlit as st -import altair as alt -import numpy as np -from collections import Counter -import pandas as pd +from .graphes import ( + lancer_visualisation_ihh_criticite, + lancer_visualisation_ihh_ivc +) -def afficher_graphique_altair(df): - ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction'] - categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()] - for cat in categories: - st.markdown(f"### {cat}") - df_cat = df[df['categorie'] == cat].copy() - - coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1))) - counts = Counter(coord_pairs) - - offset_x = [] - offset_y = {} - seen = Counter() - for pair in coord_pairs: - rank = seen[pair] - seen[pair] += 1 - if counts[pair] > 1: - angle = rank * 1.5 - radius = 0.8 + 0.4 * rank - offset_x.append(radius * np.cos(angle)) - offset_y[pair] = radius * np.sin(angle) - else: - offset_x.append(0) - offset_y[pair] = 0 - - df_cat['ihh_pays'] += offset_x - df_cat['ihh_acteurs'] += [offset_y[p] for p in coord_pairs] - df_cat['ihh_pays_text'] = df_cat['ihh_pays'] + 0.5 - df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5 - - base = alt.Chart(df_cat).encode( - x=alt.X('ihh_pays:Q', title='IHH Pays (%)'), - y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'), - size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), - color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])) - ) - - points = base.mark_circle(opacity=0.6) - lines = alt.Chart(df_cat).mark_rule(strokeWidth=0.5, color='gray').encode( - x='ihh_pays:Q', x2='ihh_pays_text:Q', - y='ihh_acteurs:Q', y2='ihh_acteurs_text:Q' - ) - - labels = alt.Chart(df_cat).mark_text( - align='left', dx=3, dy=-3, fontSize=8, font='Arial', angle=335 - ).encode( - x='ihh_pays_text:Q', - y='ihh_acteurs_text:Q', - text='nom:N' - ) - - hline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15)) - hline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25)) - vline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15)) - vline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25)) - - chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties( - width=500, - height=400, - title=f"Concentration et criticité – {cat}" - ).interactive() - - st.altair_chart(chart, use_container_width=True) - - -def creer_graphes(donnees): - if not donnees: - st.warning("Aucune donnée à afficher.") - return - - try: - df = pd.DataFrame(donnees) - df['ivc_cat'] = df['ivc'].apply(lambda x: 1 if x <= 15 else (2 if x <= 30 else 3)) - - from collections import Counter - coord_pairs = list(zip(df['ihh_extraction'].round(1), df['ihh_reserves'].round(1))) - counts = Counter(coord_pairs) - - offset_x, offset_y = [], {} - seen = Counter() - for pair in coord_pairs: - rank = seen[pair] - seen[pair] += 1 - if counts[pair] > 1: - angle = rank * 1.5 - radius = 0.8 + 0.4 * rank - offset_x.append(radius * np.cos(angle)) - offset_y[pair] = radius * np.sin(angle) - else: - offset_x.append(0) - offset_y[pair] = 0 - - df['ihh_extraction'] += offset_x - df['ihh_reserves'] += [offset_y[p] for p in coord_pairs] - df['ihh_extraction_text'] = df['ihh_extraction'] + 0.5 - df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5 - - base = alt.Chart(df).encode( - x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'), - y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'), - size=alt.Size('ivc_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), - color=alt.Color('ivc_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])), - tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q'] - ) - - points = base.mark_circle(opacity=0.6) - lines = alt.Chart(df).mark_rule(strokeWidth=0.5, color='gray').encode( - x='ihh_extraction:Q', x2='ihh_extraction_text:Q', - y='ihh_reserves:Q', y2='ihh_reserves_text:Q' - ) - - labels = alt.Chart(df).mark_text( - align='left', dx=10, dy=-10, fontSize=10, font='Arial', angle=335 - ).encode( - x='ihh_extraction_text:Q', - y='ihh_reserves_text:Q', - text='nom:N' - ) - - hline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15)) - hline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25)) - vline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15)) - vline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25)) - - chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties( - width=600, - height=500, - title="Concentration des ressources critiques vs vulnérabilité IVC" - ).interactive() - - st.altair_chart(chart, use_container_width=True) - - except Exception as e: - st.error(f"Erreur lors de la création du graphique : {e}") - - -def lancer_visualisation_ihh_criticite(graph): - try: - import networkx as nx - from utils.graph_utils import recuperer_donnees - - niveaux = nx.get_node_attributes(graph, "niveau") - noeuds = [n for n, v in niveaux.items() if v == "10" and "Reserves" not in n] - noeuds.sort() - - df = recuperer_donnees(graph, noeuds) - if df.empty: - st.warning("Aucune donnée à visualiser.") - else: - afficher_graphique_altair(df) - except Exception as e: - st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}") - - -def lancer_visualisation_ihh_ivc(graph): - try: - from utils.graph_utils import recuperer_donnees_2 - noeuds_niveau_2 = [ - n for n, data in graph.nodes(data=True) - if data.get("niveau") == "2" and "ivc" in data - ] - if not noeuds_niveau_2: - return - data = recuperer_donnees_2(graph, noeuds_niveau_2) - creer_graphes(data) - except Exception as e: - st.error(f"Erreur dans la visualisation IHH vs IVC : {e}") def interface_visualisations(G_temp, G_temp_ivc): - st.markdown("# Visualisations") + st.markdown("# Analyse du graphe") + with st.expander("Comment utiliser cet onglet ?", expanded=False): + st.markdown(""" + 1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH) + 2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC) + 3. Zoomer dans les graphes pour mieux découvrir les informations + + Il est important de se rappeler que l'IHH a deux seuils : + * en-dessous de 15, la concentration est considérée comme étant faible + * au-dessus de 25, elle est considérée comme étant forte + + Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés. + Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils. + """) + st.markdown("---") + st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs Criticité -Entre 0 et 15%, concentration faible, entre 15 et 25%, modérée, au-delà, forte. - -Taille des points = criticité substituabilité du minerai +La taille des points donne l'indication de la criticité de substituabilité du minerai. """) if st.button("Lancer", key="btn_ihh_criticite"): try: @@ -189,9 +35,7 @@ Taille des points = criticité substituabilité du minerai st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs IVC -Entre 0 et 15%, concentration faible, entre 15 et 25%, modérée, au-delà, forte. - -Taille des points = criticité concurrentielle du minerai +La taille des points donne l'indication de la criticité concurrentielle du minerai. """) if st.button("Lancer", key="btn_ihh_ivc"): diff --git a/assets/licence.md b/assets/licence.md new file mode 100644 index 0000000..e7e86bf --- /dev/null +++ b/assets/licence.md @@ -0,0 +1,3 @@ +Document généré avec [FabNum](https://fabnum.peccini.fr) par [Stéphan Peccini](mailto:stephan-pro@peccini.fr) + +Licence : [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr) - Attribution - Utilisation non commerciale - Pas d’Œuvre dérivée diff --git a/components/footer.py b/components/footer.py index f6c330b..b15af81 100644 --- a/components/footer.py +++ b/components/footer.py @@ -10,7 +10,7 @@ def afficher_pied_de_page():