staticdash 2025.18__py3-none-any.whl → 2025.20__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")
@@ -48,11 +49,12 @@ class AbstractPage:
48
49
  self.elements.append(("syntax", (code, language), width))
49
50
 
50
51
  class Page(AbstractPage):
51
- def __init__(self, slug, title, page_width=None):
52
+ def __init__(self, slug, title, page_width=None, marking=None):
52
53
  super().__init__()
53
54
  self.slug = slug
54
55
  self.title = title
55
56
  self.page_width = page_width
57
+ self.marking = marking # Page-specific marking
56
58
  self.children = []
57
59
  self.add_header(title, level=1)
58
60
 
@@ -62,6 +64,20 @@ class Page(AbstractPage):
62
64
  def render(self, index, downloads_dir=None, relative_prefix="", inherited_width=None):
63
65
  effective_width = self.page_width or inherited_width
64
66
  elements = []
67
+
68
+ # Add floating header and footer for marking
69
+ marking = self.marking or "Default Marking"
70
+ elements.append(div(
71
+ marking,
72
+ cls="floating-header",
73
+ style="position: fixed; top: 0; left: 50%; transform: translateX(-50%); width: auto; background-color: #f8f9fa; text-align: center; padding: 10px; z-index: 1000; font-weight: normal;"
74
+ ))
75
+ elements.append(div(
76
+ marking,
77
+ cls="floating-footer",
78
+ style="position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: auto; background-color: #f8f9fa; text-align: center; padding: 10px; z-index: 1000; font-weight: normal;"
79
+ ))
80
+
65
81
  for kind, content, el_width in self.elements:
66
82
  style = ""
67
83
  outer_style = ""
@@ -77,10 +93,8 @@ class Page(AbstractPage):
77
93
  elem = header_tag(text)
78
94
  elif kind == "plot":
79
95
  fig = content
80
- # Plotly support (existing)
81
96
  if hasattr(fig, "to_html"):
82
97
  elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
83
- # Matplotlib support
84
98
  else:
85
99
  try:
86
100
  buf = io.BytesIO()
@@ -88,7 +102,6 @@ class Page(AbstractPage):
88
102
  buf.seek(0)
89
103
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
90
104
  buf.close()
