""" Reusable UI components for Gradio dashboard. Filters, KPI tiles, tables, and chart templates. """ from typing import List, Dict, Any, Optional, Tuple from datetime import datetime, timedelta import logging import pandas as pd import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots from config import COLOR_PALETTE, KPI_FORMATS, MAP_STYLE, MAPBOX_TOKEN logger = logging.getLogger(__name__) # ============================================================================= # FILTER COMPONENTS # ============================================================================= def create_date_range_inputs() -> Tuple[datetime, datetime]: """Create default date range (last 90 days).""" end_date = datetime.now() start_date = end_date - timedelta(days=90) return start_date, end_date def create_filter_options() -> Dict[str, List]: """Create filter options for dropdowns.""" return { "granularity": ["day", "week", "month"], "driver_types": ["All", "Owner", "Participant", "External"], "trip_types": ["All", "Solo", "Shared"], "geo_levels": ["state", "city", "zip"], "impact_grades": ["All", "A+", "A", "B", "C", "D", "F"] } # ============================================================================= # KPI TILE COMPONENTS # ============================================================================= def create_kpi_tile( title: str, value: Any, format_type: str = "users", delta: Optional[float] = None, delta_label: str = "vs prev period" ) -> str: """ Create HTML for a KPI tile. Args: title: KPI title value: Main value to display format_type: Format type from KPI_FORMATS delta: Optional change value delta_label: Label for delta Returns: HTML string """ fmt = KPI_FORMATS.get(format_type, "{}") try: if value is None: formatted_value = "N/A" elif pd.isna(value): formatted_value = "N/A" else: formatted_value = fmt.format(float(value)) except (ValueError, TypeError): formatted_value = str(value) if value is not None else "N/A" delta_html = "" if delta is not None and not pd.isna(delta): try: delta_val = float(delta) delta_color = "#10B981" if delta_val >= 0 else "#EF4444" delta_symbol = "▲" if delta_val >= 0 else "▼" delta_html = f'
{delta_symbol} {abs(delta_val):.1f}% {delta_label}
' except (ValueError, TypeError): pass html = f"""
{title}
{formatted_value}
{delta_html}
""" return html def create_kpi_grid(kpis: List[Dict[str, Any]]) -> str: """ Create a grid of KPI tiles. Args: kpis: List of dicts with keys: title, value, format_type, delta (optional) Returns: HTML grid """ tiles = [create_kpi_tile(**kpi) for kpi in kpis] grid_html = f"""
{''.join(tiles)}
""" return grid_html # ============================================================================= # CHART COMPONENTS # ============================================================================= def create_empty_figure(message: str = "No data available") -> go.Figure: """Create an empty figure with a message.""" fig = go.Figure() fig.add_annotation( text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=16, color="#9CA3AF") ) fig.update_layout( template='plotly_white', height=400, xaxis=dict(visible=False), yaxis=dict(visible=False) ) return fig def create_line_chart( df: pd.DataFrame, x_col: str, y_col: str, title: str, x_label: str = "", y_label: str = "", color_col: Optional[str] = None ) -> go.Figure: """Create a line chart with Plotly.""" if df is None or df.empty or x_col not in df.columns or y_col not in df.columns: return create_empty_figure("No data available for this period") try: if color_col and color_col in df.columns: fig = px.line( df, x=x_col, y=y_col, color=color_col, title=title, labels={x_col: x_label, y_col: y_label}, color_discrete_sequence=COLOR_PALETTE ) else: fig = px.line( df, x=x_col, y=y_col, title=title, labels={x_col: x_label, y_col: y_label}, color_discrete_sequence=[COLOR_PALETTE[0]] ) fig.update_layout( hovermode='x unified', template='plotly_white', font=dict(size=12), title_font_size=16, height=400, margin=dict(l=40, r=40, t=60, b=40) ) fig.update_traces(line=dict(width=2.5)) return fig except Exception as e: logger.error(f"Error creating line chart: {e}") return create_empty_figure(f"Error creating chart: {str(e)}") def create_bar_chart( df: pd.DataFrame, x_col: str, y_col: str, title: str, x_label: str = "", y_label: str = "", orientation: str = "v" ) -> go.Figure: """Create a bar chart with Plotly.""" if df is None or df.empty or x_col not in df.columns or y_col not in df.columns: return create_empty_figure("No data available") try: fig = px.bar( df, x=x_col, y=y_col, title=title, labels={x_col: x_label, y_col: y_label}, orientation=orientation, color_discrete_sequence=[COLOR_PALETTE[0]] ) fig.update_layout( template='plotly_white', font=dict(size=12), title_font_size=16, height=400, margin=dict(l=40, r=40, t=60, b=40) ) return fig except Exception as e: logger.error(f"Error creating bar chart: {e}") return create_empty_figure(f"Error creating chart: {str(e)}") def create_pie_chart( df: pd.DataFrame, names_col: str, values_col: str, title: str ) -> go.Figure: """Create a pie chart with Plotly.""" if df is None or df.empty or names_col not in df.columns or values_col not in df.columns: return create_empty_figure("No data available") try: fig = px.pie( df, names=names_col, values=values_col, title=title, color_discrete_sequence=COLOR_PALETTE ) fig.update_traces( textposition='inside', textinfo='percent+label', hovertemplate='%{label}: %{value:,.0f}
%{percent}' ) fig.update_layout( template='plotly_white', font=dict(size=12), title_font_size=16, height=400, margin=dict(l=40, r=40, t=60, b=40), showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=-0.2) ) return fig except Exception as e: logger.error(f"Error creating pie chart: {e}") return create_empty_figure(f"Error creating chart: {str(e)}") def create_heatmap( df: pd.DataFrame, x_col: str, y_col: str, z_col: str, title: str, x_label: str = "", y_label: str = "" ) -> go.Figure: """Create a heatmap with Plotly.""" if df is None or df.empty: return create_empty_figure("No data available") try: # Pivot data for heatmap pivot_df = df.pivot(index=y_col, columns=x_col, values=z_col) fig = px.imshow( pivot_df, title=title, labels=dict(x=x_label, y=y_label, color=z_col), color_continuous_scale='Blues', aspect="auto" ) fig.update_layout( template='plotly_white', font=dict(size=12), title_font_size=16, height=400 ) return fig except Exception as e: logger.error(f"Error creating heatmap: {e}") return create_empty_figure(f"Error creating heatmap: {str(e)}") def create_geo_heatmap( df: pd.DataFrame, lat_col: str = "latitude", lon_col: str = "longitude", size_col: Optional[str] = None, hover_data: Optional[List[str]] = None, title: str = "Geographic Distribution" ) -> go.Figure: """Create a geographic heat map using scatter_mapbox.""" if df is None or df.empty: return create_empty_figure("No geographic data available") if lat_col not in df.columns or lon_col not in df.columns: return create_empty_figure(f"Missing required columns: {lat_col}, {lon_col}") # Remove null coordinates df_clean = df.dropna(subset=[lat_col, lon_col]).copy() if df_clean.empty: return create_empty_figure("No valid coordinates found") try: # Determine center center_lat = df_clean[lat_col].median() center_lon = df_clean[lon_col].median() # Filter hover_data to only include columns that exist if hover_data: hover_data = [col for col in hover_data if col in df_clean.columns] if not hover_data: hover_data = None # Create map fig = px.scatter_mapbox( df_clean, lat=lat_col, lon=lon_col, size=size_col if size_col and size_col in df_clean.columns else None, hover_data=hover_data, title=title, color_continuous_scale='Reds', zoom=3 ) fig.update_layout( mapbox_style=MAP_STYLE, mapbox_center={"lat": center_lat, "lon": center_lon}, template='plotly_white', height=500, font=dict(size=12), title_font_size=16, margin=dict(l=0, r=0, t=50, b=0) ) if MAPBOX_TOKEN: fig.update_layout(mapbox_accesstoken=MAPBOX_TOKEN) return fig except Exception as e: logger.error(f"Error creating geo heatmap: {e}") return create_empty_figure(f"Error creating map: {str(e)}") def create_density_heatmap( df: pd.DataFrame, lat_col: str = "latitude", lon_col: str = "longitude", z_col: Optional[str] = None, title: str = "Heat Map" ) -> go.Figure: """Create a density heat map.""" if df is None or df.empty: return create_empty_figure("No data available") if lat_col not in df.columns or lon_col not in df.columns: return create_empty_figure("Missing coordinate columns") df_clean = df.dropna(subset=[lat_col, lon_col]).copy() if df_clean.empty: return create_empty_figure("No valid coordinates") try: center_lat = df_clean[lat_col].median() center_lon = df_clean[lon_col].median() fig = px.density_mapbox( df_clean, lat=lat_col, lon=lon_col, z=z_col if z_col and z_col in df_clean.columns else None, radius=10, title=title, zoom=3, mapbox_style=MAP_STYLE ) if MAPBOX_TOKEN: fig.update_layout(mapbox_accesstoken=MAPBOX_TOKEN) fig.update_layout( mapbox_center={"lat": center_lat, "lon": center_lon}, template='plotly_white', height=500, font=dict(size=12), title_font_size=16, margin=dict(l=0, r=0, t=50, b=0) ) return fig except Exception as e: logger.error(f"Error creating density heatmap: {e}") return create_empty_figure(f"Error creating heatmap: {str(e)}") # ============================================================================= # TABLE COMPONENTS # ============================================================================= def create_data_table( df: pd.DataFrame, title: str = "", max_rows: int = 100 ) -> str: """Create an HTML table from DataFrame.""" if df is None or df.empty: return f"""

{title}

No data available

""" # Limit rows df_display = df.head(max_rows).copy() # Format numbers for col in df_display.select_dtypes(include=['float64', 'float32']).columns: df_display[col] = df_display[col].apply( lambda x: f"{x:,.2f}" if pd.notnull(x) else "" ) table_html = df_display.to_html(index=False, classes='dataframe', border=0) styled_html = f"""

{title}

{table_html}
""" if len(df) > max_rows: styled_html += f'

Showing {max_rows} of {len(df)} rows

' styled_html += "
" return styled_html # ============================================================================= # EXPORT HELPERS # ============================================================================= def df_to_csv(df: pd.DataFrame, filename: str = "export.csv") -> Optional[str]: """Convert DataFrame to CSV string for download.""" if df is None or df.empty: return None try: return df.to_csv(index=False) except Exception as e: logger.error(f"Error converting to CSV: {e}") return None