mcli-framework 7.1.2__py3-none-any.whl → 7.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/main.py +10 -0
- mcli/lib/custom_commands.py +424 -0
- mcli/lib/paths.py +12 -0
- mcli/ml/dashboard/app.py +13 -13
- mcli/ml/dashboard/app_integrated.py +1949 -70
- mcli/ml/dashboard/app_supabase.py +46 -21
- mcli/ml/dashboard/app_training.py +14 -14
- mcli/ml/dashboard/components/charts.py +258 -0
- mcli/ml/dashboard/components/metrics.py +125 -0
- mcli/ml/dashboard/components/tables.py +228 -0
- mcli/ml/dashboard/pages/cicd.py +382 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +820 -0
- mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
- mcli/ml/dashboard/pages/workflows.py +533 -0
- mcli/ml/training/train_model.py +569 -0
- mcli/self/self_cmd.py +322 -94
- mcli/workflow/politician_trading/data_sources.py +259 -1
- mcli/workflow/politician_trading/models.py +159 -1
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
- mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
- mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
- mcli/workflow/politician_trading/seed_database.py +539 -0
- mcli/workflow/workflow.py +8 -27
- {mcli_framework-7.1.2.dist-info → mcli_framework-7.2.0.dist-info}/METADATA +1 -1
- {mcli_framework-7.1.2.dist-info → mcli_framework-7.2.0.dist-info}/RECORD +29 -25
- mcli/workflow/daemon/api_daemon.py +0 -800
- mcli/workflow/daemon/commands.py +0 -1196
- mcli/workflow/dashboard/dashboard_cmd.py +0 -120
- mcli/workflow/file/file.py +0 -100
- mcli/workflow/git_commit/commands.py +0 -430
- mcli/workflow/politician_trading/commands.py +0 -1939
- mcli/workflow/scheduler/commands.py +0 -493
- mcli/workflow/sync/sync_cmd.py +0 -437
- mcli/workflow/videos/videos.py +0 -242
- {mcli_framework-7.1.2.dist-info → mcli_framework-7.2.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.2.dist-info → mcli_framework-7.2.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.2.dist-info → mcli_framework-7.2.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.2.dist-info → mcli_framework-7.2.0.dist-info}/top_level.txt +0 -0
|
@@ -42,18 +42,34 @@ st.markdown(
|
|
|
42
42
|
|
|
43
43
|
@st.cache_resource
|
|
44
44
|
def get_supabase_client() -> Client:
|
|
45
|
-
"""Get Supabase client"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
"""Get Supabase client with Streamlit Cloud secrets support"""
|
|
46
|
+
# Try Streamlit secrets first (for Streamlit Cloud), then fall back to environment variables (for local dev)
|
|
47
|
+
try:
|
|
48
|
+
url = st.secrets.get("SUPABASE_URL", "")
|
|
49
|
+
key = st.secrets.get("SUPABASE_KEY", "") or st.secrets.get("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
50
|
+
except (AttributeError, FileNotFoundError):
|
|
51
|
+
# Secrets not available, try environment variables
|
|
52
|
+
url = os.getenv("SUPABASE_URL", "")
|
|
53
|
+
key = os.getenv("SUPABASE_KEY", "") or os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
49
54
|
|
|
50
55
|
if not url or not key:
|
|
51
56
|
st.warning(
|
|
52
|
-
"⚠️ Supabase credentials not found.
|
|
57
|
+
"⚠️ Supabase credentials not found. Configure SUPABASE_URL and SUPABASE_KEY in Streamlit Cloud secrets or environment variables."
|
|
53
58
|
)
|
|
54
59
|
return None
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
try:
|
|
62
|
+
client = create_client(url, key)
|
|
63
|
+
# Test connection
|
|
64
|
+
try:
|
|
65
|
+
client.table("politicians").select("id").limit(1).execute()
|
|
66
|
+
return client
|
|
67
|
+
except Exception as e:
|
|
68
|
+
st.error(f"❌ Supabase connection test failed: {e}")
|
|
69
|
+
return None
|
|
70
|
+
except Exception as e:
|
|
71
|
+
st.error(f"❌ Failed to create Supabase client: {e}")
|
|
72
|
+
return None
|
|
57
73
|
|
|
58
74
|
|
|
59
75
|
@st.cache_data(ttl=30)
|
|
@@ -91,9 +107,18 @@ def get_disclosures_data():
|
|
|
91
107
|
.limit(500)
|
|
92
108
|
.execute()
|
|
93
109
|
)
|
|
94
|
-
|
|
110
|
+
df = pd.DataFrame(response.data)
|
|
111
|
+
|
|
112
|
+
# Convert datetime columns to proper datetime format
|
|
113
|
+
date_columns = ['transaction_date', 'disclosure_date', 'created_at', 'updated_at']
|
|
114
|
+
for col in date_columns:
|
|
115
|
+
if col in df.columns:
|
|
116
|
+
df[col] = pd.to_datetime(df[col], format='ISO8601', errors='coerce')
|
|
117
|
+
|
|
118
|
+
return df
|
|
95
119
|
except Exception as e:
|
|
96
120
|
st.error(f"Error fetching disclosures: {e}")
|
|
121
|
+
print(f"Error details: {e}") # Debug output
|
|
97
122
|
return pd.DataFrame()
|
|
98
123
|
|
|
99
124
|
|
|
@@ -330,7 +355,7 @@ def show_overview():
|
|
|
330
355
|
fig = px.pie(
|
|
331
356
|
values=type_counts.values, names=type_counts.index, title="Transaction Types"
|
|
332
357
|
)
|
|
333
|
-
st.plotly_chart(fig,
|
|
358
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
334
359
|
else:
|
|
335
360
|
st.info("No disclosure data available")
|
|
336
361
|
|
|
@@ -344,7 +369,7 @@ def show_overview():
|
|
|
344
369
|
orientation="h",
|
|
345
370
|
title="Most Traded Stocks",
|
|
346
371
|
)
|
|
347
|
-
st.plotly_chart(fig,
|
|
372
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
348
373
|
else:
|
|
349
374
|
st.info("No ticker data available")
|
|
350
375
|
|
|
@@ -383,7 +408,7 @@ def show_politicians():
|
|
|
383
408
|
filtered = filtered[filtered["is_active"] == True]
|
|
384
409
|
|
|
385
410
|
# Display data
|
|
386
|
-
st.dataframe(filtered,
|
|
411
|
+
st.dataframe(filtered, width="stretch")
|
|
387
412
|
|
|
388
413
|
# Stats
|
|
389
414
|
col1, col2 = st.columns(2)
|
|
@@ -393,14 +418,14 @@ def show_politicians():
|
|
|
393
418
|
fig = px.pie(
|
|
394
419
|
values=party_dist.values, names=party_dist.index, title="Party Distribution"
|
|
395
420
|
)
|
|
396
|
-
st.plotly_chart(fig,
|
|
421
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
397
422
|
with col2:
|
|
398
423
|
if "state" in filtered:
|
|
399
424
|
state_dist = filtered["state"].value_counts().head(10)
|
|
400
425
|
fig = px.bar(
|
|
401
426
|
x=state_dist.values, y=state_dist.index, orientation="h", title="Top States"
|
|
402
427
|
)
|
|
403
|
-
st.plotly_chart(fig,
|
|
428
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
404
429
|
else:
|
|
405
430
|
st.warning("No politician data available")
|
|
406
431
|
|
|
@@ -447,7 +472,7 @@ def show_disclosures():
|
|
|
447
472
|
]
|
|
448
473
|
|
|
449
474
|
# Display data
|
|
450
|
-
st.dataframe(filtered,
|
|
475
|
+
st.dataframe(filtered, width="stretch")
|
|
451
476
|
|
|
452
477
|
# Analysis
|
|
453
478
|
if not filtered.empty:
|
|
@@ -463,7 +488,7 @@ def show_disclosures():
|
|
|
463
488
|
y=daily_volume.values,
|
|
464
489
|
title="Trading Volume Over Time",
|
|
465
490
|
)
|
|
466
|
-
st.plotly_chart(fig,
|
|
491
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
467
492
|
|
|
468
493
|
with col2:
|
|
469
494
|
# Top politicians by trading
|
|
@@ -475,7 +500,7 @@ def show_disclosures():
|
|
|
475
500
|
orientation="h",
|
|
476
501
|
title="Most Active Traders",
|
|
477
502
|
)
|
|
478
|
-
st.plotly_chart(fig,
|
|
503
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
479
504
|
else:
|
|
480
505
|
st.warning("No disclosure data available")
|
|
481
506
|
|
|
@@ -487,14 +512,14 @@ def show_predictions():
|
|
|
487
512
|
predictions = get_predictions_data()
|
|
488
513
|
|
|
489
514
|
if not predictions.empty:
|
|
490
|
-
st.dataframe(predictions,
|
|
515
|
+
st.dataframe(predictions, width="stretch")
|
|
491
516
|
|
|
492
517
|
# Add prediction analysis charts if we have data
|
|
493
518
|
if "confidence" in predictions:
|
|
494
519
|
fig = px.histogram(
|
|
495
520
|
predictions, x="confidence", title="Prediction Confidence Distribution"
|
|
496
521
|
)
|
|
497
|
-
st.plotly_chart(fig,
|
|
522
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
498
523
|
else:
|
|
499
524
|
st.info(
|
|
500
525
|
"No ML predictions available yet. The ML pipeline will generate predictions once sufficient data is collected."
|
|
@@ -521,7 +546,7 @@ def show_jobs():
|
|
|
521
546
|
st.metric("Failed", status_counts.get("failed", 0))
|
|
522
547
|
|
|
523
548
|
# Jobs table
|
|
524
|
-
st.dataframe(jobs,
|
|
549
|
+
st.dataframe(jobs, width="stretch")
|
|
525
550
|
|
|
526
551
|
# Success rate over time
|
|
527
552
|
if "created_at" in jobs:
|
|
@@ -543,7 +568,7 @@ def show_jobs():
|
|
|
543
568
|
)
|
|
544
569
|
|
|
545
570
|
fig.update_layout(title="Job Status Over Time", xaxis_title="Date", yaxis_title="Count")
|
|
546
|
-
st.plotly_chart(fig,
|
|
571
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
547
572
|
else:
|
|
548
573
|
st.warning("No job data available")
|
|
549
574
|
|
|
@@ -617,11 +642,11 @@ def show_system_health():
|
|
|
617
642
|
|
|
618
643
|
col1, col2 = st.columns(2)
|
|
619
644
|
with col1:
|
|
620
|
-
st.dataframe(stats_df,
|
|
645
|
+
st.dataframe(stats_df, width="stretch")
|
|
621
646
|
|
|
622
647
|
with col2:
|
|
623
648
|
fig = px.bar(stats_df, x="Entity", y="Count", title="Database Records")
|
|
624
|
-
st.plotly_chart(fig,
|
|
649
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
625
650
|
|
|
626
651
|
|
|
627
652
|
if __name__ == "__main__":
|
|
@@ -203,7 +203,7 @@ def show_model_comparison():
|
|
|
203
203
|
sorted_df.style.highlight_max(
|
|
204
204
|
subset=["test_accuracy", "r2"], color="lightgreen"
|
|
205
205
|
).highlight_min(subset=["test_loss", "rmse", "mae"], color="lightgreen"),
|
|
206
|
-
|
|
206
|
+
width="stretch",
|
|
207
207
|
)
|
|
208
208
|
|
|
209
209
|
# Visualization section
|
|
@@ -222,7 +222,7 @@ def show_model_comparison():
|
|
|
222
222
|
labels={"value": "Accuracy", "variable": "Split"},
|
|
223
223
|
)
|
|
224
224
|
fig.update_layout(xaxis_tickangle=-45)
|
|
225
|
-
st.plotly_chart(fig,
|
|
225
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
226
226
|
|
|
227
227
|
with col2:
|
|
228
228
|
# Loss comparison
|
|
@@ -235,7 +235,7 @@ def show_model_comparison():
|
|
|
235
235
|
labels={"value": "Loss", "variable": "Split"},
|
|
236
236
|
)
|
|
237
237
|
fig.update_layout(xaxis_tickangle=-45)
|
|
238
|
-
st.plotly_chart(fig,
|
|
238
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
239
239
|
|
|
240
240
|
# Additional metrics if available
|
|
241
241
|
if models_df["rmse"].max() > 0:
|
|
@@ -253,7 +253,7 @@ def show_model_comparison():
|
|
|
253
253
|
hover_data=["name"],
|
|
254
254
|
title="RMSE vs MAE (sized by R²)",
|
|
255
255
|
)
|
|
256
|
-
st.plotly_chart(fig,
|
|
256
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
257
257
|
|
|
258
258
|
with col2:
|
|
259
259
|
# R² score comparison
|
|
@@ -267,7 +267,7 @@ def show_model_comparison():
|
|
|
267
267
|
color_continuous_scale="Greens",
|
|
268
268
|
)
|
|
269
269
|
fig.update_layout(xaxis_tickangle=-45)
|
|
270
|
-
st.plotly_chart(fig,
|
|
270
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
def show_residual_analysis():
|
|
@@ -313,7 +313,7 @@ def show_residual_analysis():
|
|
|
313
313
|
fig.update_layout(
|
|
314
314
|
xaxis_title="Prediction Index", yaxis_title="Residuals", hovermode="x unified"
|
|
315
315
|
)
|
|
316
|
-
st.plotly_chart(fig,
|
|
316
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
317
317
|
|
|
318
318
|
# Statistics
|
|
319
319
|
col1, col2, col3 = st.columns(3)
|
|
@@ -347,7 +347,7 @@ def show_residual_analysis():
|
|
|
347
347
|
)
|
|
348
348
|
|
|
349
349
|
fig.update_layout(xaxis_title="Residuals", yaxis_title="Frequency", showlegend=True)
|
|
350
|
-
st.plotly_chart(fig,
|
|
350
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
351
351
|
|
|
352
352
|
# Normality tests
|
|
353
353
|
_, p_value = stats.normaltest(residuals)
|
|
@@ -389,7 +389,7 @@ def show_residual_analysis():
|
|
|
389
389
|
yaxis_title="Sample Quantiles",
|
|
390
390
|
title="Q-Q Plot (Normal Distribution)",
|
|
391
391
|
)
|
|
392
|
-
st.plotly_chart(fig,
|
|
392
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
393
393
|
|
|
394
394
|
st.info(f"Correlation with normal distribution: {r:.4f}")
|
|
395
395
|
|
|
@@ -415,7 +415,7 @@ def show_residual_analysis():
|
|
|
415
415
|
yaxis_title="Residuals",
|
|
416
416
|
title="Residuals vs Predicted (looking for patterns)",
|
|
417
417
|
)
|
|
418
|
-
st.plotly_chart(fig,
|
|
418
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
419
419
|
|
|
420
420
|
st.info(
|
|
421
421
|
"💡 Ideally, residuals should be randomly scattered around zero with no clear patterns."
|
|
@@ -463,11 +463,11 @@ def show_feature_importance():
|
|
|
463
463
|
color_continuous_scale="Viridis",
|
|
464
464
|
)
|
|
465
465
|
fig.update_layout(height=600, yaxis={"categoryorder": "total ascending"})
|
|
466
|
-
st.plotly_chart(fig,
|
|
466
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
467
467
|
|
|
468
468
|
# Feature importance table
|
|
469
469
|
st.subheader("Feature Importance Table")
|
|
470
|
-
st.dataframe(feature_df.head(top_n),
|
|
470
|
+
st.dataframe(feature_df.head(top_n), width="stretch")
|
|
471
471
|
|
|
472
472
|
# Feature categories (similar to bitcoin project)
|
|
473
473
|
st.subheader("Feature Categories")
|
|
@@ -524,7 +524,7 @@ def show_feature_importance():
|
|
|
524
524
|
names="Category",
|
|
525
525
|
title="Feature Importance by Category",
|
|
526
526
|
)
|
|
527
|
-
st.plotly_chart(fig,
|
|
527
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
528
528
|
else:
|
|
529
529
|
st.warning("No feature information available for this model")
|
|
530
530
|
finally:
|
|
@@ -565,7 +565,7 @@ def show_training_history():
|
|
|
565
565
|
display_df = jobs_df[["name", "status", "started_at", "duration_seconds"]].copy()
|
|
566
566
|
display_df["duration_minutes"] = display_df["duration_seconds"] / 60
|
|
567
567
|
|
|
568
|
-
st.dataframe(display_df,
|
|
568
|
+
st.dataframe(display_df, width="stretch")
|
|
569
569
|
|
|
570
570
|
# Training duration distribution
|
|
571
571
|
if not jobs_df["duration_seconds"].isna().all():
|
|
@@ -578,7 +578,7 @@ def show_training_history():
|
|
|
578
578
|
title="Training Duration Distribution",
|
|
579
579
|
labels={"duration_seconds": "Duration (seconds)"},
|
|
580
580
|
)
|
|
581
|
-
st.plotly_chart(fig,
|
|
581
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True})
|
|
582
582
|
|
|
583
583
|
|
|
584
584
|
def main():
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Reusable chart components for Streamlit dashboards"""
|
|
2
|
+
|
|
3
|
+
import plotly.graph_objects as go
|
|
4
|
+
import plotly.express as px
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import streamlit as st
|
|
7
|
+
from typing import Optional, Dict, List, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_timeline_chart(
|
|
11
|
+
data: pd.DataFrame,
|
|
12
|
+
x_col: str,
|
|
13
|
+
y_col: str,
|
|
14
|
+
title: str = "Timeline",
|
|
15
|
+
color_col: Optional[str] = None,
|
|
16
|
+
height: int = 400
|
|
17
|
+
) -> go.Figure:
|
|
18
|
+
"""Create a timeline chart with Plotly"""
|
|
19
|
+
|
|
20
|
+
fig = px.line(
|
|
21
|
+
data,
|
|
22
|
+
x=x_col,
|
|
23
|
+
y=y_col,
|
|
24
|
+
color=color_col,
|
|
25
|
+
title=title,
|
|
26
|
+
markers=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
fig.update_layout(
|
|
30
|
+
height=height,
|
|
31
|
+
hovermode='x unified',
|
|
32
|
+
showlegend=True if color_col else False
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return fig
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_status_pie_chart(
|
|
39
|
+
data: pd.DataFrame,
|
|
40
|
+
status_col: str,
|
|
41
|
+
title: str = "Status Distribution",
|
|
42
|
+
color_map: Optional[Dict[str, str]] = None
|
|
43
|
+
) -> go.Figure:
|
|
44
|
+
"""Create a pie chart for status distribution"""
|
|
45
|
+
|
|
46
|
+
status_counts = data[status_col].value_counts()
|
|
47
|
+
|
|
48
|
+
if color_map is None:
|
|
49
|
+
color_map = {
|
|
50
|
+
'completed': '#10b981',
|
|
51
|
+
'running': '#3b82f6',
|
|
52
|
+
'pending': '#f59e0b',
|
|
53
|
+
'failed': '#ef4444',
|
|
54
|
+
'cancelled': '#6b7280'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
colors = [color_map.get(status.lower(), '#6b7280') for status in status_counts.index]
|
|
58
|
+
|
|
59
|
+
fig = go.Figure(data=[go.Pie(
|
|
60
|
+
labels=status_counts.index,
|
|
61
|
+
values=status_counts.values,
|
|
62
|
+
marker=dict(colors=colors),
|
|
63
|
+
hole=0.4
|
|
64
|
+
)])
|
|
65
|
+
|
|
66
|
+
fig.update_layout(
|
|
67
|
+
title=title,
|
|
68
|
+
height=350,
|
|
69
|
+
showlegend=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return fig
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_metric_trend_chart(
|
|
76
|
+
data: pd.DataFrame,
|
|
77
|
+
time_col: str,
|
|
78
|
+
metric_col: str,
|
|
79
|
+
title: str,
|
|
80
|
+
target_value: Optional[float] = None
|
|
81
|
+
) -> go.Figure:
|
|
82
|
+
"""Create a metric trend chart with optional target line"""
|
|
83
|
+
|
|
84
|
+
fig = go.Figure()
|
|
85
|
+
|
|
86
|
+
# Add metric line
|
|
87
|
+
fig.add_trace(go.Scatter(
|
|
88
|
+
x=data[time_col],
|
|
89
|
+
y=data[metric_col],
|
|
90
|
+
mode='lines+markers',
|
|
91
|
+
name=metric_col,
|
|
92
|
+
line=dict(width=3),
|
|
93
|
+
marker=dict(size=8)
|
|
94
|
+
))
|
|
95
|
+
|
|
96
|
+
# Add target line if specified
|
|
97
|
+
if target_value is not None:
|
|
98
|
+
fig.add_hline(
|
|
99
|
+
y=target_value,
|
|
100
|
+
line_dash="dash",
|
|
101
|
+
line_color="red",
|
|
102
|
+
annotation_text=f"Target: {target_value}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
fig.update_layout(
|
|
106
|
+
title=title,
|
|
107
|
+
xaxis_title=time_col,
|
|
108
|
+
yaxis_title=metric_col,
|
|
109
|
+
height=400,
|
|
110
|
+
hovermode='x unified'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return fig
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def create_heatmap(
|
|
117
|
+
data: pd.DataFrame,
|
|
118
|
+
x_col: str,
|
|
119
|
+
y_col: str,
|
|
120
|
+
value_col: str,
|
|
121
|
+
title: str = "Heatmap",
|
|
122
|
+
color_scale: str = "Blues"
|
|
123
|
+
) -> go.Figure:
|
|
124
|
+
"""Create a heatmap visualization"""
|
|
125
|
+
|
|
126
|
+
pivot_data = data.pivot_table(
|
|
127
|
+
index=y_col,
|
|
128
|
+
columns=x_col,
|
|
129
|
+
values=value_col,
|
|
130
|
+
aggfunc='mean'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
fig = px.imshow(
|
|
134
|
+
pivot_data,
|
|
135
|
+
color_continuous_scale=color_scale,
|
|
136
|
+
title=title,
|
|
137
|
+
aspect="auto"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
fig.update_layout(height=400)
|
|
141
|
+
|
|
142
|
+
return fig
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def create_gantt_chart(
|
|
146
|
+
data: pd.DataFrame,
|
|
147
|
+
task_col: str,
|
|
148
|
+
start_col: str,
|
|
149
|
+
end_col: str,
|
|
150
|
+
status_col: Optional[str] = None,
|
|
151
|
+
title: str = "Timeline"
|
|
152
|
+
) -> go.Figure:
|
|
153
|
+
"""Create a Gantt chart for job/task scheduling"""
|
|
154
|
+
|
|
155
|
+
fig = px.timeline(
|
|
156
|
+
data,
|
|
157
|
+
x_start=start_col,
|
|
158
|
+
x_end=end_col,
|
|
159
|
+
y=task_col,
|
|
160
|
+
color=status_col,
|
|
161
|
+
title=title
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
fig.update_yaxes(categoryorder="total ascending")
|
|
165
|
+
fig.update_layout(height=max(400, len(data) * 30))
|
|
166
|
+
|
|
167
|
+
return fig
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def create_multi_metric_gauge(
|
|
171
|
+
values: Dict[str, float],
|
|
172
|
+
max_values: Dict[str, float],
|
|
173
|
+
title: str = "Metrics"
|
|
174
|
+
) -> go.Figure:
|
|
175
|
+
"""Create multiple gauge charts in a grid"""
|
|
176
|
+
|
|
177
|
+
from plotly.subplots import make_subplots
|
|
178
|
+
|
|
179
|
+
n_metrics = len(values)
|
|
180
|
+
cols = min(3, n_metrics)
|
|
181
|
+
rows = (n_metrics + cols - 1) // cols
|
|
182
|
+
|
|
183
|
+
fig = make_subplots(
|
|
184
|
+
rows=rows,
|
|
185
|
+
cols=cols,
|
|
186
|
+
specs=[[{'type': 'indicator'}] * cols] * rows,
|
|
187
|
+
subplot_titles=list(values.keys())
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
for idx, (metric, value) in enumerate(values.items()):
|
|
191
|
+
row = idx // cols + 1
|
|
192
|
+
col = idx % cols + 1
|
|
193
|
+
max_val = max_values.get(metric, 100)
|
|
194
|
+
|
|
195
|
+
fig.add_trace(
|
|
196
|
+
go.Indicator(
|
|
197
|
+
mode="gauge+number+delta",
|
|
198
|
+
value=value,
|
|
199
|
+
title={'text': metric},
|
|
200
|
+
gauge={
|
|
201
|
+
'axis': {'range': [None, max_val]},
|
|
202
|
+
'bar': {'color': "darkblue"},
|
|
203
|
+
'steps': [
|
|
204
|
+
{'range': [0, max_val * 0.5], 'color': "lightgray"},
|
|
205
|
+
{'range': [max_val * 0.5, max_val * 0.8], 'color': "gray"}
|
|
206
|
+
],
|
|
207
|
+
'threshold': {
|
|
208
|
+
'line': {'color': "red", 'width': 4},
|
|
209
|
+
'thickness': 0.75,
|
|
210
|
+
'value': max_val * 0.9
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
row=row,
|
|
215
|
+
col=col
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
fig.update_layout(
|
|
219
|
+
title=title,
|
|
220
|
+
height=300 * rows
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return fig
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def create_waterfall_chart(
|
|
227
|
+
categories: List[str],
|
|
228
|
+
values: List[float],
|
|
229
|
+
title: str = "Waterfall Chart"
|
|
230
|
+
) -> go.Figure:
|
|
231
|
+
"""Create a waterfall chart for step-by-step changes"""
|
|
232
|
+
|
|
233
|
+
fig = go.Figure(go.Waterfall(
|
|
234
|
+
name="",
|
|
235
|
+
orientation="v",
|
|
236
|
+
measure=["relative"] * (len(values) - 1) + ["total"],
|
|
237
|
+
x=categories,
|
|
238
|
+
y=values,
|
|
239
|
+
connector={"line": {"color": "rgb(63, 63, 63)"}},
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
fig.update_layout(
|
|
243
|
+
title=title,
|
|
244
|
+
showlegend=False,
|
|
245
|
+
height=400
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return fig
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def render_chart(fig: go.Figure, key: Optional[str] = None):
|
|
252
|
+
"""Helper to render Plotly chart with consistent configuration"""
|
|
253
|
+
st.plotly_chart(
|
|
254
|
+
fig,
|
|
255
|
+
width="stretch",
|
|
256
|
+
config={"responsive": True},
|
|
257
|
+
key=key
|
|
258
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Reusable metric display components"""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def display_metric_card(
|
|
8
|
+
label: str,
|
|
9
|
+
value: Union[int, float, str],
|
|
10
|
+
delta: Optional[Union[int, float, str]] = None,
|
|
11
|
+
delta_color: str = "normal",
|
|
12
|
+
help_text: Optional[str] = None,
|
|
13
|
+
icon: Optional[str] = None
|
|
14
|
+
):
|
|
15
|
+
"""Display a metric card with optional delta and icon"""
|
|
16
|
+
|
|
17
|
+
if icon:
|
|
18
|
+
label = f"{icon} {label}"
|
|
19
|
+
|
|
20
|
+
st.metric(
|
|
21
|
+
label=label,
|
|
22
|
+
value=value,
|
|
23
|
+
delta=delta,
|
|
24
|
+
delta_color=delta_color,
|
|
25
|
+
help=help_text
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def display_status_badge(
|
|
30
|
+
status: str,
|
|
31
|
+
size: str = "medium"
|
|
32
|
+
) -> str:
|
|
33
|
+
"""Return a colored status badge"""
|
|
34
|
+
|
|
35
|
+
status_colors = {
|
|
36
|
+
'completed': '🟢',
|
|
37
|
+
'success': '🟢',
|
|
38
|
+
'running': '🔵',
|
|
39
|
+
'in_progress': '🔵',
|
|
40
|
+
'pending': '🟡',
|
|
41
|
+
'waiting': '🟡',
|
|
42
|
+
'failed': '🔴',
|
|
43
|
+
'error': '🔴',
|
|
44
|
+
'cancelled': '⚪',
|
|
45
|
+
'unknown': '⚫'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
icon = status_colors.get(status.lower(), '⚫')
|
|
49
|
+
|
|
50
|
+
if size == "small":
|
|
51
|
+
return f"{icon} {status}"
|
|
52
|
+
elif size == "large":
|
|
53
|
+
return f"## {icon} {status}"
|
|
54
|
+
else: # medium
|
|
55
|
+
return f"### {icon} {status}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def display_kpi_row(metrics: dict, columns: Optional[int] = None):
|
|
59
|
+
"""Display a row of KPIs in columns"""
|
|
60
|
+
|
|
61
|
+
if columns is None:
|
|
62
|
+
columns = len(metrics)
|
|
63
|
+
|
|
64
|
+
cols = st.columns(columns)
|
|
65
|
+
|
|
66
|
+
for idx, (label, value) in enumerate(metrics.items()):
|
|
67
|
+
with cols[idx % columns]:
|
|
68
|
+
if isinstance(value, dict):
|
|
69
|
+
display_metric_card(
|
|
70
|
+
label=label,
|
|
71
|
+
value=value.get('value', '-'),
|
|
72
|
+
delta=value.get('delta'),
|
|
73
|
+
delta_color=value.get('delta_color', 'normal'),
|
|
74
|
+
help_text=value.get('help'),
|
|
75
|
+
icon=value.get('icon')
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
st.metric(label=label, value=value)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def display_progress_bar(
|
|
82
|
+
label: str,
|
|
83
|
+
progress: float,
|
|
84
|
+
show_percentage: bool = True
|
|
85
|
+
):
|
|
86
|
+
"""Display a progress bar with label"""
|
|
87
|
+
|
|
88
|
+
st.text(label)
|
|
89
|
+
st.progress(min(1.0, max(0.0, progress)))
|
|
90
|
+
|
|
91
|
+
if show_percentage:
|
|
92
|
+
st.caption(f"{progress * 100:.1f}%")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def display_health_indicator(
|
|
96
|
+
component: str,
|
|
97
|
+
is_healthy: bool,
|
|
98
|
+
details: Optional[str] = None
|
|
99
|
+
):
|
|
100
|
+
"""Display a health status indicator"""
|
|
101
|
+
|
|
102
|
+
if is_healthy:
|
|
103
|
+
st.success(f"✅ {component}: Healthy" + (f" ({details})" if details else ""))
|
|
104
|
+
else:
|
|
105
|
+
st.error(f"❌ {component}: Unhealthy" + (f" ({details})" if details else ""))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def display_alert(
|
|
109
|
+
message: str,
|
|
110
|
+
alert_type: str = "info",
|
|
111
|
+
icon: Optional[str] = None
|
|
112
|
+
):
|
|
113
|
+
"""Display an alert message"""
|
|
114
|
+
|
|
115
|
+
if icon:
|
|
116
|
+
message = f"{icon} {message}"
|
|
117
|
+
|
|
118
|
+
if alert_type == "success":
|
|
119
|
+
st.success(message)
|
|
120
|
+
elif alert_type == "warning":
|
|
121
|
+
st.warning(message)
|
|
122
|
+
elif alert_type == "error":
|
|
123
|
+
st.error(message)
|
|
124
|
+
else: # info
|
|
125
|
+
st.info(message)
|