staticdash 2026.3__tar.gz → 2026.5__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.5}/PKG-INFO +1 -1
  2. {staticdash-2026.3 → staticdash-2026.5}/pyproject.toml +1 -1
  3. {staticdash-2026.3 → staticdash-2026.5}/staticdash/dashboard.py +244 -16
  4. {staticdash-2026.3 → staticdash-2026.5}/staticdash.egg-info/PKG-INFO +1 -1
  5. {staticdash-2026.3 → staticdash-2026.5}/README.md +0 -0
  6. {staticdash-2026.3 → staticdash-2026.5}/setup.cfg +0 -0
  7. {staticdash-2026.3 → staticdash-2026.5}/setup.py +0 -0
  8. {staticdash-2026.3 → staticdash-2026.5}/staticdash/__init__.py +0 -0
  9. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/css/style.css +0 -0
  10. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/js/script.js +0 -0
  11. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/mathjax/tex-mml-chtml.js +0 -0
  12. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/plotly/plotly.min.js +0 -0
  13. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-bash.min.js +0 -0
  14. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-c.min.js +0 -0
  15. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-javascript.min.js +0 -0
  16. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-json.min.js +0 -0
  17. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-markup.min.js +0 -0
  18. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-python.min.js +0 -0
  19. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/components/prism-sql.min.js +0 -0
  20. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/prism-tomorrow.min.css +0 -0
  21. {staticdash-2026.3 → staticdash-2026.5}/staticdash/assets/vendor/prism/prism.min.js +0 -0
  22. {staticdash-2026.3 → staticdash-2026.5}/staticdash.egg-info/SOURCES.txt +0 -0
  23. {staticdash-2026.3 → staticdash-2026.5}/staticdash.egg-info/dependency_links.txt +0 -0
  24. {staticdash-2026.3 → staticdash-2026.5}/staticdash.egg-info/requires.txt +0 -0
  25. {staticdash-2026.3 → staticdash-2026.5}/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.5
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.5"
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,15 @@ 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, el_width=None, height=None, width=None, align="center"):
58
+ # `el_width` is the fractional width (0..1) used for layout columns.
59
+ # `height` and `width` are pixel dimensions for the rendered figure/image.
60
+ # `align` controls horizontal alignment: 'left', 'center' (default), or 'right'.
61
+ # We store a tuple (plot, height, width, align) and keep `el_width` as
62
+ # the element-level fractional width for compatibility with page layout.
63
+ specified_height = height
64
+ specified_width = width
65
+ self.elements.append(("plot", (plot, specified_height, specified_width, align), el_width))
59
66
 
60
67
  def add_table(self, df, table_id=None, sortable=True, width=None):
61
68
  self.elements.append(("table", df, width))
@@ -151,7 +158,23 @@ class Page(AbstractPage):
151
158
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
152
159
  elem = header_tag(text)
153
160
  elif kind == "plot":
154
- fig = content
161
+ # content may be stored as (figure, height), (figure, height, width)
162
+ # or (figure, height, width, align)
163
+ specified_height = None
164
+ specified_width = None
165
+ specified_align = "center"
166
+ if isinstance(content, (list, tuple)):
167
+ if len(content) == 4:
168
+ fig, specified_height, specified_width, specified_align = content
169
+ elif len(content) == 3:
170
+ fig, specified_height, specified_width = content
171
+ elif len(content) == 2:
172
+ fig, specified_height = content
173
+ else:
174
+ fig = content
175
+ else:
176
+ fig = content
177
+
155
178
  if hasattr(fig, "to_html"):
156
179
  # Use local Plotly loaded in <head>
157
180
  # Ensure the figure uses a robust font family so minus signs and other
@@ -165,26 +188,127 @@ class Page(AbstractPage):
165
188
  except Exception:
166
189
  # Be defensive: don't fail rendering if layout manipulation isn't available
167
190
  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.
191
+
170
192
  try:
193
+ # Temporarily set layout height/width if specified (pixels)
194
+ orig_height = None
195
+ orig_width = None
196
+ try:
197
+ orig_height = getattr(fig.layout, 'height', None)
198
+ except Exception:
199
+ orig_height = None
200
+ try:
201
+ orig_width = getattr(fig.layout, 'width', None)
202
+ except Exception:
203
+ orig_width = None
204
+
205
+ try:
206
+ if specified_height is not None:
207
+ fig.update_layout(height=specified_height)
208
+ if specified_width is not None:
209
+ fig.update_layout(width=specified_width)
210
+ except Exception:
211
+ pass
212
+
213
+ # If a local vendored Plotly exists, rely on the head script.
214
+ # Otherwise include Plotly from CDN so the inline newPlot call works.
215
+ # Always rely on the page-level Plotly include (head). Avoid
216
+ # embedding another Plotly bundle inside each fragment which
217
+ # can lead to multiple conflicting versions in the same page.
171
218
  plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
