Spaces:
Sleeping
Sleeping
Update src/dashboard_app.py
Browse files- src/dashboard_app.py +137 -10
src/dashboard_app.py
CHANGED
|
@@ -162,6 +162,7 @@ def load_data(source_type, source_value, header_param, sep=None, db_config=None)
|
|
| 162 |
st.session_state.data_loaded_id = data_id
|
| 163 |
st.session_state.last_header_preference = (header_param == 0)
|
| 164 |
st.sidebar.success("Données chargées avec succès.")
|
|
|
|
| 165 |
st.rerun()
|
| 166 |
else:
|
| 167 |
st.session_state.dataframe_to_export = None
|
|
@@ -242,6 +243,28 @@ app_tab, manual_tab, chat_tab, schedule_tab = st.tabs([
|
|
| 242 |
# ONGLET APPLICATION PRINCIPALE
|
| 243 |
# ==============================================================================
|
| 244 |
with app_tab:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
with st.sidebar:
|
| 246 |
st.header("⚙️ Configuration")
|
| 247 |
|
|
@@ -549,7 +572,7 @@ with app_tab:
|
|
| 549 |
if columns_defined:
|
| 550 |
st.subheader("🛠️ Construire les Analyses")
|
| 551 |
st.write("Ajoutez des blocs d'analyse pour explorer vos données.")
|
| 552 |
-
col_add1, col_add2, col_add3 = st.columns(
|
| 553 |
analysis_key_suffix = st.session_state.data_loaded_id or "data_loaded"
|
| 554 |
with col_add1:
|
| 555 |
if st.button("➕ Tableau Agrégé", key=f"add_agg_{analysis_key_suffix}", help="Stats groupées (ex: moyenne par catégorie)."):
|
|
@@ -566,6 +589,11 @@ with app_tab:
|
|
| 566 |
new_id = max([a.get('id', -1) for a in st.session_state.analyses] + [-1]) + 1
|
| 567 |
st.session_state.analyses.append({'type': 'descriptive_stats', 'params': {}, 'result': None, 'id': new_id, 'executed_params': None})
|
| 568 |
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
else:
|
| 570 |
st.warning("Impossible de définir les types de colonnes. Vérifiez les données ou l'erreur de chargement. Section Analyse désactivée.")
|
| 571 |
|
|
@@ -816,6 +844,12 @@ with app_tab:
|
|
| 816 |
selected_size = st.selectbox(f"Taille (Opt., Num.):", map_options_num_orig, index=get_safe_index(map_options_num_orig, analysis['params'].get('size_column')), key=f"graph_size_{analysis_id}", disabled=size_disabled, format_func=lambda x: x if x is not None else "Aucune")
|
| 817 |
st.session_state.analyses[i]['params']['size_column'] = selected_size
|
| 818 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
# --- Facet, Hover & Autres ---
|
| 820 |
col1_extra, col2_extra = st.columns(2)
|
| 821 |
with col1_extra:
|
|
@@ -930,15 +964,21 @@ with app_tab:
|
|
| 930 |
if final_value and graph_analysis_type in ['sunburst', 'treemap']: px_args['values'] = final_value
|
| 931 |
if final_gantt_end and graph_analysis_type == 'timeline': px_args['x_end'] = final_gantt_end; px_args['x_start'] = final_x # x_start est la colonne X
|
| 932 |
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
|
| 938 |
-
if final_x: title_parts.append(final_x)
|
| 939 |
-
if final_color: title_parts.append(f"par {final_color}")
|
| 940 |
-
if is_aggregated: title_parts.append("(Agrégé)")
|
| 941 |
-
px_args['title'] = " ".join(title_parts)
|
| 942 |
|
| 943 |
# --- Génération Plotly ---
|
| 944 |
try:
|
|
@@ -1066,7 +1106,94 @@ with app_tab:
|
|
| 1066 |
analysis_type_display = st.session_state.analyses[i]['type'] # Renamed to avoid conflict
|
| 1067 |
try:
|
| 1068 |
if analysis_type_display in ['aggregated_table', 'descriptive_stats'] and isinstance(result_data, pd.DataFrame):
|
| 1069 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
elif analysis_type_display == 'graph' and isinstance(result_data, go.Figure):
|
| 1071 |
st.plotly_chart(result_data, use_container_width=True)
|
| 1072 |
else: st.write("Résultat non standard:"); st.write(result_data)
|
|
|
|
| 162 |
st.session_state.data_loaded_id = data_id
|
| 163 |
st.session_state.last_header_preference = (header_param == 0)
|
| 164 |
st.sidebar.success("Données chargées avec succès.")
|
| 165 |
+
st.info("Les données chargées sont disponibles sous le nom `data` pour les requêtes SQL.") # Added line
|
| 166 |
st.rerun()
|
| 167 |
else:
|
| 168 |
st.session_state.dataframe_to_export = None
|
|
|
|
| 243 |
# ONGLET APPLICATION PRINCIPALE
|
| 244 |
# ==============================================================================
|
| 245 |
with app_tab:
|
| 246 |
+
st.markdown("""
|
| 247 |
+
<style>
|
| 248 |
+
/* Increase font size for dataframe cells */
|
| 249 |
+
.stDataFrame div[data-testid="stDataframeData"] div {
|
| 250 |
+
font-size: 1.2em !important;
|
| 251 |
+
}
|
| 252 |
+
/* Increase font size for Plotly chart text (labels, tooltips, etc.) */
|
| 253 |
+
.js-plotly-plot .plotly .modebar,
|
| 254 |
+
.js-plotly-plot .plotly .cursor-pointer,
|
| 255 |
+
.js-plotly-plot .plotly .legendtext,
|
| 256 |
+
.js-plotly-plot .plotly .xtick,
|
| 257 |
+
.js-plotly-plot .plotly .ytick,
|
| 258 |
+
.js-plotly-plot .plotly .annotation-text,
|
| 259 |
+
.js-plotly-plot .plotly .gtext {
|
| 260 |
+
font-size: 1.2em !important;
|
| 261 |
+
}
|
| 262 |
+
/* Increase font size for table headers */
|
| 263 |
+
.stDataFrame div[data-testid="stDataframeHeaders"] div {
|
| 264 |
+
font-size: 1.2em !important;
|
| 265 |
+
}
|
| 266 |
+
</style>
|
| 267 |
+
""", unsafe_allow_html=True)
|
| 268 |
with st.sidebar:
|
| 269 |
st.header("⚙️ Configuration")
|
| 270 |
|
|
|
|
| 572 |
if columns_defined:
|
| 573 |
st.subheader("🛠️ Construire les Analyses")
|
| 574 |
st.write("Ajoutez des blocs d'analyse pour explorer vos données.")
|
| 575 |
+
col_add1, col_add2, col_add3, col_add4 = st.columns(4) # Added col_add4
|
| 576 |
analysis_key_suffix = st.session_state.data_loaded_id or "data_loaded"
|
| 577 |
with col_add1:
|
| 578 |
if st.button("➕ Tableau Agrégé", key=f"add_agg_{analysis_key_suffix}", help="Stats groupées (ex: moyenne par catégorie)."):
|
|
|
|
| 589 |
new_id = max([a.get('id', -1) for a in st.session_state.analyses] + [-1]) + 1
|
| 590 |
st.session_state.analyses.append({'type': 'descriptive_stats', 'params': {}, 'result': None, 'id': new_id, 'executed_params': None})
|
| 591 |
st.rerun()
|
| 592 |
+
with col_add4: # Added new button
|
| 593 |
+
if st.button("➕ Requête SQL", key=f"add_sql_{analysis_key_suffix}", help="Exécuter une requête SQL sur les données chargées."):
|
| 594 |
+
new_id = max([a.get('id', -1) for a in st.session_state.analyses] + [-1]) + 1
|
| 595 |
+
st.session_state.analyses.append({'type': 'sql_query', 'params': {'query': ''}, 'result': None, 'id': new_id, 'executed_params': None})
|
| 596 |
+
st.rerun()
|
| 597 |
else:
|
| 598 |
st.warning("Impossible de définir les types de colonnes. Vérifiez les données ou l'erreur de chargement. Section Analyse désactivée.")
|
| 599 |
|
|
|
|
| 844 |
selected_size = st.selectbox(f"Taille (Opt., Num.):", map_options_num_orig, index=get_safe_index(map_options_num_orig, analysis['params'].get('size_column')), key=f"graph_size_{analysis_id}", disabled=size_disabled, format_func=lambda x: x if x is not None else "Aucune")
|
| 845 |
st.session_state.analyses[i]['params']['size_column'] = selected_size
|
| 846 |
|
| 847 |
+
# Add a text input for the chart title
|
| 848 |
+
init_analysis_state(i, 'chart_title', '') # Initialize state for title
|
| 849 |
+
chart_title_input = st.text_input("Titre du graphique (Opt.):", value=analysis['params'].get('chart_title', ''), key=f"graph_title_{analysis_id}")
|
| 850 |
+
st.session_state.analyses[i]['params']['chart_title'] = chart_title_input
|
| 851 |
+
|
| 852 |
+
|
| 853 |
# --- Facet, Hover & Autres ---
|
| 854 |
col1_extra, col2_extra = st.columns(2)
|
| 855 |
with col1_extra:
|
|
|
|
| 964 |
if final_value and graph_analysis_type in ['sunburst', 'treemap']: px_args['values'] = final_value
|
| 965 |
if final_gantt_end and graph_analysis_type == 'timeline': px_args['x_end'] = final_gantt_end; px_args['x_start'] = final_x # x_start est la colonne X
|
| 966 |
|
| 967 |
+
# --- Set Title ---
|
| 968 |
+
final_chart_title = current_params.get('chart_title', '') # Retrieve the chart title
|
| 969 |
+
if final_chart_title:
|
| 970 |
+
px_args['title'] = final_chart_title
|
| 971 |
+
else:
|
| 972 |
+
# Keep the auto-generated title if no custom title is provided
|
| 973 |
+
title_parts = [graph_analysis_type.title()]
|
| 974 |
+
if final_y and graph_analysis_type not in ['histogram', 'pie', 'radar', 'scatter_matrix', 'sunburst', 'treemap']: title_parts.append(f"{final_y} vs")
|
| 975 |
+
elif graph_analysis_type == 'pie' and final_value: title_parts.append(f"{final_value} par")
|
| 976 |
+
elif graph_analysis_type == 'radar' and final_y: title_parts.append(f"{final_y} pour")
|
| 977 |
+
if final_x: title_parts.append(final_x)
|
| 978 |
+
if final_color: title_parts.append(f"par {final_color}")
|
| 979 |
+
if is_aggregated: title_parts.append("(Agrégé)")
|
| 980 |
+
px_args['title'] = " ".join(title_parts)
|
| 981 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 982 |
|
| 983 |
# --- Génération Plotly ---
|
| 984 |
try:
|
|
|
|
| 1106 |
analysis_type_display = st.session_state.analyses[i]['type'] # Renamed to avoid conflict
|
| 1107 |
try:
|
| 1108 |
if analysis_type_display in ['aggregated_table', 'descriptive_stats'] and isinstance(result_data, pd.DataFrame):
|
| 1109 |
+
if analysis_type_display == 'aggregated_table':
|
| 1110 |
+
# Apply formatting to aggregated table
|
| 1111 |
+
# Identify numerical columns in the aggregated result
|
| 1112 |
+
agg_numerical_cols = result_data.select_dtypes(include=np.number).columns.tolist()
|
| 1113 |
+
format_dict = {col: '{:,.2f}' for col in agg_numerical_cols} # Example: format numerical columns to 2 decimal places with comma separator
|
| 1114 |
+
st.dataframe(result_data.style.format(format_dict), use_container_width=True)
|
| 1115 |
+
else: # descriptive_stats
|
| 1116 |
+
st.dataframe(result_data.T, use_container_width=True)
|
| 1117 |
+
elif analysis_type_display == 'graph' and isinstance(result_data, go.Figure):
|
| 1118 |
+
st.plotly_chart(result_data, use_container_width=True)
|
| 1119 |
+
else: st.write("Résultat non standard:"); st.write(result_data)
|
| 1120 |
+
except Exception as e_display_result: st.error(f"Erreur affichage résultat {i+1}: {e_display_result}")
|
| 1121 |
+
elif executed_params_display is not None: # Si on a exécuté mais pas de résultat
|
| 1122 |
+
st.warning(f"L'exécution précédente de l'Analyse {i+1} a échoué ou n'a produit aucun résultat.", icon="⚠️")
|
| 1123 |
+
|
| 1124 |
+
# ===========================
|
| 1125 |
+
# Bloc Requête SQL
|
| 1126 |
+
# ===========================
|
| 1127 |
+
elif analysis['type'] == 'sql_query':
|
| 1128 |
+
st.markdown("##### Configuration Requête SQL")
|
| 1129 |
+
st.info("Exécutez une requête SQL sur les données chargées. Les données sont disponibles sous le nom `data`.", icon="💡")
|
| 1130 |
+
init_analysis_state(i, 'query', analysis['params'].get('query', 'SELECT * FROM data LIMIT 10')) # Default query
|
| 1131 |
+
|
| 1132 |
+
query_input = st.text_area("Entrez votre requête SQL:", value=analysis['params']['query'], height=150, key=f"sql_query_input_{analysis_id}")
|
| 1133 |
+
st.session_state.analyses[i]['params']['query'] = query_input
|
| 1134 |
+
|
| 1135 |
+
if st.button(f"Exécuter Requête SQL {i+1}", key=f"run_sql_query_{analysis_id}"):
|
| 1136 |
+
current_params = st.session_state.analyses[i]['params'].copy()
|
| 1137 |
+
sql_query = current_params.get('query', '').strip()
|
| 1138 |
+
|
| 1139 |
+
if not sql_query:
|
| 1140 |
+
st.warning("Veuillez entrer une requête SQL.")
|
| 1141 |
+
else:
|
| 1142 |
+
try:
|
| 1143 |
+
# Use pandasql to execute the query
|
| 1144 |
+
# The dataframe is available as 'data' in the query context
|
| 1145 |
+
from pandasql import sqldf
|
| 1146 |
+
|
| 1147 |
+
# Ensure the 'data' variable is available in the execution context
|
| 1148 |
+
# sqldf requires the dataframe to be in the global or local scope
|
| 1149 |
+
# We pass it explicitly via locals()
|
| 1150 |
+
result_df = sqldf(sql_query, locals())
|
| 1151 |
+
|
| 1152 |
+
if result_df is not None:
|
| 1153 |
+
st.session_state.analyses[i]['result'] = result_df
|
| 1154 |
+
st.session_state.analyses[i]['executed_params'] = current_params
|
| 1155 |
+
st.rerun()
|
| 1156 |
+
else:
|
| 1157 |
+
st.warning("La requête n'a retourné aucun résultat.")
|
| 1158 |
+
|
| 1159 |
+
except Exception as e:
|
| 1160 |
+
st.error(f"Erreur lors de l'exécution de la requête SQL {i+1}: {e}")
|
| 1161 |
+
st.session_state.analyses[i]['result'] = None
|
| 1162 |
+
st.session_state.analyses[i]['executed_params'] = current_params # Save params even if failed
|
| 1163 |
+
|
| 1164 |
+
# --- AFFICHAGE RÉSULTAT ---
|
| 1165 |
+
result_data = st.session_state.analyses[i].get('result')
|
| 1166 |
+
executed_params_display = st.session_state.analyses[i].get('executed_params')
|
| 1167 |
+
if result_data is not None:
|
| 1168 |
+
st.markdown("---"); st.write(f"**Résultat Analyse {i+1}:**")
|
| 1169 |
+
if executed_params_display:
|
| 1170 |
+
params_str_list = []
|
| 1171 |
+
for k,v in executed_params_display.items():
|
| 1172 |
+
if v is not None and v != [] and k not in ['type']:
|
| 1173 |
+
v_repr = f"[{v[0]}, ..., {v[-1]}] ({len(v)})" if isinstance(v, list) and len(v) > 3 else str(v)
|
| 1174 |
+
k_simple = k.replace('_graph','').replace('desc','').replace('_columns','').replace('column','').replace('_',' ').strip().title()
|
| 1175 |
+
params_str_list.append(f"{k_simple}={v_repr}")
|
| 1176 |
+
if params_str_list: st.caption(f"Paramètres: {'; '.join(params_str_list)}")
|
| 1177 |
+
analysis_type_display = st.session_state.analyses[i]['type'] # Renamed to avoid conflict
|
| 1178 |
+
try:
|
| 1179 |
+
if analysis_type_display in ['aggregated_table', 'descriptive_stats', 'sql_query'] and isinstance(result_data, pd.DataFrame):
|
| 1180 |
+
if analysis_type_display == 'aggregated_table':
|
| 1181 |
+
# Apply formatting to aggregated table
|
| 1182 |
+
# Identify numerical columns in the aggregated result
|
| 1183 |
+
agg_numerical_cols = result_data.select_dtypes(include=np.number).columns.tolist()
|
| 1184 |
+
# Exclude group-by columns from formatting if they are numeric (unlikely but safe)
|
| 1185 |
+
group_by_cols = st.session_state.analyses[i]['executed_params'].get('group_by_columns', [])
|
| 1186 |
+
cols_to_format = [col for col in agg_numerical_cols if col not in group_by_cols]
|
| 1187 |
+
|
| 1188 |
+
format_dict = {col: '{:,.2f}' for col in cols_to_format} # Format numerical columns to 2 decimal places with comma separator
|
| 1189 |
+
|
| 1190 |
+
# Handle the 'count' column specifically if it exists and is not a group-by column
|
| 1191 |
+
if 'count' in result_data.columns and 'count' not in group_by_cols:
|
| 1192 |
+
format_dict['count'] = '{:,}' # Format count as integer with comma separator
|
| 1193 |
+
|
| 1194 |
+
st.dataframe(result_data.style.format(format_dict), use_container_width=True)
|
| 1195 |
+
else: # descriptive_stats or sql_query
|
| 1196 |
+
st.dataframe(result_data.T if analysis_type_display == 'descriptive_stats' else result_data, use_container_width=True) # Transpose only for descriptive_stats
|
| 1197 |
elif analysis_type_display == 'graph' and isinstance(result_data, go.Figure):
|
| 1198 |
st.plotly_chart(result_data, use_container_width=True)
|
| 1199 |
else: st.write("Résultat non standard:"); st.write(result_data)
|