staticdash 2025.19__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.19 → staticdash-2025.21}/PKG-INFO +1 -1
- {staticdash-2025.19 → staticdash-2025.21}/pyproject.toml +1 -1
- {staticdash-2025.19 → staticdash-2025.21}/staticdash/dashboard.py +154 -55
- {staticdash-2025.19 → staticdash-2025.21}/staticdash.egg-info/PKG-INFO +1 -1
- {staticdash-2025.19 → staticdash-2025.21}/README.md +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/setup.cfg +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/staticdash/__init__.py +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/staticdash/assets/css/style.css +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/staticdash/assets/js/script.js +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/staticdash.egg-info/SOURCES.txt +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/staticdash.egg-info/dependency_links.txt +0 -0
- {staticdash-2025.19 → staticdash-2025.21}/staticdash.egg-info/requires.txt +0 -0
- {staticdash-2025.19 → 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" }
|
|
@@ -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:
|
|
@@ -316,57 +345,117 @@ class Dashboard:
|
|
|
316
345
|
with open(os.path.join(output_dir, "index.html"), "w") as f:
|
|
317
346
|
f.write(str(index_doc))
|
|
318
347
|
|
|
319
|
-
def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_title_page=False,
|
|
320
|
-
from reportlab.platypus import SimpleDocTemplate,
|
|
321
|
-
from reportlab.
|
|
322
|
-
from reportlab.lib.
|
|
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
|
|
323
355
|
from datetime import datetime
|
|
356
|
+
import tempfile
|
|
324
357
|
import io
|
|
358
|
+
import os
|
|
359
|
+
import plotly.io as pio
|
|
325
360
|
|
|
326
|
-
|
|
361
|
+
pio.kaleido.scope.default_format = "png"
|
|
362
|
+
|
|
363
|
+
page_size = A4 if pagesize.upper() == "A4" else letter
|
|
327
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
|
+
|
|
328
390
|
story = []
|
|
329
391
|
|
|
330
|
-
|
|
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
|
+
|
|
331
420
|
if include_title_page:
|
|
332
421
|
story.append(Spacer(1, 120))
|
|
333
422
|
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
334
423
|
story.append(Spacer(1, 48))
|
|
335
|
-
|
|
336
|
-
# Center author, affiliation, and date
|
|
424
|
+
lines = []
|
|
337
425
|
if author:
|
|
338
|
-
|
|
426
|
+
lines.append(str(author))
|
|
339
427
|
if affiliation:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
story.append(Paragraph(
|
|
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())
|
|
343
432
|
|
|
433
|
+
if include_toc:
|
|
434
|
+
toc = TableOfContents()
|
|
435
|
+
toc.levelStyles = [
|
|
436
|
+
ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
|
|
437
|
+
ParagraphStyle(name='TOCHeading2', fontSize=12, leftIndent=40, firstLineIndent=-20, spaceBefore=5, leading=12),
|
|
438
|
+
]
|
|
439
|
+
story.append(Paragraph("Table of Contents", styles["Title"]))
|
|
440
|
+
story.append(toc)
|
|
344
441
|
story.append(PageBreak())
|
|
345
442
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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()
|
|
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))
|
|
357
447
|
|
|
358
|
-
# Recursive function to render pages and subpages
|
|
359
|
-
def render_page(page):
|
|
360
|
-
# Render the current page
|
|
361
448
|
for kind, content, _ in page.elements:
|
|
362
449
|
if kind == "text":
|
|
363
|
-
story.append(Paragraph(content,
|
|
450
|
+
story.append(Paragraph(content, normal_style))
|
|
364
451
|
story.append(Spacer(1, 8))
|
|
452
|
+
|
|
365
453
|
elif kind == "header":
|
|
366
|
-
text,
|
|
367
|
-
|
|
368
|
-
story.append(Paragraph(text,
|
|
454
|
+
text, lvl = content
|
|
455
|
+
style = styles['Heading{}'.format(min(lvl + 1, 4))]
|
|
456
|
+
story.append(Paragraph(text, style))
|
|
369
457
|
story.append(Spacer(1, 8))
|
|
458
|
+
|
|
370
459
|
elif kind == "table":
|
|
371
460
|
df = content
|
|
372
461
|
try:
|
|
@@ -392,47 +481,57 @@ class Dashboard:
|
|
|
392
481
|
story.append(t)
|
|
393
482
|
story.append(Spacer(1, 12))
|
|
394
483
|
except Exception:
|
|
395
|
-
story.append(Paragraph("Table could not be rendered.",
|
|
484
|
+
story.append(Paragraph("Table could not be rendered.", normal_style))
|
|
485
|
+
|
|
396
486
|
elif kind == "plot":
|
|
397
487
|
fig = content
|
|
398
488
|
try:
|
|
399
|
-
|
|
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"):
|
|
489
|
+
if hasattr(fig, "savefig"): # Matplotlib
|
|
408
490
|
buf = io.BytesIO()
|
|
409
491
|
fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
|
410
492
|
buf.seek(0)
|
|
411
|
-
|
|
412
|
-
story.append(
|
|
493
|
+
story.append(Spacer(1, 8))
|
|
494
|
+
story.append(Image(buf, width=6*inch, height=4.5*inch))
|
|
413
495
|
story.append(Spacer(1, 12))
|
|
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)
|
|
414
504
|
except Exception as e:
|
|
415
|
-
story.append(Paragraph(f"Plot
|
|
505
|
+
story.append(Paragraph(f"Plot rendering failed: {e}", normal_style))
|
|
506
|
+
|
|
416
507
|
elif kind == "syntax":
|
|
417
|
-
|
|
418
|
-
|
|
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))
|
|
515
|
+
|
|
419
516
|
elif kind == "minipage":
|
|
420
|
-
|
|
421
|
-
pass
|
|
517
|
+
render_page(content, level=level + 1, sec_prefix=sec_prefix)
|
|
422
518
|
|
|
423
|
-
# Recursively render subpages
|
|
424
519
|
for child in getattr(page, "children", []):
|
|
425
|
-
story.append(PageBreak())
|
|
426
|
-
render_page(child)
|
|
520
|
+
story.append(PageBreak())
|
|
521
|
+
render_page(child, level=level + 1, sec_prefix=sec_prefix + [1])
|
|
522
|
+
|
|
523
|
+
story.append(PageBreak())
|
|
427
524
|
|
|
428
|
-
# Render all pages
|
|
429
525
|
for page in self.pages:
|
|
430
526
|
render_page(page)
|
|
431
|
-
story.append(PageBreak()) # Add a PageBreak after each top-level page
|
|
432
527
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
528
|
+
doc = MyDocTemplate(
|
|
529
|
+
output_path,
|
|
530
|
+
pagesize=page_size,
|
|
531
|
+
rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
doc.multiBuild(
|
|
436
535
|
story,
|
|
437
536
|
onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
|
|
438
537
|
onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
|
|
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
|