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.
Files changed (69) hide show
  1. deepboard/__init__.py +1 -0
  2. deepboard/__version__.py +4 -0
  3. deepboard/gui/THEME.yml +28 -0
  4. deepboard/gui/__init__.py +0 -0
  5. deepboard/gui/assets/artefacts.css +108 -0
  6. deepboard/gui/assets/base.css +208 -0
  7. deepboard/gui/assets/base.js +77 -0
  8. deepboard/gui/assets/charts.css +188 -0
  9. deepboard/gui/assets/compare.css +90 -0
  10. deepboard/gui/assets/datagrid.css +120 -0
  11. deepboard/gui/assets/fileview.css +13 -0
  12. deepboard/gui/assets/right_panel.css +227 -0
  13. deepboard/gui/assets/theme.css +85 -0
  14. deepboard/gui/components/__init__.py +8 -0
  15. deepboard/gui/components/artefact_group.py +12 -0
  16. deepboard/gui/components/chart_type.py +22 -0
  17. deepboard/gui/components/legend.py +34 -0
  18. deepboard/gui/components/log_selector.py +22 -0
  19. deepboard/gui/components/modal.py +20 -0
  20. deepboard/gui/components/smoother.py +21 -0
  21. deepboard/gui/components/split_selector.py +21 -0
  22. deepboard/gui/components/stat_line.py +8 -0
  23. deepboard/gui/entry.py +21 -0
  24. deepboard/gui/main.py +93 -0
  25. deepboard/gui/pages/__init__.py +1 -0
  26. deepboard/gui/pages/compare_page/__init__.py +6 -0
  27. deepboard/gui/pages/compare_page/compare_page.py +22 -0
  28. deepboard/gui/pages/compare_page/components/__init__.py +4 -0
  29. deepboard/gui/pages/compare_page/components/card_list.py +19 -0
  30. deepboard/gui/pages/compare_page/components/chart.py +54 -0
  31. deepboard/gui/pages/compare_page/components/compare_setup.py +30 -0
  32. deepboard/gui/pages/compare_page/components/split_card.py +51 -0
  33. deepboard/gui/pages/compare_page/components/utils.py +20 -0
  34. deepboard/gui/pages/compare_page/routes.py +58 -0
  35. deepboard/gui/pages/main_page/__init__.py +4 -0
  36. deepboard/gui/pages/main_page/datagrid/__init__.py +5 -0
  37. deepboard/gui/pages/main_page/datagrid/compare_button.py +21 -0
  38. deepboard/gui/pages/main_page/datagrid/datagrid.py +67 -0
  39. deepboard/gui/pages/main_page/datagrid/handlers.py +54 -0
  40. deepboard/gui/pages/main_page/datagrid/header.py +43 -0
  41. deepboard/gui/pages/main_page/datagrid/routes.py +112 -0
  42. deepboard/gui/pages/main_page/datagrid/row.py +20 -0
  43. deepboard/gui/pages/main_page/datagrid/sortable_column_js.py +45 -0
  44. deepboard/gui/pages/main_page/datagrid/utils.py +9 -0
  45. deepboard/gui/pages/main_page/handlers.py +16 -0
  46. deepboard/gui/pages/main_page/main_page.py +21 -0
  47. deepboard/gui/pages/main_page/right_panel/__init__.py +12 -0
  48. deepboard/gui/pages/main_page/right_panel/config.py +57 -0
  49. deepboard/gui/pages/main_page/right_panel/fragments.py +133 -0
  50. deepboard/gui/pages/main_page/right_panel/hparams.py +25 -0
  51. deepboard/gui/pages/main_page/right_panel/images.py +358 -0
  52. deepboard/gui/pages/main_page/right_panel/run_info.py +86 -0
  53. deepboard/gui/pages/main_page/right_panel/scalars.py +251 -0
  54. deepboard/gui/pages/main_page/right_panel/template.py +151 -0
  55. deepboard/gui/pages/main_page/routes.py +25 -0
  56. deepboard/gui/pages/not_found.py +3 -0
  57. deepboard/gui/requirements.txt +5 -0
  58. deepboard/gui/utils.py +267 -0
  59. deepboard/resultTable/__init__.py +2 -0
  60. deepboard/resultTable/cursor.py +20 -0
  61. deepboard/resultTable/logwritter.py +667 -0
  62. deepboard/resultTable/resultTable.py +529 -0
  63. deepboard/resultTable/scalar.py +29 -0
  64. deepboard/resultTable/table_schema.py +135 -0
  65. deepboard/resultTable/utils.py +50 -0
  66. deepboard-0.2.0.dist-info/METADATA +164 -0
  67. deepboard-0.2.0.dist-info/RECORD +69 -0
  68. deepboard-0.2.0.dist-info/WHEEL +4 -0
  69. deepboard-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,358 @@
