Code/app/visualisations/graphes.py
Stéphan Peccini f812fac89e
feat: Amelioration structure - tests, documentation et qualite du code
Cette mise a jour complete ameliore significativement la qualite et la maintenabilite du projet.

1. Extension de la couverture de tests

Couverture globale passee de 8% a 16% (+100%)
- Ajout de 25 nouveaux tests (total: 67 tests, 100% passent)
- Nouveaux fichiers de tests:
  * tests/unit/test_gitea.py (17 tests)
  * tests/unit/test_fiches_tickets.py (8 tests)

Etat de la couverture par module:
- utils/gitea.py: 100%
- utils/widgets.py: 100%
- utils/logger.py: 94%
- app/fiches/utils/tickets/core.py: 77%
- utils/graph_utils.py: 59%

2. Documentation d'architecture complete

Creation de 3 nouveaux documents (30 Ko total):
- docs/ARCHITECTURE.md (15 Ko)
  * Architecture complete du projet
  * Flux de donnees detailles
  * Indices de vulnerabilite (IHH, ISG, ICS, IVC)
  * Structure du graphe NetworkX

- docs/MODULES.md (15 Ko)
  * Guide des 11 modules principaux
  * Exemples de code (15+ snippets)
  * Bonnes pratiques
  * Guide de depannage

- docs/README.md (4 Ko)
  * Index de toute la documentation

Contenu documente:
- 5 modules applicatifs
- 6 modules utilitaires
- 4 indices de vulnerabilite avec formules et seuils
- Conventions de code

3. Reorganisation de la documentation

Structure finale optimisee:
- Racine: README.md (mis a jour) + Instructions.md
- docs/: 11 documents organises par categorie

Fichiers deplaces vers docs/:
- README_connexion.md -> docs/CONNEXION.md
- GUIDE_LOGS.md -> docs/
- GUIDE_RUFF.md -> docs/
- RAPPORT_RUFF.md -> docs/
- RAPPORT_CORRECTIONS_AUTO.md -> docs/
- REFACTORING_REPORT.md -> docs/
- VERIFICATION_LOGS.md -> docs/
- TODO_IA_BATCH.md -> docs/

4. Ajout de docstrings

52 fonctions documentees en style Google (100%)
Documentation en francais avec Args, Returns, Raises

5. Corrections automatiques Ruff

Application de 347 corrections automatiques:
- Formatage du code (line-length: 120)
- Organisation des imports
- Simplifications syntaxiques
- Suppressions de code mort
- Ameliorations de performance

6. Configuration qualite du code

Nouveaux fichiers:
- pyproject.toml: configuration Ruff complete
- .vscode/settings.json: integration Ruff avec formatOnSave
- GUIDE_RUFF.md: documentation du linter
- GUIDE_LOGS.md: documentation du logging
- .gitignore: ajout htmlcov/ pour rapports de couverture

Etat final du projet:
- Linter: Ruff configure (15 regles actives)
- Tests: 67 tests (100% passent)
- Couverture de code: 16%
- Docstrings: 52/52 (100%)
- Documentation: 11 fichiers organises

Impact:
- Tests plus robustes et maintenables
- Documentation technique complete
- Meilleure organisation des fichiers
- Workflow optimise avec Ruff
- Code pret pour integration continue

References:
- Architecture: docs/ARCHITECTURE.md
- Guide modules: docs/MODULES.md
- Tests: tests/unit/
- Configuration: pyproject.toml

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-07 19:00:49 +01:00

246 lines
10 KiB
Python

