staticdash 2025.27__py3-none-any.whl → 2025.29__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.
@@ -371,4 +371,25 @@ body.sidebar-animate .sidebar-arrow {
371
371
  background-color: #34495e;
372
372
  color: #fff;
373
373
  text-decoration: none;
374
- }
374
+ }
375
+
376
+ :root {
377
+ --sidebar-width: 240px; /* matches #sidebar width */
378
+ --content-padding-x: 20px; /* matches #content horizontal padding */
379
+ }
380
+
381
+ /* Pure-CSS triangle for sidebar arrow */
382
+ .sidebar-arrow {
383
+ width: 0;
384
+ height: 0;
385
+ border-top: 6px solid transparent;
386
+ border-bottom: 6px solid transparent;
387
+ border-left: 8px solid #bdc3c7; /* arrow color */
388
+ margin-right: 6px;
389
+ }
390
+
391
+ /* Rotate when group is open (you already have this rule; keep it) */
392
+ body.sidebar-animate .sidebar-group.open .sidebar-arrow {
393
+ transform: rotate(90deg);
394
+ transform-origin: 30% 50%;
395
+ }
staticdash/dashboard.py CHANGED
@@ -7,22 +7,13 @@ from dominate import document
7
7
  from dominate.tags import div, h1, h2, h3, h4, p, a, script, link, span
8
8
  from dominate.util import raw as raw_util
9
9
  import html
10
- from reportlab.lib.pagesizes import letter, A4
11
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, Image
12
- from reportlab.platypus.tableofcontents import TableOfContents
13
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
- from reportlab.lib import colors
15
- from reportlab.lib.units import inch
16
10
  import io
17
- import tempfile
18
- import matplotlib.pyplot as plt
19
- import io, base64
11
+ import base64
20
12
 
21
13
  class AbstractPage:
22
14
  def __init__(self):
23
15
  self.elements = []
24
16
 
25
-
26
17
  def add_header(self, text, level=1, width=None):
27
18
  if level not in (1, 2, 3, 4):
28
19
  raise ValueError("Header level must be 1, 2, 3, or 4")
@@ -48,58 +39,68 @@ class AbstractPage:
48
39
  def add_syntax(self, code, language="python", width=None):
49
40
  self.elements.append(("syntax", (code, language), width))
50
41
 
42
+
51
43
  class Page(AbstractPage):
52
44
  def __init__(self, slug, title, page_width=None, marking=None):
53
45
  super().__init__()
54
46
  self.slug = slug
55
47
  self.title = title
56
48
  self.page_width = page_width
57
- self.marking = marking # Page-specific marking
49
+ self.marking = marking # None => inherit dashboard; str => override
58
50
  self.children = []
59
- # self.add_header(title, level=1)
60
51
 
61
52
  def add_subpage(self, page):
62
53
  self.children.append(page)
63
54
 
64
- def render(self, index, downloads_dir=None, relative_prefix="", inherited_width=None):
55
+ def render(
56
+ self,
57
+ index,
58
+ downloads_dir=None,
59
+ relative_prefix="",
60
+ inherited_width=None,
61
+ inherited_marking=None,
62
+ inherited_distribution=None
63
+ ):
65
64
  effective_width = self.page_width or inherited_width
65
+ effective_marking = self.marking if (self.marking is not None) else inherited_marking
66
+ effective_distribution = getattr(self, "distribution", None) or inherited_distribution
67
+
66
68
  elements = []
