afshin-dini's picture
Change the ip bacj=k to what it was
e3e1e3e
"""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
@self.app.callback(
Output("stored-data", "data"),
Input("upload-data", "contents"),
Input("load-url", "n_clicks"),
State("csv-url", "value"),
prevent_initial_call=True,
)
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
@self.app.callback(
Output("dashboard-ui", "children"),
Input("stored-data", "data"),
prevent_initial_call=True,
)
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"),
]
)
@self.app.callback(
Output("dynamic-plot-select", "options"),
Output("dynamic-plot-select", "value"),
Input("sidebar-menu", "data"),
)
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
@self.app.callback(
Output("sidebar-menu", "data"),
Input("menu-tables", "n_clicks"),
Input("menu-basic", "n_clicks"),
Input("menu-statistical", "n_clicks"),
Input("menu-geo", "n_clicks"),
Input("menu-dim", "n_clicks"),
Input("menu-advanced", "n_clicks"),
Input("menu-nlp", "n_clicks"),
)
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
@self.app.callback(
Output("table-controls", "children"),
Output("table-output", "children"),
Input("table-select", "value"),
)
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
@self.app.callback(
Output("plots-container", "children"),
Input("dynamic-plot-select", "value"),
)
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
@self.app.callback(
Output("sidebar-options", "children"),
Input("sidebar-menu", "data"),
prevent_initial_call=True,
)
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
@self.app.callback(
Output("table-controls", "style"),
Output("table-output", "style"),
Output("plots-container", "style"),
Input("sidebar-menu", "data"),
)
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"},
)
@self.app.callback(
Output("table-select-container", "style"),
Output("plot-select-container", "style"),
Input("sidebar-menu", "data"),
)
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