dash base implementation completed
This commit is contained in:
parent
c670f48b5b
commit
020728ed5d
File diff suppressed because it is too large
Load Diff
|
@ -18,6 +18,7 @@ abstention = "^0.1.3.1"
|
||||||
main = "quacc.main:main"
|
main = "quacc.main:main"
|
||||||
run = "run:run"
|
run = "run:run"
|
||||||
panel = "qcpanel.run:run"
|
panel = "qcpanel.run:run"
|
||||||
|
dash = "qcdash.app:run"
|
||||||
sync_up = "remote:sync_code"
|
sync_up = "remote:sync_code"
|
||||||
sync_down = "remote:sync_output"
|
sync_down = "remote:sync_output"
|
||||||
merge_data = "merge_data:run"
|
merge_data = "merge_data:run"
|
||||||
|
@ -48,6 +49,8 @@ ipympl = "^0.9.3"
|
||||||
ipykernel = "^6.26.0"
|
ipykernel = "^6.26.0"
|
||||||
ipywidgets-bokeh = "^1.5.0"
|
ipywidgets-bokeh = "^1.5.0"
|
||||||
pandas-stubs = "^2.1.1.230928"
|
pandas-stubs = "^2.1.1.230928"
|
||||||
|
dash = "^2.14.1"
|
||||||
|
dash-bootstrap-components = "^1.5.0"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--cov=quacc --capture=tee-sys"
|
addopts = "--cov=quacc --capture=tee-sys"
|
||||||
|
|
229
qcdash/app.py
229
qcdash/app.py
|
@ -1,43 +1,226 @@
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
from dash import Dash, dash_table, dcc, html
|
from dash import Dash, Input, Output, State, callback, dash_table, dcc, html
|
||||||
|
|
||||||
from quacc.evaluation.report import DatasetReport
|
from quacc import plot
|
||||||
|
from quacc.evaluation.estimators import CE
|
||||||
|
from quacc.evaluation.report import CompReport, DatasetReport
|
||||||
|
|
||||||
|
backend = plot.get_backend("plotly")
|
||||||
|
|
||||||
|
valid_plot_modes = defaultdict(lambda: CompReport._default_modes)
|
||||||
|
valid_plot_modes["avg"] = DatasetReport._default_dr_modes
|
||||||
|
|
||||||
|
|
||||||
def get_fig(data: pd.DataFrame):
|
def get_datasets(root: str | Path) -> List[DatasetReport]:
|
||||||
fig = go.Figure()
|
def explore_datasets(root: str | Path) -> List[Path]:
|
||||||
xs = data.index.to_numpy()
|
if isinstance(root, str):
|
||||||
for col in data.columns.unique(0):
|
root = Path(root)
|
||||||
_line = go.Scatter(x=xs, y=data.loc[:, col], mode="lines+markers", name=col)
|
|
||||||
fig.add_trace(_line)
|
|
||||||
|
|
||||||
fig.update_layout(xaxis_title="test_prevalence", yaxis_title="acc. error")
|
if root.name == "plot":
|
||||||
|
return []
|
||||||
|
|
||||||
return fig
|
if not root.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
dr_paths = []
|
||||||
|
for f in os.listdir(root):
|
||||||
|
if (root / f).is_dir():
|
||||||
|
dr_paths += explore_datasets(root / f)
|
||||||
|
elif f == f"{root.name}.pickle":
|
||||||
|
dr_paths.append(root / f)
|
||||||
|
|
||||||
|
return dr_paths
|
||||||
|
|
||||||
|
dr_paths = sorted(explore_datasets(root), key=lambda t: (-len(t.parts), t))
|
||||||
|
return {str(drp.parent): DatasetReport.unpickle(drp) for drp in dr_paths}
|
||||||
|
|
||||||
|
|
||||||
def app_instance():
|
def get_fig(dr: DatasetReport, metric, estimators, view, mode):
|
||||||
dr: DatasetReport = DatasetReport.unpickle(Path("output/debug/imdb/imdb.pickle"))
|
estimators = CE.name[estimators]
|
||||||
data = dr.data(metric="acc").groupby(level=1).mean()
|
match (view, mode):
|
||||||
|
case ("avg", _):
|
||||||
|
return dr.get_plots(
|
||||||
|
mode=mode,
|
||||||
|
metric=metric,
|
||||||
|
estimators=estimators,
|
||||||
|
conf="plotly",
|
||||||
|
save_fig=False,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
case (_, _):
|
||||||
|
cr = dr.crs[[str(round(c.train_prev[1] * 100)) for c in dr.crs].index(view)]
|
||||||
|
return cr.get_plots(
|
||||||
|
mode=mode,
|
||||||
|
metric=metric,
|
||||||
|
estimators=estimators,
|
||||||
|
conf="plotly",
|
||||||
|
save_fig=False,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
|
||||||
app = Dash(__name__)
|
|
||||||
|
|
||||||
app.layout = html.Div(
|
datasets = get_datasets("output")
|
||||||
[
|
|
||||||
# html.Div(children="Hello World"),
|
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
|
||||||
# dash_table.DataTable(data=df.to_dict("records")),
|
|
||||||
dcc.Graph(figure=get_fig(data), style={"height": "95vh"}),
|
sidebar_style = {
|
||||||
]
|
"top": 0,
|
||||||
)
|
"left": 0,
|
||||||
return app
|
"bottom": 0,
|
||||||
|
"padding": "1vw",
|
||||||
|
"padding-top": "2vw",
|
||||||
|
"margin": "0px",
|
||||||
|
"flex": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
content_style = {
|
||||||
|
# "margin-left": "18vw",
|
||||||
|
"flex": 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sidebar = html.Div(
|
||||||
|
children=[
|
||||||
|
html.H4("Parameters:", style={"margin-bottom": "1vw"}),
|
||||||
|
dbc.Select(
|
||||||
|
options=list(datasets.keys()),
|
||||||
|
value=list(datasets.keys())[0],
|
||||||
|
id="dataset",
|
||||||
|
),
|
||||||
|
dbc.Select(
|
||||||
|
# clearable=False,
|
||||||
|
# searchable=False,
|
||||||
|
id="metric",
|
||||||
|
style={"margin-top": "1vh"},
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
dbc.RadioItems(
|
||||||
|
id="view",
|
||||||
|
class_name="btn-group mt-3",
|
||||||
|
input_class_name="btn-check",
|
||||||
|
label_class_name="btn btn-outline-primary",
|
||||||
|
label_checked_class_name="active",
|
||||||
|
),
|
||||||
|
dbc.RadioItems(
|
||||||
|
id="mode",
|
||||||
|
class_name="btn-group mt-3",
|
||||||
|
input_class_name="btn-check",
|
||||||
|
label_class_name="btn btn-outline-primary",
|
||||||
|
label_checked_class_name="active",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
className="radio-group-v d-flex justify-content-around",
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
dbc.Checklist(
|
||||||
|
id="estimators",
|
||||||
|
className="btn-group mt-3",
|
||||||
|
inputClassName="btn-check",
|
||||||
|
labelClassName="btn btn-outline-primary",
|
||||||
|
labelCheckedClassName="active",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
className="radio-group-wide",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style=sidebar_style,
|
||||||
|
id="app-sidebar",
|
||||||
|
)
|
||||||
|
|
||||||
|
content = html.Div(
|
||||||
|
children=[
|
||||||
|
dcc.Graph(
|
||||||
|
style={"margin": 0, "height": "100vh"},
|
||||||
|
id="graph1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style=content_style,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.layout = html.Div(
|
||||||
|
children=[sidebar, content],
|
||||||
|
style={"display": "flex", "flexDirection": "row"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("metric", "options"),
|
||||||
|
Output("metric", "value"),
|
||||||
|
Input("dataset", "value"),
|
||||||
|
State("metric", "value"),
|
||||||
|
)
|
||||||
|
def update_metrics(dataset, old_metric):
|
||||||
|
dr = datasets[dataset]
|
||||||
|
valid_metrics = [m for m in dr.data().columns.unique(0) if not m.endswith("_score")]
|
||||||
|
new_metric = old_metric if old_metric in valid_metrics else valid_metrics[0]
|
||||||
|
return valid_metrics, new_metric
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("estimators", "options"),
|
||||||
|
Output("estimators", "value"),
|
||||||
|
Input("dataset", "value"),
|
||||||
|
Input("metric", "value"),
|
||||||
|
State("estimators", "value"),
|
||||||
|
)
|
||||||
|
def update_estimators(dataset, metric, old_estimators):
|
||||||
|
dr = datasets[dataset]
|
||||||
|
valid_estimators = dr.data(metric=metric).columns.unique(0).to_numpy()
|
||||||
|
new_estimators = valid_estimators[
|
||||||
|
np.isin(valid_estimators, old_estimators)
|
||||||
|
].tolist()
|
||||||
|
return valid_estimators, new_estimators
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("view", "options"),
|
||||||
|
Output("view", "value"),
|
||||||
|
Input("dataset", "value"),
|
||||||
|
State("view", "value"),
|
||||||
|
)
|
||||||
|
def update_view(dataset, old_view):
|
||||||
|
dr = datasets[dataset]
|
||||||
|
valid_views = ["avg"] + [str(round(cr.train_prev[1] * 100)) for cr in dr.crs]
|
||||||
|
new_view = old_view if old_view in valid_views else valid_views[0]
|
||||||
|
return valid_views, new_view
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("mode", "options"),
|
||||||
|
Output("mode", "value"),
|
||||||
|
Input("view", "value"),
|
||||||
|
State("mode", "value"),
|
||||||
|
)
|
||||||
|
def update_mode(view, old_mode):
|
||||||
|
valid_modes = [m for m in valid_plot_modes[view] if not m.endswith("table")]
|
||||||
|
new_mode = old_mode if old_mode in valid_modes else valid_modes[0]
|
||||||
|
return valid_modes, new_mode
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("graph1", "figure"),
|
||||||
|
Input("dataset", "value"),
|
||||||
|
Input("metric", "value"),
|
||||||
|
Input("estimators", "value"),
|
||||||
|
Input("view", "value"),
|
||||||
|
Input("mode", "value"),
|
||||||
|
)
|
||||||
|
def update_graph(dataset, metric, estimators, view, mode):
|
||||||
|
dr = datasets[dataset]
|
||||||
|
return get_fig(dr=dr, metric=metric, estimators=estimators, view=view, mode=mode)
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
app = app_instance()
|
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* restyle radio items */
|
||||||
|
.radio-group .form-check {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group .btn-group > .form-check:not(:last-child) > .btn {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group .btn-group > .form-check:not(:first-child) > .btn {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-v{
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.radio-group-v .form-check {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-v .btn-group {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-v > .btn-group:first-child {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-v > .btn-group:last-child {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-v .btn-group > .form-check:not(:last-child) > .btn {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-v .btn-group > .form-check:not(:first-child) > .btn {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.radio-group-v .btn-group .btn{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-wide .form-check {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-wide .btn-group{
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-wide .btn-group .form-check{
|
||||||
|
flex: 1;
|
||||||
|
margin-top: -3px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.radio-group-wide .btn-group .form-check .btn{
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-wide .btn-group .form-check:first-child > .btn{
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-wide .btn-group .form-check:last-child > .btn{
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#app-sidebar{
|
||||||
|
border-right: 2px solid var(--primary);
|
||||||
|
}
|
Loading…
Reference in New Issue