staticdash 0.2.0__tar.gz → 0.2.1__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.
- {staticdash-0.2.0 → staticdash-0.2.1}/PKG-INFO +1 -1
- {staticdash-0.2.0 → staticdash-0.2.1}/pyproject.toml +1 -1
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash/assets/css/style.css +24 -1
- staticdash-0.2.1/staticdash/assets/js/script.js +94 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash/dashboard.py +47 -9
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash.egg-info/PKG-INFO +1 -1
- staticdash-0.2.0/staticdash/assets/js/script.js +0 -43
- {staticdash-0.2.0 → staticdash-0.2.1}/README.md +0 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/setup.cfg +0 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash/__init__.py +0 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash.egg-info/SOURCES.txt +0 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash.egg-info/dependency_links.txt +0 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash.egg-info/requires.txt +0 -0
- {staticdash-0.2.0 → staticdash-0.2.1}/staticdash.egg-info/top_level.txt +0 -0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "staticdash"
|
7
|
-
version = "0.2.
|
7
|
+
version = "0.2.1"
|
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" }
|
@@ -128,7 +128,7 @@ tr:nth-child(even) {
|
|
128
128
|
background-color: #f2f2f2;
|
129
129
|
}
|
130
130
|
|
131
|
-
tr:hover {
|
131
|
+
tbody tr:hover {
|
132
132
|
background-color: #e8f0fe;
|
133
133
|
}
|
134
134
|
|
@@ -151,4 +151,27 @@ tr:hover {
|
|
151
151
|
|
152
152
|
.download-button:hover {
|
153
153
|
background-color: #547a9f;
|
154
|
+
}
|
155
|
+
|
156
|
+
table.sortable th {
|
157
|
+
cursor: pointer;
|
158
|
+
position: relative;
|
159
|
+
padding-right: 1.5em;
|
160
|
+
user-select: none;
|
161
|
+
}
|
162
|
+
|
163
|
+
table.sortable th::after {
|
164
|
+
content: "";
|
165
|
+
position: absolute;
|
166
|
+
right: 0.5em;
|
167
|
+
font-size: 0.9em;
|
168
|
+
pointer-events: none;
|
169
|
+
}
|
170
|
+
|
171
|
+
table.sortable th.sorted-asc::after {
|
172
|
+
content: "\25B2";
|
173
|
+
}
|
174
|
+
|
175
|
+
table.sortable th.sorted-desc::after {
|
176
|
+
content: "\25BC";
|
154
177
|
}
|
@@ -0,0 +1,94 @@
|
|
1
|
+
document.addEventListener("DOMContentLoaded", () => {
|
2
|
+
const links = document.querySelectorAll(".nav-link");
|
3
|
+
const sections = document.querySelectorAll(".page-section");
|
4
|
+
|
5
|
+
function resizePlotsIn(section) {
|
6
|
+
const plots = section.querySelectorAll(".plotly-graph-div");
|
7
|
+
plots.forEach(plot => {
|
8
|
+
if (typeof Plotly !== "undefined" && plot.data) {
|
9
|
+
Plotly.Plots.resize(plot);
|
10
|
+
}
|
11
|
+
});
|
12
|
+
}
|
13
|
+
|
14
|
+
function showPage(id) {
|
15
|
+
sections.forEach(section => section.style.display = "none");
|
16
|
+
const page = document.getElementById(id);
|
17
|
+
if (page) {
|
18
|
+
page.style.display = "block";
|
19
|
+
|
20
|
+
// Resize Plotly charts within the newly visible section
|
21
|
+
requestAnimationFrame(() => {
|
22
|
+
resizePlotsIn(page);
|
23
|
+
});
|
24
|
+
}
|
25
|
+
|
26
|
+
links.forEach(link => link.classList.remove("active"));
|
27
|
+
const activeLink = Array.from(links).find(link => link.dataset.target === id);
|
28
|
+
if (activeLink) activeLink.classList.add("active");
|
29
|
+
}
|
30
|
+
|
31
|
+
links.forEach(link => {
|
32
|
+
link.addEventListener("click", e => {
|
33
|
+
e.preventDefault();
|
34
|
+
const targetId = link.dataset.target;
|
35
|
+
showPage(targetId);
|
36
|
+
});
|
37
|
+
});
|
38
|
+
|
39
|
+
// Initial page load
|
40
|
+
if (sections.length > 0) {
|
41
|
+
showPage(sections[0].id);
|
42
|
+
}
|
43
|
+
|
44
|
+
// Table sorting for tables with class "sortable"
|
45
|
+
document.querySelectorAll("table.sortable").forEach(function (table) {
|
46
|
+
const headers = table.querySelectorAll("thead th");
|
47
|
+
let originalRows = null;
|
48
|
+
|
49
|
+
headers.forEach(function (th, colIdx) {
|
50
|
+
th.addEventListener("click", function () {
|
51
|
+
if (!originalRows) {
|
52
|
+
const tbody = table.querySelector("tbody");
|
53
|
+
originalRows = Array.from(tbody.querySelectorAll("tr"));
|
54
|
+
}
|
55
|
+
|
56
|
+
// Cycle: none -> asc -> desc -> none
|
57
|
+
let state = th.dataset.sorted;
|
58
|
+
let nextState = state === "asc" ? "desc" : state === "desc" ? "none" : "asc";
|
59
|
+
|
60
|
+
headers.forEach(h => {
|
61
|
+
h.classList.remove("sorted-asc", "sorted-desc");
|
62
|
+
h.dataset.sorted = "none";
|
63
|
+
});
|
64
|
+
|
65
|
+
th.dataset.sorted = nextState;
|
66
|
+
if (nextState === "asc") th.classList.add("sorted-asc");
|
67
|
+
if (nextState === "desc") th.classList.add("sorted-desc");
|
68
|
+
|
69
|
+
const tbody = table.querySelector("tbody");
|
70
|
+
if (nextState === "none") {
|
71
|
+
originalRows.forEach(row => tbody.appendChild(row));
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
|
75
|
+
const rows = Array.from(tbody.querySelectorAll("tr"));
|
76
|
+
rows.sort(function (a, b) {
|
77
|
+
const aText = a.children[colIdx].textContent.trim();
|
78
|
+
const bText = b.children[colIdx].textContent.trim();
|
79
|
+
// Numeric sort if both are numbers
|
80
|
+
const aNum = parseFloat(aText);
|
81
|
+
const bNum = parseFloat(bText);
|
82
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
83
|
+
return nextState === "asc" ? aNum - bNum : bNum - aNum;
|
84
|
+
}
|
85
|
+
// String sort (works for ISO dates)
|
86
|
+
return nextState === "asc"
|
87
|
+
? aText.localeCompare(bText)
|
88
|
+
: bText.localeCompare(aText);
|
89
|
+
});
|
90
|
+
rows.forEach(row => tbody.appendChild(row));
|
91
|
+
});
|
92
|
+
});
|
93
|
+
});
|
94
|
+
});
|
@@ -4,7 +4,7 @@ import uuid
|
|
4
4
|
import pandas as pd
|
5
5
|
import plotly.graph_objects as go
|
6
6
|
from dominate import document
|
7
|
-
from dominate.tags import div, h1, h2, p, a, script, link, table, thead, tr, th, tbody, td, span
|
7
|
+
from dominate.tags import div, h1, h2, h3, h4, p, a, script, link, table, thead, tr, th, tbody, td, span
|
8
8
|
from dominate.util import raw as raw_util
|
9
9
|
|
10
10
|
class Page:
|
@@ -12,6 +12,12 @@ class Page:
|
|
12
12
|
self.slug = slug
|
13
13
|
self.title = title
|
14
14
|
self.elements = []
|
15
|
+
self.add_header(title, level=1) # Add page title as level 1 header
|
16
|
+
|
17
|
+
def add_header(self, text, level=1):
|
18
|
+
if level not in (1, 2, 3, 4):
|
19
|
+
raise ValueError("Header level must be 1, 2, 3, or 4")
|
20
|
+
self.elements.append(("header", (text, level)))
|
15
21
|
|
16
22
|
def add_text(self, text):
|
17
23
|
self.elements.append(("text", text))
|
@@ -20,10 +26,13 @@ class Page:
|
|
20
26
|
html = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
|
21
27
|
self.elements.append(("plot", html))
|
22
28
|
|
23
|
-
def add_table(self, df, table_id=None):
|
29
|
+
def add_table(self, df, table_id=None, sortable=True):
|
24
30
|
if table_id is None:
|
25
31
|
table_id = f"table-{len(self.elements)}"
|
26
|
-
|
32
|
+
classes = "table-hover table-striped"
|
33
|
+
if sortable:
|
34
|
+
classes += " sortable"
|
35
|
+
html = df.to_html(classes=classes, index=False, border=0, table_id=table_id, escape=False)
|
27
36
|
self.elements.append(("table", (html, table_id)))
|
28
37
|
|
29
38
|
def add_download(self, file_path, label=None):
|
@@ -43,9 +52,18 @@ class Page:
|
|
43
52
|
|
44
53
|
def render(self, index):
|
45
54
|
section = div(id=f"page-{index}", cls="page-section")
|
46
|
-
section += h1(self.title)
|
47
55
|
for kind, content in self.elements:
|
48
|
-
if kind == "
|
56
|
+
if kind == "header":
|
57
|
+
text, level = content
|
58
|
+
if level == 1:
|
59
|
+
section += h1(text)
|
60
|
+
elif level == 2:
|
61
|
+
section += h2(text)
|
62
|
+
elif level == 3:
|
63
|
+
section += h3(text)
|
64
|
+
elif level == 4:
|
65
|
+
section += h4(text)
|
66
|
+
elif kind == "text":
|
49
67
|
section += p(content)
|
50
68
|
elif kind == "plot":
|
51
69
|
section += div(content, cls="plot-container")
|
@@ -81,9 +99,19 @@ class Dashboard:
|
|
81
99
|
|
82
100
|
with doc:
|
83
101
|
with div(cls="page-section", id=f"page-{page.slug}"):
|
84
|
-
h1(page.title)
|
102
|
+
# Remove h1(page.title) here, since it's already a header element
|
85
103
|
for kind, content in page.elements:
|
86
|
-
if kind == "
|
104
|
+
if kind == "header":
|
105
|
+
text, level = content
|
106
|
+
if level == 1:
|
107
|
+
h1(text)
|
108
|
+
elif level == 2:
|
109
|
+
h2(text)
|
110
|
+
elif level == 3:
|
111
|
+
h3(text)
|
112
|
+
elif level == 4:
|
113
|
+
h4(text)
|
114
|
+
elif kind == "text":
|
87
115
|
p(content)
|
88
116
|
elif kind == "plot":
|
89
117
|
div(content, cls="plot-container")
|
@@ -110,9 +138,19 @@ class Dashboard:
|
|
110
138
|
with div(id="content"):
|
111
139
|
for page in self.pages:
|
112
140
|
with div(id=f"page-{page.slug}", cls="page-section", style="display:none;"):
|
113
|
-
h2(page.title)
|
141
|
+
# Remove h2(page.title) here, since it's already a header element
|
114
142
|
for kind, content in page.elements:
|
115
|
-
if kind == "
|
143
|
+
if kind == "header":
|
144
|
+
text, level = content
|
145
|
+
if level == 1:
|
146
|
+
h1(text)
|
147
|
+
elif level == 2:
|
148
|
+
h2(text)
|
149
|
+
elif level == 3:
|
150
|
+
h3(text)
|
151
|
+
elif level == 4:
|
152
|
+
h4(text)
|
153
|
+
elif kind == "text":
|
116
154
|
p(content)
|
117
155
|
elif kind == "plot":
|
118
156
|
div(content, cls="plot-container")
|
@@ -1,43 +0,0 @@
|
|
1
|
-
document.addEventListener("DOMContentLoaded", () => {
|
2
|
-
const links = document.querySelectorAll(".nav-link");
|
3
|
-
const sections = document.querySelectorAll(".page-section");
|
4
|
-
|
5
|
-
function resizePlotsIn(section) {
|
6
|
-
const plots = section.querySelectorAll(".plotly-graph-div");
|
7
|
-
plots.forEach(plot => {
|
8
|
-
if (typeof Plotly !== "undefined" && plot.data) {
|
9
|
-
Plotly.Plots.resize(plot);
|
10
|
-
}
|
11
|
-
});
|
12
|
-
}
|
13
|
-
|
14
|
-
function showPage(id) {
|
15
|
-
sections.forEach(section => section.style.display = "none");
|
16
|
-
const page = document.getElementById(id);
|
17
|
-
if (page) {
|
18
|
-
page.style.display = "block";
|
19
|
-
|
20
|
-
// Resize Plotly charts within the newly visible section
|
21
|
-
requestAnimationFrame(() => {
|
22
|
-
resizePlotsIn(page);
|
23
|
-
});
|
24
|
-
}
|
25
|
-
|
26
|
-
links.forEach(link => link.classList.remove("active"));
|
27
|
-
const activeLink = Array.from(links).find(link => link.dataset.target === id);
|
28
|
-
if (activeLink) activeLink.classList.add("active");
|
29
|
-
}
|
30
|
-
|
31
|
-
links.forEach(link => {
|
32
|
-
link.addEventListener("click", e => {
|
33
|
-
e.preventDefault();
|
34
|
-
const targetId = link.dataset.target;
|
35
|
-
showPage(targetId);
|
36
|
-
});
|
37
|
-
});
|
38
|
-
|
39
|
-
// Initial page load
|
40
|
-
if (sections.length > 0) {
|
41
|
-
showPage(sections[0].id);
|
42
|
-
}
|
43
|
-
});
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|