staticdash 2025.27__py3-none-any.whl → 2025.28__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/css/style.css +22 -1
- staticdash/dashboard.py +113 -379
- {staticdash-2025.27.dist-info → staticdash-2025.28.dist-info}/METADATA +1 -1
- staticdash-2025.28.dist-info/RECORD +8 -0
- staticdash-2025.27.dist-info/RECORD +0 -8
- {staticdash-2025.27.dist-info → staticdash-2025.28.dist-info}/WHEEL +0 -0
- {staticdash-2025.27.dist-info → staticdash-2025.28.dist-info}/top_level.txt +0 -0
staticdash/assets/css/style.css
CHANGED
|
@@ -371,4 +371,25 @@ body.sidebar-animate .sidebar-arrow {
|
|
|
371
371
|
background-color: #34495e;
|
|
372
372
|
color: #fff;
|
|
373
373
|
text-decoration: none;
|
|
374
|
-
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
:root {
|
|
377
|
+
--sidebar-width: 240px; /* matches #sidebar width */
|
|
378
|
+
--content-padding-x: 20px; /* matches #content horizontal padding */
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* Pure-CSS triangle for sidebar arrow */
|
|
382
|
+
.sidebar-arrow {
|
|
383
|
+
width: 0;
|
|
384
|
+
height: 0;
|
|
385
|
+
border-top: 6px solid transparent;
|
|
386
|
+
border-bottom: 6px solid transparent;
|
|
387
|
+
border-left: 8px solid #bdc3c7; /* arrow color */
|
|
388
|
+
margin-right: 6px;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* Rotate when group is open (you already have this rule; keep it) */
|
|
392
|
+
body.sidebar-animate .sidebar-group.open .sidebar-arrow {
|
|
393
|
+
transform: rotate(90deg);
|
|
394
|
+
transform-origin: 30% 50%;
|
|
395
|
+
}
|
staticdash/dashboard.py
CHANGED
|
@@ -7,22 +7,13 @@ from dominate import document
|
|
|
7
7
|
from dominate.tags import div, h1, h2, h3, h4, p, a, script, link, span
|
|
8
8
|
from dominate.util import raw as raw_util
|
|
9
9
|
import html
|
|
10
|
-
from reportlab.lib.pagesizes import letter, A4
|
|
11
|
-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, Image
|
|
12
|
-
from reportlab.platypus.tableofcontents import TableOfContents
|
|
13
|
-
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
14
|
-
from reportlab.lib import colors
|
|
15
|
-
from reportlab.lib.units import inch
|
|
16
10
|
import io
|
|
17
|
-
import
|
|
18
|
-
import matplotlib.pyplot as plt
|
|
19
|
-
import io, base64
|
|
11
|
+
import base64
|
|
20
12
|
|
|
21
13
|
class AbstractPage:
|
|
22
14
|
def __init__(self):
|
|
23
15
|
self.elements = []
|
|
24
16
|
|
|
25
|
-
|
|
26
17
|
def add_header(self, text, level=1, width=None):
|
|
27
18
|
if level not in (1, 2, 3, 4):
|
|
28
19
|
raise ValueError("Header level must be 1, 2, 3, or 4")
|
|
@@ -48,58 +39,68 @@ class AbstractPage:
|
|
|
48
39
|
def add_syntax(self, code, language="python", width=None):
|
|
49
40
|
self.elements.append(("syntax", (code, language), width))
|
|
50
41
|
|
|
42
|
+
|
|
51
43
|
class Page(AbstractPage):
|
|
52
44
|
def __init__(self, slug, title, page_width=None, marking=None):
|
|
53
45
|
super().__init__()
|
|
54
46
|
self.slug = slug
|
|
55
47
|
self.title = title
|
|
56
48
|
self.page_width = page_width
|
|
57
|
-
self.marking = marking #
|
|
49
|
+
self.marking = marking # None => inherit dashboard; str => override
|
|
58
50
|
self.children = []
|
|
59
|
-
# self.add_header(title, level=1)
|
|
60
51
|
|
|
61
52
|
def add_subpage(self, page):
|
|
62
53
|
self.children.append(page)
|
|
63
54
|
|
|
64
|
-
def render(
|
|
55
|
+
def render(
|
|
56
|
+
self,
|
|
57
|
+
index,
|
|
58
|
+
downloads_dir=None,
|
|
59
|
+
relative_prefix="",
|
|
60
|
+
inherited_width=None,
|
|
61
|
+
inherited_marking=None,
|
|
62
|
+
inherited_distribution=None
|
|
63
|
+
):
|
|
65
64
|
effective_width = self.page_width or inherited_width
|
|
65
|
+
effective_marking = self.marking if (self.marking is not None) else inherited_marking
|
|
66
|
+
effective_distribution = getattr(self, "distribution", None) or inherited_distribution
|
|
67
|
+
|
|
66
68
|
elements = []
|
|
67
69
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
footer_block.append(div(
|
|
94
|
-
footer_block.append(div(marking))
|
|
95
|
-
|
|
96
|
-
elements.append(div(
|
|
97
|
-
*footer_block,
|
|
98
|
-
cls="floating-footer",
|
|
99
|
-
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;"
|
|
100
|
-
))
|
|
70
|
+
# Only show bars if a marking is present (no default text at all).
|
|
71
|
+
if effective_marking:
|
|
72
|
+
# Center within the MAIN CONTENT AREA (exclude sidebar and #content padding):
|
|
73
|
+
# left = sidebar_width + content_padding_x
|
|
74
|
+
# width = viewport - sidebar_width - 2*content_padding_x
|
|
75
|
+
shared_pos = (
|
|
76
|
+
"position: fixed; "
|
|
77
|
+
"left: calc(var(--sidebar-width, 240px) + var(--content-padding-x, 20px)); "
|
|
78
|
+
"width: calc(100vw - var(--sidebar-width, 240px) - 2*var(--content-padding-x, 20px)); "
|
|
79
|
+
"text-align: center; "
|
|
80
|
+
"background-color: #f8f9fa; "
|
|
81
|
+
"padding: 10px; "
|
|
82
|
+
"z-index: 1000; "
|
|
83
|
+
"font-weight: normal;"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
elements.append(div(
|
|
87
|
+
effective_marking,
|
|
88
|
+
cls="floating-header",
|
|
89
|
+
style=f"{shared_pos} top: 0;"
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
footer_block = []
|
|
93
|
+
if effective_distribution:
|
|
94
|
+
footer_block.append(div(effective_distribution, style="margin-bottom: 4px; font-size: 10pt;"))
|
|
95
|
+
footer_block.append(div(effective_marking))
|
|
101
96
|
|
|
97
|
+
elements.append(div(
|
|
98
|
+
*footer_block,
|
|
99
|
+
cls="floating-footer",
|
|
100
|
+
style=f"{shared_pos} bottom: 0;"
|
|
101
|
+
))
|
|
102
102
|
|
|
103
|
+
# Render elements
|
|
103
104
|
for kind, content, el_width in self.elements:
|
|
104
105
|
style = ""
|
|
105
106
|
outer_style = ""
|
|
@@ -150,22 +151,36 @@ class Page(AbstractPage):
|
|
|
150
151
|
cls="syntax-block"
|
|
151
152
|
)
|
|
152
153
|
elif kind == "minipage":
|
|
153
|
-
elem = content.render(
|
|
154
|
+
elem = content.render(
|
|
155
|
+
index,
|
|
156
|
+
downloads_dir=downloads_dir,
|
|
157
|
+
relative_prefix=relative_prefix,
|
|
158
|
+
inherited_width=effective_width,
|
|
159
|
+
inherited_marking=effective_marking,
|
|
160
|
+
inherited_distribution=effective_distribution
|
|
161
|
+
)
|
|
154
162
|
if el_width is not None:
|
|
155
163
|
elem = div(elem, style=style)
|
|
156
164
|
elem = div(elem, style=outer_style)
|
|
157
165
|
elements.append(elem)
|
|
158
166
|
|
|
159
|
-
#
|
|
160
|
-
|
|
167
|
+
# Expose --content-width if you need it later, but centering no longer depends on it
|
|
168
|
+
wrapper_style = (
|
|
169
|
+
f"max-width: {effective_width}px; "
|
|
170
|
+
"margin: 0 auto; width: 100%; "
|
|
171
|
+
"padding-top: 80px; padding-bottom: 80px; "
|
|
172
|
+
f"--content-width: {effective_width}px;"
|
|
173
|
+
)
|
|
174
|
+
wrapper = div(*elements, style=wrapper_style)
|
|
161
175
|
return [wrapper]
|
|
162
176
|
|
|
177
|
+
|
|
163
178
|
class MiniPage(AbstractPage):
|
|
164
179
|
def __init__(self, page_width=None):
|
|
165
180
|
super().__init__()
|
|
166
181
|
self.page_width = page_width
|
|
167
182
|
|
|
168
|
-
def render(self, index=None, downloads_dir=None, relative_prefix="", inherited_width=None):
|
|
183
|
+
def render(self, index=None, downloads_dir=None, relative_prefix="", inherited_width=None, inherited_marking=None, inherited_distribution=None):
|
|
169
184
|
effective_width = self.page_width or inherited_width
|
|
170
185
|
row_div = div(cls="minipage-row", style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%;")
|
|
171
186
|
for kind, content, el_width in self.elements:
|
|
@@ -183,10 +198,8 @@ class MiniPage(AbstractPage):
|
|
|
183
198
|
elem = header_tag(text)
|
|
184
199
|
elif kind == "plot":
|
|
185
200
|
fig = content
|
|
186
|
-
# Plotly support (existing)
|
|
187
201
|
if hasattr(fig, "to_html"):
|
|
188
202
|
elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
|
|
189
|
-
# Matplotlib support
|
|
190
203
|
else:
|
|
191
204
|
try:
|
|
192
205
|
buf = io.BytesIO()
|
|
@@ -194,7 +207,6 @@ class MiniPage(AbstractPage):
|
|
|
194
207
|
buf.seek(0)
|
|
195
208
|
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
196
209
|
buf.close()
|
|
197
|
-
# Center the image using a div with inline styles
|
|
198
210
|
elem = div(
|
|
199
211
|
raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
|
|
200
212
|
style="display: flex; justify-content: center; align-items: center;"
|
|
@@ -218,7 +230,14 @@ class MiniPage(AbstractPage):
|
|
|
218
230
|
cls="syntax-block"
|
|
219
231
|
)
|
|
220
232
|
elif kind == "minipage":
|
|
221
|
-
elem = content.render(
|
|
233
|
+
elem = content.render(
|
|
234
|
+
index,
|
|
235
|
+
downloads_dir=downloads_dir,
|
|
236
|
+
relative_prefix=relative_prefix,
|
|
237
|
+
inherited_width=effective_width,
|
|
238
|
+
inherited_marking=inherited_marking,
|
|
239
|
+
inherited_distribution=inherited_distribution
|
|
240
|
+
)
|
|
222
241
|
if el_width is not None:
|
|
223
242
|
elem = div(elem, style=style)
|
|
224
243
|
elem = div(elem, style=outer_style)
|
|
@@ -226,67 +245,50 @@ class MiniPage(AbstractPage):
|
|
|
226
245
|
row_div += cell
|
|
227
246
|
return row_div
|
|
228
247
|
|
|
248
|
+
|
|
229
249
|
class Dashboard:
|
|
230
250
|
def __init__(self, title="Dashboard", page_width=900, marking=None, distribution=None):
|
|
231
251
|
self.title = title
|
|
232
252
|
self.pages = []
|
|
233
253
|
self.page_width = page_width
|
|
234
|
-
self.marking = marking
|
|
235
|
-
self.distribution = distribution #
|
|
254
|
+
self.marking = marking # Dashboard-wide default (None => no marking)
|
|
255
|
+
self.distribution = distribution # Dashboard-wide distribution statement
|
|
236
256
|
|
|
237
257
|
def add_page(self, page):
|
|
238
258
|
self.pages.append(page)
|
|
239
|
-
|
|
240
|
-
# def _track_outline(self, flowable):
|
|
241
|
-
# """
|
|
242
|
-
# Hook for collecting TOC entries and setting bookmarks.
|
|
243
|
-
# """
|
|
244
|
-
# from reportlab.platypus import Paragraph
|
|
245
|
-
# if isinstance(flowable, Paragraph):
|
|
246
|
-
# text = flowable.getPlainText()
|
|
247
|
-
# style_name = flowable.style.name
|
|
248
|
-
# if style_name.startswith("Heading"):
|
|
249
|
-
# try:
|
|
250
|
-
# level = int(style_name.replace("Heading", ""))
|
|
251
|
-
# except ValueError:
|
|
252
|
-
# return
|
|
253
|
-
# key = f"bookmark_{uuid.uuid4().hex}"
|
|
254
|
-
# flowable.canv.bookmarkPage(key)
|
|
255
|
-
# flowable.canv.addOutlineEntry(text, key, level=level - 1, closed=False)
|
|
256
|
-
# flowable._bookmarkName = key
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def _track_outline(self, canvas, doc):
|
|
260
|
-
if hasattr(doc, '_last_heading'):
|
|
261
|
-
level, text = doc._last_heading
|
|
262
|
-
key = f"bookmark_{uuid.uuid4().hex}"
|
|
263
|
-
canvas.bookmarkPage(key)
|
|
264
|
-
canvas.addOutlineEntry(text, key, level=level - 1, closed=False)
|
|
265
|
-
del doc._last_heading
|
|
266
|
-
|
|
267
259
|
|
|
268
260
|
def _render_sidebar(self, pages, prefix="", current_slug=None):
|
|
261
|
+
# Structure preserved for your JS/CSS:
|
|
262
|
+
# <div class="sidebar-group [open]">
|
|
263
|
+
# <a class="nav-link sidebar-parent" href="...">
|
|
264
|
+
# <span class="sidebar-arrow"></span>Title
|
|
265
|
+
# </a>
|
|
266
|
+
# <div class="sidebar-children"> ... </div>
|
|
267
|
+
# </div>
|
|
269
268
|
for page in pages:
|
|
270
269
|
page_href = f"{prefix}{page.slug}.html"
|
|
271
270
|
is_active = (page.slug == current_slug)
|
|
272
|
-
|
|
271
|
+
|
|
272
|
+
def has_active_child(pg):
|
|
273
273
|
return any(
|
|
274
274
|
child.slug == current_slug or has_active_child(child)
|
|
275
|
-
for child in getattr(
|
|
275
|
+
for child in getattr(pg, "children", [])
|
|
276
276
|
)
|
|
277
|
+
|
|
277
278
|
group_open = has_active_child(page)
|
|
278
279
|
link_classes = "nav-link"
|
|
279
280
|
if getattr(page, "children", []):
|
|
280
281
|
link_classes += " sidebar-parent"
|
|
281
282
|
if is_active:
|
|
282
283
|
link_classes += " active"
|
|
284
|
+
|
|
283
285
|
if getattr(page, "children", []):
|
|
284
286
|
group_cls = "sidebar-group"
|
|
285
287
|
if group_open or is_active:
|
|
286
288
|
group_cls += " open"
|
|
287
289
|
with div(cls=group_cls):
|
|
288
290
|
a([
|
|
289
|
-
span("
|
|
291
|
+
span("", cls="sidebar-arrow"), # pure-CSS triangle (no Unicode)
|
|
290
292
|
page.title
|
|
291
293
|
], cls=link_classes, href=page_href)
|
|
292
294
|
with div(cls="sidebar-children"):
|
|
@@ -316,7 +318,8 @@ class Dashboard:
|
|
|
316
318
|
doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"))
|
|
317
319
|
doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"))
|
|
318
320
|
doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"))
|
|
319
|
-
#
|
|
321
|
+
# Defaults that match your CSS; override in CSS if they change
|
|
322
|
+
doc.head.add(raw_util("<style>:root{--sidebar-width:240px;--content-padding-x:20px;}</style>"))
|
|
320
323
|
doc.head.add(raw_util(f"<style>.content-inner {{ max-width: {effective_width}px !important; }}</style>"))
|
|
321
324
|
with doc:
|
|
322
325
|
with div(id="sidebar"):
|
|
@@ -326,17 +329,26 @@ class Dashboard:
|
|
|
326
329
|
a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
|
|
327
330
|
with div(id="content"):
|
|
328
331
|
with div(cls="content-inner"):
|
|
329
|
-
for el in page.render(
|
|
332
|
+
for el in page.render(
|
|
333
|
+
0,
|
|
334
|
+
downloads_dir=downloads_dir,
|
|
335
|
+
relative_prefix="../",
|
|
336
|
+
inherited_width=self.page_width,
|
|
337
|
+
inherited_marking=self.marking,
|
|
338
|
+
inherited_distribution=self.distribution
|
|
339
|
+
):
|
|
330
340
|
div(el)
|
|
331
|
-
|
|
341
|
+
|
|
342
|
+
with open(os.path.join(pages_dir, f"{page.slug}.html"), "w", encoding="utf-8") as f:
|
|
332
343
|
f.write(str(doc))
|
|
344
|
+
|
|
333
345
|
for child in getattr(page, "children", []):
|
|
334
346
|
write_page(child)
|
|
335
347
|
|
|
336
348
|
for page in self.pages:
|
|
337
349
|
write_page(page)
|
|
338
350
|
|
|
339
|
-
# Index page
|
|
351
|
+
# Index page (first page)
|
|
340
352
|
index_doc = document(title=self.title)
|
|
341
353
|
effective_width = self.pages[0].page_width or self.page_width or 900
|
|
342
354
|
with index_doc.head:
|
|
@@ -352,7 +364,7 @@ class Dashboard:
|
|
|
352
364
|
index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"))
|
|
353
365
|
index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-json.min.js"))
|
|
354
366
|
index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-c.min.js"))
|
|
355
|
-
|
|
367
|
+
index_doc.head.add(raw_util("<style>:root{--sidebar-width:240px;--content-padding-x:20px;}</style>"))
|
|
356
368
|
index_doc.head.add(raw_util(f"<style>.content-inner {{ max-width: {effective_width}px !important; }}</style>"))
|
|
357
369
|
with index_doc:
|
|
358
370
|
with div(id="sidebar"):
|
|
@@ -362,293 +374,15 @@ class Dashboard:
|
|
|
362
374
|
a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
|
|
363
375
|
with div(id="content"):
|
|
364
376
|
with div(cls="content-inner"):
|
|
365
|
-
for el in self.pages[0].render(
|
|
377
|
+
for el in self.pages[0].render(
|
|
378
|
+
0,
|
|
379
|
+
downloads_dir=downloads_dir,
|
|
380
|
+
relative_prefix="",
|
|
381
|
+
inherited_width=self.page_width,
|
|
382
|
+
inherited_marking=self.marking,
|
|
383
|
+
inherited_distribution=self.distribution
|
|
384
|
+
):
|
|
366
385
|
div(el)
|
|
367
386
|
|
|
368
|
-
with open(os.path.join(output_dir, "index.html"), "w") as f:
|
|
387
|
+
with open(os.path.join(output_dir, "index.html"), "w", encoding="utf-8") as f:
|
|
369
388
|
f.write(str(index_doc))
|
|
370
|
-
|
|
371
|
-
def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_toc=True, include_title_page=False, author=None, affiliation=None, title_page_marking=None):
|
|
372
|
-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, Image
|
|
373
|
-
from reportlab.platypus.tableofcontents import TableOfContents
|
|
374
|
-
from reportlab.lib.pagesizes import A4, letter
|
|
375
|
-
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
376
|
-
from reportlab.lib import colors
|
|
377
|
-
from reportlab.lib.units import inch
|
|
378
|
-
from datetime import datetime
|
|
379
|
-
import tempfile
|
|
380
|
-
import io
|
|
381
|
-
import os
|
|
382
|
-
import plotly.io as pio
|
|
383
|
-
|
|
384
|
-
pio.kaleido.scope.default_format = "png"
|
|
385
|
-
|
|
386
|
-
page_size = A4 if pagesize.upper() == "A4" else letter
|
|
387
|
-
styles = getSampleStyleSheet()
|
|
388
|
-
normal_style = styles['Normal']
|
|
389
|
-
|
|
390
|
-
styles['Heading1'].fontSize = 18
|
|
391
|
-
styles['Heading1'].spaceAfter = 12
|
|
392
|
-
styles['Heading1'].spaceBefore = 18
|
|
393
|
-
styles['Heading1'].fontName = 'Helvetica-Bold'
|
|
394
|
-
|
|
395
|
-
styles['Heading2'].fontSize = 14
|
|
396
|
-
styles['Heading2'].spaceAfter = 8
|
|
397
|
-
styles['Heading2'].spaceBefore = 12
|
|
398
|
-
styles['Heading2'].fontName = 'Helvetica-Bold'
|
|
399
|
-
|
|
400
|
-
if 'CodeBlock' not in styles:
|
|
401
|
-
styles.add(ParagraphStyle(
|
|
402
|
-
name='CodeBlock',
|
|
403
|
-
fontName='Courier',
|
|
404
|
-
fontSize=9,
|
|
405
|
-
leading=12,
|
|
406
|
-
backColor=colors.whitesmoke,
|
|
407
|
-
leftIndent=12,
|
|
408
|
-
rightIndent=12,
|
|
409
|
-
spaceAfter=8,
|
|
410
|
-
borderPadding=4
|
|
411
|
-
))
|
|
412
|
-
|
|
413
|
-
story = []
|
|
414
|
-
|
|
415
|
-
class MyDocTemplate(SimpleDocTemplate):
|
|
416
|
-
def __init__(self, *args, **kwargs):
|
|
417
|
-
self.outline_entries = []
|
|
418
|
-
self._outline_idx = 0
|
|
419
|
-
super().__init__(*args, **kwargs)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def afterFlowable(self, flowable):
|
|
423
|
-
from reportlab.platypus import Paragraph
|
|
424
|
-
if isinstance(flowable, Paragraph):
|
|
425
|
-
style_name = flowable.style.name
|
|
426
|
-
if style_name.startswith('Heading'):
|
|
427
|
-
try:
|
|
428
|
-
level = int(style_name.replace("Heading", ""))
|
|
429
|
-
except ValueError:
|
|
430
|
-
return # Not a valid heading style
|
|
431
|
-
|
|
432
|
-
# Convert to outline level (0 = H1, 1 = H2, etc.)
|
|
433
|
-
outline_level = level - 1
|
|
434
|
-
|
|
435
|
-
# Clamp max to 2 for PDF outline safety
|
|
436
|
-
outline_level = max(0, min(outline_level, 2))
|
|
437
|
-
|
|
438
|
-
# Prevent skipping levels: ensure intermediates exist
|
|
439
|
-
# Track previous levels (add this as a class attribute if needed)
|
|
440
|
-
if not hasattr(self, "_last_outline_level"):
|
|
441
|
-
self._last_outline_level = -1
|
|
442
|
-
|
|
443
|
-
if outline_level > self._last_outline_level + 1:
|
|
444
|
-
outline_level = self._last_outline_level + 1 # prevent jump
|
|
445
|
-
|
|
446
|
-
self._last_outline_level = outline_level
|
|
447
|
-
|
|
448
|
-
text = flowable.getPlainText()
|
|
449
|
-
key = 'heading_%s' % self.seq.nextf('heading')
|
|
450
|
-
self.canv.bookmarkPage(key)
|
|
451
|
-
self.canv.addOutlineEntry(text, key, level=outline_level, closed=False)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
self.notify('TOCEntry', (outline_level, text, self.page))
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
def add_marking(canvas, doc, marking):
|
|
458
|
-
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
459
|
-
|
|
460
|
-
distribution = self.distribution
|
|
461
|
-
canvas.saveState()
|
|
462
|
-
canvas.setFont("Helvetica", 10)
|
|
463
|
-
width, height = doc.pagesize
|
|
464
|
-
|
|
465
|
-
line_height = 12
|
|
466
|
-
margin_y = 36
|
|
467
|
-
|
|
468
|
-
# Top of page marking
|
|
469
|
-
if marking:
|
|
470
|
-
top_width = stringWidth(marking, "Helvetica", 10)
|
|
471
|
-
x_top = (width - top_width) / 2
|
|
472
|
-
canvas.drawString(x_top, height - margin_y, marking)
|
|
473
|
-
|
|
474
|
-
# Bottom of page — prepare to stack upward
|
|
475
|
-
y = margin_y
|
|
476
|
-
|
|
477
|
-
# First: bottom marking
|
|
478
|
-
if marking:
|
|
479
|
-
bot_width = stringWidth(marking, "Helvetica", 10)
|
|
480
|
-
x_bot = (width - bot_width) / 2
|
|
481
|
-
canvas.drawString(x_bot, y, marking)
|
|
482
|
-
y += line_height # make room above
|
|
483
|
-
|
|
484
|
-
# Then: bottom distribution (above marking)
|
|
485
|
-
if distribution:
|
|
486
|
-
max_width = width - 144 # ~1" margins
|
|
487
|
-
words = distribution.split()
|
|
488
|
-
lines = []
|
|
489
|
-
current_line = []
|
|
490
|
-
|
|
491
|
-
for word in words:
|
|
492
|
-
test_line = " ".join(current_line + [word])
|
|
493
|
-
if stringWidth(test_line, "Helvetica", 10) <= max_width:
|
|
494
|
-
current_line.append(word)
|
|
495
|
-
else:
|
|
496
|
-
lines.append(" ".join(current_line))
|
|
497
|
-
current_line = [word]
|
|
498
|
-
|
|
499
|
-
if current_line:
|
|
500
|
-
lines.append(" ".join(current_line))
|
|
501
|
-
|
|
502
|
-
# Draw top-down (above marking)
|
|
503
|
-
for line in reversed(lines):
|
|
504
|
-
line_width = stringWidth(line, "Helvetica", 10)
|
|
505
|
-
x = (width - line_width) / 2
|
|
506
|
-
y += line_height
|
|
507
|
-
canvas.drawString(x, y, line)
|
|
508
|
-
|
|
509
|
-
canvas.restoreState()
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if include_title_page:
|
|
513
|
-
story.append(Spacer(1, 120))
|
|
514
|
-
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
515
|
-
story.append(Spacer(1, 48))
|
|
516
|
-
lines = []
|
|
517
|
-
if author:
|
|
518
|
-
lines.append(str(author))
|
|
519
|
-
if affiliation:
|
|
520
|
-
lines.append(str(affiliation))
|
|
521
|
-
lines.append(datetime.now().strftime('%B %d, %Y'))
|
|
522
|
-
story.append(Paragraph("<para align='center'>" + "<br/>".join(lines) + "</para>", normal_style))
|
|
523
|
-
story.append(PageBreak())
|
|
524
|
-
|
|
525
|
-
if include_toc:
|
|
526
|
-
toc = TableOfContents()
|
|
527
|
-
toc.levelStyles = [
|
|
528
|
-
ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
|
|
529
|
-
ParagraphStyle(name='TOCHeading2', fontSize=12, leftIndent=40, firstLineIndent=-20, spaceBefore=5, leading=12),
|
|
530
|
-
]
|
|
531
|
-
story.append(Paragraph("Table of Contents", styles["Title"]))
|
|
532
|
-
story.append(toc)
|
|
533
|
-
story.append(PageBreak())
|
|
534
|
-
|
|
535
|
-
def render_page(page, level=0, sec_prefix=[]):
|
|
536
|
-
# Generate section numbering like "1", "1.2", etc.
|
|
537
|
-
section_number = ".".join(map(str, sec_prefix))
|
|
538
|
-
heading_text = f"{section_number} {page.title}"
|
|
539
|
-
|
|
540
|
-
heading_style = styles.get(f'Heading{min(level + 1, 4)}', styles['Heading4'])
|
|
541
|
-
story.append(Paragraph(heading_text, heading_style))
|
|
542
|
-
story.append(Spacer(1, 12))
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
# Remember where we started
|
|
546
|
-
content_start = len(story)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
for kind, content, _ in page.elements:
|
|
550
|
-
try:
|
|
551
|
-
if kind == "text":
|
|
552
|
-
story.append(Paragraph(content, normal_style))
|
|
553
|
-
story.append(Spacer(1, 8))
|
|
554
|
-
|
|
555
|
-
elif kind == "header":
|
|
556
|
-
text, lvl = content
|
|
557
|
-
safe_lvl = max(1, min(lvl + 1, 4))
|
|
558
|
-
style = styles[f'Heading{safe_lvl}']
|
|
559
|
-
story.append(Paragraph(text, style))
|
|
560
|
-
story.append(Spacer(1, 8))
|
|
561
|
-
|
|
562
|
-
elif kind == "table":
|
|
563
|
-
df = content
|
|
564
|
-
data = [df.columns.tolist()] + df.values.tolist()
|
|
565
|
-
t = Table(data, repeatRows=1)
|
|
566
|
-
t.setStyle(TableStyle([
|
|
567
|
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
|
|
568
|
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
569
|
-
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
570
|
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
571
|
-
('FONTSIZE', (0, 0), (-1, 0), 11),
|
|
572
|
-
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
|
573
|
-
('TOPPADDING', (0, 0), (-1, 0), 10),
|
|
574
|
-
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
|
|
575
|
-
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
|
|
576
|
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
577
|
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
|
578
|
-
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
579
|
-
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
580
|
-
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
581
|
-
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
582
|
-
]))
|
|
583
|
-
story.append(t)
|
|
584
|
-
story.append(Spacer(1, 12))
|
|
585
|
-
|
|
586
|
-
elif kind == "plot":
|
|
587
|
-
fig = content
|
|
588
|
-
buf = io.BytesIO()
|
|
589
|
-
if hasattr(fig, "savefig"):
|
|
590
|
-
fig.savefig(buf, format="png", bbox_inches="tight", dpi=600)
|
|
591
|
-
else:
|
|
592
|
-
fig.write_image(buf, format="png", width=800, height=600, scale=3)
|
|
593
|
-
buf.seek(0)
|
|
594
|
-
story.append(Image(buf, width=6 * inch, height=4.5 * inch))
|
|
595
|
-
story.append(Spacer(1, 12))
|
|
596
|
-
|
|
597
|
-
elif kind == "syntax":
|
|
598
|
-
code, language = content
|
|
599
|
-
from html import escape
|
|
600
|
-
story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
|
|
601
|
-
story.append(Spacer(1, 4))
|
|
602
|
-
code_html = escape(code).replace(" ", " ").replace("\n", "<br/>")
|
|
603
|
-
story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
|
|
604
|
-
story.append(Spacer(1, 12))
|
|
605
|
-
|
|
606
|
-
elif kind == "minipage":
|
|
607
|
-
render_page(content, level=level + 1, sec_prefix=sec_prefix)
|
|
608
|
-
|
|
609
|
-
except Exception as e:
|
|
610
|
-
story.append(Paragraph(f"Error rendering element: {e}", normal_style))
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
just_broke = False
|
|
614
|
-
|
|
615
|
-
for i, child in enumerate(page.children):
|
|
616
|
-
if i > 0 and not just_broke:
|
|
617
|
-
story.append(PageBreak())
|
|
618
|
-
|
|
619
|
-
before = len(story)
|
|
620
|
-
render_page(child, level=level + 1, sec_prefix=sec_prefix + [i + 1])
|
|
621
|
-
after = len(story)
|
|
622
|
-
|
|
623
|
-
# Determine if child added a PageBreak
|
|
624
|
-
just_broke = isinstance(story[-1], PageBreak) if after > before else False
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
# Determine if anything meaningful was added
|
|
628
|
-
def has_meaningful_content(start_idx):
|
|
629
|
-
for elem in story[start_idx:]:
|
|
630
|
-
if isinstance(elem, (Paragraph, Table, Image)):
|
|
631
|
-
return True
|
|
632
|
-
return False
|
|
633
|
-
|
|
634
|
-
if not page.children and has_meaningful_content(content_start):
|
|
635
|
-
story.append(PageBreak())
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
for i, page in enumerate(self.pages):
|
|
640
|
-
render_page(page, level=0, sec_prefix=[i + 1])
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
doc = MyDocTemplate(
|
|
645
|
-
output_path,
|
|
646
|
-
pagesize=page_size,
|
|
647
|
-
rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
|
|
648
|
-
)
|
|
649
|
-
|
|
650
|
-
doc.multiBuild(
|
|
651
|
-
story,
|
|
652
|
-
onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
|
|
653
|
-
onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
|
|
654
|
-
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
|
|
2
|
+
staticdash/dashboard.py,sha256=0RVyqL3oEi7-Ey6ZZa9Y0V3WQ6MC8YiGZ0swKtLYnEg,18233
|
|
3
|
+
staticdash/assets/css/style.css,sha256=JCoEkEzDiGc29jjhtWBMv-cirwzSWgOpCviMVIm6x2s,6533
|
|
4
|
+
staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
|
|
5
|
+
staticdash-2025.28.dist-info/METADATA,sha256=gfFu59nNzPJDjOnt5jep1zHdTmrt8PuWVXon_IJWEs8,1960
|
|
6
|
+
staticdash-2025.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
staticdash-2025.28.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
|
8
|
+
staticdash-2025.28.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
|
|
2
|
-
staticdash/dashboard.py,sha256=nqA9MLxSqZNFs-Ko54czD1kZgQ4EP9qC4HvgvR54_ds,29891
|
|
3
|
-
staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
|
|
4
|
-
staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
|
|
5
|
-
staticdash-2025.27.dist-info/METADATA,sha256=lNhzK7RFO-Ewxc6K1E7NNexBEgV7un7x14ip3VkvQ4c,1960
|
|
6
|
-
staticdash-2025.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
staticdash-2025.27.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
|
8
|
-
staticdash-2025.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|