67
69
 
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
-
81
- # Add floating header and footer with optional distribution
82
- marking = self.marking or "Default Marking"
83
- distribution = getattr(self, "distribution", None)
84
-
85
- elements.append(div(
86
- marking,
87
- cls="floating-header",
88
- 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;"
89
- ))
90
-
91
- footer_block = []
92
- if distribution:
93
- footer_block.append(div(distribution, style="margin-bottom: 4px; font-size: 10pt;"))
94
- footer_block.append(div(marking))
95
-
96
- elements.append(div(
97
- *footer_block,
98
- cls="floating-footer",
99
- 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;"
100
- ))
70
+ # Only show bars if a marking is present (no default text at all).
71
+ if effective_marking:
72
+ # Center within the MAIN CONTENT AREA (exclude sidebar and #content padding):
73
+ # left = sidebar_width + content_padding_x
74
+ # width = viewport - sidebar_width - 2*content_padding_x
75
+ shared_pos = (
76
+ "position: fixed; "
77
+ "left: calc(var(--sidebar-width, 240px) + var(--content-padding-x, 20px)); "
78
+ "width: calc(100vw - var(--sidebar-width, 240px) - 2*var(--content-padding-x, 20px)); "
79
+ "text-align: center; "
80
+ "background-color: #f8f9fa; "
81
+ "padding: 10px; "
82
+ "z-index: 1000; "
83
+ "font-weight: normal;"
84
+ )
85
+
86
+ elements.append(div(
87
+ effective_marking,
88
+ cls="floating-header",
89
+ style=f"{shared_pos} top: 0;"
90
+ ))
91
+
92
+ footer_block = []
93
+ if effective_distribution:
94
+ footer_block.append(div(effective_distribution, style="margin-bottom: 4px; font-size: 10pt;"))
95
+ footer_block.append(div(effective_marking))
101
96
 
97
+ elements.append(div(
98
+ *footer_block,
99
+ cls="floating-footer",
100
+ style=f"{shared_pos} bottom: 0;"
101
+ ))
102
102
 
103
+ # Render elements
103
104
  for kind, content, el_width in self.elements:
104
105
  style = ""
105
106
  outer_style = ""
@@ -150,22 +151,36 @@ class Page(AbstractPage):
150
151
  cls="syntax-block"
151
152
  )
152
153
  elif kind == "minipage":
153
- elem = content.render(index, downloads_dir=downloads_dir, relative_prefix=relative_prefix, inherited_width=effective_width)
154
+ elem = content.render(
155
+ index,
156
+ downloads_dir=downloads_dir,
157
+ relative_prefix=relative_prefix,
158
+ inherited_width=effective_width,
159
+ inherited_marking=effective_marking,
160
+ inherited_distribution=effective_distribution
161
+ )
154
162
  if el_width is not None:
155
163
  elem = div(elem, style=style)
156
164
  elem = div(elem, style=outer_style)
157
165
  elements.append(elem)
158
166
 
159
- # Add padding to avoid overlap with header and footer
160
- wrapper = div(*elements, style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%; padding-top: 80px; padding-bottom: 80px;")
167
+ # Expose --content-width if you need it later, but centering no longer depends on it
168
+ wrapper_style = (
169
+ f"max-width: {effective_width}px; "
170
+ "margin: 0 auto; width: 100%; "
171
+ "padding-top: 80px; padding-bottom: 80px; "
172
+ f"--content-width: {effective_width}px;"
173
+ )
174
+ wrapper = div(*elements, style=wrapper_style)
161
175
  return [wrapper]
162
176
 
177
+
163
178
  class MiniPage(AbstractPage):
164
179
  def __init__(self, page_width=None):
165
180
  super().__init__()
166
181
  self.page_width = page_width
167
182
 
168
- def render(self, index=None, downloads_dir=None, relative_prefix="", inherited_width=None):
183
+ def render(self, index=None, downloads_dir=None, relative_prefix="", inherited_width=None, inherited_marking=None, inherited_distribution=None):
169
184
  effective_width = self.page_width or inherited_width
170
185
  row_div = div(cls="minipage-row", style=f"max-width: {effective_width}px; margin: 0 auto; width: 100%;")
171
186
  for kind, content, el_width in self.elements:
@@ -183,10 +198,8 @@ class MiniPage(AbstractPage):
183
198
  elem = header_tag(text)
184
199
  elif kind == "plot":
185
200
  fig = content
186
- # Plotly support (existing)
187
201
  if hasattr(fig, "to_html"):
188
202
  elem = div(raw_util(fig.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True})))
