staticdash 0.3.1__tar.gz → 0.3.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 0.3.1
3
+ Version: 0.3.2
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 = "0.3.1"
7
+ version = "0.3.2"
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" }
@@ -104,12 +104,14 @@ body {
104
104
  height: auto !important;
105
105
  }
106
106
 
107
+ /* Tables */
107
108
  table {
108
109
  width: 100%;
109
110
  border-collapse: collapse;
110
111
  margin-top: 1em;
111
112
  background-color: white;
112
113
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
114
+ overflow-x: auto;
113
115
  }
114
116
 
115
117
  thead {
@@ -153,6 +155,7 @@ tbody tr:hover {
153
155
  background-color: #547a9f;
154
156
  }
155
157
 
158
+ /* Sortable Tables */
156
159
  table.sortable th {
157
160
  cursor: pointer;
158
161
  position: relative;
@@ -176,13 +179,89 @@ table.sortable th.sorted-desc::after {
176
179
  content: "\25BC";
177
180
  }
178
181
 
182
+ /* MiniPage Layout */
179
183
  .minipage-row {
180
184
  display: flex;
185
+ width: 100%;
181
186
  gap: 20px;
182
187
  margin-bottom: 20px;
188
+ box-sizing: border-box;
183
189
  }
184
190
 
185
- .minipage {
191
+ .minipage-cell {
192
+ flex: 1 1 0;
193
+ min-width: 0;
186
194
  box-sizing: border-box;
187
195
  padding: 10px;
196
+ overflow-x: auto;
197
+ }
198
+
199
+ .minipage-cell pre {
200
+ flex: 0 1 auto;
201
+ max-height: 400px;
202
+ overflow: auto;
203
+ }
204
+
205
+ .minipage-cell h4 {
206
+ margin-top: 0;
207
+ }
208
+
209
+ .minipage-cell .syntax-block {
210
+ margin-top: 0.5em;
211
+ margin-bottom: 0.5em;
212
+ }
213
+
214
+ .minipage-cell table {
215
+ display: block;
216
+ overflow-x: auto;
217
+ width: 100%;
218
+ }
219
+
220
+ /* Syntax Blocks */
221
+ .syntax-block {
222
+ position: relative;
223
+ margin: 1em 0;
224
+ background: #23272e;
225
+ border-radius: 6px;
226
+ overflow: auto;
227
+ color: #f8f8f2;
228
+ }
229
+
230
+ .syntax-block pre {
231
+ margin: 0;
232
+ padding: 1em;
233
+ background: none;
234
+ color: inherit;
235
+ font-family: 'Fira Mono', 'Consolas', monospace;
236
+ font-size: 0.95em;
237
+ }
238
+
239
+ .code-toolbar {
240
+ position: absolute;
241
+ top: 6px;
242
+ right: 12px;
243
+ z-index: 2;
244
+ }
245
+
246
+ .copy-btn,
247
+ .view-raw-btn {
248
+ color: #fff;
249
+ background: #444;
250
+ border-radius: 4px;
251
+ padding: 2px 8px;
252
+ font-size: 0.85em;
253
+ text-decoration: none;
254
+ margin-left: 4px;
255
+ cursor: pointer;
256
+ transition: background 0.2s;
257
+ }
258
+
259
+ .copy-btn:hover,
260
+ .view-raw-btn:hover {
261
+ background: #1abc9c;
262
+ }
263
+
264
+ .table-wrapper {
265
+ overflow-x: auto;
266
+ width: 100%;
188
267
  }
@@ -11,9 +11,9 @@ document.addEventListener("DOMContentLoaded", () => {
11
11
  });
12
12
  }
13
13
 
14
- function showPage(id) {
14
+ function showPage(pageId) {
15
15
  sections.forEach(section => section.style.display = "none");
16
- const page = document.getElementById(id);
16
+ const page = document.getElementById(pageId);
17
17
  if (page) {
18
18
  page.style.display = "block";
19
19
 
@@ -24,8 +24,12 @@ document.addEventListener("DOMContentLoaded", () => {
24
24
  }
25
25
 
26
26
  links.forEach(link => link.classList.remove("active"));
27
- const activeLink = Array.from(links).find(link => link.dataset.target === id);
27
+ const activeLink = Array.from(links).find(link => link.dataset.target === pageId);
28
28
  if (activeLink) activeLink.classList.add("active");
29
+
30
+ if (window.Prism && typeof Prism.highlightAll === "function") {
31
+ Prism.highlightAll();
32
+ }
29
33
  }
30
34
 
31
35
  links.forEach(link => {
@@ -91,4 +95,32 @@ document.addEventListener("DOMContentLoaded", () => {
91
95
  });
92
96
  });
93
97
  });
98
+
99
+ // Syntax block copy/view raw
100
+ document.querySelectorAll(".copy-btn").forEach(btn => {
101
+ btn.addEventListener("click", e => {
102
+ e.preventDefault();
103
+ const codeId = btn.dataset.target;
104
+ const code = document.getElementById(codeId);
105
+ if (code) {
106
+ navigator.clipboard.writeText(code.textContent);
107
+ btn.textContent = "Copied!";
108
+ setTimeout(() => { btn.textContent = "Copy"; }, 1200);
109
+ }
110
+ });
111
+ });
112
+ document.querySelectorAll(".view-raw-btn").forEach(btn => {
113
+ btn.addEventListener("click", e => {
114
+ e.preventDefault();
115
+ const codeId = btn.dataset.target;
116
+ const code = document.getElementById(codeId);
117
+ if (code) {
118
+ const win = window.open("", "_blank");
119
+ win.document.write("<pre>" + code.textContent.replace(/[<>&]/g, c => ({
120
+ '<': '&lt;', '>': '&gt;', '&': '&amp;'
121
+ }[c])) + "</pre>");
122
+ win.document.close();
123
+ }
124
+ });
125
+ });
94
126
  });
@@ -0,0 +1,212 @@
1
+ import os
2
+ import shutil
3
+ import uuid
4
+ import pandas as pd
5
+ import plotly.graph_objects as go
6
+ from dominate import document
7
+ from dominate.tags import div, h1, h2, h3, h4, p, a, script, link
8
+ from dominate.util import raw as raw_util
9
+ import html
10
+
11
+ class AbstractPage:
12
+ def __init__(self):
13
+ self.elements = []
14
+
15
+ def add_header(self, text, level=1):
16
+ if level not in (1, 2, 3, 4):
17
+ raise ValueError("Header level must be 1, 2, 3, or 4")
18
+ self.elements.append(("header", (text, level)))
19
+
20
+ def add_text(self, text):
21
+ self.elements.append(("text", text))
22
+
23
+ def add_plot(self, plot):
24
+ html_plot = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
25
+ self.elements.append(("plot", html_plot))
26
+
27
+ def add_table(self, df, table_id=None, sortable=True):
28
+ if table_id is None:
29
+ table_id = f"table-{len(self.elements)}"
30
+ classes = "table-hover table-striped"
31
+ if sortable:
32
+ classes += " sortable"
33
+ html_table = df.to_html(classes=classes, index=False, border=0, table_id=table_id, escape=False)
34
+ self.elements.append(("table", (html_table, table_id)))
35
+
36
+ def add_download(self, file_path, label=None):
37
+ if not os.path.isfile(file_path):
38
+ raise FileNotFoundError(f"File not found: {file_path}")
39
+ self.elements.append(("download", (file_path, label)))
40
+
41
+ def add_minipage(self, minipage):
42
+ self.elements.append(("minipage", minipage))
43
+
44
+ def add_syntax(self, code, language="python"):
45
+ self.elements.append(("syntax", (code, language)))
46
+
47
+ class Page(AbstractPage):
48
+ def __init__(self, slug, title):
49
+ super().__init__()
50
+ self.slug = slug
51
+ self.title = title
52
+ self.add_header(title, level=1)
53
+
54
+ def render(self, index, downloads_dir=None, relative_prefix=""):
55
+ elements = []
56
+ for kind, content in self.elements:
57
+ if kind == "minipage":
58
+ row_div = div(cls="minipage-row")
59
+ row_div += content.render(index, downloads_dir=downloads_dir, relative_prefix=relative_prefix)
60
+ elements.append(row_div)
61
+ elif kind == "header":
62
+ text, level = content
63
+ elements.append({1: h1, 2: h2, 3: h3, 4: h4}[level](text))
64
+ elif kind == "text":
65
+ elements.append(p(content))
66
+ elif kind == "plot":
67
+ elements.append(div(content, cls="plot-container"))
68
+ elif kind == "table":
69
+ table_html, _ = content
70
+ wrapped = div(raw_util(table_html), cls="table-wrapper")
71
+ elements.append(wrapped)
72
+ elif kind == "download":
73
+ file_path, label = content
74
+ file_uuid = f"{uuid.uuid4().hex}_{os.path.basename(file_path)}"
75
+ dst_path = os.path.join(downloads_dir, file_uuid)
76
+ shutil.copy2(file_path, dst_path)
77
+ btn = a(label or os.path.basename(file_path),
78
+ href=f"{relative_prefix}downloads/{file_uuid}",
79
+ cls="download-button",
80
+ download=True)
81
+ elements.append(div(btn))
82
+ elif kind == "syntax":
83
+ code, language = content
84
+ code_id = f"code-{uuid.uuid4().hex[:8]}"
85
+ toolbar = div(cls="code-toolbar")
86
+ toolbar += a("Copy", href="#", cls="copy-btn", **{"data-target": code_id})
87
+ toolbar += a("View Raw", href="#", cls="view-raw-btn", **{"data-target": code_id, "style": "margin-left:10px;"})
88
+ escaped_code = html.escape(code)
89
+ header_text = f"{language.capitalize()} Example"
90
+ block_wrapper = div(
91
+ h4(header_text),
92
+ div(
93
+ toolbar,
94
+ raw_util(f'<pre><code id="{code_id}" class="language-{language}">{escaped_code}</code></pre>'),
95
+ cls="syntax-block"
96
+ )
97
+ )
98
+ elements.append(block_wrapper)
99
+ return elements
100
+
101
+ class MiniPage(AbstractPage):
102
+ def __init__(self):
103
+ super().__init__()
104
+
105
+ def render(self, index=None, downloads_dir=None, relative_prefix=""):
106
+ row_div = div(cls="minipage-row")
107
+ for kind, content in self.elements:
108
+ cell = div(cls="minipage-cell")
109
+ if kind == "header":
110
+ text, level = content
111
+ cell += {1: h1, 2: h2, 3: h3, 4: h4}[level](text)
112
+ elif kind == "text":
113
+ cell += p(content)
114
+ elif kind == "plot":
115
+ cell += div(content, cls="plot-container")
116
+ elif kind == "table":
117
+ table_html, _ = content
118
+ cell += div(raw_util(table_html), cls="table-wrapper")
119
+ elif kind == "download":
120
+ file_path, label = content
121
+ file_uuid = f"{uuid.uuid4().hex}_{os.path.basename(file_path)}"
122
+ dst_path = os.path.join(downloads_dir, file_uuid)
123
+ shutil.copy2(file_path, dst_path)
124
+ btn = a(label or os.path.basename(file_path),
125
+ href=f"{relative_prefix}downloads/{file_uuid}",
126
+ cls="download-button",
127
+ download=True)
128
+ cell += div(btn)
129
+ elif kind == "syntax":
130
+ code, language = content
131
+ code_id = f"code-{uuid.uuid4().hex[:8]}"
132
+ toolbar = div(cls="code-toolbar")
133
+ toolbar += a("Copy", href="#", cls="copy-btn", **{"data-target": code_id})
134
+ toolbar += a("View Raw", href="#", cls="view-raw-btn", **{"data-target": code_id, "style": "margin-left:10px;"})
135
+ escaped_code = html.escape(code)
136
+ header_text = f"{language.capitalize()} Example"
137
+ block_wrapper = div(
138
+ h4(header_text),
139
+ div(
140
+ toolbar,
141
+ raw_util(f'<pre><code id="{code_id}" class="language-{language}">{escaped_code}</code></pre>'),
142
+ cls="syntax-block"
143
+ )
144
+ )
145
+ cell += block_wrapper
146
+ row_div += cell
147
+ return row_div
148
+
149
+ class Dashboard:
150
+ def __init__(self, title="Dashboard"):
151
+ self.title = title
152
+ self.pages = []
153
+
154
+ def add_page(self, page: Page):
155
+ self.pages.append(page)
156
+
157
+ def publish(self, output_dir="output"):
158
+ output_dir = os.path.abspath(output_dir)
159
+ pages_dir = os.path.join(output_dir, "pages")
160
+ downloads_dir = os.path.join(output_dir, "downloads")
161
+ assets_src = os.path.join(os.path.dirname(__file__), "assets")
162
+ assets_dst = os.path.join(output_dir, "assets")
163
+
164
+ os.makedirs(pages_dir, exist_ok=True)
165
+ os.makedirs(downloads_dir, exist_ok=True)
166
+ shutil.copytree(assets_src, assets_dst, dirs_exist_ok=True)
167
+
168
+ # Per-page HTML
169
+ for page in self.pages:
170
+ doc = document(title=page.title)
171
+ with doc.head:
172
+ doc.head.add(link(rel="stylesheet", href="../assets/css/style.css"))
173
+ doc.head.add(script(type="text/javascript", src="../assets/js/script.js"))
174
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))
175
+ doc.head.add(link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css"))
176
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"))
177
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"))
178
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"))
179
+ with doc:
180
+ with div(cls="page-section", id=f"page-{page.slug}") as section:
181
+ for el in page.render(0, downloads_dir=downloads_dir, relative_prefix="../"):
182
+ section += el
183
+ with open(os.path.join(pages_dir, f"{page.slug}.html"), "w") as f:
184
+ f.write(str(doc))
185
+
186
+ # Main index.html
187
+ index_doc = document(title=self.title)
188
+ with index_doc.head:
189
+ index_doc.head.add(link(rel="stylesheet", href="assets/css/style.css"))
190
+ index_doc.head.add(script(type="text/javascript", src="assets/js/script.js"))
191
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))
192
+ index_doc.head.add(link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css"))
193
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"))
194
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"))
195
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"))
196
+
197
+ with index_doc:
198
+ with div(id="sidebar"):
199
+ h1(self.title)
200
+ for page in self.pages:
201
+ a(page.title, cls="nav-link", href="#", **{"data-target": f"page-{page.slug}"})
202
+ with div(id="sidebar-footer"):
203
+ a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
204
+
205
+ with div(id="content"):
206
+ for idx, page in enumerate(self.pages):
207
+ with div(id=f"page-{page.slug}", cls="page-section", style="display:none;") as section:
208
+ for el in page.render(idx, downloads_dir=downloads_dir, relative_prefix=""):
209
+ section += el
210
+
211
+ with open(os.path.join(output_dir, "index.html"), "w") as f:
212
+ f.write(str(index_doc))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 0.3.1
3
+ Version: 0.3.2
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
@@ -1,205 +0,0 @@
1
- import os
2
- import shutil
3
- import uuid
4
- import pandas as pd
5
- import plotly.graph_objects as go
6
- from dominate import document
7
- from dominate.tags import div, h1, h2, h3, h4, p, a, script, link
8
- from dominate.util import raw as raw_util
9
-
10
- class AbstractPage:
11
- def __init__(self):
12
- self.elements = []
13
-
14
- def add_header(self, text, level=1):
15
- if level not in (1, 2, 3, 4):
16
- raise ValueError("Header level must be 1, 2, 3, or 4")
17
- self.elements.append(("header", (text, level)))
18
-
19
- def add_text(self, text):
20
- self.elements.append(("text", text))
21
-
22
- def add_plot(self, plot):
23
- html = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
24
- self.elements.append(("plot", html))
25
-
26
- def add_table(self, df, table_id=None, sortable=True):
27
- if table_id is None:
28
- table_id = f"table-{len(self.elements)}"
29
- classes = "table-hover table-striped"
30
- if sortable:
31
- classes += " sortable"
32
- html = df.to_html(classes=classes, index=False, border=0, table_id=table_id, escape=False)
33
- self.elements.append(("table", (html, table_id)))
34
-
35
- def add_download(self, file_path, label=None):
36
- if not os.path.isfile(file_path):
37
- raise FileNotFoundError(f"File not found: {file_path}")
38
- self.elements.append(("download", (file_path, label)))
39
-
40
- def add_minipage(self, minipage):
41
- self.elements.append(("minipage", minipage))
42
-
43
- class Page(AbstractPage):
44
- def __init__(self, slug, title):
45
- super().__init__()
46
- self.slug = slug
47
- self.title = title
48
- self.add_header(title, level=1)
49
-
50
- def render(self, index):
51
- section = div()
52
- minipage_row = []
53
- for kind, content in self.elements:
54
- if kind == "minipage":
55
- row_div = div(cls="minipage-row")
56
- row_div += content.render(index)
57
- section += row_div
58
- elif kind == "header":
59
- text, level = content
60
- if level == 1:
61
- section += h1(text)
62
- elif level == 2:
63
- section += h2(text)
64
- elif level == 3:
65
- section += h3(text)
66
- elif level == 4:
67
- section += h4(text)
68
- elif kind == "text":
69
- section += p(content)
70
- elif kind == "plot":
71
- section += div(content, cls="plot-container")
72
- elif kind == "table":
73
- table_html, _ = content
74
- section += raw_util(table_html)
75
- elif kind == "download":
76
- file_path, label = content
77
- btn = a(label or os.path.basename(file_path),
78
- href=file_path,
79
- cls="download-button",
80
- download=True)
81
- section += div(btn)
82
- return section
83
-
84
- class MiniPage(AbstractPage):
85
- def __init__(self, width=1.0):
86
- super().__init__()
87
- self.width = width
88
-
89
- def render(self, index=None):
90
- style = f"flex: 0 0 {self.width * 100}%; max-width: {self.width * 100}%;"
91
- container = div(cls="minipage", style=style)
92
- minipage_row = []
93
- for kind, content in self.elements:
94
- if kind == "minipage":
95
- minipage_row.append(content)
96
- else:
97
- if minipage_row:
98
- row_div = div(cls="minipage-row")
99
- for mp in minipage_row:
100
- row_div += mp.render(index)
101
- container += row_div
102
- minipage_row = []
103
- if kind == "header":
104
- text, level = content
105
- if level == 1:
106
- container += h1(text)
107
- elif level == 2:
108
- container += h2(text)
109
- elif level == 3:
110
- container += h3(text)
111
- elif level == 4:
112
- container += h4(text)
113
- elif kind == "text":
114
- container += p(content)
115
- elif kind == "plot":
116
- container += div(content, cls="plot-container")
117
- elif kind == "table":
118
- table_html, _ = content
119
- container += raw_util(table_html)
120
- elif kind == "download":
121
- file_path, label = content
122
- btn = a(label or os.path.basename(file_path),
123
- href=file_path,
124
- cls="download-button",
125
- download=True)
126
- container += div(btn)
127
- if minipage_row:
128
- row_div = div(cls="minipage-row")
129
- for mp in minipage_row:
130
- row_div += mp.render(index)
131
- container += row_div
132
- return container
133
-
134
- class Dashboard:
135
- def __init__(self, title="Dashboard"):
136
- self.title = title
137
- self.pages = []
138
-
139
- def add_page(self, page: Page):
140
- self.pages.append(page)
141
-
142
- def publish(self, output_dir="output"):
143
- output_dir = os.path.abspath(output_dir)
144
- pages_dir = os.path.join(output_dir, "pages")
145
- downloads_dir = os.path.join(output_dir, "downloads")
146
- assets_src = os.path.join(os.path.dirname(__file__), "assets")
147
- assets_dst = os.path.join(output_dir, "assets")
148
-
149
- os.makedirs(pages_dir, exist_ok=True)
150
- os.makedirs(downloads_dir, exist_ok=True)
151
- shutil.copytree(assets_src, assets_dst, dirs_exist_ok=True)
152
-
153
- for page in self.pages:
154
- doc = document(title=page.title)
155
- with doc.head:
156
- doc.head.add(link(rel="stylesheet", href="../assets/css/style.css"))
157
- doc.head.add(script(type="text/javascript", src="../assets/js/script.js"))
158
- doc.head.add(script(src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))
159
-
160
- with doc:
161
- with div(cls="page-section", id=f"page-{page.slug}"):
162
- for kind, content in page.elements:
163
- if kind == "header":
164
- text, level = content
165
- if level == 1:
166
- h1(text)
167
- elif level == 2:
168
- h2(text)
169
- elif level == 3:
170
- h3(text)
171
- elif level == 4:
172
- h4(text)
173
- elif kind == "text":
174
- p(content)
175
- elif kind == "plot":
176
- div(content, cls="plot-container")
177
- elif kind == "table":
178
- table_html, _ = content
179
- doc.add(raw_util(table_html))
180
-
181
- with open(os.path.join(pages_dir, f"{page.slug}.html"), "w") as f:
182
- f.write(str(doc))
183
-
184
- index_doc = document(title=self.title)
185
- with index_doc.head:
186
- index_doc.head.add(link(rel="stylesheet", href="assets/css/style.css"))
187
- index_doc.head.add(script(type="text/javascript", src="assets/js/script.js"))
188
- index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))
189
-
190
- with index_doc:
191
- with div(id="sidebar"):
192
- h1(self.title)
193
- for page in self.pages:
194
- a(page.title, cls="nav-link", href="#", **{"data-target": f"page-{page.slug}"})
195
- with div(id="sidebar-footer"):
196
- a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
197
-
198
- with div(id="content"):
199
- for idx, page in enumerate(self.pages):
200
- with div(id=f"page-{page.slug}", cls="page-section", style="display:none;") as section:
201
- rendered = page.render(idx)
202
- section += rendered # This is correct
203
-
204
- with open(os.path.join(output_dir, "index.html"), "w") as f:
205
- f.write(str(index_doc))
File without changes
File without changes