172
- elem = div(raw_util(plotly_html))
219
+
220
+ # Wrap the Plotly HTML in a container with explicit pixel sizing
221
+ container_style = "width:100%;"
222
+ if specified_width is not None:
223
+ container_style = f"width:{specified_width}px;"
224
+ if specified_height is not None:
225
+ container_style = container_style + f" height:{specified_height}px;"
226
+
227
+ plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
228
+ # Apply alignment wrapper
229
+ if specified_align not in ("left", "right", "center"):
230
+ specified_align = "center"
231
+ if specified_align == "center":
232
+ align_style = "display:flex; justify-content:center; align-items:center;"
233
+ elif specified_align == "left":
234
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
235
+ else:
236
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
237
+
238
+ outer = f'<div style="{align_style}">{plot_wrapped}</div>'
239
+ elem = div(raw_util(outer))
240
+
241
+ # restore original height/width if we changed them
242
+ try:
243
+ if specified_height is not None:
244
+ fig.update_layout(height=orig_height)
245
+ if specified_width is not None:
246
+ fig.update_layout(width=orig_width)
247
+ except Exception:
248
+ pass
173
249
  except Exception as e:
174
250
  elem = div(f"Plotly figure could not be rendered: {e}")
175
251
  else:
252
+ # Robust Matplotlib -> PNG path. Ensure `buf` exists and is closed.
253
+ buf = io.BytesIO()
176
254
  try:
255
+ # If pixel width/height specified, attempt to adjust figure size
256
+ orig_size = None
257
+ try:
258
+ dpi = fig.get_dpi()
259
+ except Exception:
260
+ dpi = None
261
+ try:
262
+ if dpi is not None and (specified_height is not None or specified_width is not None):
263
+ orig_size = fig.get_size_inches()
264
+ new_w = orig_size[0]
265
+ new_h = orig_size[1]
266
+ if specified_width is not None:
267
+ new_w = specified_width / dpi
268
+ if specified_height is not None:
269
+ new_h = specified_height / dpi
270
+ fig.set_size_inches(new_w, new_h)
271
+ except Exception:
272
+ orig_size = None
273
+
177
274
  with rc_context({"axes.unicode_minus": False}):
178
275
  fig.savefig(buf, format="png", bbox_inches="tight")
276
+
277
+ # restore original size if changed
278
+ try:
279
+ if orig_size is not None:
280
+ fig.set_size_inches(orig_size)
281
+ except Exception:
282
+ pass
283
+
179
284
  buf.seek(0)
180
285
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
181
- buf.close()
286
+ img_style = "max-width:100%;"
287
+ if specified_height is not None:
288
+ img_style = img_style + f" height:{specified_height}px;"
289
+ if specified_width is not None:
290
+ img_style = img_style + f" width:{specified_width}px;"
291
+
292
+ if specified_align not in ("left", "right", "center"):
293
+ specified_align = "center"
294
+ if specified_align == "center":
295
+ align_style = "display:flex; justify-content:center; align-items:center;"
296
+ elif specified_align == "left":
297
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
298
+ else:
299
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
300
+
182
301
  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;"
302
+ raw_util(f'<img src="data:image/png;base64,{img_base64}" style="{img_style}">'),
303
+ style=align_style
185
304
  )
186
305
  except Exception as e:
187
306
  elem = div(f"Matplotlib figure could not be rendered: {e}")
307
+ finally:
308
+ try:
309
+ buf.close()
310
+ except Exception:
311
+ pass
188
312
  elif kind == "table":
189
313
  df = content
190
314
  try:
@@ -255,7 +379,22 @@ class MiniPage(AbstractPage):
255
379
  header_tag = {1: h1, 2: h2, 3: h3, 4: h4}[level]
256
380
  elem = header_tag(text)
257
381
  elif kind == "plot":
258
- fig = content
382
+ # content may be stored as (figure, height, width, align)
383
+ specified_height = None
384
+ specified_width = None
385
+ specified_align = "center"
386
+ if isinstance(content, (list, tuple)):
387
+ if len(content) == 4:
388
+ fig, specified_height, specified_width, specified_align = content
389
+ elif len(content) == 3:
390
+ fig, specified_height, specified_width = content
391
+ elif len(content) == 2:
392
+ fig, specified_height = content
393
+ else:
394
+ fig = content
395
+ else:
396
+ fig = content
397
+
259
398
  if hasattr(fig, "to_html"):
260
399
  # Use local Plotly loaded in <head>
261
400
  # Ensure the figure uses a robust font family so minus signs and other