189
- # Matplotlib support
190
203
  else:
191
204
  try:
192
205
  buf = io.BytesIO()
@@ -194,7 +207,6 @@ class MiniPage(AbstractPage):
194
207
  buf.seek(0)
195
208
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
196
209
  buf.close()
197
- # Center the image using a div with inline styles
198
210
  elem = div(
199
211
  raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
200
212
  style="display: flex; justify-content: center; align-items: center;"
@@ -218,7 +230,14 @@ class MiniPage(AbstractPage):
218
230
  cls="syntax-block"
219
231
  )
220
232
  elif kind == "minipage":
221
- elem = content.render(index, downloads_dir=downloads_dir, relative_prefix=relative_prefix, inherited_width=effective_width)
233
+ elem = content.render(
234
+ index,
235
+ downloads_dir=downloads_dir,
236
+ relative_prefix=relative_prefix,
237
+ inherited_width=effective_width,
238
+ inherited_marking=inherited_marking,
239
+ inherited_distribution=inherited_distribution
240
+ )
222
241
  if el_width is not None:
223
242
  elem = div(elem, style=style)
224
243
  elem = div(elem, style=outer_style)
@@ -226,67 +245,50 @@ class MiniPage(AbstractPage):
226
245
  row_div += cell
227
246
  return row_div
228
247
 
248
+
229
249
  class Dashboard:
230
250
  def __init__(self, title="Dashboard", page_width=900, marking=None, distribution=None):
231
251
  self.title = title
232
252
  self.pages = []
233
253
  self.page_width = page_width
234
- self.marking = marking # Dashboard-wide marking
235
- self.distribution = distribution # NEW: Distribution statement
254
+ self.marking = marking # Dashboard-wide default (None => no marking)
255
+ self.distribution = distribution # Dashboard-wide distribution statement
236
256
 
237
257
  def add_page(self, page):
238
258
  self.pages.append(page)
239
-
240
- # def _track_outline(self, flowable):
241
- # """
242
- # Hook for collecting TOC entries and setting bookmarks.
243
- # """
244
- # from reportlab.platypus import Paragraph
245
- # if isinstance(flowable, Paragraph):
246
- # text = flowable.getPlainText()
247
- # style_name = flowable.style.name
248
- # if style_name.startswith("Heading"):
249
- # try:
250
- # level = int(style_name.replace("Heading", ""))
251
- # except ValueError:
252
- # return
253
- # key = f"bookmark_{uuid.uuid4().hex}"
254
- # flowable.canv.bookmarkPage(key)
255
- # flowable.canv.addOutlineEntry(text, key, level=level - 1, closed=False)
256
- # flowable._bookmarkName = key
257
-
258
-
259
- def _track_outline(self, canvas, doc):
260
- if hasattr(doc, '_last_heading'):
261
- level, text = doc._last_heading
262
- key = f"bookmark_{uuid.uuid4().hex}"
263
- canvas.bookmarkPage(key)
264
- canvas.addOutlineEntry(text, key, level=level - 1, closed=False)
265
- del doc._last_heading
266
-
267
259
 
268
260
  def _render_sidebar(self, pages, prefix="", current_slug=None):
261
+ # Structure preserved for your JS/CSS:
262
+ # <div class="sidebar-group [open]">
263
+ # <a class="nav-link sidebar-parent" href="...">
264
+ # <span class="sidebar-arrow"></span>Title
265
+ # </a>
266
+ # <div class="sidebar-children"> ... </div>
267
+ # </div>
269
268
  for page in pages:
270
269
  page_href = f"{prefix}{page.slug}.html"
271
270
  is_active = (page.slug == current_slug)
272
- def has_active_child(page):
271
+
272
+ def has_active_child(pg):
273
273
  return any(
274
274
  child.slug == current_slug or has_active_child(child)
275
- for child in getattr(page, "children", [])
275
+ for child in getattr(pg, "children", [])
276
276
  )
277
+
277
278
  group_open = has_active_child(page)
