staticdash 2025.17__tar.gz → 2025.19__tar.gz
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-2025.17 → staticdash-2025.19}/PKG-INFO +2 -1
- {staticdash-2025.17 → staticdash-2025.19}/pyproject.toml +3 -2
- {staticdash-2025.17 → staticdash-2025.19}/staticdash/dashboard.py +167 -156
- {staticdash-2025.17 → staticdash-2025.19}/staticdash.egg-info/PKG-INFO +2 -1
- {staticdash-2025.17 → staticdash-2025.19}/staticdash.egg-info/requires.txt +1 -0
- {staticdash-2025.17 → staticdash-2025.19}/README.md +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/setup.cfg +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/staticdash/__init__.py +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/staticdash/assets/css/style.css +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/staticdash/assets/js/script.js +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/staticdash.egg-info/SOURCES.txt +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/staticdash.egg-info/dependency_links.txt +0 -0
- {staticdash-2025.17 → staticdash-2025.19}/staticdash.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: staticdash
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.19
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "staticdash"
|
|
7
|
-
version = "2025.
|
|
7
|
+
version = "2025.19"
|
|
8
8
|
description = "A lightweight static HTML dashboard generator with Plotly and pandas support."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Brian Day", email = "brian.day1@gmail.com" }
|
|
@@ -17,7 +17,8 @@ dependencies = [
|
|
|
17
17
|
"pandas",
|
|
18
18
|
"dominate",
|
|
19
19
|
"reportlab",
|
|
20
|
-
"kaleido"
|
|
20
|
+
"kaleido",
|
|
21
|
+
"matplotlib"
|
|
21
22
|
]
|
|
22
23
|
|
|
23
24
|
[tool.setuptools]
|
|
@@ -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):
|
|
@@ -46,11 +48,12 @@ class AbstractPage:
|
|
|
46
48
|
self.elements.append(("syntax", (code, language), width))
|
|
47
49
|
|
|
48
50
|
class Page(AbstractPage):
|
|
49
|
-
def __init__(self, slug, title, page_width=None):
|
|
51
|
+
def __init__(self, slug, title, page_width=None, marking=None):
|
|
50
52
|
super().__init__()
|
|
51
53
|
self.slug = slug
|
|
52
54
|
self.title = title
|
|
53
55
|
self.page_width = page_width
|
|
56
|
+
self.marking = marking # Page-specific marking
|
|
54
57
|
self.children = []
|
|
55
58
|
self.add_header(title, level=1)
|
|
56
59
|
|
|
@@ -60,6 +63,20 @@ class Page(AbstractPage):
|
|
|
60
63
|
def render(self, index, downloads_dir=None, relative_prefix="", inherited_width=None):
|
|
61
64
|
effective_width = self.page_width or inherited_width
|
|
62
65
|
elements = []
|
|
66
|
+
|
|
67
|
+
# Add floating header and footer for marking
|
|
68
|
+
marking = self.marking or "Default Marking"
|
|
69
|
+
elements.append(div(
|
|
70
|
+
marking,
|
|
71
|
+
cls="floating-header",
|
|
72
|
+
style="position: fixed; top: 0; left: 50%; transform: translateX(-50%); width: auto; background-color: #f8f9fa; text-align: center; padding: 10px; z-index: 1000; font-weight: normal;"
|
|
73
|
+
))
|
|
74
|
+
elements.append(div(
|
|
75
|
+
marking,
|
|
76
|
+
cls="floating-footer",
|
|
77
|
+
style="position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: auto; background-color: #f8f9fa; text-align: center; padding: 10px; z-index: 1000; font-weight: normal;"
|
|
78
|
+
))
|
|
79
|
+
|
|
63
80
|
for kind, content, el_width in self.elements:
|
|
64
81
|
style = ""
|
|
65
82
|
outer_style = ""
|
|
@@ -75,11 +92,28 @@ class Page(AbstractPage):
|
|
|
75
92
|
elem = header_tag(text)
|
|
76
93
|
elif kind == "plot":
|
|
77
94
|
fig = content
|
|
78
|
-
|
|
95
|
+
if hasattr(fig, "to_html"):
|
|
96
|
+
elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
|
|
97
|
+
else:
|
|
98
|
+
try:
|
|
99
|
+
buf = io.BytesIO()
|
|
100
|
+
fig.savefig(buf, format="png", bbox_inches="tight")
|
|
101
|
+
buf.seek(0)
|
|
102
|
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
103
|
+
buf.close()
|
|
104
|
+
elem = div(
|
|
105
|
+
raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
|
|
106
|
+
style="display: flex; justify-content: center; align-items: center;"
|
|
107
|
+
)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
elem = div(f"Matplotlib figure could not be rendered: {e}")
|
|
79
110
|
elif kind == "table":
|
|
80
111
|
df = content
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
try:
|
|
113
|
+
html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
|
|
114
|
+
elem = div(raw_util(html_table))
|
|
115
|
+
except Exception as e:
|
|
116
|
+
elem = div(f"Table could not be rendered: {e}")
|
|
83
117
|
elif kind == "download":
|
|
84
118
|
file_path, label = content
|
|
85
119
|
btn = a(label or os.path.basename(file_path), href=file_path, cls="download-button", download=True)
|
|
@@ -98,7 +132,9 @@ class Page(AbstractPage):
|
|
|
98
132
|
elem = div(elem, style=style)
|
|
99
133
|
elem = div(elem, style=outer_style)
|
|
100
134
|
elements.append(elem)
|
|
101
|
-
|
|
135
|
+
|
|
136
|
+
# Add padding to avoid overlap with header and footer
|
|
137
|
+
wrapper = div(*elements, style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%; padding-top: 80px; padding-bottom: 80px;")
|
|
102
138
|
return [wrapper]
|
|
103
139
|
|
|
104
140
|
class MiniPage(AbstractPage):
|
|
@@ -124,7 +160,24 @@ class MiniPage(AbstractPage):
|
|
|
124
160
|
elem = header_tag(text)
|
|
125
161
|
elif kind == "plot":
|
|
126
162
|
fig = content
|
|
127
|
-
|
|
163
|
+
# Plotly support (existing)
|
|
164
|
+
if hasattr(fig, "to_html"):
|
|
165
|
+
elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
|
|
166
|
+
# Matplotlib support
|
|
167
|
+
else:
|
|
168
|
+
try:
|
|
169
|
+
buf = io.BytesIO()
|
|
170
|
+
fig.savefig(buf, format="png", bbox_inches="tight")
|
|
171
|
+
buf.seek(0)
|
|
172
|
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
173
|
+
buf.close()
|
|
174
|
+
# Center the image using a div with inline styles
|
|
175
|
+
elem = div(
|
|
176
|
+
raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
|
|
177
|
+
style="display: flex; justify-content: center; align-items: center;"
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
elem = div(f"Matplotlib figure could not be rendered: {e}")
|
|
128
181
|
elif kind == "table":
|
|
129
182
|
df = content
|
|
130
183
|
html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
|
|
@@ -151,10 +204,11 @@ class MiniPage(AbstractPage):
|
|
|
151
204
|
return row_div
|
|
152
205
|
|
|
153
206
|
class Dashboard:
|
|
154
|
-
def __init__(self, title="Dashboard", page_width=900):
|
|
207
|
+
def __init__(self, title="Dashboard", page_width=900, marking=None):
|
|
155
208
|
self.title = title
|
|
156
209
|
self.pages = []
|
|
157
210
|
self.page_width = page_width
|
|
211
|
+
self.marking = marking # Dashboard-wide marking
|
|
158
212
|
|
|
159
213
|
def add_page(self, page):
|
|
160
214
|
self.pages.append(page)
|
|
@@ -262,167 +316,124 @@ class Dashboard:
|
|
|
262
316
|
with open(os.path.join(output_dir, "index.html"), "w") as f:
|
|
263
317
|
f.write(str(index_doc))
|
|
264
318
|
|
|
265
|
-
def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4",
|
|
266
|
-
from reportlab.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
319
|
+
def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_title_page=False, title_page_marking=None, author=None, affiliation=None):
|
|
320
|
+
from reportlab.platypus import SimpleDocTemplate, Spacer, Paragraph, PageBreak, Image
|
|
321
|
+
from reportlab.lib.pagesizes import letter, A4
|
|
322
|
+
from reportlab.lib.styles import getSampleStyleSheet
|
|
323
|
+
from datetime import datetime
|
|
324
|
+
import io
|
|
271
325
|
|
|
326
|
+
page_size = A4 if pagesize == "A4" else letter
|
|
272
327
|
styles = getSampleStyleSheet()
|
|
273
|
-
styles['Heading1'].fontSize = 18
|
|
274
|
-
styles['Heading1'].leading = 22
|
|
275
|
-
styles['Heading1'].spaceAfter = 12
|
|
276
|
-
styles['Heading1'].spaceBefore = 18
|
|
277
|
-
styles['Heading1'].fontName = 'Helvetica-Bold'
|
|
278
|
-
styles['Heading2'].fontSize = 14
|
|
279
|
-
styles['Heading2'].leading = 18
|
|
280
|
-
styles['Heading2'].spaceAfter = 8
|
|
281
|
-
styles['Heading2'].spaceBefore = 12
|
|
282
|
-
styles['Heading2'].fontName = 'Helvetica-Bold'
|
|
283
|
-
if 'CodeBlock' not in styles:
|
|
284
|
-
styles.add(ParagraphStyle(name='CodeBlock', fontName='Courier', fontSize=9, leading=12, backColor=colors.whitesmoke, leftIndent=12, rightIndent=12, spaceAfter=8, borderPadding=4))
|
|
285
|
-
normal_style = styles['Normal']
|
|
286
|
-
|
|
287
328
|
story = []
|
|
288
|
-
outline_entries = []
|
|
289
|
-
heading_paragraphs = []
|
|
290
|
-
|
|
291
|
-
class MyDocTemplate(SimpleDocTemplate):
|
|
292
|
-
def __init__(self, *args, outline_entries=None, headings=None, **kwargs):
|
|
293
|
-
super().__init__(*args, **kwargs)
|
|
294
|
-
self.outline_entries = outline_entries or []
|
|
295
|
-
self.headings = headings or []
|
|
296
|
-
self._outline_idx = 0
|
|
297
|
-
|
|
298
|
-
def afterFlowable(self, flowable):
|
|
299
|
-
if hasattr(flowable, 'getPlainText'):
|
|
300
|
-
text = flowable.getPlainText().strip()
|
|
301
|
-
if self._outline_idx < len(self.outline_entries):
|
|
302
|
-
expected_title, level, section_num = self.outline_entries[self._outline_idx]
|
|
303
|
-
expected = expected_title.strip()
|
|
304
|
-
if text == expected:
|
|
305
|
-
bookmark_name = f"section_{section_num.replace('.', '_')}"
|
|
306
|
-
self.canv.bookmarkPage(bookmark_name)
|
|
307
|
-
self.canv.addOutlineEntry(expected_title, bookmark_name, level=level, closed=False)
|
|
308
|
-
self._outline_idx += 1
|
|
309
|
-
|
|
310
|
-
def render_page(page, level=0, sec_prefix=[]):
|
|
311
|
-
if len(sec_prefix) <= level:
|
|
312
|
-
sec_prefix.append(1)
|
|
313
|
-
else:
|
|
314
|
-
sec_prefix[level] += 1
|
|
315
|
-
sec_prefix = sec_prefix[:level+1]
|
|
316
|
-
section_num = ".".join(str(n) for n in sec_prefix)
|
|
317
|
-
section_title = f"{section_num} {page.title}"
|
|
318
|
-
outline_entries.append((section_title, level, section_num))
|
|
319
|
-
style = styles['Heading1'] if level == 0 else styles['Heading2']
|
|
320
|
-
bookmark_name = f"section_{section_num.replace('.', '_')}"
|
|
321
|
-
para = Paragraph(f'<a name="{bookmark_name}"/>{section_title}', style)
|
|
322
|
-
heading_paragraphs.append(para)
|
|
323
|
-
story.append(para)
|
|
324
|
-
story.append(Spacer(1, 12))
|
|
325
|
-
|
|
326
|
-
def render_elements(elements):
|
|
327
|
-
for kind, content, _ in elements:
|
|
328
|
-
if kind == "text":
|
|
329
|
-
story.append(Paragraph(content, normal_style))
|
|
330
|
-
story.append(Spacer(1, 8))
|
|
331
|
-
elif kind == "header":
|
|
332
|
-
text, level_ = content
|
|
333
|
-
header_style = styles['Heading{}'.format(min(level_+1, 4))]
|
|
334
|
-
story.append(Paragraph(text, header_style))
|
|
335
|
-
story.append(Spacer(1, 8))
|
|
336
|
-
elif kind == "table":
|
|
337
|
-
df = content
|
|
338
|
-
try:
|
|
339
|
-
data = [df.columns.tolist()] + df.values.tolist()
|
|
340
|
-
t = Table(data, repeatRows=1)
|
|
341
|
-
t.setStyle(TableStyle([
|
|
342
|
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
|
|
343
|
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
344
|
-
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
345
|
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
346
|
-
('FONTSIZE', (0, 0), (-1, 0), 11),
|
|
347
|
-
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
|
348
|
-
('TOPPADDING', (0, 0), (-1, 0), 10),
|
|
349
|
-
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
|
|
350
|
-
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
|
|
351
|
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
352
|
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
|
353
|
-
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
354
|
-
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
355
|
-
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
356
|
-
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
357
|
-
]))
|
|
358
|
-
story.append(t)
|
|
359
|
-
story.append(Spacer(1, 12))
|
|
360
|
-
except Exception:
|
|
361
|
-
story.append(Paragraph("Table could not be rendered.", normal_style))
|
|
362
|
-
elif kind == "plot":
|
|
363
|
-
fig = content
|
|
364
|
-
try:
|
|
365
|
-
import plotly.graph_objects as go
|
|
366
|
-
if not isinstance(fig, go.Figure):
|
|
367
|
-
raise ValueError("add_plot must be called with a plotly.graph_objects.Figure")
|
|
368
|
-
fig.update_layout(margin=dict(l=10, r=10, t=30, b=30), width=900, height=540)
|
|
369
|
-
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
|
|
370
|
-
fig.write_image(tmpfile.name, width=600, height=360, scale=2, format="png", engine="kaleido")
|
|
371
|
-
with open(tmpfile.name, "rb") as imgf:
|
|
372
|
-
img_bytes = imgf.read()
|
|
373
|
-
img_buf = io.BytesIO(img_bytes)
|
|
374
|
-
story.append(Spacer(1, 8))
|
|
375
|
-
story.append(Image(img_buf, width=6*inch, height=3.6*inch))
|
|
376
|
-
story.append(Spacer(1, 12))
|
|
377
|
-
os.unlink(tmpfile.name)
|
|
378
|
-
except Exception as e:
|
|
379
|
-
story.append(Paragraph(f"Plot rendering not supported in PDF: {e}", normal_style))
|
|
380
|
-
elif kind == "syntax":
|
|
381
|
-
code, language = content
|
|
382
|
-
story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
|
|
383
|
-
story.append(Spacer(1, 4))
|
|
384
|
-
code_html = html.escape(code).replace(' ', ' ').replace('\n', '<br/>')
|
|
385
|
-
story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
|
|
386
|
-
story.append(Spacer(1, 12))
|
|
387
|
-
elif kind == "minipage":
|
|
388
|
-
render_elements(content.elements)
|
|
389
|
-
|
|
390
|
-
render_elements(page.elements)
|
|
391
|
-
|
|
392
|
-
for i, child in enumerate(getattr(page, "children", []), start=0):
|
|
393
|
-
story.append(PageBreak())
|
|
394
|
-
child_sec_prefix = sec_prefix.copy() + [i]
|
|
395
|
-
render_page(child, level=level+1, sec_prefix=child_sec_prefix)
|
|
396
|
-
|
|
397
|
-
if level == 0:
|
|
398
|
-
story.append(PageBreak())
|
|
399
329
|
|
|
330
|
+
# Add title page
|
|
400
331
|
if include_title_page:
|
|
401
332
|
story.append(Spacer(1, 120))
|
|
402
|
-
# Title, centered and bold
|
|
403
333
|
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
404
334
|
story.append(Spacer(1, 48))
|
|
405
|
-
|
|
406
|
-
|
|
335
|
+
|
|
336
|
+
# Center author, affiliation, and date
|
|
407
337
|
if author:
|
|
408
|
-
|
|
338
|
+
story.append(Paragraph(f"<para align='center'>{author}</para>", styles['Normal']))
|
|
409
339
|
if affiliation:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
340
|
+
story.append(Paragraph(f"<para align='center'>{affiliation}</para>", styles['Normal']))
|
|
341
|
+
current_date = datetime.now().strftime("%B %d, %Y")
|
|
342
|
+
story.append(Paragraph(f"<para align='center'>{current_date}</para>", styles['Normal']))
|
|
343
|
+
|
|
414
344
|
story.append(PageBreak())
|
|
415
345
|
|
|
346
|
+
# Add markings to the PDF
|
|
347
|
+
def add_marking(canvas, doc, marking):
|
|
348
|
+
if marking:
|
|
349
|
+
canvas.saveState()
|
|
350
|
+
canvas.setFont("Helvetica", 10)
|
|
351
|
+
page_width = doc.pagesize[0]
|
|
352
|
+
text_width = canvas.stringWidth(marking, "Helvetica", 10)
|
|
353
|
+
x_position = (page_width - text_width) / 2 # Center the marking
|
|
354
|
+
canvas.drawString(x_position, doc.pagesize[1] - 36, marking) # Header
|
|
355
|
+
canvas.drawString(x_position, 36, marking) # Footer
|
|
356
|
+
canvas.restoreState()
|
|
357
|
+
|
|
358
|
+
# Recursive function to render pages and subpages
|
|
359
|
+
def render_page(page):
|
|
360
|
+
# Render the current page
|
|
361
|
+
for kind, content, _ in page.elements:
|
|
362
|
+
if kind == "text":
|
|
363
|
+
story.append(Paragraph(content, styles['Normal']))
|
|
364
|
+
story.append(Spacer(1, 8))
|
|
365
|
+
elif kind == "header":
|
|
366
|
+
text, level = content
|
|
367
|
+
header_style = styles[f'Heading{min(level + 1, 4)}']
|
|
368
|
+
story.append(Paragraph(text, header_style))
|
|
369
|
+
story.append(Spacer(1, 8))
|
|
370
|
+
elif kind == "table":
|
|
371
|
+
df = content
|
|
372
|
+
try:
|
|
373
|
+
data = [df.columns.tolist()] + df.values.tolist()
|
|
374
|
+
t = Table(data, repeatRows=1)
|
|
375
|
+
t.setStyle(TableStyle([
|
|
376
|
+
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
|
|
377
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
378
|
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
379
|
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
380
|
+
('FONTSIZE', (0, 0), (-1, 0), 11),
|
|
381
|
+
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
|
382
|
+
('TOPPADDING', (0, 0), (-1, 0), 10),
|
|
383
|
+
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
|
|
384
|
+
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
|
|
385
|
+
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
386
|
+
('FONTSIZE', (0, 1), (-1, -1), 10),
|
|
387
|
+
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
388
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
389
|
+
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
390
|
+
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
391
|
+
]))
|
|
392
|
+
story.append(t)
|
|
393
|
+
story.append(Spacer(1, 12))
|
|
394
|
+
except Exception:
|
|
395
|
+
story.append(Paragraph("Table could not be rendered.", styles['Normal']))
|
|
396
|
+
elif kind == "plot":
|
|
397
|
+
fig = content
|
|
398
|
+
try:
|
|
399
|
+
# Handle Plotly figures
|
|
400
|
+
if hasattr(fig, "to_image"):
|
|
401
|
+
img_bytes = fig.to_image(format="png", width=800, height=600, scale=2)
|
|
402
|
+
img_buffer = io.BytesIO(img_bytes)
|
|
403
|
+
img = Image(img_buffer, width=6 * inch, height=4.5 * inch)
|
|
404
|
+
story.append(img)
|
|
405
|
+
story.append(Spacer(1, 12))
|
|
406
|
+
# Handle Matplotlib figures
|
|
407
|
+
elif hasattr(fig, "savefig"):
|
|
408
|
+
buf = io.BytesIO()
|
|
409
|
+
fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
|
410
|
+
buf.seek(0)
|
|
411
|
+
img = Image(buf, width=6 * inch, height=4.5 * inch)
|
|
412
|
+
story.append(img)
|
|
413
|
+
story.append(Spacer(1, 12))
|
|
414
|
+
except Exception as e:
|
|
415
|
+
story.append(Paragraph(f"Plot could not be rendered: {e}", styles['Normal']))
|
|
416
|
+
elif kind == "syntax":
|
|
417
|
+
# Handle syntax blocks
|
|
418
|
+
pass
|
|
419
|
+
elif kind == "minipage":
|
|
420
|
+
# Handle subpages
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
# Recursively render subpages
|
|
424
|
+
for child in getattr(page, "children", []):
|
|
425
|
+
story.append(PageBreak()) # Add a PageBreak before rendering subpages
|
|
426
|
+
render_page(child)
|
|
427
|
+
|
|
428
|
+
# Render all pages
|
|
416
429
|
for page in self.pages:
|
|
417
430
|
render_page(page)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
431
|
+
story.append(PageBreak()) # Add a PageBreak after each top-level page
|
|
432
|
+
|
|
433
|
+
# Build the PDF
|
|
434
|
+
doc = SimpleDocTemplate(output_path, pagesize=page_size)
|
|
435
|
+
doc.build(
|
|
436
|
+
story,
|
|
437
|
+
onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
|
|
438
|
+
onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
|
|
426
439
|
)
|
|
427
|
-
|
|
428
|
-
doc.multiBuild(story)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: staticdash
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.19
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|