1
+ from fasthtml.common import *
2
+ from starlette.responses import Response
3
+ from typing import *
4
+ from deepboard.gui.components import Modal, SplitSelector, StatLine, ArtefactGroup
5
+
6
+ def _get_images_groups(socket, type: Literal["IMAGE", "PLOT"]):
7
+ if type == "IMAGE":
8
+ images = socket.read_images()
9
+ else:
10
+ images = socket.read_figures()
11
+
12
+ index = list({(img["step"], img["epoch"], img["run_rep"], img["split"]) for img in images})
13
+
14
+ splits = list({elem[3] for elem in index})
15
+
16
+ # Package images
17
+ images_groups = {}
18
+ for key in index:
19
+ cropped_key = key[:-1] # Remove split
20
+ if cropped_key not in images_groups:
21
+ images_groups[cropped_key] = {split: [] for split in splits}
22
+
23
+ for img in images:
24
+ key = img["step"], img["epoch"], img["run_rep"]
25
+ split = img["split"]
26
+ images_groups[key][split].append(img["id"])
27
+
28
+ # Sort image groups by step, and run_rep
29
+ return dict(sorted(images_groups.items(), key=lambda x: (x[0][0], x[0][2])))
30
+
31
+
32
+ def ImageComponent(image_id: int):
33
+ """
34
+ Create a single image component with a specific style.
35
+ :param image: PIL Image object.
36
+ :return: Div containing the image.
37
+ """
38
+ return Div(
39
+ A(
40
+ Img(src=f"/images/id={image_id}", alt="Image"),
41
+ hx_get=f"/images/open_modal?id={image_id}",
42
+ hx_target="#modal",
43
+ hx_swap="outerHTML",
44
+ style='cursor: pointer;',
45
+ ),
46
+ cls="image",
47
+ )
48
+
49
+ def InteractiveImage(image_id: int):
50
+ return Div(
51
+ Script(
52
+ """
53
+ // For image zoom and pan in modal
54
+ var scale = 1;
55
+ var translateX = 0;
56
+ var translateY = 0;
57
+ var zoomableDiv = document.getElementById('zoomableDiv');
58
+ var container = document.querySelector('.interactive-image-container');
59
+
60
+ // Mouse/touch state
61
+ var isDragging = false;
62
+ var lastX = 0;
63
+ var lastY = 0;
64
+
65
+ // Apply transform with both scale and translate
66
+ function applyTransform() {
67
+ zoomableDiv.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`;
68
+ }
69
+
70
+ // Mouse wheel and trackpad zoom
71
+ var lastWheelTime = 0;
72
+ var wheelAccumulator = 0;
73
+
74
+ container.addEventListener('wheel', (e) => {
75
+ e.preventDefault();
76
+
77
+ const currentTime = Date.now();
78
+ const timeDelta = currentTime - lastWheelTime;
79
+
80
+ // Detect if this is likely a Mac trackpad (rapid events with ctrlKey)
81
+ const isMacTrackpad = e.ctrlKey || (timeDelta < 50 && Math.abs(e.deltaY) > 10);
82
+
83
+ let zoomFactor;
84
+ if (isMacTrackpad) {
85
+ // For Mac trackpad: use smaller, more gradual changes
86
+ // Accumulate small changes for smoother zooming
87
+ wheelAccumulator += e.deltaY;
88
+
89
+ // Only apply zoom when accumulator reaches threshold
90
+ if (Math.abs(wheelAccumulator) > 20) {
91
+ zoomFactor = wheelAccumulator > 0 ? 0.95 : 1.05;
92
+ wheelAccumulator *= 0.7; // Reduce accumulator but don't reset completely
93
+ } else {
94
+ lastWheelTime = currentTime;
95
+ return; // Skip this event
96
+ }
97
+ } else {
98
+ // For regular mouse wheel: use normal zoom steps
99
+ zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
100
+ wheelAccumulator = 0; // Reset accumulator for mouse wheel
101
+ }
102
+
103
+ const newScale = Math.max(0.5, Math.min(3, scale * zoomFactor));
104
+
105
+ // Get cursor position relative to container
106
+ const rect = container.getBoundingClientRect();
107
+ const mouseX = e.clientX - rect.left - rect.width / 2;
108
+ const mouseY = e.clientY - rect.top - rect.height / 2;
109
+
110
+ // Adjust translation to zoom towards cursor position
111
+ const scaleRatio = newScale / scale;
112
+ translateX = mouseX * (1 - scaleRatio) + translateX * scaleRatio;
113
+ translateY = mouseY * (1 - scaleRatio) + translateY * scaleRatio;
114
+
115
+ scale = newScale;
116
+ lastWheelTime = currentTime;
117
+ applyTransform();
118
+ });
119
+
120
+ // Mouse drag for panning
121
+ container.addEventListener('mousedown', (e) => {
122
+ if (scale > 1) { // Only allow panning when zoomed
123
+ isDragging = true;
124
+ lastX = e.clientX;
125
+ lastY = e.clientY;
126
+ container.style.cursor = 'grabbing';
127
+ e.preventDefault();
128
+ }
129
+ });
130
+
131
+ document.addEventListener('mousemove', (e) => {
132
+ if (isDragging) {
133
+ const deltaX = e.clientX - lastX;
134
+ const deltaY = e.clientY - lastY;
135
+
136
+ translateX += deltaX / scale; // Adjust for current scale
137
+ translateY += deltaY / scale;
138
+
139
+ lastX = e.clientX;
140
+ lastY = e.clientY;
141
+
142
+ applyTransform();
143
+ e.preventDefault();
144
+ }
145
+ });
146
+
147
+ document.addEventListener('mouseup', () => {
148
+ if (isDragging) {
149
+ isDragging = false;
150
+ container.style.cursor = scale > 1 ? 'grab' : 'default';
151
+ }
152
+ });
153
+
154
+ // Touch zoom (pinch)
155
+ var initialDistance = 0;
156
+ var initialScale = 1;
157
+ var touchStartX = 0;
158
+ var touchStartY = 0;
159
+ var initialTranslateX = 0;
160
+ var initialTranslateY = 0;
161
+
162
+ container.addEventListener('touchstart', (e) => {
163
+ if (e.touches.length === 2) {
164
+ // Two finger pinch zoom
165
+ initialDistance = getDistance(e.touches[0], e.touches[1]);
166
+ initialScale = scale;
167
+ e.preventDefault();
168
+ } else if (e.touches.length === 1 && scale > 1) {
169
+ // Single finger pan when zoomed
170
+ isDragging = true;
171
+ touchStartX = e.touches[0].clientX;
172
+ touchStartY = e.touches[0].clientY;
173
+ initialTranslateX = translateX;
174
+ initialTranslateY = translateY;
175
+ e.preventDefault();
176
+ }
177
+ });
178
+
179
+ container.addEventListener('touchmove', (e) => {
180
+ if (e.touches.length === 2) {
181
+ // Handle pinch zoom
182
+ const currentDistance = getDistance(e.touches[0], e.touches[1]);
183
+ const ratio = currentDistance / initialDistance;
184
+ const dampedRatio = 1 + (ratio - 1) * 0.5; // 50% sensitivity
185
+ scale = Math.max(0.5, Math.min(3, initialScale * dampedRatio));
186
+ applyTransform();
187
+ e.preventDefault();
188
+ } else if (e.touches.length === 1 && isDragging) {
189
+ // Handle single finger pan
190
+ const deltaX = e.touches[0].clientX - touchStartX;
191
+ const deltaY = e.touches[0].clientY - touchStartY;
192
+
193
+ translateX = initialTranslateX + deltaX / scale;
194
+ translateY = initialTranslateY + deltaY / scale;
195
+
196
+ applyTransform();
197
+ e.preventDefault();
198
+ }
199
+ });
200
+
201
+ container.addEventListener('touchend', (e) => {
202
+ if (e.touches.length === 0) {
203
+ isDragging = false;
204
+ }
205
+ });
206
+
207
+ // Reset on double click/tap
208
+ container.addEventListener('dblclick', () => {
209
+ scale = 1;
210
+ translateX = 0;
211
+ translateY = 0;
212
+ applyTransform();
213
+ container.style.cursor = 'default';
214
+ });
215
+
216
+ // Update cursor based on zoom level
217
+ container.addEventListener('mouseenter', () => {
218
+ container.style.cursor = scale > 1 ? 'grab' : 'default';
219
+ });
220
+
221
+ function getDistance(touch1, touch2) {
222
+ const dx = touch1.clientX - touch2.clientX;
223
+ const dy = touch1.clientY - touch2.clientY;
224
+ return Math.sqrt(dx * dx + dy * dy);
225
+ }
226
+
227
+ // Prevent context menu on long press for mobile
228
+ container.addEventListener('contextmenu', (e) => {
229
+ e.preventDefault();
230
+ });
231
+ """
232
+ ),
233
+ Div(
234
+ Div(
235
+ Img(src=f"/images/id={image_id}", alt="Image"),
236
+ cls="interactive-image",
237
+ id="zoomableDiv",
238
+ style="transition: transform 0.1s ease-out;"
239
+ ),
240
+ cls="interactive-image-container",
241
+ style="overflow: hidden; position: relative; user-select: none; touch-action: none;"
242
+ )
243
+ )
244
+
245
+
246
+
247
+ def ImageCard(runID: int, step: int, epoch: Optional[int], run_rep: int, img_type: Literal["IMAGE", "PLOT"],
248
+ selected: Optional[str] = None):
249
+ from __main__ import rTable
250
+
251
+ socket = rTable.load_run(runID)
252
+ data = _get_images_groups(socket, type=img_type)
253
+
254
+ if (step, epoch, run_rep) not in data:
255
+ avail_splits = []
256
+ images = []
257
+ else:
258
+ images_splits = data[(step, epoch, run_rep)]
259
+ avail_splits = list(images_splits.keys())
260
+ avail_splits.sort()
261
+ if selected is None:
262
+ selected = avail_splits[0]
263
+ images = images_splits[selected]
264
+
265
+ return Div(
266
+ Div(
267
+ SplitSelector(runID, avail_splits, selected=selected, step=step, epoch=epoch, run_rep=run_rep,
268
+ type=img_type, path="/images/change_split"),
269
+ Div(
270
+ StatLine("Step", str(step)),
271
+ StatLine("Epoch", str(epoch) if epoch is not None else "N/A"),
272
+ StatLine("Run Repetition", str(run_rep)),
273
+ cls="artefact-stats-column"
274
+ ),
275
+ cls="artefact-card-header",
276
+ ),
277
+ ArtefactGroup(*[ImageComponent(image_id) for image_id in images]),
278
+ id=f"artefact-card-{step}-{epoch}-{run_rep}",
279
+ cls="artefact-card",
280
+ )
281
+
282
+ def ImageTab(session, runID, type: Literal["IMAGE", "PLOT"], swap: bool = False):
283
+ from __main__ import rTable
284
+ socket = rTable.load_run(runID)
285
+
286
+ images_groups = _get_images_groups(socket, type=type)
287
+ return Div(
288
+ *[
289
+ ImageCard(runID, step, epoch, run_rep, img_type=type)
290
+ for step, epoch, run_rep in images_groups.keys()
291
+ ],
292
+ style="display; flex; flex-direction: column; align-items: center; justify-content: center;",
293
+ id="images-tab",
294
+ hx_swap_oob="true" if swap else None,
295
+ )
296
+
297
+
298
+ def images_enable(runID, type: Literal["IMAGES", "PLOT"]):
299
+ """
300
+ Check if some scalars are logged and available for the runID. If not, we consider disable it.
301
+ :param runID: The runID to check.
302
+ :return: True if scalars are available, False otherwise.
303
+ """
304
+ from __main__ import rTable
305
+ socket = rTable.load_run(runID)
306
+ if type == "IMAGES":
307
+ return len(socket.read_images()) > 0
308
+ else:
309
+ return len(socket.read_figures()) > 0
310
+
311
+ # routes
312
+ def build_images_routes(rt):
313
+ rt("/images/change_split")(change_split)
314
+ rt("/images/id={image_id}")(load_image)
315
+ rt("/images/open_modal")(open_image_modal)
316
+
317
+
318
+ def change_split(session, runID: int, step: int, epoch: Optional[int], run_rep: int, split_select: str, type: str):
319
+ """
320
+ Change the split for the images.
321
+ :param session: The session object.
322
+ :param step: The step of the images.
323
+ :param epoch: The epoch of the images.
324
+ :param run_rep: The run repetition of the images.
325
+ :param split: The split to change to.
326
+ :param type: The type of split to change.
327
+ :return: The updated image card HTML.
328
+ """
329
+ return ImageCard(
330
+ runID,
331
+ step,
332
+ epoch,
333
+ run_rep,
334
+ img_type=type,
335
+ selected=split_select,
336
+ )
337
+
338
+ def load_image(image_id: int):
339
+ from __main__ import rTable
340
+ img = rTable.get_image_by_id(image_id)
341
+ if img is None:
342
+ return Response(f"Image not found with id: {image_id}:(", status_code=404)
343
+ img_buffer = io.BytesIO()
344
+ img.save(img_buffer, format='PNG')
345
+ img_buffer.seek(0)
346
+
347
+ return Response(
348
+ content=img_buffer.getvalue(),
349
+ media_type="image/png"
350
+ )
351
+
352
+ def open_image_modal(session, id: int):
353
+ return Modal(
354
+ InteractiveImage(
355
+ id
356
+ ),
357
+ active=True,
358
+ )
@@ -0,0 +1,86 @@
1
+ from typing import *
2
+ from datetime import datetime, timedelta
3
+ from fasthtml.common import *
4
+ from markupsafe import Markup
5
+
6
+
7
+ def CopyToClipboard(text: str, cls):
8
+ return Div(
9
+ Span(
10
+ I(cls=f'fas fa-copy copy-icon default-icon {cls}'),
11
+ I(cls=f'fas fa-check copy-icon check-icon {cls}'),
12
+ cls='copy-icon-container',
13
+ ),
14
+ Span(text, cls='copy-text' + ' ' + cls),
15
+ onclick='copyToClipboard(this)',
16
+ cls='copy-container'
17
+ )
18
+
19
+ def Status(runID: int, status: Literal["running", "finished", "failed"], swap: bool = False):
20
+ swap_oob = dict(swap_oob="true") if swap else {}
21
+ return Select(
22
+ Option("Running", value="running", selected=status == "running", cls="run-status-option running"),
23
+ Option("Finished", value="finished", selected=status == "finished", cls="run-status-option finished"),
24
+ Option("Failed", value="failed", selected=status == "failed", cls="run-status-option failed"),
25
+ id=f"runstatus-select",
26
+ name="run_status",
27
+ hx_get=f"/runinfo/change_status?runID={runID}",
28
+ hx_target="#runstatus-select",
29
+ hx_trigger="change",
30
+ hx_swap="outerHTML",
31
+ hx_params="*",
32
+ **swap_oob,
33
+ cls="run-status-select" + " " + status,
34
+ )
35
+
36
+ def DiffView(diff: Optional[str]):
37
+ diff_parts = diff.splitlines() if diff else [""]
38
+ dff = []
39
+ for part in diff_parts:
40
+ dff.append(P(Markup(part), cls="config-part"))
41
+ return Div(
42
+ H2("Diff"),
43
+ Div(
44
+ *dff,
45
+ cls="file-view",
46
+ )
47
+ )
48
+
49
+
50
+ def InfoView(runID: int):
51
+ from __main__ import rTable
52
+ # Cli
53
+ row = rTable.fetch_experiment(runID)
54
+ # RunID, Exp name, cfg, cfg hash, cli, command, comment, start, status, commit, diff
55
+ start: datetime = datetime.fromisoformat(row[7])
56
+ status = row[8]
57
+ commit = row[9]
58
+ diff = row[10]
59
+ return (Table(
60
+ Tr(
61
+ Td(H3("Start", cls="info-label")),
62
+ Td(H3(start.strftime("%Y-%m-%d %H:%M:%S"), cls="info-value")),
63
+ ),
64
+ Tr(
65
+ Td(H3("Status", cls="info-label")),
66
+ Td(Status(runID, status), cls="align-right"),
67
+ ),
68
+ Tr(
69
+ Td(H3("Commit", cls="info-label")),
70
+ Td(CopyToClipboard(commit, cls="info-value"), cls="align-right"),
71
+ ),
72
+ cls="info-table",
73
+ ),
74
+ DiffView(diff))
75
+
76
+
77
+
78
+ # Routes
79
+ def build_info_routes(rt):
80
+ rt("/runinfo/change_status")(change_status)
81
+
82
+ def change_status(session, runID: int, run_status: str):
83
+ from __main__ import rTable
84
+ socket = rTable.load_run(runID)
85
+ socket.set_status(run_status)
86
+ return Status(runID, run_status, swap=True)
@@ -0,0 +1,251 @@
1
+ import plotly.graph_objects as go
2
+ import pandas as pd
3
+ from typing import *
4
+ from datetime import datetime, timedelta
5
+ from fasthtml.common import *
6
+ from fh_plotly import plotly2fasthtml
7
+ from deepboard.gui.components import Legend, Smoother, ChartType, LogSelector
8
+ from deepboard.gui.utils import get_lines, make_fig
9
+
10
+ def make_step_lines(socket, splits: set[str], metric: str, keys: set[tuple[str, str]]):
11
+ from __main__ import CONFIG
12
+ lines = []
13
+ for i, split in enumerate(splits):
14
+ tag = (split, metric)
15
+ if tag in keys:
16
+ reps = get_lines(socket, split, metric, key="step")
17
+
18
+ if len(reps) > 1:
19
+ for rep_idx, rep in enumerate(reps):
20
+ lines.append((
21
+ f'{split}_{rep_idx}',
22
+ rep["index"],
23
+ rep["value"],
24
+ CONFIG.COLORS[i % len(CONFIG.COLORS)],
25
+ rep["epoch"],
26
+ ))
27
+ else:
28
+ lines.append((
29
+ f'{split}',
30
+ reps[0]["index"],
31
+ reps[0]["value"],
32
+ CONFIG.COLORS[i % len(CONFIG.COLORS)],
33
+ reps[0]["epoch"],
34
+ ))
35
+ return lines
36
+
37
+ def make_time_lines(socket, splits: set[str], metric: str, keys: set[tuple[str, str]]):
38
+ from __main__ import CONFIG
39
+ lines = []
40
+ for i, split in enumerate(splits):
41
+ tag = (split, metric)
42
+ if tag in keys:
43
+ reps = get_lines(socket, split, metric, key="duration")
44
+
45
+ if len(reps) > 1:
46
+ for rep_idx, rep in enumerate(reps):
47
+ lines.append((
48
+ f'{split}_{rep_idx}',
49
+ rep["index"],
50
+ rep["value"],
51
+ CONFIG.COLORS[i % len(CONFIG.COLORS)],
52
+ rep["epoch"],
53
+ ))
54
+ else:
55
+ lines.append((
56
+ f'{split}',
57
+ reps[0]["index"],
58
+ reps[0]["value"],
59
+ CONFIG.COLORS[i % len(CONFIG.COLORS)],
60
+ reps[0]["epoch"],
61
+ ))
62
+ return lines
63
+
64
+
65
+
66
+ def Setup(session, labels: list[tuple]):
67
+ return Div(
68
+ H1("Setup", cls="chart-scalar-title"),
69
+ Div(
70
+ Div(
71
+ Smoother(session, path = "/scalars", selected_rows_key="datagrid", session_path="scalars"),
72
+ ChartType(session, path = "/scalars", selected_rows_key="datagrid", session_path="scalars"),
73
+ LogSelector(session, path = "/scalars", selected_rows_key="datagrid", session_path="scalars"),
74
+ style="width: 100%; margin-right: 1em; display: flex; flex-direction: column; align-items: flex-start",
75
+ ),
76
+ Legend(session, labels, path = "/scalars", selected_rows_key="datagrid"),
77
+ cls="chart-setup-container",
78
+ ),
79
+ cls="chart-setup",
80
+ )
81
+ # Components
82
+ def Chart(session, runID: int, metric: str, type: str = "step", running: bool = False, logscale: bool = False):
83
+ from __main__ import rTable
84
+ socket = rTable.load_run(runID)
85
+ keys = socket.formatted_scalars
86
+ # metrics = {label for split, label in keys}
87
+ splits = {split for split, label in keys}
88
+ hidden_lines = session["scalars"]["hidden_lines"] if "hidden_lines" in session["scalars"] else []
89
+ smoothness = session["scalars"]["smoother_value"] - 1 if "smoother_value" in session["scalars"] else 0
90
+ if type == "step":
91
+ lines = make_step_lines(socket, splits, metric, keys)
92
+ elif type == "time":
93
+ lines = make_time_lines(socket, splits, metric, keys)
94
+ else:
95
+ raise ValueError(f"Unknown plotting type: {type}")
96
+
97
+ # # Sort lines by label
98
+ lines.sort(key=lambda x: x[0])
99
+ # Hide lines if needed
100
+ lines = [line for line in lines if line[0] not in hidden_lines]
101
+ fig = make_fig(lines, type=type, smoothness=smoothness, log_scale=logscale)
102
+
103
+ if running:
104
+ update_params = dict(
105
+ hx_get=f"/scalars/chart?runID={runID}&metric={metric}&type={type}&running={running}&logscale={logscale}",
106
+ hx_target=f"#chart-container-{runID}-{metric}",
107
+ hx_trigger="every 10s",
108
+ hx_swap="outerHTML",
109
+ )
110
+ else:
111
+ update_params = {}
112
+ return Div(
113
+ plotly2fasthtml(fig, js_options=dict(responsive=True)),
114
+ cls="chart-container",
115
+ id=f"chart-container-{runID}-{metric}",
116
+ **update_params
117
+ )
118
+
119
+ def LoadingChart(session, runID: int, metric: str, type: str, running: bool = False, logscale: bool = False):
120
+ return Div(
121
+ Div(
122
+ H1(metric, cls="chart-title"),
123
+ cls="chart-header",
124
+ id=f"chart-header-{runID}-{metric}"
125
+ ),
126
+ Div(
127
+ cls="chart-container",
128
+ id=f"chart-container-{runID}-{metric}",
129
+ hx_get=f"/scalars/chart?runID={runID}&metric={metric}&type={type}&running={running}&logscale={logscale}",
130
+ hx_target=f"#chart-container-{runID}-{metric}",
131
+ hx_trigger="load",
132
+ ),
133
+ cls="chart",
134
+ id=f"chart-{runID}-{metric}",
135
+ )
136
+
137
+ def Charts(session, runID: int, swap: bool = False, status: Literal["running", "finished", "failed"] = "running"):
138
+ from __main__ import rTable
139
+ socket = rTable.load_run(runID)
140
+ keys = socket.formatted_scalars
141
+ metrics = {label for split, label in keys}
142
+ type = session["scalars"]["chart_type"] if "chart_type" in session["scalars"] else "step"
143
+ logscale = session["scalars"]["chart_scale"] == "log" if "chart_scale" in session["scalars"] else False
144
+ out = Div(
145
+ H1("Charts", cls="chart-scalar-title"),
146
+ Ul(
147
+ *[
148
+ Li(LoadingChart(session, runID, metric, type=type, running=status == "running", logscale=logscale), cls="chart-list-item")
149
+ for metric in metrics
150
+ ],
151
+ cls="chart-list",
152
+ ),
153
+ cls="chart-section",
154
+ id=f"charts-section",
155
+ hx_swap_oob="true" if swap else None,
156
+ )
157
+ return out
158
+
159
+ def ScalarTab(session, runID, swap: bool = False):
160
+ from __main__ import CONFIG, rTable
161
+ if 'hidden_lines' not in session["scalars"]:
162
+ session["scalars"]['hidden_lines'] = []
163
+ socket = rTable.load_run(runID)
164
+ keys = socket.formatted_scalars
165
+ splits = {split for split, label in keys}
166
+ # Get repetitions
167
+ available_rep = socket.get_repetitions()
168
+ if len(available_rep) > 1:
169
+ line_names = [(f'{split}_{rep}', CONFIG.COLORS[i % len(CONFIG.COLORS)], f'{split}_{rep}' in session["scalars"]['hidden_lines']) for i, split in enumerate(splits) for rep in
170
+ available_rep]
171
+ else:
172
+ line_names = [(f'{split}', CONFIG.COLORS[i % len(CONFIG.COLORS)], f'{split}' in session["scalars"]['hidden_lines']) for i, split in enumerate(splits)]
173
+ # Sort lines by label
174
+ line_names.sort(key=lambda x: x[0])
175
+ status = socket.status
176
+ return Div(
177
+ Setup(session, line_names),
178
+ Charts(session, runID, status=status),
179
+ style="display; flex; width: 40vw; flex-direction: column; align-items: center; justify-content: center;",
180
+ id="scalar-tab",
181
+ hx_swap_oob="true" if swap else None,
182
+ )
183
+
184
+ def scalar_enable(runID):
185
+ """
186
+ Check if some scalars are logged and available for the runID. If not, we consider disable it.
187
+ :param runID: The runID to check.
188
+ :return: True if scalars are available, False otherwise.
189
+ """
190
+ from __main__ import rTable
191
+ socket = rTable.load_run(runID)
192
+ keys = socket.formatted_scalars
193
+ return len(keys) > 0
194
+
195
+ def build_scalar_routes(rt):
196
+ rt("/scalars/change_chart")(change_chart_type)
197
+ rt("/scalars/hide_line")(hide_line)
198
+ rt("/scalars/show_line")(show_line)
199
+ rt("/scalars/change_smoother")(change_smoother)
200
+ rt("/scalars/change_scale")(change_chart_scale)
201
+ rt("/scalars/chart")(load_chart)
202
+
203
+
204
+ # Interactive Routes
205
+ def change_chart_type(session, runIDs: str, step: bool):
206
+ new_type = "time" if step else "step"
207
+ session["scalars"]["chart_type"] = new_type
208
+ runIds = runIDs.split(",")
209
+ runID = runIds[0]
210
+ return (
211
+ ChartType(session, path="/scalars", selected_rows_key="datagrid", session_path="scalars"), # We want to toggle it
212
+ Charts(session, int(runID), swap=True)
213
+ )
214
+
215
+ def hide_line(session, runIDs: str, label: str):
216
+ if 'hidden_lines' not in session["scalars"]:
217
+ session["scalars"]['hidden_lines'] = []
218
+ runIds = runIDs.split(",")
219
+ runID = runIds[0]
220
+ session["scalars"]['hidden_lines'].append(label)
221
+ return ScalarTab(session, runID, swap=True)
222
+
223
+
224
+ def show_line(session, runIDs: str, label: str):
225
+ if 'hidden_lines' not in session["scalars"]:
226
+ session["scalars"]['hidden_lines'] = []
227
+ runIds = runIDs.split(",")
228
+ runID = runIds[0]
229
+ if label in session["scalars"]['hidden_lines']:
230
+ session["scalars"]['hidden_lines'].remove(label)
231
+
232
+ return ScalarTab(session, runID, swap=True)
233
+
234
+ def change_smoother(session, runIDs: str, smoother: int):
235
+ session["scalars"]["smoother_value"] = smoother
236
+ runIds = runIDs.split(",")
237
+ runID = runIds[0]
238
+ return ScalarTab(session, runID, swap=True)
239
+
240
+ def change_chart_scale(session, runIDs: str, log: bool):
241
+ new_type = "default" if log else "log"
242
+ session["scalars"]["chart_scale"] = new_type
243
+ runIds = runIDs.split(",")
244
+ runID = runIds[0]
245
+ return (
246
+ LogSelector(session, path="/scalars", selected_rows_key="datagrid", session_path="scalars"), # We want to toggle it
247
+ Charts(session, int(runID), swap=True)
248
+ )
249
+
250
+ def load_chart(session, runID: int, metric: str, type: str, running: bool, logscale: bool):
251
+ return Chart(session, runID, metric, type, running, logscale=logscale)