278
279
  link_classes = "nav-link"
279
280
  if getattr(page, "children", []):
280
281
  link_classes += " sidebar-parent"
281
282
  if is_active:
282
283
  link_classes += " active"
284
+
283
285
  if getattr(page, "children", []):
284
286
  group_cls = "sidebar-group"
285
287
  if group_open or is_active:
286
288
  group_cls += " open"
287
289
  with div(cls=group_cls):
288
290
  a([
289
- span("", cls="sidebar-arrow"),
291
+ span("", cls="sidebar-arrow"), # pure-CSS triangle (no Unicode)
290
292
  page.title
291
293
  ], cls=link_classes, href=page_href)
292
294
  with div(cls="sidebar-children"):
@@ -316,7 +318,8 @@ class Dashboard:
316
318
  doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"))
317
319
  doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"))
318
320
  doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"))
319
- # Inject dynamic page width
321
+ # Defaults that match your CSS; override in CSS if they change
322
+ doc.head.add(raw_util("<style>:root{--sidebar-width:240px;--content-padding-x:20px;}</style>"))
320
323
  doc.head.add(raw_util(f"<style>.content-inner {{ max-width: {effective_width}px !important; }}</style>"))
321
324
  with doc:
322
325
  with div(id="sidebar"):
@@ -326,17 +329,26 @@ class Dashboard:
326
329
  a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
327
330
  with div(id="content"):
328
331
  with div(cls="content-inner"):
329
- for el in page.render(0, downloads_dir=downloads_dir, relative_prefix="../"):
332
+ for el in page.render(
333
+ 0,
334
+ downloads_dir=downloads_dir,
335
+ relative_prefix="../",
336
+ inherited_width=self.page_width,
337
+ inherited_marking=self.marking,
338
+ inherited_distribution=self.distribution
339
+ ):
330
340
  div(el)
331
- with open(os.path.join(pages_dir, f"{page.slug}.html"), "w") as f:
341
+
342
+ with open(os.path.join(pages_dir, f"{page.slug}.html"), "w", encoding="utf-8") as f:
332
343
  f.write(str(doc))
344
+
333
345
  for child in getattr(page, "children", []):
334
346
  write_page(child)
335
347
 
336
348
  for page in self.pages:
337
349
  write_page(page)
338
350
 
339
- # Index page
351
+ # Index page (first page)
340
352
  index_doc = document(title=self.title)
341
353
  effective_width = self.pages[0].page_width or self.page_width or 900
342
354
  with index_doc.head:
@@ -352,7 +364,7 @@ class Dashboard:
352
364
  index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"))
353
365
  index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-json.min.js"))
354
366
  index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-c.min.js"))
355
- # Inject dynamic page width
367
+ index_doc.head.add(raw_util("<style>:root{--sidebar-width:240px;--content-padding-x:20px;}</style>"))
356
368
  index_doc.head.add(raw_util(f"<style>.content-inner {{ max-width: {effective_width}px !important; }}</style>"))
357
369
  with index_doc:
358
370
  with div(id="sidebar"):
@@ -362,293 +374,15 @@ class Dashboard:
362
374
  a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
363
375
  with div(id="content"):
364
376
  with div(cls="content-inner"):
365
- for el in self.pages[0].render(0, downloads_dir=downloads_dir, relative_prefix=""):
377
+ for el in self.pages[0].render(
378
+ 0,
379
+ downloads_dir=downloads_dir,
380
+ relative_prefix="",
381
+ inherited_width=self.page_width,
382
+ inherited_marking=self.marking,
383
+ inherited_distribution=self.distribution
384
+ ):
366
385
  div(el)
367
386
 
368
- with open(os.path.join(output_dir, "index.html"), "w") as f:
387
+ with open(os.path.join(output_dir, "index.html"), "w", encoding="utf-8") as f:
369
388
  f.write(str(index_doc))
