staticdash 2026.4__tar.gz → 2026.6__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.4 → staticdash-2026.6}/PKG-INFO +1 -1
  2. {staticdash-2026.4 → staticdash-2026.6}/pyproject.toml +1 -1
  3. {staticdash-2026.4 → staticdash-2026.6}/staticdash/dashboard.py +110 -58
  4. {staticdash-2026.4 → staticdash-2026.6}/staticdash.egg-info/PKG-INFO +1 -1
  5. {staticdash-2026.4 → staticdash-2026.6}/README.md +0 -0
  6. {staticdash-2026.4 → staticdash-2026.6}/setup.cfg +0 -0
  7. {staticdash-2026.4 → staticdash-2026.6}/setup.py +0 -0
  8. {staticdash-2026.4 → staticdash-2026.6}/staticdash/__init__.py +0 -0
  9. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/css/style.css +0 -0
  10. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/js/script.js +0 -0
  11. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/mathjax/tex-mml-chtml.js +0 -0
  12. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/plotly/plotly.min.js +0 -0
  13. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-bash.min.js +0 -0
  14. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-c.min.js +0 -0
  15. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-javascript.min.js +0 -0
  16. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-json.min.js +0 -0
  17. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-markup.min.js +0 -0
  18. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-python.min.js +0 -0
  19. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/components/prism-sql.min.js +0 -0
  20. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/prism-tomorrow.min.css +0 -0
  21. {staticdash-2026.4 → staticdash-2026.6}/staticdash/assets/vendor/prism/prism.min.js +0 -0
  22. {staticdash-2026.4 → staticdash-2026.6}/staticdash.egg-info/SOURCES.txt +0 -0
  23. {staticdash-2026.4 → staticdash-2026.6}/staticdash.egg-info/dependency_links.txt +0 -0
  24. {staticdash-2026.4 → staticdash-2026.6}/staticdash.egg-info/requires.txt +0 -0
  25. {staticdash-2026.4 → staticdash-2026.6}/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.4
3
+ Version: 2026.6
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.4"
7
+ version = "2026.6"
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" }
@@ -12,6 +12,7 @@ import io
12
12
  import base64
13
13
  import matplotlib
14
14
  from matplotlib import rc_context
15
+ import json
15
16
 
16
17
  def split_paragraphs_preserving_math(text):