91
- # Center the image using a div with inline styles
92
105
  elem = div(
93
106
  raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
94
107
  style="display: flex; justify-content: center; align-items: center;"
@@ -97,8 +110,11 @@ class Page(AbstractPage):
97
110
  elem = div(f"Matplotlib figure could not be rendered: {e}")
98
111
  elif kind == "table":
99
112
  df = content
100
- html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
101
- elem = div(raw_util(html_table))
113
+ try:
114
+ html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
115
+ elem = div(raw_util(html_table))
116
+ except Exception as e:
117
+ elem = div(f"Table could not be rendered: {e}")
102
118
  elif kind == "download":
103
119
  file_path, label = content
104
120
  btn = a(label or os.path.basename(file_path), href=file_path, cls="download-button", download=True)
@@ -117,7 +133,9 @@ class Page(AbstractPage):
117
133
  elem = div(elem, style=style)
118
134
  elem = div(elem, style=outer_style)
119
135
  elements.append(elem)
120
- wrapper = div(*elements, style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%;")
136
+
137
+ # Add padding to avoid overlap with header and footer
138
+ wrapper = div(*elements, style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%; padding-top: 80px; padding-bottom: 80px;")
121
139
  return [wrapper]
122
140
 
123
141
  class MiniPage(AbstractPage):
@@ -187,13 +205,42 @@ class MiniPage(AbstractPage):
187
205
  return row_div
188
206
 
189
207
  class Dashboard:
190
- def __init__(self, title="Dashboard", page_width=900):
208
+ def __init__(self, title="Dashboard", page_width=900, marking=None):
191
209
  self.title = title
192
210
  self.pages = []
193
211
  self.page_width = page_width
212
+ self.marking = marking # Dashboard-wide marking
194
213
 
195
214
  def add_page(self, page):
196
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
+
197
244
 
198
245
  def _render_sidebar(self, pages, prefix="", current_slug=None):
199
246
  for page in pages:
@@ -298,77 +345,75 @@ class Dashboard:
298
345
  with open(os.path.join(output_dir, "index.html"), "w") as f:
299
346
  f.write(str(index_doc))
300
347
 
301
- def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_toc=True, include_title_page=False, author=None, affiliation=None):
302
- from reportlab.lib.pagesizes import A4, letter
303
- page_size = A4 if pagesize.upper() == "A4" else letter
304
-
305
- import plotly.io as pio
306
- pio.kaleido.scope.default_format = "png"
307
-
308
- styles = getSampleStyleSheet()
309
- styles['Heading1'].fontSize = 18
310
- styles['Heading1'].leading = 22
311
- styles['Heading1'].spaceAfter = 12
312
- styles['Heading1'].spaceBefore = 18
313
- styles['Heading1'].fontName = 'Helvetica-Bold'
314
- styles['Heading2'].fontSize = 14
315
- styles['Heading2'].leading = 18
316
- styles['Heading2'].spaceAfter = 8
317
- styles['Heading2'].spaceBefore = 12
318
- styles['Heading2'].fontName = 'Helvetica-Bold'
319
- if 'CodeBlock' not in styles:
320
- styles.add(ParagraphStyle(name='CodeBlock', fontName='Courier', fontSize=9, leading=12, backColor=colors.whitesmoke, leftIndent=12, rightIndent=12, spaceAfter=8, borderPadding=4))
321
- normal_style = styles['Normal']
322
-
323
- story = []
324
- outline_entries = []
325
- heading_paragraphs = []
326
-
327
- class MyDocTemplate(SimpleDocTemplate):
328
- def __init__(self, *args, outline_entries=None, headings=None, **kwargs):
329
- super().__init__(*args, **kwargs)
330
- self.outline_entries = outline_entries or []
331
- self.headings = headings or []
332
- self._outline_idx = 0
333
-
334
- def afterFlowable(self, flowable):
335
- if hasattr(flowable, 'getPlainText'):
336
- text = flowable.getPlainText().strip()
337
- if self._outline_idx < len(self.outline_entries):
338
- expected_title, level, section_num = self.outline_entries[self._outline_idx]
339
- expected = expected_title.strip()
340
- if text == expected:
341
- bookmark_name = f"section_{section_num.replace('.', '_')}"
342
- self.canv.bookmarkPage(bookmark_name)
343
- self.canv.addOutlineEntry(expected_title, bookmark_name, level=level, closed=False)
344
- self._outline_idx += 1
345
-
346
- def render_page(page, level=0, sec_prefix=[]):
347
- if len(sec_prefix) <= level:
348
- sec_prefix.append(1)
349
- else:
350
- sec_prefix[level] += 1
351
- sec_prefix = sec_prefix[:level+1]
352
- section_num = ".".join(str(n) for n in sec_prefix)
353
- section_title = f"{section_num} {page.title}"
354
- outline_entries.append((section_title, level, section_num))
355
- style = styles['Heading1'] if level == 0 else styles['Heading2']
356
- bookmark_name = f"section_{section_num.replace('.', '_')}"
357
- para = Paragraph(f'<a name="{bookmark_name}"/>{section_title}', style)
358
- heading_paragraphs.append(para)
359
- story.append(para)
360
- story.append(Spacer(1, 12))
361
-
362
- def render_elements(elements):
363
- for kind, content, _ in elements:
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())
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)
381
+ story.append(PageBreak())
382
+
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:
364
396
  if kind == "text":
365
- story.append(Paragraph(content, normal_style))
397
+ story.append(Paragraph(content, styles['Normal']))
366
398
  story.append(Spacer(1, 8))
399
+
367
400
  elif kind == "header":
368
- text, level_ = content
369
- header_style = styles['Heading{}'.format(min(level_+1, 4))]
370
- story.append(Paragraph(text, header_style))
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)
371
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
+
372
417
  elif kind == "table":
373
418
  df = content
374
419
  try:
@@ -394,106 +439,64 @@ class Dashboard:
394
439
  story.append(t)
395
440
  story.append(Spacer(1, 12))
396
441
  except Exception:
397
- story.append(Paragraph("Table could not be rendered.", normal_style))
442
+ story.append(Paragraph("Table could not be rendered.", styles['Normal']))
398
443
  elif kind == "plot":
399
444
  fig = content
400
445
  try:
401
- import plotly.graph_objects as go
402
- import matplotlib.figure
403
- import io
404
- from reportlab.platypus import Image
405
-
406
- # Plotly support
407
- if isinstance(fig, go.Figure):
408
- # Configure the figure layout for PDF rendering
409
- fig.update_layout(
410
- margin=dict(l=10, r=10, t=30, b=30),
411
- width=900,
412
- height=540
413
- )
414
-
415
- # Use kaleido to export the figure as a PNG
416
- png_bytes = fig.to_image(format="png", width=900, height=540, engine="kaleido")
417
-
418
- # Wrap the PNG bytes in a BytesIO buffer
419
- img_buf = io.BytesIO(png_bytes)
420
-
421
- # Add the image to the PDF story
422
- story.append(Spacer(1, 8))
423
- story.append(Image(img_buf, width=6 * inch, height=3.6 * inch))
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)
424
451
  story.append(Spacer(1, 12))
