staticdash 2026.3__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.3 → staticdash-2026.4}/PKG-INFO +1 -1
  2. {staticdash-2026.3 → staticdash-2026.4}/pyproject.toml +1 -1
  3. {staticdash-2026.3 → staticdash-2026.4}/staticdash/dashboard.py +243 -18
  4. {staticdash-2026.3 → staticdash-2026.4}/staticdash.egg-info/PKG-INFO +1 -1
  5. {staticdash-2026.3 → staticdash-2026.4}/README.md +0 -0
  6. {staticdash-2026.3 → staticdash-2026.4}/setup.cfg +0 -0
  7. {staticdash-2026.3 → staticdash-2026.4}/setup.py +0 -0
  8. {staticdash-2026.3 → staticdash-2026.4}/staticdash/__init__.py +0 -0
  9. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/css/style.css +0 -0
  10. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/js/script.js +0 -0
  11. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/mathjax/tex-mml-chtml.js +0 -0
  12. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/plotly/plotly.min.js +0 -0
  13. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-bash.min.js +0 -0
  14. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-c.min.js +0 -0
  15. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-javascript.min.js +0 -0
  16. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-json.min.js +0 -0
  17. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-markup.min.js +0 -0
  18. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-python.min.js +0 -0
  19. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/components/prism-sql.min.js +0 -0
  20. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/prism-tomorrow.min.css +0 -0
  21. {staticdash-2026.3 → staticdash-2026.4}/staticdash/assets/vendor/prism/prism.min.js +0 -0
  22. {staticdash-2026.3 → staticdash-2026.4}/staticdash.egg-info/SOURCES.txt +0 -0
  23. {staticdash-2026.3 → staticdash-2026.4}/staticdash.egg-info/dependency_links.txt +0 -0
  24. {staticdash-2026.3 → staticdash-2026.4}/staticdash.egg-info/requires.txt +0 -0
  25. {staticdash-2026.3 → 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.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "staticdash"
7
- version = "2026.3"
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" }
@@ -54,8 +54,13 @@ class AbstractPage:
54
54
  def add_text(self, text, width=None):
55
55
  self.elements.append(("text", text, width))
56
56
 
57
- def add_plot(self, plot, width=None):
58
- 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))
59
64
 
60
65
  def add_table(self, df, table_id=None, sortable=True, width=None):
61
66
  self.elements.append(("table", df, width))
@@ -151,7 +156,23 @@ class Page(AbstractPage):
151
156
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
152
157
  elem = header_tag(text)
153
158
  elif kind == "plot":
154
- 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
+
155
176
  if hasattr(fig, "to_html"):
156
177
  # Use local Plotly loaded in <head>
157
178
  # Ensure the figure uses a robust font family so minus signs and other
@@ -165,26 +186,128 @@ class Page(AbstractPage):
165
186
  except Exception:
166
187
  # Be defensive: don't fail rendering if layout manipulation isn't available
167
188
  pass
168
- # Render Plotly HTML and replace Unicode minus (U+2212) with ASCII hyphen-minus
169
- # to avoid rendering issues when a user's font lacks the U+2212 glyph.
189
+
170
190
  try:
171
- plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
172
- 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
173
248
  except Exception as e:
174
249
  elem = div(f"Plotly figure could not be rendered: {e}")
175
250
  else:
251
+ # Robust Matplotlib -> PNG path. Ensure `buf` exists and is closed.
252
+ buf = io.BytesIO()
176
253
  try:
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
+
177
273
  with rc_context({"axes.unicode_minus": False}):
178
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
+
179
283
  buf.seek(0)
180
284
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
181
- 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
+
182
300
  elem = div(
183
- raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
184
- 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
185
303
  )
186
304
  except Exception as e:
187
305
  elem = div(f"Matplotlib figure could not be rendered: {e}")
306
+ finally:
307
+ try:
308
+ buf.close()
309
+ except Exception:
310
+ pass
188
311
  elif kind == "table":
189
312
  df = content
190
313
  try:
@@ -255,7 +378,19 @@ class MiniPage(AbstractPage):
255
378
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
256
379
  elem = header_tag(text)
257
380
  elif kind == "plot":
258
- 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
+
259
394
  if hasattr(fig, "to_html"):
260
395
  # Use local Plotly loaded in <head>
261
396
  # Ensure the figure uses a robust font family so minus signs and other
@@ -269,25 +404,115 @@ class MiniPage(AbstractPage):
269
404
  except Exception:
270
405
  pass
271
406
  try:
272
- plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
273
- 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
274
455
  except Exception as e:
275
456
  elem = div(f"Plotly figure could not be rendered: {e}")
276
457
  else:
458
+ # Robust Matplotlib -> PNG path. Ensure `buf` exists and is closed.
459
+ buf = io.BytesIO()
277
460
  try:
278
- buf = io.BytesIO()
279
- # Matplotlib may not be installed in minimal installs; import lazily
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
+
280
480
  with rc_context({"axes.unicode_minus": False}):
281
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
+
282
490
  buf.seek(0)
283
491
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
284
- 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;"
285
505
  elem = div(
286
- raw_util(f'<img src="data:image/png;base64,{img_base64}" style="max-width:100%;">'),
287
- 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
288
508
  )
289
509
  except Exception as e:
290
510
  elem = div(f"Matplotlib figure could not be rendered: {e}")
511
+ finally:
512
+ try:
513
+ buf.close()
514
+ except Exception:
515
+ pass
291
516
  elif kind == "table":
292
517
  df = content
293
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.3
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
File without changes
File without changes
File without changes