staticdash 2025.17__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
@@ -15,6 +15,8 @@ from reportlab.lib import colors
15
15
  from reportlab.lib.units import inch
16
16
  import io
17
17
  import tempfile
18
+ import matplotlib.pyplot as plt
19
+ import io, base64
18
20
 
19
21
  class AbstractPage:
20
22
  def __init__(self):
@@ -46,11 +48,12 @@ class AbstractPage:
46
48
  self.elements.append(("syntax", (code, language), width))
47
49
 
48
50
  class Page(AbstractPage):
49
- def __init__(self, slug, title, page_width=None):
51
+ def __init__(self, slug, title, page_width=None, marking=None):
50
52
  super().__init__()
51
53
  self.slug = slug
52
54
  self.title = title
53
55
  self.page_width = page_width
56
+ self.marking = marking # Page-specific marking
54
57
  self.children = []
55
58
  self.add_header(title, level=1)
56
59
 
@@ -60,6 +63,20 @@ class Page(AbstractPage):
60
63
  def render(self, index, downloads_dir=None, relative_prefix="", inherited_width=None):
61
64
  effective_width = self.page_width or inherited_width
62
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
+
63
80
  for kind, content, el_width in self.elements:
64
81
  style = ""
65
82
  outer_style = ""
@@ -75,11 +92,28 @@ class Page(AbstractPage):
75
92
  elem = header_tag(text)
76
93
  elif kind == "plot":
77
94
  fig = content
78
- elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
95
+ if hasattr(fig, "to_html"):
96
+ elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
97
+ else:
98
+ try:
99
+ buf = io.BytesIO()
100
+ fig.savefig(buf, format="png", bbox_inches="tight")
101
+ buf.seek(0)
102
+ img_base64 = base64.b64encode(buf.read()).decode("utf-8")
103
+ buf.close()
104
+ elem = div(
105
+ raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
106
+ style="display: flex; justify-content: center; align-items: center;"
107
+ )
108
+ except Exception as e:
109
+ elem = div(f"Matplotlib figure could not be rendered: {e}")
79
110
  elif kind == "table":
80
111
  df = content
81
- html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
82
- 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}")
83
117
  elif kind == "download":
84
118
  file_path, label = content
85
119
  btn = a(label or os.path.basename(file_path), href=file_path, cls="download-button", download=True)
@@ -98,7 +132,9 @@ class Page(AbstractPage):
98
132
  elem = div(elem, style=style)
99
133
  elem = div(elem, style=outer_style)
100
134
  elements.append(elem)
101
- 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;")
102
138
  return [wrapper]
103
139
 
104
140
  class MiniPage(AbstractPage):
@@ -124,7 +160,24 @@ class MiniPage(AbstractPage):
124
160
  elem = header_tag(text)
125
161
  elif kind == "plot":
126
162
  fig = content
127
- elem = raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
163
+ # Plotly support (existing)
164
+ if hasattr(fig, "to_html"):
165
+ elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
166
+ # Matplotlib support
167
+ else:
168
+ try:
169
+ buf = io.BytesIO()
170
+ fig.savefig(buf, format="png", bbox_inches="tight")
171
+ buf.seek(0)
172
+ img_base64 = base64.b64encode(buf.read()).decode("utf-8")
173
+ buf.close()
174
+ # Center the image using a div with inline styles
175
+ elem = div(
176
+ raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
177
+ style="display: flex; justify-content: center; align-items: center;"
178
+ )
179
+ except Exception as e:
180
+ elem = div(f"Matplotlib figure could not be rendered: {e}")
128
181
  elif kind == "table":
129
182
  df = content
130
183
  html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
@@ -151,10 +204,11 @@ class MiniPage(AbstractPage):
151
204
  return row_div
152
205
 
153
206
  class Dashboard:
154
- def __init__(self, title="Dashboard", page_width=900):
207
+ def __init__(self, title="Dashboard", page_width=900, marking=None):
155
208
  self.title = title
156
209
  self.pages = []
157
210
  self.page_width = page_width
211
+ self.marking = marking # Dashboard-wide marking
158
212
 
159
213
  def add_page(self, page):
160
214
  self.pages.append(page)