@@ -269,25 +408,114 @@ class MiniPage(AbstractPage):
269
408
  except Exception:
270
409
  pass
271
410
  try:
411
+ orig_height = None
412
+ orig_width = None
413
+ try:
414
+ orig_height = getattr(fig.layout, 'height', None)
415
+ except Exception:
416
+ orig_height = None
417
+ try:
418
+ orig_width = getattr(fig.layout, 'width', None)
419
+ except Exception:
420
+ orig_width = None
421
+
422
+ try:
423
+ if specified_height is not None:
424
+ fig.update_layout(height=specified_height)
425
+ if specified_width is not None:
426
+ fig.update_layout(width=specified_width)
427
+ except Exception:
428
+ pass
429
+
430
+ # Always rely on the page-level Plotly include (head). Avoid
431
+ # embedding another Plotly bundle inside each fragment which
432
+ # can lead to multiple conflicting versions in the same page.
272
433
  plotly_html = fig.to_html(full_html=False, include_plotlyjs=False, config={'responsive': True})
273
- elem = div(raw_util(plotly_html))
434
+ container_style = "width:100%;"
435
+ if specified_width is not None:
436
+ container_style = f"width:{specified_width}px;"
437
+ if specified_height is not None:
438
+ container_style = container_style + f" height:{specified_height}px;"
439
+ plot_wrapped = f'<div style="{container_style}">{plotly_html}</div>'
440
+ if specified_align not in ("left", "right", "center"):
441
+ specified_align = "center"
442
+ if specified_align == "center":
443
+ align_style = "display:flex; justify-content:center; align-items:center;"
444
+ elif specified_align == "left":
445
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
446
+ else:
447
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
448
+ outer = f'<div style="{align_style}">{plot_wrapped}</div>'
449
+ elem = div(raw_util(outer))
450
+
451
+ try:
452
+ if specified_height is not None:
453
+ fig.update_layout(height=orig_height)
454
+ if specified_width is not None:
455
+ fig.update_layout(width=orig_width)
456
+ except Exception:
457
+ pass
274
458
  except Exception as e:
275
459
  elem = div(f"Plotly figure could not be rendered: {e}")
276
460
  else:
461
+ # Robust Matplotlib -> PNG path. Ensure `buf` exists and is closed.
462
+ buf = io.BytesIO()
277
463
  try:
278
- buf = io.BytesIO()
279
- # Matplotlib may not be installed in minimal installs; import lazily
464
+ # If pixel width/height specified, attempt to adjust figure size
465
+ orig_size = None
466
+ try:
467
+ dpi = fig.get_dpi()
468
+ except Exception:
469
+ dpi = None
470
+ try:
471
+ if dpi is not None and (specified_height is not None or specified_width is not None):
472
+ orig_size = fig.get_size_inches()
473
+ new_w = orig_size[0]
474
+ new_h = orig_size[1]
475
+ if specified_width is not None:
476
+ new_w = specified_width / dpi
477
+ if specified_height is not None:
478
+ new_h = specified_height / dpi
479
+ fig.set_size_inches(new_w, new_h)
480
+ except Exception:
481
+ orig_size = None
482
+
280
483
  with rc_context({"axes.unicode_minus": False}):
281
484
  fig.savefig(buf, format="png", bbox_inches="tight")
485
+
486
+ # restore original size if changed
487
+ try:
488
+ if orig_size is not None:
489
+ fig.set_size_inches(orig_size)
490
+ except Exception:
491
+ pass
492
+
282
493
  buf.seek(0)
283
494
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
284
- buf.close()
495
+ img_style = "max-width:100%;"
496
+ if specified_height is not None:
497
+ img_style = img_style + f" height:{specified_height}px;"
498
+ if specified_width is not None:
499
+ img_style = img_style + f" width:{specified_width}px;"
500
+ if specified_align not in ("left", "right", "center"):
501
+ specified_align = "center"
502
+ if specified_align == "center":
503
+ align_style = "display:flex; justify-content:center; align-items:center;"
504
+ elif specified_align == "left":
505
+ align_style = "display:flex; justify-content:flex-start; align-items:center;"
506
+ else:
507
+ align_style = "display:flex; justify-content:flex-end; align-items:center;"
285
508
  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;"
509
+ raw_util(f'<img src="data:image/png;base64,{img_base64}" style="{img_style}">'),
510
+ style=align_style
288
511
  )
289
512
  except Exception as e:
290
513
  elem = div(f"Matplotlib figure could not be rendered: {e}")
514
+ finally:
515
+ try:
516
+ buf.close()
517
+ except Exception:
518
+ pass
291
519
  elif kind == "table":
292
520
  df = content
293
521
  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.5
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