staticdash 2025.20__py3-none-any.whl → 2025.22__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/dashboard.py +213 -146
- {staticdash-2025.20.dist-info → staticdash-2025.22.dist-info}/METADATA +1 -1
- staticdash-2025.22.dist-info/RECORD +8 -0
- staticdash-2025.20.dist-info/RECORD +0 -8
- {staticdash-2025.20.dist-info → staticdash-2025.22.dist-info}/WHEEL +0 -0
- {staticdash-2025.20.dist-info → staticdash-2025.22.dist-info}/top_level.txt +0 -0
staticdash/dashboard.py
CHANGED
|
@@ -56,7 +56,7 @@ class Page(AbstractPage):
|
|
|
56
56
|
self.page_width = page_width
|
|
57
57
|
self.marking = marking # Page-specific marking
|
|
58
58
|
self.children = []
|
|
59
|
-
self.add_header(title, level=1)
|
|
59
|
+
# self.add_header(title, level=1)
|
|
60
60
|
|
|
61
61
|
def add_subpage(self, page):
|
|
62
62
|
self.children.append(page)
|
|
@@ -345,158 +345,225 @@ 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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
|
|
399
|
+
def afterFlowable(self, flowable):
|
|
400
|
+
from reportlab.platypus import Paragraph
|
|
401
|
+
if isinstance(flowable, Paragraph):
|
|
402
|
+
style_name = flowable.style.name
|
|
403
|
+
if style_name.startswith('Heading'):
|
|
404
|
+
try:
|
|
405
|
+
level = int(style_name.replace("Heading", ""))
|
|
406
|
+
except ValueError:
|
|
407
|
+
return # Not a valid heading style
|
|
408
|
+
|
|
409
|
+
# Convert to outline level (0 = H1, 1 = H2, etc.)
|
|
410
|
+
outline_level = level - 1
|
|
411
|
+
|
|
412
|
+
# Clamp max to 2 for PDF outline safety
|
|
413
|
+
outline_level = max(0, min(outline_level, 2))
|
|
414
|
+
|
|
415
|
+
# Prevent skipping levels: ensure intermediates exist
|
|
416
|
+
# Track previous levels (add this as a class attribute if needed)
|
|
417
|
+
if not hasattr(self, "_last_outline_level"):
|
|
418
|
+
self._last_outline_level = -1
|
|
419
|
+
|
|
420
|
+
if outline_level > self._last_outline_level + 1:
|
|
421
|
+
outline_level = self._last_outline_level + 1 # prevent jump
|
|
422
|
+
|
|
423
|
+
self._last_outline_level = outline_level
|
|
424
|
+
|
|
425
|
+
text = flowable.getPlainText()
|
|
426
|
+
key = 'heading_%s' % self.seq.nextf('heading')
|
|
427
|
+
self.canv.bookmarkPage(key)
|
|
428
|
+
self.canv.addOutlineEntry(text, key, level=outline_level, closed=False)
|
|
429
|
+
|
|
430
|
+
self.notify('TOCEntry', (outline_level, text, self.page))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def add_marking(canvas, doc, marking):
|
|
435
|
+
if marking:
|
|
436
|
+
canvas.saveState()
|
|
437
|
+
canvas.setFont("Helvetica", 10)
|
|
438
|
+
width, height = doc.pagesize
|
|
439
|
+
text_width = canvas.stringWidth(marking, "Helvetica", 10)
|
|
440
|
+
x = (width - text_width) / 2
|
|
441
|
+
canvas.drawString(x, height - 36, marking)
|
|
442
|
+
canvas.drawString(x, 36, marking)
|
|
443
|
+
canvas.restoreState()
|
|
444
|
+
|
|
445
|
+
if include_title_page:
|
|
446
|
+
story.append(Spacer(1, 120))
|
|
447
|
+
story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
|
|
448
|
+
story.append(Spacer(1, 48))
|
|
449
|
+
lines = []
|
|
450
|
+
if author:
|
|
451
|
+
lines.append(str(author))
|
|
452
|
+
if affiliation:
|
|
453
|
+
lines.append(str(affiliation))
|
|
454
|
+
lines.append(datetime.now().strftime('%B %d, %Y'))
|
|
455
|
+
story.append(Paragraph("<para align='center'>" + "<br/>".join(lines) + "</para>", normal_style))
|
|
456
|
+
story.append(PageBreak())
|
|
457
|
+
|
|
458
|
+
if include_toc:
|
|
373
459
|
toc = TableOfContents()
|
|
374
460
|
toc.levelStyles = [
|
|
375
461
|
ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
|
|
376
462
|
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
463
|
]
|
|
379
464
|
story.append(Paragraph("Table of Contents", styles["Title"]))
|
|
380
465
|
story.append(toc)
|
|
381
466
|
story.append(PageBreak())
|
|
382
467
|
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
story.append(t)
|
|
468
|
+
def render_page(page, level=0, sec_prefix=[]):
|
|
469
|
+
heading_style = styles['Heading1'] if level == 0 else styles['Heading2']
|
|
470
|
+
|
|
471
|
+
# Only add the page.title as a real heading if it's a top-level page
|
|
472
|
+
if level == 0:
|
|
473
|
+
story.append(Paragraph(page.title, heading_style))
|
|
474
|
+
story.append(Spacer(1, 12))
|
|
475
|
+
|
|
476
|
+
for kind, content, _ in page.elements:
|
|
477
|
+
if kind == "text":
|
|
478
|
+
story.append(Paragraph(content, normal_style))
|
|
479
|
+
story.append(Spacer(1, 8))
|
|
480
|
+
|
|
481
|
+
elif kind == "header":
|
|
482
|
+
text, lvl = content
|
|
483
|
+
safe_lvl = max(1, min(lvl + 1, 4)) # Clamp to Heading1–Heading4
|
|
484
|
+
style = styles[f'Heading{safe_lvl}']
|
|
485
|
+
story.append(Paragraph(text, style))
|
|
486
|
+
story.append(Spacer(1, 8))
|
|
487
|
+
|
|
488
|
+
elif kind == "table":
|
|
489
|
+
df = content
|
|
490
|
+
try:
|
|
491
|
+
data = [df.columns.tolist()] + df.values.tolist()
|
|
492
|
+
t = Table(data, repeatRows=1)
|
|
493
|
+
t.setStyle(TableStyle([
|
|
494
|
+
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
|
|
495
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
496
|
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
497
|
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
498
|
+
('FONTSIZE', (0, 0), (-1, 0), 11),
|
|
499
|
+
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
|
500
|
+
('TOPPADDING', (0, 0), (-1, 0), 10),
|
|
501
|
+
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
|
|
502
|
+
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
|
|
503
|
+
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
504
|
+
('FONTSIZE', (0, 1), (-1, -1), 10),
|
|
505
|
+
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
506
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
507
|
+
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
508
|
+
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
509
|
+
]))
|
|
510
|
+
story.append(t)
|
|
511
|
+
story.append(Spacer(1, 12))
|
|
512
|
+
except Exception:
|
|
513
|
+
story.append(Paragraph("Table could not be rendered.", normal_style))
|
|
514
|
+
|
|
515
|
+
elif kind == "plot":
|
|
516
|
+
fig = content
|
|
517
|
+
try:
|
|
518
|
+
if hasattr(fig, "savefig"): # Matplotlib
|
|
519
|
+
buf = io.BytesIO()
|
|
520
|
+
fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
|
521
|
+
buf.seek(0)
|
|
522
|
+
story.append(Spacer(1, 8))
|
|
523
|
+
story.append(Image(buf, width=6*inch, height=4.5*inch))
|
|
440
524
|
story.append(Spacer(1, 12))
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
)
|
|
525
|
+
elif hasattr(fig, "write_image"): # Plotly
|
|
526
|
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
|
|
527
|
+
fig.write_image(tmpfile.name, width=600, height=360, scale=2)
|
|
528
|
+
with open(tmpfile.name, "rb") as f:
|
|
529
|
+
story.append(Spacer(1, 8))
|
|
530
|
+
story.append(Image(io.BytesIO(f.read()), width=6*inch, height=3.6*inch))
|
|
531
|
+
story.append(Spacer(1, 12))
|
|
532
|
+
os.unlink(tmpfile.name)
|
|
533
|
+
except Exception as e:
|
|
534
|
+
story.append(Paragraph(f"Plot rendering failed: {e}", normal_style))
|
|
535
|
+
|
|
536
|
+
elif kind == "syntax":
|
|
537
|
+
code, language = content
|
|
538
|
+
from html import escape
|
|
539
|
+
story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
|
|
540
|
+
story.append(Spacer(1, 4))
|
|
541
|
+
code_html = escape(code).replace(" ", " ").replace("\n", "<br/>")
|
|
542
|
+
story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
|
|
543
|
+
story.append(Spacer(1, 12))
|
|
544
|
+
|
|
545
|
+
elif kind == "minipage":
|
|
546
|
+
render_page(content, level=level + 1, sec_prefix=sec_prefix)
|
|
547
|
+
|
|
548
|
+
# for child in getattr(page, "children", []):
|
|
549
|
+
# story.append(PageBreak())
|
|
550
|
+
# render_page(child, level=level + 2, sec_prefix=sec_prefix + [1])
|
|
551
|
+
|
|
552
|
+
story.append(PageBreak())
|
|
553
|
+
|
|
554
|
+
for page in self.pages:
|
|
555
|
+
render_page(page)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
doc = MyDocTemplate(
|
|
560
|
+
output_path,
|
|
561
|
+
pagesize=page_size,
|
|
562
|
+
rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
doc.multiBuild(
|
|
566
|
+
story,
|
|
567
|
+
onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
|
|
568
|
+
onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
|
|
569
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
|
|
2
|
+
staticdash/dashboard.py,sha256=YPpfUFHwMo0olakaviFdJFDRnMHbTeR5XtXz9yUsxGU,27120
|
|
3
|
+
staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
|
|
4
|
+
staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
|
|
5
|
+
staticdash-2025.22.dist-info/METADATA,sha256=KpCDLsvS8fGjFe_AZ6zZyjUl2LICSNb5lFCdTuQIGZk,1960
|
|
6
|
+
staticdash-2025.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
staticdash-2025.22.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
|
8
|
+
staticdash-2025.22.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
|
|
2
|
-
staticdash/dashboard.py,sha256=YkP5ZXQQupIh5S0PuKXihoAtCitJRBTaUvD58bZuNSo,25190
|
|
3
|
-
staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
|
|
4
|
-
staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
|
|
5
|
-
staticdash-2025.20.dist-info/METADATA,sha256=FYxpbdPmwurhbyawG79E2jj9K4csiRcjadusnoDLH44,1960
|
|
6
|
-
staticdash-2025.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
staticdash-2025.20.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
|
8
|
-
staticdash-2025.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|