LeonceNsh commited on
Commit
ebf7153
·
verified ·
1 Parent(s): 7334b11

Update components.py

Browse files
Files changed (1) hide show
  1. components.py +285 -253
components.py CHANGED
@@ -21,20 +21,21 @@ logger = logging.getLogger(__name__)
21
  # FILTER COMPONENTS
22
  # =============================================================================
23
 
24
- def create_date_range_inputs() -> Tuple:
25
- """Create date range input components."""
26
  end_date = datetime.now()
27
  start_date = end_date - timedelta(days=90)
28
- return start_date.date(), end_date.date()
29
 
30
 
31
  def create_filter_options() -> Dict[str, List]:
32
  """Create filter options for dropdowns."""
33
  return {
34
  "granularity": ["day", "week", "month"],
35
- "driver_types": ["All", "car", "transit", "bike", "walk", "carpool", "vanpool", "ebike", "scooter"],
36
  "trip_types": ["All", "Solo", "Shared"],
37
- "geo_levels": ["state", "city", "zip"]
 
38
  }
39
 
40
 
@@ -65,27 +66,37 @@ def create_kpi_tile(
65
  fmt = KPI_FORMATS.get(format_type, "{}")
66
 
67
  try:
68
- formatted_value = fmt.format(value) if value is not None else "N/A"
 
 
 
 
 
69
  except (ValueError, TypeError):
70
- formatted_value = str(value)
71
 
72
  delta_html = ""
73
- if delta is not None:
74
- delta_color = "green" if delta >= 0 else "red"
75
- delta_symbol = "▲" if delta >= 0 else "▼"
76
- delta_html = f'<div style="color: {delta_color}; font-size: 14px;">{delta_symbol} {abs(delta):.1f}% {delta_label}</div>'
 
 
 
 
77
 
78
  html = f"""
79
  <div style="
80
- border: 1px solid #ddd;
81
- border-radius: 8px;
82
- padding: 20px;
83
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
84
  color: white;
85
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
 
86
  ">
87
- <div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">{title}</div>
88
- <div style="font-size: 32px; font-weight: bold; margin-bottom: 8px;">{formatted_value}</div>
89
  {delta_html}
90
  </div>
91
  """
@@ -107,9 +118,9 @@ def create_kpi_grid(kpis: List[Dict[str, Any]]) -> str:
107
  grid_html = f"""
108
  <div style="
109
  display: grid;
110
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
111
- gap: 20px;
112
- margin-bottom: 30px;
113
  ">
114
  {''.join(tiles)}
115
  </div>
@@ -121,6 +132,24 @@ def create_kpi_grid(kpis: List[Dict[str, Any]]) -> str:
121
  # CHART COMPONENTS
122
  # =============================================================================
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def create_line_chart(
125
  df: pd.DataFrame,
126
  x_col: str,
@@ -131,39 +160,40 @@ def create_line_chart(
131
  color_col: Optional[str] = None
132
  ) -> go.Figure:
133
  """Create a line chart with Plotly."""
134
- if df.empty:
135
- fig = go.Figure()
136
- fig.add_annotation(
137
- text="No data available",
138
- xref="paper", yref="paper",
139
- x=0.5, y=0.5, showarrow=False,
140
- font=dict(size=20, color="gray")
141
- )
142
- return fig
143
 
144
- if color_col:
145
- fig = px.line(
146
- df, x=x_col, y=y_col, color=color_col,
147
- title=title,
148
- labels={x_col: x_label, y_col: y_label},
149
- color_discrete_sequence=COLOR_PALETTE
150
- )
151
- else:
152
- fig = px.line(
153
- df, x=x_col, y=y_col,
154
- title=title,
155
- labels={x_col: x_label, y_col: y_label},
156
- color_discrete_sequence=[COLOR_PALETTE[0]]
 
 
 
 
 
 
 
 
 
 
157
  )
158
-
159
- fig.update_layout(
160
- hovermode='x unified',
161
- template='plotly_white',
162
- font=dict(size=12),
163
- title_font_size=16
164
- )
165
-
166
- return fig
167
 
168
 
169
  def create_bar_chart(
@@ -176,31 +206,30 @@ def create_bar_chart(
176
  orientation: str = "v"
177
  ) -> go.Figure:
178
  """Create a bar chart with Plotly."""
179
- if df.empty:
180
- fig = go.Figure()
181
- fig.add_annotation(
182
- text="No data available",
183
- xref="paper", yref="paper",
184
- x=0.5, y=0.5, showarrow=False,
185
- font=dict(size=20, color="gray")
 
 
 
186
  )
 
 
 
 
 
 
 
 
 
187
  return fig
188
-
189
- fig = px.bar(
190
- df, x=x_col, y=y_col,
191
- title=title,
192
- labels={x_col: x_label, y_col: y_label},
193
- orientation=orientation,
194
- color_discrete_sequence=[COLOR_PALETTE[0]]
195
- )
196
-
197
- fig.update_layout(
198
- template='plotly_white',
199
- font=dict(size=12),
200
- title_font_size=16
201
- )
202
-
203
- return fig
204
 
205
 
206
  def create_pie_chart(
@@ -210,30 +239,35 @@ def create_pie_chart(
210
  title: str
211
  ) -> go.Figure:
212
  """Create a pie chart with Plotly."""
213
- if df.empty:
214
- fig = go.Figure()
215
- fig.add_annotation(
216
- text="No data available",
217
- xref="paper", yref="paper",
218
- x=0.5, y=0.5, showarrow=False,
219
- font=dict(size=20, color="gray")
 
220
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  return fig
222
-
223
- fig = px.pie(
224
- df, names=names_col, values=values_col,
225
- title=title,
226
- color_discrete_sequence=COLOR_PALETTE
227
- )
228
-
229
- fig.update_traces(textposition='inside', textinfo='percent+label')
230
- fig.update_layout(
231
- template='plotly_white',
232
- font=dict(size=12),
233
- title_font_size=16
234
- )
235
-
236
- return fig
237
 
238
 
239
  def create_heatmap(
@@ -246,34 +280,32 @@ def create_heatmap(
246
  y_label: str = ""
247
  ) -> go.Figure:
248
  """Create a heatmap with Plotly."""
249
- if df.empty:
250
- fig = go.Figure()
251
- fig.add_annotation(
252
- text="No data available",
253
- xref="paper", yref="paper",
254
- x=0.5, y=0.5, showarrow=False,
255
- font=dict(size=20, color="gray")
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  )
 
257
  return fig
258
-
259
- # Pivot data for heatmap
260
- pivot_df = df.pivot(index=y_col, columns=x_col, values=z_col)
261
-
262
- fig = px.imshow(
263
- pivot_df,
264
- title=title,
265
- labels=dict(x=x_label, y=y_label, color=z_col),
266
- color_continuous_scale='Blues',
267
- aspect="auto"
268
- )
269
-
270
- fig.update_layout(
271
- template='plotly_white',
272
- font=dict(size=12),
273
- title_font_size=16
274
- )
275
-
276
- return fig
277
 
278
 
279
  def create_geo_heatmap(
@@ -285,69 +317,58 @@ def create_geo_heatmap(
285
  title: str = "Geographic Distribution"
286
  ) -> go.Figure:
287
  """Create a geographic heat map using scatter_mapbox."""
288
- if df.empty or lat_col not in df.columns or lon_col not in df.columns:
289
- fig = go.Figure()
290
- fig.add_annotation(
291
- text="No geographic data available",
292
- xref="paper", yref="paper",
293
- x=0.5, y=0.5, showarrow=False,
294
- font=dict(size=20, color="gray")
295
- )
296
- return fig
297
 
298
  # Remove null coordinates
299
- df_clean = df.dropna(subset=[lat_col, lon_col])
300
 
301
  if df_clean.empty:
302
- fig = go.Figure()
303
- fig.add_annotation(
304
- text="No valid coordinates",
305
- xref="paper", yref="paper",
306
- x=0.5, y=0.5, showarrow=False,
307
- font=dict(size=20, color="gray")
308
- )
309
- return fig
310
-
311
- # Determine center
312
- center_lat = df_clean[lat_col].median()
313
- center_lon = df_clean[lon_col].median()
314
 
315
- # Create map
316
- if MAPBOX_TOKEN:
317
- fig = px.scatter_mapbox(
318
- df_clean,
319
- lat=lat_col,
320
- lon=lon_col,
321
- size=size_col,
322
- hover_data=hover_data,
323
- title=title,
324
- color_continuous_scale='Reds',
325
- zoom=3,
326
- mapbox_style='light'
327
- )
328
- fig.update_layout(mapbox_accesstoken=MAPBOX_TOKEN)
329
- else:
330
  fig = px.scatter_mapbox(
331
  df_clean,
332
  lat=lat_col,
333
  lon=lon_col,
334
- size=size_col,
335
  hover_data=hover_data,
336
  title=title,
337
  color_continuous_scale='Reds',
338
  zoom=3
339
  )
340
-
341
- fig.update_layout(
342
- mapbox_style=MAP_STYLE,
343
- mapbox_center={"lat": center_lat, "lon": center_lon},
344
- template='plotly_white',
345
- height=600,
346
- font=dict(size=12),
347
- title_font_size=16
348
- )
349
-
350
- return fig
 
 
 
 
 
 
 
351
 
352
 
353
  def create_density_heatmap(
@@ -358,54 +379,48 @@ def create_density_heatmap(
358
  title: str = "Heat Map"
359
  ) -> go.Figure:
360
  """Create a density heat map."""
361
- if df.empty:
362
- fig = go.Figure()
363
- fig.add_annotation(
364
- text="No data available",
365
- xref="paper", yref="paper",
366
- x=0.5, y=0.5, showarrow=False,
367
- font=dict(size=20, color="gray")
368
- )
369
- return fig
370
 
371
- df_clean = df.dropna(subset=[lat_col, lon_col])
 
 
 
372
 
373
  if df_clean.empty:
374
- fig = go.Figure()
375
- fig.add_annotation(
376
- text="No valid coordinates",
377
- xref="paper", yref="paper",
378
- x=0.5, y=0.5, showarrow=False,
379
- font=dict(size=20, color="gray")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  )
 
381
  return fig
382
-
383
- center_lat = df_clean[lat_col].median()
384
- center_lon = df_clean[lon_col].median()
385
-
386
- fig = px.density_mapbox(
387
- df_clean,
388
- lat=lat_col,
389
- lon=lon_col,
390
- z=z_col if z_col else None,
391
- radius=10,
392
- title=title,
393
- zoom=3,
394
- mapbox_style=MAP_STYLE
395
- )
396
-
397
- if MAPBOX_TOKEN:
398
- fig.update_layout(mapbox_accesstoken=MAPBOX_TOKEN)
399
-
400
- fig.update_layout(
401
- mapbox_center={"lat": center_lat, "lon": center_lon},
402
- template='plotly_white',
403
- height=600,
404
- font=dict(size=12),
405
- title_font_size=16
406
- )
407
-
408
- return fig
409
 
410
 
411
  # =============================================================================
@@ -418,51 +433,64 @@ def create_data_table(
418
  max_rows: int = 100
419
  ) -> str:
420
  """Create an HTML table from DataFrame."""
421
- if df.empty:
422
- return f"<h3>{title}</h3><p>No data available</p>"
 
 
 
 
 
423
 
424
  # Limit rows
425
- df_display = df.head(max_rows)
426
 
427
  # Format numbers
428
- df_display = df_display.copy()
429
- for col in df_display.select_dtypes(include=['float64']).columns:
430
- df_display[col] = df_display[col].apply(lambda x: f"{x:,.2f}" if pd.notnull(x) else "")
 
431
 
432
  table_html = df_display.to_html(index=False, classes='dataframe', border=0)
433
 
434
  styled_html = f"""
435
- <h3>{title}</h3>
436
- <div style="max-height: 400px; overflow-y: auto;">
437
- <style>
438
- .dataframe {{
439
- border-collapse: collapse;
440
- width: 100%;
441
- font-size: 14px;
442
- }}
443
- .dataframe th {{
444
- background-color: #667eea;
445
- color: white;
446
- padding: 12px;
447
- text-align: left;
448
- position: sticky;
449
- top: 0;
450
- z-index: 10;
451
- }}
452
- .dataframe td {{
453
- padding: 10px;
454
- border-bottom: 1px solid #ddd;
455
- }}
456
- .dataframe tr:hover {{
457
- background-color: #f5f5f5;
458
- }}
459
- </style>
460
- {table_html}
461
- </div>
 
 
 
 
 
462
  """
463
 
464
  if len(df) > max_rows:
465
- styled_html += f"<p><em>Showing {max_rows} of {len(df)} rows</em></p>"
 
 
466
 
467
  return styled_html
468
 
@@ -471,8 +499,12 @@ def create_data_table(
471
  # EXPORT HELPERS
472
  # =============================================================================
473
 
474
- def df_to_csv(df: pd.DataFrame, filename: str = "export.csv") -> str:
475
- """Convert DataFrame to CSV for download."""
476
- if df.empty:
477
  return None
478
- return df.to_csv(index=False)
 
 
 
 
 
21
  # FILTER COMPONENTS
22
  # =============================================================================
23
 
24
+ def create_date_range_inputs() -> Tuple[datetime, datetime]:
25
+ """Create default date range (last 90 days)."""
26
  end_date = datetime.now()
27
  start_date = end_date - timedelta(days=90)
28
+ return start_date, end_date
29
 
30
 
31
  def create_filter_options() -> Dict[str, List]:
32
  """Create filter options for dropdowns."""
33
  return {
34
  "granularity": ["day", "week", "month"],
35
+ "driver_types": ["All", "Owner", "Participant", "External"],
36
  "trip_types": ["All", "Solo", "Shared"],
37
+ "geo_levels": ["state", "city", "zip"],
38
+ "impact_grades": ["All", "A+", "A", "B", "C", "D", "F"]
39
  }
40
 
41
 
 
66
  fmt = KPI_FORMATS.get(format_type, "{}")
67
 
68
  try:
69
+ if value is None:
70
+ formatted_value = "N/A"
71
+ elif pd.isna(value):
72
+ formatted_value = "N/A"
73
+ else:
74
+ formatted_value = fmt.format(float(value))
75
  except (ValueError, TypeError):
76
+ formatted_value = str(value) if value is not None else "N/A"
77
 
78
  delta_html = ""
79
+ if delta is not None and not pd.isna(delta):
80
+ try:
81
+ delta_val = float(delta)
82
+ delta_color = "#10B981" if delta_val >= 0 else "#EF4444"
83
+ delta_symbol = "▲" if delta_val >= 0 else "▼"
84
+ delta_html = f'<div style="color: {delta_color}; font-size: 14px; margin-top: 4px;">{delta_symbol} {abs(delta_val):.1f}% {delta_label}</div>'
85
+ except (ValueError, TypeError):
86
+ pass
87
 
88
  html = f"""
89
  <div style="
90
+ border: 1px solid #e5e7eb;
91
+ border-radius: 12px;
92
+ padding: 24px;
93
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
94
  color: white;
95
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
96
+ min-height: 120px;
97
  ">
98
+ <div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px; font-weight: 500;">{title}</div>
99
+ <div style="font-size: 32px; font-weight: 700; margin-bottom: 4px;">{formatted_value}</div>
100
  {delta_html}
101
  </div>
102
  """
 
118
  grid_html = f"""
119
  <div style="
120
  display: grid;
121
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
122
+ gap: 16px;
123
+ margin-bottom: 24px;
124
  ">
125
  {''.join(tiles)}
126
  </div>
 
132
  # CHART COMPONENTS
133
  # =============================================================================
134
 
135
+ def create_empty_figure(message: str = "No data available") -> go.Figure:
136
+ """Create an empty figure with a message."""
137
+ fig = go.Figure()
138
+ fig.add_annotation(
139
+ text=message,
140
+ xref="paper", yref="paper",
141
+ x=0.5, y=0.5, showarrow=False,
142
+ font=dict(size=16, color="#9CA3AF")
143
+ )
144
+ fig.update_layout(
145
+ template='plotly_white',
146
+ height=400,
147
+ xaxis=dict(visible=False),
148
+ yaxis=dict(visible=False)
149
+ )
150
+ return fig
151
+
152
+
153
  def create_line_chart(
154
  df: pd.DataFrame,
155
  x_col: str,
 
160
  color_col: Optional[str] = None
161
  ) -> go.Figure:
162
  """Create a line chart with Plotly."""
163
+ if df is None or df.empty or x_col not in df.columns or y_col not in df.columns:
164
+ return create_empty_figure("No data available for this period")
 
 
 
 
 
 
 
165
 
166
+ try:
167
+ if color_col and color_col in df.columns:
168
+ fig = px.line(
169
+ df, x=x_col, y=y_col, color=color_col,
170
+ title=title,
171
+ labels={x_col: x_label, y_col: y_label},
172
+ color_discrete_sequence=COLOR_PALETTE
173
+ )
174
+ else:
175
+ fig = px.line(
176
+ df, x=x_col, y=y_col,
177
+ title=title,
178
+ labels={x_col: x_label, y_col: y_label},
179
+ color_discrete_sequence=[COLOR_PALETTE[0]]
180
+ )
181
+
182
+ fig.update_layout(
183
+ hovermode='x unified',
184
+ template='plotly_white',
185
+ font=dict(size=12),
186
+ title_font_size=16,
187
+ height=400,
188
+ margin=dict(l=40, r=40, t=60, b=40)
189
  )
190
+
191
+ fig.update_traces(line=dict(width=2.5))
192
+
193
+ return fig
194
+ except Exception as e:
195
+ logger.error(f"Error creating line chart: {e}")
196
+ return create_empty_figure(f"Error creating chart: {str(e)}")
 
 
197
 
198
 
199
  def create_bar_chart(
 
206
  orientation: str = "v"
207
  ) -> go.Figure:
208
  """Create a bar chart with Plotly."""
209
+ if df is None or df.empty or x_col not in df.columns or y_col not in df.columns:
210
+ return create_empty_figure("No data available")
211
+
212
+ try:
213
+ fig = px.bar(
214
+ df, x=x_col, y=y_col,
215
+ title=title,
216
+ labels={x_col: x_label, y_col: y_label},
217
+ orientation=orientation,
218
+ color_discrete_sequence=[COLOR_PALETTE[0]]
219
  )
220
+
221
+ fig.update_layout(
222
+ template='plotly_white',
223
+ font=dict(size=12),
224
+ title_font_size=16,
225
+ height=400,
226
+ margin=dict(l=40, r=40, t=60, b=40)
227
+ )
228
+
229
  return fig
230
+ except Exception as e:
231
+ logger.error(f"Error creating bar chart: {e}")
232
+ return create_empty_figure(f"Error creating chart: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
 
235
  def create_pie_chart(
 
239
  title: str
240
  ) -> go.Figure:
241
  """Create a pie chart with Plotly."""
242
+ if df is None or df.empty or names_col not in df.columns or values_col not in df.columns:
243
+ return create_empty_figure("No data available")
244
+
245
+ try:
246
+ fig = px.pie(
247
+ df, names=names_col, values=values_col,
248
+ title=title,
249
+ color_discrete_sequence=COLOR_PALETTE
250
  )
251
+
252
+ fig.update_traces(
253
+ textposition='inside',
254
+ textinfo='percent+label',
255
+ hovertemplate='%{label}: %{value:,.0f}<br>%{percent}'
256
+ )
257
+ fig.update_layout(
258
+ template='plotly_white',
259
+ font=dict(size=12),
260
+ title_font_size=16,
261
+ height=400,
262
+ margin=dict(l=40, r=40, t=60, b=40),
263
+ showlegend=True,
264
+ legend=dict(orientation="h", yanchor="bottom", y=-0.2)
265
+ )
266
+
267
  return fig
268
+ except Exception as e:
269
+ logger.error(f"Error creating pie chart: {e}")
270
+ return create_empty_figure(f"Error creating chart: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
 
273
  def create_heatmap(
 
280
  y_label: str = ""
281
  ) -> go.Figure:
282
  """Create a heatmap with Plotly."""
283
+ if df is None or df.empty:
284
+ return create_empty_figure("No data available")
285
+
286
+ try:
287
+ # Pivot data for heatmap
288
+ pivot_df = df.pivot(index=y_col, columns=x_col, values=z_col)
289
+
290
+ fig = px.imshow(
291
+ pivot_df,
292
+ title=title,
293
+ labels=dict(x=x_label, y=y_label, color=z_col),
294
+ color_continuous_scale='Blues',
295
+ aspect="auto"
296
+ )
297
+
298
+ fig.update_layout(
299
+ template='plotly_white',
300
+ font=dict(size=12),
301
+ title_font_size=16,
302
+ height=400
303
  )
304
+
305
  return fig
306
+ except Exception as e:
307
+ logger.error(f"Error creating heatmap: {e}")
308
+ return create_empty_figure(f"Error creating heatmap: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
 
311
  def create_geo_heatmap(
 
317
  title: str = "Geographic Distribution"
318
  ) -> go.Figure:
319
  """Create a geographic heat map using scatter_mapbox."""
320
+ if df is None or df.empty:
321
+ return create_empty_figure("No geographic data available")
322
+
323
+ if lat_col not in df.columns or lon_col not in df.columns:
324
+ return create_empty_figure(f"Missing required columns: {lat_col}, {lon_col}")
 
 
 
 
325
 
326
  # Remove null coordinates
327
+ df_clean = df.dropna(subset=[lat_col, lon_col]).copy()
328
 
329
  if df_clean.empty:
330
+ return create_empty_figure("No valid coordinates found")
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ try:
333
+ # Determine center
334
+ center_lat = df_clean[lat_col].median()
335
+ center_lon = df_clean[lon_col].median()
336
+
337
+ # Filter hover_data to only include columns that exist
338
+ if hover_data:
339
+ hover_data = [col for col in hover_data if col in df_clean.columns]
340
+ if not hover_data:
341
+ hover_data = None
342
+
343
+ # Create map
 
 
 
344
  fig = px.scatter_mapbox(
345
  df_clean,
346
  lat=lat_col,
347
  lon=lon_col,
348
+ size=size_col if size_col and size_col in df_clean.columns else None,
349
  hover_data=hover_data,
350
  title=title,
351
  color_continuous_scale='Reds',
352
  zoom=3
353
  )
354
+
355
+ fig.update_layout(
356
+ mapbox_style=MAP_STYLE,
357
+ mapbox_center={"lat": center_lat, "lon": center_lon},
358
+ template='plotly_white',
359
+ height=500,
360
+ font=dict(size=12),
361
+ title_font_size=16,
362
+ margin=dict(l=0, r=0, t=50, b=0)
363
+ )
364
+
365
+ if MAPBOX_TOKEN:
366
+ fig.update_layout(mapbox_accesstoken=MAPBOX_TOKEN)
367
+
368
+ return fig
369
+ except Exception as e:
370
+ logger.error(f"Error creating geo heatmap: {e}")
371
+ return create_empty_figure(f"Error creating map: {str(e)}")
372
 
373
 
374
  def create_density_heatmap(
 
379
  title: str = "Heat Map"
380
  ) -> go.Figure:
381
  """Create a density heat map."""
382
+ if df is None or df.empty:
383
+ return create_empty_figure("No data available")
 
 
 
 
 
 
 
384
 
385
+ if lat_col not in df.columns or lon_col not in df.columns:
386
+ return create_empty_figure("Missing coordinate columns")
387
+
388
+ df_clean = df.dropna(subset=[lat_col, lon_col]).copy()
389
 
390
  if df_clean.empty:
391
+ return create_empty_figure("No valid coordinates")
392
+
393
+ try:
394
+ center_lat = df_clean[lat_col].median()
395
+ center_lon = df_clean[lon_col].median()
396
+
397
+ fig = px.density_mapbox(
398
+ df_clean,
399
+ lat=lat_col,
400
+ lon=lon_col,
401
+ z=z_col if z_col and z_col in df_clean.columns else None,
402
+ radius=10,
403
+ title=title,
404
+ zoom=3,
405
+ mapbox_style=MAP_STYLE
406
+ )
407
+
408
+ if MAPBOX_TOKEN:
409
+ fig.update_layout(mapbox_accesstoken=MAPBOX_TOKEN)
410
+
411
+ fig.update_layout(
412
+ mapbox_center={"lat": center_lat, "lon": center_lon},
413
+ template='plotly_white',
414
+ height=500,
415
+ font=dict(size=12),
416
+ title_font_size=16,
417
+ margin=dict(l=0, r=0, t=50, b=0)
418
  )
419
+
420
  return fig
421
+ except Exception as e:
422
+ logger.error(f"Error creating density heatmap: {e}")
423
+ return create_empty_figure(f"Error creating heatmap: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
 
426
  # =============================================================================
 
433
  max_rows: int = 100
434
  ) -> str:
435
  """Create an HTML table from DataFrame."""
436
+ if df is None or df.empty:
437
+ return f"""
438
+ <div style="padding: 20px; text-align: center;">
439
+ <h3 style="margin-bottom: 16px; color: #374151;">{title}</h3>
440
+ <p style="color: #9CA3AF;">No data available</p>
441
+ </div>
442
+ """
443
 
444
  # Limit rows
445
+ df_display = df.head(max_rows).copy()
446
 
447
  # Format numbers
448
+ for col in df_display.select_dtypes(include=['float64', 'float32']).columns:
449
+ df_display[col] = df_display[col].apply(
450
+ lambda x: f"{x:,.2f}" if pd.notnull(x) else ""
451
+ )
452
 
453
  table_html = df_display.to_html(index=False, classes='dataframe', border=0)
454
 
455
  styled_html = f"""
456
+ <div style="padding: 16px;">
457
+ <h3 style="margin-bottom: 16px; color: #374151; font-size: 18px; font-weight: 600;">{title}</h3>
458
+ <div style="max-height: 400px; overflow-y: auto; border-radius: 8px; border: 1px solid #e5e7eb;">
459
+ <style>
460
+ .dataframe {{
461
+ border-collapse: collapse;
462
+ width: 100%;
463
+ font-size: 14px;
464
+ }}
465
+ .dataframe th {{
466
+ background-color: #6366f1;
467
+ color: white;
468
+ padding: 12px 16px;
469
+ text-align: left;
470
+ position: sticky;
471
+ top: 0;
472
+ z-index: 10;
473
+ font-weight: 600;
474
+ }}
475
+ .dataframe td {{
476
+ padding: 12px 16px;
477
+ border-bottom: 1px solid #e5e7eb;
478
+ }}
479
+ .dataframe tr:hover {{
480
+ background-color: #f9fafb;
481
+ }}
482
+ .dataframe tr:nth-child(even) {{
483
+ background-color: #f9fafb;
484
+ }}
485
+ </style>
486
+ {table_html}
487
+ </div>
488
  """
489
 
490
  if len(df) > max_rows:
491
+ styled_html += f'<p style="margin-top: 8px; color: #6B7280; font-size: 12px;"><em>Showing {max_rows} of {len(df)} rows</em></p>'
492
+
493
+ styled_html += "</div>"
494
 
495
  return styled_html
496
 
 
499
  # EXPORT HELPERS
500
  # =============================================================================
501
 
502
+ def df_to_csv(df: pd.DataFrame, filename: str = "export.csv") -> Optional[str]:
503
+ """Convert DataFrame to CSV string for download."""
504
+ if df is None or df.empty:
505
  return None
506
+ try:
507
+ return df.to_csv(index=False)
508
+ except Exception as e:
509
+ logger.error(f"Error converting to CSV: {e}")
510
+ return None