@@ -262,167 +316,124 @@ class Dashboard:
262
316
  with open(os.path.join(output_dir, "index.html"), "w") as f:
263
317
  f.write(str(index_doc))
264
318
 
265
- def publish_pdf(self, output_path="dashboard_report.pdf", pagesize="A4", include_toc=True, include_title_page=False, author=None, affiliation=None):
266
- from reportlab.lib.pagesizes import A4, letter
267
- page_size = A4 if pagesize.upper() == "A4" else letter
268
-
269
- import plotly.io as pio
270
- 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
271
325
 
326
+ page_size = A4 if pagesize == "A4" else letter
272
327
  styles = getSampleStyleSheet()
273
- styles['Heading1'].fontSize = 18
274
- styles['Heading1'].leading = 22
275
- styles['Heading1'].spaceAfter = 12
276
- styles['Heading1'].spaceBefore = 18
277
- styles['Heading1'].fontName = 'Helvetica-Bold'
278
- styles['Heading2'].fontSize = 14
279
- styles['Heading2'].leading = 18
280
- styles['Heading2'].spaceAfter = 8
281
- styles['Heading2'].spaceBefore = 12
282
- styles['Heading2'].fontName = 'Helvetica-Bold'
283
- if 'CodeBlock' not in styles:
284
- styles.add(ParagraphStyle(name='CodeBlock', fontName='Courier', fontSize=9, leading=12, backColor=colors.whitesmoke, leftIndent=12, rightIndent=12, spaceAfter=8, borderPadding=4))
285
- normal_style = styles['Normal']
286
-
287
328
  story = []
288
- outline_entries = []
289
- heading_paragraphs = []
290
-
291
- class MyDocTemplate(SimpleDocTemplate):
292
- def __init__(self, *args, outline_entries=None, headings=None, **kwargs):
293
- super().__init__(*args, **kwargs)
294
- self.outline_entries = outline_entries or []
295
- self.headings = headings or []
296
- self._outline_idx = 0
297
-
298
- def afterFlowable(self, flowable):
299
- if hasattr(flowable, 'getPlainText'):
300
- text = flowable.getPlainText().strip()
301
- if self._outline_idx < len(self.outline_entries):
302
- expected_title, level, section_num = self.outline_entries[self._outline_idx]
303
- expected = expected_title.strip()
304
- if text == expected:
305
- bookmark_name = f"section_{section_num.replace('.', '_')}"
306
- self.canv.bookmarkPage(bookmark_name)
307
- self.canv.addOutlineEntry(expected_title, bookmark_name, level=level, closed=False)
308
- self._outline_idx += 1
309
-
310
- def render_page(page, level=0, sec_prefix=[]):
311
- if len(sec_prefix) <= level:
312
- sec_prefix.append(1)
313
- else:
314
- sec_prefix[level] += 1
315
- sec_prefix = sec_prefix[:level+1]
316
- section_num = ".".join(str(n) for n in sec_prefix)
317
- section_title = f"{section_num} {page.title}"
318
- outline_entries.append((section_title, level, section_num))
319
- style = styles['Heading1'] if level == 0 else styles['Heading2']
320
- bookmark_name = f"section_{section_num.replace('.', '_')}"
321
- para = Paragraph(f'<a name="{bookmark_name}"/>{section_title}', style)
322
- heading_paragraphs.append(para)
323
- story.append(para)
324
- story.append(Spacer(1, 12))
325
-
326
- def render_elements(elements):
327
- for kind, content, _ in elements:
328
- if kind == "text":
329
- story.append(Paragraph(content, normal_style))
330
- story.append(Spacer(1, 8))
331
- elif kind == "header":
332
- text, level_ = content
333
- header_style = styles['Heading{}'.format(min(level_+1, 4))]
334
- story.append(Paragraph(text, header_style))
335
- story.append(Spacer(1, 8))
336
- elif kind == "table":
337
- df = content
338
- try:
339
- data = [df.columns.tolist()] + df.values.tolist()
340
- t = Table(data, repeatRows=1)
341
- t.setStyle(TableStyle([
342
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
343
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
344
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
345
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
346
- ('FONTSIZE', (0, 0), (-1, 0), 11),
347
- ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
348
- ('TOPPADDING', (0, 0), (-1, 0), 10),
349
- ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
350
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
351
- ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
352
- ('FONTSIZE', (0, 1), (-1, -1), 10),
353
- ('LEFTPADDING', (0, 0), (-1, -1), 6),
354
- ('RIGHTPADDING', (0, 0), (-1, -1), 6),
355
- ('TOPPADDING', (0, 1), (-1, -1), 6),
356
- ('BOTTOMPADDING', (0, 1), (-1, -1), 6),
357
- ]))
358
- story.append(t)
359
- story.append(Spacer(1, 12))
360
- except Exception:
361
- story.append(Paragraph("Table could not be rendered.", normal_style))
362
- elif kind == "plot":
363
- fig = content
364
- try:
365
- import plotly.graph_objects as go
366
- if not isinstance(fig, go.Figure):
367
- raise ValueError("add_plot must be called with a plotly.graph_objects.Figure")
368
- fig.update_layout(margin=dict(l=10, r=10, t=30, b=30), width=900, height=540)
369
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
370
- fig.write_image(tmpfile.name, width=600, height=360, scale=2, format="png", engine="kaleido")
371
- with open(tmpfile.name, "rb") as imgf:
372
- img_bytes = imgf.read()
373
- img_buf = io.BytesIO(img_bytes)
374
- story.append(Spacer(1, 8))
375
- story.append(Image(img_buf, width=6*inch, height=3.6*inch))
376
- story.append(Spacer(1, 12))
377
- os.unlink(tmpfile.name)
378
- except Exception as e:
379
- story.append(Paragraph(f"Plot rendering not supported in PDF: {e}", normal_style))
380
- elif kind == "syntax":
381
- code, language = content
382
- story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
383
- story.append(Spacer(1, 4))
384
- code_html = html.escape(code).replace(' ', '&nbsp;').replace('\n', '<br/>')
385
- story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
386
- story.append(Spacer(1, 12))
387
- elif kind == "minipage":
388
- render_elements(content.elements)
389
-
390
- render_elements(page.elements)
391
-
392
- for i, child in enumerate(getattr(page, "children", []), start=0):
393
- story.append(PageBreak())
394
- child_sec_prefix = sec_prefix.copy() + [i]
395
- render_page(child, level=level+1, sec_prefix=child_sec_prefix)
396
-
397
- if level == 0:
398
- story.append(PageBreak())
399
329
 
