Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Investor-facing Interactive Analytics Dashboard
|
| 3 |
Built with Gradio for deployment to Hugging Face Spaces
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
|
@@ -66,6 +67,25 @@ class AppState:
|
|
| 66 |
app_state = AppState()
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# =============================================================================
|
| 70 |
# DATA FETCHING FUNCTIONS
|
| 71 |
# =============================================================================
|
|
@@ -83,15 +103,30 @@ def fetch_data(query_func, *args, **kwargs) -> Optional[pd.DataFrame]:
|
|
| 83 |
"""
|
| 84 |
try:
|
| 85 |
if app_state.demo_mode:
|
| 86 |
-
# Use demo data
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
else:
|
| 93 |
-
logger.warning(f"Demo method not found: get{method_name}")
|
| 94 |
-
return None
|
| 95 |
else:
|
| 96 |
# Use database
|
| 97 |
query, params = query_func(*args, **kwargs)
|
|
@@ -107,12 +142,16 @@ def fetch_data(query_func, *args, **kwargs) -> Optional[pd.DataFrame]:
|
|
| 107 |
# =============================================================================
|
| 108 |
|
| 109 |
def render_overview_tab(
|
| 110 |
-
|
| 111 |
-
|
| 112 |
granularity: str
|
| 113 |
) -> Tuple:
|
| 114 |
"""Render Overview tab with KPIs and trends."""
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
# Calculate previous period for comparison
|
| 117 |
period_days = (end_date - start_date).days
|
| 118 |
prev_start = start_date - timedelta(days=period_days)
|
|
@@ -143,7 +182,7 @@ def render_overview_tab(
|
|
| 143 |
# Calculate KPIs
|
| 144 |
total_new_users = new_users_df['new_users'].sum() if new_users_df is not None and not new_users_df.empty else 0
|
| 145 |
prev_total_users = prev_users_df['new_users'].sum() if prev_users_df is not None and not prev_users_df.empty else 1
|
| 146 |
-
user_delta = ((total_new_users - prev_total_users) / prev_total_users * 100)
|
| 147 |
|
| 148 |
# Rolling active users (MAU)
|
| 149 |
if app_state.demo_mode:
|
|
@@ -158,22 +197,22 @@ def render_overview_tab(
|
|
| 158 |
prev_mau_df = db_connector.execute_query(prev_query, prev_params)
|
| 159 |
prev_mau = prev_mau_df.iloc[0]['active_users'] if prev_mau_df is not None and not prev_mau_df.empty else 1
|
| 160 |
|
| 161 |
-
mau_delta = ((mau - prev_mau) / prev_mau * 100)
|
| 162 |
|
| 163 |
# Trip metrics
|
| 164 |
if trip_metrics_df is not None and not trip_metrics_df.empty:
|
| 165 |
total_trips = trip_metrics_df.iloc[0]['total_trips']
|
| 166 |
-
avg_distance = trip_metrics_df.iloc[0]['avg_distance_miles']
|
| 167 |
-
total_co2 = trip_metrics_df.iloc[0]['total_co2_reduced']
|
| 168 |
else:
|
| 169 |
total_trips = avg_distance = total_co2 = 0
|
| 170 |
|
| 171 |
if prev_trip_metrics_df is not None and not prev_trip_metrics_df.empty:
|
| 172 |
-
prev_trips = prev_trip_metrics_df.iloc[0]['total_trips']
|
| 173 |
else:
|
| 174 |
prev_trips = 1
|
| 175 |
|
| 176 |
-
trip_delta = ((total_trips - prev_trips) / prev_trips * 100)
|
| 177 |
|
| 178 |
# Completion rate
|
| 179 |
if app_state.demo_mode:
|
|
@@ -250,12 +289,16 @@ def render_overview_tab(
|
|
| 250 |
# =============================================================================
|
| 251 |
|
| 252 |
def render_users_tab(
|
| 253 |
-
|
| 254 |
-
|
| 255 |
granularity: str
|
| 256 |
) -> Tuple:
|
| 257 |
"""Render Users tab with growth and retention metrics."""
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
# New users chart
|
| 260 |
new_users_df = fetch_data(
|
| 261 |
query_builder.get_new_users_query,
|
|
@@ -300,14 +343,17 @@ def render_users_tab(
|
|
| 300 |
# Generate mock cohort data
|
| 301 |
cohort_data = []
|
| 302 |
for i in range(12):
|
| 303 |
-
cohort_date = start_date + timedelta(days=i*
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
| 309 |
retention_df = pd.DataFrame(cohort_data)
|
| 310 |
-
|
|
|
|
| 311 |
else:
|
| 312 |
query, params = query_builder.get_cohort_retention_query(start_date, end_date, 7)
|
| 313 |
retention_df = db_connector.execute_query(query, params)
|
|
@@ -321,13 +367,14 @@ def render_users_tab(
|
|
| 321 |
|
| 322 |
# Export CSV
|
| 323 |
export_data = new_users_df if new_users_df is not None else pd.DataFrame()
|
|
|
|
| 324 |
|
| 325 |
return (
|
| 326 |
new_users_chart,
|
| 327 |
verified_chart,
|
| 328 |
activated_chart,
|
| 329 |
retention_table,
|
| 330 |
-
|
| 331 |
)
|
| 332 |
|
| 333 |
|
|
@@ -336,13 +383,17 @@ def render_users_tab(
|
|
| 336 |
# =============================================================================
|
| 337 |
|
| 338 |
def render_trips_tab(
|
| 339 |
-
|
| 340 |
-
|
| 341 |
granularity: str,
|
| 342 |
driver_type: str
|
| 343 |
) -> Tuple:
|
| 344 |
"""Render Trips tab with volume and impact metrics."""
|
| 345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
driver_type_filter = None if driver_type == "All" else driver_type
|
| 347 |
|
| 348 |
# Trip volume
|
|
@@ -389,25 +440,30 @@ def render_trips_tab(
|
|
| 389 |
)
|
| 390 |
|
| 391 |
if metrics_df is not None and not metrics_df.empty:
|
|
|
|
| 392 |
impact_data = pd.DataFrame([
|
| 393 |
-
{'Metric': 'Total COβ Reduced (
|
| 394 |
-
{'Metric': 'Avg COβ per Trip (
|
| 395 |
-
{'Metric': 'Total NOx Reduced (
|
| 396 |
-
{'Metric': 'Total PM2.5 Reduced (
|
| 397 |
-
{'Metric': 'Total Distance (miles)', 'Value': f"{
|
| 398 |
-
{'Metric': 'Shared Miles', 'Value': f"{
|
|
|
|
|
|
|
| 399 |
])
|
| 400 |
else:
|
| 401 |
impact_data = pd.DataFrame()
|
| 402 |
|
| 403 |
impact_table = create_data_table(impact_data, "Environmental Impact Summary")
|
| 404 |
|
|
|
|
|
|
|
| 405 |
return (
|
| 406 |
trips_chart,
|
| 407 |
driver_pie,
|
| 408 |
solo_shared_pie,
|
| 409 |
impact_table,
|
| 410 |
-
|
| 411 |
)
|
| 412 |
|
| 413 |
|
|
@@ -427,16 +483,24 @@ def render_geography_tab() -> Tuple:
|
|
| 427 |
query, params = query_builder.get_user_locations_query()
|
| 428 |
locations_df = db_connector.execute_query(query, params)
|
| 429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
# Create heat map
|
| 431 |
heat_map = create_geo_heatmap(
|
| 432 |
locations_df if locations_df is not None else pd.DataFrame(),
|
| 433 |
'latitude', 'longitude',
|
| 434 |
-
size_col='user_count' if 'user_count' in
|
| 435 |
-
hover_data=
|
| 436 |
title='User Geographic Distribution'
|
| 437 |
)
|
| 438 |
|
| 439 |
-
# Top markets table
|
| 440 |
if locations_df is not None and not locations_df.empty and 'state' in locations_df.columns:
|
| 441 |
top_markets = locations_df.groupby('state')['user_count'].sum().reset_index()
|
| 442 |
top_markets = top_markets.sort_values('user_count', ascending=False).head(10)
|
|
@@ -446,10 +510,12 @@ def render_geography_tab() -> Tuple:
|
|
| 446 |
|
| 447 |
markets_table = create_data_table(top_markets, "Top 10 Markets by Users")
|
| 448 |
|
|
|
|
|
|
|
| 449 |
return (
|
| 450 |
heat_map,
|
| 451 |
markets_table,
|
| 452 |
-
|
| 453 |
)
|
| 454 |
|
| 455 |
|
|
@@ -458,12 +524,16 @@ def render_geography_tab() -> Tuple:
|
|
| 458 |
# =============================================================================
|
| 459 |
|
| 460 |
def render_rewards_tab(
|
| 461 |
-
|
| 462 |
-
|
| 463 |
granularity: str
|
| 464 |
) -> Tuple:
|
| 465 |
"""Render Rewards tab with transaction metrics."""
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
# Points earned over time (from trip history)
|
| 468 |
metrics_df = fetch_data(
|
| 469 |
query_builder.get_trip_metrics_query,
|
|
@@ -471,22 +541,25 @@ def render_rewards_tab(
|
|
| 471 |
)
|
| 472 |
|
| 473 |
if metrics_df is not None and not metrics_df.empty:
|
| 474 |
-
total_points = metrics_df.iloc[0]
|
|
|
|
| 475 |
points_summary = pd.DataFrame([
|
| 476 |
-
{'Metric': 'Total Points Earned', 'Value': f"{total_points:,.0f}"}
|
|
|
|
| 477 |
])
|
| 478 |
else:
|
| 479 |
points_summary = pd.DataFrame()
|
| 480 |
|
| 481 |
-
points_table = create_data_table(points_summary, "
|
| 482 |
|
| 483 |
-
#
|
| 484 |
if app_state.demo_mode:
|
| 485 |
-
|
|
|
|
| 486 |
trans_df = pd.DataFrame({
|
| 487 |
'period': periods,
|
| 488 |
-
'transaction_count': np.random.randint(50, 200,
|
| 489 |
-
'total_amount': np.random.uniform(500, 2000,
|
| 490 |
})
|
| 491 |
else:
|
| 492 |
query, params = query_builder.get_transactions_over_time_query(start_date, end_date, granularity)
|
|
@@ -499,38 +572,40 @@ def render_rewards_tab(
|
|
| 499 |
'Date', 'Transactions'
|
| 500 |
)
|
| 501 |
|
|
|
|
|
|
|
| 502 |
return (
|
| 503 |
points_table,
|
| 504 |
trans_chart,
|
| 505 |
-
|
| 506 |
)
|
| 507 |
|
| 508 |
|
| 509 |
# =============================================================================
|
| 510 |
-
# GRADIO UI
|
| 511 |
# =============================================================================
|
| 512 |
|
| 513 |
def build_gradio_app():
|
| 514 |
"""Build and configure Gradio interface."""
|
| 515 |
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
) as demo:
|
| 529 |
|
| 530 |
# Header
|
| 531 |
gr.Markdown(
|
| 532 |
"""
|
| 533 |
-
# π Trip Analytics Dashboard
|
| 534 |
### Real-time insights for investor conversations
|
| 535 |
"""
|
| 536 |
)
|
|
@@ -539,12 +614,20 @@ def build_gradio_app():
|
|
| 539 |
mode_text = "π‘ DEMO MODE - Using synthetic data" if app_state.demo_mode else "π’ LIVE MODE - Connected to database"
|
| 540 |
status_display = gr.Markdown(mode_text)
|
| 541 |
|
| 542 |
-
# Global filters
|
| 543 |
with gr.Row():
|
| 544 |
with gr.Column(scale=2):
|
| 545 |
start_default, end_default = create_date_range_inputs()
|
| 546 |
-
start_date_input = gr.
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
|
| 549 |
with gr.Column(scale=1):
|
| 550 |
filter_opts = create_filter_options()
|
|
@@ -574,20 +657,15 @@ def build_gradio_app():
|
|
| 574 |
overview_user_chart = gr.Plot()
|
| 575 |
overview_trip_chart = gr.Plot()
|
| 576 |
|
| 577 |
-
def update_overview(start, end, gran):
|
| 578 |
-
start_dt = datetime.combine(start, datetime.min.time())
|
| 579 |
-
end_dt = datetime.combine(end, datetime.max.time())
|
| 580 |
-
return render_overview_tab(start_dt, end_dt, gran)
|
| 581 |
-
|
| 582 |
refresh_btn.click(
|
| 583 |
-
|
| 584 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 585 |
outputs=[overview_kpis, overview_user_chart, overview_trip_chart]
|
| 586 |
)
|
| 587 |
|
| 588 |
# Initial load
|
| 589 |
demo.load(
|
| 590 |
-
|
| 591 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 592 |
outputs=[overview_kpis, overview_user_chart, overview_trip_chart]
|
| 593 |
)
|
|
@@ -600,22 +678,18 @@ def build_gradio_app():
|
|
| 600 |
|
| 601 |
users_activated_chart = gr.Plot()
|
| 602 |
users_retention_table = gr.HTML()
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
def update_users(start, end, gran):
|
| 606 |
-
start_dt = datetime.combine(start, datetime.min.time())
|
| 607 |
-
end_dt = datetime.combine(end, datetime.max.time())
|
| 608 |
-
return render_users_tab(start_dt, end_dt, gran)
|
| 609 |
|
| 610 |
refresh_btn.click(
|
| 611 |
-
|
| 612 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 613 |
outputs=[
|
| 614 |
users_new_chart,
|
| 615 |
users_verified_chart,
|
| 616 |
users_activated_chart,
|
| 617 |
users_retention_table,
|
| 618 |
-
|
| 619 |
]
|
| 620 |
)
|
| 621 |
|
|
@@ -627,22 +701,18 @@ def build_gradio_app():
|
|
| 627 |
trips_solo_shared_pie = gr.Plot()
|
| 628 |
|
| 629 |
trips_impact_table = gr.HTML()
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
def update_trips(start, end, gran, dtype):
|
| 633 |
-
start_dt = datetime.combine(start, datetime.min.time())
|
| 634 |
-
end_dt = datetime.combine(end, datetime.max.time())
|
| 635 |
-
return render_trips_tab(start_dt, end_dt, gran, dtype)
|
| 636 |
|
| 637 |
refresh_btn.click(
|
| 638 |
-
|
| 639 |
inputs=[start_date_input, end_date_input, granularity_input, driver_type_input],
|
| 640 |
outputs=[
|
| 641 |
trips_volume_chart,
|
| 642 |
trips_driver_pie,
|
| 643 |
trips_solo_shared_pie,
|
| 644 |
trips_impact_table,
|
| 645 |
-
|
| 646 |
]
|
| 647 |
)
|
| 648 |
|
|
@@ -650,31 +720,31 @@ def build_gradio_app():
|
|
| 650 |
with gr.Tab("πΊοΈ Geography"):
|
| 651 |
geo_heat_map = gr.Plot()
|
| 652 |
geo_markets_table = gr.HTML()
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
def update_geography():
|
| 656 |
-
return render_geography_tab()
|
| 657 |
|
| 658 |
refresh_btn.click(
|
| 659 |
-
|
| 660 |
-
outputs=[geo_heat_map, geo_markets_table,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
)
|
| 662 |
|
| 663 |
# TAB 5: Rewards
|
| 664 |
with gr.Tab("π Rewards"):
|
| 665 |
rewards_points_table = gr.HTML()
|
| 666 |
rewards_trans_chart = gr.Plot()
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
def update_rewards(start, end, gran):
|
| 670 |
-
start_dt = datetime.combine(start, datetime.min.time())
|
| 671 |
-
end_dt = datetime.combine(end, datetime.max.time())
|
| 672 |
-
return render_rewards_tab(start_dt, end_dt, gran)
|
| 673 |
|
| 674 |
refresh_btn.click(
|
| 675 |
-
|
| 676 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 677 |
-
outputs=[rewards_points_table, rewards_trans_chart,
|
| 678 |
)
|
| 679 |
|
| 680 |
# Toggle mode functionality
|
|
@@ -695,10 +765,12 @@ def build_gradio_app():
|
|
| 695 |
---
|
| 696 |
**Data Security Notice**: All database credentials are stored as encrypted environment variables.
|
| 697 |
No sensitive information is logged or displayed.
|
|
|
|
|
|
|
| 698 |
"""
|
| 699 |
)
|
| 700 |
|
| 701 |
-
return demo
|
| 702 |
|
| 703 |
|
| 704 |
# =============================================================================
|
|
@@ -706,10 +778,12 @@ def build_gradio_app():
|
|
| 706 |
# =============================================================================
|
| 707 |
|
| 708 |
if __name__ == "__main__":
|
| 709 |
-
app = build_gradio_app()
|
| 710 |
app.launch(
|
| 711 |
server_name="0.0.0.0",
|
| 712 |
server_port=7860,
|
| 713 |
share=False,
|
| 714 |
-
show_error=True
|
| 715 |
-
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
Investor-facing Interactive Analytics Dashboard
|
| 3 |
Built with Gradio for deployment to Hugging Face Spaces
|
| 4 |
+
Updated for Gradio 6.0 compatibility
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
|
|
|
| 67 |
app_state = AppState()
|
| 68 |
|
| 69 |
|
| 70 |
+
# =============================================================================
|
| 71 |
+
# DATE PARSING HELPERS
|
| 72 |
+
# =============================================================================
|
| 73 |
+
|
| 74 |
+
def parse_date_string(date_str: str, is_end: bool = False) -> datetime:
|
| 75 |
+
"""Parse date string to datetime object."""
|
| 76 |
+
try:
|
| 77 |
+
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
| 78 |
+
if is_end:
|
| 79 |
+
dt = dt.replace(hour=23, minute=59, second=59)
|
| 80 |
+
return dt
|
| 81 |
+
except (ValueError, TypeError):
|
| 82 |
+
# Default to last 90 days if parsing fails
|
| 83 |
+
if is_end:
|
| 84 |
+
return datetime.now().replace(hour=23, minute=59, second=59)
|
| 85 |
+
else:
|
| 86 |
+
return datetime.now() - timedelta(days=90)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
# =============================================================================
|
| 90 |
# DATA FETCHING FUNCTIONS
|
| 91 |
# =============================================================================
|
|
|
|
| 103 |
"""
|
| 104 |
try:
|
| 105 |
if app_state.demo_mode:
|
| 106 |
+
# Use demo data - map query function to demo generator method
|
| 107 |
+
func_name = query_func.__name__
|
| 108 |
+
|
| 109 |
+
# Map query builder methods to demo generator methods
|
| 110 |
+
method_mapping = {
|
| 111 |
+
'get_new_users_query': 'get_new_users',
|
| 112 |
+
'get_verified_users_query': 'get_verified_users',
|
| 113 |
+
'get_activated_by_first_trip_query': 'get_activated_by_first_trip',
|
| 114 |
+
'get_trips_over_time_query': 'get_trips_over_time',
|
| 115 |
+
'get_trip_metrics_query': 'get_trip_metrics',
|
| 116 |
+
'get_driver_type_distribution_query': 'get_driver_type_distribution',
|
| 117 |
+
'get_solo_shared_split_query': 'get_solo_shared_split',
|
| 118 |
+
'get_user_locations_query': 'get_user_locations',
|
| 119 |
+
'get_transactions_over_time_query': 'get_transactions_over_time'
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
demo_method_name = method_mapping.get(func_name)
|
| 123 |
+
if demo_method_name:
|
| 124 |
+
demo_method = getattr(demo_generator, demo_method_name, None)
|
| 125 |
+
if demo_method:
|
| 126 |
+
return demo_method(*args, **kwargs)
|
| 127 |
|
| 128 |
+
logger.warning(f"Demo method not found for: {func_name}")
|
| 129 |
+
return None
|
|
|
|
|
|
|
|
|
|
| 130 |
else:
|
| 131 |
# Use database
|
| 132 |
query, params = query_func(*args, **kwargs)
|
|
|
|
| 142 |
# =============================================================================
|
| 143 |
|
| 144 |
def render_overview_tab(
|
| 145 |
+
start_date_str: str,
|
| 146 |
+
end_date_str: str,
|
| 147 |
granularity: str
|
| 148 |
) -> Tuple:
|
| 149 |
"""Render Overview tab with KPIs and trends."""
|
| 150 |
|
| 151 |
+
# Parse dates
|
| 152 |
+
start_date = parse_date_string(start_date_str)
|
| 153 |
+
end_date = parse_date_string(end_date_str, is_end=True)
|
| 154 |
+
|
| 155 |
# Calculate previous period for comparison
|
| 156 |
period_days = (end_date - start_date).days
|
| 157 |
prev_start = start_date - timedelta(days=period_days)
|
|
|
|
| 182 |
# Calculate KPIs
|
| 183 |
total_new_users = new_users_df['new_users'].sum() if new_users_df is not None and not new_users_df.empty else 0
|
| 184 |
prev_total_users = prev_users_df['new_users'].sum() if prev_users_df is not None and not prev_users_df.empty else 1
|
| 185 |
+
user_delta = ((total_new_users - prev_total_users) / max(prev_total_users, 1) * 100)
|
| 186 |
|
| 187 |
# Rolling active users (MAU)
|
| 188 |
if app_state.demo_mode:
|
|
|
|
| 197 |
prev_mau_df = db_connector.execute_query(prev_query, prev_params)
|
| 198 |
prev_mau = prev_mau_df.iloc[0]['active_users'] if prev_mau_df is not None and not prev_mau_df.empty else 1
|
| 199 |
|
| 200 |
+
mau_delta = ((mau - prev_mau) / max(prev_mau, 1) * 100)
|
| 201 |
|
| 202 |
# Trip metrics
|
| 203 |
if trip_metrics_df is not None and not trip_metrics_df.empty:
|
| 204 |
total_trips = trip_metrics_df.iloc[0]['total_trips']
|
| 205 |
+
avg_distance = trip_metrics_df.iloc[0]['avg_distance_miles'] or 0
|
| 206 |
+
total_co2 = trip_metrics_df.iloc[0]['total_co2_reduced'] or 0
|
| 207 |
else:
|
| 208 |
total_trips = avg_distance = total_co2 = 0
|
| 209 |
|
| 210 |
if prev_trip_metrics_df is not None and not prev_trip_metrics_df.empty:
|
| 211 |
+
prev_trips = prev_trip_metrics_df.iloc[0]['total_trips'] or 1
|
| 212 |
else:
|
| 213 |
prev_trips = 1
|
| 214 |
|
| 215 |
+
trip_delta = ((total_trips - prev_trips) / max(prev_trips, 1) * 100)
|
| 216 |
|
| 217 |
# Completion rate
|
| 218 |
if app_state.demo_mode:
|
|
|
|
| 289 |
# =============================================================================
|
| 290 |
|
| 291 |
def render_users_tab(
|
| 292 |
+
start_date_str: str,
|
| 293 |
+
end_date_str: str,
|
| 294 |
granularity: str
|
| 295 |
) -> Tuple:
|
| 296 |
"""Render Users tab with growth and retention metrics."""
|
| 297 |
|
| 298 |
+
# Parse dates
|
| 299 |
+
start_date = parse_date_string(start_date_str)
|
| 300 |
+
end_date = parse_date_string(end_date_str, is_end=True)
|
| 301 |
+
|
| 302 |
# New users chart
|
| 303 |
new_users_df = fetch_data(
|
| 304 |
query_builder.get_new_users_query,
|
|
|
|
| 343 |
# Generate mock cohort data
|
| 344 |
cohort_data = []
|
| 345 |
for i in range(12):
|
| 346 |
+
cohort_date = start_date + timedelta(days=i*7)
|
| 347 |
+
if cohort_date <= end_date:
|
| 348 |
+
cohort_size = np.random.randint(100, 500)
|
| 349 |
+
cohort_data.append({
|
| 350 |
+
'cohort_date': cohort_date.strftime('%Y-%m-%d'),
|
| 351 |
+
'cohort_size': cohort_size,
|
| 352 |
+
'retained_users': int(cohort_size * np.random.uniform(0.2, 0.6))
|
| 353 |
+
})
|
| 354 |
retention_df = pd.DataFrame(cohort_data)
|
| 355 |
+
if not retention_df.empty:
|
| 356 |
+
retention_df['retention_rate'] = (retention_df['retained_users'] / retention_df['cohort_size'] * 100).round(1)
|
| 357 |
else:
|
| 358 |
query, params = query_builder.get_cohort_retention_query(start_date, end_date, 7)
|
| 359 |
retention_df = db_connector.execute_query(query, params)
|
|
|
|
| 367 |
|
| 368 |
# Export CSV
|
| 369 |
export_data = new_users_df if new_users_df is not None else pd.DataFrame()
|
| 370 |
+
csv_data = df_to_csv(export_data, "users_export.csv")
|
| 371 |
|
| 372 |
return (
|
| 373 |
new_users_chart,
|
| 374 |
verified_chart,
|
| 375 |
activated_chart,
|
| 376 |
retention_table,
|
| 377 |
+
csv_data
|
| 378 |
)
|
| 379 |
|
| 380 |
|
|
|
|
| 383 |
# =============================================================================
|
| 384 |
|
| 385 |
def render_trips_tab(
|
| 386 |
+
start_date_str: str,
|
| 387 |
+
end_date_str: str,
|
| 388 |
granularity: str,
|
| 389 |
driver_type: str
|
| 390 |
) -> Tuple:
|
| 391 |
"""Render Trips tab with volume and impact metrics."""
|
| 392 |
|
| 393 |
+
# Parse dates
|
| 394 |
+
start_date = parse_date_string(start_date_str)
|
| 395 |
+
end_date = parse_date_string(end_date_str, is_end=True)
|
| 396 |
+
|
| 397 |
driver_type_filter = None if driver_type == "All" else driver_type
|
| 398 |
|
| 399 |
# Trip volume
|
|
|
|
| 440 |
)
|
| 441 |
|
| 442 |
if metrics_df is not None and not metrics_df.empty:
|
| 443 |
+
row = metrics_df.iloc[0]
|
| 444 |
impact_data = pd.DataFrame([
|
| 445 |
+
{'Metric': 'Total COβ Reduced (g)', 'Value': f"{row.get('total_co2_reduced', 0) or 0:,.1f}"},
|
| 446 |
+
{'Metric': 'Avg COβ per Trip (g)', 'Value': f"{row.get('avg_co2_per_trip', 0) or 0:,.2f}"},
|
| 447 |
+
{'Metric': 'Total NOx Reduced (g)', 'Value': f"{row.get('total_nox_reduced', 0) or 0:,.1f}"},
|
| 448 |
+
{'Metric': 'Total PM2.5 Reduced (g)', 'Value': f"{row.get('total_pm25_reduced', 0) or 0:,.2f}"},
|
| 449 |
+
{'Metric': 'Total Distance (miles)', 'Value': f"{row.get('total_distance_miles', 0) or 0:,.0f}"},
|
| 450 |
+
{'Metric': 'Shared Miles', 'Value': f"{row.get('total_shared_miles', 0) or 0:,.0f}"},
|
| 451 |
+
{'Metric': 'Trees Saved', 'Value': f"{row.get('total_trees_saved', 0) or 0:,.2f}"},
|
| 452 |
+
{'Metric': 'Total Points Earned', 'Value': f"{row.get('total_points', 0) or 0:,.0f}"}
|
| 453 |
])
|
| 454 |
else:
|
| 455 |
impact_data = pd.DataFrame()
|
| 456 |
|
| 457 |
impact_table = create_data_table(impact_data, "Environmental Impact Summary")
|
| 458 |
|
| 459 |
+
csv_data = df_to_csv(trips_df if trips_df is not None else pd.DataFrame(), "trips_export.csv")
|
| 460 |
+
|
| 461 |
return (
|
| 462 |
trips_chart,
|
| 463 |
driver_pie,
|
| 464 |
solo_shared_pie,
|
| 465 |
impact_table,
|
| 466 |
+
csv_data
|
| 467 |
)
|
| 468 |
|
| 469 |
|
|
|
|
| 483 |
query, params = query_builder.get_user_locations_query()
|
| 484 |
locations_df = db_connector.execute_query(query, params)
|
| 485 |
|
| 486 |
+
# Determine hover data columns
|
| 487 |
+
hover_cols = None
|
| 488 |
+
if locations_df is not None and not locations_df.empty:
|
| 489 |
+
available_cols = locations_df.columns.tolist()
|
| 490 |
+
hover_cols = [c for c in ['city', 'state'] if c in available_cols]
|
| 491 |
+
if not hover_cols:
|
| 492 |
+
hover_cols = None
|
| 493 |
+
|
| 494 |
# Create heat map
|
| 495 |
heat_map = create_geo_heatmap(
|
| 496 |
locations_df if locations_df is not None else pd.DataFrame(),
|
| 497 |
'latitude', 'longitude',
|
| 498 |
+
size_col='user_count' if locations_df is not None and 'user_count' in locations_df.columns else None,
|
| 499 |
+
hover_data=hover_cols,
|
| 500 |
title='User Geographic Distribution'
|
| 501 |
)
|
| 502 |
|
| 503 |
+
# Top markets table
|
| 504 |
if locations_df is not None and not locations_df.empty and 'state' in locations_df.columns:
|
| 505 |
top_markets = locations_df.groupby('state')['user_count'].sum().reset_index()
|
| 506 |
top_markets = top_markets.sort_values('user_count', ascending=False).head(10)
|
|
|
|
| 510 |
|
| 511 |
markets_table = create_data_table(top_markets, "Top 10 Markets by Users")
|
| 512 |
|
| 513 |
+
csv_data = df_to_csv(locations_df if locations_df is not None else pd.DataFrame(), "geography_export.csv")
|
| 514 |
+
|
| 515 |
return (
|
| 516 |
heat_map,
|
| 517 |
markets_table,
|
| 518 |
+
csv_data
|
| 519 |
)
|
| 520 |
|
| 521 |
|
|
|
|
| 524 |
# =============================================================================
|
| 525 |
|
| 526 |
def render_rewards_tab(
|
| 527 |
+
start_date_str: str,
|
| 528 |
+
end_date_str: str,
|
| 529 |
granularity: str
|
| 530 |
) -> Tuple:
|
| 531 |
"""Render Rewards tab with transaction metrics."""
|
| 532 |
|
| 533 |
+
# Parse dates
|
| 534 |
+
start_date = parse_date_string(start_date_str)
|
| 535 |
+
end_date = parse_date_string(end_date_str, is_end=True)
|
| 536 |
+
|
| 537 |
# Points earned over time (from trip history)
|
| 538 |
metrics_df = fetch_data(
|
| 539 |
query_builder.get_trip_metrics_query,
|
|
|
|
| 541 |
)
|
| 542 |
|
| 543 |
if metrics_df is not None and not metrics_df.empty:
|
| 544 |
+
total_points = metrics_df.iloc[0].get('total_points', 0) or 0
|
| 545 |
+
total_gas_savings = metrics_df.iloc[0].get('total_gas_savings', 0) or 0
|
| 546 |
points_summary = pd.DataFrame([
|
| 547 |
+
{'Metric': 'Total Points Earned', 'Value': f"{total_points:,.0f}"},
|
| 548 |
+
{'Metric': 'Total Gas Savings', 'Value': f"${total_gas_savings:,.2f}"}
|
| 549 |
])
|
| 550 |
else:
|
| 551 |
points_summary = pd.DataFrame()
|
| 552 |
|
| 553 |
+
points_table = create_data_table(points_summary, "Rewards Summary")
|
| 554 |
|
| 555 |
+
# Transaction chart
|
| 556 |
if app_state.demo_mode:
|
| 557 |
+
num_periods = min(20, (end_date - start_date).days)
|
| 558 |
+
periods = pd.date_range(start=start_date, end=end_date, periods=max(num_periods, 2))
|
| 559 |
trans_df = pd.DataFrame({
|
| 560 |
'period': periods,
|
| 561 |
+
'transaction_count': np.random.randint(50, 200, len(periods)),
|
| 562 |
+
'total_amount': np.random.uniform(500, 2000, len(periods))
|
| 563 |
})
|
| 564 |
else:
|
| 565 |
query, params = query_builder.get_transactions_over_time_query(start_date, end_date, granularity)
|
|
|
|
| 572 |
'Date', 'Transactions'
|
| 573 |
)
|
| 574 |
|
| 575 |
+
csv_data = df_to_csv(trans_df if trans_df is not None else pd.DataFrame(), "rewards_export.csv")
|
| 576 |
+
|
| 577 |
return (
|
| 578 |
points_table,
|
| 579 |
trans_chart,
|
| 580 |
+
csv_data
|
| 581 |
)
|
| 582 |
|
| 583 |
|
| 584 |
# =============================================================================
|
| 585 |
+
# GRADIO UI (Updated for Gradio 6.0)
|
| 586 |
# =============================================================================
|
| 587 |
|
| 588 |
def build_gradio_app():
|
| 589 |
"""Build and configure Gradio interface."""
|
| 590 |
|
| 591 |
+
# Define theme and CSS for launch() method
|
| 592 |
+
app_theme = gr.themes.Soft()
|
| 593 |
+
app_css = """
|
| 594 |
+
.gradio-container {
|
| 595 |
+
font-family: 'Arial', sans-serif;
|
| 596 |
+
}
|
| 597 |
+
.tab-nav button {
|
| 598 |
+
font-size: 16px;
|
| 599 |
+
font-weight: 500;
|
| 600 |
+
}
|
| 601 |
+
"""
|
| 602 |
+
|
| 603 |
+
with gr.Blocks() as demo:
|
| 604 |
|
| 605 |
# Header
|
| 606 |
gr.Markdown(
|
| 607 |
"""
|
| 608 |
+
# π Hytch Trip Analytics Dashboard
|
| 609 |
### Real-time insights for investor conversations
|
| 610 |
"""
|
| 611 |
)
|
|
|
|
| 614 |
mode_text = "π‘ DEMO MODE - Using synthetic data" if app_state.demo_mode else "π’ LIVE MODE - Connected to database"
|
| 615 |
status_display = gr.Markdown(mode_text)
|
| 616 |
|
| 617 |
+
# Global filters - Using Textbox instead of Date for Gradio 6.0 compatibility
|
| 618 |
with gr.Row():
|
| 619 |
with gr.Column(scale=2):
|
| 620 |
start_default, end_default = create_date_range_inputs()
|
| 621 |
+
start_date_input = gr.Textbox(
|
| 622 |
+
label="Start Date (YYYY-MM-DD)",
|
| 623 |
+
value=start_default.strftime("%Y-%m-%d"),
|
| 624 |
+
placeholder="2024-01-01"
|
| 625 |
+
)
|
| 626 |
+
end_date_input = gr.Textbox(
|
| 627 |
+
label="End Date (YYYY-MM-DD)",
|
| 628 |
+
value=end_default.strftime("%Y-%m-%d"),
|
| 629 |
+
placeholder="2024-12-31"
|
| 630 |
+
)
|
| 631 |
|
| 632 |
with gr.Column(scale=1):
|
| 633 |
filter_opts = create_filter_options()
|
|
|
|
| 657 |
overview_user_chart = gr.Plot()
|
| 658 |
overview_trip_chart = gr.Plot()
|
| 659 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
refresh_btn.click(
|
| 661 |
+
render_overview_tab,
|
| 662 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 663 |
outputs=[overview_kpis, overview_user_chart, overview_trip_chart]
|
| 664 |
)
|
| 665 |
|
| 666 |
# Initial load
|
| 667 |
demo.load(
|
| 668 |
+
render_overview_tab,
|
| 669 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 670 |
outputs=[overview_kpis, overview_user_chart, overview_trip_chart]
|
| 671 |
)
|
|
|
|
| 678 |
|
| 679 |
users_activated_chart = gr.Plot()
|
| 680 |
users_retention_table = gr.HTML()
|
| 681 |
+
users_export_data = gr.Textbox(label="Export Data (CSV)", lines=5, visible=False)
|
| 682 |
+
users_export_btn = gr.Button("π₯ Export Users Data")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
refresh_btn.click(
|
| 685 |
+
render_users_tab,
|
| 686 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 687 |
outputs=[
|
| 688 |
users_new_chart,
|
| 689 |
users_verified_chart,
|
| 690 |
users_activated_chart,
|
| 691 |
users_retention_table,
|
| 692 |
+
users_export_data
|
| 693 |
]
|
| 694 |
)
|
| 695 |
|
|
|
|
| 701 |
trips_solo_shared_pie = gr.Plot()
|
| 702 |
|
| 703 |
trips_impact_table = gr.HTML()
|
| 704 |
+
trips_export_data = gr.Textbox(label="Export Data (CSV)", lines=5, visible=False)
|
| 705 |
+
trips_export_btn = gr.Button("π₯ Export Trips Data")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
| 707 |
refresh_btn.click(
|
| 708 |
+
render_trips_tab,
|
| 709 |
inputs=[start_date_input, end_date_input, granularity_input, driver_type_input],
|
| 710 |
outputs=[
|
| 711 |
trips_volume_chart,
|
| 712 |
trips_driver_pie,
|
| 713 |
trips_solo_shared_pie,
|
| 714 |
trips_impact_table,
|
| 715 |
+
trips_export_data
|
| 716 |
]
|
| 717 |
)
|
| 718 |
|
|
|
|
| 720 |
with gr.Tab("πΊοΈ Geography"):
|
| 721 |
geo_heat_map = gr.Plot()
|
| 722 |
geo_markets_table = gr.HTML()
|
| 723 |
+
geo_export_data = gr.Textbox(label="Export Data (CSV)", lines=5, visible=False)
|
| 724 |
+
geo_export_btn = gr.Button("π₯ Export Geography Data")
|
|
|
|
|
|
|
| 725 |
|
| 726 |
refresh_btn.click(
|
| 727 |
+
render_geography_tab,
|
| 728 |
+
outputs=[geo_heat_map, geo_markets_table, geo_export_data]
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
# Load geography on tab view
|
| 732 |
+
demo.load(
|
| 733 |
+
render_geography_tab,
|
| 734 |
+
outputs=[geo_heat_map, geo_markets_table, geo_export_data]
|
| 735 |
)
|
| 736 |
|
| 737 |
# TAB 5: Rewards
|
| 738 |
with gr.Tab("π Rewards"):
|
| 739 |
rewards_points_table = gr.HTML()
|
| 740 |
rewards_trans_chart = gr.Plot()
|
| 741 |
+
rewards_export_data = gr.Textbox(label="Export Data (CSV)", lines=5, visible=False)
|
| 742 |
+
rewards_export_btn = gr.Button("π₯ Export Rewards Data")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
|
| 744 |
refresh_btn.click(
|
| 745 |
+
render_rewards_tab,
|
| 746 |
inputs=[start_date_input, end_date_input, granularity_input],
|
| 747 |
+
outputs=[rewards_points_table, rewards_trans_chart, rewards_export_data]
|
| 748 |
)
|
| 749 |
|
| 750 |
# Toggle mode functionality
|
|
|
|
| 765 |
---
|
| 766 |
**Data Security Notice**: All database credentials are stored as encrypted environment variables.
|
| 767 |
No sensitive information is logged or displayed.
|
| 768 |
+
|
| 769 |
+
**Hytch** - Rideshare and carpooling with environmental impact tracking π±
|
| 770 |
"""
|
| 771 |
)
|
| 772 |
|
| 773 |
+
return demo, app_theme, app_css
|
| 774 |
|
| 775 |
|
| 776 |
# =============================================================================
|
|
|
|
| 778 |
# =============================================================================
|
| 779 |
|
| 780 |
if __name__ == "__main__":
|
| 781 |
+
app, theme, css = build_gradio_app()
|
| 782 |
app.launch(
|
| 783 |
server_name="0.0.0.0",
|
| 784 |
server_port=7860,
|
| 785 |
share=False,
|
| 786 |
+
show_error=True,
|
| 787 |
+
theme=theme,
|
| 788 |
+
css=css
|
| 789 |
+
)
|