staticdash 2026.2__tar.gz → 2026.4__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.
Files changed (25) hide show
  1. {staticdash-2026.2 → staticdash-2026.4}/PKG-INFO +5 -3
  2. {staticdash-2026.2 → staticdash-2026.4}/pyproject.toml +14 -7
  3. {staticdash-2026.2 → staticdash-2026.4}/staticdash/dashboard.py +283 -22
  4. {staticdash-2026.2 → staticdash-2026.4}/staticdash.egg-info/PKG-INFO +5 -3
  5. {staticdash-2026.2 → staticdash-2026.4}/staticdash.egg-info/requires.txt +6 -2
  6. {staticdash-2026.2 → staticdash-2026.4}/README.md +0 -0
  7. {staticdash-2026.2 → staticdash-2026.4}/setup.cfg +0 -0
  8. {staticdash-2026.2 → staticdash-2026.4}/setup.py +0 -0
  9. {staticdash-2026.2 → staticdash-2026.4}/staticdash/__init__.py +0 -0
  10. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/css/style.css +0 -0
  11. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/js/script.js +0 -0
  12. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/mathjax/tex-mml-chtml.js +0 -0
  13. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/plotly/plotly.min.js +0 -0
  14. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-bash.min.js +0 -0
  15. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-c.min.js +0 -0
  16. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-javascript.min.js +0 -0
  17. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-json.min.js +0 -0
  18. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-markup.min.js +0 -0
  19. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-python.min.js +0 -0
  20. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-sql.min.js +0 -0
  21. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/prism-tomorrow.min.css +0 -0
  22. {staticdash-2026.2 → staticdash-2026.4}/staticdash/assets/vendor/prism/prism.min.js +0 -0
  23. {staticdash-2026.2 → staticdash-2026.4}/staticdash.egg-info/SOURCES.txt +0 -0
  24. {staticdash-2026.2 → staticdash-2026.4}/staticdash.egg-info/dependency_links.txt +0 -0
  25. {staticdash-2026.2 → staticdash-2026.4}/staticdash.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2026.2
3
+ Version: 2026.4
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
@@ -11,9 +11,11 @@ Description-Content-Type: text/markdown
11
11
  Requires-Dist: plotly
12
12
  Requires-Dist: pandas
13
13
  Requires-Dist: dominate
14
- Requires-Dist: reportlab
15
- Requires-Dist: kaleido
16
14
  Requires-Dist: matplotlib
15
+ Provides-Extra: images
16
+ Requires-Dist: kaleido; extra == "images"
17
+ Provides-Extra: all
18
+ Requires-Dist: kaleido; extra == "all"
17
19
 
18
20
  # staticdash
19
21
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "staticdash"
7
- version = "2026.2"
7
+ version = "2026.4"
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" }
@@ -13,14 +13,21 @@ license = { text = "CC0-1.0" }
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.8"
15
15
  dependencies = [
16
- "plotly",
17
- "pandas",
18
- "dominate",
19
- "reportlab",
20
- "kaleido",
21
- "matplotlib"
16
+ "plotly",
17
+ "pandas",
18
+ "dominate",
19
+ "matplotlib",
22
20
  ]
23
21
 
22
+ [project.optional-dependencies]
23
+ # Optional plotting backends and export tools
24
+ images = ["kaleido"]
25
+ # PDF export (reportlab) was removed from defaults because it's not referenced from the package code.
26
+ # If you need PDF export, add reportlab explicitly: `pip install "staticdash[pdf]"` once the feature is implemented.
27
+
28
+ # Convenience bundle for full feature set
29
+ all = ["kaleido"]
30
+
24
31
  [tool.setuptools]
25
32
  packages = ["staticdash"]
26
33
 
@@ -10,8 +10,38 @@ from dominate.util import raw as raw_util
10
10
  import html
11
11
  import io
12
12
  import base64
13
+ import matplotlib
13
14
  from matplotlib import rc_context
14
15
 
