deepboard 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepboard/__init__.py +1 -0
- deepboard/__version__.py +4 -0
- deepboard/gui/THEME.yml +28 -0
- deepboard/gui/__init__.py +0 -0
- deepboard/gui/assets/artefacts.css +108 -0
- deepboard/gui/assets/base.css +208 -0
- deepboard/gui/assets/base.js +77 -0
- deepboard/gui/assets/charts.css +188 -0
- deepboard/gui/assets/compare.css +90 -0
- deepboard/gui/assets/datagrid.css +120 -0
- deepboard/gui/assets/fileview.css +13 -0
- deepboard/gui/assets/right_panel.css +227 -0
- deepboard/gui/assets/theme.css +85 -0
- deepboard/gui/components/__init__.py +8 -0
- deepboard/gui/components/artefact_group.py +12 -0
- deepboard/gui/components/chart_type.py +22 -0
- deepboard/gui/components/legend.py +34 -0
- deepboard/gui/components/log_selector.py +22 -0
- deepboard/gui/components/modal.py +20 -0
- deepboard/gui/components/smoother.py +21 -0
- deepboard/gui/components/split_selector.py +21 -0
- deepboard/gui/components/stat_line.py +8 -0
- deepboard/gui/entry.py +21 -0
- deepboard/gui/main.py +93 -0
- deepboard/gui/pages/__init__.py +1 -0
- deepboard/gui/pages/compare_page/__init__.py +6 -0
- deepboard/gui/pages/compare_page/compare_page.py +22 -0
- deepboard/gui/pages/compare_page/components/__init__.py +4 -0
- deepboard/gui/pages/compare_page/components/card_list.py +19 -0
- deepboard/gui/pages/compare_page/components/chart.py +54 -0
- deepboard/gui/pages/compare_page/components/compare_setup.py +30 -0
- deepboard/gui/pages/compare_page/components/split_card.py +51 -0
- deepboard/gui/pages/compare_page/components/utils.py +20 -0
- deepboard/gui/pages/compare_page/routes.py +58 -0
- deepboard/gui/pages/main_page/__init__.py +4 -0
- deepboard/gui/pages/main_page/datagrid/__init__.py +5 -0
- deepboard/gui/pages/main_page/datagrid/compare_button.py +21 -0
- deepboard/gui/pages/main_page/datagrid/datagrid.py +67 -0
- deepboard/gui/pages/main_page/datagrid/handlers.py +54 -0
- deepboard/gui/pages/main_page/datagrid/header.py +43 -0
- deepboard/gui/pages/main_page/datagrid/routes.py +112 -0
- deepboard/gui/pages/main_page/datagrid/row.py +20 -0
- deepboard/gui/pages/main_page/datagrid/sortable_column_js.py +45 -0
- deepboard/gui/pages/main_page/datagrid/utils.py +9 -0
- deepboard/gui/pages/main_page/handlers.py +16 -0
- deepboard/gui/pages/main_page/main_page.py +21 -0
- deepboard/gui/pages/main_page/right_panel/__init__.py +12 -0
- deepboard/gui/pages/main_page/right_panel/config.py +57 -0
- deepboard/gui/pages/main_page/right_panel/fragments.py +133 -0
- deepboard/gui/pages/main_page/right_panel/hparams.py +25 -0
- deepboard/gui/pages/main_page/right_panel/images.py +358 -0
- deepboard/gui/pages/main_page/right_panel/run_info.py +86 -0
- deepboard/gui/pages/main_page/right_panel/scalars.py +251 -0
- deepboard/gui/pages/main_page/right_panel/template.py +151 -0
- deepboard/gui/pages/main_page/routes.py +25 -0
- deepboard/gui/pages/not_found.py +3 -0
- deepboard/gui/requirements.txt +5 -0
- deepboard/gui/utils.py +267 -0
- deepboard/resultTable/__init__.py +2 -0
- deepboard/resultTable/cursor.py +20 -0
- deepboard/resultTable/logwritter.py +667 -0
- deepboard/resultTable/resultTable.py +529 -0
- deepboard/resultTable/scalar.py +29 -0
- deepboard/resultTable/table_schema.py +135 -0
- deepboard/resultTable/utils.py +50 -0
- deepboard-0.2.0.dist-info/METADATA +164 -0
- deepboard-0.2.0.dist-info/RECORD +69 -0
- deepboard-0.2.0.dist-info/WHEEL +4 -0
- deepboard-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
from fasthtml.common import *
|
2
|
+
from datetime import datetime
|
3
|
+
from .scalars import ScalarTab, scalar_enable
|
4
|
+
from .images import ImageTab, images_enable
|
5
|
+
from .fragments import FragmentTab, fragment_enable
|
6
|
+
from .config import ConfigView
|
7
|
+
from .hparams import HParamsView
|
8
|
+
from .run_info import InfoView
|
9
|
+
|
10
|
+
def reset_scalar_session(session):
|
11
|
+
session["scalars"] = dict(
|
12
|
+
hidden_lines=[],
|
13
|
+
smoother_value=1,
|
14
|
+
chart_type='step'
|
15
|
+
)
|
16
|
+
|
17
|
+
def RightPanelContent(session, run_id: int, active_tab: str):
|
18
|
+
from __main__ import rTable
|
19
|
+
scalar_is_available = scalar_enable(run_id)
|
20
|
+
images_is_available = images_enable(run_id, type="IMAGES")
|
21
|
+
plot_is_available = images_enable(run_id, type="PLOT")
|
22
|
+
text_is_available = fragment_enable(run_id, type="RAW")
|
23
|
+
fragment_is_available = fragment_enable(run_id, type="HTML")
|
24
|
+
|
25
|
+
|
26
|
+
# Verify that the active tab requested is availalable
|
27
|
+
if active_tab == 'scalars' and not scalar_is_available:
|
28
|
+
active_tab = "images"
|
29
|
+
if active_tab == "images" and not images_is_available:
|
30
|
+
active_tab = "figures"
|
31
|
+
if active_tab == "figures" and not plot_is_available:
|
32
|
+
active_tab = "text"
|
33
|
+
if active_tab == "text" and not text_is_available:
|
34
|
+
active_tab = "fragments"
|
35
|
+
if active_tab == "fragments" and not fragment_is_available:
|
36
|
+
active_tab = "config"
|
37
|
+
|
38
|
+
if active_tab == 'scalars':
|
39
|
+
tab_content = ScalarTab(session, run_id)
|
40
|
+
elif active_tab == 'images':
|
41
|
+
tab_content = ImageTab(session, run_id, type="IMAGE")
|
42
|
+
elif active_tab == 'figures':
|
43
|
+
tab_content = ImageTab(session, run_id, type="PLOT")
|
44
|
+
elif active_tab == 'text':
|
45
|
+
tab_content = FragmentTab(session, run_id, type="RAW")
|
46
|
+
elif active_tab == 'fragments':
|
47
|
+
tab_content = FragmentTab(session, run_id, type="HTML")
|
48
|
+
elif active_tab == 'config':
|
49
|
+
tab_content = ConfigView(run_id)
|
50
|
+
elif active_tab == 'hparams':
|
51
|
+
tab_content = HParamsView(run_id)
|
52
|
+
elif active_tab == 'run_info':
|
53
|
+
tab_content = InfoView(run_id)
|
54
|
+
else:
|
55
|
+
tab_content = Div(
|
56
|
+
P("Invalid tab selected.", cls="error-message")
|
57
|
+
)
|
58
|
+
run_name = "DEBUG" if run_id == -1 else run_id
|
59
|
+
tabs = []
|
60
|
+
|
61
|
+
# If the runID does not have any scalars logged, we do not show the scalars tab
|
62
|
+
if scalar_is_available:
|
63
|
+
tabs.append(
|
64
|
+
Div('Scalars', cls='tab active' if active_tab == 'scalars' else 'tab',
|
65
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=scalars', hx_target='#right-panel-content',
|
66
|
+
hx_swap='outerHTML')
|
67
|
+
)
|
68
|
+
|
69
|
+
if images_is_available:
|
70
|
+
tabs.append(
|
71
|
+
Div('Images', cls='tab active' if active_tab == 'images' else 'tab',
|
72
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=images', hx_target='#right-panel-content',
|
73
|
+
hx_swap='outerHTML')
|
74
|
+
)
|
75
|
+
|
76
|
+
if plot_is_available:
|
77
|
+
tabs.append(
|
78
|
+
Div('Figures', cls='tab active' if active_tab == 'figures' else 'tab',
|
79
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=figures', hx_target='#right-panel-content',
|
80
|
+
hx_swap='outerHTML')
|
81
|
+
)
|
82
|
+
|
83
|
+
if text_is_available:
|
84
|
+
tabs.append(
|
85
|
+
Div('Text', cls='tab active' if active_tab == 'text' else 'tab',
|
86
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=text', hx_target='#right-panel-content',
|
87
|
+
hx_swap='outerHTML')
|
88
|
+
)
|
89
|
+
|
90
|
+
if fragment_is_available:
|
91
|
+
tabs.append(
|
92
|
+
Div('Fragments', cls='tab active' if active_tab == 'fragments' else 'tab',
|
93
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=fragments', hx_target='#right-panel-content',
|
94
|
+
hx_swap='outerHTML')
|
95
|
+
)
|
96
|
+
|
97
|
+
tabs += [
|
98
|
+
Div('Config', cls='tab active' if active_tab == 'config' else 'tab',
|
99
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=config', hx_target='#right-panel-content', hx_swap='outerHTML'),
|
100
|
+
Div('HParams', cls='tab active' if active_tab == 'hparams' else 'tab',
|
101
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=hparams', hx_target='#right-panel-content', hx_swap='outerHTML'),
|
102
|
+
Div('Info', cls='tab active' if active_tab == 'run_info' else 'tab',
|
103
|
+
hx_get=f'/fillpanel?run_id={run_id}&tab=run_info', hx_target='#right-panel-content',
|
104
|
+
hx_swap='outerHTML'),
|
105
|
+
]
|
106
|
+
return Div(
|
107
|
+
H1(f"Run: {run_name}"),
|
108
|
+
Div(
|
109
|
+
*tabs,
|
110
|
+
cls='tab-menu'
|
111
|
+
),
|
112
|
+
Div(
|
113
|
+
tab_content,
|
114
|
+
id='tab-content', cls='tab-content'
|
115
|
+
),
|
116
|
+
cls="right-panel-content",
|
117
|
+
id="right-panel-content"
|
118
|
+
),
|
119
|
+
|
120
|
+
def OpenPanel(session, run_id: int, active_tab: str = 'scalars'):
|
121
|
+
return Div(
|
122
|
+
RightPanelContent(session, run_id, active_tab),
|
123
|
+
cls="open-right-panel"
|
124
|
+
)
|
125
|
+
|
126
|
+
def RightPanel(session, closed: bool = False):
|
127
|
+
placeholder_text = [
|
128
|
+
P("⌘ / ctrl + click to compare runs", cls="right-panel-placeholder"),
|
129
|
+
P("'F' for fullscreen", cls="right-panel-placeholder")
|
130
|
+
]
|
131
|
+
if "datagrid" in session and session["datagrid"].get("selected-rows") and len(session["datagrid"]["selected-rows"]) == 1:
|
132
|
+
run_id = session["datagrid"]["selected-rows"][0]
|
133
|
+
else:
|
134
|
+
run_id = None
|
135
|
+
return Div(
|
136
|
+
Button(
|
137
|
+
I(cls="fas fa-times"),
|
138
|
+
hx_get="/reset",
|
139
|
+
hx_target="#container",
|
140
|
+
hx_swap="outerHTML",
|
141
|
+
cls="close-button",
|
142
|
+
) if run_id is not None else None,
|
143
|
+
Div(*placeholder_text) if run_id is None else OpenPanel(session, run_id),
|
144
|
+
id='right-panel',
|
145
|
+
cls="right-panel" if not closed else "right-panel-closed",
|
146
|
+
hx_swap_oob='true'
|
147
|
+
),
|
148
|
+
|
149
|
+
|
150
|
+
def fill_panel(session, run_id: int, tab: str):
|
151
|
+
return RightPanelContent(session, run_id, tab)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from .handlers import click_row_handler
|
2
|
+
from .datagrid import build_datagrid_endpoints
|
3
|
+
from .right_panel import build_right_panel_routes
|
4
|
+
from .main_page import MainPage
|
5
|
+
from deepboard.gui.components import Modal
|
6
|
+
|
7
|
+
def build_main_page_endpoints(rt):
|
8
|
+
rt("/click_row")(click_row_handler)
|
9
|
+
rt("/reset")(reset)
|
10
|
+
rt("/fullscreen")(fullscreen)
|
11
|
+
rt("/close_modal")(close_modal)
|
12
|
+
build_datagrid_endpoints(rt)
|
13
|
+
build_right_panel_routes(rt)
|
14
|
+
|
15
|
+
def reset(session):
|
16
|
+
if "show_hidden" not in session:
|
17
|
+
session["show_hidden"] = False
|
18
|
+
session["datagrid"] = dict()
|
19
|
+
return MainPage(session, swap=True)
|
20
|
+
|
21
|
+
def close_modal(session):
|
22
|
+
return Modal(active=False)
|
23
|
+
|
24
|
+
def fullscreen(session, full: bool):
|
25
|
+
return MainPage(session, swap=True, fullscreen=full)
|
deepboard/gui/utils.py
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
from datetime import datetime, date, timedelta
|
2
|
+
import sqlite3
|
3
|
+
from typing import *
|
4
|
+
import pandas as pd
|
5
|
+
import plotly.graph_objects as go
|
6
|
+
from decimal import Decimal, ROUND_HALF_UP
|
7
|
+
import yaml
|
8
|
+
import os
|
9
|
+
import shutil
|
10
|
+
import argparse
|
11
|
+
import math
|
12
|
+
|
13
|
+
def _adapt_date_iso(val):
|
14
|
+
"""Adapt datetime.date to ISO 8601 date."""
|
15
|
+
return val.isoformat()
|
16
|
+
|
17
|
+
|
18
|
+
def _adapt_datetime_iso(val):
|
19
|
+
"""Adapt datetime.datetime to timezone-naive ISO 8601 date."""
|
20
|
+
return val.isoformat()
|
21
|
+
|
22
|
+
def _convert_date(val):
|
23
|
+
"""Convert ISO 8601 date to datetime.date object."""
|
24
|
+
return date.fromisoformat(val.decode())
|
25
|
+
|
26
|
+
|
27
|
+
def _convert_datetime(val):
|
28
|
+
"""Convert ISO 8601 datetime to datetime.datetime object."""
|
29
|
+
return datetime.fromisoformat(val.decode())
|
30
|
+
|
31
|
+
def prepare_db():
|
32
|
+
|
33
|
+
sqlite3.register_adapter(datetime.date, _adapt_date_iso)
|
34
|
+
sqlite3.register_adapter(datetime, _adapt_datetime_iso)
|
35
|
+
|
36
|
+
|
37
|
+
sqlite3.register_converter("date", _convert_date)
|
38
|
+
sqlite3.register_converter("datetime", _convert_datetime)
|
39
|
+
|
40
|
+
def make_df(socket, tag: Tuple[str, str]) -> Tuple[pd.DataFrame, List[int], List[int]]:
|
41
|
+
scalars = socket.read_scalar("/".join(tag))
|
42
|
+
steps = [scalar.step for scalar in scalars]
|
43
|
+
value = [scalar.value for scalar in scalars]
|
44
|
+
repetition = [scalar.run_rep for scalar in scalars]
|
45
|
+
walltime = [scalar.wall_time for scalar in scalars]
|
46
|
+
epochs = [scalar.epoch for scalar in scalars]
|
47
|
+
df = pd.DataFrame({
|
48
|
+
"step": steps,
|
49
|
+
"value": value,
|
50
|
+
"repetition": repetition,
|
51
|
+
"duration": walltime,
|
52
|
+
"epoch": epochs
|
53
|
+
})
|
54
|
+
available_rep = df["repetition"].unique()
|
55
|
+
available_epochs = df["epoch"].unique()
|
56
|
+
df = df.set_index(["step", "repetition"])
|
57
|
+
return df, available_rep, available_epochs
|
58
|
+
|
59
|
+
def get_lines(socket, split, metric, key: Literal["step", "duration"]):
|
60
|
+
out = []
|
61
|
+
df, available_rep, available_epochs = make_df(socket, (split, metric))
|
62
|
+
for rep in available_rep:
|
63
|
+
rep_df = df.loc[(slice(None), rep), :]
|
64
|
+
out.append({
|
65
|
+
"index": rep_df.index.get_level_values("step") if key == "step" else rep_df["duration"],
|
66
|
+
"value": rep_df["value"],
|
67
|
+
"epoch": rep_df["epoch"].values if len(available_epochs) > 1 else None,
|
68
|
+
})
|
69
|
+
return out
|
70
|
+
|
71
|
+
def ema(values, alpha):
|
72
|
+
"""
|
73
|
+
Compute the Exponential Moving Average (EMA) of a list of values.
|
74
|
+
|
75
|
+
Parameters:
|
76
|
+
- values (list or numpy array): The data series.
|
77
|
+
- alpha (float): Smoothing factor (between 0 and 1).
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
- list: EMA-smoothed values.
|
81
|
+
"""
|
82
|
+
return values.ewm(alpha=alpha, adjust=False).mean()
|
83
|
+
|
84
|
+
def make_fig(lines, type: str = "step", smoothness: float = 0., log_scale: bool = False):
|
85
|
+
from __main__ import CONFIG
|
86
|
+
fig = go.Figure()
|
87
|
+
|
88
|
+
for label, steps, values, color, epochs in lines:
|
89
|
+
# Smooth the values
|
90
|
+
if smoothness > 0:
|
91
|
+
values = ema(values, 1.01 - smoothness / 100)
|
92
|
+
if epochs is not None:
|
93
|
+
additional_setup = dict(hovertext=values, customdata=[[e, label] for e in epochs],
|
94
|
+
hovertemplate="%{customdata[1]} : %{y:.4f} | Epoch: %{customdata[0]}<extra></extra>")
|
95
|
+
else:
|
96
|
+
additional_setup = dict(hovertext=values, customdata=[[label] for _ in values],
|
97
|
+
hovertemplate="%{customdata[0]} : %{y:.4f}<extra></extra>")
|
98
|
+
|
99
|
+
if type == "time":
|
100
|
+
steps = [datetime(1970, 1, 1) + timedelta(seconds=s) for s in steps]
|
101
|
+
fig.add_trace(go.Scatter(
|
102
|
+
x=steps,
|
103
|
+
y=values,
|
104
|
+
mode='lines',
|
105
|
+
name=label,
|
106
|
+
line=dict(color=color),
|
107
|
+
**additional_setup
|
108
|
+
))
|
109
|
+
if log_scale:
|
110
|
+
fig.update_yaxes(type="log")
|
111
|
+
|
112
|
+
if type == "step":
|
113
|
+
fig.update_layout(
|
114
|
+
CONFIG.PLOTLY_THEME,
|
115
|
+
xaxis_title="Step",
|
116
|
+
yaxis_title="Value",
|
117
|
+
hovermode="x unified",
|
118
|
+
showlegend=False,
|
119
|
+
autosize=True,
|
120
|
+
height=None, # Let CSS control it
|
121
|
+
width=None, # Let CSS control it
|
122
|
+
margin=dict(l=0, r=0, t=15, b=0)
|
123
|
+
)
|
124
|
+
elif type == "time":
|
125
|
+
fig.update_layout(
|
126
|
+
CONFIG.PLOTLY_THEME,
|
127
|
+
xaxis_title="Duration",
|
128
|
+
yaxis_title="Value",
|
129
|
+
hovermode="x unified",
|
130
|
+
showlegend=False,
|
131
|
+
xaxis_tickformat="%H:%M:%S", # format the ticks like 01:23:45
|
132
|
+
autosize=True,
|
133
|
+
height=None, # Let CSS control it
|
134
|
+
width=None, # Let CSS control it
|
135
|
+
margin=dict(l=0, r=0, t=15, b=0)
|
136
|
+
)
|
137
|
+
else:
|
138
|
+
raise ValueError(f"Unknown plotting type: {type}")
|
139
|
+
return fig
|
140
|
+
|
141
|
+
|
142
|
+
def smart_round(val, decimals=4):
|
143
|
+
"""
|
144
|
+
Round a float to the given number of decimal places. However, if the float already has less decimal, noting is done!
|
145
|
+
In addition, if the value is about to get rounded to zero, it is converted to scientific notation.
|
146
|
+
:param val: The value to round.
|
147
|
+
:param decimals: The maximum number of decimal places to round.
|
148
|
+
:return: The rounded value.
|
149
|
+
"""
|
150
|
+
# If infinity or NaN, return as is
|
151
|
+
if isinstance(val, float) and (math.isinf(val) or math.isnan(val)):
|
152
|
+
return str(val)
|
153
|
+
val_dec = Decimal(str(val))
|
154
|
+
quantizer = Decimal('1').scaleb(-decimals)
|
155
|
+
rounded = val_dec.quantize(quantizer, rounding=ROUND_HALF_UP)
|
156
|
+
if rounded.is_zero() and not val_dec.is_zero():
|
157
|
+
return f"{val:.{decimals}e}"
|
158
|
+
return str(rounded.normalize())
|
159
|
+
|
160
|
+
def initiate_files():
|
161
|
+
abs_path = os.path.abspath(os.path.dirname(__file__))
|
162
|
+
if not os.path.exists(os.path.expanduser('~/.config/deepboard')):
|
163
|
+
os.makedirs(os.path.expanduser('~/.config/deepboard'))
|
164
|
+
|
165
|
+
if not os.path.exists(os.path.expanduser('~/.config/deepboard/THEME.yml')):
|
166
|
+
shutil.copy(f"{abs_path}/THEME.yml", os.path.expanduser('~/.config/deepboard/THEME.yml'))
|
167
|
+
|
168
|
+
if not os.path.exists(os.path.expanduser('~/.config/deepboard/THEME.css')):
|
169
|
+
shutil.copy(f"{abs_path}/assets/theme.css", os.path.expanduser('~/.config/deepboard/THEME.css'))
|
170
|
+
|
171
|
+
def get_table_path_from_cli(default: str = "results/result_table.db") -> str:
|
172
|
+
# Parse cli args
|
173
|
+
parser = argparse.ArgumentParser(description="DeepBoard WebUI")
|
174
|
+
# Positional
|
175
|
+
parser.add_argument(
|
176
|
+
"table_path",
|
177
|
+
nargs="?", # Make it optional positionally
|
178
|
+
help="Path to the result table db file"
|
179
|
+
)
|
180
|
+
|
181
|
+
# Optional keyword argument --table_path
|
182
|
+
parser.add_argument(
|
183
|
+
"--table_path",
|
184
|
+
help="Path to the result table db file"
|
185
|
+
)
|
186
|
+
args = parser.parse_args()
|
187
|
+
|
188
|
+
# Resolve to either positional or optional
|
189
|
+
table_path = args.table_path or args.__dict__.get("table_path")
|
190
|
+
|
191
|
+
if table_path is None:
|
192
|
+
table_path = default
|
193
|
+
|
194
|
+
return table_path
|
195
|
+
|
196
|
+
def verify_runids(session: dict, rTable):
|
197
|
+
"""
|
198
|
+
Verify that the runIDs in the session are valid and exist in the result table. If not, remove them from the session.
|
199
|
+
:param session: The fastHTML session dictionary.
|
200
|
+
:param rTable: The result table object.
|
201
|
+
:return: Modify inplace so it returns nothing.
|
202
|
+
"""
|
203
|
+
if "datagrid" in session and session["datagrid"].get("selected-rows"):
|
204
|
+
valid_run_ids = []
|
205
|
+
runs = set(rTable.runs)
|
206
|
+
for run_id in session["datagrid"]["selected-rows"]:
|
207
|
+
if run_id in runs:
|
208
|
+
valid_run_ids.append(run_id)
|
209
|
+
|
210
|
+
# Set it back
|
211
|
+
session["datagrid"]["selected-rows"] = valid_run_ids
|
212
|
+
class Config:
|
213
|
+
COLORS = [
|
214
|
+
"#1f77b4", # muted blue
|
215
|
+
"#ff7f0e", # vivid orange
|
216
|
+
"#2ca02c", # medium green
|
217
|
+
"#d62728", # brick red
|
218
|
+
"#9467bd", # muted purple
|
219
|
+
"#8c564b", # brownish pink
|
220
|
+
"#e377c2", # pink
|
221
|
+
"#7f7f7f", # gray
|
222
|
+
"#bcbd22", # lime yellow
|
223
|
+
"#17becf", # cyan
|
224
|
+
]
|
225
|
+
HIDDEN_COLOR = "#333333" # gray for hidden lines
|
226
|
+
|
227
|
+
PLOTLY_THEME = dict(
|
228
|
+
plot_bgcolor='#111111', # dark background for the plotting area
|
229
|
+
paper_bgcolor='#111111', # dark background for the full figure
|
230
|
+
font=dict(color='white'), # white text everywhere (axes, legend, etc.)
|
231
|
+
xaxis=dict(
|
232
|
+
gridcolor='#333333', # subtle dark grid lines
|
233
|
+
zerolinecolor='#333333'
|
234
|
+
),
|
235
|
+
yaxis=dict(
|
236
|
+
gridcolor='#333333',
|
237
|
+
zerolinecolor='#333333'
|
238
|
+
),
|
239
|
+
)
|
240
|
+
|
241
|
+
MAX_DEC = 4 # Maximum number of decimals
|
242
|
+
|
243
|
+
@classmethod
|
244
|
+
def FromFile(cls, path: str):
|
245
|
+
self = cls()
|
246
|
+
if os.path.exists(path):
|
247
|
+
with open(path, 'r') as f:
|
248
|
+
data = yaml.load(f, Loader=yaml.FullLoader)
|
249
|
+
self._update_theme(data)
|
250
|
+
else:
|
251
|
+
print("NO theme found, using default theme")
|
252
|
+
return self
|
253
|
+
|
254
|
+
def _update_theme(self, data: dict):
|
255
|
+
for key, value in data.items():
|
256
|
+
if not isinstance(value, dict):
|
257
|
+
setattr(self, key, value)
|
258
|
+
else:
|
259
|
+
self._merge_dict(getattr(self, key), value)
|
260
|
+
|
261
|
+
def _merge_dict(self, d, u):
|
262
|
+
for k, v in u.items():
|
263
|
+
if isinstance(v, dict) and isinstance(d.get(k), dict):
|
264
|
+
self._merge_dict(d[k], v)
|
265
|
+
else:
|
266
|
+
d[k] = v
|
267
|
+
return d
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import sqlite3
|
2
|
+
|
3
|
+
class Cursor:
|
4
|
+
def __init__(self, db_path: str, format_as_dict: bool = False):
|
5
|
+
self.db_path = db_path
|
6
|
+
self.format_as_dict = format_as_dict
|
7
|
+
|
8
|
+
def __enter__(self):
|
9
|
+
self._conn = sqlite3.connect(self.db_path)
|
10
|
+
if self.format_as_dict:
|
11
|
+
self._conn.row_factory = sqlite3.Row
|
12
|
+
self._cursor = self._conn.cursor()
|
13
|
+
return self._cursor
|
14
|
+
|
15
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
16
|
+
"""Commits changes if no exception occurred, then closes the connection."""
|
17
|
+
if self._conn:
|
18
|
+
if exc_type is None: # No exceptions, commit changes
|
19
|
+
self._conn.commit()
|
20
|
+
self._conn.close()
|