330
+ # Add title page
400
331
  if include_title_page:
401
332
  story.append(Spacer(1, 120))
402
- # Title, centered and bold
403
333
  story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
404
334
  story.append(Spacer(1, 48))
405
- # Centered info block (no labels)
406
- info_lines = []
335
+
336
+ # Center author, affiliation, and date
407
337
  if author:
408
- info_lines.append(str(author))
338
+ story.append(Paragraph(f"<para align='center'>{author}</para>", styles['Normal']))
409
339
  if affiliation:
410
- info_lines.append(str(affiliation))
411
- info_lines.append(pd.Timestamp.now().strftime('%B %d, %Y'))
412
- info_html = "<br/>".join(info_lines)
413
- 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
+
414
344
  story.append(PageBreak())
415
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
416
429
  for page in self.pages:
417
430
  render_page(page)
418
-
419
-
420
- doc = MyDocTemplate(
421
- output_path,
422
- pagesize=page_size,
423
- rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
424
- outline_entries=outline_entries,
425
- 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)
426
439
  )
427
-
428
- doc.multiBuild(story)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.17
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
@@ -13,6 +13,7 @@ Requires-Dist: pandas
13
13
  Requires-Dist: dominate
14
14
  Requires-Dist: reportlab
15
15
  Requires-Dist: kaleido
16
+ Requires-Dist: matplotlib
16
17
 
17
18
  # staticdash
18
19
 
@@ -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=jMUKft0qxqUcO7tg3WpzfRCtyUhROIXG7DHkuP4XbSk,21653
3
- staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
4
- staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
5
- staticdash-2025.17.dist-info/METADATA,sha256=sZnMrJks-Q5mmJjsp-ZP-kMnI2UbpfE5SIr8WpI7FBM,1934
6
- staticdash-2025.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- staticdash-2025.17.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
- staticdash-2025.17.dist-info/RECORD,,