staticdash 2025.19__tar.gz → 2025.20__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.19
3
+ Version: 2025.20
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "staticdash"
7
- version = "2025.19"
7
+ version = "2025.20"
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:
@@ -317,123 +346,157 @@ class Dashboard:
317
346
  f.write(str(index_doc))
318
347
 
319
348
  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
323
- from datetime import datetime
324
- import io
325
-
326
- page_size = A4 if pagesize == "A4" else letter
327
- styles = getSampleStyleSheet()
328
- story = []
329
-
330
- # Add title page
331
- if include_title_page:
332
- story.append(Spacer(1, 120))
333
- story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
334
- story.append(Spacer(1, 48))
335
-
336
- # Center author, affiliation, and date
337
- if author:
338
- story.append(Paragraph(f"<para align='center'>{author}</para>", styles['Normal']))
339
- 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']))
343
-
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())
371
+
372
+ # Add Table of Contents
373
+ toc = TableOfContents()
374
+ toc.levelStyles = [
375
+ ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
376
+ 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
+ ]
379
+ story.append(Paragraph("Table of Contents", styles["Title"]))
380
+ story.append(toc)
344
381
  story.append(PageBreak())
345
382
 
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()
357
-
358
- # Recursive function to render pages and subpages
359
- def render_page(page):
360
- # Render the current page
361
- for kind, content, _ in page.elements:
362
- if kind == "text":
363
- story.append(Paragraph(content, styles['Normal']))
364
- story.append(Spacer(1, 8))
365
- elif kind == "header":
366
- text, level = content
367
- header_style = styles[f'Heading{min(level + 1, 4)}']
368
- story.append(Paragraph(text, header_style))
369
- story.append(Spacer(1, 8))
370
- elif kind == "table":
371
- df = content
372
- try:
373
- data = [df.columns.tolist()] + df.values.tolist()
374
- t = Table(data, repeatRows=1)
375
- t.setStyle(TableStyle([
376
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
377
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
378
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
379
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
380
- ('FONTSIZE', (0, 0), (-1, 0), 11),
381
- ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
382
- ('TOPPADDING', (0, 0), (-1, 0), 10),
383
- ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
384
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
385
- ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
386
- ('FONTSIZE', (0, 1), (-1, -1), 10),
387
- ('LEFTPADDING', (0, 0), (-1, -1), 6),
388
- ('RIGHTPADDING', (0, 0), (-1, -1), 6),
389
- ('TOPPADDING', (0, 1), (-1, -1), 6),
390
- ('BOTTOMPADDING', (0, 1), (-1, -1), 6),
391
- ]))
392
- story.append(t)
393
- story.append(Spacer(1, 12))
394
- except Exception:
395
- story.append(Paragraph("Table could not be rendered.", styles['Normal']))
396
- elif kind == "plot":
397
- fig = content
398
- 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"):
408
- buf = io.BytesIO()
409
- fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
410
- buf.seek(0)
411
- img = Image(buf, width=6 * inch, height=4.5 * inch)
412
- story.append(img)
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)
413
440
  story.append(Spacer(1, 12))
414
- except Exception as e:
415
- story.append(Paragraph(f"Plot could not be rendered: {e}", styles['Normal']))
416
- elif kind == "syntax":
417
- # Handle syntax blocks
418
- pass
419
- elif kind == "minipage":
420
- # Handle subpages
421
- pass
422
-
423
- # Recursively render subpages
424
- for child in getattr(page, "children", []):
425
- story.append(PageBreak()) # Add a PageBreak before rendering subpages
426
- render_page(child)
427
-
428
- # Render all pages
429
- for page in self.pages:
430
- render_page(page)
431
- story.append(PageBreak()) # Add a PageBreak after each top-level page
432
-
433
- # Build the PDF
434
- doc = SimpleDocTemplate(output_path, pagesize=page_size)
435
- doc.build(
436
- story,
437
- onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
438
- onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
439
- )
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)
482
+
483
+ elif kind == "minipage":
484
+ pass
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.19
3
+ Version: 2025.20
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
File without changes
File without changes