16
+ def split_paragraphs_preserving_math(text):
17
+ """
18
+ Split text into paragraphs on double newlines, preserving math expressions.
19
+ Assumes math is in $...$ (inline) or $$...$$ (display) format.
20
+ """
21
+ math_blocks = []
22
+
23
+ # Replace math with placeholders
24
+ def replace_math(match):
25
+ math_blocks.append(match.group(0))
26
+ return f"__MATH_BLOCK_{len(math_blocks)-1}__"
27
+
28
+ # Handle display math first ($$...$$), then inline ($...$)
29
+ text = re.sub(r'\$\$([^$]+)\$\$', replace_math, text, flags=re.DOTALL)
30
+ text = re.sub(r'\$([^$]+)\$', replace_math, text, flags=re.DOTALL)
31
+
32
+ # Split on double newlines
33
+ paragraphs = text.split('\n\n')
34
+
35
+ # Restore math in each paragraph
36
+ restored_paragraphs = []
37
+ for para in paragraphs:
38
+ for i, block in enumerate(math_blocks):
39
+ para = para.replace(f"__MATH_BLOCK_{i}__", block)
40
+ restored_paragraphs.append(para.strip())
41
+
42
+ # Filter out empty paragraphs
43
+ return [para for para in restored_paragraphs if para]
44
+
15
45
  class AbstractPage:
16
46
  def __init__(self):
17
47
  self.elements = []
@@ -24,8 +54,13 @@ class AbstractPage:
24
54
  def add_text(self, text, width=None):
25
55
  self.elements.append(("text", text, width))
26
56
 
27
- def add_plot(self, plot, width=None):
28
- self.elements.append(("plot", plot, width))
57
+ def add_plot(self, plot, width=None, height=None, width_px=None, align="center"):
58
+ # Keep backward-compatible: `width` is a fraction (0..1) of page width.
59
+ # `height` is pixels (existing behavior). New optional `width_px`
60
+ # (pixels) can be supplied to control plot width. `align` controls
61
+ # horizontal alignment: 'left', 'center' (default), or 'right'.
62
+ # We store a tuple (plot, height_px, width_px, align) for forward-compatibility.
63
+ self.elements.append(("plot", (plot, height, width_px, align), width))
29
64
 
30
65
  def add_table(self, df, table_id=None, sortable=True, width=None):
31
66
  self.elements.append(("table", df, width))
@@ -111,13 +146,33 @@ class Page(AbstractPage):
111
146
  outer_style = "display: flex; justify-content: center; margin: 0 auto;"
112
147
  elem = None
113
148
  if kind == "text":
114
- elem = p(content)
149
+ paragraphs = split_paragraphs_preserving_math(content)
150
+ if paragraphs:
151
+ elem = div(*[p(para) for para in paragraphs])
152
+ else:
153
+ elem = p(content)
115
154
  elif kind == "header":
116
155
  text, level = content
117
156
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
118
157
  elem = header_tag(text)
119
158
  elif kind == "plot":
120
- fig = content
159
+ # content may be stored as (figure, height), (figure, height, width_px)
160
+ # or (figure, height, width_px, align)
161
+ specified_height = None
162
+ specified_width_px = None
163
+ specified_align = "center"
164
+ if isinstance(content, (list, tuple)):
165
+ if len(content) == 4:
166
+ fig, specified_height, specified_width_px, specified_align = content
167
+ elif len(content) == 3:
168
+ fig, specified_height, specified_width_px = content
169
+ elif len(content) == 2:
170
+ fig, specified_height = content
171
+ else:
172
+ fig = content
173
+ else:
174
+ fig = content
175
+
121
176
  if hasattr(fig, "to_html"):
122
177
  # Use local Plotly loaded in <head>
123
178
  # Ensure the figure uses a robust font family so minus signs and other
@@ -131,28 +186,128 @@ class Page(AbstractPage):
131
186
  except Exception:
132
187
  # Be defensive: don't fail rendering if layout manipulation isn't available
133
188
  pass
134
- # Render Plotly HTML and replace Unicode minus (U+2212) with ASCII hyphen-minus
135
- # to avoid rendering issues when a user's font lacks the U+2212 glyph.
189
+
136
190
  try:
137
- plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
138
- elem = div(raw_util(plotly_html))
191
+ # Temporarily set layout height/width if specified (pixels)
192
+ orig_height = None
193
+ orig_width = None
194
+ try:
195
+ orig_height = getattr(fig.layout, 'height', None)
196
+ except Exception:
197
+ orig_height = None
198
+ try:
199
+ orig_width = getattr(fig.layout, 'width', None)
200
+ except Exception:
201
+ orig_width = None
202
+
203
+ try:
204
+ if specified_height is not None:
205
+ fig.update_layout(height=specified_height)
206
+ if specified_width_px is not None:
207
+ fig.update_layout(width=specified_width_px)
208
+ except Exception:
209
+ pass
210
+
211
+ # If a local vendored Plotly exists, rely on the head script.
212
+ # Otherwise include Plotly from CDN so the inline newPlot call works.
213
+ vendor_plotly = os.path.join(os.path.dirname(__file__), "assets", "vendor", "plotly", "plotly.min.js")
214
+ include_plotly = False
215
+ if not os.path.exists(vendor_plotly):
216
+ include_plotly = "cdn"
217
+ plotly_html = fig.to_html(full_html=False, include_plotlyjs=include_plotly, config={'responsive': True})
218
+
219
+ # Wrap the Plotly HTML in a container with explicit pixel sizing
220
+ container_style = "width:100%;"
221
+ if specified_width_px is not None:
222
+ container_style = f"width:{specified_width_px}px;"
223
+ if specified_height is not None:
224
+ container_style = container_style + f" height:{specified_height}px;"
225
+
226
+ plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
227
+ # Apply alignment wrapper
228
+ if specified_align not in ("left", "right", "center"):
229
+ specified_align = "center"
230
+ if specified_align == "center":
231
+ align_style = "display:flex; justify-content:center; align-items:center;"
232
+ elif specified_align == "left":
233
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
234
+ else:
235
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
236
+
237
+ outer = f'<div style="{align_style}">{plot_wrapped}</div>'
238
+ elem = div(raw_util(outer))
239
+
240
+ # restore original height/width if we changed them
241
+ try:
242
+ if specified_height is not None:
243
+ fig.update_layout(height=orig_height)
244
+ if specified_width_px is not None:
245
+ fig.update_layout(width=orig_width)
246
+ except Exception:
247
+ pass
139
248
  except Exception as e:
140
249
  elem = div(f"Plotly figure could not be rendered: {e}")
141
250
  else:
251
+ # Robust Matplotlib -> PNG path. Ensure `buf` exists and is closed.
252
+ buf = io.BytesIO()
142
253
  try:
143
- buf = io.BytesIO()
144
- # Ensure we use ASCII hyphen-minus for negative ticks when saving
254
+ # If pixel width/height specified, attempt to adjust figure size
255
+ orig_size = None
256
+ try:
257
+ dpi = fig.get_dpi()
258
+ except Exception:
259
+ dpi = None
260
+ try:
261
+ if dpi is not None and (specified_height is not None or specified_width_px is not None):
262
+ orig_size = fig.get_size_inches()
263
+ new_w = orig_size[0]
264
+ new_h = orig_size[1]
265
+ if specified_width_px is not None:
266
+ new_w = specified_width_px / dpi
267
+ if specified_height is not None:
268
+ new_h = specified_height / dpi
269
+ fig.set_size_inches(new_w, new_h)
270
+ except Exception:
271
+ orig_size = None
272
+
145
273
  with rc_context({"axes.unicode_minus": False}):
146
274
  fig.savefig(buf, format="png", bbox_inches="tight")
275
+
276
+ # restore original size if changed
277
+ try:
278
+ if orig_size is not None:
279
+ fig.set_size_inches(orig_size)
280
+ except Exception:
281
+ pass
282
+
147
283
  buf.seek(0)
