df2tables 0.0.8__py2.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.
- df2tables/__init__.py +2 -0
- df2tables/comnt.py +227 -0
- df2tables/datatable_templ.html +374 -0
- df2tables/df2tables.py +275 -0
- df2tables-0.0.8.dist-info/METADATA +397 -0
- df2tables-0.0.8.dist-info/RECORD +8 -0
- df2tables-0.0.8.dist-info/WHEEL +5 -0
- df2tables-0.0.8.dist-info/licenses/LICENCE.txt +21 -0
df2tables/__init__.py
ADDED
df2tables/comnt.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding=utf8
|
|
3
|
+
from sys import exc_info
|
|
4
|
+
"""
|
|
5
|
+
Comnt - template content using block-annotated comments
|
|
6
|
+
|
|
7
|
+
A dead-simple, human-friendly way to manage template regions using
|
|
8
|
+
**standard comment syntax** — no weird symbols, no broken files, no preprocessing
|
|
9
|
+
to view your code in a browser.
|
|
10
|
+
|
|
11
|
+
One huge practical benefit: you can inject actual JavaScript-ready values from Python—
|
|
12
|
+
not just strings or you can refer to javascript functions defined elsewhere.
|
|
13
|
+
That means you don’t need to wrap your data in quotes and then call
|
|
14
|
+
`JSON.parse(...)` on the frontend, like you often do with Jinja2.
|
|
15
|
+
|
|
16
|
+
Supported tag/comment Formats:
|
|
17
|
+
HTML/XML style:
|
|
18
|
+
<!--[tag_name-->
|
|
19
|
+
Content to be replaced
|
|
20
|
+
<!--tag_name]-->
|
|
21
|
+
|
|
22
|
+
JavaScript/CSS style:
|
|
23
|
+
/*[tag_name*/
|
|
24
|
+
const arr_to_change = [0,1]
|
|
25
|
+
/*tag_name]*/
|
|
26
|
+
|
|
27
|
+
Tag Format Rules:
|
|
28
|
+
- Opening tag: Comment start + '[' + tag_name + Comment continuation
|
|
29
|
+
- Closing tag: Comment start + tag_name + ']' + Comment end
|
|
30
|
+
- Tag names must match exactly between opening and closing tags
|
|
31
|
+
|
|
32
|
+
Author: [Tomasz Sługocki]
|
|
33
|
+
Version: 0.0.1
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__all__ = ['render', 'write_from_template', 'simple_example', 'example']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotFoundError(Exception):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render_block(text, tag_id, val=None):
|
|
44
|
+
assert isinstance(text, str) and isinstance(tag_id, str)
|
|
45
|
+
assert len(text) > len(tag_id)
|
|
46
|
+
|
|
47
|
+
assert val is None or isinstance(val, str)
|
|
48
|
+
|
|
49
|
+
def get_tags(text, tag_id, ext):
|
|
50
|
+
assert ext in ("html", "js")
|
|
51
|
+
start, end = ("/*", "*/") if ext == "js" else ("<!--", "-->")
|
|
52
|
+
start_tag_js, start_tag_html = f"/*[{tag_id}*/", f"<!--[{tag_id}-->"
|
|
53
|
+
if start_tag_js in text and start_tag_html in text:
|
|
54
|
+
raise AssertionError(f"same tag id in javascript and html is not allowed")
|
|
55
|
+
start_tag = "".join((start, "[", tag_id, end))
|
|
56
|
+
end_tag = "".join((start, tag_id, "]", end))
|
|
57
|
+
start_cnt = len(text.split(start_tag)) - 1
|
|
58
|
+
end_cnt = len(text.split(end_tag)) - 1
|
|
59
|
+
if not start_cnt and not end_cnt:
|
|
60
|
+
raise ValueError(f"not found tags {start_tag} {end_tag}, ext:{ext}")
|
|
61
|
+
if not start_cnt:
|
|
62
|
+
raise NotFoundError(f"start tag not found in form: '{start_tag}'")
|
|
63
|
+
if not end_cnt:
|
|
64
|
+
raise NotFoundError(f"end tag not found in form: '{end_tag}'")
|
|
65
|
+
if start_cnt > 1:
|
|
66
|
+
raise AssertionError(f"more than one start tag: '{start_tag}'")
|
|
67
|
+
if end_cnt > 1:
|
|
68
|
+
raise AssertionError(f"more than one end tag: '{end_tag}'")
|
|
69
|
+
return start_tag, end_tag
|
|
70
|
+
|
|
71
|
+
start_tag, end_tag = None, None
|
|
72
|
+
errs = []
|
|
73
|
+
for ext in ["js", "html"]:
|
|
74
|
+
try:
|
|
75
|
+
start_tag, end_tag = get_tags(text, tag_id, ext)
|
|
76
|
+
except ValueError:
|
|
77
|
+
errs.append(repr(exc_info()[1]))
|
|
78
|
+
if not start_tag:
|
|
79
|
+
raise ValueError(f"Errors happened: {''.join(errs)}")
|
|
80
|
+
|
|
81
|
+
start_idx = text.find(start_tag)
|
|
82
|
+
end_idx = text.find(end_tag)
|
|
83
|
+
prefix = text[:start_idx + len(start_tag)]
|
|
84
|
+
suffix = text[end_idx:]
|
|
85
|
+
|
|
86
|
+
if val is None: # empty strings are ok
|
|
87
|
+
old_val = text[start_idx + len(start_tag):end_idx]
|
|
88
|
+
return old_val
|
|
89
|
+
return "\n".join((prefix, val, suffix))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_tag_content(tag, instr):
|
|
93
|
+
assert len(instr) > len(tag) and tag + instr
|
|
94
|
+
return _render_block(instr, tag, None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def render(instr, repldict):
|
|
98
|
+
for key, val in repldict.items():
|
|
99
|
+
try:
|
|
100
|
+
instr = _render_block(instr, key, val)
|
|
101
|
+
except ValueError:
|
|
102
|
+
print(exc_info()[1])
|
|
103
|
+
|
|
104
|
+
return instr
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def write_from_template(template, newfile, repldict):
|
|
108
|
+
assert isinstance(repldict, dict)
|
|
109
|
+
assert template != newfile
|
|
110
|
+
with open(template, encoding="utf-8") as op_file:
|
|
111
|
+
instr = op_file.read()
|
|
112
|
+
replaced = render(instr, repldict)
|
|
113
|
+
with open(newfile, "w", encoding="utf8") as outfile:
|
|
114
|
+
outfile.write(replaced)
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def example():
|
|
119
|
+
import os
|
|
120
|
+
import subprocess
|
|
121
|
+
import sys
|
|
122
|
+
|
|
123
|
+
def open_file(filename):
|
|
124
|
+
if sys.platform.startswith("win"):
|
|
125
|
+
os.startfile(filename)
|
|
126
|
+
else:
|
|
127
|
+
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
|
128
|
+
subprocess.call([opener, filename])
|
|
129
|
+
|
|
130
|
+
content = """
|
|
131
|
+
<!DOCTYPE html>
|
|
132
|
+
<html>
|
|
133
|
+
<head>
|
|
134
|
+
<title>
|
|
135
|
+
Example comnt rendered page
|
|
136
|
+
</title>
|
|
137
|
+
|
|
138
|
+
</head>
|
|
139
|
+
<body> <h3>
|
|
140
|
+
<!--[title-->
|
|
141
|
+
Welcome to our site!
|
|
142
|
+
|
|
143
|
+
<!--title]--></h3>
|
|
144
|
+
<!--[content-->
|
|
145
|
+
|
|
146
|
+
<p>
|
|
147
|
+
This is placeholder content that shows in the browser.
|
|
148
|
+
</p>
|
|
149
|
+
<!--content]-->
|
|
150
|
+
<code id="data-display" style=
|
|
151
|
+
"background-color: rgb(245, 245, 245); padding: 10px; border: 1px solid rgb(221, 221, 221);">
|
|
152
|
+
</code>
|
|
153
|
+
<script>
|
|
154
|
+
// Example data array for
|
|
155
|
+
const data = /*[data_arr*/ [0, 1]
|
|
156
|
+
/*data_arr]*/;
|
|
157
|
+
|
|
158
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
159
|
+
// Display data array in paragraph (like Python's repr)
|
|
160
|
+
const dataDisplay = document.getElementById('data-display');
|
|
161
|
+
dataDisplay.textContent = JSON.stringify(data, null, 2);
|
|
162
|
+
});
|
|
163
|
+
</script>
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
outstr = render(
|
|
169
|
+
content,
|
|
170
|
+
{
|
|
171
|
+
"title": "Example rendered python object",
|
|
172
|
+
"content": "<p>Below python range rendered as javascript variable</p>",
|
|
173
|
+
"data_arr": repr(list(range(10))), # dont even need json here
|
|
174
|
+
# , 'none_existing' : '0'
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
file_name = os.path.join(os.getcwd(), "comnt_test.html")
|
|
178
|
+
with open(file_name, "w", encoding="utf8") as outfile:
|
|
179
|
+
outfile.write(outstr)
|
|
180
|
+
open_file(file_name)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def simple_example():
|
|
184
|
+
import json
|
|
185
|
+
import os
|
|
186
|
+
import subprocess
|
|
187
|
+
import sys
|
|
188
|
+
|
|
189
|
+
def open_file(filename):
|
|
190
|
+
if sys.platform.startswith("win"):
|
|
191
|
+
os.startfile(filename)
|
|
192
|
+
else:
|
|
193
|
+
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
|
194
|
+
subprocess.call([opener, filename])
|
|
195
|
+
|
|
196
|
+
content = """
|
|
197
|
+
<!DOCTYPE html>
|
|
198
|
+
<p id="title"> </p>
|
|
199
|
+
<div id="data_arr"></div>
|
|
200
|
+
<script>
|
|
201
|
+
const title = /*[title*/
|
|
202
|
+
"Example title"
|
|
203
|
+
/*title]*/;
|
|
204
|
+
const data = /*[data_arr*/
|
|
205
|
+
[0, 1]
|
|
206
|
+
/*data_arr]*/;
|
|
207
|
+
|
|
208
|
+
document.getElementById("data_arr").textContent = JSON.stringify(data);
|
|
209
|
+
document.getElementById("title").textContent = title;
|
|
210
|
+
</script>
|
|
211
|
+
"""
|
|
212
|
+
outstr = render(
|
|
213
|
+
content,
|
|
214
|
+
{
|
|
215
|
+
"title": json.dumps("Example rendered python object"),
|
|
216
|
+
"data_arr": json.dumps(list(range(10))),
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
file_name = os.path.join(os.getcwd(), "comnt_simple.html")
|
|
220
|
+
with open(file_name, "w", encoding="utf8") as outfile:
|
|
221
|
+
outfile.write(outstr)
|
|
222
|
+
open_file(file_name)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
# simple_example()
|
|
227
|
+
example()
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
|
|
6
|
+
integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<title>DataFrame</title>
|
|
9
|
+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
10
|
+
<link href="https://cdn.datatables.net/2.3.2/css/dataTables.dataTables.min.css" rel="stylesheet" />
|
|
11
|
+
<script src="https://cdn.datatables.net/2.3.2/js/dataTables.min.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
.dataTables_wrapper {
|
|
14
|
+
margin-top: 20px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
td.dt-nowrap {
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
table.dataTable.display tbody td {
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
table.dataTable.display tbody td span {
|
|
26
|
+
float: right;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dt-info {
|
|
30
|
+
padding: 1em;
|
|
31
|
+
font-size: 10pt;
|
|
32
|
+
color: gray;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
custom.select {
|
|
36
|
+
color: black;
|
|
37
|
+
max-width: 15.5em;
|
|
38
|
+
margin: 1px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
table.dataTable span.dtcc span.dtcc-button-icon {
|
|
42
|
+
color: #000ee7;
|
|
43
|
+
line-height: var(--dtcc-button-icon_size);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:root {
|
|
47
|
+
--dtcc-button_opacity: 0.3;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dtlogo {
|
|
51
|
+
opacity: 0.5;
|
|
52
|
+
width: auto;
|
|
53
|
+
display: block;
|
|
54
|
+
z-index: 2;
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 0px;
|
|
57
|
+
right: 0px;
|
|
58
|
+
margin: 1em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dt-info {
|
|
62
|
+
padding: 0;
|
|
63
|
+
font-size: 9pt;
|
|
64
|
+
color: gray;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
span.dt-column-title {
|
|
68
|
+
white-space: wrap;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
|
|
73
|
+
<body>
|
|
74
|
+
<!--[min_content-->
|
|
75
|
+
|
|
76
|
+
<div class="pure-g">
|
|
77
|
+
<div class="pure-u-1 pure-u-md-1-1">
|
|
78
|
+
<div id="tabcnt" style="width: fit-content; margin-left: 1em">
|
|
79
|
+
<p>
|
|
80
|
+
<!--[title-->
|
|
81
|
+
Example dataframe
|
|
82
|
+
<!--title]-->
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
<p id="testp"></p>
|
|
86
|
+
|
|
87
|
+
<table id="pd_datatab" class="display compact hover order-column" style="font-size: 10pt"></table>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<script type="text/javascript">
|
|
91
|
+
(function () {
|
|
92
|
+
function _anyNumber(a) {
|
|
93
|
+
var reg = /[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?/;
|
|
94
|
+
if (typeof a === "string") {
|
|
95
|
+
a = a.replace(",", ".").replace(" ", "").match(reg);
|
|
96
|
+
a = a !== null ? parseFloat(a[0]) : Number.POSITIVE_INFINITY;
|
|
97
|
+
}
|
|
98
|
+
return a;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
jQuery.extend(jQuery.fn.dataTableExt.oSort, {
|
|
102
|
+
"num-html-pre": function (a) {
|
|
103
|
+
var x = String(a).replace(/<[\s\S]*?>/g, "");
|
|
104
|
+
return parseFloat(_anyNumber(x));
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
"num-html-asc": function (a, b) {
|
|
108
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
"num-html-desc": function (a, b) {
|
|
112
|
+
return a < b ? 1 : a > b ? -1 : 0;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const render_num = (data, type) => {
|
|
116
|
+
const number = DataTable.render.number(" ", ",", 2).display(data);
|
|
117
|
+
|
|
118
|
+
if (type !== "display") return number;
|
|
119
|
+
|
|
120
|
+
const color = data < 0 ? "red" : "black";
|
|
121
|
+
return '<span style="color:' + color + '">' + number + "</span>";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const select_cols = /*[select_cols*/[2, 6];
|
|
125
|
+
/*select_cols]*/
|
|
126
|
+
const tab_data =
|
|
127
|
+
/*[tab_data*/
|
|
128
|
+
[
|
|
129
|
+
[
|
|
130
|
+
"2025-07-01T08:52:56.760930",
|
|
131
|
+
0.09,
|
|
132
|
+
"ZZ",
|
|
133
|
+
-0.33,
|
|
134
|
+
-1000,
|
|
135
|
+
"a",
|
|
136
|
+
"1",
|
|
137
|
+
-1,
|
|
138
|
+
"AA",
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing",
|
|
142
|
+
-0.59,
|
|
143
|
+
"BB",
|
|
144
|
+
1.0,
|
|
145
|
+
1,
|
|
146
|
+
"BB",
|
|
147
|
+
"0",
|
|
148
|
+
1,
|
|
149
|
+
"QQQ",
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
"<b>Integer</b> laoreet odio et.",
|
|
153
|
+
0.2,
|
|
154
|
+
"BB",
|
|
155
|
+
-9.0,
|
|
156
|
+
2,
|
|
157
|
+
"BB",
|
|
158
|
+
"1",
|
|
159
|
+
2,
|
|
160
|
+
"CC",
|
|
161
|
+
],
|
|
162
|
+
[NaN, -0.49, "CC", 4.0, 3, "CC", "0", 1, "0"],
|
|
163
|
+
[
|
|
164
|
+
" class 'datetime.datetime' ",
|
|
165
|
+
-0.18,
|
|
166
|
+
"CC",
|
|
167
|
+
2.0,
|
|
168
|
+
4,
|
|
169
|
+
"CC",
|
|
170
|
+
"1",
|
|
171
|
+
1,
|
|
172
|
+
"1",
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
" function sample_df. locals . lambda at 0x7a5c64431d00 ",
|
|
176
|
+
-0.8,
|
|
177
|
+
"ZZ",
|
|
178
|
+
3.0,
|
|
179
|
+
5,
|
|
180
|
+
"F",
|
|
181
|
+
"0",
|
|
182
|
+
0,
|
|
183
|
+
"0",
|
|
184
|
+
],
|
|
185
|
+
["C", -0.52, "ZZ", 1111.11, 70000, "X ", "0", 0, "0"],
|
|
186
|
+
];
|
|
187
|
+
/*tab_data]*/
|
|
188
|
+
|
|
189
|
+
const columns =
|
|
190
|
+
/*[tab_columns*/
|
|
191
|
+
[
|
|
192
|
+
{ title: "col1", searchable: true },
|
|
193
|
+
{
|
|
194
|
+
title: "col2",
|
|
195
|
+
searchable: true,
|
|
196
|
+
render: render_num,
|
|
197
|
+
type: "num-html",
|
|
198
|
+
},
|
|
199
|
+
{ title: "Long column title ", orderable: false },
|
|
200
|
+
{
|
|
201
|
+
title: "col4",
|
|
202
|
+
searchable: true,
|
|
203
|
+
render: render_num,
|
|
204
|
+
type: "num-html",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
title: "col5",
|
|
208
|
+
searchable: true,
|
|
209
|
+
render: render_num,
|
|
210
|
+
type: "num-html",
|
|
211
|
+
},
|
|
212
|
+
{ title: "col6", searchable: true },
|
|
213
|
+
{ title: "col7", orderable: false },
|
|
214
|
+
{ title: "col8", searchable: true },
|
|
215
|
+
{ title: "col9", searchable: true },
|
|
216
|
+
];
|
|
217
|
+
/*tab_columns]*/
|
|
218
|
+
|
|
219
|
+
const search_columns =
|
|
220
|
+
/*[search_columns*/
|
|
221
|
+
["col1", "col3", "col6", "col7", "col9"];
|
|
222
|
+
/*search_columns]*/
|
|
223
|
+
|
|
224
|
+
var cc_defs =
|
|
225
|
+
/*[column_control*/
|
|
226
|
+
[
|
|
227
|
+
{ targets: [2, 6], columnControl: [["searchList"]] },
|
|
228
|
+
{
|
|
229
|
+
targets: [0, 1, 3, 4, 5, 7, 8],
|
|
230
|
+
columnControl: ["order", "searchDropdown"],
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
/*column_control]*/
|
|
234
|
+
|
|
235
|
+
DataTable.defaults.layout = {
|
|
236
|
+
topStart: "info",
|
|
237
|
+
top1Start: "search",
|
|
238
|
+
topEnd: null,
|
|
239
|
+
bottomStart: null,
|
|
240
|
+
bottomEnd: null,
|
|
241
|
+
bottom: "paging",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const build_table = function (cc_defs) {
|
|
245
|
+
const table = $("#pd_datatab").DataTable({
|
|
246
|
+
data: tab_data,
|
|
247
|
+
autoWidth: /*[auto_width*/ true /*auto_width]*/,
|
|
248
|
+
columns: columns,
|
|
249
|
+
pageLength: 100,
|
|
250
|
+
responsive: true,
|
|
251
|
+
scrollX: false,
|
|
252
|
+
order: [],
|
|
253
|
+
columnDefs: cc_defs,
|
|
254
|
+
initComplete: function () {
|
|
255
|
+
const searchableColumns = search_columns;
|
|
256
|
+
const searchNote = $("<p>")
|
|
257
|
+
.css({
|
|
258
|
+
"margin-bottom": "10px",
|
|
259
|
+
"font-size": "0.7em",
|
|
260
|
+
color: "#666",
|
|
261
|
+
})
|
|
262
|
+
.text(
|
|
263
|
+
"Search is enabled for text columns: " +
|
|
264
|
+
searchableColumns.join(", "),
|
|
265
|
+
);
|
|
266
|
+
$("#pd_datatab_wrapper").prepend(searchNote);
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const loadScript = function (src) {
|
|
272
|
+
return new Promise(function (resolve, reject) {
|
|
273
|
+
var s;
|
|
274
|
+
s = document.createElement("script");
|
|
275
|
+
s.src = src;
|
|
276
|
+
s.onload = resolve;
|
|
277
|
+
s.onerror = reject;
|
|
278
|
+
document.head.appendChild(s);
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const loadStyle = function (url) {
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
let link = document.createElement("link");
|
|
285
|
+
link.type = "text/css";
|
|
286
|
+
link.rel = "stylesheet";
|
|
287
|
+
link.onload = () => {
|
|
288
|
+
resolve();
|
|
289
|
+
//~ console.log("style has loaded");
|
|
290
|
+
};
|
|
291
|
+
link.onerror = reject;
|
|
292
|
+
link.href = url;
|
|
293
|
+
|
|
294
|
+
let headScript = document.querySelector("script");
|
|
295
|
+
headScript.parentNode.insertBefore(link, headScript);
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const load_many = function (scripts_arr, callback) {
|
|
300
|
+
function load_next(idx) {
|
|
301
|
+
if (idx >= scripts_arr.length) {
|
|
302
|
+
if (callback) {
|
|
303
|
+
callback();
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
var c_url = scripts_arr[idx];
|
|
308
|
+
|
|
309
|
+
loadScript(c_url)
|
|
310
|
+
.then(
|
|
311
|
+
function () {
|
|
312
|
+
console.log(c_url);
|
|
313
|
+
load_next(++idx);
|
|
314
|
+
},
|
|
315
|
+
function () {
|
|
316
|
+
build_table();
|
|
317
|
+
$("#error_info").append(
|
|
318
|
+
"<em>!error column control not loaded </em>",
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
.catch(console.log("catch", c_url));
|
|
323
|
+
}
|
|
324
|
+
load_next(0);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const render_inline = /*[render_inline*/ true; /*render_inline]*/
|
|
328
|
+
|
|
329
|
+
const load_column_control = /*[load_column_control*/ true;
|
|
330
|
+
/*load_column_control]*/
|
|
331
|
+
|
|
332
|
+
const columncontrol_js = [
|
|
333
|
+
"https://cdn.datatables.net/columncontrol/1.0.6/js/dataTables.columnControl.js",
|
|
334
|
+
"https://cdn.datatables.net/columncontrol/1.0.6/js/columnControl.dataTables.js",
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const columncontrol_css =
|
|
338
|
+
"https://cdn.datatables.net/columncontrol/1.0.6/css/columnControl.dataTables.css";
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
$(document).ready(function () {
|
|
343
|
+
if (load_column_control) {
|
|
344
|
+
// Load CSS first
|
|
345
|
+
loadStyle(columncontrol_css).then(() => {
|
|
346
|
+
// Then load JavaScript files
|
|
347
|
+
load_many(columncontrol_js, function () {
|
|
348
|
+
// Finally build table wrapped in setTimeout
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
build_table(cc_defs);
|
|
351
|
+
}, 0);
|
|
352
|
+
});
|
|
353
|
+
}, () => {
|
|
354
|
+
$("#error_info").append("<em> style not loaded </em>");
|
|
355
|
+
});
|
|
356
|
+
} else {
|
|
357
|
+
build_table();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
})();
|
|
361
|
+
</script>
|
|
362
|
+
<!--min_content]-->
|
|
363
|
+
|
|
364
|
+
<figure class="dtlogo">
|
|
365
|
+
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/Datatables_logo_square.png" width="20"
|
|
366
|
+
style="display: block; margin-left: auto; margin-right: auto" />
|
|
367
|
+
<figcaption style="text-align: center">
|
|
368
|
+
<small style="font-size: 9px"><a target="blank" href="https://datatables.net">DataTables.net</a></small>
|
|
369
|
+
</figcaption>
|
|
370
|
+
</figure>
|
|
371
|
+
</div>
|
|
372
|
+
</body>
|
|
373
|
+
|
|
374
|
+
</html>
|
df2tables/df2tables.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding=utf8
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from functools import partial
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
TEMPLATE_FILE = "datatable_templ.html"
|
|
13
|
+
try:
|
|
14
|
+
# python 3.9+
|
|
15
|
+
from importlib import resources
|
|
16
|
+
|
|
17
|
+
TEMPLATE_PATH = str(resources.files("df2tables").joinpath(TEMPLATE_FILE))
|
|
18
|
+
except ImportError:
|
|
19
|
+
TEMPLATE_PATH = os.path.join(
|
|
20
|
+
os.path.dirname(os.path.abspath(__file__)), TEMPLATE_FILE
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .comnt import get_tag_content
|
|
25
|
+
from .comnt import render as c_render
|
|
26
|
+
except ImportError:
|
|
27
|
+
from comnt import get_tag_content
|
|
28
|
+
from comnt import render as c_render
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"TEMPLATE_PATH",
|
|
32
|
+
"render",
|
|
33
|
+
"render_inline",
|
|
34
|
+
"render_sample_df",
|
|
35
|
+
"get_sample_df",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def open_file(filename):
|
|
40
|
+
if sys.platform.startswith("win"):
|
|
41
|
+
os.startfile(filename)
|
|
42
|
+
else:
|
|
43
|
+
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
|
44
|
+
subprocess.call([opener, filename])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DataJSONEncoder(json.JSONEncoder):
|
|
48
|
+
"""Custom JSON encoder with fallback to string representation"""
|
|
49
|
+
|
|
50
|
+
def default(self, obj):
|
|
51
|
+
try:
|
|
52
|
+
obj_type = str(type(obj))
|
|
53
|
+
if "str" in obj_type:
|
|
54
|
+
return obj.strip()
|
|
55
|
+
elif "date" in obj_type or "Timestamp" in obj_type:
|
|
56
|
+
return obj.isoformat()
|
|
57
|
+
elif "int" in obj_type:
|
|
58
|
+
return int(obj)
|
|
59
|
+
elif "float" in obj_type:
|
|
60
|
+
return round(float(obj), 2)
|
|
61
|
+
elif "bool" in obj_type:
|
|
62
|
+
return bool(obj)
|
|
63
|
+
elif isinstance(obj, (np.void)):
|
|
64
|
+
return 0
|
|
65
|
+
return super().default(obj)
|
|
66
|
+
except BaseException:
|
|
67
|
+
print("json error: ", repr(obj), sys.exc_info()[1])
|
|
68
|
+
# Fallback to string representation for any problematic objects
|
|
69
|
+
return repr(obj).replace("<", " ").replace(">", " ")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def fix_df_columns(df):
|
|
73
|
+
for col in df.columns:
|
|
74
|
+
try:
|
|
75
|
+
test_val = (
|
|
76
|
+
df[col].dropna().values.tolist() if not df[col].isna().all() else None
|
|
77
|
+
)
|
|
78
|
+
json.dumps(test_val, cls=DataJSONEncoder)
|
|
79
|
+
except BaseException:
|
|
80
|
+
print(f"! column error: {col}", sys.exc_info())
|
|
81
|
+
df[col] = df[col].apply(lambda x: repr(x) if not pd.isna(x) else None)
|
|
82
|
+
return df
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render(
|
|
86
|
+
df,
|
|
87
|
+
title="Title",
|
|
88
|
+
precision=2,
|
|
89
|
+
num_html=[],
|
|
90
|
+
to_file=None,
|
|
91
|
+
startfile=True,
|
|
92
|
+
templ_path=TEMPLATE_PATH,
|
|
93
|
+
load_column_control=True,
|
|
94
|
+
# the maximum number of unique values in a column that qualifies it as categorical
|
|
95
|
+
# (and therefore eligible for a dropdown filter).
|
|
96
|
+
dropdown_select_threshold=5,
|
|
97
|
+
):
|
|
98
|
+
assert isinstance(df, pd.DataFrame)
|
|
99
|
+
assert isinstance(title, str)
|
|
100
|
+
assert isinstance(load_column_control, bool)
|
|
101
|
+
|
|
102
|
+
if "MultiIndex" in repr(df.columns): # experimental
|
|
103
|
+
df.columns = ["_".join(x) for x in df.columns]
|
|
104
|
+
|
|
105
|
+
missing_cols = set(num_html).difference(df.columns)
|
|
106
|
+
if missing_cols:
|
|
107
|
+
raise AssertionError(f"column(s): {missing_cols} not found in dataframe")
|
|
108
|
+
|
|
109
|
+
float_cols = df.select_dtypes(include=[np.float16, np.float32, np.float64])
|
|
110
|
+
str_cols = df.select_dtypes(include=["object", "string"]).columns
|
|
111
|
+
df.loc[:, float_cols.columns] = np.round(float_cols, precision)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
data_arrays = df.values.tolist()
|
|
115
|
+
data_json = json.dumps(data_arrays, cls=DataJSONEncoder)
|
|
116
|
+
except:
|
|
117
|
+
print(" json error", sys.exc_info())
|
|
118
|
+
df = fix_df_columns(df)
|
|
119
|
+
data_arrays = df.values.tolist()
|
|
120
|
+
data_json = json.dumps(data_arrays, cls=DataJSONEncoder)
|
|
121
|
+
|
|
122
|
+
columns, select_cols = [], []
|
|
123
|
+
for i, col in enumerate(df.columns):
|
|
124
|
+
try:
|
|
125
|
+
nunique = df[col].nunique()
|
|
126
|
+
except TypeError:
|
|
127
|
+
# for nested rows unhashable type ex: 'list'
|
|
128
|
+
df[col] = df[col].apply(lambda x: repr(x))
|
|
129
|
+
nunique = df[col].nunique()
|
|
130
|
+
|
|
131
|
+
if len(col) > 20 and '_' in col:
|
|
132
|
+
col = col.replace('_', ' ')
|
|
133
|
+
|
|
134
|
+
if nunique < dropdown_select_threshold:
|
|
135
|
+
select_cols.append(i) # columns when dropdown select makes sense
|
|
136
|
+
col_def = {"title": col, "orderable": True}
|
|
137
|
+
else:
|
|
138
|
+
col_def = {"title": col, "searchable": True, "orderable": True}
|
|
139
|
+
if col in num_html:
|
|
140
|
+
col_def["render"] = "#render_num"
|
|
141
|
+
col_def["type"] = "num-html"
|
|
142
|
+
columns.append(col_def)
|
|
143
|
+
column_control = [
|
|
144
|
+
{
|
|
145
|
+
"targets": select_cols,
|
|
146
|
+
"columnControl": [["order", "searchList"]],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"targets": list(set(range(len(df.columns))).difference(select_cols)),
|
|
150
|
+
"columnControl": ["order", "searchDropdown"],
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
columns_json = json.dumps(columns)
|
|
154
|
+
if num_html:
|
|
155
|
+
# we need properly refer to javascript function defined in template
|
|
156
|
+
# json must have string so get rid of the quotes
|
|
157
|
+
columns_json = columns_json.replace('"#render_num"', "render_num")
|
|
158
|
+
|
|
159
|
+
auto_width = True if len(df.index) < 100 else False
|
|
160
|
+
|
|
161
|
+
template_vars = {
|
|
162
|
+
"title": str(title),
|
|
163
|
+
"auto_width": json.dumps(auto_width),
|
|
164
|
+
"tab_data": data_json,
|
|
165
|
+
"tab_columns": columns_json,
|
|
166
|
+
"search_columns": json.dumps(list(str_cols)),
|
|
167
|
+
"select_cols": json.dumps(select_cols),
|
|
168
|
+
"column_control": json.dumps(column_control),
|
|
169
|
+
"load_column_control": json.dumps(load_column_control),
|
|
170
|
+
}
|
|
171
|
+
with open(templ_path, encoding="utf-8") as op_file:
|
|
172
|
+
instr = op_file.read()
|
|
173
|
+
html = c_render(instr, template_vars)
|
|
174
|
+
if not to_file:
|
|
175
|
+
return html
|
|
176
|
+
assert templ_path != to_file and templ_path not in to_file
|
|
177
|
+
with open(to_file, "w", encoding="utf8") as outfile:
|
|
178
|
+
outfile.write(html)
|
|
179
|
+
if startfile:
|
|
180
|
+
open_file(to_file)
|
|
181
|
+
return outfile
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
_render_str = partial(render, to_file=None)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def render_inline(df, **kwargs):
|
|
188
|
+
if "to_file" in kwargs:
|
|
189
|
+
print(
|
|
190
|
+
f"wrong argument:[to_file] {kwargs.pop('to_file')} is not allowed in render_inline"
|
|
191
|
+
)
|
|
192
|
+
# del kwargs['to_file']
|
|
193
|
+
html = _render_str(df, **kwargs)
|
|
194
|
+
min_content = c_render(
|
|
195
|
+
get_tag_content("min_content", html), {"render_inline": "true"}
|
|
196
|
+
)
|
|
197
|
+
return min_content
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_sample_df():
|
|
201
|
+
import datetime
|
|
202
|
+
import random
|
|
203
|
+
|
|
204
|
+
healthcare = ["Low priority", "Medium priority", "High priority", "Emergency"]
|
|
205
|
+
product = ["Premium", "Standard", "Budget"]
|
|
206
|
+
grades = ["A", "B", "C", "D", "F"]
|
|
207
|
+
return pd.DataFrame(
|
|
208
|
+
{
|
|
209
|
+
"col1": [
|
|
210
|
+
datetime.datetime.now(),
|
|
211
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing",
|
|
212
|
+
"<b>Integer</b> laoreet odio et.",
|
|
213
|
+
np.nan,
|
|
214
|
+
datetime.datetime,
|
|
215
|
+
0, # lambda x: 1 / x,
|
|
216
|
+
"C",
|
|
217
|
+
],
|
|
218
|
+
"col2": [0.09, -0.591, 0.201, -0.487, -0.175, -0.797, -0.519],
|
|
219
|
+
"Column 3": [random.choice(grades) for x in range(7)],
|
|
220
|
+
# "col3": [["ZZ","AA"], {'BB' : 1, 'BB' : 2}, "CC", "CC","CC", "ZZ", "ZZ"], #error rows
|
|
221
|
+
"col4": [-0.333, 1, -9, 4, 2, 3, 1111.111],
|
|
222
|
+
"col5": [-1000, 1, 2, 3, 4, 5, 70_000],
|
|
223
|
+
"col6": [random.choice(product) for x in range(7)],
|
|
224
|
+
"col7": ["1", "0", "1", "0", "1", "0", "0"],
|
|
225
|
+
"col8": [-1, 1, 2, 1, 1, 0, 0],
|
|
226
|
+
"col9": [random.choice(healthcare) for x in range(7)],
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def render_sample_df(to_file="df_table.html"):
|
|
232
|
+
df = get_sample_df()
|
|
233
|
+
result = render(
|
|
234
|
+
df,
|
|
235
|
+
to_file=to_file,
|
|
236
|
+
title="Example dataframe",
|
|
237
|
+
num_html=["col5", "col4", "col2"],
|
|
238
|
+
load_column_control=True,
|
|
239
|
+
dropdown_select_threshold=5,
|
|
240
|
+
)
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
def main():
|
|
244
|
+
print(render_sample_df(to_file="1test.html"))
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
main()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: df2tables
|
|
3
|
+
Version: 0.0.8
|
|
4
|
+
Summary: df2tables: Pandas DataFrames to Interactive DataTables
|
|
5
|
+
Project-URL: Homepage, https://github.com/ts-kontakt/df2tables
|
|
6
|
+
Project-URL: Issues, https://github.com/ts-kontakt/df2tables/issues
|
|
7
|
+
Author-email: Tomasz Sługocki <ts.kontakt@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENCE.txt
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# df2tables: Pandas DataFrames to Interactive DataTables
|
|
15
|
+
|
|
16
|
+
`df2tables` is a Python utility for exporting `pandas.DataFrame` objects to interactive HTML tables using [DataTables](https://datatables.net/)—an excellent JavaScript library for table functionality. It generates standalone `.html` files viewable in any browser without Jupyter notebooks, servers, or frameworks.
|
|
17
|
+
|
|
18
|
+
Useful for data inspection, feature engineering workflows, especially with large datasets that need interactive exploration.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install df2tables
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Screenshots
|
|
27
|
+
A standalone html file containing a js array as data source for datatables has several advantages, e.g. you can browse quite large datasets locally (something you don't usually do on a server).
|
|
28
|
+
The column control feature provides dropdown filters for categorical data and search functionality for text columns, enhancing data exploration capabilities through the excellent [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/).
|
|
29
|
+
(By default, filtering is enabled for all non-numeric columns)
|
|
30
|
+
|
|
31
|
+
Below is an example of 1 million rows with additional html rendering.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Converts `pandas.DataFrame` to interactive standalone HTML tables
|
|
38
|
+
- You can browse **quite large data sets** using filters and sorting
|
|
39
|
+
- **DataTables Column Control integration**: Smartly leverages the powerful [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) for automatic dropdown filters and advanced search functionality, loaded programmatically via JavaScript
|
|
40
|
+
- Self-contained HTML files with embedded data—no external dependencies at runtime
|
|
41
|
+
- Works independently of Jupyter or web servers—viewable offline in any browser, portable and easy to share
|
|
42
|
+
- Color-coded formatting for numeric columns
|
|
43
|
+
- **Useful for some training dataset inspection and feature engineering**: Quickly browse through large datasets, identify outliers, and data quality issues interactively
|
|
44
|
+
- **Minimal HTML snippet generation**: Generate embeddable HTML content for Flask or other web frameworks
|
|
45
|
+
- Easy customizable HTML
|
|
46
|
+
- **Smart column detection**: Automatically identifies categorical columns (≤5 unique values by default) for dropdown filtering
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import pandas as pd
|
|
52
|
+
import df2tables as df2t
|
|
53
|
+
|
|
54
|
+
df = pd.DataFrame({
|
|
55
|
+
"Name": ["Alice", "Bob", "Carol"],
|
|
56
|
+
"Score": [92.5, -78.3, 85.0],
|
|
57
|
+
"Joined": pd.to_datetime(["2021-01-05", "2021-02-10", "2021-03-15"])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
# Basic usage with color-coded numeric columns
|
|
61
|
+
df2t.render(
|
|
62
|
+
df,
|
|
63
|
+
title="User Scores",
|
|
64
|
+
precision=1,
|
|
65
|
+
num_html=["Score"],
|
|
66
|
+
to_file="output.html",
|
|
67
|
+
startfile=True
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Main Functions
|
|
72
|
+
|
|
73
|
+
### render
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
df2t.render(
|
|
77
|
+
df: pd.DataFrame,
|
|
78
|
+
title: str = "Title",
|
|
79
|
+
precision: int = 2,
|
|
80
|
+
num_html: List[str] = [],
|
|
81
|
+
to_file: Optional[str] = None,
|
|
82
|
+
startfile: bool = True,
|
|
83
|
+
templ_path: str = TEMPLATE_PATH,
|
|
84
|
+
load_column_control: bool = True,
|
|
85
|
+
dropdown_select_threshold: int = 5
|
|
86
|
+
) -> Union[str, file_object]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Parameters:**
|
|
90
|
+
- `df`: Input pandas DataFrame
|
|
91
|
+
- `title`: Title for the HTML table
|
|
92
|
+
- `precision`: Number of decimal places for floating-point numbers
|
|
93
|
+
- `num_html`: List of numeric column names to render with color-coded HTML formatting (negative values in red)
|
|
94
|
+
- `to_file`: Output HTML file path. If None, returns HTML string instead of writing file
|
|
95
|
+
- `startfile`: If True, automatically opens the generated HTML file in default browser
|
|
96
|
+
- `templ_path`: Path to custom HTML template (uses default if not specified)
|
|
97
|
+
- `load_column_control`: If True, smartly integrates the exceptional [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) programmatically for enhanced filtering and search capabilities (default: True)
|
|
98
|
+
- `dropdown_select_threshold`: Maximum number of unique values in a column to qualify for dropdown filtering (default: 5)
|
|
99
|
+
|
|
100
|
+
**Returns:**
|
|
101
|
+
- HTML string if `to_file=None`
|
|
102
|
+
- File object if `to_file` is specified
|
|
103
|
+
|
|
104
|
+
### render_inline
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
df2t.render_inline(
|
|
108
|
+
df: pd.DataFrame,
|
|
109
|
+
**kwargs
|
|
110
|
+
) -> str
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Generates minimal HTML content suitable for embedding in Flask or other web framework templates. This function:
|
|
114
|
+
- Returns only the table markup and JavaScript data bindings
|
|
115
|
+
- Excludes full HTML document structure (no `<html>`, `<head>`, `<body>` tags)
|
|
116
|
+
- **Important**: Does NOT automatically load jQuery or DataTables libraries - you must include these dependencies in your host page
|
|
117
|
+
- Perfect for embedding interactive data previews in existing web applications
|
|
118
|
+
|
|
119
|
+
**Parameters:**
|
|
120
|
+
- Same as `render()` except `to_file` is not allowed (always returns string)
|
|
121
|
+
|
|
122
|
+
### Column Name Formatting
|
|
123
|
+
|
|
124
|
+
For better readability in table headers, `df2tables` automatically converts underscores to spaces in column names when the column name is longer than 20 characters and contains underscores. This improves word wrapping and prevents excessively wide columns.
|
|
125
|
+
To disable this automatic word wrapping behavior, add the following CSS to your custom template:
|
|
126
|
+
```css
|
|
127
|
+
span.dt-column-title {
|
|
128
|
+
white-space: nowrap;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Web Framework Integration
|
|
133
|
+
|
|
134
|
+
The `render_inline()` function makes it easy to embed interactive DataTables in web applications. **Important**: You must include the required JavaScript libraries (jQuery, DataTables) in your host page as `render_inline()` does not automatically include them.
|
|
135
|
+
|
|
136
|
+
### Complete Flask Example
|
|
137
|
+
|
|
138
|
+
Here's a complete, **working** Flask application that demonstrates how to properly embed a DataTable with all required dependencies:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import df2tables as df2t
|
|
142
|
+
from flask import Flask, render_template_string
|
|
143
|
+
|
|
144
|
+
app = Flask(__name__)
|
|
145
|
+
|
|
146
|
+
@app.route("/")
|
|
147
|
+
def home():
|
|
148
|
+
# Generate sample data (or use your own DataFrame)
|
|
149
|
+
df = df2t.get_sample_df()
|
|
150
|
+
# For larger datasets, you might use:
|
|
151
|
+
# df = generate_large_dataframe(10000) # Your data source
|
|
152
|
+
|
|
153
|
+
df_title = "DataFrame Rendered as DataTable inline in <strong>Flask</strong>"
|
|
154
|
+
|
|
155
|
+
# Generate the embeddable DataTable HTML
|
|
156
|
+
string_datatable = df2t.render_inline(
|
|
157
|
+
df,
|
|
158
|
+
title=df_title,
|
|
159
|
+
dropdown_select_threshold=5,
|
|
160
|
+
load_column_control=True,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Embed in a complete HTML template with all required dependencies
|
|
164
|
+
return render_template_string(
|
|
165
|
+
"""
|
|
166
|
+
<!DOCTYPE html>
|
|
167
|
+
<html>
|
|
168
|
+
<head>
|
|
169
|
+
<title>Flask Data Dashboard</title>
|
|
170
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
171
|
+
|
|
172
|
+
<!-- Required: jQuery must be loaded first -->
|
|
173
|
+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
174
|
+
|
|
175
|
+
<!-- Required: DataTables CSS and JS -->
|
|
176
|
+
<link href="https://cdn.datatables.net/2.3.2/css/dataTables.dataTables.min.css" rel="stylesheet">
|
|
177
|
+
<script src="https://cdn.datatables.net/2.3.2/js/dataTables.min.js"></script>
|
|
178
|
+
|
|
179
|
+
<!-- Optional: PureCSS for styling -->
|
|
180
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css">
|
|
181
|
+
</head>
|
|
182
|
+
<body style="background-color: #f4f4f4;">
|
|
183
|
+
<div style="background-color: #fff; padding: 20px; margin: 20px;">
|
|
184
|
+
<h1>My Flask Data Dashboard</h1>
|
|
185
|
+
{{ inline_datatable | safe }}
|
|
186
|
+
</div>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|
|
189
|
+
""",
|
|
190
|
+
inline_datatable=string_datatable,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
app.run(debug=True)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Key points for web framework integration:**
|
|
198
|
+
|
|
199
|
+
- **Required dependencies**: Always include jQuery and DataTables CSS/JS in your host page
|
|
200
|
+
- **Column Control**: When `load_column_control=True`, the extension is loaded automatically by the generated JavaScript
|
|
201
|
+
- **Self-contained data**: The `render_inline()` function includes all table data and initialization code
|
|
202
|
+
- **Smart filtering**: Automatic dropdown filters for categorical columns
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
### DataTables Column Control Extension Integration
|
|
206
|
+
|
|
207
|
+
The `load_column_control` parameter enables smart integration with the remarkable [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/), bringing professional-grade filtering capabilities to your data tables:
|
|
208
|
+
|
|
209
|
+
- **Categorical columns** (≤`dropdown_select_threshold` unique values): Get elegant dropdown select filters (`searchList`) for intuitive data filtering
|
|
210
|
+
- **Text/numeric columns**: Benefit from sophisticated search functionality (`searchDropdown`) and ordering controls
|
|
211
|
+
- **Intelligent detection**: The module automatically identifies column types and applies the most appropriate Column Control features
|
|
212
|
+
- **Seamless loading**: The outstanding [Column Control extension](https://datatables.net/extensions/columncontrol/) is loaded dynamically via JavaScript, ensuring optimal performance and compatibility
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# Enable smart integration with DataTables Column Control extension (default)
|
|
216
|
+
df2t.render(df, load_column_control=True, to_file="enhanced_table.html")
|
|
217
|
+
|
|
218
|
+
# Disable Column Control for simpler tables
|
|
219
|
+
df2t.render(df, load_column_control=False, to_file="simple_table.html")
|
|
220
|
+
|
|
221
|
+
# Customize dropdown threshold
|
|
222
|
+
df2t.render(df, dropdown_select_threshold=10, to_file="custom_table.html")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### get_sample_df / render_sample_df
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# Get sample DataFrame for testing
|
|
229
|
+
sample_df = df2t.get_sample_df()
|
|
230
|
+
|
|
231
|
+
# Generate and render sample DataFrame
|
|
232
|
+
html_string = df2t.render_sample_df(to_file="sample_table.html")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Fast Dataset Browsing
|
|
236
|
+
|
|
237
|
+
One of the key strengths of `df2tables` is its ability to quickly generate interactive HTML tables for rapid dataset exploration. The combination of standalone HTML files and the [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) makes it exceptionally fast to browse through multiple datasets.
|
|
238
|
+
|
|
239
|
+
### Bulk Dataset Processing
|
|
240
|
+
|
|
241
|
+
For exploratory data analysis across multiple datasets, you can generate tables programmatically. The example below uses the [vega_datasets](https://github.com/altair-viz/vega_datasets) package, which provides easy access to a variety of sample datasets commonly used in data visualization and analysis.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
**Note**: Install vega_datasets with `pip install vega_datasets` to run this example.
|
|
246
|
+
### Quick browse first 10 vega datasets
|
|
247
|
+
```python
|
|
248
|
+
import df2tables as df2t
|
|
249
|
+
from vega_datasets import data
|
|
250
|
+
|
|
251
|
+
# WARNING: This will open many browser tabs! Use with caution.
|
|
252
|
+
# Consider setting startfile=False for bulk processing.
|
|
253
|
+
|
|
254
|
+
for dataset_name in (sorted(dir(data))[:10]):
|
|
255
|
+
dataset_func = getattr(data, dataset_name)
|
|
256
|
+
try:
|
|
257
|
+
df = dataset_func()
|
|
258
|
+
print(f"{dataset_name}: {len(df.index)} rows")
|
|
259
|
+
|
|
260
|
+
# df2tables can handle datasets above 100k rows, but we limit to smaller datasets
|
|
261
|
+
# for this demo to avoid generating too many large files
|
|
262
|
+
if len(df.index) < 100_000:
|
|
263
|
+
df2t.render(
|
|
264
|
+
df,
|
|
265
|
+
title=f'Dataset: {dataset_name}',
|
|
266
|
+
to_file=f'{dataset_name}.html',
|
|
267
|
+
startfile=True
|
|
268
|
+
)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
print(f'Error processing {dataset_name}: {e}')
|
|
271
|
+
|
|
272
|
+
print("Generated HTML files. Open them manually to browse datasets.")
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**⚠️ Important Note**: When `startfile=True` (default), each generated HTML file opens automatically in your default browser. For bulk processing, set `startfile=False` to avoid opening dozens of browser tabs simultaneously.
|
|
276
|
+
|
|
277
|
+
### Benefits for Fast Browsing
|
|
278
|
+
|
|
279
|
+
- **Instant loading**: HTML files with embedded data load immediately without server dependencies
|
|
280
|
+
- **Interactive filtering**: The [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) enables quick data exploration
|
|
281
|
+
- **Offline browsing**: Generated files work completely offline
|
|
282
|
+
- **Portable**: Share HTML files easily with colleagues for collaborative data exploration
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
## Requirements
|
|
286
|
+
|
|
287
|
+
- Python 3.7+
|
|
288
|
+
- pandas
|
|
289
|
+
- numpy
|
|
290
|
+
|
|
291
|
+
## Technical Details
|
|
292
|
+
|
|
293
|
+
### DataTables Column Control Extension Integration
|
|
294
|
+
|
|
295
|
+
The module smartly integrates with the exceptional [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) for optimal user experience:
|
|
296
|
+
|
|
297
|
+
- **Select columns**: Columns with ≤`dropdown_select_threshold` unique values get sophisticated dropdown filters (`searchList`) via Column Control
|
|
298
|
+
- **Search columns**: Other columns benefit from Column Control's advanced search functionality (`searchDropdown`) and ordering controls
|
|
299
|
+
- **Dynamic loading**: The [Column Control extension](https://datatables.net/extensions/columncontrol/) JavaScript libraries are loaded programmatically to maintain clean templates
|
|
300
|
+
- **Robust fallback**: If the Column Control extension cannot be loaded, tables gracefully fall back to standard DataTables functionality
|
|
301
|
+
|
|
302
|
+
### Error Handling
|
|
303
|
+
|
|
304
|
+
The module includes error handling for:
|
|
305
|
+
- **JSON serialization**: Custom encoder handles complex pandas data types
|
|
306
|
+
- **Column compatibility**: Automatically converts problematic column types to string representation
|
|
307
|
+
- **Missing columns**: Validates `num_html` column names against DataFrame columns
|
|
308
|
+
- **Script loading**: Graceful fallback if the [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) cannot be loaded
|
|
309
|
+
|
|
310
|
+
## TODO / Future Enhancements
|
|
311
|
+
|
|
312
|
+
### DataTables Configuration Expansion
|
|
313
|
+
|
|
314
|
+
Currently, `df2tables` uses a predefined set of DataTables configuration options. Future versions could expose more DataTables initialization parameters directly from Python:
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT License
|
|
320
|
+
© Tomasz Sługocki
|
|
321
|
+
|
|
322
|
+
## Appendix: Template Customization
|
|
323
|
+
|
|
324
|
+
### Offline Usage
|
|
325
|
+
*Note: "Offline" viewing assumes internet connectivity for CDN resources (DataTables, jQuery, PureCSS, [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/)). For truly offline usage, modify the template to reference local copies of these libraries instead of CDN links.*
|
|
326
|
+
|
|
327
|
+
Templates use [comnt](https://github.com/ts-kontakt/comnt), a minimal markup system based on HTML/JS comments.
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<!--[title-->
|
|
331
|
+
My Table Title
|
|
332
|
+
<!--title]-->
|
|
333
|
+
|
|
334
|
+
const data = /*[tab_data*/ [...] /*tab_data]*/;
|
|
335
|
+
```
|
|
336
|
+
The default HTML template includes:
|
|
337
|
+
- **PureCSS** (CDN) for responsive styling
|
|
338
|
+
- **DataTables 2.3.2** (CDN) for table interactivity
|
|
339
|
+
- **jQuery 3.7.1** (CDN)
|
|
340
|
+
- **[DataTables Column Control Extension](https://datatables.net/extensions/columncontrol/)** (CDN) - the outstanding Column Control extension loaded programmatically when enabled
|
|
341
|
+
- JavaScript enhancements for sorting HTML-formatted numbers and coloring negative values
|
|
342
|
+
|
|
343
|
+
### DataTables Column Control Extension CDN Resources
|
|
344
|
+
|
|
345
|
+
When `load_column_control=True`, the following resources from the excellent [DataTables Column Control extension](https://datatables.net/extensions/columncontrol/) are loaded dynamically:
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
// JavaScript libraries loaded programmatically
|
|
349
|
+
const columncontrol_js = [
|
|
350
|
+
"https://cdn.datatables.net/columncontrol/1.0.6/js/dataTables.columnControl.js",
|
|
351
|
+
"https://cdn.datatables.net/columncontrol/1.0.6/js/columnControl.dataTables.js"
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
// CSS loaded after JavaScript initialization
|
|
355
|
+
const columncontrol_css =
|
|
356
|
+
"https://cdn.datatables.net/columncontrol/1.0.6/css/columnControl.dataTables.css";
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
While [comnt](https://github.com/ts-kontakt/comnt) is used to ensure that the HTML template just works independently (and avoid Json.parse), you can also use other templating systems like Jinja2 by rendering the final content after.
|
|
360
|
+
|
|
361
|
+
### Custom Templates
|
|
362
|
+
|
|
363
|
+
Copy and modify `datatable_templ.html` to apply custom styling or libraries, then pass the new template path to `templ_path`.
|
|
364
|
+
|
|
365
|
+
### Customization
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
# Return HTML string for further processing
|
|
369
|
+
html_content = df2t.render(df, to_file=None)
|
|
370
|
+
|
|
371
|
+
# Generate minimal HTML for embedding (requires jQuery/DataTables in host page)
|
|
372
|
+
html_snippet = df2t.render_inline(df, title="Embedded Table")
|
|
373
|
+
|
|
374
|
+
# Use custom template
|
|
375
|
+
df2t.render(
|
|
376
|
+
df,
|
|
377
|
+
to_file="custom_output.html",
|
|
378
|
+
templ_path="my_custom_template.html"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Disable DataTables Column Control extension for custom implementations
|
|
382
|
+
df2t.render(
|
|
383
|
+
df,
|
|
384
|
+
to_file="basic_table.html",
|
|
385
|
+
load_column_control=False
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Adjust dropdown threshold for categorical columns
|
|
389
|
+
df2t.render(
|
|
390
|
+
df,
|
|
391
|
+
dropdown_select_threshold=10, # Columns with ≤10 unique values get dropdowns
|
|
392
|
+
to_file="custom_filtering.html"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Handle MultiIndex columns (experimental)
|
|
396
|
+
# MultiIndex columns are automatically flattened with underscore separation
|
|
397
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
df2tables/__init__.py,sha256=Ry5LO4692ZzOnP1mf7Z8VU4FWXx_h2e6vKwue1jEFGg,74
|
|
2
|
+
df2tables/comnt.py,sha256=BeVQLMnCVhX39nP3cfMPrIe85bQV8zRTg0pNFw2TFJ0,6931
|
|
3
|
+
df2tables/datatable_templ.html,sha256=ERpNwSS9RRRltTcPhtJFIC1Z-6aPALg4FPs-SlSuCbk,10384
|
|
4
|
+
df2tables/df2tables.py,sha256=kxlvHmh1E5YcuEd-52ZEmZKv-G5fdIYwqRdJHjGnt-w,7891
|
|
5
|
+
df2tables-0.0.8.dist-info/METADATA,sha256=S-hT_dIU_2z0WgaPPFU8I2r1Oq3V1v_dHHLaMOKEDVc,17008
|
|
6
|
+
df2tables-0.0.8.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
7
|
+
df2tables-0.0.8.dist-info/licenses/LICENCE.txt,sha256=ACwmltkrXIz5VsEQcrqljq-fat6ZXAMepjXGoe40KtE,1069
|
|
8
|
+
df2tables-0.0.8.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|