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.
- {staticdash-2025.23 → staticdash-2025.25}/PKG-INFO +1 -1
- {staticdash-2025.23 → staticdash-2025.25}/pyproject.toml +1 -1
- {staticdash-2025.23 → staticdash-2025.25}/staticdash/dashboard.py +140 -67
- {staticdash-2025.23 → staticdash-2025.25}/staticdash.egg-info/PKG-INFO +1 -1
- {staticdash-2025.23 → staticdash-2025.25}/README.md +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/setup.cfg +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash/__init__.py +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash/assets/css/style.css +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash/assets/js/script.js +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash.egg-info/SOURCES.txt +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash.egg-info/dependency_links.txt +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash.egg-info/requires.txt +0 -0
- {staticdash-2025.23 → staticdash-2025.25}/staticdash.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "staticdash"
|
|
7
|
-
version = "2025.
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
536
|
+
heading_style = styles.get(f'Heading{min(level + 1, 4)}', styles['Heading4'])
|
|
471
537
|
|
|
472
|
-
#
|
|
473
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if hasattr(fig, "savefig"):
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
story.append(
|
|
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(" ", " ").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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
552
|
-
|
|
614
|
+
before = len(story)
|
|
615
|
+
render_page(child, level=level + 1, sec_prefix=sec_prefix + [i + 1])
|
|
616
|
+
after = len(story)
|
|
553
617
|
|
|
554
|
-
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|