425
-
426
- # Matplotlib support
427
- elif isinstance(fig, matplotlib.figure.Figure):
452
+ elif hasattr(fig, "savefig"):
428
453
  buf = io.BytesIO()
429
- # Save the figure with higher DPI for better quality
430
454
  fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
431
455
  buf.seek(0)
432
-
433
- # Calculate aspect ratio
434
- fig_width, fig_height = fig.get_size_inches()
435
- aspect_ratio = fig_height / fig_width
436
-
437
- # Set width and calculate height based on aspect ratio
438
- pdf_width = 6 * inch
439
- pdf_height = pdf_width * aspect_ratio
440
-
441
- # Add the image to the PDF story
442
- story.append(Spacer(1, 8))
443
- story.append(Image(buf, width=pdf_width, height=pdf_height))
456
+ img = Image(buf, width=6 * inch, height=4.5 * inch)
457
+ story.append(img)
444
458
  story.append(Spacer(1, 12))
445
-
446
- else:
447
- raise ValueError("add_plot must be called with a plotly.graph_objects.Figure or matplotlib.figure.Figure")
448
-
449
459
  except Exception as e:
450
- story.append(Paragraph(f"Plot rendering not supported in PDF: {e}", normal_style))
460
+ story.append(Paragraph(f"Plot could not be rendered: {e}", styles['Normal']))
461
+ elif kind == "syntax":
462
+ pass
451
463
  elif kind == "syntax":
452
464
  code, language = content
453
- story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
454
- story.append(Spacer(1, 4))
455
- code_html = html.escape(code).replace(' ', '&nbsp;').replace('\n', '<br/>')
456
- story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
457
- story.append(Spacer(1, 12))
458
- elif kind == "minipage":
459
- render_elements(content.elements)
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)
460
482
 
461
- render_elements(page.elements)
483
+ elif kind == "minipage":
484
+ pass
462
485
 
463
- for i, child in enumerate(getattr(page, "children", []), start=0):
464
- story.append(PageBreak())
465
- child_sec_prefix = sec_prefix.copy() + [i]
466
- render_page(child, level=level+1, sec_prefix=child_sec_prefix)
486
+ for child in getattr(page, "children", []):
487
+ story.append(PageBreak())
488
+ render_page(child)
467
489
 
468
- if level == 0:
490
+ for page in self.pages:
491
+ render_page(page)
469
492
  story.append(PageBreak())
470
493
 
471
- if include_title_page:
472
- story.append(Spacer(1, 120))
473
- # Title, centered and bold
474
- story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
475
- story.append(Spacer(1, 48))
476
- # Centered info block (no labels)
477
- info_lines = []
478
- if author:
479
- info_lines.append(str(author))
480
- if affiliation:
481
- info_lines.append(str(affiliation))
482
- info_lines.append(pd.Timestamp.now().strftime('%B %d, %Y'))
483
- info_html = "<br/>".join(info_lines)
484
- story.append(Paragraph(f'<para align="center">{info_html}</para>', styles['Normal']))
485
- story.append(PageBreak())
486
-
487
- for page in self.pages:
488
- render_page(page)
489
-
490
-
491
- doc = MyDocTemplate(
492
- output_path,
493
- pagesize=page_size,
494
- rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
495
- outline_entries=outline_entries,
496
- headings=heading_paragraphs
497
- )
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)
498
497
 
499
- doc.multiBuild(story)
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.18
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
@@ -0,0 +1,8 @@
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,,
@@ -1,8 +0,0 @@
1
- staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
2
- staticdash/dashboard.py,sha256=6d3TbXVN0ZdWD5awsqWH9H6tH64fpOanTAiVWG_q-Eo,25056
3
- staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
4
- staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
5
- staticdash-2025.18.dist-info/METADATA,sha256=_Zrirh0NBbV36fcXJQ4oqGQ_lr-UPhYSwLr6Vy3ur3U,1960
6
- staticdash-2025.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- staticdash-2025.18.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
- staticdash-2025.18.dist-info/RECORD,,