staticdash 0.1.4__py3-none-any.whl → 0.2.1__py3-none-any.whl
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/assets/css/style.css +83 -20
- staticdash/assets/js/script.js +51 -0
- staticdash/dashboard.py +52 -32
- {staticdash-0.1.4.dist-info → staticdash-0.2.1.dist-info}/METADATA +1 -1
- staticdash-0.2.1.dist-info/RECORD +8 -0
- staticdash-0.1.4.dist-info/RECORD +0 -8
- {staticdash-0.1.4.dist-info → staticdash-0.2.1.dist-info}/WHEEL +0 -0
- {staticdash-0.1.4.dist-info → staticdash-0.2.1.dist-info}/top_level.txt +0 -0
staticdash/assets/css/style.css
CHANGED
@@ -12,9 +12,24 @@ body {
|
|
12
12
|
width: 240px;
|
13
13
|
height: 100vh;
|
14
14
|
background-color: #2c3e50;
|
15
|
-
padding: 20px;
|
15
|
+
padding: 20px 20px 60px 20px;
|
16
16
|
box-sizing: border-box;
|
17
17
|
overflow-y: auto;
|
18
|
+
scrollbar-width: thin;
|
19
|
+
scrollbar-color: #888 #2c3e50;
|
20
|
+
}
|
21
|
+
|
22
|
+
#sidebar::-webkit-scrollbar {
|
23
|
+
width: 6px;
|
24
|
+
}
|
25
|
+
|
26
|
+
#sidebar::-webkit-scrollbar-track {
|
27
|
+
background: #2c3e50;
|
28
|
+
}
|
29
|
+
|
30
|
+
#sidebar::-webkit-scrollbar-thumb {
|
31
|
+
background-color: #888;
|
32
|
+
border-radius: 3px;
|
18
33
|
}
|
19
34
|
|
20
35
|
#sidebar h1 {
|
@@ -29,12 +44,40 @@ body {
|
|
29
44
|
text-decoration: none;
|
30
45
|
margin: 10px 0;
|
31
46
|
font-weight: bold;
|
32
|
-
|
47
|
+
padding: 10px 15px;
|
48
|
+
border-radius: 10px;
|
49
|
+
transition: background-color 0.3s, color 0.3s;
|
33
50
|
}
|
34
51
|
|
35
|
-
.nav-link.active,
|
36
52
|
.nav-link:hover {
|
37
53
|
color: #ffffff;
|
54
|
+
background-color: #34495e;
|
55
|
+
}
|
56
|
+
|
57
|
+
.nav-link.active {
|
58
|
+
color: #ffffff;
|
59
|
+
background-color: #1abc9c;
|
60
|
+
}
|
61
|
+
|
62
|
+
#sidebar-footer {
|
63
|
+
position: fixed;
|
64
|
+
bottom: 20px;
|
65
|
+
left: 20px;
|
66
|
+
width: 200px;
|
67
|
+
font-size: 12px;
|
68
|
+
color: #7f8c8d;
|
69
|
+
text-align: center;
|
70
|
+
line-height: 1.4;
|
71
|
+
background-color: #2c3e50;
|
72
|
+
}
|
73
|
+
|
74
|
+
#sidebar-footer a {
|
75
|
+
color: #1abc9c;
|
76
|
+
text-decoration: none;
|
77
|
+
}
|
78
|
+
|
79
|
+
#sidebar-footer a:hover {
|
80
|
+
text-decoration: underline;
|
38
81
|
}
|
39
82
|
|
40
83
|
#content {
|
@@ -53,6 +96,12 @@ body {
|
|
53
96
|
|
54
97
|
.plot-container {
|
55
98
|
margin: 20px 0;
|
99
|
+
width: 100%;
|
100
|
+
}
|
101
|
+
|
102
|
+
.plot-container .plotly-graph-div {
|
103
|
+
width: 100% !important;
|
104
|
+
height: auto !important;
|
56
105
|
}
|
57
106
|
|
58
107
|
table {
|
@@ -79,7 +128,7 @@ tr:nth-child(even) {
|
|
79
128
|
background-color: #f2f2f2;
|
80
129
|
}
|
81
130
|
|
82
|
-
tr:hover {
|
131
|
+
tbody tr:hover {
|
83
132
|
background-color: #e8f0fe;
|
84
133
|
}
|
85
134
|
|
@@ -89,26 +138,40 @@ tr:hover {
|
|
89
138
|
margin: 1em 0;
|
90
139
|
}
|
91
140
|
|
141
|
+
.download-button {
|
142
|
+
display: inline-block;
|
143
|
+
padding: 0.5em 1em;
|
144
|
+
margin: 1em 0;
|
145
|
+
background-color: #2c3e50;
|
146
|
+
color: white;
|
147
|
+
text-decoration: none;
|
148
|
+
border-radius: 5px;
|
149
|
+
font-weight: bold;
|
150
|
+
}
|
92
151
|
|
93
|
-
.
|
94
|
-
|
152
|
+
.download-button:hover {
|
153
|
+
background-color: #547a9f;
|
95
154
|
}
|
96
155
|
|
97
|
-
.
|
98
|
-
|
99
|
-
|
156
|
+
table.sortable th {
|
157
|
+
cursor: pointer;
|
158
|
+
position: relative;
|
159
|
+
padding-right: 1.5em;
|
160
|
+
user-select: none;
|
100
161
|
}
|
101
162
|
|
102
|
-
.
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
text-decoration: none;
|
109
|
-
border-radius: 5px;
|
110
|
-
font-weight: bold;
|
163
|
+
table.sortable th::after {
|
164
|
+
content: "";
|
165
|
+
position: absolute;
|
166
|
+
right: 0.5em;
|
167
|
+
font-size: 0.9em;
|
168
|
+
pointer-events: none;
|
111
169
|
}
|
112
|
-
|
113
|
-
|
170
|
+
|
171
|
+
table.sortable th.sorted-asc::after {
|
172
|
+
content: "\25B2";
|
173
|
+
}
|
174
|
+
|
175
|
+
table.sortable th.sorted-desc::after {
|
176
|
+
content: "\25BC";
|
114
177
|
}
|
staticdash/assets/js/script.js
CHANGED
@@ -40,4 +40,55 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
40
40
|
if (sections.length > 0) {
|
41
41
|
showPage(sections[0].id);
|
42
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
|
+
});
|
43
94
|
});
|
staticdash/dashboard.py
CHANGED
@@ -3,16 +3,21 @@ import shutil
|
|
3
3
|
import uuid
|
4
4
|
import pandas as pd
|
5
5
|
import plotly.graph_objects as go
|
6
|
-
import plotly.express as px
|
7
6
|
from dominate import document
|
8
|
-
from dominate.tags import div, h1, h2, p, a, script, link, table, thead, tr, th, tbody, td, span
|
9
|
-
from dominate.util import raw as raw_util
|
7
|
+
from dominate.tags import div, h1, h2, h3, h4, p, a, script, link, table, thead, tr, th, tbody, td, span
|
8
|
+
from dominate.util import raw as raw_util
|
10
9
|
|
11
10
|
class Page:
|
12
11
|
def __init__(self, slug, title):
|
13
12
|
self.slug = slug
|
14
13
|
self.title = title
|
15
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)))
|
16
21
|
|
17
22
|
def add_text(self, text):
|
18
23
|
self.elements.append(("text", text))
|
@@ -21,12 +26,15 @@ class Page:
|
|
21
26
|
html = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
|
22
27
|
self.elements.append(("plot", html))
|
23
28
|
|
24
|
-
def add_table(self, df, table_id=None):
|
29
|
+
def add_table(self, df, table_id=None, sortable=True):
|
25
30
|
if table_id is None:
|
26
31
|
table_id = f"table-{len(self.elements)}"
|
27
|
-
|
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)
|
28
36
|
self.elements.append(("table", (html, table_id)))
|
29
|
-
|
37
|
+
|
30
38
|
def add_download(self, file_path, label=None):
|
31
39
|
if not os.path.isfile(file_path):
|
32
40
|
raise FileNotFoundError(f"File not found: {file_path}")
|
@@ -44,9 +52,18 @@ class Page:
|
|
44
52
|
|
45
53
|
def render(self, index):
|
46
54
|
section = div(id=f"page-{index}", cls="page-section")
|
47
|
-
section += h1(self.title)
|
48
55
|
for kind, content in self.elements:
|
49
|
-
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":
|
50
67
|
section += p(content)
|
51
68
|
elif kind == "plot":
|
52
69
|
section += div(content, cls="plot-container")
|
@@ -70,13 +87,10 @@ class Dashboard:
|
|
70
87
|
assets_src = os.path.join(os.path.dirname(__file__), "assets")
|
71
88
|
assets_dst = os.path.join(output_dir, "assets")
|
72
89
|
|
73
|
-
# Ensure directories exist
|
74
90
|
os.makedirs(pages_dir, exist_ok=True)
|
75
91
|
os.makedirs(downloads_dir, exist_ok=True)
|
76
92
|
shutil.copytree(assets_src, assets_dst, dirs_exist_ok=True)
|
77
93
|
|
78
|
-
|
79
|
-
# Generate each page
|
80
94
|
for page in self.pages:
|
81
95
|
doc = document(title=page.title)
|
82
96
|
with doc.head:
|
@@ -85,9 +99,19 @@ class Dashboard:
|
|
85
99
|
|
86
100
|
with doc:
|
87
101
|
with div(cls="page-section", id=f"page-{page.slug}"):
|
88
|
-
h1(page.title)
|
102
|
+
# Remove h1(page.title) here, since it's already a header element
|
89
103
|
for kind, content in page.elements:
|
90
|
-
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":
|
91
115
|
p(content)
|
92
116
|
elif kind == "plot":
|
93
117
|
div(content, cls="plot-container")
|
@@ -98,7 +122,6 @@ class Dashboard:
|
|
98
122
|
with open(os.path.join(pages_dir, f"{page.slug}.html"), "w") as f:
|
99
123
|
f.write(str(doc))
|
100
124
|
|
101
|
-
# Generate index.html with navigation
|
102
125
|
index_doc = document(title=self.title)
|
103
126
|
with index_doc.head:
|
104
127
|
index_doc.head.add(link(rel="stylesheet", href="assets/css/style.css"))
|
@@ -109,46 +132,43 @@ class Dashboard:
|
|
109
132
|
h1(self.title)
|
110
133
|
for page in self.pages:
|
111
134
|
a(page.title, cls="nav-link", href="#", **{"data-target": f"page-{page.slug}"})
|
135
|
+
with div(id="sidebar-footer"):
|
136
|
+
a("Produced by staticdash", href="https://pypi.org/project/staticdash/", target="_blank")
|
137
|
+
|
112
138
|
with div(id="content"):
|
113
139
|
for page in self.pages:
|
114
140
|
with div(id=f"page-{page.slug}", cls="page-section", style="display:none;"):
|
115
|
-
h2(page.title)
|
141
|
+
# Remove h2(page.title) here, since it's already a header element
|
116
142
|
for kind, content in page.elements:
|
117
|
-
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":
|
118
154
|
p(content)
|
119
155
|
elif kind == "plot":
|
120
156
|
div(content, cls="plot-container")
|
121
157
|
elif kind == "table":
|
122
158
|
table_html, _ = content
|
123
159
|
div(raw_util(table_html))
|
124
|
-
# elif kind == "download":
|
125
|
-
# src_path, label = content
|
126
|
-
# file_uuid = f"{uuid.uuid4().hex}_{os.path.basename(src_path)}"
|
127
|
-
# dst_path = os.path.join(downloads_dir, file_uuid)
|
128
|
-
# shutil.copy2(src_path, dst_path)
|
129
|
-
# a(label or os.path.basename(src_path),
|
130
|
-
# href=f"{downloads_dir}/{file_uuid}",
|
131
|
-
# cls="download-button",
|
132
|
-
# download=True)
|
133
160
|
elif kind == "download":
|
134
161
|
src_path, label = content
|
135
162
|
file_uuid = f"{uuid.uuid4().hex}_{os.path.basename(src_path)}"
|
136
163
|
dst_path = os.path.join(downloads_dir, file_uuid)
|
137
164
|
shutil.copy2(src_path, dst_path)
|
138
|
-
|
139
|
-
# Use relative link from HTML to downloads/ folder
|
140
165
|
download_link = f"downloads/{file_uuid}"
|
141
|
-
|
142
|
-
# Create button and append it to the current div
|
143
166
|
btn = a(label or os.path.basename(src_path),
|
144
167
|
href=download_link,
|
145
168
|
cls="download-button",
|
146
169
|
download=True)
|
147
|
-
|
148
170
|
div(btn)
|
149
171
|
div(raw_util("<br>"))
|
150
172
|
|
151
|
-
|
152
|
-
|
153
173
|
with open(os.path.join(output_dir, "index.html"), "w") as f:
|
154
174
|
f.write(str(index_doc))
|
@@ -0,0 +1,8 @@
|
|
1
|
+
staticdash/__init__.py,sha256=KqViaDkiQnhBI8-j3hr14umLDmPgddvdB_G1nJeC5Xs,38
|
2
|
+
staticdash/dashboard.py,sha256=trPzco0AZw4IAwZzgY__RsOjwg4UrD7etB-4Da1kHE0,7515
|
3
|
+
staticdash/assets/css/style.css,sha256=kL6FAGxIwYrfNoo4oCzC-ygHmyOZDEjhjixpY0YGpss,2649
|
4
|
+
staticdash/assets/js/script.js,sha256=SMOyh7_E_BlvLYMEwYlvCnV7-GnMR-x8PtEiFbIIAsw,3089
|
5
|
+
staticdash-0.2.1.dist-info/METADATA,sha256=IohPqKSIpXUNPsDhZUFXTpMbtwcrHzYwrhQnUX1Wync,2409
|
6
|
+
staticdash-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
+
staticdash-0.2.1.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
8
|
+
staticdash-0.2.1.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
staticdash/__init__.py,sha256=KqViaDkiQnhBI8-j3hr14umLDmPgddvdB_G1nJeC5Xs,38
|
2
|
-
staticdash/dashboard.py,sha256=Pj5or33xXyOJCxyvOVQTs8rDuYcqOYBVy11Z7ViRUuo,6557
|
3
|
-
staticdash/assets/css/style.css,sha256=pXKDi54gcDaxF5ABji5zfAS3ckHK4DhQU0onSBKkmWo,1587
|
4
|
-
staticdash/assets/js/script.js,sha256=rAGEB9sgv8LGdpA1JLQwYAZVRimpEfFSELKb0fFeATI,1222
|
5
|
-
staticdash-0.1.4.dist-info/METADATA,sha256=Dv6QYAxvJ31nrdpqohJ8BTEkXTvN-9jCLfnp6LHPmcI,2409
|
6
|
-
staticdash-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
-
staticdash-0.1.4.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
8
|
-
staticdash-0.1.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|