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.
@@ -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
- transition: color 0.3s;
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
- .plot-container {
94
- width: 100%;
152
+ .download-button:hover {
153
+ background-color: #547a9f;
95
154
  }
96
155
 
97
- .plot-container .plotly-graph-div {
98
- width: 100% !important;
99
- height: auto !important;
156
+ table.sortable th {
157
+ cursor: pointer;
158
+ position: relative;
159
+ padding-right: 1.5em;
160
+ user-select: none;
100
161
  }
101
162
 
102
- .download-button {
103
- display: inline-block;
104
- padding: 0.5em 1em;
105
- margin: 1em 0;
106
- background-color: #2c3e50;
107
- color: white;
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
- .download-button:hover {
113
- background-color: #547a9f;
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
  }
@@ -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 # To avoid ambiguity
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
- html = df.to_html(classes="table-hover table-striped", index=False, border=0, table_id=table_id)
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 == "text":
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 == "text":
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 == "text":
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 0.1.4
3
+ Version: 0.2.1
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
@@ -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,,