staticdash 2025.20__py3-none-any.whl → 2025.21__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 CHANGED
@@ -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, title_page_marking=None, author=None, affiliation=None):
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())
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
- # Add Table of Contents
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
- 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)
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
- 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)
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
- elif kind == "minipage":
484
- pass
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(" ", "&nbsp;").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
- for child in getattr(page, "children", []):
487
- story.append(PageBreak())
488
- render_page(child)
516
+ elif kind == "minipage":
517
+ render_page(content, level=level + 1, sec_prefix=sec_prefix)
489
518
 
490
- for page in self.pages:
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
- 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)
523
+ story.append(PageBreak())
497
524
 
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
+ 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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.20
3
+ Version: 2025.21
4
4
  Summary: A lightweight static HTML dashboard generator with Plotly and pandas support.
5
5
  Author-email: Brian Day <brian.day1@gmail.com>
6
6
  License: CC0-1.0
@@ -0,0 +1,8 @@
1
+ staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
2
+ staticdash/dashboard.py,sha256=b6xx5-zdSCzbcaBTe52flSXrK_cdAo3vzGfIeR2MsNQ,25950
3
+ staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
4
+ staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
5
+ staticdash-2025.21.dist-info/METADATA,sha256=qKcshAi26kzpuRYjYpUppRTQW3F3ZsoKiKjHxIAbfWQ,1960
6
+ staticdash-2025.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ staticdash-2025.21.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
+ staticdash-2025.21.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,,