staticdash 2025.20__tar.gz → 2025.21__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.20 → staticdash-2025.21}/PKG-INFO +1 -1
- {staticdash-2025.20 → staticdash-2025.21}/pyproject.toml +1 -1
- {staticdash-2025.20 → staticdash-2025.21}/staticdash/dashboard.py +174 -138
- {staticdash-2025.20 → staticdash-2025.21}/staticdash.egg-info/PKG-INFO +1 -1
- {staticdash-2025.20 → staticdash-2025.21}/README.md +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/setup.cfg +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/staticdash/__init__.py +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/staticdash/assets/css/style.css +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/staticdash/assets/js/script.js +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/staticdash.egg-info/SOURCES.txt +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/staticdash.egg-info/dependency_links.txt +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/staticdash.egg-info/requires.txt +0 -0
- {staticdash-2025.20 → staticdash-2025.21}/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.21"
|
|
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" }
|
|
@@ -345,158 +345,194 @@ class Dashboard:
|
|
|
345
345
|
with open(os.path.join(output_dir, "index.html"), "w") as f:
|
|
346
346
|
f.write(str(index_doc))
|
|
347
347
|
|
|
348
|
-
def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_title_page=False,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
348
|
+
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):
|
|
349
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, Image
|
|
350
|
+
from reportlab.platypus.tableofcontents import TableOfContents
|
|
351
|
+
from reportlab.lib.pagesizes import A4, letter
|
|
352
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
353
|
+
from reportlab.lib import colors
|
|
354
|
+
from reportlab.lib.units import inch
|
|
355
|
+
from datetime import datetime
|
|
356
|
+
import tempfile
|
|
357
|
+
import io
|
|
358
|
+
import os
|
|
359
|
+
import plotly.io as pio
|
|
360
|
+
|
|
361
|
+
pio.kaleido.scope.default_format = "png"
|
|
362
|
+
|
|
363
|
+
page_size = A4 if pagesize.upper() == "A4" else letter
|
|
364
|
+
styles = getSampleStyleSheet()
|
|
365
|
+
normal_style = styles['Normal']
|
|
366
|
+
|
|
367
|
+
styles['Heading1'].fontSize = 18
|
|
368
|
+
styles['Heading1'].spaceAfter = 12
|
|
369
|
+
styles['Heading1'].spaceBefore = 18
|
|
370
|
+
styles['Heading1'].fontName = 'Helvetica-Bold'
|
|
371
|
+
|
|
372
|
+
styles['Heading2'].fontSize = 14
|
|
373
|
+
styles['Heading2'].spaceAfter = 8
|
|
374
|
+
styles['Heading2'].spaceBefore = 12
|
|
375
|
+
styles['Heading2'].fontName = 'Helvetica-Bold'
|
|
376
|
+
|
|
377
|
+
if 'CodeBlock' not in styles:
|
|
378
|
+
styles.add(ParagraphStyle(
|
|
379
|
+
name='CodeBlock',
|
|
380
|
+
fontName='Courier',
|
|
381
|
+
fontSize=9,
|
|
382
|
+
leading=12,
|
|
383
|
+
backColor=colors.whitesmoke,
|
|
384
|
+
leftIndent=12,
|
|
385
|
+
rightIndent=12,
|
|
386
|
+
spaceAfter=8,
|
|
387
|
+
borderPadding=4
|
|
388
|
+
))
|
|
389
|
+
|
|
390
|
+
story = []
|
|
391
|
+
|
|
392
|
+
class MyDocTemplate(SimpleDocTemplate):
|
|
393
|
+
def __init__(self, *args, **kwargs):
|
|
394
|
+
self.outline_entries = []
|
|
395
|
+
self._outline_idx = 0
|
|
396
|
+
super().__init__(*args, **kwargs)
|
|
397
|
+
|
|
398
|
+
def afterFlowable(self, flowable):
|
|
399
|
+
if hasattr(flowable, 'style') and hasattr(flowable, 'getPlainText'):
|
|
400
|
+
style_name = flowable.style.name
|
|
401
|
+
if style_name.startswith("Heading"):
|
|
402
|
+
level = int(style_name.replace("Heading", ""))
|
|
403
|
+
text = flowable.getPlainText().strip()
|
|
404
|
+
key = f"heading_{uuid.uuid4().hex}"
|
|
405
|
+
self.canv.bookmarkPage(key)
|
|
406
|
+
self.canv.addOutlineEntry(text, key, level=level-1, closed=False)
|
|
407
|
+
self.notify('TOCEntry', (level, text, self.page))
|
|
408
|
+
|
|
409
|
+
def add_marking(canvas, doc, marking):
|
|
410
|
+
if marking:
|
|
411
|
+
canvas.saveState()
|
|
412
|
+
canvas.setFont("Helvetica", 10)
|
|
413
|
+
width, height = doc.pagesize
|
|
414
|
+
text_width = canvas.stringWidth(marking, "Helvetica", 10)
|
|
415
|
+
x = (width - text_width) / 2
|
|
416
|
+
canvas.drawString(x, height - 36, marking)
|
|
417
|
+
canvas.drawString(x, 36, marking)
|
|
418
|
+
canvas.restoreState()
|
|
419
|
+
|
|
420
|
+
if include_title_page:
|
|
421
|
+
story.append(Spacer(1, 120))
|
|
422
|
+
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
423
|
+
story.append(Spacer(1, 48))
|
|
424
|
+
lines = []
|
|
425
|
+
if author:
|
|
426
|
+
lines.append(str(author))
|
|
427
|
+
if affiliation:
|
|
428
|
+
lines.append(str(affiliation))
|
|
429
|
+
lines.append(datetime.now().strftime('%B %d, %Y'))
|
|
430
|
+
story.append(Paragraph("<para align='center'>" + "<br/>".join(lines) + "</para>", normal_style))
|
|
431
|
+
story.append(PageBreak())
|
|
371
432
|
|
|
372
|
-
|
|
433
|
+
if include_toc:
|
|
373
434
|
toc = TableOfContents()
|
|
374
435
|
toc.levelStyles = [
|
|
375
436
|
ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
|
|
376
437
|
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
438
|
]
|
|
379
439
|
story.append(Paragraph("Table of Contents", styles["Title"]))
|
|
380
440
|
story.append(toc)
|
|
381
441
|
story.append(PageBreak())
|
|
382
442
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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)
|
|
443
|
+
def render_page(page, level=0, sec_prefix=[]):
|
|
444
|
+
heading_style = styles['Heading1'] if level == 0 else styles['Heading2']
|
|
445
|
+
story.append(Paragraph(page.title, heading_style))
|
|
446
|
+
story.append(Spacer(1, 12))
|
|
447
|
+
|
|
448
|
+
for kind, content, _ in page.elements:
|
|
449
|
+
if kind == "text":
|
|
450
|
+
story.append(Paragraph(content, normal_style))
|
|
451
|
+
story.append(Spacer(1, 8))
|
|
452
|
+
|
|
453
|
+
elif kind == "header":
|
|
454
|
+
text, lvl = content
|
|
455
|
+
style = styles['Heading{}'.format(min(lvl + 1, 4))]
|
|
456
|
+
story.append(Paragraph(text, style))
|
|
457
|
+
story.append(Spacer(1, 8))
|
|
458
|
+
|
|
459
|
+
elif kind == "table":
|
|
460
|
+
df = content
|
|
461
|
+
try:
|
|
462
|
+
data = [df.columns.tolist()] + df.values.tolist()
|
|
463
|
+
t = Table(data, repeatRows=1)
|
|
464
|
+
t.setStyle(TableStyle([
|
|
465
|
+
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
|
|
466
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
467
|
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
468
|
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
469
|
+
('FONTSIZE', (0, 0), (-1, 0), 11),
|
|
470
|
+
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
|
471
|
+
('TOPPADDING', (0, 0), (-1, 0), 10),
|
|
472
|
+
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
|
|
473
|
+
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
|
|
474
|
+
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
475
|
+
('FONTSIZE', (0, 1), (-1, -1), 10),
|
|
476
|
+
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
477
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
478
|
+
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
479
|
+
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
480
|
+
]))
|
|
481
|
+
story.append(t)
|
|
482
|
+
story.append(Spacer(1, 12))
|
|
483
|
+
except Exception:
|
|
484
|
+
story.append(Paragraph("Table could not be rendered.", normal_style))
|
|
485
|
+
|
|
486
|
+
elif kind == "plot":
|
|
487
|
+
fig = content
|
|
488
|
+
try:
|
|
489
|
+
if hasattr(fig, "savefig"): # Matplotlib
|
|
490
|
+
buf = io.BytesIO()
|
|
491
|
+
fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
|
492
|
+
buf.seek(0)
|
|
493
|
+
story.append(Spacer(1, 8))
|
|
494
|
+
story.append(Image(buf, width=6*inch, height=4.5*inch))
|
|
440
495
|
story.append(Spacer(1, 12))
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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)
|
|
496
|
+
elif hasattr(fig, "write_image"): # Plotly
|
|
497
|
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
|
|
498
|
+
fig.write_image(tmpfile.name, width=600, height=360, scale=2)
|
|
499
|
+
with open(tmpfile.name, "rb") as f:
|
|
500
|
+
story.append(Spacer(1, 8))
|
|
501
|
+
story.append(Image(io.BytesIO(f.read()), width=6*inch, height=3.6*inch))
|
|
502
|
+
story.append(Spacer(1, 12))
|
|
503
|
+
os.unlink(tmpfile.name)
|
|
504
|
+
except Exception as e:
|
|
505
|
+
story.append(Paragraph(f"Plot rendering failed: {e}", normal_style))
|
|
482
506
|
|
|
483
|
-
|
|
484
|
-
|
|
507
|
+
elif kind == "syntax":
|
|
508
|
+
code, language = content
|
|
509
|
+
from html import escape
|
|
510
|
+
story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
|
|
511
|
+
story.append(Spacer(1, 4))
|
|
512
|
+
code_html = escape(code).replace(" ", " ").replace("\n", "<br/>")
|
|
513
|
+
story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
|
|
514
|
+
story.append(Spacer(1, 12))
|
|
485
515
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
render_page(child)
|
|
516
|
+
elif kind == "minipage":
|
|
517
|
+
render_page(content, level=level + 1, sec_prefix=sec_prefix)
|
|
489
518
|
|
|
490
|
-
for
|
|
491
|
-
render_page(page)
|
|
519
|
+
for child in getattr(page, "children", []):
|
|
492
520
|
story.append(PageBreak())
|
|
521
|
+
render_page(child, level=level + 1, sec_prefix=sec_prefix + [1])
|
|
493
522
|
|
|
494
|
-
|
|
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)
|
|
523
|
+
story.append(PageBreak())
|
|
497
524
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
525
|
+
for page in self.pages:
|
|
526
|
+
render_page(page)
|
|
527
|
+
|
|
528
|
+
doc = MyDocTemplate(
|
|
529
|
+
output_path,
|
|
530
|
+
pagesize=page_size,
|
|
531
|
+
rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
doc.multiBuild(
|
|
535
|
+
story,
|
|
536
|
+
onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
|
|
537
|
+
onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
|
|
538
|
+
)
|
|
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
|