|
|
|
|
|
""" |
|
|
Interface web Streamlit pour le réseau bayésien d'autonomie |
|
|
Application déployée sur Hugging Face Spaces |
|
|
""" |
|
|
|
|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import plotly.graph_objects as go |
|
|
import plotly.express as px |
|
|
import json |
|
|
from bayesian_network_interface import AutonomyBayesianNetwork |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Réseau Bayésien - Autonomie", |
|
|
page_icon="🧠", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
def main(): |
|
|
"""Interface principale Streamlit""" |
|
|
|
|
|
st.title("🧠 Réseau Bayésien pour l'Évaluation d'Autonomie") |
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
if 'network' not in st.session_state: |
|
|
with st.spinner("Chargement du réseau bayésien..."): |
|
|
try: |
|
|
st.session_state.network = AutonomyBayesianNetwork() |
|
|
|
|
|
if hasattr(st.session_state.network, 'pgmpy_model') and st.session_state.network.pgmpy_model: |
|
|
st.session_state.loading_debug = f"✅ Réseau chargé: {len(list(st.session_state.network.pgmpy_model.nodes()))} nœuds" |
|
|
else: |
|
|
st.session_state.loading_debug = "❌ Erreur: pgmpy_model non chargé" |
|
|
except Exception as e: |
|
|
st.session_state.loading_debug = f"❌ Erreur de chargement: {str(e)}" |
|
|
st.session_state.network = None |
|
|
|
|
|
network = st.session_state.network |
|
|
|
|
|
|
|
|
st.sidebar.title("Navigation") |
|
|
page = st.sidebar.selectbox( |
|
|
"Choisir une page", |
|
|
["🏠 Accueil", "📊 Structure du Réseau", "🌐 Visualisation D3.js", "🔍 Inférence", |
|
|
"💊 Analyse d'Intervention", "📈 Facteurs Influents", "📋 Recommandations"] |
|
|
) |
|
|
|
|
|
if page == "🏠 Accueil": |
|
|
show_home_page(network) |
|
|
elif page == "📊 Structure du Réseau": |
|
|
show_structure_page(network) |
|
|
elif page == "🌐 Visualisation D3.js": |
|
|
show_d3_visualization_page(network) |
|
|
elif page == "🔍 Inférence": |
|
|
show_inference_page(network) |
|
|
elif page == "💊 Analyse d'Intervention": |
|
|
show_intervention_page(network) |
|
|
elif page == "📈 Facteurs Influents": |
|
|
show_factors_page(network) |
|
|
elif page == "📋 Recommandations": |
|
|
show_recommendations_page(network) |
|
|
|
|
|
def show_home_page(network): |
|
|
"""Page d'accueil avec démonstration""" |
|
|
st.header("🏠 Bienvenue dans l'Interface d'Évaluation d'Autonomie") |
|
|
|
|
|
st.markdown(""" |
|
|
Cette application utilise un **réseau bayésien complet** avec **12 variables** pour évaluer |
|
|
l'autonomie des personnes âgées et générer des recommandations personnalisées. |
|
|
|
|
|
### 🎯 **Fonctionnalités principales** |
|
|
- **Inférence probabiliste** : Calcul des probabilités d'autonomie |
|
|
- **Analyse d'interventions** : Impact des changements de comportement |
|
|
- **Recommandations personnalisées** : Conseils basés sur le profil individuel |
|
|
- **Facteurs influents** : Identification des variables les plus importantes |
|
|
- **Visualisation interactive** : Graphique D3.js du réseau |
|
|
""") |
|
|
|
|
|
st.subheader("🔍 Démonstration avec deux scénarios") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.markdown("**👤 Scénario 1: Personne Active**") |
|
|
evidence1 = { |
|
|
'Age': 'age_60_69', |
|
|
'Physical_Activity': 'high', |
|
|
'BMI_Category': 'underweight_normal' |
|
|
} |
|
|
|
|
|
for key, value in evidence1.items(): |
|
|
st.write(f"• {key}: {value}") |
|
|
|
|
|
result1 = network.perform_inference_pgmpy(evidence1, ['Global_Autonomy']) |
|
|
if not result1.empty: |
|
|
autonomous_prob = result1[result1['State'] == 'autonomous']['Probability'].iloc[0] |
|
|
st.success(f"🎉 **Probabilité d'autonomie: {autonomous_prob:.1%}**") |
|
|
|
|
|
with col2: |
|
|
st.markdown("**👴 Scénario 2: Personne Sédentaire**") |
|
|
evidence2 = { |
|
|
'Age': 'age_80_89', |
|
|
'Physical_Activity': 'sedentary', |
|
|
'BMI_Category': 'obese_severe' |
|
|
} |
|
|
|
|
|
for key, value in evidence2.items(): |
|
|
st.write(f"• {key}: {value}") |
|
|
|
|
|
result2 = network.perform_inference_pgmpy(evidence2, ['Global_Autonomy']) |
|
|
if not result2.empty: |
|
|
autonomous_prob = result2[result2['State'] == 'autonomous']['Probability'].iloc[0] |
|
|
st.warning(f"⚠️ **Probabilité d'autonomie: {autonomous_prob:.1%}**") |
|
|
|
|
|
|
|
|
if not result1.empty and not result2.empty: |
|
|
st.markdown("---") |
|
|
st.subheader("📊 Comparaison des Scénarios") |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
|
|
|
states1 = result1['State'].tolist() |
|
|
probs1 = result1['Probability'].tolist() |
|
|
|
|
|
|
|
|
states2 = result2['State'].tolist() |
|
|
probs2 = result2['Probability'].tolist() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
name='Personne Active (60-69 ans)', |
|
|
x=states1, |
|
|
y=probs1, |
|
|
marker_color='lightgreen' |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
name='Personne Sédentaire (80-89 ans)', |
|
|
x=states2, |
|
|
y=probs2, |
|
|
marker_color='lightcoral' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title="Comparaison des Probabilités d'Autonomie", |
|
|
xaxis_title="État d'Autonomie", |
|
|
yaxis_title="Probabilité", |
|
|
barmode='group', |
|
|
height=500 |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("---") |
|
|
st.info("💡 **Conseil**: Explorez les autres pages pour des analyses plus détaillées!") |
|
|
|
|
|
def show_structure_page(network): |
|
|
"""Page structure du réseau""" |
|
|
st.header("📊 Structure du Réseau Bayésien") |
|
|
|
|
|
|
|
|
st.write("🔍 **Debug Info:**") |
|
|
|
|
|
|
|
|
if hasattr(st.session_state, 'loading_debug'): |
|
|
st.write(f"- Chargement initial: {st.session_state.loading_debug}") |
|
|
|
|
|
if network is None: |
|
|
st.error("❌ Réseau non initialisé!") |
|
|
return |
|
|
|
|
|
|
|
|
if hasattr(network, 'debug_messages'): |
|
|
st.write("**Messages de chargement détaillés:**") |
|
|
for msg in network.debug_messages: |
|
|
st.code(msg) |
|
|
|
|
|
st.write(f"- pgmpy_model exists: {network.pgmpy_model is not None}") |
|
|
if network.pgmpy_model: |
|
|
st.write(f"- pgmpy nodes: {len(list(network.pgmpy_model.nodes()))}") |
|
|
st.write(f"- pgmpy edges: {len(list(network.pgmpy_model.edges()))}") |
|
|
|
|
|
st.write(f"- pyagrum_model exists: {hasattr(network, 'pyagrum_model') and network.pyagrum_model is not None}") |
|
|
st.write(f"- actionable_vars: {len(getattr(network, 'actionable_vars', []))}") |
|
|
|
|
|
try: |
|
|
structure = network.get_network_structure() |
|
|
st.write(f"- Structure nodes: {len(structure['nodes'])}") |
|
|
st.write(f"- Structure edges: {len(structure['edges'])}") |
|
|
except Exception as e: |
|
|
st.error(f"Erreur lors de l'obtention de la structure: {e}") |
|
|
structure = {'nodes': [], 'edges': []} |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
st.metric("Nombre de nœuds", len(structure.get('nodes', []))) |
|
|
with col2: |
|
|
st.metric("Nombre d'arcs", len(structure.get('edges', []))) |
|
|
with col3: |
|
|
st.metric("Variables actionnables", len(network.actionable_vars)) |
|
|
|
|
|
|
|
|
st.subheader("Variables par catégorie") |
|
|
|
|
|
categories = {} |
|
|
for node in structure['nodes']: |
|
|
cat = node['category'] |
|
|
if cat not in categories: |
|
|
categories[cat] = [] |
|
|
categories[cat].append(node['name']) |
|
|
|
|
|
for cat in ['non_actionable', 'actionable', 'intermediate', 'outcome']: |
|
|
if cat in categories: |
|
|
with st.expander(f"{cat.replace('_', ' ').title()} ({len(categories[cat])} variables)"): |
|
|
for var in categories[cat]: |
|
|
st.write(f"• {var}") |
|
|
|
|
|
def show_inference_page(network): |
|
|
"""Page d'inférence""" |
|
|
st.header("🔍 Inférence Bayésienne") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.subheader("Variables Non-Actionnables") |
|
|
evidence = {} |
|
|
|
|
|
age = st.selectbox("Âge", |
|
|
["", "age_60_69", "age_70_79", "age_80_89", "age_90_plus"]) |
|
|
if age: |
|
|
evidence['Age'] = age |
|
|
|
|
|
sex = st.selectbox("Sexe", ["", "male", "female"]) |
|
|
if sex: |
|
|
evidence['Sex'] = sex |
|
|
|
|
|
education = st.selectbox("Niveau d'Éducation", |
|
|
["", "primary_or_below", "secondary", "higher_education"]) |
|
|
if education: |
|
|
evidence['Education_Level'] = education |
|
|
|
|
|
st.subheader("Variables Actionnables") |
|
|
|
|
|
activity = st.selectbox("Activité Physique", |
|
|
["", "sedentary", "low", "moderate", "high"]) |
|
|
if activity: |
|
|
evidence['Physical_Activity'] = activity |
|
|
|
|
|
bmi = st.selectbox("Catégorie IMC", |
|
|
["", "underweight_normal", "overweight", "obese_mild", "obese_severe"]) |
|
|
if bmi: |
|
|
evidence['BMI_Category'] = bmi |
|
|
|
|
|
smoking = st.selectbox("Statut Tabagique", |
|
|
["", "never", "former", "current"]) |
|
|
if smoking: |
|
|
evidence['Smoking_Status'] = smoking |
|
|
|
|
|
social_eng = st.selectbox("Engagement Social", |
|
|
["", "low", "moderate", "high"]) |
|
|
if social_eng: |
|
|
evidence['Social_Engagement'] = social_eng |
|
|
|
|
|
social_sup = st.selectbox("Support Social", |
|
|
["", "poor", "moderate", "good"]) |
|
|
if social_sup: |
|
|
evidence['Social_Support'] = social_sup |
|
|
|
|
|
with col2: |
|
|
st.subheader("Variables à inférer") |
|
|
query_vars = st.multiselect( |
|
|
"Sélectionner les variables", |
|
|
network.outcome_vars + network.intermediate_vars, |
|
|
default=['Global_Autonomy'] |
|
|
) |
|
|
|
|
|
if st.button("🔍 Effectuer l'inférence"): |
|
|
if query_vars: |
|
|
with st.spinner("Calcul en cours..."): |
|
|
result = network.perform_inference_pgmpy(evidence, query_vars) |
|
|
|
|
|
if not result.empty: |
|
|
st.subheader("📊 Résultats") |
|
|
|
|
|
for var in query_vars: |
|
|
var_data = result[result['Variable'] == var] |
|
|
if not var_data.empty: |
|
|
fig = px.bar(var_data, x='State', y='Probability', |
|
|
title=f"Distribution de probabilité pour {var}", |
|
|
color='Probability', |
|
|
color_continuous_scale='viridis') |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.dataframe(var_data[['State', 'Probability']].round(4)) |
|
|
else: |
|
|
st.error("❌ Aucun résultat disponible") |
|
|
else: |
|
|
st.warning("⚠️ Veuillez sélectionner au moins une variable à inférer") |
|
|
|
|
|
def show_intervention_page(network): |
|
|
"""Page analyse d'intervention""" |
|
|
st.header("💊 Analyse d'Intervention") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.markdown("**Intervention à tester**") |
|
|
intervention = {} |
|
|
|
|
|
new_activity = st.selectbox("Nouvelle Activité Physique", |
|
|
["Non modifié", "sedentary", "low", "moderate", "high"]) |
|
|
if new_activity != "Non modifié": |
|
|
intervention['Physical_Activity'] = new_activity |
|
|
|
|
|
new_bmi = st.selectbox("Nouvelle Catégorie IMC", |
|
|
["Non modifié", "underweight_normal", "overweight", "obese_mild", "obese_severe"]) |
|
|
if new_bmi != "Non modifié": |
|
|
intervention['BMI_Category'] = new_bmi |
|
|
|
|
|
with col2: |
|
|
st.markdown("**Profil de base (optionnel)**") |
|
|
baseline_evidence = {} |
|
|
|
|
|
base_age = st.selectbox("Âge de référence", |
|
|
["", "age_60_69", "age_70_79", "age_80_89", "age_90_plus"]) |
|
|
if base_age: |
|
|
baseline_evidence['Age'] = base_age |
|
|
|
|
|
if st.button("🧪 Analyser l'intervention"): |
|
|
if intervention: |
|
|
with st.spinner("Analyse en cours..."): |
|
|
baseline_result = network.perform_inference_pgmpy(baseline_evidence, ['Global_Autonomy']) |
|
|
|
|
|
|
|
|
intervention_evidence = baseline_evidence.copy() |
|
|
intervention_evidence.update(intervention) |
|
|
intervention_result = network.perform_inference_pgmpy(intervention_evidence, ['Global_Autonomy']) |
|
|
|
|
|
if not baseline_result.empty and not intervention_result.empty: |
|
|
st.subheader("📊 Comparaison Avant/Après") |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
states = baseline_result['State'].tolist() |
|
|
baseline_probs = baseline_result['Probability'].tolist() |
|
|
intervention_probs = intervention_result['Probability'].tolist() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
name='Avant intervention', |
|
|
x=states, |
|
|
y=baseline_probs, |
|
|
marker_color='lightblue' |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
name='Après intervention', |
|
|
x=states, |
|
|
y=intervention_probs, |
|
|
marker_color='darkgreen' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title="Impact de l'intervention sur l'autonomie", |
|
|
xaxis_title="État d'Autonomie", |
|
|
yaxis_title="Probabilité", |
|
|
barmode='group', |
|
|
height=500 |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
baseline_autonomous = baseline_result[baseline_result['State'] == 'autonomous']['Probability'].iloc[0] |
|
|
intervention_autonomous = intervention_result[intervention_result['State'] == 'autonomous']['Probability'].iloc[0] |
|
|
improvement = intervention_autonomous - baseline_autonomous |
|
|
|
|
|
if improvement > 0: |
|
|
st.success(f"✅ **Amélioration**: +{improvement:.1%} de probabilité d'autonomie") |
|
|
elif improvement < 0: |
|
|
st.warning(f"⚠️ **Détérioration**: {improvement:.1%} de probabilité d'autonomie") |
|
|
else: |
|
|
st.info("➡️ **Aucun changement** significatif") |
|
|
|
|
|
else: |
|
|
st.error("❌ Impossible d'effectuer l'analyse") |
|
|
else: |
|
|
st.warning("⚠️ Veuillez définir au moins une intervention") |
|
|
|
|
|
def show_factors_page(network): |
|
|
"""Page facteurs influents""" |
|
|
st.header("📈 Facteurs les Plus Influents") |
|
|
|
|
|
if st.button("🔍 Analyser les facteurs influents"): |
|
|
with st.spinner("Calcul en cours..."): |
|
|
factors = network.get_most_influential_factors() |
|
|
|
|
|
if factors: |
|
|
st.subheader("🎯 Classement des Variables") |
|
|
|
|
|
|
|
|
variables = [factor[0] for factor in factors] |
|
|
influences = [factor[1] for factor in factors] |
|
|
|
|
|
fig = px.bar( |
|
|
x=variables, |
|
|
y=influences, |
|
|
title="Impact des Variables sur l'Autonomie", |
|
|
labels={'x': 'Variables', 'y': 'Influence (différence de probabilité)'} |
|
|
) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
df_factors = pd.DataFrame(factors, columns=['Variable', 'Influence']) |
|
|
df_factors['Influence %'] = (df_factors['Influence'] * 100).round(1) |
|
|
st.dataframe(df_factors) |
|
|
|
|
|
else: |
|
|
st.info("ℹ️ Aucun facteur influent identifié avec les données actuelles") |
|
|
|
|
|
def show_recommendations_page(network): |
|
|
"""Page recommandations""" |
|
|
st.header("📋 Recommandations Personnalisées") |
|
|
|
|
|
st.subheader("Profil du Patient") |
|
|
|
|
|
profile = {} |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
age = st.selectbox("Âge", |
|
|
["age_60_69", "age_70_79", "age_80_89", "age_90_plus"]) |
|
|
profile['Age'] = age |
|
|
|
|
|
activity = st.selectbox("Activité Physique actuelle", |
|
|
["sedentary", "low", "moderate", "high"]) |
|
|
profile['Physical_Activity'] = activity |
|
|
|
|
|
with col2: |
|
|
sex = st.selectbox("Sexe", ["male", "female"]) |
|
|
profile['Sex'] = sex |
|
|
|
|
|
education = st.selectbox("Niveau d'éducation", |
|
|
["primary_or_below", "secondary", "higher_education"]) |
|
|
profile['Education_Level'] = education |
|
|
|
|
|
bmi = st.selectbox("Catégorie IMC actuelle", |
|
|
["underweight_normal", "overweight", "obese_mild", "obese_severe"]) |
|
|
profile['BMI_Category'] = bmi |
|
|
|
|
|
if st.button("🎯 Générer les recommandations"): |
|
|
with st.spinner("Analyse du profil..."): |
|
|
|
|
|
current_state = network.perform_inference_pgmpy(profile, ['Global_Autonomy']) |
|
|
|
|
|
if not current_state.empty: |
|
|
current_autonomous = current_state[current_state['State'] == 'autonomous']['Probability'].iloc[0] |
|
|
|
|
|
st.subheader("📊 État Actuel") |
|
|
st.metric("Probabilité d'autonomie", f"{current_autonomous:.1%}") |
|
|
|
|
|
|
|
|
recommendations = network.generate_recommendations(profile) |
|
|
|
|
|
if recommendations: |
|
|
st.subheader("🎯 Recommandations Prioritaires") |
|
|
|
|
|
for i, rec in enumerate(recommendations): |
|
|
with st.expander(f"💡 Recommandation #{i+1} - Priorité {rec['priority']}"): |
|
|
st.write(f"**{rec['recommendation']}**") |
|
|
st.info(f"📈 **Amélioration attendue**: +{rec['expected_improvement']:.1f}%") |
|
|
|
|
|
|
|
|
col_metric, col_bar = st.columns([1, 3]) |
|
|
with col_metric: |
|
|
st.metric("Impact", f"+{rec['expected_improvement']:.1f}%") |
|
|
with col_bar: |
|
|
progress = min(rec['expected_improvement'] / 20.0, 1.0) |
|
|
st.progress(progress) |
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
else: |
|
|
st.info("ℹ️ Aucune recommandation spécifique disponible pour ce profil") |
|
|
|
|
|
else: |
|
|
st.error("❌ Impossible d'analyser le profil") |
|
|
|
|
|
def show_d3_visualization_page(network): |
|
|
"""Page avec visualisation D3.js du réseau bayésien""" |
|
|
st.header("🌐 Visualisation Interactive D3.js") |
|
|
|
|
|
st.markdown(""" |
|
|
Cette page présente une visualisation interactive du réseau bayésien utilisant D3.js. |
|
|
Les nœuds sont colorés selon leur catégorie et les arcs montrent les dépendances causales. |
|
|
""") |
|
|
|
|
|
|
|
|
structure = network.get_network_structure() |
|
|
|
|
|
|
|
|
nodes = [] |
|
|
links = [] |
|
|
|
|
|
|
|
|
color_map = { |
|
|
'non_actionable': '#FF6B6B', |
|
|
'actionable': '#4ECDC4', |
|
|
'intermediate': '#45B7D1', |
|
|
'outcome': '#96CEB4' |
|
|
} |
|
|
|
|
|
|
|
|
for i, node in enumerate(structure['nodes']): |
|
|
nodes.append({ |
|
|
"id": node['name'], |
|
|
"group": node['category'], |
|
|
"color": color_map.get(node['category'], '#DDDDDD'), |
|
|
"title": f"{node['name']} ({node['category']})" |
|
|
}) |
|
|
|
|
|
|
|
|
for source, target in structure['edges']: |
|
|
links.append({ |
|
|
"source": source, |
|
|
"target": target |
|
|
}) |
|
|
|
|
|
|
|
|
d3_code = f""" |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<script src="https://d3js.org/d3.v7.min.js"></script> |
|
|
<style> |
|
|
.node {{ |
|
|
stroke: #fff; |
|
|
stroke-width: 2px; |
|
|
cursor: pointer; |
|
|
}} |
|
|
.link {{ |
|
|
stroke: #999; |
|
|
stroke-opacity: 0.6; |
|
|
stroke-width: 2px; |
|
|
}} |
|
|
.node-label {{ |
|
|
font: 12px sans-serif; |
|
|
pointer-events: none; |
|
|
text-anchor: middle; |
|
|
fill: #333; |
|
|
}} |
|
|
.tooltip {{ |
|
|
position: absolute; |
|
|
padding: 8px; |
|
|
background: rgba(0, 0, 0, 0.8); |
|
|
color: white; |
|
|
border-radius: 4px; |
|
|
pointer-events: none; |
|
|
font-size: 12px; |
|
|
}} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="network"></div> |
|
|
<div class="tooltip" style="opacity: 0;"></div> |
|
|
|
|
|
<script> |
|
|
// Dimensions |
|
|
const width = 800; |
|
|
const height = 600; |
|
|
|
|
|
// Données du réseau |
|
|
const nodes = {nodes}; |
|
|
const links = {links}; |
|
|
|
|
|
// Créer le SVG |
|
|
const svg = d3.select("#network") |
|
|
.append("svg") |
|
|
.attr("width", width) |
|
|
.attr("height", height); |
|
|
|
|
|
// Tooltip |
|
|
const tooltip = d3.select(".tooltip"); |
|
|
|
|
|
// Simulation de force |
|
|
const simulation = d3.forceSimulation(nodes) |
|
|
.force("link", d3.forceLink(links).id(d => d.id).distance(100)) |
|
|
.force("charge", d3.forceManyBody().strength(-300)) |
|
|
.force("center", d3.forceCenter(width / 2, height / 2)); |
|
|
|
|
|
// Créer les liens |
|
|
const link = svg.append("g") |
|
|
.selectAll("line") |
|
|
.data(links) |
|
|
.enter().append("line") |
|
|
.attr("class", "link"); |
|
|
|
|
|
// Créer les nœuds |
|
|
const node = svg.append("g") |
|
|
.selectAll("circle") |
|
|
.data(nodes) |
|
|
.enter().append("circle") |
|
|
.attr("class", "node") |
|
|
.attr("r", 15) |
|
|
.attr("fill", d => d.color) |
|
|
.on("mouseover", function(event, d) {{ |
|
|
tooltip.transition().duration(200).style("opacity", .9); |
|
|
tooltip.html(d.title) |
|
|
.style("left", (event.pageX + 10) + "px") |
|
|
.style("top", (event.pageY - 28) + "px"); |
|
|
}}) |
|
|
.on("mouseout", function() {{ |
|
|
tooltip.transition().duration(500).style("opacity", 0); |
|
|
}}) |
|
|
.call(d3.drag() |
|
|
.on("start", dragstarted) |
|
|
.on("drag", dragged) |
|
|
.on("end", dragended)); |
|
|
|
|
|
// Étiquettes des nœuds |
|
|
const labels = svg.append("g") |
|
|
.selectAll("text") |
|
|
.data(nodes) |
|
|
.enter().append("text") |
|
|
.attr("class", "node-label") |
|
|
.text(d => d.id.replace('_', ' ')); |
|
|
|
|
|
// Mise à jour de la simulation |
|
|
simulation.on("tick", () => {{ |
|
|
link |
|
|
.attr("x1", d => d.source.x) |
|
|
.attr("y1", d => d.source.y) |
|
|
.attr("x2", d => d.target.x) |
|
|
.attr("y2", d => d.target.y); |
|
|
|
|
|
node |
|
|
.attr("cx", d => d.x) |
|
|
.attr("cy", d => d.y); |
|
|
|
|
|
labels |
|
|
.attr("x", d => d.x) |
|
|
.attr("y", d => d.y + 25); |
|
|
}}); |
|
|
|
|
|
// Fonctions de drag |
|
|
function dragstarted(event, d) {{ |
|
|
if (!event.active) simulation.alphaTarget(0.3).restart(); |
|
|
d.fx = d.x; |
|
|
d.fy = d.y; |
|
|
}} |
|
|
|
|
|
function dragged(event, d) {{ |
|
|
d.fx = event.x; |
|
|
d.fy = event.y; |
|
|
}} |
|
|
|
|
|
function dragended(event, d) {{ |
|
|
if (!event.active) simulation.alphaTarget(0); |
|
|
d.fx = null; |
|
|
d.fy = null; |
|
|
}} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
|
|
|
st.components.v1.html(d3_code, height=650) |
|
|
|
|
|
|
|
|
st.subheader("📋 Légende") |
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
with col1: |
|
|
st.markdown("🔴 **Non-actionnables**") |
|
|
st.write("Variables démographiques") |
|
|
|
|
|
with col2: |
|
|
st.markdown("🔵 **Actionnables**") |
|
|
st.write("Variables modifiables") |
|
|
|
|
|
with col3: |
|
|
st.markdown("🟦 **Intermédiaires**") |
|
|
st.write("Variables de transition") |
|
|
|
|
|
with col4: |
|
|
st.markdown("🟢 **Résultat**") |
|
|
st.write("Variable d'autonomie") |
|
|
|
|
|
st.info("💡 **Interaction**: Cliquez et faites glisser les nœuds pour explorer la structure du réseau!") |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |