staticdash 0.1.0__py3-none-any.whl → 0.1.2__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/__init__.py +1 -2
- staticdash/assets/css/style.css +100 -0
- staticdash/assets/js/script.js +43 -0
- staticdash/dashboard.py +85 -54
- {staticdash-0.1.0.dist-info → staticdash-0.1.2.dist-info}/METADATA +40 -37
- staticdash-0.1.2.dist-info/RECORD +8 -0
- staticdash-0.1.0.dist-info/RECORD +0 -6
- {staticdash-0.1.0.dist-info → staticdash-0.1.2.dist-info}/WHEEL +0 -0
- {staticdash-0.1.0.dist-info → staticdash-0.1.2.dist-info}/top_level.txt +0 -0
staticdash/__init__.py
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
from .dashboard import Page, Dashboard
|
2
|
-
import simpledash.assets.css
|
1
|
+
from .dashboard import Page, Dashboard
|
@@ -0,0 +1,100 @@
|
|
1
|
+
body {
|
2
|
+
margin: 0;
|
3
|
+
font-family: sans-serif;
|
4
|
+
background-color: #f9f9f9;
|
5
|
+
color: #333;
|
6
|
+
}
|
7
|
+
|
8
|
+
#sidebar {
|
9
|
+
position: fixed;
|
10
|
+
top: 0;
|
11
|
+
left: 0;
|
12
|
+
width: 240px;
|
13
|
+
height: 100vh;
|
14
|
+
background-color: #2c3e50;
|
15
|
+
padding: 20px;
|
16
|
+
box-sizing: border-box;
|
17
|
+
overflow-y: auto;
|
18
|
+
}
|
19
|
+
|
20
|
+
#sidebar h1 {
|
21
|
+
color: #ecf0f1;
|
22
|
+
font-size: 20px;
|
23
|
+
margin-bottom: 20px;
|
24
|
+
}
|
25
|
+
|
26
|
+
.nav-link {
|
27
|
+
display: block;
|
28
|
+
color: #bdc3c7;
|
29
|
+
text-decoration: none;
|
30
|
+
margin: 10px 0;
|
31
|
+
font-weight: bold;
|
32
|
+
transition: color 0.3s;
|
33
|
+
}
|
34
|
+
|
35
|
+
.nav-link.active,
|
36
|
+
.nav-link:hover {
|
37
|
+
color: #ffffff;
|
38
|
+
}
|
39
|
+
|
40
|
+
#content {
|
41
|
+
margin-left: 260px;
|
42
|
+
padding: 20px;
|
43
|
+
box-sizing: border-box;
|
44
|
+
}
|
45
|
+
|
46
|
+
.page-section {
|
47
|
+
display: none;
|
48
|
+
}
|
49
|
+
|
50
|
+
.page-section.active {
|
51
|
+
display: block;
|
52
|
+
}
|
53
|
+
|
54
|
+
.plot-container {
|
55
|
+
margin: 20px 0;
|
56
|
+
}
|
57
|
+
|
58
|
+
table {
|
59
|
+
width: 100%;
|
60
|
+
border-collapse: collapse;
|
61
|
+
margin-top: 1em;
|
62
|
+
background-color: white;
|
63
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
64
|
+
}
|
65
|
+
|
66
|
+
thead {
|
67
|
+
background-color: #2c3e50;
|
68
|
+
color: white;
|
69
|
+
}
|
70
|
+
|
71
|
+
th,
|
72
|
+
td {
|
73
|
+
padding: 10px;
|
74
|
+
border: 1px solid #ddd;
|
75
|
+
text-align: left;
|
76
|
+
}
|
77
|
+
|
78
|
+
tr:nth-child(even) {
|
79
|
+
background-color: #f2f2f2;
|
80
|
+
}
|
81
|
+
|
82
|
+
tr:hover {
|
83
|
+
background-color: #e8f0fe;
|
84
|
+
}
|
85
|
+
|
86
|
+
.dash-image {
|
87
|
+
max-width: 100%;
|
88
|
+
height: auto;
|
89
|
+
margin: 1em 0;
|
90
|
+
}
|
91
|
+
|
92
|
+
|
93
|
+
.plot-container {
|
94
|
+
width: 100%;
|
95
|
+
}
|
96
|
+
|
97
|
+
.plot-container .plotly-graph-div {
|
98
|
+
width: 100% !important;
|
99
|
+
height: auto !important;
|
100
|
+
}
|
@@ -0,0 +1,43 @@
|
|
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
|
+
});
|
staticdash/dashboard.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
import os
|
2
2
|
import shutil
|
3
|
-
import
|
4
|
-
from dominate.tags import div, h1, h2, p, a, script, link, table, thead, tr, th, tbody, td
|
5
|
-
from dominate.util import raw # ✅ correct usage
|
6
|
-
|
7
|
-
import plotly.graph_objs as go
|
3
|
+
import uuid
|
8
4
|
import pandas as pd
|
9
|
-
|
5
|
+
import plotly.graph_objects as go
|
6
|
+
import plotly.express as px
|
7
|
+
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
|
10
10
|
|
11
11
|
class Page:
|
12
12
|
def __init__(self, slug, title):
|
@@ -14,75 +14,106 @@ class Page:
|
|
14
14
|
self.title = title
|
15
15
|
self.elements = []
|
16
16
|
|
17
|
-
def
|
17
|
+
def add_text(self, text):
|
18
|
+
self.elements.append(("text", text))
|
19
|
+
|
20
|
+
def add_plot(self, plot):
|
21
|
+
html = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
|
22
|
+
self.elements.append(("plot", html))
|
23
|
+
|
24
|
+
def add_table(self, df, table_id=None):
|
25
|
+
if table_id is None:
|
26
|
+
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)
|
28
|
+
self.elements.append(("table", (html, table_id)))
|
29
|
+
|
30
|
+
def add(self, element):
|
18
31
|
if isinstance(element, str):
|
19
|
-
self.
|
32
|
+
self.add_text(element)
|
20
33
|
elif isinstance(element, go.Figure):
|
21
|
-
|
22
|
-
self.elements.append(raw(html))
|
34
|
+
self.add_plot(element)
|
23
35
|
elif isinstance(element, pd.DataFrame):
|
24
|
-
|
25
|
-
tbl.add(thead(tr(*[th(col) for col in element.columns])))
|
26
|
-
tb = tbody()
|
27
|
-
for _, row in element.iterrows():
|
28
|
-
tb.add(tr(*[td(str(val)) for val in row]))
|
29
|
-
tbl.add(tb)
|
30
|
-
self.elements.append(tbl)
|
36
|
+
self.add_table(element)
|
31
37
|
else:
|
32
|
-
|
38
|
+
raise ValueError(f"Unsupported element type: {type(element)}")
|
33
39
|
|
40
|
+
def render(self, index):
|
41
|
+
section = div(id=f"page-{index}", cls="page-section")
|
42
|
+
section += h1(self.title)
|
43
|
+
for kind, content in self.elements:
|
44
|
+
if kind == "text":
|
45
|
+
section += p(content)
|
46
|
+
elif kind == "plot":
|
47
|
+
section += div(content, cls="plot-container")
|
48
|
+
elif kind == "table":
|
49
|
+
table_html, _ = content
|
50
|
+
section += raw_util(table_html)
|
51
|
+
return section
|
34
52
|
|
35
53
|
class Dashboard:
|
36
54
|
def __init__(self, title="Dashboard"):
|
37
55
|
self.title = title
|
38
56
|
self.pages = []
|
39
57
|
|
40
|
-
def add_page(self, page):
|
58
|
+
def add_page(self, page: Page):
|
41
59
|
self.pages.append(page)
|
42
60
|
|
43
61
|
def publish(self, output_dir="output"):
|
44
62
|
os.makedirs(output_dir, exist_ok=True)
|
45
|
-
|
46
|
-
|
63
|
+
pages_dir = os.path.join(output_dir, "pages")
|
64
|
+
os.makedirs(pages_dir, exist_ok=True)
|
47
65
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
shutil.copyfile(css_src, os.path.join(css_dst_dir, "style.css"))
|
66
|
+
# Copy assets
|
67
|
+
assets_src = os.path.join(os.path.dirname(__file__), "assets")
|
68
|
+
assets_dst = os.path.join(output_dir, "assets")
|
69
|
+
shutil.copytree(assets_src, assets_dst, dirs_exist_ok=True)
|
53
70
|
|
54
|
-
|
55
|
-
|
71
|
+
# Generate each page
|
72
|
+
for page in self.pages:
|
73
|
+
doc = document(title=page.title)
|
74
|
+
with doc.head:
|
75
|
+
doc.head.add(link(rel="stylesheet", href="../assets/css/style.css"))
|
76
|
+
doc.head.add(script(type="text/javascript", src="../assets/js/script.js"))
|
56
77
|
|
57
|
-
|
58
|
-
|
59
|
-
|
78
|
+
with doc:
|
79
|
+
with div(cls="page-section", id=f"page-{page.slug}"):
|
80
|
+
h1(page.title)
|
81
|
+
for kind, content in page.elements:
|
82
|
+
if kind == "text":
|
83
|
+
p(content)
|
84
|
+
elif kind == "plot":
|
85
|
+
div(content, cls="plot-container")
|
86
|
+
elif kind == "table":
|
87
|
+
table_html, _ = content
|
88
|
+
doc.add(raw_util(table_html))
|
60
89
|
|
61
|
-
|
90
|
+
with open(os.path.join(pages_dir, f"{page.slug}.html"), "w") as f:
|
91
|
+
f.write(str(doc))
|
92
|
+
|
93
|
+
# Generate index.html with navigation
|
94
|
+
index_doc = document(title=self.title)
|
95
|
+
with index_doc.head:
|
96
|
+
index_doc.head.add(link(rel="stylesheet", href="assets/css/style.css"))
|
97
|
+
index_doc.head.add(script(type="text/javascript", src="assets/js/script.js"))
|
98
|
+
|
99
|
+
with index_doc:
|
62
100
|
with div(id="sidebar"):
|
63
101
|
h1(self.title)
|
64
|
-
for
|
65
|
-
a(page.title, href="#",
|
66
|
-
|
102
|
+
for page in self.pages:
|
103
|
+
a(page.title, cls="nav-link", href="#", **{"data-target": f"page-{page.slug}"})
|
67
104
|
with div(id="content"):
|
68
|
-
for
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
event.target.classList.add('active');
|
81
|
-
}
|
82
|
-
window.onload = () => showPage('page-0');
|
83
|
-
"""
|
84
|
-
with doc:
|
85
|
-
script(raw(js_code))
|
105
|
+
for page in self.pages:
|
106
|
+
with div(id=f"page-{page.slug}", cls="page-section", style="display:none;"):
|
107
|
+
h2(page.title)
|
108
|
+
for kind, content in page.elements:
|
109
|
+
if kind == "text":
|
110
|
+
p(content)
|
111
|
+
elif kind == "plot":
|
112
|
+
div(content, cls="plot-container")
|
113
|
+
elif kind == "table":
|
114
|
+
table_html, _ = content
|
115
|
+
div(raw_util(table_html))
|
116
|
+
|
86
117
|
|
87
118
|
with open(os.path.join(output_dir, "index.html"), "w") as f:
|
88
|
-
f.write(
|
119
|
+
f.write(str(index_doc))
|
@@ -1,13 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: staticdash
|
3
|
-
Version: 0.1.
|
4
|
-
Summary: A
|
3
|
+
Version: 0.1.2
|
4
|
+
Summary: A lightweight static HTML dashboard generator with Plotly and pandas support.
|
5
5
|
Author-email: Brian Day <brian.day1@gmail.com>
|
6
|
-
License
|
6
|
+
License: CC0-1.0
|
7
7
|
Project-URL: Homepage, https://github.com/briday1/staticdash
|
8
8
|
Project-URL: Repository, https://github.com/briday1/staticdash
|
9
9
|
Requires-Python: >=3.8
|
10
10
|
Description-Content-Type: text/markdown
|
11
|
+
Requires-Dist: plotly
|
12
|
+
Requires-Dist: pandas
|
13
|
+
Requires-Dist: dominate
|
11
14
|
|
12
15
|
# staticdash
|
13
16
|
|
@@ -29,50 +32,50 @@ Create a Python script like this:
|
|
29
32
|
|
30
33
|
```python
|
31
34
|
from staticdash.dashboard import Dashboard, Page
|
32
|
-
import plotly.
|
35
|
+
import plotly.express as px
|
33
36
|
import pandas as pd
|
37
|
+
import numpy as np
|
34
38
|
|
35
|
-
# Create
|
36
|
-
|
39
|
+
# Create sample data
|
40
|
+
df = pd.DataFrame({
|
41
|
+
"Category": ["A", "B", "C", "D"],
|
42
|
+
"Value": [10, 20, 30, 40]
|
43
|
+
})
|
37
44
|
|
38
|
-
|
39
|
-
|
45
|
+
df2 = pd.DataFrame({
|
46
|
+
"Time": pd.date_range("2024-01-01", periods=10, freq="D"),
|
47
|
+
"Signal": np.random.randn(10).cumsum()
|
48
|
+
})
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 1, 6], mode='lines+markers', name="Demo Line"))
|
44
|
-
fig.update_layout(title="Sample Plot")
|
45
|
-
page1.append(fig)
|
50
|
+
fig1 = px.bar(df, x="Category", y="Value", title="Bar Chart Example")
|
51
|
+
fig2 = px.line(df2, x="Time", y="Signal", title="Signal over Time")
|
46
52
|
|
47
|
-
#
|
48
|
-
|
49
|
-
"Category": ["A", "B", "C"],
|
50
|
-
"Value": [100, 200, 150]
|
51
|
-
})
|
52
|
-
page1.append(df1)
|
53
|
+
# Build dashboard
|
54
|
+
dashboard = Dashboard(title="StaticDash Demo")
|
53
55
|
|
54
|
-
#
|
55
|
-
page1
|
56
|
+
# Page 1: Overview
|
57
|
+
page1 = Page("overview", "Overview")
|
58
|
+
page1.add("Welcome to the StaticDash demo. Below is a bar chart and a table.")
|
59
|
+
page1.add(fig1)
|
60
|
+
page1.add(df)
|
61
|
+
|
62
|
+
# Page 2: Timeseries
|
63
|
+
page2 = Page("timeseries", "Timeseries")
|
64
|
+
page2.add("Here is a random time series with cumulative noise.")
|
65
|
+
page2.add(fig2)
|
66
|
+
page2.add(df2)
|
67
|
+
|
68
|
+
# Page 3: Summary
|
69
|
+
page3 = Page("summary", "Summary")
|
70
|
+
page3.add("Summary and notes can be added here.")
|
71
|
+
page3.add("StaticDash is a lightweight static dashboard generator.")
|
72
|
+
|
73
|
+
# Register pages
|
56
74
|
dashboard.add_page(page1)
|
57
|
-
|
58
|
-
# Page 2: Data Table
|
59
|
-
page2 = Page("data", "Data")
|
60
|
-
df2 = pd.DataFrame({
|
61
|
-
"Name": ["Alice", "Bob", "Charlie"],
|
62
|
-
"Score": [85, 92, 78],
|
63
|
-
"Passed": [True, True, False]
|
64
|
-
})
|
65
|
-
page2.append("This table shows individual scores and pass/fail status.")
|
66
|
-
page2.append(df2)
|
67
75
|
dashboard.add_page(page2)
|
68
|
-
|
69
|
-
# Page 3: Notes
|
70
|
-
page3 = Page("notes", "Notes")
|
71
|
-
page3.append("These are concluding notes about the dataset.")
|
72
|
-
page3.append("You can also add multiple text blocks like this.")
|
73
76
|
dashboard.add_page(page3)
|
74
77
|
|
75
|
-
#
|
78
|
+
# Export
|
76
79
|
dashboard.publish(output_dir="output")
|
77
80
|
```
|
78
81
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
staticdash/__init__.py,sha256=KqViaDkiQnhBI8-j3hr14umLDmPgddvdB_G1nJeC5Xs,38
|
2
|
+
staticdash/dashboard.py,sha256=DFTPvqSOI00c2qVS4MvOyia1ly-Ph8-oSlbQDT9TSj8,4644
|
3
|
+
staticdash/assets/css/style.css,sha256=OIcaUe9z3uQjfnisZiyxV-UodHQLq4kIWKxtq7BL2-Q,1315
|
4
|
+
staticdash/assets/js/script.js,sha256=GA6dd--Z_gvsLylOHZX73FkPPkznuFzsFGYYKL-sDxM,1223
|
5
|
+
staticdash-0.1.2.dist-info/METADATA,sha256=Mir0l0LNUGiusMsNv2lCOSB2y-sYwzmY10aUVUD5Gfg,2157
|
6
|
+
staticdash-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
+
staticdash-0.1.2.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
8
|
+
staticdash-0.1.2.dist-info/RECORD,,
|
@@ -1,6 +0,0 @@
|
|
1
|
-
staticdash/__init__.py,sha256=NBEjkx3xBwyM5uAbBRGypiokkebsNwF7yM7AFP0quDg,67
|
2
|
-
staticdash/dashboard.py,sha256=ShL9YkqiTL4Lwd9Wkvr6TEBFUFmMN5BOXoC3wKJ5rkQ,3081
|
3
|
-
staticdash-0.1.0.dist-info/METADATA,sha256=cRIzyfwdxk1knwaC786xdqArgSbGu3i4YAllAV_qfwA,2075
|
4
|
-
staticdash-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
5
|
-
staticdash-0.1.0.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
|
6
|
-
staticdash-0.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|