staticdash 2025.16__py3-none-any.whl → 2025.18__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.
- staticdash/assets/js/script.js +73 -8
- staticdash/dashboard.py +94 -16
- {staticdash-2025.16.dist-info → staticdash-2025.18.dist-info}/METADATA +2 -1
- staticdash-2025.18.dist-info/RECORD +8 -0
- staticdash-2025.16.dist-info/RECORD +0 -8
- {staticdash-2025.16.dist-info → staticdash-2025.18.dist-info}/WHEEL +0 -0
- {staticdash-2025.16.dist-info → staticdash-2025.18.dist-info}/top_level.txt +0 -0
staticdash/assets/js/script.js
CHANGED
|
@@ -85,6 +85,25 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
85
85
|
return link ? link.getAttribute('href') : null;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function saveSidebarState() {
|
|
89
|
+
const openGroups = Array.from(document.querySelectorAll('.sidebar-group.open'))
|
|
90
|
+
.map(getGroupSlug)
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
localStorage.setItem("sidebar-open-groups", JSON.stringify(openGroups));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function restoreSidebarState() {
|
|
96
|
+
const openGroups = JSON.parse(localStorage.getItem("sidebar-open-groups") || "[]");
|
|
97
|
+
document.querySelectorAll('.sidebar-group').forEach(group => {
|
|
98
|
+
const slug = getGroupSlug(group);
|
|
99
|
+
if (slug && openGroups.includes(slug)) {
|
|
100
|
+
group.classList.add('open');
|
|
101
|
+
} else {
|
|
102
|
+
group.classList.remove('open');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
// Restore open groups from localStorage
|
|
89
108
|
const openGroups = JSON.parse(localStorage.getItem("sidebar-open-groups") || "[]");
|
|
90
109
|
document.querySelectorAll('.sidebar-group').forEach(group => {
|
|
@@ -102,13 +121,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
102
121
|
const group = parent.closest('.sidebar-group');
|
|
103
122
|
if (group) {
|
|
104
123
|
group.classList.toggle('open');
|
|
105
|
-
|
|
106
|
-
const allGroups = Array.from(document.querySelectorAll('.sidebar-group'));
|
|
107
|
-
const open = allGroups
|
|
108
|
-
.filter(g => g.classList.contains('open'))
|
|
109
|
-
.map(getGroupSlug)
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
localStorage.setItem("sidebar-open-groups", JSON.stringify(open));
|
|
124
|
+
saveSidebarState();
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
127
|
});
|
|
@@ -131,4 +144,56 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
131
144
|
setTimeout(() => {
|
|
132
145
|
document.body.classList.add("sidebar-animate");
|
|
133
146
|
}, 0);
|
|
134
|
-
|
|
147
|
+
|
|
148
|
+
// --- Sidebar collapse/expand memory ---
|
|
149
|
+
function saveSidebarState() {
|
|
150
|
+
const expanded = [];
|
|
151
|
+
document.querySelectorAll('.sidebar-group .sidebar-children').forEach(el => {
|
|
152
|
+
if (el.classList.contains('expanded')) {
|
|
153
|
+
// Use parent page slug as key
|
|
154
|
+
const parent = el.parentElement.querySelector('.sidebar-parent, .nav-link');
|
|
155
|
+
if (parent && parent.getAttribute('href')) {
|
|
156
|
+
expanded.push(parent.getAttribute('href'));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function restoreSidebarState() {
|
|
164
|
+
const expanded = JSON.parse(localStorage.getItem('sidebarExpanded') || '[]');
|
|
165
|
+
document.querySelectorAll('.sidebar-group').forEach(group => {
|
|
166
|
+
const parent = group.querySelector('.sidebar-parent, .nav-link');
|
|
167
|
+
const children = group.querySelector('.sidebar-children');
|
|
168
|
+
if (parent && children) {
|
|
169
|
+
if (expanded.includes(parent.getAttribute('href'))) {
|
|
170
|
+
children.classList.add('expanded');
|
|
171
|
+
} else {
|
|
172
|
+
children.classList.remove('expanded');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
restoreSidebarState();
|
|
179
|
+
|
|
180
|
+
document.querySelectorAll('.sidebar-group .sidebar-parent, .sidebar-group .nav-link').forEach(parent => {
|
|
181
|
+
parent.addEventListener('click', function(e) {
|
|
182
|
+
// Only toggle if arrow or parent link is clicked
|
|
183
|
+
if (e.target.classList.contains('sidebar-arrow') || e.currentTarget === e.target) {
|
|
184
|
+
const group = parent.closest('.sidebar-group');
|
|
185
|
+
const children = group.querySelector('.sidebar-children');
|
|
186
|
+
if (children) {
|
|
187
|
+
children.classList.toggle('expanded');
|
|
188
|
+
saveSidebarState();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/* CSS */
|
|
196
|
+
/* Add this CSS to handle the rotation of the arrow icon */
|
|
197
|
+
body.sidebar-animate .sidebar-group.open .sidebar-arrow {
|
|
198
|
+
transform: rotate(90deg);
|
|
199
|
+
}
|
staticdash/dashboard.py
CHANGED
|
@@ -15,6 +15,8 @@ from reportlab.lib import colors
|
|
|
15
15
|
from reportlab.lib.units import inch
|
|
16
16
|
import io
|
|
17
17
|
import tempfile
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
import io, base64
|
|
18
20
|
|
|
19
21
|
class AbstractPage:
|
|
20
22
|
def __init__(self):
|
|
@@ -75,7 +77,24 @@ class Page(AbstractPage):
|
|
|
75
77
|
elem = header_tag(text)
|
|
76
78
|
elif kind == "plot":
|
|
77
79
|
fig = content
|
|
78
|
-
|
|
80
|
+
# Plotly support (existing)
|
|
81
|
+
if hasattr(fig, "to_html"):
|
|
82
|
+
elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
|
|
83
|
+
# Matplotlib support
|
|
84
|
+
else:
|
|
85
|
+
try:
|
|
86
|
+
buf = io.BytesIO()
|
|
87
|
+
fig.savefig(buf, format="png", bbox_inches="tight")
|
|
88
|
+
buf.seek(0)
|
|
89
|
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
90
|
+
buf.close()
|
|
91
|
+
# Center the image using a div with inline styles
|
|
92
|
+
elem = div(
|
|
93
|
+
raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
|
|
94
|
+
style="display: flex; justify-content: center; align-items: center;"
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
elem = div(f"Matplotlib figure could not be rendered: {e}")
|
|
79
98
|
elif kind == "table":
|
|
80
99
|
df = content
|
|
81
100
|
html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
|
|
@@ -124,7 +143,24 @@ class MiniPage(AbstractPage):
|
|
|
124
143
|
elem = header_tag(text)
|
|
125
144
|
elif kind == "plot":
|
|
126
145
|
fig = content
|
|
127
|
-
|
|
146
|
+
# Plotly support (existing)
|
|
147
|
+
if hasattr(fig, "to_html"):
|
|
148
|
+
elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
|
|
149
|
+
# Matplotlib support
|
|
150
|
+
else:
|
|
151
|
+
try:
|
|
152
|
+
buf = io.BytesIO()
|
|
153
|
+
fig.savefig(buf, format="png", bbox_inches="tight")
|
|
154
|
+
buf.seek(0)
|
|
155
|
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
156
|
+
buf.close()
|
|
157
|
+
# Center the image using a div with inline styles
|
|
158
|
+
elem = div(
|
|
159
|
+
raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
|
|
160
|
+
style="display: flex; justify-content: center; align-items: center;"
|
|
161
|
+
)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
elem = div(f"Matplotlib figure could not be rendered: {e}")
|
|
128
164
|
elif kind == "table":
|
|
129
165
|
df = content
|
|
130
166
|
html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
|
|
@@ -363,18 +399,53 @@ class Dashboard:
|
|
|
363
399
|
fig = content
|
|
364
400
|
try:
|
|
365
401
|
import plotly.graph_objects as go
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
import matplotlib.figure
|
|
403
|
+
import io
|
|
404
|
+
from reportlab.platypus import Image
|
|
405
|
+
|
|
406
|
+
# Plotly support
|
|
407
|
+
if isinstance(fig, go.Figure):
|
|
408
|
+
# Configure the figure layout for PDF rendering
|
|
409
|
+
fig.update_layout(
|
|
410
|
+
margin=dict(l=10, r=10, t=30, b=30),
|
|
411
|
+
width=900,
|
|
412
|
+
height=540
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Use kaleido to export the figure as a PNG
|
|
416
|
+
png_bytes = fig.to_image(format="png", width=900, height=540, engine="kaleido")
|
|
417
|
+
|
|
418
|
+
# Wrap the PNG bytes in a BytesIO buffer
|
|
419
|
+
img_buf = io.BytesIO(png_bytes)
|
|
420
|
+
|
|
421
|
+
# Add the image to the PDF story
|
|
374
422
|
story.append(Spacer(1, 8))
|
|
375
|
-
story.append(Image(img_buf, width=6*inch, height=3.6*inch))
|
|
423
|
+
story.append(Image(img_buf, width=6 * inch, height=3.6 * inch))
|
|
376
424
|
story.append(Spacer(1, 12))
|
|
377
|
-
|
|
425
|
+
|
|
426
|
+
# Matplotlib support
|
|
427
|
+
elif isinstance(fig, matplotlib.figure.Figure):
|
|
428
|
+
buf = io.BytesIO()
|
|
429
|
+
# Save the figure with higher DPI for better quality
|
|
430
|
+
fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
|
431
|
+
buf.seek(0)
|
|
432
|
+
|
|
433
|
+
# Calculate aspect ratio
|
|
434
|
+
fig_width, fig_height = fig.get_size_inches()
|
|
435
|
+
aspect_ratio = fig_height / fig_width
|
|
436
|
+
|
|
437
|
+
# Set width and calculate height based on aspect ratio
|
|
438
|
+
pdf_width = 6 * inch
|
|
439
|
+
pdf_height = pdf_width * aspect_ratio
|
|
440
|
+
|
|
441
|
+
# Add the image to the PDF story
|
|
442
|
+
story.append(Spacer(1, 8))
|
|
443
|
+
story.append(Image(buf, width=pdf_width, height=pdf_height))
|
|
444
|
+
story.append(Spacer(1, 12))
|
|
445
|
+
|
|
446
|
+
else:
|
|
447
|
+
raise ValueError("add_plot must be called with a plotly.graph_objects.Figure or matplotlib.figure.Figure")
|
|
448
|
+
|
|
378
449
|
except Exception as e:
|
|
379
450
|
story.append(Paragraph(f"Plot rendering not supported in PDF: {e}", normal_style))
|
|
380
451
|
elif kind == "syntax":
|
|
@@ -398,12 +469,19 @@ class Dashboard:
|
|
|
398
469
|
story.append(PageBreak())
|
|
399
470
|
|
|
400
471
|
if include_title_page:
|
|
401
|
-
story.append(
|
|
472
|
+
story.append(Spacer(1, 120))
|
|
473
|
+
# Title, centered and bold
|
|
474
|
+
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
475
|
+
story.append(Spacer(1, 48))
|
|
476
|
+
# Centered info block (no labels)
|
|
477
|
+
info_lines = []
|
|
402
478
|
if author:
|
|
403
|
-
|
|
479
|
+
info_lines.append(str(author))
|
|
404
480
|
if affiliation:
|
|
405
|
-
|
|
406
|
-
|
|
481
|
+
info_lines.append(str(affiliation))
|
|
482
|
+
info_lines.append(pd.Timestamp.now().strftime('%B %d, %Y'))
|
|
483
|
+
info_html = "<br/>".join(info_lines)
|
|
484
|
+
story.append(Paragraph(f'<para align="center">{info_html}</para>', styles['Normal']))
|
|
407
485
|
story.append(PageBreak())
|
|
408
486
|
|
|
409
487
|
for page in self.pages:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: staticdash
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.18
|
|
4
4
|
Summary: A lightweight static HTML dashboard generator with Plotly and pandas support.
|
|
5
5
|
Author-email: Brian Day <brian.day1@gmail.com>
|
|
6
6
|
License: CC0-1.0
|
|
@@ -13,6 +13,7 @@ Requires-Dist: pandas
|
|
|
13
13
|
Requires-Dist: dominate
|
|
14
14
|
Requires-Dist: reportlab
|
|
15
15
|
Requires-Dist: kaleido
|
|
16
|
+
Requires-Dist: matplotlib
|
|
16
17
|
|
|
17
18
|
# staticdash
|
|
18
19
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
|
|
2
|
+
staticdash/dashboard.py,sha256=6d3TbXVN0ZdWD5awsqWH9H6tH64fpOanTAiVWG_q-Eo,25056
|
|
3
|
+
staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
|
|
4
|
+
staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
|
|
5
|
+
staticdash-2025.18.dist-info/METADATA,sha256=_Zrirh0NBbV36fcXJQ4oqGQ_lr-UPhYSwLr6Vy3ur3U,1960
|
|
6
|
+
staticdash-2025.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
staticdash-2025.18.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
|
8
|
+
staticdash-2025.18.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
|
|
2
|
-
staticdash/dashboard.py,sha256=798Itq6FgsU3OY4sq7PQIttLdkX0Au2ls0iyp5Pkxtc,21395
|
|
3
|
-
staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
|
|
4
|
-
staticdash/assets/js/script.js,sha256=vBFL98da30_zHUKVqhLDBL2HzrI9huSHsea-e8A2qUk,4818
|
|
5
|
-
staticdash-2025.16.dist-info/METADATA,sha256=Nwms4u35b4I231PGM0KFL2MUQwUeUlZAb3xA1yEyTJE,1934
|
|
6
|
-
staticdash-2025.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
staticdash-2025.16.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
|
8
|
-
staticdash-2025.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|