370
-
371
- 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):
372
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, Image
373
- from reportlab.platypus.tableofcontents import TableOfContents
374
- from reportlab.lib.pagesizes import A4, letter
375
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
376
- from reportlab.lib import colors
377
- from reportlab.lib.units import inch
378
- from datetime import datetime
379
- import tempfile
380
- import io
381
- import os
382
- import plotly.io as pio
383
-
384
- pio.kaleido.scope.default_format = "png"
385
-
386
- page_size = A4 if pagesize.upper() == "A4" else letter
387
- styles = getSampleStyleSheet()
388
- normal_style = styles['Normal']
389
-
390
- styles['Heading1'].fontSize = 18
391
- styles['Heading1'].spaceAfter = 12
392
- styles['Heading1'].spaceBefore = 18
393
- styles['Heading1'].fontName = 'Helvetica-Bold'
394
-
395
- styles['Heading2'].fontSize = 14
396
- styles['Heading2'].spaceAfter = 8
397
- styles['Heading2'].spaceBefore = 12
398
- styles['Heading2'].fontName = 'Helvetica-Bold'
399
-
400
- if 'CodeBlock' not in styles:
401
- styles.add(ParagraphStyle(
402
- name='CodeBlock',
403
- fontName='Courier',
404
- fontSize=9,
405
- leading=12,
406
- backColor=colors.whitesmoke,
407
- leftIndent=12,
408
- rightIndent=12,
409
- spaceAfter=8,
410
- borderPadding=4
411
- ))
412
-
413
- story = []
414
-
415
- class MyDocTemplate(SimpleDocTemplate):
416
- def __init__(self, *args, **kwargs):
417
- self.outline_entries = []
418
- self._outline_idx = 0
419
- super().__init__(*args, **kwargs)
420
-
421
-
422
- def afterFlowable(self, flowable):
423
- from reportlab.platypus import Paragraph
424
- if isinstance(flowable, Paragraph):
425
- style_name = flowable.style.name
426
- if style_name.startswith('Heading'):
427
- try:
428
- level = int(style_name.replace("Heading", ""))
429
- except ValueError:
430
- return # Not a valid heading style
431
-
432
- # Convert to outline level (0 = H1, 1 = H2, etc.)
433
- outline_level = level - 1
434
-
435
- # Clamp max to 2 for PDF outline safety
436
- outline_level = max(0, min(outline_level, 2))
437
-
438
- # Prevent skipping levels: ensure intermediates exist
439
- # Track previous levels (add this as a class attribute if needed)
440
- if not hasattr(self, "_last_outline_level"):
441
- self._last_outline_level = -1
442
-
443
- if outline_level > self._last_outline_level + 1:
444
- outline_level = self._last_outline_level + 1 # prevent jump
445
-
446
- self._last_outline_level = outline_level
447
-
448
- text = flowable.getPlainText()
449
- key = 'heading_%s' % self.seq.nextf('heading')
450
- self.canv.bookmarkPage(key)
451
- self.canv.addOutlineEntry(text, key, level=outline_level, closed=False)
452
-
453
-
454
- self.notify('TOCEntry', (outline_level, text, self.page))
455
-
456
-
457
- def add_marking(canvas, doc, marking):
458
- from reportlab.pdfbase.pdfmetrics import stringWidth
459
-
460
- distribution = self.distribution
461
- canvas.saveState()
462
- canvas.setFont("Helvetica", 10)
463
- width, height = doc.pagesize
464
-
465
- line_height = 12
466
- margin_y = 36
467
-
468
- # Top of page marking
469
- if marking:
470
- top_width = stringWidth(marking, "Helvetica", 10)
471
- x_top = (width - top_width) / 2
472
- canvas.drawString(x_top, height - margin_y, marking)
473
-
474
- # Bottom of page — prepare to stack upward
475
- y = margin_y
476
-
477
- # First: bottom marking
478
- if marking:
479
- bot_width = stringWidth(marking, "Helvetica", 10)
480
- x_bot = (width - bot_width) / 2
481
- canvas.drawString(x_bot, y, marking)
482
- y += line_height # make room above
483
-
484
- # Then: bottom distribution (above marking)
485
- if distribution:
486
- max_width = width - 144 # ~1" margins
487
- words = distribution.split()
488
- lines = []
489
- current_line = []
490
-
491
- for word in words:
492
- test_line = " ".join(current_line + [word])
493
- if stringWidth(test_line, "Helvetica", 10) <= max_width:
494
- current_line.append(word)
495
- else:
496
- lines.append(" ".join(current_line))
497
- current_line = [word]
498
-
499
- if current_line:
500
- lines.append(" ".join(current_line))
501
-
502
- # Draw top-down (above marking)
503
- for line in reversed(lines):
504
- line_width = stringWidth(line, "Helvetica", 10)
505
- x = (width - line_width) / 2
506
- y += line_height
507
- canvas.drawString(x, y, line)
508
-
509
- canvas.restoreState()
510
-
511
-
512
- if include_title_page:
513
- story.append(Spacer(1, 120))
514
- story.append(Paragraph(f"<b>{self.title}</b>", styles['Title']))
515
- story.append(Spacer(1, 48))
516
- lines = []
517
- if author:
518
- lines.append(str(author))
519
- if affiliation:
520
- lines.append(str(affiliation))
521
- lines.append(datetime.now().strftime('%B %d, %Y'))
522
- story.append(Paragraph("<para align='center'>" + "<br/>".join(lines) + "</para>", normal_style))
523
- story.append(PageBreak())
524
-
525
- if include_toc:
526
- toc = TableOfContents()
527
- toc.levelStyles = [
528
- ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, firstLineIndent=-20, spaceBefore=10, leading=16),
529
- ParagraphStyle(name='TOCHeading2', fontSize=12, leftIndent=40, firstLineIndent=-20, spaceBefore=5, leading=12),
530
- ]
531
- story.append(Paragraph("Table of Contents", styles["Title"]))
532
- story.append(toc)
533
- story.append(PageBreak())
534
-
535
- def render_page(page, level=0, sec_prefix=[]):
536
- # Generate section numbering like "1", "1.2", etc.
537
- section_number = ".".join(map(str, sec_prefix))
538
- heading_text = f"{section_number} {page.title}"
539
-
540
- heading_style = styles.get(f'Heading{min(level + 1, 4)}', styles['Heading4'])
541
- story.append(Paragraph(heading_text, heading_style))
542
- story.append(Spacer(1, 12))
543
-
544
-
545
- # Remember where we started
546
- content_start = len(story)
547
-
548
-
549
- for kind, content, _ in page.elements:
550
- try:
551
- if kind == "text":
552
- story.append(Paragraph(content, normal_style))
553
- story.append(Spacer(1, 8))
554
-
555
- elif kind == "header":
556
- text, lvl = content
557
- safe_lvl = max(1, min(lvl + 1, 4))
558
- style = styles[f'Heading{safe_lvl}']
559
- story.append(Paragraph(text, style))
560
- story.append(Spacer(1, 8))
561
-
562
- elif kind == "table":
563
- df = content
564
- data = [df.columns.tolist()] + df.values.tolist()
565
- t = Table(data, repeatRows=1)
566
- t.setStyle(TableStyle([
567
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#222C36")),
568
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
569
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
570
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
571
- ('FONTSIZE', (0, 0), (-1, 0), 11),
572
- ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
573
- ('TOPPADDING', (0, 0), (-1, 0), 10),
574
- ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
575
- ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor("#B0B8C1")),
576
- ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
577
- ('FONTSIZE', (0, 1), (-1, -1), 10),
578
- ('LEFTPADDING', (0, 0), (-1, -1), 6),
579
- ('RIGHTPADDING', (0, 0), (-1, -1), 6),
580
- ('TOPPADDING', (0, 1), (-1, -1), 6),
581
- ('BOTTOMPADDING', (0, 1), (-1, -1), 6),
582
- ]))
583
- story.append(t)
584
- story.append(Spacer(1, 12))
585
-
586
- elif kind == "plot":
587
- fig = content
588
- buf = io.BytesIO()
589
- if hasattr(fig, "savefig"):
590
- fig.savefig(buf, format="png", bbox_inches="tight", dpi=600)
591
- else:
592
- fig.write_image(buf, format="png", width=800, height=600, scale=3)
593
- buf.seek(0)
594
- story.append(Image(buf, width=6 * inch, height=4.5 * inch))
595
- story.append(Spacer(1, 12))
596
-
597
- elif kind == "syntax":
598
- code, language = content
599
- from html import escape
600
- story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
601
- story.append(Spacer(1, 4))
602
- code_html = escape(code).replace(" ", "&nbsp;").replace("\n", "<br/>")
603
- story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
604
- story.append(Spacer(1, 12))
605
-
606
- elif kind == "minipage":
607
- render_page(content, level=level + 1, sec_prefix=sec_prefix)
608
-
609
- except Exception as e:
610
- story.append(Paragraph(f"Error rendering element: {e}", normal_style))
611
-
612
-
613
- just_broke = False
614
-
615
- for i, child in enumerate(page.children):
616
- if i > 0 and not just_broke:
617
- story.append(PageBreak())
618
-
619
- before = len(story)
620
- render_page(child, level=level + 1, sec_prefix=sec_prefix + [i + 1])
621
- after = len(story)
622
-
623
- # Determine if child added a PageBreak
624
- just_broke = isinstance(story[-1], PageBreak) if after > before else False
625
-
626
-
627
- # Determine if anything meaningful was added
628
- def has_meaningful_content(start_idx):
629
- for elem in story[start_idx:]:
630
- if isinstance(elem, (Paragraph, Table, Image)):
631
- return True
632
- return False
633
-
634
- if not page.children and has_meaningful_content(content_start):
635
- story.append(PageBreak())
636
-
637
-
638
-
639
- for i, page in enumerate(self.pages):
640
- render_page(page, level=0, sec_prefix=[i + 1])
641
-
642
-
643
-
644
- doc = MyDocTemplate(
645
- output_path,
646
- pagesize=page_size,
647
- rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72,
648
- )
649
-
650
- doc.multiBuild(
651
- story,
652
- onFirstPage=lambda canvas, doc: add_marking(canvas, doc, title_page_marking),
653
- onLaterPages=lambda canvas, doc: add_marking(canvas, doc, self.marking)
654
- )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.27
3
+ Version: 2025.29
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=0RVyqL3oEi7-Ey6ZZa9Y0V3WQ6MC8YiGZ0swKtLYnEg,18233
3
+ staticdash/assets/css/style.css,sha256=JCoEkEzDiGc29jjhtWBMv-cirwzSWgOpCviMVIm6x2s,6533
4
+ staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
5
+ staticdash-2025.29.dist-info/METADATA,sha256=fG2DNkSklYBSHYYByt9ZY-CNc5rv2FEsnnRXCKFpP8A,1960
6
+ staticdash-2025.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ staticdash-2025.29.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
+ staticdash-2025.29.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- staticdash/__init__.py,sha256=UN_-h8wFGfTPHYjnEb7N9CsxqXo-DQVo0cmREOtvRXE,244
2
- staticdash/dashboard.py,sha256=nqA9MLxSqZNFs-Ko54czD1kZgQ4EP9qC4HvgvR54_ds,29891
3
- staticdash/assets/css/style.css,sha256=RVqNdwBsaDv8izdOQjGmUZ4NROWF8uZhiq8DTNvUB1M,5962
4
- staticdash/assets/js/script.js,sha256=7xBRlz_19wybbNVwAcfuKNXtDEojGB4EB0Yj4klsoTA,6998
5
- staticdash-2025.27.dist-info/METADATA,sha256=lNhzK7RFO-Ewxc6K1E7NNexBEgV7un7x14ip3VkvQ4c,1960
6
- staticdash-2025.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- staticdash-2025.27.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
- staticdash-2025.27.dist-info/RECORD,,