from collections import Counter
from typing import Any
import altair as alt
import networkx as nx
import numpy as np
import pandas as pd
import streamlit as st
from utils.translations import _
def afficher_graphique_altair(df: pd.DataFrame) -> None:
"""Affiche un graphique Altair pour les données d'IHH.
Args:
df (pd.DataFrame): DataFrame contenant les données de IHH.
Notes:
Cette fonction crée un graphique interactif pour visualiser les
données d'IHH selon différentes catégories et niveaux de gravité.
"""
# Définir les catégories originales (en français) et leur ordre
categories_fr = ["Assemblage", "Fabrication", "Traitement", "Extraction"]
# Créer un dictionnaire de mappage entre les catégories originales et leurs traductions
mappage_categories = {
"Assemblage": str(_("pages.visualisations.categories.assembly")),
"Fabrication": str(_("pages.visualisations.categories.manufacturing")),
"Traitement": str(_("pages.visualisations.categories.processing")),
"Extraction": str(_("pages.visualisations.categories.extraction"))
}
# Filtrer les catégories qui existent dans les données
categories_fr_filtrees = [cat for cat in categories_fr if cat in df['categorie'].unique()]
# Parcourir les catégories dans l'ordre défini
for cat_fr in categories_fr_filtrees:
# Obtenir le nom traduit de la catégorie pour l'affichage
cat_traduit = mappage_categories[cat_fr]
st.markdown(f"### {cat_traduit}")
# Mais filtrer sur le nom original dans les données
df_cat = df[df['categorie'] == cat_fr].copy()
# Convertir les colonnes en float pour éviter les warnings de compatibilité
df_cat = df_cat.astype({'ihh_pays': float, 'ihh_acteurs': float})
coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1), strict=False))
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.loc[:, 'ihh_pays'] = df_cat['ihh_pays'] + offset_x
df_cat.loc[:, 'ihh_acteurs'] = df_cat['ihh_acteurs'] + [offset_y[p] for p in coord_pairs]
df_cat.loc[:, 'ihh_pays_text'] = df_cat['ihh_pays'] + 0.5
df_cat.loc[:, 'ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
base = alt.Chart(df_cat).encode(
x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries"))),
y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors"))),
size=alt.Size('ics_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
color=alt.Color('ics_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=str(_("pages.visualisations.chart_titles.concentration_criticality")).format(cat_traduit)
).interactive()
st.altair_chart(chart, use_container_width=True)
def creer_graphes(donnees: list[dict[str, Any]] | None) -> None:
"""Crée un graphique Altair pour les données d'IVC.
Args:
donnees (Optional[List[Dict[str, Any]]]): Liste des données d'IVC.
Returns:
None.
Notes:
Cette fonction traite les données d'IVC et crée un graphique
interactif pour visualiser la concentration des ressources.
"""
if not donnees:
st.warning(str(_("pages.visualisations.no_data")))
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))
# Convertir les colonnes en float pour éviter les warnings de compatibilité
df = df.astype({'ihh_extraction': float, 'ihh_reserves': float})
from collections import Counter
coord_pairs = list(zip(df['ihh_extraction'].round(1), df['ihh_reserves'].round(1), strict=False))
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.loc[:, 'ihh_extraction'] = df['ihh_extraction'] + offset_x
df.loc[:, 'ihh_reserves'] = df['ihh_reserves'] + [offset_y[p] for p in coord_pairs]
df.loc[:, 'ihh_extraction_text'] = df['ihh_extraction'] + 0.5
df.loc[:, 'ihh_reserves_text'] = df['ihh_reserves'] + 0.5
base = alt.Chart(df).encode(
x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction"))),
y=alt.Y('ihh_reserves:Q', title=str(_("pages.visualisations.axis_titles.ihh_reserves"))),
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=str(_("pages.visualisations.chart_titles.concentration_resources"))
).interactive()
st.altair_chart(chart, use_container_width=True)
except Exception as e:
st.error(f"{str(_('errors.graph_creation_error'))} {e}")
def lancer_visualisation_ihh_ics(graph: nx.DiGraph) -> None:
"""Lance une visualisation Altair pour les données d'IHH critique.
Args:
graph (nx.DiGraph): Le graphe NetworkX contenant les données de IHH.
Notes:
Cette fonction traite le graphe et crée un graphique Altair
pour visualiser les données d'IHH critique.
"""
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(str(_("pages.visualisations.no_data")))
else:
afficher_graphique_altair(df)
except Exception as e:
st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
def lancer_visualisation_ihh_ivc(graph: nx.DiGraph) -> None:
"""Lance une visualisation Altair pour les données d'IVC.
Args:
graph (Annx.Graphy): Le graphe NetworkX contenant les données de IV C.
Notes:
Cette fonction traite le graphe et crée un graphique Altair
pour visualiser les données d'IV C.
"""
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"{str(_('errors.ihh_ivc_error'))} {e}")