Spaces:
Running
Running
| """The dashboard app sample to see how it works.""" | |
| from typing import Any, List | |
| import logging | |
| from io import StringIO | |
| import pandas as pd | |
| from dash import Dash, html, dcc, callback_context | |
| from dash.dependencies import Input, Output, State | |
| from dash.exceptions import PreventUpdate | |
| from .loader import DataLoader | |
| from .plugin_manager import PluginManager | |
| logger = logging.getLogger(__name__) | |
| class DashboardApp(DataLoader): | |
| """Dashboard Application Class.""" | |
| def __init__( | |
| self, | |
| data_path: str = "", # pylint: disable=line-too-long | |
| ) -> None: | |
| """Initialize the Dashboard App.""" | |
| super().__init__(data_path) | |
| self.app = Dash(suppress_callback_exceptions=True) | |
| self.plugins = PluginManager() | |
| self.register_plugin_callbacks() | |
| def register_plugin_callbacks(self) -> None: | |
| """Register callbacks for all plugins.""" | |
| if self.plugins is None: | |
| return | |
| for group in self.plugins.plot_groups.values(): | |
| for p in group: # type: ignore | |
| p.register_callbacks(self.app) | |
| for t in self.plugins.tables: | |
| t.register_callbacks(self.app) | |
| def layout(self) -> None: | |
| """Define the layout of the dashboard.""" | |
| menu_style = { | |
| "padding": "12px 16px", | |
| "borderRadius": "8px", | |
| "cursor": "pointer", | |
| "fontWeight": "600", | |
| "backgroundColor": "#ffffff", | |
| "color": "#333", | |
| "boxShadow": "0 1px 3px rgba(0,0,0,0.08)", | |
| "transition": "all 0.2s ease-in-out", | |
| "marginBottom": "12px", | |
| } | |
| self.app.layout = html.Div( | |
| [ | |
| html.Div( | |
| [ | |
| html.H2("Menu"), | |
| # Tables | |
| html.Div( | |
| "Tables", id="menu-tables", n_clicks=0, style=menu_style | |
| ), | |
| # Plot groups | |
| html.Div( | |
| "Basic Plots", id="menu-basic", n_clicks=0, style=menu_style | |
| ), | |
| html.Div( | |
| "Statistical Plots", | |
| id="menu-statistical", | |
| n_clicks=0, | |
| style=menu_style, | |
| ), | |
| html.Div( | |
| "Geospatial Plots", | |
| id="menu-geo", | |
| n_clicks=0, | |
| style=menu_style, | |
| ), | |
| html.Div( | |
| "Dimensionality Reduction", | |
| id="menu-dim", | |
| n_clicks=0, | |
| style=menu_style, | |
| ), | |
| html.Div( | |
| "Advanced Visualizations", | |
| id="menu-advanced", | |
| n_clicks=0, | |
| style=menu_style, | |
| ), | |
| html.Div("NLP", id="menu-nlp", n_clicks=0, style=menu_style), | |
| dcc.Store(id="sidebar-menu", data="tables"), # default mode | |
| ], | |
| style={ | |
| "width": "180px", | |
| "backgroundColor": "#f4f4f4", | |
| "padding": "20px", | |
| "height": "100vh", | |
| "position": "fixed", | |
| "left": 0, | |
| "top": 0, | |
| "overflowY": "auto", | |
| "borderRight": "1px solid #ccc", | |
| }, | |
| ), | |
| html.Div( | |
| [ | |
| html.H1("Customized Dashboard"), | |
| html.H3( | |
| "Load CSV by upload the file or enter the URL and then select visualizations from left menu." | |
| ), | |
| # FILE LOAD SECTION | |
| html.Div( | |
| [ | |
| html.Div( | |
| dcc.Upload( | |
| id="upload-data", | |
| children=html.Div( | |
| ["Drag & Drop or ", html.A("Select File")] | |
| ), | |
| style={ | |
| "width": "100%", | |
| "height": "60px", | |
| "lineHeight": "60px", | |
| "borderWidth": "1px", | |
| "borderStyle": "dashed", | |
| "borderRadius": "5px", | |
| "textAlign": "center", | |
| }, | |
| multiple=False, | |
| ), | |
| style={"flex": "1", "marginRight": "20px"}, | |
| ), | |
| html.Div( | |
| [ | |
| dcc.Input( | |
| id="csv-url", | |
| placeholder="Enter CSV URL...", | |
| value="https://gist.githubusercontent.com/chriddyp/5d1ea79569ed194d432e56108a04d188/raw/a9f9e8076b837d541398e999dcbac2b2826a81f8/gdp-life-exp-2007.csv", # pylint: disable=line-too-long | |
| style={ | |
| "width": "75%", | |
| "marginRight": "10px", | |
| }, | |
| ), | |
| html.Button("Load URL", id="load-url"), | |
| ], | |
| style={ | |
| "flex": "1", | |
| "display": "flex", | |
| "flexDirection": "row", | |
| "alignItems": "center", | |
| }, | |
| ), | |
| ], | |
| style={ | |
| "display": "flex", | |
| "flexDirection": "row", | |
| "justifyContent": "space-between", | |
| "marginBottom": "20px", | |
| }, | |
| ), | |
| dcc.Store(id="stored-data"), | |
| html.Hr(), | |
| html.Div(id="dashboard-ui"), | |
| ], | |
| style={"marginLeft": "220px", "padding": "20px"}, | |
| ), | |
| ] | |
| ) | |
| # load csv data upload or from url | |
| def load_data(upload: Any, url_click: Any, url_text: Any) -> Any: | |
| """Load data from upload or URL.""" | |
| _ = url_click | |
| ctx = callback_context | |
| if not ctx.triggered: | |
| raise PreventUpdate | |
| trigger = ctx.triggered[0]["prop_id"].split(".")[0] | |
| if trigger == "upload-data" and upload: | |
| df = self.load_uploaded_csv(upload) | |
| return df.to_json(orient="split") | |
| if trigger == "load-url" and url_text: | |
| df = self.load_from_url(url_text) | |
| return df.to_json(orient="split") | |
| raise PreventUpdate | |
| def show_dashboard(json_df: Any) -> Any: | |
| """Create PluginManager + Render UI AFTER data is loaded.""" | |
| df = pd.read_json(StringIO(json_df), orient="split") | |
| # Build plugin manager dynamically | |
| self.plugins.set_dataframe(df) | |
| table_names = self.plugins.get_table_names() | |
| return html.Div( | |
| [ | |
| html.H2("Dataset Loaded Successfully"), | |
| # Table dropdown | |
| html.Div( | |
| [ | |
| html.Label("Select Table"), | |
| dcc.Dropdown( | |
| id="table-select", | |
| options=[{"label": t, "value": t} for t in table_names], # type: ignore | |
| value=table_names[0], | |
| clearable=False, | |
| ), | |
| ], | |
| id="table-select-container", | |
| style={"marginBottom": "20px"}, | |
| ), | |
| html.Div(id="table-controls"), | |
| html.Div(id="table-output"), | |
| html.Hr(), | |
| # Plot selection | |
| html.Div( | |
| [ | |
| html.Label("Select Plots"), | |
| dcc.Dropdown( | |
| id="dynamic-plot-select", | |
| options=[], | |
| multi=True, | |
| ), | |
| ], | |
| id="plot-select-container", | |
| style={"marginBottom": "20px"}, | |
| ), | |
| html.Div(id="plots-container"), | |
| ] | |
| ) | |
| def update_plot_dropdown(menu: str) -> Any: | |
| """Update plot dropdown based on menu selection.""" | |
| if menu == "tables": | |
| return [], [] | |
| # Get list of plugin names in selected category | |
| try: | |
| names = self.plugins.get_plots_in_category(menu) | |
| except KeyError: | |
| return [], [] | |
| options = [{"label": n, "value": n} for n in names] | |
| return options, [] | |
| # Update menu selection | |
| def update_menu( # pylint: disable=R0913,R0917 | |
| tbl: Any, basic: Any, stat: Any, geo: Any, dim: Any, adv: Any, nlp: Any | |
| ) -> Any: | |
| """Update sidebar menu selection.""" | |
| _ = tbl, basic, stat, geo, dim, adv, nlp | |
| ctx = callback_context | |
| if not ctx.triggered: | |
| raise PreventUpdate | |
| clicked = ctx.triggered_id | |
| mapping = { | |
| "menu-tables": "tables", | |
| "menu-basic": "Basic Plots", | |
| "menu-statistical": "Statistical", | |
| "menu-geo": "Geospatial", | |
| "menu-dim": "Dimensionality Reduction", | |
| "menu-advanced": "Advanced Visualizations", | |
| "menu-nlp": "NLP", | |
| } | |
| return mapping[clicked] | |
| # Table update | |
| def update_table(name: str) -> Any: | |
| """Update table based on selection.""" | |
| t = self.plugins.get_table(name) | |
| return t.controls(), t.render() | |
| # Plot block builder | |
| def build_plots(selected: List[str]) -> Any: | |
| """Build plot blocks based on selected plots.""" | |
| blocks = [] | |
| for name in selected: | |
| plug = self.plugins.get_plot(name) | |
| # Single plot block | |
| block = html.Div( | |
| [ | |
| html.Div( | |
| html.H3(name, style={"margin": "0"}), | |
| style={ | |
| "textAlign": "center", | |
| "width": "100%", | |
| "marginBottom": "10px", | |
| }, | |
| ), | |
| html.Div( | |
| [ | |
| html.Div( | |
| plug.controls(), | |
| style={ | |
| "width": "140px", | |
| "display": "flex", | |
| "flexDirection": "column", | |
| "gap": "2px", | |
| }, | |
| ), | |
| html.Div( | |
| id={"type": "plot-output", "plot": name}, | |
| style={"flex": "1"}, | |
| ), | |
| ], | |
| style={ | |
| "display": "flex", | |
| "flexDirection": "row", | |
| "alignItems": "flex-start", | |
| }, | |
| ), | |
| ], | |
| className="plot-card", | |
| style={ | |
| "padding": "18px", | |
| "borderRadius": "10px", | |
| "backgroundColor": "#1f1f1f", | |
| "border": "1px solid #333", | |
| "boxShadow": "0px 0px 10px rgba(0,0,0,0.5)", | |
| "color": "#eee", | |
| }, | |
| ) | |
| blocks.append(block) | |
| # Put blocks into 2-column grid | |
| grid = html.Div( | |
| [ | |
| html.Div(block, style={"width": "48%", "margin": "1%"}) | |
| for block in blocks | |
| ], | |
| style={ | |
| "display": "flex", | |
| "flexWrap": "wrap", | |
| "justifyContent": "space-between", | |
| }, | |
| ) | |
| return grid | |
| def update_sidebar(menu: str) -> Any: | |
| """Update sidebar options based on menu selection.""" | |
| if menu == "tables": | |
| return html.Div() | |
| if menu == "plots": | |
| return html.Div() | |
| raise PreventUpdate | |
| def toggle_sections(menu: str) -> Any: | |
| """Show tables only for table mode, show plots for all plot categories.""" | |
| if menu == "tables": | |
| return ( | |
| {"display": "block"}, | |
| {"display": "block"}, | |
| {"display": "none"}, | |
| ) | |
| # All plot groups | |
| return ( | |
| {"display": "none"}, | |
| {"display": "none"}, | |
| {"display": "block"}, | |
| ) | |
| def toggle_dropdowns(menu: str) -> Any: | |
| """Show table dropdown ONLY for tables; show plot dropdown for all plot groups.""" | |
| if menu == "tables": | |
| return {"display": "block"}, {"display": "none"} | |
| # Any plot group → show the plot dropdown | |
| return {"display": "none"}, {"display": "block"} | |
| def run(self) -> None: | |
| """Run the dashboard app.""" | |
| self.layout() | |
| self.app.run(host="0.0.0.0", port=7860, debug=False) # nosec | |