148
284
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
149
- buf.close()
285
+ img_style = "max-width:100%;"
286
+ if specified_height is not None:
287
+ img_style = img_style + f" height:{specified_height}px;"
288
+ if specified_width_px is not None:
289
+ img_style = img_style + f" width:{specified_width_px}px;"
290
+
291
+ if specified_align not in ("left", "right", "center"):
292
+ specified_align = "center"
293
+ if specified_align == "center":
294
+ align_style = "display:flex; justify-content:center; align-items:center;"
295
+ elif specified_align == "left":
296
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
297
+ else:
298
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
299
+
150
300
  elem = div(
151
- raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
152
- style="display: flex; justify-content: center; align-items: center;"
301
+ raw_util(f'<img src="data:image/png;base64,{img_base64}" style="{img_style}">'),
302
+ style=align_style
153
303
  )
154
304
  except Exception as e:
155
305
  elem = div(f"Matplotlib figure could not be rendered: {e}")
306
+ finally:
307
+ try:
308
+ buf.close()
309
+ except Exception:
310
+ pass
156
311
  elif kind == "table":
157
312
  df = content
158
313
  try:
@@ -213,13 +368,29 @@ class MiniPage(AbstractPage):
213
368
  outer_style = "display: flex; justify-content: center; margin: 0 auto;"
214
369
  elem = None
215
370
  if kind == "text":
216
- elem = p(content)
371
+ paragraphs = split_paragraphs_preserving_math(content)
372
+ if paragraphs:
373
+ elem = div(*[p(para) for para in paragraphs])
374
+ else:
375
+ elem = p(content)
217
376
  elif kind == "header":
218
377
  text, level = content
219
378
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
220
379
  elem = header_tag(text)
221
380
  elif kind == "plot":
222
- fig = content
381
+ # content may be stored as (figure, height) or (figure, height, width_px)
382
+ specified_height = None
383
+ specified_width_px = None
384
+ if isinstance(content, (list, tuple)):
385
+ if len(content) == 3:
386
+ fig, specified_height, specified_width_px = content
387
+ elif len(content) == 2:
388
+ fig, specified_height = content
389
+ else:
390
+ fig = content
391
+ else:
392
+ fig = content
393
+
223
394
  if hasattr(fig, "to_html"):
224
395
  # Use local Plotly loaded in <head>
225
396
  # Ensure the figure uses a robust font family so minus signs and other
@@ -233,25 +404,115 @@ class MiniPage(AbstractPage):
233
404
  except Exception:
234
405
  pass
235
406
  try:
236
- plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
237
- elem = div(raw_util(plotly_html))
407
+ orig_height = None
408
+ orig_width = None
409
+ try:
410
+ orig_height = getattr(fig.layout, 'height', None)
411
+ except Exception:
412
+ orig_height = None
413
+ try:
414
+ orig_width = getattr(fig.layout, 'width', None)
415
+ except Exception:
416
+ orig_width = None
417
+
418
+ try:
419
+ if specified_height is not None:
420
+ fig.update_layout(height=specified_height)
421
+ if specified_width_px is not None:
422
+ fig.update_layout(width=specified_width_px)
423
+ except Exception:
424
+ pass
425
+
426
+ vendor_plotly = os.path.join(os.path.dirname(__file__), "assets", "vendor", "plotly", "plotly.min.js")
427
+ include_plotly = False
428
+ if not os.path.exists(vendor_plotly):
429
+ include_plotly = "cdn"
430
+ plotly_html = fig.to_html(full_html=False, include_plotlyjs=include_plotly, config={'responsive': True})
431
+ container_style = "width:100%;"
432
+ if specified_width_px is not None:
433
+ container_style = f"width:{specified_width_px}px;"
434
+ if specified_height is not None:
435
+ container_style = container_style + f" height:{specified_height}px;"
436
+ plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
437
+ if specified_align not in ("left", "right", "center"):
438
+ specified_align = "center"
439
+ if specified_align == "center":
440
+ align_style = "display:flex; justify-content:center; align-items:center;"
441
+ elif specified_align == "left":
442
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
443
+ else:
444
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
445
+ outer = f'<div style="{align_style}">{plot_wrapped}</div>'
446
+ elem = div(raw_util(outer))
447
+
448
+ try:
449
+ if specified_height is not None:
450
+ fig.update_layout(height=orig_height)
451
+ if specified_width_px is not None:
452
+ fig.update_layout(width=orig_width)
453
+ except Exception:
454
+ pass
238
455
  except Exception as e:
239
456
  elem = div(f"Plotly figure could not be rendered: {e}")
240
457
  else:
458
+ # Robust Matplotlib -> PNG path. Ensure `buf` exists and is closed.
459
+ buf = io.BytesIO()
241
460
  try:
242
- buf = io.BytesIO()
243
- # Ensure we use ASCII hyphen-minus for negative ticks when saving
461
+ # If pixel width/height specified, attempt to adjust figure size
462
+ orig_size = None
463
+ try:
464
+ dpi = fig.get_dpi()
465
+ except Exception:
466
+ dpi = None
467
+ try:
468
+ if dpi is not None and (specified_height is not None or specified_width_px is not None):
469
+ orig_size = fig.get_size_inches()
470
+ new_w = orig_size[0]
471
+ new_h = orig_size[1]
472
+ if specified_width_px is not None:
473
+ new_w = specified_width_px / dpi
474
+ if specified_height is not None:
475
+ new_h = specified_height / dpi
476
+ fig.set_size_inches(new_w, new_h)
477
+ except Exception:
478
+ orig_size = None
479
+
244
480
  with rc_context({"axes.unicode_minus": False}):
245
481
  fig.savefig(buf, format="png", bbox_inches="tight")
482
+
483
+ # restore original size if changed
484
+ try:
485
+ if orig_size is not None:
486
+ fig.set_size_inches(orig_size)
487
+ except Exception:
488
+ pass
489
+
246
490
  buf.seek(0)
247
491
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
248
- buf.close()
492
+ img_style = "max-width:100%;"
493
+ if specified_height is not None:
494
+ img_style = img_style + f" height:{specified_height}px;"
495
+ if specified_width_px is not None:
496
+ img_style = img_style + f" width:{specified_width_px}px;"
497
+ if specified_align not in ("left", "right", "center"):
498
+ specified_align = "center"
499
+ if specified_align == "center":
500
+ align_style = "display:flex; justify-content:center; align-items:center;"
501
+ elif specified_align == "left":
502
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
503
+ else:
504
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
249
505
  elem = div(
250
- raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
251
- style="display: flex; justify-content: center; align-items: center;"
506
+ raw_util(f'<img src="data:image/png;base64,{img_base64}" style="{img_style}">'),
507
+ style=align_style
252
508
  )
253
509
  except Exception as e:
254
510
  elem = div(f"Matplotlib figure could not be rendered: {e}")
511
+ finally:
512
+ try:
513
+ buf.close()
514
+ except Exception:
515
+ pass
255
516
  elif kind == "table":
256
517
  df = content
257
518
  html_table = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=f"table-{index}", escape=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2026.2
3
+ Version: 2026.4
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
@@ -11,9 +11,11 @@ Description-Content-Type: text/markdown
11
11
  Requires-Dist: plotly
12
12
  Requires-Dist: pandas
13
13
  Requires-Dist: dominate
14
- Requires-Dist: reportlab
15
- Requires-Dist: kaleido
16
14
  Requires-Dist: matplotlib
15
+ Provides-Extra: images
16
+ Requires-Dist: kaleido; extra == "images"
17
+ Provides-Extra: all
18
+ Requires-Dist: kaleido; extra == "all"
17
19
 
18
20
  # staticdash
19
21
 
@@ -1,6 +1,10 @@
1
1
  plotly
2
2
  pandas
3
3
  dominate
4
- reportlab
5
- kaleido
6
4
  matplotlib
5
+
6
+ [all]
7
+ kaleido
8
+
9
+ [images]
10
+ kaleido
File without changes
File without changes
File without changes