staticdash 2025.18__py3-none-any.whl → 2025.19__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
@@ -48,11 +48,12 @@ class AbstractPage:
48
48
  self.elements.append(("syntax", (code, language), width))
49
49
 
50
50
  class Page(AbstractPage):
51
- def __init__(self, slug, title, page_width=None):
51
+ def __init__(self, slug, title, page_width=None, marking=None):
52
52
  super().__init__()
53
53
  self.slug = slug
54
54
  self.title = title
55
55
  self.page_width = page_width
56
+ self.marking = marking # Page-specific marking
56
57
  self.children = []
57
58
  self.add_header(title, level=1)
58
59
 
@@ -62,6 +63,20 @@ class Page(AbstractPage):
62
63
  def render(self, index, downloads_dir=None, relative_prefix="", inherited_width=None):
63
64
  effective_width = self.page_width or inherited_width
64
65
  elements = []
66
+
67
+ # Add floating header and footer for marking
68
+ marking = self.marking or "Default Marking"
69
+ elements.append(div(
70
+ marking,
71
+ cls="floating-header",
72
+ 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;"
73
+ ))
74
+ elements.append(div(
75
+ marking,
76
+ cls="floating-footer",
77
+ 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;"
78
+ ))
79
+
65
80
  for kind, content, el_width in self.elements:
66
81
  style = ""
67
82
  outer_style = ""
@@ -77,10 +92,8 @@ class Page(AbstractPage):
77
92
  elem = header_tag(text)
78
93
  elif kind == "plot":
79
94
  fig = content
80
- # Plotly support (existing)
81
95
  if hasattr(fig, "to_html"):
82
96
  elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
83
- # Matplotlib support
84
97
  else:
85
98
  try:
86
99
  buf = io.BytesIO()
@@ -88,7 +101,6 @@ class Page(AbstractPage):
88
101
  buf.seek(0)
89
102
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
90
103
  buf.close()
91
- # Center the image using a div with inline styles
92
104
  elem = div(
93
105
  raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
94
106
  style="display: flex; justify-content: center; align-items: center;"
@@ -97,8 +109,11 @@ class Page(AbstractPage):
97
109
  elem = div(f"Matplotlib figure could not be rendered: {e}")
98
110
  elif kind == "table":
99
111
  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))
112
+ try:
113
+ html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
114
+ elem = div(raw_util(html_table))
115
+ except Exception as e:
116
+ elem = div(f"Table could not be rendered: {e}")
102
117
  elif kind == "download":
103
118
  file_path, label = content
104
119
  btn = a(label or os.path.basename(file_path), href=file_path, cls="download-button", download=True)
@@ -117,7 +132,9 @@ class Page(AbstractPage):
117
132
  elem = div(elem, style=style)
118
133
  elem = div(elem, style=outer_style)
119
134
  elements.append(elem)
120
- wrapper = div(*elements, style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%;")
135
+
136
+ # Add padding to avoid overlap with header and footer
137
+ wrapper = div(*elements, style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%; padding-top: 80px; padding-bottom: 80px;")
121
138
  return [wrapper]
122
139
 
123
140
  class MiniPage(AbstractPage):
@@ -187,10 +204,11 @@ class MiniPage(AbstractPage):
187
204
  return row_div
188
205
 
189
206
  class Dashboard:
190
- def __init__(self, title="Dashboard", page_width=900):
207
+ def __init__(self, title="Dashboard", page_width=900, marking=None):
191
208
  self.title = title
192
209
  self.pages = []
193
210
  self.page_width = page_width
211
+ self.marking = marking # Dashboard-wide marking
194
212
 
195
213
  def add_page(self, page):
196
214
  self.pages.append(page)
@@ -298,202 +316,124 @@ class Dashboard:
298
316
  with open(os.path.join(output_dir, "index.html"), "w") as f:
299
317
  f.write(str(index_doc))
300
318
 
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"
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
323
+ from datetime import datetime
324
+ import io
307
325
 
326
+ page_size = A4 if pagesize == "A4" else letter
308
327
  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
328
  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:
364
- if kind == "text":
365
- story.append(Paragraph(content, normal_style))
366
- story.append(Spacer(1, 8))
367
- elif kind == "header":
368
- text, level_ = content
369
- header_style = styles['Heading{}'.format(min(level_+1, 4))]
370
- story.append(Paragraph(text, header_style))
371
- story.append(Spacer(1, 8))
372
- elif kind == "table":
373
- df = content
374
- try:
375
- data = [df.columns.tolist()] + df.values.tolist()
376
- t = Table(data, repeatRows=1)
377
- t.setStyle(TableStyle([
378
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
379
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
380
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
381
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
382
- ('FONTSIZE', (0, 0), (-1, 0), 11),
383
- ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
384
- ('TOPPADDING', (0, 0), (-1, 0), 10),
385
- ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
386
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
387
- ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
388
- ('FONTSIZE', (0, 1), (-1, -1), 10),
389
- ('LEFTPADDING', (0, 0), (-1, -1), 6),
390
- ('RIGHTPADDING', (0, 0), (-1, -1), 6),
391
- ('TOPPADDING', (0, 1), (-1, -1), 6),
392
- ('BOTTOMPADDING', (0, 1), (-1, -1), 6),
393
- ]))
394
- story.append(t)
395
- story.append(Spacer(1, 12))
396
- except Exception:
397
- story.append(Paragraph("Table could not be rendered.", normal_style))
398
- elif kind == "plot":
399
- fig = content
400
- 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))
424
- story.append(Spacer(1, 12))
425
-
426
- # Matplotlib support
427
- elif isinstance(fig, matplotlib.figure.Figure):
428
- buf = io.BytesIO()
429
- # Save the figure with higher DPI for better quality
430
- fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
431
- 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))
444
- 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
- except Exception as e:
450
- story.append(Paragraph(f"Plot rendering not supported in PDF: {e}", normal_style))
451
- elif kind == "syntax":
452
- 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)
460
-
461
- render_elements(page.elements)
462
-
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)
467
-
468
- if level == 0:
469
- story.append(PageBreak())
470
329
 
330
+ # Add title page
471
331
  if include_title_page:
472
332
  story.append(Spacer(1, 120))
473
- # Title, centered and bold
474
333
  story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
475
334
  story.append(Spacer(1, 48))
476
- # Centered info block (no labels)
477
- info_lines = []
335
+
336
+ # Center author, affiliation, and date
478
337
  if author:
479
- info_lines.append(str(author))
338
+ story.append(Paragraph(f"<para align='center'>{author}</para>", styles['Normal']))
480
339
  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']))
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
+
485
344
  story.append(PageBreak())
486
345
 
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)
413
+ 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
487
429
  for page in self.pages:
488
430
  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
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)
497
439
  )
498
-
499
- doc.multiBuild(story)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.18
3
+ Version: 2025.19
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=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,,
@@ -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,,