17
18
  """
@@ -54,13 +55,15 @@ class AbstractPage:
54
55
  def add_text(self, text, width=None):
55
56
  self.elements.append(("text", text, width))
56
57
 
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))
58
+ def add_plot(self, plot, el_width=None, height=None, width=None, align="center"):
59
+ # `el_width` is the fractional width (0..1) used for layout columns.
60
+ # `height` and `width` are pixel dimensions for the rendered figure/image.
61
+ # `align` controls horizontal alignment: 'left', 'center' (default), or 'right'.
62
+ # We store a tuple (plot, height, width, align) and keep `el_width` as
63
+ # the element-level fractional width for compatibility with page layout.
64
+ specified_height = height
65
+ specified_width = width
66
+ self.elements.append(("plot", (plot, specified_height, specified_width, align), el_width))
64
67
 
65
68
  def add_table(self, df, table_id=None, sortable=True, width=None):
66
69
  self.elements.append(("table", df, width))
@@ -156,16 +159,16 @@ class Page(AbstractPage):
156
159
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
157
160
  elem = header_tag(text)
158
161
  elif kind == "plot":
159
- # content may be stored as (figure, height), (figure, height, width_px)
160
- # or (figure, height, width_px, align)
162
+ # content may be stored as (figure, height), (figure, height, width)
163
+ # or (figure, height, width, align)
161
164
  specified_height = None
162
- specified_width_px = None
165
+ specified_width = None
163
166
  specified_align = "center"
164
167
  if isinstance(content, (list, tuple)):
165
168
  if len(content) == 4:
166
- fig, specified_height, specified_width_px, specified_align = content
169
+ fig, specified_height, specified_width, specified_align = content
167
170
  elif len(content) == 3:
168
- fig, specified_height, specified_width_px = content
171
+ fig, specified_height, specified_width = content
169
172
  elif len(content) == 2:
170
173
  fig, specified_height = content
171
174
  else:
@@ -203,27 +206,48 @@ class Page(AbstractPage):
203
206
  try:
204
207
  if specified_height is not None:
205
208
  fig.update_layout(height=specified_height)
206
- if specified_width_px is not None:
207
- fig.update_layout(width=specified_width_px)
209
+ if specified_width is not None:
210
+ fig.update_layout(width=specified_width)
208
211
  except Exception:
209
212
  pass
210
213
 
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;"
214
+ # Defer Plotly.newPlot until the Plotly bundle is available
215
+ # by embedding the figure JSON and calling newPlot from a
216
+ # small loader that polls for `window.Plotly`.
217
+ try:
218
+ fig_json = fig.to_json()
219
+ except Exception:
220
+ # Fallback: use the html fragment (older path)
221
+ plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
222
+ container_style = "width:100%;"
223
+ if specified_width is not None:
224
+ container_style = f"width:{specified_width}px;"
225
+ if specified_height is not None:
226
+ container_style = container_style + f" height:{specified_height}px;"
227
+ plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
228
+ else:
229
+ fig_json = fig_json.replace('</script>', '<\\/script>')
230
+ div_id = f'plot-{uuid.uuid4()}'
231
+ container_style = "width:100%;"
232
+ if specified_width is not None:
233
+ container_style = f"width:{specified_width}px;"
234
+ if specified_height is not None:
235
+ container_style = container_style + f" height:{specified_height}px;"
236
+
237
+ plot_div = f'<div id="{div_id}" class="plotly-graph-div" style="{container_style}"></div>'
238
+ loader = (
239
+ '<script type="text/javascript">(function(){' \
240
+ f'var fig = {fig_json};' \
241
+ 'function tryPlot(){' \
242
+ 'if(window.Plotly && typeof window.Plotly.newPlot === "function"){' \
243
+ f'Plotly.newPlot("{div_id}", fig.data, fig.layout, {json.dumps({"responsive": True})});' \
244
+ '} else { setTimeout(tryPlot, 50); }' \
245
+ '}' \
246
+ 'if(document.readyState === "complete"){ tryPlot(); } else { window.addEventListener("load", tryPlot); }' \
247
+ '})();</script>'
248
+ )
249
+ plot_wrapped = plot_div + loader
225
250
 
226
- plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
227
251
  # Apply alignment wrapper
228
252
  if specified_align not in ("left", "right", "center"):
229
253
  specified_align = "center"
@@ -241,7 +265,7 @@ class Page(AbstractPage):
241
265
  try:
242
266
  if specified_height is not None:
243
267
  fig.update_layout(height=orig_height)
244
- if specified_width_px is not None:
268
+ if specified_width is not None:
245
269
  fig.update_layout(width=orig_width)
246
270
  except Exception:
247
271
  pass
@@ -258,12 +282,12 @@ class Page(AbstractPage):
258
282
  except Exception:
259
283
  dpi = None
260
284
  try:
261
- if dpi is not None and (specified_height is not None or specified_width_px is not None):
285
+ if dpi is not None and (specified_height is not None or specified_width is not None):
262
286
  orig_size = fig.get_size_inches()
263
287
  new_w = orig_size[0]
264
288
  new_h = orig_size[1]
265
- if specified_width_px is not None:
266
- new_w = specified_width_px / dpi
289
+ if specified_width is not None:
290
+ new_w = specified_width / dpi
267
291
  if specified_height is not None:
268
292
  new_h = specified_height / dpi
269
293
  fig.set_size_inches(new_w, new_h)
@@ -285,8 +309,8 @@ class Page(AbstractPage):
285
309
  img_style = "max-width:100%;"
286
310
  if specified_height is not None:
287
311
  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;"
312
+ if specified_width is not None:
313
+ img_style = img_style + f" width:{specified_width}px;"
290
314
 
291
315
  if specified_align not in ("left", "right", "center"):
292
316
  specified_align = "center"
@@ -378,12 +402,15 @@ class MiniPage(AbstractPage):
378
402
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
379
403
  elem = header_tag(text)
380
404
  elif kind == "plot":
381
- # content may be stored as (figure, height) or (figure, height, width_px)
405
+ # content may be stored as (figure, height, width, align)
382
406
  specified_height = None
383
- specified_width_px = None
407
+ specified_width = None
408
+ specified_align = "center"
384
409
  if isinstance(content, (list, tuple)):
385
- if len(content) == 3:
386
- fig, specified_height, specified_width_px = content
410
+ if len(content) == 4:
411
+ fig, specified_height, specified_width, specified_align = content
412
+ elif len(content) == 3:
413
+ fig, specified_height, specified_width = content
387
414
  elif len(content) == 2:
388
415
  fig, specified_height = content
389
416
  else:
@@ -418,22 +445,47 @@ class MiniPage(AbstractPage):
418
445
  try:
419
446
  if specified_height is not None:
420
447
  fig.update_layout(height=specified_height)
421
- if specified_width_px is not None:
422
- fig.update_layout(width=specified_width_px)
448
+ if specified_width is not None:
449
+ fig.update_layout(width=specified_width)
423
450
  except Exception:
424
451
  pass
425
452
 
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>'
453
+ # Defer Plotly.newPlot until the Plotly bundle is available
454
+ # by embedding the figure JSON and calling newPlot from a
455
+ # small loader that polls for `window.Plotly`.
456
+ try:
457
+ fig_json = fig.to_json()
458
+ except Exception:
459
+ # Fallback: use the html fragment (older path)
460
+ plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
461
+ container_style = "width:100%;"
462
+ if specified_width is not None:
463
+ container_style = f"width:{specified_width}px;"
464
+ if specified_height is not None:
465
+ container_style = container_style + f" height:{specified_height}px;"
466
+ plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
467
+ else:
468
+ fig_json = fig_json.replace('</script>', '<\\/script>')
469
+ div_id = f'plot-{uuid.uuid4()}'
470
+ container_style = "width:100%;"
471
+ if specified_width is not None:
472
+ container_style = f"width:{specified_width}px;"
473
+ if specified_height is not None:
474
+ container_style = container_style + f" height:{specified_height}px;"
475
+
476
+ plot_div = f'<div id="{div_id}" class="plotly-graph-div" style="{container_style}"></div>'
477
+ loader = (
478
+ '<script type="text/javascript">(function(){' \
479
+ f'var fig = {fig_json};' \
480
+ 'function tryPlot(){' \
481
+ 'if(window.Plotly && typeof window.Plotly.newPlot === "function"){' \
482
+ f'Plotly.newPlot("{div_id}", fig.data, fig.layout, {json.dumps({"responsive": True})});' \
483
+ '} else { setTimeout(tryPlot, 50); }' \
484
+ '}' \
485
+ 'if(document.readyState === "complete"){ tryPlot(); } else { window.addEventListener("load", tryPlot); }' \
486
+ '})();</script>'
487
+ )
488
+ plot_wrapped = plot_div + loader
437
489
  if specified_align not in ("left", "right", "center"):
438
490
  specified_align = "center"
439
491
  if specified_align == "center":
@@ -448,7 +500,7 @@ class MiniPage(AbstractPage):
448
500
  try:
449
501
  if specified_height is not None:
450
502
  fig.update_layout(height=orig_height)
451
- if specified_width_px is not None:
503
+ if specified_width is not None:
452
504
  fig.update_layout(width=orig_width)
453
505
  except Exception:
454
506
  pass
@@ -465,12 +517,12 @@ class MiniPage(AbstractPage):
465
517
  except Exception:
466
518
  dpi = None
467
519
  try:
468
- if dpi is not None and (specified_height is not None or specified_width_px is not None):
520
+ if dpi is not None and (specified_height is not None or specified_width is not None):
469
521
  orig_size = fig.get_size_inches()
470
522
  new_w = orig_size[0]
471
523
  new_h = orig_size[1]
472
- if specified_width_px is not None:
473
- new_w = specified_width_px / dpi
524
+ if specified_width is not None:
525
+ new_w = specified_width / dpi
474
526
  if specified_height is not None:
475
527
  new_h = specified_height / dpi
476
528
  fig.set_size_inches(new_w, new_h)
@@ -492,8 +544,8 @@ class MiniPage(AbstractPage):
492
544
  img_style = "max-width:100%;"
493
545
  if specified_height is not None:
494
546
  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;"
547
+ if specified_width is not None:
548
+ img_style = img_style + f" width:{specified_width}px;"
497
549
  if specified_align not in ("left", "right", "center"):
498
550
  specified_align = "center"
499
551
  if specified_align == "center":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 2026.4
3
+ Version: 2026.6
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