staticdash 2025.19__tar.gz → 2025.20__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.19 → staticdash-2025.20}/PKG-INFO +1 -1
- {staticdash-2025.19 → staticdash-2025.20}/pyproject.toml +1 -1
- {staticdash-2025.19 → staticdash-2025.20}/staticdash/dashboard.py +180 -117
- {staticdash-2025.19 → staticdash-2025.20}/staticdash.egg-info/PKG-INFO +1 -1
- {staticdash-2025.19 → staticdash-2025.20}/README.md +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/setup.cfg +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash/__init__.py +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash/assets/css/style.css +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash/assets/js/script.js +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash.egg-info/SOURCES.txt +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash.egg-info/dependency_links.txt +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash.egg-info/requires.txt +0 -0
- {staticdash-2025.19 → staticdash-2025.20}/staticdash.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "staticdash"
|
|
7
|
-
version = "2025.
|
|
7
|
+
version = "2025.20"
|
|
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" }
|
|
@@ -22,6 +22,7 @@ class AbstractPage:
|
|
|
22
22
|
def __init__(self):
|
|
23
23
|
self.elements = []
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
def add_header(self, text, level=1, width=None):
|
|
26
27
|
if level not in (1, 2, 3, 4):
|
|
27
28
|
raise ValueError("Header level must be 1, 2, 3, or 4")
|
|
@@ -212,6 +213,34 @@ class Dashboard:
|
|
|
212
213
|
|
|
213
214
|
def add_page(self, page):
|
|
214
215
|
self.pages.append(page)
|
|
216
|
+
|
|
217
|
+
# def _track_outline(self, flowable):
|
|
218
|
+
# """
|
|
219
|
+
# Hook for collecting TOC entries and setting bookmarks.
|
|
220
|
+
# """
|
|
221
|
+
# from reportlab.platypus import Paragraph
|
|
222
|
+
# if isinstance(flowable, Paragraph):
|
|
223
|
+
# text = flowable.getPlainText()
|
|
224
|
+
# style_name = flowable.style.name
|
|
225
|
+
# if style_name.startswith("Heading"):
|
|
226
|
+
# try:
|
|
227
|
+
# level = int(style_name.replace("Heading", ""))
|
|
228
|
+
# except ValueError:
|
|
229
|
+
# return
|
|
230
|
+
# key = f"bookmark_{uuid.uuid4().hex}"
|
|
231
|
+
# flowable.canv.bookmarkPage(key)
|
|
232
|
+
# flowable.canv.addOutlineEntry(text, key, level=level - 1, closed=False)
|
|
233
|
+
# flowable._bookmarkName = key
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _track_outline(self, canvas, doc):
|
|
237
|
+
if hasattr(doc, '_last_heading'):
|
|
238
|
+
level, text = doc._last_heading
|
|
239
|
+
key = f"bookmark_{uuid.uuid4().hex}"
|
|
240
|
+
canvas.bookmarkPage(key)
|
|
241
|
+
canvas.addOutlineEntry(text, key, level=level - 1, closed=False)
|
|
242
|
+
del doc._last_heading
|
|
243
|
+
|
|
215
244
|
|
|
216
245
|
def _render_sidebar(self, pages, prefix="", current_slug=None):
|
|
217
246
|
for page in pages:
|
|
@@ -317,123 +346,157 @@ class Dashboard:
|
|
|
317
346
|
f.write(str(index_doc))
|
|
318
347
|
|
|
319
348
|
def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_title_page=False, title_page_marking=None, author=None, affiliation=None):
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
story.append(Paragraph(f"<para align='center'>{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
from reportlab.platypus import SimpleDocTemplate, Spacer, Paragraph, PageBreak, Image
|
|
350
|
+
from reportlab.lib.pagesizes import letter, A4
|
|
351
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
352
|
+
from datetime import datetime
|
|
353
|
+
import io
|
|
354
|
+
|
|
355
|
+
page_size = A4 if pagesize == "A4" else letter
|
|
356
|
+
styles = getSampleStyleSheet()
|
|
357
|
+
story = []
|
|
358
|
+
|
|
359
|
+
# Add title page
|
|
360
|
+
if include_title_page:
|
|
361
|
+
story.append(Spacer(1, 120))
|
|
362
|
+
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
363
|
+
story.append(Spacer(1, 48))
|
|
364
|
+
if author:
|
|
365
|
+
story.append(Paragraph(f"<para align='center'>{author}</para>", styles['Normal']))
|
|
366
|
+
if affiliation:
|
|
367
|
+
story.append(Paragraph(f"<para align='center'>{affiliation}</para>", styles['Normal']))
|
|
368
|
+
current_date = datetime.now().strftime("%B %d, %Y")
|
|
369
|
+
story.append(Paragraph(f"<para align='center'>{current_date}</para>", styles['Normal']))
|
|
370
|
+
story.append(PageBreak())
|
|
371
|
+
|
|
372
|
+
# Add Table of Contents
|
|
373
|
+
toc = TableOfContents()
|
|
374
|
+
toc.levelStyles = [
|
|
375
|
+
ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
|
|
376
|
+
ParagraphStyle(name='TOCHeading2', fontSize=12, leftIndent=40, firstLineIndent=-20, spaceBefore=5, leading=12),
|
|
377
|
+
ParagraphStyle(name='TOCHeading3', fontSize=10, leftIndent=60, firstLineIndent=-20, spaceBefore=5, leading=10),
|
|
378
|
+
]
|
|
379
|
+
story.append(Paragraph("Table of Contents", styles["Title"]))
|
|
380
|
+
story.append(toc)
|
|
344
381
|
story.append(PageBreak())
|
|
345
382
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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)
|
|
383
|
+
def add_marking(canvas, doc, marking):
|
|
384
|
+
if marking:
|
|
385
|
+
canvas.saveState()
|
|
386
|
+
canvas.setFont("Helvetica", 10)
|
|
387
|
+
page_width = doc.pagesize[0]
|
|
388
|
+
text_width = canvas.stringWidth(marking, "Helvetica", 10)
|
|
389
|
+
x_position = (page_width - text_width) / 2
|
|
390
|
+
canvas.drawString(x_position, doc.pagesize[1] - 36, marking)
|
|
391
|
+
canvas.drawString(x_position, 36, marking)
|
|
392
|
+
canvas.restoreState()
|
|
393
|
+
|
|
394
|
+
def render_page(page):
|
|
395
|
+
for kind, content, _ in page.elements:
|
|
396
|
+
if kind == "text":
|
|
397
|
+
story.append(Paragraph(content, styles['Normal']))
|
|
398
|
+
story.append(Spacer(1, 8))
|
|
399
|
+
|
|
400
|
+
elif kind == "header":
|
|
401
|
+
text, level = content
|
|
402
|
+
style_key = f'Heading{min(level + 1, 4)}'
|
|
403
|
+
header_style = styles[style_key]
|
|
404
|
+
para = Paragraph(text, header_style)
|
|
405
|
+
story.append(para)
|
|
406
|
+
story.append(Spacer(1, 8))
|
|
407
|
+
|
|
408
|
+
# Capture this heading for TOC/outline
|
|
409
|
+
def capture_heading(canvas, doc, level=level, text=text):
|
|
410
|
+
doc._last_heading = (level, text)
|
|
411
|
+
|
|
412
|
+
# Hack: attach postRender callback to dummy flowable
|
|
413
|
+
spacer = Spacer(0, 0)
|
|
414
|
+
spacer.__dict__["postRender"] = capture_heading
|
|
415
|
+
story.append(spacer)
|
|
416
|
+
|
|
417
|
+
elif kind == "table":
|
|
418
|
+
df = content
|
|
419
|
+
try:
|
|
420
|
+
data = [df.columns.tolist()] + df.values.tolist()
|
|
421
|
+
t = Table(data, repeatRows=1)
|
|
422
|
+
t.setStyle(TableStyle([
|
|
423
|
+
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
|
|
424
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
425
|
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
426
|
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
427
|
+
('FONTSIZE', (0, 0), (-1, 0), 11),
|
|
428
|
+
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
|
429
|
+
('TOPPADDING', (0, 0), (-1, 0), 10),
|
|
430
|
+
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
|
|
431
|
+
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
|
|
432
|
+
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
433
|
+
('FONTSIZE', (0, 1), (-1, -1), 10),
|
|
434
|
+
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
435
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
436
|
+
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
437
|
+
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
438
|
+
]))
|
|
439
|
+
story.append(t)
|
|
413
440
|
story.append(Spacer(1, 12))
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
441
|
+
except Exception:
|
|
442
|
+
story.append(Paragraph("Table could not be rendered.", styles['Normal']))
|
|
443
|
+
elif kind == "plot":
|
|
444
|
+
fig = content
|
|
445
|
+
try:
|
|
446
|
+
if hasattr(fig, "to_image"):
|
|
447
|
+
img_bytes = fig.to_image(format="png", width=800, height=600, scale=2)
|
|
448
|
+
img_buffer = io.BytesIO(img_bytes)
|
|
449
|
+
img = Image(img_buffer, width=6 * inch, height=4.5 * inch)
|
|
450
|
+
story.append(img)
|
|
451
|
+
story.append(Spacer(1, 12))
|
|
452
|
+
elif hasattr(fig, "savefig"):
|
|
453
|
+
buf = io.BytesIO()
|
|
454
|
+
fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
|
455
|
+
buf.seek(0)
|
|
456
|
+
img = Image(buf, width=6 * inch, height=4.5 * inch)
|
|
457
|
+
story.append(img)
|
|
458
|
+
story.append(Spacer(1, 12))
|
|
459
|
+
except Exception as e:
|
|
460
|
+
story.append(Paragraph(f"Plot could not be rendered: {e}", styles['Normal']))
|
|
461
|
+
elif kind == "syntax":
|
|
462
|
+
pass
|
|
463
|
+
elif kind == "syntax":
|
|
464
|
+
code, language = content
|
|
465
|
+
style = ParagraphStyle(
|
|
466
|
+
name='CodeBlock',
|
|
467
|
+
fontName='Courier',
|
|
468
|
+
fontSize=8,
|
|
469
|
+
leading=10,
|
|
470
|
+
leftIndent=12,
|
|
471
|
+
rightIndent=12,
|
|
472
|
+
spaceBefore=6,
|
|
473
|
+
spaceAfter=6,
|
|
474
|
+
borderPadding=6,
|
|
475
|
+
backColor=colors.whitesmoke
|
|
476
|
+
)
|
|
477
|
+
# Escape special characters for XML
|
|
478
|
+
from xml.sax.saxutils import escape
|
|
479
|
+
code_escaped = escape(code)
|
|
480
|
+
para = Paragraph(f"<pre>{code_escaped}</pre>", style)
|
|
481
|
+
story.append(para)
|
|
482
|
+
|
|
483
|
+
elif kind == "minipage":
|
|
484
|
+
pass
|
|
485
|
+
|
|
486
|
+
for child in getattr(page, "children", []):
|
|
487
|
+
story.append(PageBreak())
|
|
488
|
+
render_page(child)
|
|
489
|
+
|
|
490
|
+
for page in self.pages:
|
|
491
|
+
render_page(page)
|
|
492
|
+
story.append(PageBreak())
|
|
493
|
+
|
|
494
|
+
doc = SimpleDocTemplate(output_path, pagesize=page_size)
|
|
495
|
+
# doc.afterFlowable = self._track_outline
|
|
496
|
+
doc.afterFlowable = lambda flowable: getattr(flowable, "postRender", lambda c, d: None)(doc.canv, doc) or self._track_outline(doc.canv, doc)
|
|
497
|
+
|
|
498
|
+
doc.build(
|
|
499
|
+
story,
|
|
500
|
+
onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
|
|
501
|
+
onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
|
|
502
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|