df2tables 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -0,0 +1 @@
1
+ include df2tables/datatable_templ.html
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: df2tables
3
+ Version: 0.0.1
4
+ Summary: dftables creation
5
+ Author-email: Tomasz Sługocki <ts.kontakt@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ts-kontakt/df2tables
8
+ Project-URL: Issues, https://github.com/ts-kontakt/df2tables/issues
9
+ License-File: LICENCE.txt
10
+ Requires-Dist: pandas
11
+ Requires-Dist: numpy
12
+ Dynamic: license-file
@@ -0,0 +1,178 @@
1
+ # df2tables: Pandas DataFrames to interactive DataTables
2
+
3
+ `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.
4
+
5
+ Useful for data inspection, feature engineering workflows, especially with large datasets that need interactive exploration.
6
+
7
+ ## Features
8
+
9
+ - Converts `pandas.DataFrame` to **interactive** standalone HTML tables
10
+ - Self-contained HTML files with embedded data—no external dependencies at runtime
11
+ - Works independently of Jupyter or web servers—viewable offline in any browser, portable and easy to share
12
+ - Color-coded formatting for numeric columns with customizable precision
13
+ - Easy customizable HTML (minimal template system using [comnt](https://github.com/ts-kontakt/comnt) included)
14
+ - **Useful for some training dataset inspection and feature engineering**: Quickly browse through large datasets, identify outliers, and data quality issues interactively
15
+
16
+ ## Screenshots
17
+ A standalone html file containing a js array as data source for datatables has several advantages, e.g. you can browse quite large data locally (something you don't usually do on a server).
18
+ Below is an example of 100k rows with additional html rendering.
19
+
20
+ ![](https://github.com/ts-kontakt/df2tables/raw/main/df2tables-big.gif)
21
+
22
+ ## Quick Start
23
+
24
+ ```python
25
+ import pandas as pd
26
+ import df2tables as df2t
27
+
28
+ df = pd.DataFrame({
29
+ "Name": ["Alice", "Bob", "Carol"],
30
+ "Score": [92.5, -78.3, 85.0],
31
+ "Joined": pd.to_datetime(["2021-01-05", "2021-02-10", "2021-03-15"])
32
+ })
33
+
34
+ # Basic usage with color-coded numeric columns
35
+ df2t.render(
36
+ df,
37
+ title="User Scores",
38
+ precision=1,
39
+ num_html=["Score"],
40
+ to_file="output.html",
41
+ startfile=True
42
+ )
43
+ ```
44
+
45
+ ## Main Function
46
+
47
+ ### render
48
+
49
+ ```python
50
+ df2t.render(
51
+ df: pd.DataFrame,
52
+ title: str = "Title",
53
+ precision: int = 2,
54
+ num_html: List[str] = [],
55
+ to_file: Optional[str] = None,
56
+ startfile: bool = True,
57
+ templ_path: str = TEMPLATE_PATH
58
+ ) -> Union[str, file_object]
59
+ ```
60
+
61
+ **Parameters:**
62
+ - `df`: Input pandas DataFrame
63
+ - `title`: Title for the HTML table
64
+ - `precision`: Number of decimal places for floating-point numbers
65
+ - `num_html`: List of numeric column names to render with color-coded HTML formatting (negative values in red)
66
+ - `to_file`: Output HTML file path. If None, returns HTML string instead of writing file
67
+ - `startfile`: If True, automatically opens the generated HTML file in default browser
68
+ - `templ_path`: Path to custom HTML template (uses default if not specified)
69
+
70
+ **Returns:**
71
+ - HTML string if `to_file=None`
72
+ - File object if `to_file` is specified
73
+
74
+ ### sample_df
75
+
76
+ Generates and renders a built-in example DataFrame for testing:
77
+
78
+ ```python
79
+ html_string = df2t.sample_df()
80
+ ```
81
+
82
+ ## Feature Engineering Example
83
+
84
+ ```python
85
+ import pandas as pd
86
+ import df2tables as df2t
87
+
88
+ # Load your training dataset
89
+ df = pd.read_csv("training_data.csv")
90
+
91
+ # Quick inspection of the entire dataset
92
+ df2t.render(
93
+ df,
94
+ title="Training Dataset Overview",
95
+ to_file="dataset_overview.html"
96
+ )
97
+
98
+ # Focus on specific numeric features with color coding
99
+ numeric_features = ["feature1", "feature2", "target_variable"]
100
+ df2t.render(
101
+ df[numeric_features + ["id", "category"]],
102
+ title="Key Numeric Features",
103
+ precision=3,
104
+ num_html=numeric_features,
105
+ to_file="numeric_features.html"
106
+ )
107
+
108
+ # Inspect feature correlations or engineered features
109
+ feature_stats = df.describe().T
110
+ df2t.render(
111
+ feature_stats,
112
+ title="Feature Statistics",
113
+ precision=4,
114
+ num_html=["mean", "std", "min", "max"],
115
+ to_file="feature_stats.html"
116
+ )
117
+ ```
118
+
119
+ ## Requirements
120
+
121
+ - Python 3.7+
122
+ - pandas
123
+ - numpy
124
+
125
+ ## Installation
126
+
127
+ ```bash
128
+ pip install df2tables
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT License
134
+ © ts-kontakt
135
+
136
+ ## Appendix: Template Customization
137
+
138
+ ### Offline Usage
139
+ *Note: "Offline" viewing assumes internet connectivity for CDN resources (DataTables, jQuery, PureCSS). For truly offline usage, modify the template to reference local copies of these libraries instead of CDN links.*
140
+
141
+ Templates use [comnt](https://github.com/ts-kontakt/comnt), a minimal markup system based on HTML/JS comments.
142
+
143
+ ```html
144
+ <!--[title-->
145
+ My Table Title
146
+ <!--title]-->
147
+
148
+ const data = /*[tab_data*/ [...] /*tab_data]*/;
149
+ ```
150
+ The default HTML template includes:
151
+ - **PureCSS** (CDN) for responsive styling
152
+ - **DataTables 2.3.2** (CDN) for table interactivity
153
+ - **jQuery 3.7.1** (CDN)
154
+ - JavaScript enhancements for sorting HTML-formatted numbers and coloring negative values
155
+
156
+
157
+ While [comnt](https://github.com/ts-kontakt/comnt) is used to ensure that the HTML template just works independently, you can also use other templating systems like Jinja2 by rendering the final content after.
158
+
159
+ ### Custom Templates
160
+
161
+ Copy and modify `datatable_templ.html` to apply custom styling or libraries, then pass the new template path to `templ_path`.
162
+
163
+ ### Customization
164
+
165
+ ```python
166
+ # Return HTML string for further processing
167
+ html_content = df2t.render(df, to_file=None)
168
+
169
+ # Use custom template
170
+ df2t.render(
171
+ df,
172
+ to_file="custom_output.html",
173
+ templ_path="my_custom_template.html"
174
+ )
175
+
176
+ # Handle MultiIndex columns (experimental)
177
+ # MultiIndex columns are automatically flattened with underscore separation
178
+ ```
@@ -0,0 +1,2 @@
1
+ from .df2tables import *
2
+ # from .comnt import render, write_from_template
@@ -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(instr, tag):
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,192 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <link
5
+ rel="stylesheet"
6
+ href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
7
+ integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls"
8
+ crossorigin="anonymous"
9
+ />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
11
+ <title>DataFrame</title>
12
+ <script
13
+ src="https://code.jquery.com/jquery-3.7.1.min.js"
14
+ ></script>
15
+ <link
16
+ href="https://cdn.datatables.net/2.3.2/css/dataTables.dataTables.min.css"
17
+ rel="stylesheet"
18
+ />
19
+ <script src="https://cdn.datatables.net/2.3.2/js/dataTables.min.js"></script>
20
+ <style>
21
+ <style > .dataTables_wrapper {
22
+ margin-top: 20px;
23
+ }
24
+ td.dt-nowrap {
25
+ white-space: nowrap;
26
+ }
27
+ table.dataTable.display tbody td {
28
+ white-space: nowrap;
29
+ }
30
+ table.dataTable.display tbody td span {
31
+
32
+ float: right;
33
+ }
34
+ .dt-info {
35
+ padding: 1em;
36
+ font-size: 10pt;
37
+ color: gray;
38
+ }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <!--[min_content-->
43
+ <div class="pure-g">
44
+ <div class="pure-u-23-24">
45
+ <div id="tabcnt" style="width: fit-content; margin-left: 1em">
46
+
47
+ <!-- <div id="tabcnt" style="width: 800px; margin-left: 1em"> -->
48
+ <p>
49
+ <!--[title-->
50
+ Example datatable
51
+ <!--title]-->
52
+ </p>
53
+
54
+ <table
55
+ id="pd_datatab"
56
+ class="display compact hover"
57
+ style="font-size: 10pt"
58
+ ></table>
59
+ </div>
60
+ </div>
61
+ <script type="text/javascript">
62
+ function _anyNumber(a) {
63
+ var reg = /[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?/;
64
+ if (typeof a === "string") {
65
+ a = a.replace(",", ".").replace(" ", "").match(reg);
66
+ a = a !== null ? parseFloat(a[0]) : Number.POSITIVE_INFINITY;
67
+ }
68
+ return a;
69
+ }
70
+
71
+ jQuery.extend(jQuery.fn.dataTableExt.oSort, {
72
+ "num-html-pre": function (a) {
73
+ var x = String(a).replace(/<[\s\S]*?>/g, "");
74
+ return parseFloat(_anyNumber(x));
75
+ },
76
+
77
+ "num-html-asc": function (a, b) {
78
+ return a < b ? -1 : a > b ? 1 : 0;
79
+ },
80
+
81
+ "num-html-desc": function (a, b) {
82
+ return a < b ? 1 : a > b ? -1 : 0;
83
+ },
84
+ });
85
+ const render_num = (data, type) => {
86
+ const number = DataTable.render.number(" ", ",", 2).display(data);
87
+
88
+ if (type !== "display") return number;
89
+
90
+ const color = data < 0 ? "red" : "black";
91
+ return `<span style="color:${color}">${number}</span>`;
92
+ };
93
+ $(document).ready(function () {
94
+ const table = $("#pd_datatab").DataTable({
95
+ data: data,
96
+ autoWidth: true,
97
+ columns: columns,
98
+ pageLength: 100,
99
+ responsive: true,
100
+ scrollX: true,
101
+ layout: {
102
+ topStart: {
103
+ search: {
104
+ placeholder: "Search",
105
+ },
106
+ },
107
+ topEnd: "info",
108
+ },
109
+ order: [],
110
+ language: {
111
+ search: "Filter:",
112
+ },
113
+ initComplete: function () {
114
+ // Add a note about searchable columns
115
+ const searchableColumns = search_columns;
116
+ const searchNote = $("<p>")
117
+ .css({
118
+ "margin-bottom": "10px",
119
+ "font-size": "0.7em",
120
+ color: "#666",
121
+ })
122
+ .text(
123
+ "Search is enabled for text columns: " +
124
+ searchableColumns.join(", "),
125
+ );
126
+
127
+ $("#pd_datatab_wrapper").prepend(searchNote);
128
+ },
129
+ });
130
+ });
131
+
132
+ const data =
133
+ /*[tab_data*/
134
+ [
135
+ ["2025-06-22T10:26:53.635125", 0.09, 2.11, -0.33, -1000, "a"],
136
+ [
137
+ "Lorem ipsum dolor sit amet, consectetur adipiscing",
138
+ -0.59,
139
+ 1,
140
+ 1.0,
141
+ 1,
142
+ "B",
143
+ ],
144
+ ["<b>Integer</b> laoreet odio et.", 0.2, 9, -9.0, 2, "c"],
145
+ [NaN, -0.49, 8, 4.0, 3, "D"],
146
+ [" class 'datetime.datetime' ", -0.18, 7, 2.0, 4, "e"],
147
+ [
148
+ " function simple. locals . lambda at 0x7847f6b207c0 ",
149
+ -0.8,
150
+ 4,
151
+ 3.0,
152
+ 5,
153
+ "F",
154
+ ],
155
+ ["C", -0.52, true, 1111.11, 70000, "X "],
156
+ ];
157
+ /*tab_data]*/
158
+
159
+ const columns =
160
+ /*[tab_columns*/
161
+ [
162
+ { title: "col1", searchable: true },
163
+ {
164
+ title: "col2",
165
+ searchable: false,
166
+ render: render_num,
167
+ type: "num-html",
168
+ },
169
+ { title: "col3", searchable: true },
170
+ {
171
+ title: "col4",
172
+ searchable: false,
173
+ render: render_num,
174
+ type: "num-html",
175
+ },
176
+ {
177
+ title: "col5",
178
+ searchable: false,
179
+ render: render_num,
180
+ type: "num-html",
181
+ },
182
+ { title: "col6", searchable: true },
183
+ ];
184
+ /*tab_columns]*/
185
+
186
+ const search_columns = /*[search_columns*/ ["col1"];
187
+ /*search_columns]*/
188
+ </script>
189
+ </div>
190
+ <!--min_content]-->
191
+ </body>
192
+ </html>
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/python
2
+ # coding=utf8
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ TEMPLATE_FILE = "datatable_templ.html"
12
+ try:
13
+ # python 3.9+
14
+ from importlib import resources
15
+ TEMPLATE_PATH = str(resources.files("df2tables").joinpath(TEMPLATE_FILE))
16
+ except ImportError:
17
+ TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), TEMPLATE_FILE)
18
+
19
+ try:
20
+ from .comnt import render as c_render
21
+ except ImportError:
22
+ from comnt import render as c_render
23
+
24
+ __all__ = ["TEMPLATE_PATH", "sample_df", "render"]
25
+
26
+
27
+ def open_file(filename):
28
+ if sys.platform.startswith("win"):
29
+ os.startfile(filename)
30
+ else:
31
+ opener = "open" if sys.platform == "darwin" else "xdg-open"
32
+ subprocess.call([opener, filename])
33
+
34
+
35
+ class DataJSONEncoder(json.JSONEncoder):
36
+ """Custom JSON encoder with fallback to string representation"""
37
+
38
+ def default(self, obj):
39
+ try:
40
+ obj_type = str(type(obj))
41
+ if "str" in obj_type:
42
+ return obj.strip()
43
+ elif "date" in obj_type:
44
+ return obj.isoformat()
45
+ elif "int" in obj_type:
46
+ return int(obj)
47
+ elif "float" in obj_type:
48
+ return round(float(obj), 2)
49
+ elif "bool" in obj_type:
50
+ return bool(obj)
51
+ elif isinstance(obj, (np.void)):
52
+ return 0
53
+ return super().default(obj)
54
+ except BaseException:
55
+ print("json error: ", repr(obj), sys.exc_info()[1])
56
+ # Fallback to string representation for any problematic objects
57
+ return repr(obj).replace("<", " ").replace(">", " ")
58
+
59
+
60
+ def render(df,
61
+ title="Title",
62
+ precision=2,
63
+ num_html=[],
64
+ to_file=None,
65
+ startfile=True,
66
+ templ_path=TEMPLATE_PATH):
67
+
68
+ if "MultiIndex" in repr(df.columns): # experimental
69
+ df.columns = ["_".join(x) for x in df.columns]
70
+
71
+ assert isinstance(df, pd.DataFrame)
72
+
73
+ missing_cols = set(num_html).difference(df.columns)
74
+ if missing_cols:
75
+ raise AssertionError(f"column(s): {missing_cols} not found in dataframe")
76
+
77
+ for col in df.columns:
78
+ try:
79
+ test_val = df[col].dropna().iloc[0] if not df[col].isna().all() else None
80
+ json.dumps(test_val, cls=DataJSONEncoder)
81
+ except BaseException:
82
+ print(f"! column error: {col}", sys.exc_info())
83
+ df[col] = df[col].apply(lambda x: repr(x) if not pd.isna(x) else None)
84
+
85
+ float_cols = df.select_dtypes(include=[np.float16, np.float32, np.float64])
86
+
87
+ df.loc[:, float_cols.columns] = np.round(float_cols, precision)
88
+ data_arrays = df.values.tolist()
89
+ data_json = json.dumps(data_arrays, cls=DataJSONEncoder)
90
+
91
+ # Get string column indices
92
+ str_cols = df.select_dtypes(include=["object", "string"]).columns
93
+ str_col_indices = [list(df.columns).index(col) for col in str_cols]
94
+
95
+ # Get column names and create column definitions for DataTable
96
+ columns = []
97
+ for i, col in enumerate(df.columns):
98
+ is_text = i in str_col_indices
99
+ col_def = {"title": col, "searchable": is_text}
100
+ if col in num_html:
101
+ col_def["render"] = "#render_num"
102
+ col_def["type"] = "num-html"
103
+ columns.append(col_def)
104
+
105
+ columns_json = json.dumps(columns)
106
+ if num_html:
107
+ # we need properly refer to javascript function - json can have string so get rid of the quotes
108
+ columns_json = columns_json.replace('"#render_num"', "render_num")
109
+
110
+ search_cols_json = json.dumps(list(str_cols))
111
+
112
+ template_vars = {
113
+ "title": title,
114
+ "tab_data": data_json,
115
+ "tab_columns": columns_json,
116
+ "search_columns": search_cols_json,
117
+ }
118
+ with open(templ_path, encoding="utf-8") as op_file:
119
+ instr = op_file.read()
120
+ html = c_render(instr, template_vars)
121
+ if not to_file:
122
+ return html
123
+ else:
124
+ assert templ_path != to_file and templ_path not in to_file
125
+ with open(to_file, "w", encoding="utf8") as outfile:
126
+ outfile.write(html)
127
+ if startfile:
128
+ open_file(to_file)
129
+ return outfile
130
+
131
+
132
+ def sample_df():
133
+ import datetime
134
+
135
+ df = pd.DataFrame({
136
+ "col1": [
137
+ datetime.datetime.now(),
138
+ "Lorem ipsum dolor sit amet, consectetur adipiscing",
139
+ "<b>Integer</b> laoreet odio et.",
140
+ np.nan,
141
+ datetime.datetime,
142
+ lambda x: 1 / x,
143
+ "C",
144
+ ],
145
+ "col2": [0.09, -0.591, 0.201, -0.487, -0.175, -0.797, -0.519],
146
+ "col3": [2.11, 1, 9, 8, 7, 4, True],
147
+ "col4": [-0.333, 1, -9, 4, 2, 3, 1111.111],
148
+ "col5": [-1000, 1, 2, 3, 4, 5, 70_000],
149
+ "col6": ["a", "B", "c", "D", "e", "F", "X "],
150
+ })
151
+
152
+ outfile = "df_table.html"
153
+ result = render(df,
154
+ to_file=None,
155
+ title="Example dataframe",
156
+ num_html=["col5", "col4", "col2"])
157
+ return result
158
+
159
+
160
+ if __name__ == "__main__":
161
+ sample_df()
@@ -0,0 +1,112 @@
1
+
2
+
3
+
4
+
5
+ import random
6
+ import string
7
+ from datetime import datetime, timedelta
8
+
9
+ import matplotlib
10
+ import matplotlib.colors as mcolors
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ import df2tables as df2dtb
15
+
16
+
17
+ def random_data(num_rows=100):
18
+
19
+ def get_rdylgn_colors(num_colors=100):
20
+ cmap = matplotlib.colormaps["YlGnBu"]
21
+ color_indices = np.linspace(0, 1, num_colors)
22
+ colors = []
23
+ for i in color_indices:
24
+ rgba_color = cmap(i)
25
+ hex_color = mcolors.rgb2hex(rgba_color[:3])
26
+ colors.append(
27
+ f"<div style='font-family: monospace;background-color:{hex_color}'>{hex_color}</div>"
28
+ )
29
+ return colors
30
+
31
+ unicode_ranges = [
32
+ (0x0020, 0x007E), # Basic Latin (printable ASCII)
33
+ (0x00A0, 0x00FF), # Latin-1 Supplement (e.g., accented characters)
34
+ (0x0100, 0x017F), # Latin Extended-A (more European characters)
35
+ ]
36
+
37
+ def gen_datetime(min_year=1990, max_year=datetime.now().year):
38
+ # generate a datetime in format yyyy-mm-dd hh:mm:ss.000000
39
+ start = datetime(min_year, 1, 1, 00, 00, 00)
40
+ years = max_year - min_year + 1
41
+ end = start + timedelta(days=365 * years)
42
+ return start + (end - start) * random.random()
43
+
44
+ def get_random_unicode_char():
45
+ """Get a random Unicode character from various language ranges."""
46
+ range_start, range_end = random.choice(unicode_ranges)
47
+ code_point = random.randint(range_start, range_end)
48
+ try:
49
+ return chr(code_point)
50
+ except ValueError:
51
+ return chr(random.randint(0x00C0, 0x00FF))
52
+
53
+ result = []
54
+ # colors = list(reversed(get_rdylgn_colors(num_rows)))
55
+ colors = get_rdylgn_colors(num_rows)
56
+ for i in range(num_rows):
57
+ row = [
58
+ random.choice(string.ascii_letters),
59
+ random.randint(100, 100000),
60
+ random.uniform(-1, 1),
61
+ get_random_unicode_char(),
62
+ random.choice([True, False]),
63
+ str(gen_datetime()),
64
+ colors[i],
65
+ ]
66
+ result.append(row)
67
+ columns= [f'col{i}' for i in range(len(result[0]))]
68
+ df = pd.DataFrame(result, columns=columns)
69
+
70
+ outfile = "rnd_table2.html"
71
+ df2dtb.to_html(df, outfile=outfile, title="Example Diverse Random Data", html_cols=["col1", "col2"])
72
+ return result
73
+
74
+ def pkg_test():
75
+
76
+ def get_packages():
77
+ try:
78
+ import pkg_resources
79
+
80
+ dists = [repr(d).split(" ") for d in sorted(pkg_resources.working_set)]
81
+ dists = sorted(dists, key=lambda x: x[0].lower())
82
+ except ModuleNotFoundError:
83
+ print("Error loading module pkg_resources - using random data")
84
+ dists = generate_random_data(num_rows=100)
85
+ # dists.insert(0, ["name1", "name2", "name3", "name4", "name5", "name6"])
86
+ return dists
87
+
88
+ header_list = ["name ", "ver ", "full package path"]
89
+ df = pd.DataFrame(get_packages(), columns=header_list)
90
+
91
+ outfile = "pkg_table.html"
92
+ df2dtb.to_html(df, outfile=outfile)
93
+
94
+ def test_yfinance(ticker='AAPL'):
95
+ import yfinance as yf
96
+ per = "2y"
97
+ df = yf.download(ticker, period=per)
98
+ df['Date'] = df.index.map(lambda x: pd.to_datetime(x).date())
99
+
100
+ outfile = "yfinance.html"
101
+ df2dtb.to_html(
102
+ df,
103
+ outfile=outfile,
104
+ title=f"{ticker}, last {per}",
105
+ )
106
+
107
+
108
+
109
+ if __name__ == "__main__":
110
+ # random_data()
111
+ test_yfinance()
112
+ # pkg_test()
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: df2tables
3
+ Version: 0.0.1
4
+ Summary: dftables creation
5
+ Author-email: Tomasz Sługocki <ts.kontakt@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ts-kontakt/df2tables
8
+ Project-URL: Issues, https://github.com/ts-kontakt/df2tables/issues
9
+ License-File: LICENCE.txt
10
+ Requires-Dist: pandas
11
+ Requires-Dist: numpy
12
+ Dynamic: license-file
@@ -0,0 +1,14 @@
1
+ LICENCE.txt
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ df2tables/__init__.py
6
+ df2tables/comnt.py
7
+ df2tables/datatable_templ.html
8
+ df2tables/df2tables.py
9
+ df2tables/examples.py
10
+ df2tables.egg-info/PKG-INFO
11
+ df2tables.egg-info/SOURCES.txt
12
+ df2tables.egg-info/dependency_links.txt
13
+ df2tables.egg-info/requires.txt
14
+ df2tables.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ pandas
2
+ numpy
@@ -0,0 +1 @@
1
+ df2tables
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "df2tables"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Tomasz Sługocki", email="ts.kontakt@gmail.com" },
10
+ ]
11
+ description = "dftables creation"
12
+ dependencies = [
13
+ "pandas","numpy"
14
+ ]
15
+
16
+ license = "MIT"
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/ts-kontakt/df2tables"
20
+ Issues = "https://github.com/ts-kontakt/df2tables/issues"
21
+
22
+ [tool.setuptools]
23
+ packages = ['df2tables']
24
+ #py-modules = ["df2tables", "comnt", "examples"]
25
+ [tool.setuptools.package-data]
26
+ df2tables = ["df2tables/datatable_templ.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+