LeonceNsh commited on
Commit
9a20d39
Β·
verified Β·
1 Parent(s): e16d53e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -108
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
- method_name = query_func.__name__.replace('_query', '')
88
- demo_method = getattr(demo_generator, f'get{method_name}', None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- if demo_method:
91
- return demo_method(*args, **kwargs)
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
- start_date: datetime,
111
- end_date: datetime,
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) if prev_total_users > 0 else 0
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) if prev_mau > 0 else 0
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) if prev_trips > 0 else 0
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
- start_date: datetime,
254
- end_date: datetime,
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*30)
304
- cohort_data.append({
305
- 'cohort_date': cohort_date,
306
- 'cohort_size': np.random.randint(100, 500),
307
- 'retained_users': np.random.randint(30, 200)
308
- })
 
 
309
  retention_df = pd.DataFrame(cohort_data)
310
- retention_df['retention_rate'] = (retention_df['retained_users'] / retention_df['cohort_size'] * 100).round(1)
 
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
- df_to_csv(export_data, "users_export.csv")
331
  )
332
 
333
 
@@ -336,13 +383,17 @@ def render_users_tab(
336
  # =============================================================================
337
 
338
  def render_trips_tab(
339
- start_date: datetime,
340
- end_date: datetime,
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 (kg)', 'Value': f"{metrics_df.iloc[0]['total_co2_reduced']:,.1f}"},
394
- {'Metric': 'Avg COβ‚‚ per Trip (kg)', 'Value': f"{metrics_df.iloc[0]['avg_co2_per_trip']:,.2f}"},
395
- {'Metric': 'Total NOx Reduced (kg)', 'Value': f"{metrics_df.iloc[0]['total_nox_reduced']:,.1f}"},
396
- {'Metric': 'Total PM2.5 Reduced (kg)', 'Value': f"{metrics_df.iloc[0]['total_pm25_reduced']:,.2f}"},
397
- {'Metric': 'Total Distance (miles)', 'Value': f"{metrics_df.iloc[0]['total_distance_miles']:,.0f}"},
398
- {'Metric': 'Shared Miles', 'Value': f"{metrics_df.iloc[0]['total_shared_miles']:,.0f}"}
 
 
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
- df_to_csv(trips_df if trips_df is not None else pd.DataFrame(), "trips_export.csv")
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 (locations_df.columns if locations_df is not None else []) else None,
435
- hover_data=['city', 'state'] if locations_df is not None and 'city' in locations_df.columns else None,
436
  title='User Geographic Distribution'
437
  )
438
 
439
- # Top markets table (mock for demo)
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
- df_to_csv(locations_df if locations_df is not None else pd.DataFrame(), "geography_export.csv")
453
  )
454
 
455
 
@@ -458,12 +524,16 @@ def render_geography_tab() -> Tuple:
458
  # =============================================================================
459
 
460
  def render_rewards_tab(
461
- start_date: datetime,
462
- end_date: datetime,
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]['total_points']
 
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, "Points Summary")
482
 
483
- # Mock transaction chart for demo
484
  if app_state.demo_mode:
485
- periods = pd.date_range(start=start_date, end=end_date, periods=20)
 
486
  trans_df = pd.DataFrame({
487
  'period': periods,
488
- 'transaction_count': np.random.randint(50, 200, 20),
489
- 'total_amount': np.random.uniform(500, 2000, 20)
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
- df_to_csv(trans_df if trans_df is not None else pd.DataFrame(), "rewards_export.csv")
506
  )
507
 
508
 
509
  # =============================================================================
510
- # GRADIO UI
511
  # =============================================================================
512
 
513
  def build_gradio_app():
514
  """Build and configure Gradio interface."""
515
 
516
- with gr.Blocks(
517
- title="Trip Analytics Dashboard",
518
- theme=gr.themes.Soft(),
519
- css="""
520
- .gradio-container {
521
- font-family: 'Arial', sans-serif;
522
- }
523
- .tab-nav button {
524
- font-size: 16px;
525
- font-weight: 500;
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.Date(label="Start Date", value=start_default)
547
- end_date_input = gr.Date(label="End Date", value=end_default)
 
 
 
 
 
 
 
 
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
- update_overview,
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
- update_overview,
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
- users_export_btn = gr.File(label="Export CSV")
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
- update_users,
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
- users_export_btn
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
- trips_export_btn = gr.File(label="Export CSV")
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
- update_trips,
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
- trips_export_btn
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
- geo_export_btn = gr.File(label="Export CSV")
654
-
655
- def update_geography():
656
- return render_geography_tab()
657
 
658
  refresh_btn.click(
659
- update_geography,
660
- outputs=[geo_heat_map, geo_markets_table, geo_export_btn]
 
 
 
 
 
 
661
  )
662
 
663
  # TAB 5: Rewards
664
  with gr.Tab("🎁 Rewards"):
665
  rewards_points_table = gr.HTML()
666
  rewards_trans_chart = gr.Plot()
667
- rewards_export_btn = gr.File(label="Export CSV")
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
- update_rewards,
676
  inputs=[start_date_input, end_date_input, granularity_input],
677
- outputs=[rewards_points_table, rewards_trans_chart, rewards_export_btn]
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
+ )