diff --git a/.gitignore b/.gitignore index fb52b72..bf55d34 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ Local/ HTML/ # Ignorer données Fiches (adapté à ton projet) -Instructions.md Fiches/ HTML/ static/Fiches/ diff --git a/Instructions.md b/Instructions.md new file mode 100644 index 0000000..8b995ef --- /dev/null +++ b/Instructions.md @@ -0,0 +1,88 @@ +# Bienvenue dans l'application Fabnum + +## Présentation de l'application + +Cet outil interactif vous permet d'explorer et d'analyser les vulnérabilités de la chaîne de fabrication du numérique. Grâce à une interface, espérons-le, intuitive, vous pourrez visualiser les différentes étapes de production, identifier les points critiques et comprendre les enjeux géopolitiques et plus liés à la fabrication des technologies numériques. + +L'application vous offre diverses fonctionnalités : + +* Visualisation des données et des graphiques +* Personnalisation des produits à analyser +* Exploration détaillée des indices de vulnérabilité et des opérations +* Possibilité d'exporter les résultats selon différents formats selon autorisation + +## Comprendre la chaîne de fabrication numérique + +### Structure de la chaîne +La chaîne de fabrication numérique se décompose en trois niveaux hiérarchiques : + +* Produits finaux : appareils complets comme les smartphones, ordinateurs, serveurs, etc. +* Composants : éléments constitutifs comme les processeurs, écrans, capteurs, batteries, etc. +* Minerais et matériaux : ressources de base nécessaires à la fabrication des composants + +### Opérations à chaque niveau +Chaque niveau de la chaîne implique des opérations spécifiques : + +* Niveau Produit final : assemblage des composants pour créer le produit fini +* Niveau Composant : fabrication des pièces à partir des minerais et matériaux +* Niveau Minerai : extraction et traitement des ressources premières + +### Dimension géopolitique +Pour chaque opération, l'application détaille : + +* Les pays où l'opération est réalisée, avec leur part respective du marché mondial +* Les acteurs économiques impliqués dans chaque pays, avec leur part de marché +* Les liens entre les pays d'opération, les acteurs et leur contexte géopolitique + +[Voir la vision détaillée du projet](https://fabnum-dev.peccini.fr/app/static/Description/Objectif%20final.pdf) + +## Comprendre les indices de vulnérabilité + +L'application utilise quatre indices clés pour évaluer les vulnérabilités dans la chaîne de fabrication numérique : + +### Indice de Herfindahl-Hirschmann (IHH) + +* Que mesure-t-il ? La concentration géographique ou industrielle d'une opération. +* Comment l'interpréter ? Plus l'indice est élevé, plus l'opération est concentrée dans un nombre limité de pays, ce qui augmente la vulnérabilité. +* Où le trouver ? Associé à chaque opération dans les graphiques d'analyse. + +[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20IHH.pdf) + +### Indice de Vulnérabilité Concurrentielle (IVC) + +* Que mesure-t-il ? La pression exercée par d'autres secteurs industriels sur une même ressource minérale. +* Comment l'interpréter ? Un IVC élevé indique une forte compétition pour accéder à ce minerai, augmentant le risque de pénurie. +* Où le trouver ? Associé à chaque minerai dans les visualisations et fiches. + +[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20IVC.pdf) + +### Indice de Criticité de Substituabilité (ICS) + +* Que mesure-t-il ? La possibilité de remplacer un minerai par un autre dans la fabrication d'un composant. Plus rarement, il matérialise la capacité de remplacer un procédé. +* Comment l'interpréter ? Un ICS élevé signifie qu'il est difficile de trouver une alternative à ce minerai. +* Où le trouver ? Associé à la relation entre composants et minerais. + +[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20ICS.pdf) + +### Indice de Stabilité Géopolitique (ISG) + +* Que mesure-t-il ? La stabilité politique, économique et sociale d'un pays, basée sur trois sous-indicateurs. +* Comment l'interpréter ? Un ISG élevé indique un pays instable, ce qui augmente les risques d'approvisionnement. +* Où le trouver ? Utilisé pour pondérer les risques identifiés tout au long de la chaîne. + +[Voir la fiche détaillée](https://fabnum-dev.peccini.fr/app/static/Fiches/Criticit%C3%A9s/Fiche%20technique%20ISG.pdf) + +## Guide d'utilisation de l'application + +L'application est organisée en quatre onglets principaux, chacun offrant une perspective différente sur la chaîne de fabrication numérique : + +* Onglet Personnalisation : Créer et gérer des produits finaux personnalisés pour des analyses spécifiques. + * À noter : Les produits personnalisés sont temporaires par défaut, mais peuvent être sauvegardés pour une utilisation ultérieure. +* Onglet Analyse : Explorer visuellement les relations entre les différents niveaux de la chaîne de fabrication. + * Exemple d'utilisation : Pour comprendre les vulnérabilités liées aux composants d'un smartphone, sélectionnez « Produit final » comme niveau de départ, « Composant » comme niveau d'arrivée, puis spécifiez « Smartphone » comme item de produit final. +* Onglet Visualisations : Observer les corrélations entre les différents indices et comprendre les tendances globales. + * Indicateurs clés : Portez attention aux points situés dans les zones de haute valeur pour les deux indices, car ils représentent les vulnérabilités les plus critiques. +* Onglet Fiches : Accéder à des informations détaillées sur chaque opération et minerai. + * À explorer : Les fiches contiennent souvent des informations qui ne sont pas visibles directement dans les graphiques, comme des tendances historiques ou des prévisions futures ; n'hésitez pas à les consulter + +En début de chacun des onglets, vous trouverez un mini-guide spécifique sur leur utilisation. diff --git a/app/analyse/interface.py b/app/analyse/interface.py index 093b56e..466f965 100644 --- a/app/analyse/interface.py +++ b/app/analyse/interface.py @@ -1,5 +1,6 @@ import streamlit as st from utils.translations import _ +from utils.widgets import html_expander from .sankey import afficher_sankey @@ -123,8 +124,7 @@ def configurer_filtres_vulnerabilite(): def interface_analyse(G_temp): st.markdown(f"# {str(_('pages.analyse.title'))}") - with st.expander(str(_("pages.analyse.help")), expanded=False): - st.markdown("\n".join(_("pages.analyse.help_content"))) + html_expander(f"{str(_('pages.analyse.help'))}", content="\n".join(_("pages.analyse.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") try: diff --git a/app/fiches/interface.py b/app/fiches/interface.py index 44596c3..d990df7 100644 --- a/app/fiches/interface.py +++ b/app/fiches/interface.py @@ -2,7 +2,6 @@ import streamlit as st import requests import os -import pathlib from utils.translations import _ from .utils.tickets.display import afficher_tickets_par_fiche @@ -16,11 +15,11 @@ from utils.gitea import charger_arborescence_fiches from .utils.fiche_utils import load_seuils, doit_regenerer_fiche from .generer import generer_fiche +from utils.widgets import html_expander def interface_fiches(): st.markdown(f"# {str(_('pages.fiches.title'))}") - with st.expander(str(_("pages.fiches.help")), expanded=False): - st.markdown("\n".join(_("pages.fiches.help_content"))) + html_expander(f"{str(_('pages.fiches.help'))}", content="\n".join(_("pages.fiches.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") if "fiches_arbo" not in st.session_state: @@ -102,4 +101,4 @@ def interface_fiches(): formulaire_creation_ticket_dynamique(fiche_choisie) except Exception as e: - st.error(f"{str(_('pages.fiches.loading_error', 'Erreur lors du chargement de la fiche :'))} {e}") + st.error(f"{str(_('pages.fiches.loading_error'))} {e}") diff --git a/app/personnalisation/interface.py b/app/personnalisation/interface.py index bcaae19..9e27d1d 100644 --- a/app/personnalisation/interface.py +++ b/app/personnalisation/interface.py @@ -5,11 +5,11 @@ from utils.translations import _ from .ajout import ajouter_produit from .modification import modifier_produit from .import_export import importer_exporter_graph +from utils.widgets import html_expander def interface_personnalisation(G): st.markdown(f"# {str(_('pages.personnalisation.title'))}") - with st.expander(str(_("pages.personnalisation.help")), expanded=False): - st.markdown("\n".join(_("pages.personnalisation.help_content"))) + html_expander(f"{str(_('pages.personnalisation.help'))}", content="\n".join(_("pages.personnalisation.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") G = ajouter_produit(G) diff --git a/app/visualisations/interface.py b/app/visualisations/interface.py index 49d9db8..d82e781 100644 --- a/app/visualisations/interface.py +++ b/app/visualisations/interface.py @@ -1,4 +1,5 @@ import streamlit as st +from utils.widgets import html_expander from utils.translations import _ from .graphes import ( @@ -9,8 +10,7 @@ from .graphes import ( def interface_visualisations(G_temp, G_temp_ivc): st.markdown(f"# {str(_('pages.visualisations.title'))}") - with st.expander(str(_("pages.visualisations.help")), expanded=False): - st.markdown("\n".join(_("pages.visualisations.help_content"))) + html_expander(f"{str(_('pages.visualisations.help'))}", content="\n".join(_("pages.visualisations.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality"))} diff --git a/assets/styles/base.css b/assets/styles/base.css index dcbda92..0663f09 100644 --- a/assets/styles/base.css +++ b/assets/styles/base.css @@ -4,33 +4,33 @@ 1. Reset et base ========================================== */ .stAppHeader { - visibility: hidden; + visibility: hidden; } body, html { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, - sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif; } body, .stApp, .block-container { - background-color: var(--bg-color) !important; - color: var(--text-color) !important; + background-color: var(--bg-color) !important; + color: var(--text-color) !important; } /* ========================================== 2. Layout et containers ========================================== */ .block-container { - max-width: 1024px !important; - padding: 0 1rem 10rem; + max-width: 1024px !important; + padding: 0 1rem 10rem; } .stVerticalBlock { - gap: 0.5rem !important; + gap: 0.5rem !important; } /* ========================================== @@ -42,97 +42,97 @@ body, .stDownloadButton > button, .stFormSubmitButton > button, .stSlider > div > div { - background-color: darkgreen !important; - color: white !important; - border: 1px solid grey; + background-color: darkgreen !important; + color: white !important; + border: 1px solid grey; } .st-key-FormSubmitter-auth_form-Se-connecter { - margin-left: auto; - margin-right: auto; + margin-left: auto; + margin-right: auto; } section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-primary"], + button[data-testid="stBaseButton-primary"], section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-secondary"] { - color: white !important; - background: darkgreen !important; + button[data-testid="stBaseButton-secondary"] { + color: white !important; + background: darkgreen !important; } section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-primary"] - p, + button[data-testid="stBaseButton-primary"] + p, section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-secondary"] - p { - color: white !important; + button[data-testid="stBaseButton-secondary"] + p { + color: white !important; } .bouton-fictif { - display: inline-flex; - -moz-box-align: center; - align-items: center; - -moz-box-pack: center; - justify-content: center; - padding: 0.25rem 0.75rem; - border-radius: 0.5rem; - min-height: 2.5rem; - margin-bottom: 20px; - line-height: 1; - text-transform: none; - font-size: x-large; - font-family: inherit; - user-select: none; - border: 1px solid rgba(49, 51, 63, 0.2); - background-color: darkgrey !important; - color: darkgreen !important; - font-weight: bold !important; - width: 100%; - letter-spacing: 0.2em; + display: inline-flex; + -moz-box-align: center; + align-items: center; + -moz-box-pack: center; + justify-content: center; + padding: 0.25rem 0.75rem; + border-radius: 0.5rem; + min-height: 2.5rem; + margin-bottom: 20px; + line-height: 1; + text-transform: none; + font-size: x-large; + font-family: inherit; + user-select: none; + border: 1px solid rgba(49, 51, 63, 0.2); + background-color: darkgrey !important; + color: darkgreen !important; + font-weight: bold !important; + width: 100%; + letter-spacing: 0.2em; } button[data-testid="stBaseButton-headerNoPadding"] svg { - fill: var(--text-color) !important; + fill: var(--text-color) !important; } /* --- 3.2 Onglets et radiogroup --- */ div[role="radiogroup"] > label { - padding: 0.5em 1em; - border-radius: 0.4em; - margin-right: 0.5em; - cursor: pointer; - border: 1px solid #fff; + padding: 0.5em 1em; + border-radius: 0.4em; + margin-right: 0.5em; + cursor: pointer; + border: 1px solid #fff; } div[role="radiogroup"] > label[data-selected="true"] { - font-weight: bold; - border: 2px solid #145a1a; + font-weight: bold; + border: 2px solid #145a1a; } section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label p { - background-color: var(--radio-bg) !important; - color: var(--radio-text) !important; + background-color: var(--radio-bg) !important; + color: var(--radio-text) !important; } section:not([data-testid="stSidebar"]) - div[role="radiogroup"] - > label[data-selected="true"] { - background-color: var(--radio-selected-bg) !important; - color: var(--radio-selected-text) !important; + div[role="radiogroup"] + > label[data-selected="true"] { + background-color: var(--radio-selected-bg) !important; + color: var(--radio-selected-text) !important; } /* --- 3.3 Champs de formulaire --- */ div[data-baseweb="select"], section:not([data-testid="stSidebar"]) div[data-baseweb="base-input"], section[data-testid="stFileUploaderDropzone"] { - border: 1px solid var(--input-border) !important; - border-radius: 5px; - padding: 4px; + border: 1px solid var(--input-border) !important; + border-radius: 5px; + padding: 4px; } small { - display: none; + display: none; } section:not([data-testid="stSidebar"]) div[data-testid="stSelectbox"] p, @@ -142,7 +142,7 @@ section:not([data-testid="stSidebar"]) div[data-testid="stCheckbox"] p, section:not([data-testid="stSidebar"]) div[data-testid="stTextInput"] p, section:not([data-testid="stSidebar"]) div[data-testid="stTextArea"] p, section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p { - color: var(--text-color) !important; + color: var(--text-color) !important; } /* ========================================== @@ -151,98 +151,98 @@ section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p { /* --- 4.1 Header --- */ .wide-header { - width: 100vw; - margin-left: calc(-50vw + 50%); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-bottom: 1px solid #ddd; - text-align: center; - padding-top: 1rem; - margin-top: -1.25em; - background-color: var(--header-bg); + width: 100vw; + margin-left: calc(-50vw + 50%); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-bottom: 1px solid #ddd; + text-align: center; + padding-top: 1rem; + margin-top: -1.25em; + background-color: var(--header-bg); } .titre-header { - font-size: 2rem !important; - font-weight: bolder !important; - color: var(--header-title); + font-size: 2rem !important; + font-weight: bolder !important; + color: var(--header-title); } /* --- 4.2 Footer --- */ .wide-footer { - width: 100vw; - margin-left: calc(-50vw + 50%); - margin-top: 3rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-top: 1px solid #ddd; - text-align: center; - padding-top: 1rem; - background-color: var(--footer-bg); + width: 100vw; + margin-left: calc(-50vw + 50%); + margin-top: 3rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-top: 1px solid #ddd; + text-align: center; + padding-top: 1rem; + background-color: var(--footer-bg); } .info-footer { - font-size: 1rem !important; - font-weight: 800; - color: var(--footer-text); + font-size: 1rem !important; + font-weight: 800; + color: var(--footer-text); } /* ========================================== 5. Sidebar ========================================== */ section[data-testid="stSidebar"] { - background-color: #ccc !important; - color: #111 !important; + background-color: #ccc !important; + color: #111 !important; } section[data-testid="stSidebar"] .stButton > button { - background-color: darkgreen !important; - color: white !important; - font-weight: bold !important; - border: 1px solid #ccc !important; - width: 100%; + background-color: darkgreen !important; + color: white !important; + font-weight: bold !important; + border: 1px solid #ccc !important; + width: 100%; } section[data-testid="stSidebar"] .decorative-heading { - font-size: 1.25rem; - font-weight: bold; - margin-bottom: 0.5rem; - text-align: center; - color: #145a1a; + font-size: 1.25rem; + font-weight: bold; + margin-bottom: 0.5rem; + text-align: center; + color: #145a1a; } section[data-testid="stSidebar"] div[role="radiogroup"] { - justify-content: center !important; - display: flex !important; - gap: 1rem; + justify-content: center !important; + display: flex !important; + gap: 1rem; } /* ========================================== 6. Tables ========================================== */ table { - border: 1px solid var(--table-border) !important; - border-collapse: collapse; - width: 100%; - margin-bottom: 1.5em; + border: 1px solid var(--table-border) !important; + border-collapse: collapse; + width: 100%; + margin-bottom: 1.5em; } th, td { - border: 1px solid var(--table-border) !important; - padding: 8px; - text-align: left; + border: 1px solid var(--table-border) !important; + padding: 8px; + text-align: left; } caption { - caption-side: top; - font-weight: bold; - padding: 0.5em; - text-align: left; - caption-side: bottom; - text-align: center; + caption-side: top; + font-weight: bold; + padding: 0.5em; + text-align: left; + caption-side: bottom; + text-align: center; } table[role="table"] th[scope="col"] { - background-color: var(--background-color); + background-color: var(--background-color); } /* ========================================== @@ -254,94 +254,99 @@ table[role="table"] th[scope="col"] { /* --- 7.2 Graphiques --- */ .stPlotlyChart text { - fill: var(--plot-text) !important; + fill: var(--plot-text) !important; } .stPlotlyChart text { - fill: black !important; - text-shadow: none !important; - font-weight: bold !important; - font-size: 14px !important; - font-family: Verdana, sans-serif !important; + fill: black !important; + text-shadow: none !important; + font-weight: bold !important; + font-size: 14px !important; + font-family: Verdana, sans-serif !important; } /* Cache complètement la section d'actions Vega */ .vega-actions { - display: none !important; + display: none !important; } /* Et aussi le
parent, s'il faut tout masquer */ details[title="Click to view actions"] { - display: none !important; + display: none !important; } /* --- 7.3 Détails et paragraphes --- */ details { - border: 1px solid #ccc; - border-radius: 6px; - padding: 0.5em; - margin-bottom: 0.5em; - background-color: var(--background-color); - border-color: var(--details-border) !important; + border: 1px solid #ccc; + border-radius: 6px; + padding: 0.5em; + margin-bottom: 0.5em; + background-color: var(--background-color); + border-color: var(--details-border) !important; } section:not([data-testid="stSidebar"]) - div:not[data-testid="stElementContainer"] - p:not(#Authentification):not(#Theme) { - color: var(--paragraph-color) !important; + div:not[data-testid="stElementContainer"] + p:not(#Authentification):not(#Theme) { + color: var(--paragraph-color) !important; } section:not([data-testid="stSidebar"]) hr { - background-color: var(--hr-color) !important; + background-color: var(--hr-color) !important; } /* --- 7.4 Conteneurs de commentaires et tickets --- */ .conteneur_commentaire, .conteneur_ticket { - background: var(--background-color); - padding: 1em; - border-radius: 8px; - margin-bottom: 1em; - border: 1px solid #ccc; + background: var(--background-color); + padding: 1em; + border-radius: 8px; + margin-bottom: 1em; + border: 1px solid #ccc; } .commentaire_auteur, .ticket_auteur { - color: var(--text-color) !important; - margin: 0; + color: var(--text-color) !important; + margin: 0; } .commentaire_contenu, .ticket_contenu { - color: var(--text-color) !important; - margin: 0.5rem 0 0; + color: var(--text-color) !important; + margin: 0.5rem 0 0; } /* --- 7.5 Blocs mathématiques --- */ .math-block { - display: block; - text-align: center; - margin: 1em 0; - border: 1px solid var(--math-block-border); - border-radius: 10px; - background: var(--math-block-bg); - font-size: x-large; - color: var(--text-color); + display: block; + text-align: center; + margin: 1em 0; + border: 1px solid var(--math-block-border); + border-radius: 10px; + background: var(--math-block-bg); + font-size: x-large; + color: var(--text-color); } .math-block math { - display: inline-block; + display: inline-block; } /* ========================================== 8. Éléments spécifiques ========================================== */ div.stElementContainer.element-container.st-key-nom_utilisateur { - display: none !important; + display: none !important; } .st-key-telecharger_fiche_pdf { - margin-left: auto; - margin-right: auto; - margin-top: 1rem; + margin-left: auto; + margin-right: auto; + margin-top: 1rem; +} + +.details_introduction { + margin-top: 0.5rem; + margin-bottom: 0.5rem; } diff --git a/fabnum.py b/fabnum.py index cb1d0b3..ac52ed2 100644 --- a/fabnum.py +++ b/fabnum.py @@ -19,6 +19,8 @@ from utils.gitea import ( # Import du module de traductions from utils.translations import init_translations, _, set_language +from utils.widgets import html_expander + def afficher_instructions_avec_expanders(markdown_content): """ Affiche le contenu markdown avec les sections de niveau 2 (## Titre) dans des expanders @@ -57,8 +59,8 @@ def afficher_instructions_avec_expanders(markdown_content): # Affichage dans un expander status = True if i == 1 else False - with st.expander(f"## {titre_section}", expanded=status): - st.markdown(contenu_section, unsafe_allow_html=True) + # with st.expander(f"## {titre_section}", expanded=status): + html_expander(f"{titre_section}", content=contenu_section, open_by_default=status, details_class="details_introduction") from utils.graph_utils import ( charger_graphe diff --git a/utils/widgets.py b/utils/widgets.py new file mode 100644 index 0000000..a1eeba6 --- /dev/null +++ b/utils/widgets.py @@ -0,0 +1,49 @@ +import streamlit as st +import uuid +import markdown +import html + +# html_expander remplace st.expander +# +# En effet, st.expander présente un défaut lors de la fermeture +# avec une fois la fermeture terminée, un dernier mouvement +# gênant visuellement. +def html_expander(title, content, open_by_default=False, details_class="", summary_class=""): + """ + Creates an HTML details/summary expander with content inside. + + Args: + title (str): Text to display in the summary/header. + content (str): Markdown content to display inside the expander. + open_by_default (bool): Whether the expander is open by default. + details_class (str): CSS class for the details element. + summary_class (str): CSS class for the summary element. + """ + # Generate a unique ID for this expander + expander_id = f"expander_{uuid.uuid4().hex}" + + # Set attributes + expanded = "open" if open_by_default else "" + details_attr = f'class="{details_class}"' if details_class else "" + summary_attr = f'class="{summary_class}"' if summary_class else "" + + # Convert markdown to HTML (use Python's markdown library) + try: + # Try to use markdown package if available + html_content = markdown.markdown(content) + except: + # Fallback to basic html escaping if markdown package not available + html_content = html.escape(content).replace('\n', '
') + + # Build the complete HTML structure + complete_html = f""" +
+ {title} +
+ {html_content} +
+
+ """ + + # Render the complete HTML in one call + st.markdown(complete_html, unsafe_allow_html=True)