staticdash 2025.23__tar.gz → 2025.25__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.23
3
+ Version: 2025.25
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.23"
7
+ version = "2025.25"
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" }
@@ -65,19 +65,41 @@ class Page(AbstractPage):
65
65
  effective_width = self.page_width or inherited_width
66
66
  elements = []
67
67
 
68
- # Add floating header and footer for marking
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
69
82
  marking = self.marking or "Default Marking"
83
+ distribution = getattr(self, "distribution", None)
84
+
70
85
  elements.append(div(
71
86
  marking,
72
87
  cls="floating-header",
73
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;"
74
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
+
75
96
  elements.append(div(
76
- marking,
97
+ *footer_block,
77
98
  cls="floating-footer",
78
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;"
79
100
  ))
80
101
 
102
+
81
103
  for kind, content, el_width in self.elements:
82
104
  style = ""
83
105
  outer_style = ""
@@ -205,11 +227,12 @@ class MiniPage(AbstractPage):
205
227
  return row_div
206
228
 
207
229
  class Dashboard:
208
- def __init__(self, title="Dashboard", page_width=900, marking=None):
230
+ def __init__(self, title="Dashboard", page_width=900, marking=None, distribution=None):
209
231
  self.title = title
210
232
  self.pages = []
211
233
  self.page_width = page_width
212
234
  self.marking = marking # Dashboard-wide marking
235
+ self.distribution = distribution # NEW: Distribution statement
213
236
 
214
237
  def add_page(self, page):
215
238
  self.pages.append(page)
@@ -431,17 +454,60 @@ class Dashboard:
431
454
  self.notify('TOCEntry', (outline_level, text, self.page))
432
455
 
433
456
 
434
-
435
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
436
478
  if marking:
437
- canvas.saveState()
438
- canvas.setFont("Helvetica", 10)
439
- width, height = doc.pagesize
440
- text_width = canvas.stringWidth(marking, "Helvetica", 10)
441
- x = (width - text_width) / 2
442
- canvas.drawString(x, height - 36, marking)
443
- canvas.drawString(x, 36, marking)
444
- canvas.restoreState()
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
+
445
511
 
446
512
  if include_title_page:
447
513
  story.append(Spacer(1, 120))
@@ -467,33 +533,29 @@ class Dashboard:
467
533
  story.append(PageBreak())
468
534
 
469
535
  def render_page(page, level=0, sec_prefix=[]):
470
- heading_style = styles['Heading1'] if level == 0 else styles['Heading2']
536
+ heading_style = styles.get(f'Heading{min(level + 1, 4)}', styles['Heading4'])
471
537
 
472
- # Only add the page.title as a real heading if it's a top-level page
473
- # if level == 0:
474
- # story.append(Paragraph(page.title, heading_style))
475
- # story.append(Spacer(1, 12))
538
+ # Remember where we started
539
+ content_start = len(story)
476
540
 
477
- heading_style = styles['Heading1'] if level == 0 else styles['Heading2']
478
541
  story.append(Paragraph(page.title, heading_style))
479
542
  story.append(Spacer(1, 12))
480
543
 
481
-
482
544
  for kind, content, _ in page.elements:
483
- if kind == "text":
484
- story.append(Paragraph(content, normal_style))
485
- story.append(Spacer(1, 8))
486
-
487
- elif kind == "header":
488
- text, lvl = content
489
- safe_lvl = max(1, min(lvl + 1, 4)) # Clamp to Heading1–Heading4
490
- style = styles[f'Heading{safe_lvl}']
491
- story.append(Paragraph(text, style))
492
- story.append(Spacer(1, 8))
493
-
494
- elif kind == "table":
495
- df = content
496
- try:
545
+ try:
546
+ if kind == "text":
547
+ story.append(Paragraph(content, normal_style))
548
+ story.append(Spacer(1, 8))
549
+
550
+ elif kind == "header":
551
+ text, lvl = content
552
+ safe_lvl = max(1, min(lvl + 1, 4))
553
+ style = styles[f'Heading{safe_lvl}']
554
+ story.append(Paragraph(text, style))
555
+ story.append(Spacer(1, 8))
556
+
557
+ elif kind == "table":
558
+ df = content
497
559
  data = [df.columns.tolist()] + df.values.tolist()
498
560
  t = Table(data, repeatRows=1)
499
561
  t.setStyle(TableStyle([
@@ -515,48 +577,59 @@ class Dashboard:
515
577
  ]))
516
578
  story.append(t)
517
579
  story.append(Spacer(1, 12))
518
- except Exception:
519
- story.append(Paragraph("Table could not be rendered.", normal_style))
520
580
 
521
- elif kind == "plot":
522
- fig = content
523
- try:
524
- if hasattr(fig, "savefig"): # Matplotlib
525
- buf = io.BytesIO()
581
+ elif kind == "plot":
582
+ fig = content
583
+ buf = io.BytesIO()
584
+ if hasattr(fig, "savefig"):
526
585
  fig.savefig(buf, format="png", bbox_inches="tight", dpi=300)
527
- buf.seek(0)
528
- story.append(Spacer(1, 8))
529
- story.append(Image(buf, width=6*inch, height=4.5*inch))
530
- story.append(Spacer(1, 12))
531
- elif hasattr(fig, "write_image"): # Plotly
532
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
533
- fig.write_image(tmpfile.name, width=600, height=360, scale=2)
534
- with open(tmpfile.name, "rb") as f:
535
- story.append(Spacer(1, 8))
536
- story.append(Image(io.BytesIO(f.read()), width=6*inch, height=3.6*inch))
537
- story.append(Spacer(1, 12))
538
- os.unlink(tmpfile.name)
539
- except Exception as e:
540
- story.append(Paragraph(f"Plot rendering failed: {e}", normal_style))
586
+ else:
587
+ fig.write_image(buf, format="png", width=600, height=360, scale=2)
588
+ buf.seek(0)
589
+ story.append(Image(buf, width=6 * inch, height=4.5 * inch))
590
+ story.append(Spacer(1, 12))
591
+
592
+ elif kind == "syntax":
593
+ code, language = content
594
+ from html import escape
595
+ story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
596
+ story.append(Spacer(1, 4))
597
+ code_html = escape(code).replace(" ", "&nbsp;").replace("\n", "<br/>")
598
+ story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
599
+ story.append(Spacer(1, 12))
600
+
601
+ elif kind == "minipage":
602
+ render_page(content, level=level + 1, sec_prefix=sec_prefix)
541
603
 
542
- elif kind == "syntax":
543
- code, language = content
544
- from html import escape
545
- story.append(Paragraph(f"<b>Code ({language}):</b>", normal_style))
546
- story.append(Spacer(1, 4))
547
- code_html = escape(code).replace(" ", "&nbsp;").replace("\n", "<br/>")
548
- story.append(Paragraph(f"<font face='Courier'>{code_html}</font>", styles['CodeBlock']))
549
- story.append(Spacer(1, 12))
604
+ except Exception as e:
605
+ story.append(Paragraph(f"Error rendering element: {e}", normal_style))
606
+
607
+
608
+ just_broke = False
609
+
610
+ for i, child in enumerate(page.children):
611
+ if i > 0 and not just_broke:
612
+ story.append(PageBreak())
550
613
 
551
- elif kind == "minipage":
552
- render_page(content, level=level + 1, sec_prefix=sec_prefix)
614
+ before = len(story)
615
+ render_page(child, level=level + 1, sec_prefix=sec_prefix + [i + 1])
616
+ after = len(story)
553
617
 
554
- for child in page.children:
618
+ # Determine if child added a PageBreak
619
+ just_broke = isinstance(story[-1], PageBreak) if after > before else False
620
+
621
+
622
+ # Determine if anything meaningful was added
623
+ def has_meaningful_content(start_idx):
624
+ for elem in story[start_idx:]:
625
+ if isinstance(elem, (Paragraph, Table, Image)):
626
+ return True
627
+ return False
628
+
629
+ if not page.children and has_meaningful_content(content_start):
555
630
  story.append(PageBreak())
556
- render_page(child, level=level + 1, sec_prefix=sec_prefix + [1])
557
631
 
558
632
 
559
- story.append(PageBreak())
560
633
 
561
634
  for page in self.pages:
562
635
  render_page(page)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2025.23
3
+ Version: 2025.25
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