staticdash 2025.19__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
@@ -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, title_page_marking=None, author=None, affiliation=None):
320
- from reportlab.platypus import SimpleDocTemplate, Spacer, Paragraph, PageBreak, Image
321
- from reportlab.lib.pagesizes import letter, A4
322
- from reportlab.lib.styles import getSampleStyleSheet
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
- page_size = A4 if pagesize == "A4" else letter
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
- # Add title page
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
- story.append(Paragraph(f"<para align='center'>{author}</para>", styles['Normal']))
426
+ lines.append(str(author))
339
427
  if affiliation:
340
- story.append(Paragraph(f"<para align='center'>{affiliation}</para>", styles['Normal']))
341
- current_date = datetime.now().strftime("%B %d, %Y")
342
- story.append(Paragraph(f"<para align='center'>{current_date}</para>", styles['Normal']))
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
- # Add markings to the PDF
347
- def add_marking(canvas, doc, marking):
348
- if marking:
349
- canvas.saveState()
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, styles['Normal']))
450
+ story.append(Paragraph(content, normal_style))
364
451
  story.append(Spacer(1, 8))
452
+
365
453
  elif kind == "header":
366
- text, level = content
367
- header_style = styles[f'Heading{min(level + 1, 4)}']
368
- story.append(Paragraph(text, header_style))
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.", styles['Normal']))
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
- # Handle Plotly figures
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
- img = Image(buf, width=6 * inch, height=4.5 * inch)
412
- story.append(img)
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 could not be rendered: {e}", styles['Normal']))
505
+ story.append(Paragraph(f"Plot rendering failed: {e}", normal_style))
506
+
416
507
  elif kind == "syntax":
417
- # Handle syntax blocks
418
- pass
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))
515
+
419
516
  elif kind == "minipage":
420
- # Handle subpages
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()) # Add a PageBreak before rendering subpages
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
- # Build the PDF
434
- doc = SimpleDocTemplate(output_path, pagesize=page_size)
435
- doc.build(
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.19
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=fTxesbZOkM4H2VEDZX152z8gLLZ3w0ahdYA7yBJ08aA,21960
3
- staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
4
- staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
5
- staticdash-2025.19.dist-info/METADATA,sha256=Mmt-_2QcTkiWSV6nXRUZhuNBbMchCom9UC3lW45aG70,1960
6
- staticdash-2025.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- staticdash-2025.19.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
- staticdash-2025.19.dist-info/RECORD,,