dfpretty 0.1.0__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.
- dfpretty-0.1.0/LICENSE +21 -0
- dfpretty-0.1.0/PKG-INFO +148 -0
- dfpretty-0.1.0/README.md +96 -0
- dfpretty-0.1.0/dfpretty/__init__.py +25 -0
- dfpretty-0.1.0/dfpretty/_html.py +523 -0
- dfpretty-0.1.0/dfpretty/core.py +99 -0
- dfpretty-0.1.0/dfpretty/themes.py +142 -0
- dfpretty-0.1.0/dfpretty.egg-info/PKG-INFO +148 -0
- dfpretty-0.1.0/dfpretty.egg-info/SOURCES.txt +13 -0
- dfpretty-0.1.0/dfpretty.egg-info/dependency_links.txt +1 -0
- dfpretty-0.1.0/dfpretty.egg-info/requires.txt +8 -0
- dfpretty-0.1.0/dfpretty.egg-info/top_level.txt +1 -0
- dfpretty-0.1.0/pyproject.toml +49 -0
- dfpretty-0.1.0/setup.cfg +4 -0
- dfpretty-0.1.0/tests/test_pretty.py +52 -0
dfpretty-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 YOUR_NAME
|
|
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.
|
dfpretty-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dfpretty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pretty-print pandas DataFrames as styled interactive HTML tables in your browser
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 YOUR_NAME
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/dfpretty
|
|
28
|
+
Project-URL: Repository, https://github.com/YOUR_USERNAME/dfpretty
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/dfpretty/issues
|
|
30
|
+
Keywords: pandas,dataframe,visualization,html,jupyter
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: pandas>=1.5
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
48
|
+
Requires-Dist: ruff; extra == "dev"
|
|
49
|
+
Requires-Dist: build; extra == "dev"
|
|
50
|
+
Requires-Dist: twine; extra == "dev"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# dfpretty
|
|
54
|
+
|
|
55
|
+
> Pretty-print pandas DataFrames as styled interactive HTML tables — with theme switcher and Excel-like column filters.
|
|
56
|
+
|
|
57
|
+
Opens a standalone browser window (no Jupyter required).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from dfpretty import pretty
|
|
61
|
+
pretty(df, theme="tableau", title="Sales Q1")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# pip
|
|
72
|
+
pip install dfpretty
|
|
73
|
+
|
|
74
|
+
# conda (once on conda-forge)
|
|
75
|
+
conda install -c conda-forge dfpretty
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import pandas as pd
|
|
84
|
+
from dfpretty import pretty
|
|
85
|
+
|
|
86
|
+
df = pd.read_csv("data.csv")
|
|
87
|
+
|
|
88
|
+
pretty(df) # dark theme, opens browser
|
|
89
|
+
pretty(df, theme="tableau", title="My Table") # Tableau style
|
|
90
|
+
pretty(df, theme="terminal") # green-on-black
|
|
91
|
+
pretty(df, locale="de-DE") # German number formatting
|
|
92
|
+
pretty(df, save="report.html", open_browser=False) # save without opening
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Parameters
|
|
96
|
+
|
|
97
|
+
| Parameter | Type | Default | Description |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `df` | `pd.DataFrame` | — | DataFrame to display |
|
|
100
|
+
| `title` | `str` | `"DataFrame"` | Title in the top bar |
|
|
101
|
+
| `theme` | `str` | `"dark"` | Initial colour theme |
|
|
102
|
+
| `locale` | `str` | `"en-US"` | BCP-47 locale for number formatting |
|
|
103
|
+
| `save` | `str \| Path \| None` | `None` | Save HTML to this path |
|
|
104
|
+
| `open_browser` | `bool` | `True` | Open browser automatically |
|
|
105
|
+
|
|
106
|
+
**Returns:** `Path` — path to the generated HTML file.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Themes
|
|
111
|
+
|
|
112
|
+
Themes can be switched live in the browser via the buttons in the top bar.
|
|
113
|
+
|
|
114
|
+
| Name | Style |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `dark` | Deep blue-slate, blue accents |
|
|
117
|
+
| `tableau` | Cream background, charcoal header, orange accent — Tableau-inspired |
|
|
118
|
+
| `light` | Clean white, indigo accents |
|
|
119
|
+
| `terminal` | Black, green-on-black Matrix style |
|
|
120
|
+
| `notion` | Soft white, editorial typography |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Features
|
|
125
|
+
|
|
126
|
+
- **Column filters** — click ▾ on any column header to filter by value (Excel-style)
|
|
127
|
+
- **Global search** — filter across all columns at once
|
|
128
|
+
- **Sort** — click any column name to sort ↑ ↓
|
|
129
|
+
- **Number formatting** — integers and floats formatted with locale-aware separators
|
|
130
|
+
- **Theme switcher** — switch themes live without reopening
|
|
131
|
+
- **Save to file** — export a standalone HTML report
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/YOUR_USERNAME/dfpretty
|
|
139
|
+
cd dfpretty
|
|
140
|
+
pip install -e ".[dev]"
|
|
141
|
+
pytest
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
dfpretty-0.1.0/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# dfpretty
|
|
2
|
+
|
|
3
|
+
> Pretty-print pandas DataFrames as styled interactive HTML tables — with theme switcher and Excel-like column filters.
|
|
4
|
+
|
|
5
|
+
Opens a standalone browser window (no Jupyter required).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from dfpretty import pretty
|
|
9
|
+
pretty(df, theme="tableau", title="Sales Q1")
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# pip
|
|
20
|
+
pip install dfpretty
|
|
21
|
+
|
|
22
|
+
# conda (once on conda-forge)
|
|
23
|
+
conda install -c conda-forge dfpretty
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import pandas as pd
|
|
32
|
+
from dfpretty import pretty
|
|
33
|
+
|
|
34
|
+
df = pd.read_csv("data.csv")
|
|
35
|
+
|
|
36
|
+
pretty(df) # dark theme, opens browser
|
|
37
|
+
pretty(df, theme="tableau", title="My Table") # Tableau style
|
|
38
|
+
pretty(df, theme="terminal") # green-on-black
|
|
39
|
+
pretty(df, locale="de-DE") # German number formatting
|
|
40
|
+
pretty(df, save="report.html", open_browser=False) # save without opening
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Parameters
|
|
44
|
+
|
|
45
|
+
| Parameter | Type | Default | Description |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `df` | `pd.DataFrame` | — | DataFrame to display |
|
|
48
|
+
| `title` | `str` | `"DataFrame"` | Title in the top bar |
|
|
49
|
+
| `theme` | `str` | `"dark"` | Initial colour theme |
|
|
50
|
+
| `locale` | `str` | `"en-US"` | BCP-47 locale for number formatting |
|
|
51
|
+
| `save` | `str \| Path \| None` | `None` | Save HTML to this path |
|
|
52
|
+
| `open_browser` | `bool` | `True` | Open browser automatically |
|
|
53
|
+
|
|
54
|
+
**Returns:** `Path` — path to the generated HTML file.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Themes
|
|
59
|
+
|
|
60
|
+
Themes can be switched live in the browser via the buttons in the top bar.
|
|
61
|
+
|
|
62
|
+
| Name | Style |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `dark` | Deep blue-slate, blue accents |
|
|
65
|
+
| `tableau` | Cream background, charcoal header, orange accent — Tableau-inspired |
|
|
66
|
+
| `light` | Clean white, indigo accents |
|
|
67
|
+
| `terminal` | Black, green-on-black Matrix style |
|
|
68
|
+
| `notion` | Soft white, editorial typography |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **Column filters** — click ▾ on any column header to filter by value (Excel-style)
|
|
75
|
+
- **Global search** — filter across all columns at once
|
|
76
|
+
- **Sort** — click any column name to sort ↑ ↓
|
|
77
|
+
- **Number formatting** — integers and floats formatted with locale-aware separators
|
|
78
|
+
- **Theme switcher** — switch themes live without reopening
|
|
79
|
+
- **Save to file** — export a standalone HTML report
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/YOUR_USERNAME/dfpretty
|
|
87
|
+
cd dfpretty
|
|
88
|
+
pip install -e ".[dev]"
|
|
89
|
+
pytest
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dfpretty
|
|
3
|
+
~~~~~~~~
|
|
4
|
+
Pretty-print pandas DataFrames as styled interactive HTML tables
|
|
5
|
+
that open in your browser — with theme switcher and Excel-like filters.
|
|
6
|
+
|
|
7
|
+
Basic usage::
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from dfpretty import pretty
|
|
11
|
+
|
|
12
|
+
df = pd.DataFrame(...)
|
|
13
|
+
pretty(df) # dark theme (default)
|
|
14
|
+
pretty(df, theme="tableau")
|
|
15
|
+
pretty(df, theme="terminal", title="My Results")
|
|
16
|
+
pretty(df, save="report.html", open_browser=False)
|
|
17
|
+
|
|
18
|
+
Available themes: dark · tableau · light · terminal · notion
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .core import pretty
|
|
22
|
+
from .themes import AVAILABLE_THEMES
|
|
23
|
+
|
|
24
|
+
__all__ = ["pretty", "AVAILABLE_THEMES"]
|
|
25
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dfpretty._html
|
|
3
|
+
~~~~~~~~~~~~~~
|
|
4
|
+
Builds the self-contained HTML string rendered in the browser.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import json
|
|
9
|
+
from .themes import AVAILABLE_THEMES, build_css_vars
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_GOOGLE_FONTS = (
|
|
13
|
+
"https://fonts.googleapis.com/css2?"
|
|
14
|
+
"family=IBM+Plex+Mono:wght@400;600"
|
|
15
|
+
"&family=IBM+Plex+Sans:wght@400;500;600"
|
|
16
|
+
"&family=Geist+Mono:wght@400;600"
|
|
17
|
+
"&family=Merriweather+Sans:wght@400;600"
|
|
18
|
+
"&display=swap"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_BASE_CSS = """
|
|
22
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
font-family: var(--font-body);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
transition: background 0.25s, color 0.25s;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Topbar */
|
|
35
|
+
.topbar {
|
|
36
|
+
background: var(--surface);
|
|
37
|
+
border-bottom: 1px solid var(--border);
|
|
38
|
+
padding: 12px 20px;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
position: sticky;
|
|
43
|
+
top: 0;
|
|
44
|
+
z-index: 100;
|
|
45
|
+
gap: 16px;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
}
|
|
48
|
+
.topbar-title {
|
|
49
|
+
font-family: var(--font-mono);
|
|
50
|
+
font-size: 13px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
color: var(--text-bright);
|
|
53
|
+
letter-spacing: 0.04em;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
.topbar-meta {
|
|
57
|
+
font-family: var(--font-mono);
|
|
58
|
+
font-size: 11px;
|
|
59
|
+
color: var(--text-dim);
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
}
|
|
62
|
+
.topbar-right {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 6px;
|
|
66
|
+
flex-wrap: wrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Theme switcher */
|
|
70
|
+
.theme-label {
|
|
71
|
+
font-family: var(--font-mono);
|
|
72
|
+
font-size: 10px;
|
|
73
|
+
color: var(--text-dim);
|
|
74
|
+
text-transform: uppercase;
|
|
75
|
+
letter-spacing: 0.08em;
|
|
76
|
+
margin-right: 2px;
|
|
77
|
+
}
|
|
78
|
+
.theme-btn {
|
|
79
|
+
padding: 4px 10px;
|
|
80
|
+
border-radius: 6px;
|
|
81
|
+
font-size: 11px;
|
|
82
|
+
font-family: var(--font-mono);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
background: var(--bg2);
|
|
86
|
+
color: var(--text-muted);
|
|
87
|
+
transition: all 0.15s;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
.theme-btn:hover { background: var(--surface2); color: var(--text-bright); }
|
|
91
|
+
.theme-btn.active {
|
|
92
|
+
background: var(--accent);
|
|
93
|
+
color: #fff;
|
|
94
|
+
border-color: var(--accent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Searchbar */
|
|
98
|
+
.searchbar {
|
|
99
|
+
padding: 10px 20px;
|
|
100
|
+
background: var(--bg);
|
|
101
|
+
border-bottom: 1px solid var(--border2);
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 8px;
|
|
105
|
+
}
|
|
106
|
+
.searchbar input {
|
|
107
|
+
background: var(--surface);
|
|
108
|
+
border: 1px solid var(--border);
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
color: var(--text-bright);
|
|
111
|
+
font-family: var(--font-body);
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
padding: 7px 12px;
|
|
114
|
+
outline: none;
|
|
115
|
+
width: 300px;
|
|
116
|
+
transition: border-color 0.2s;
|
|
117
|
+
}
|
|
118
|
+
.searchbar input:focus { border-color: var(--accent); }
|
|
119
|
+
.searchbar input::placeholder { color: var(--text-dim); }
|
|
120
|
+
.clear-btn {
|
|
121
|
+
background: var(--surface);
|
|
122
|
+
border: 1px solid var(--border);
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
color: var(--text-muted);
|
|
125
|
+
font-size: 12px;
|
|
126
|
+
padding: 7px 12px;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
font-family: var(--font-body);
|
|
129
|
+
transition: all 0.2s;
|
|
130
|
+
}
|
|
131
|
+
.clear-btn:hover { background: var(--surface2); color: var(--text-bright); }
|
|
132
|
+
|
|
133
|
+
/* Table */
|
|
134
|
+
.table-wrap {
|
|
135
|
+
flex: 1;
|
|
136
|
+
overflow: auto;
|
|
137
|
+
padding: 16px 20px 60px;
|
|
138
|
+
}
|
|
139
|
+
table {
|
|
140
|
+
border-collapse: collapse;
|
|
141
|
+
width: 100%;
|
|
142
|
+
font-size: 13px;
|
|
143
|
+
min-width: 400px;
|
|
144
|
+
}
|
|
145
|
+
thead th {
|
|
146
|
+
background: var(--header-bg);
|
|
147
|
+
color: var(--header-text);
|
|
148
|
+
font-family: var(--font-mono);
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
letter-spacing: 0.07em;
|
|
153
|
+
padding: 0;
|
|
154
|
+
border-bottom: 2px solid var(--th-border);
|
|
155
|
+
border-right: 1px solid var(--border);
|
|
156
|
+
position: sticky;
|
|
157
|
+
top: 0;
|
|
158
|
+
z-index: 10;
|
|
159
|
+
white-space: nowrap;
|
|
160
|
+
transition: background 0.25s;
|
|
161
|
+
}
|
|
162
|
+
thead th:last-child { border-right: none; }
|
|
163
|
+
|
|
164
|
+
.th-inner { display: flex; align-items: stretch; }
|
|
165
|
+
.th-label {
|
|
166
|
+
flex: 1;
|
|
167
|
+
padding: 11px 13px;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
user-select: none;
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 5px;
|
|
173
|
+
transition: color 0.15s;
|
|
174
|
+
}
|
|
175
|
+
.th-label:hover { color: var(--text-bright); }
|
|
176
|
+
.sort-icon { font-size: 10px; opacity: 0.3; transition: opacity 0.2s; }
|
|
177
|
+
.th-label:hover .sort-icon { opacity: 0.7; }
|
|
178
|
+
th.sorted .sort-icon { opacity: 1; color: var(--accent); }
|
|
179
|
+
|
|
180
|
+
/* Filter */
|
|
181
|
+
.filter-btn {
|
|
182
|
+
padding: 0 9px;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
color: var(--text-dim);
|
|
185
|
+
background: transparent;
|
|
186
|
+
border: none;
|
|
187
|
+
border-left: 1px solid var(--border);
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
transition: all 0.15s;
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
}
|
|
193
|
+
.filter-btn:hover { color: var(--text-bright); background: var(--surface2); }
|
|
194
|
+
.filter-btn.active { color: var(--accent); }
|
|
195
|
+
|
|
196
|
+
.filter-dropdown {
|
|
197
|
+
display: none;
|
|
198
|
+
position: absolute;
|
|
199
|
+
top: 100%;
|
|
200
|
+
left: 0;
|
|
201
|
+
z-index: 200;
|
|
202
|
+
background: var(--surface);
|
|
203
|
+
border: 1px solid var(--border);
|
|
204
|
+
border-radius: 10px;
|
|
205
|
+
padding: 10px;
|
|
206
|
+
min-width: 210px;
|
|
207
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.18);
|
|
208
|
+
}
|
|
209
|
+
.filter-dropdown.open { display: block; }
|
|
210
|
+
|
|
211
|
+
.filter-search {
|
|
212
|
+
width: 100%;
|
|
213
|
+
background: var(--bg);
|
|
214
|
+
border: 1px solid var(--border);
|
|
215
|
+
border-radius: 6px;
|
|
216
|
+
color: var(--text-bright);
|
|
217
|
+
font-family: var(--font-body);
|
|
218
|
+
font-size: 12px;
|
|
219
|
+
padding: 6px 10px;
|
|
220
|
+
outline: none;
|
|
221
|
+
margin-bottom: 8px;
|
|
222
|
+
}
|
|
223
|
+
.filter-search:focus { border-color: var(--accent); }
|
|
224
|
+
|
|
225
|
+
.filter-options {
|
|
226
|
+
max-height: 210px;
|
|
227
|
+
overflow-y: auto;
|
|
228
|
+
scrollbar-width: thin;
|
|
229
|
+
scrollbar-color: var(--surface2) transparent;
|
|
230
|
+
}
|
|
231
|
+
.filter-option {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 8px;
|
|
235
|
+
padding: 5px 6px;
|
|
236
|
+
border-radius: 5px;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
color: var(--text-muted);
|
|
240
|
+
transition: background 0.1s;
|
|
241
|
+
}
|
|
242
|
+
.filter-option:hover { background: var(--surface2); color: var(--text-bright); }
|
|
243
|
+
.filter-option input[type=checkbox] {
|
|
244
|
+
accent-color: var(--accent);
|
|
245
|
+
width: 13px; height: 13px;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.filter-actions {
|
|
250
|
+
display: flex;
|
|
251
|
+
gap: 6px;
|
|
252
|
+
margin-top: 8px;
|
|
253
|
+
padding-top: 8px;
|
|
254
|
+
border-top: 1px solid var(--border);
|
|
255
|
+
}
|
|
256
|
+
.filter-actions button {
|
|
257
|
+
flex: 1;
|
|
258
|
+
padding: 6px;
|
|
259
|
+
border-radius: 6px;
|
|
260
|
+
font-size: 11px;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
font-family: var(--font-body);
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
transition: all 0.15s;
|
|
265
|
+
}
|
|
266
|
+
.btn-apply { background: var(--accent); color: #fff; border: 1px solid var(--accent); }
|
|
267
|
+
.btn-apply:hover { background: var(--accent2); }
|
|
268
|
+
.btn-clear { background: transparent; color: var(--text-muted); border: 1px solid var(--border); }
|
|
269
|
+
.btn-clear:hover { background: var(--surface2); color: var(--text-bright); }
|
|
270
|
+
|
|
271
|
+
/* Rows */
|
|
272
|
+
tbody tr { transition: background 0.08s; }
|
|
273
|
+
tbody tr:nth-child(odd) td { background: var(--row-odd); }
|
|
274
|
+
tbody tr:nth-child(even) td { background: var(--row-even); }
|
|
275
|
+
tbody tr:hover td { background: var(--row-hover) !important; color: var(--text-bright); }
|
|
276
|
+
|
|
277
|
+
td {
|
|
278
|
+
padding: 9px 13px;
|
|
279
|
+
border-bottom: 1px solid var(--border2);
|
|
280
|
+
border-right: 1px solid var(--border2);
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
transition: background 0.08s, color 0.08s;
|
|
283
|
+
}
|
|
284
|
+
td:last-child { border-right: none; }
|
|
285
|
+
td.num {
|
|
286
|
+
font-family: var(--font-mono);
|
|
287
|
+
text-align: right;
|
|
288
|
+
color: var(--num-color);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Footer */
|
|
292
|
+
.footer {
|
|
293
|
+
position: fixed;
|
|
294
|
+
bottom: 0; left: 0; right: 0;
|
|
295
|
+
background: var(--surface);
|
|
296
|
+
border-top: 1px solid var(--border);
|
|
297
|
+
padding: 7px 20px;
|
|
298
|
+
font-family: var(--font-mono);
|
|
299
|
+
font-size: 11px;
|
|
300
|
+
color: var(--text-dim);
|
|
301
|
+
display: flex;
|
|
302
|
+
justify-content: space-between;
|
|
303
|
+
z-index: 100;
|
|
304
|
+
transition: background 0.25s;
|
|
305
|
+
}
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def build_html(
|
|
310
|
+
data_json: str,
|
|
311
|
+
cols_json: str,
|
|
312
|
+
title: str,
|
|
313
|
+
theme: str,
|
|
314
|
+
locale: str,
|
|
315
|
+
) -> str:
|
|
316
|
+
"""Return the full self-contained HTML string."""
|
|
317
|
+
|
|
318
|
+
theme_buttons = "\n ".join(
|
|
319
|
+
f'<button class="theme-btn" onclick="setTheme(\'{t}\')">'
|
|
320
|
+
f'{t.capitalize()}</button>'
|
|
321
|
+
for t in AVAILABLE_THEMES
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
css_vars = build_css_vars(theme)
|
|
325
|
+
|
|
326
|
+
return f"""<!DOCTYPE html>
|
|
327
|
+
<html lang="es" data-theme="{theme}">
|
|
328
|
+
<head>
|
|
329
|
+
<meta charset="UTF-8">
|
|
330
|
+
<title>{title}</title>
|
|
331
|
+
<link rel="stylesheet" href="{_GOOGLE_FONTS}">
|
|
332
|
+
<style>
|
|
333
|
+
{css_vars}
|
|
334
|
+
{_BASE_CSS}
|
|
335
|
+
</style>
|
|
336
|
+
</head>
|
|
337
|
+
<body>
|
|
338
|
+
|
|
339
|
+
<div class="topbar">
|
|
340
|
+
<div class="topbar-title">⬡ {title}</div>
|
|
341
|
+
<div class="topbar-right">
|
|
342
|
+
<span class="theme-label">theme</span>
|
|
343
|
+
{theme_buttons}
|
|
344
|
+
</div>
|
|
345
|
+
<div class="topbar-meta" id="meta"></div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div class="searchbar">
|
|
349
|
+
<input type="text" id="globalSearch" placeholder="🔍 Search…" oninput="applyAll()">
|
|
350
|
+
<button class="clear-btn" onclick="clearAll()">✕ Clear filters</button>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<div class="table-wrap">
|
|
354
|
+
<table id="tbl">
|
|
355
|
+
<thead id="thead"></thead>
|
|
356
|
+
<tbody id="tbody"></tbody>
|
|
357
|
+
</table>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div class="footer">
|
|
361
|
+
<span>dfpretty</span>
|
|
362
|
+
<span id="footerRight"></span>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<script>
|
|
366
|
+
const RAW = {data_json};
|
|
367
|
+
const COLS = {cols_json};
|
|
368
|
+
const LOCALE = "{locale}";
|
|
369
|
+
|
|
370
|
+
let sortCol = null, sortDir = 1;
|
|
371
|
+
const activeFilters = {{}};
|
|
372
|
+
let openDropdown = null;
|
|
373
|
+
|
|
374
|
+
const numCols = new Set(
|
|
375
|
+
COLS.filter(c => RAW.every(r => r[c] === null || r[c] === undefined || typeof r[c] === "number"))
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
/* ── Theme ── */
|
|
379
|
+
function setTheme(t) {{
|
|
380
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
381
|
+
document.querySelectorAll(".theme-btn").forEach(b =>
|
|
382
|
+
b.classList.toggle("active", b.textContent.toLowerCase() === t)
|
|
383
|
+
);
|
|
384
|
+
}}
|
|
385
|
+
(function() {{
|
|
386
|
+
const cur = document.documentElement.getAttribute("data-theme");
|
|
387
|
+
document.querySelectorAll(".theme-btn").forEach(b =>
|
|
388
|
+
b.classList.toggle("active", b.textContent.toLowerCase() === cur)
|
|
389
|
+
);
|
|
390
|
+
}})();
|
|
391
|
+
|
|
392
|
+
/* ── Format ── */
|
|
393
|
+
function fmt(v) {{
|
|
394
|
+
if (v === null || v === undefined) return "—";
|
|
395
|
+
if (typeof v === "number")
|
|
396
|
+
return Number.isInteger(v)
|
|
397
|
+
? v.toLocaleString(LOCALE)
|
|
398
|
+
: v.toLocaleString(LOCALE, {{minimumFractionDigits:2, maximumFractionDigits:2}});
|
|
399
|
+
return v;
|
|
400
|
+
}}
|
|
401
|
+
|
|
402
|
+
/* ── Header ── */
|
|
403
|
+
function buildHeader() {{
|
|
404
|
+
const tr = document.createElement("tr");
|
|
405
|
+
COLS.forEach(col => {{
|
|
406
|
+
const th = document.createElement("th");
|
|
407
|
+
th.dataset.col = col;
|
|
408
|
+
th.style.position = "relative";
|
|
409
|
+
th.innerHTML = `
|
|
410
|
+
<div class="th-inner">
|
|
411
|
+
<div class="th-label" onclick="sortBy('${{col}}')">
|
|
412
|
+
${{col}}<span class="sort-icon" id="si-${{col}}">⇅</span>
|
|
413
|
+
</div>
|
|
414
|
+
<button class="filter-btn" id="fb-${{col}}" onclick="toggleDropdown(event,'${{col}}')">▾</button>
|
|
415
|
+
<div class="filter-dropdown" id="fd-${{col}}">
|
|
416
|
+
<input class="filter-search" placeholder="Search…" oninput="renderOptions('${{col}}',this.value)">
|
|
417
|
+
<div class="filter-options" id="fo-${{col}}"></div>
|
|
418
|
+
<div class="filter-actions">
|
|
419
|
+
<button class="btn-clear" onclick="clearFilter('${{col}}')">Clear</button>
|
|
420
|
+
<button class="btn-apply" onclick="closeDropdown()">Apply</button>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</div>`;
|
|
424
|
+
tr.appendChild(th);
|
|
425
|
+
}});
|
|
426
|
+
document.getElementById("thead").appendChild(tr);
|
|
427
|
+
}}
|
|
428
|
+
|
|
429
|
+
function uniqueVals(col) {{
|
|
430
|
+
return [...new Set(RAW.map(r => r[col]))].sort((a,b) => {{
|
|
431
|
+
if (a === null) return 1; if (b === null) return -1;
|
|
432
|
+
return a > b ? 1 : -1;
|
|
433
|
+
}});
|
|
434
|
+
}}
|
|
435
|
+
|
|
436
|
+
function renderOptions(col, search="") {{
|
|
437
|
+
const container = document.getElementById(`fo-${{col}}`);
|
|
438
|
+
const vals = uniqueVals(col).filter(v => String(v).toLowerCase().includes(search.toLowerCase()));
|
|
439
|
+
const sel = activeFilters[col] || new Set(uniqueVals(col).map(String));
|
|
440
|
+
container.innerHTML = vals.map(v => `
|
|
441
|
+
<label class="filter-option">
|
|
442
|
+
<input type="checkbox" value="${{v}}" ${{sel.has(String(v)) ? "checked" : ""}}
|
|
443
|
+
onchange="toggleVal('${{col}}',this.value,this.checked)">
|
|
444
|
+
${{fmt(v)}}
|
|
445
|
+
</label>`).join("");
|
|
446
|
+
}}
|
|
447
|
+
|
|
448
|
+
function toggleVal(col, val, checked) {{
|
|
449
|
+
if (!activeFilters[col]) activeFilters[col] = new Set(uniqueVals(col).map(String));
|
|
450
|
+
checked ? activeFilters[col].add(val) : activeFilters[col].delete(val);
|
|
451
|
+
document.getElementById(`fb-${{col}}`).classList.toggle(
|
|
452
|
+
"active", activeFilters[col].size < uniqueVals(col).length
|
|
453
|
+
);
|
|
454
|
+
applyAll();
|
|
455
|
+
}}
|
|
456
|
+
|
|
457
|
+
function clearFilter(col) {{
|
|
458
|
+
delete activeFilters[col];
|
|
459
|
+
document.getElementById(`fb-${{col}}`).classList.remove("active");
|
|
460
|
+
renderOptions(col);
|
|
461
|
+
applyAll();
|
|
462
|
+
}}
|
|
463
|
+
|
|
464
|
+
function clearAll() {{
|
|
465
|
+
COLS.forEach(c => {{ delete activeFilters[c]; document.getElementById(`fb-${{c}}`).classList.remove("active"); }});
|
|
466
|
+
document.getElementById("globalSearch").value = "";
|
|
467
|
+
applyAll();
|
|
468
|
+
}}
|
|
469
|
+
|
|
470
|
+
function toggleDropdown(e, col) {{
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
const fd = document.getElementById(`fd-${{col}}`);
|
|
473
|
+
if (openDropdown && openDropdown !== fd) openDropdown.classList.remove("open");
|
|
474
|
+
const isOpen = fd.classList.toggle("open");
|
|
475
|
+
openDropdown = isOpen ? fd : null;
|
|
476
|
+
if (isOpen) renderOptions(col);
|
|
477
|
+
}}
|
|
478
|
+
|
|
479
|
+
function closeDropdown() {{
|
|
480
|
+
if (openDropdown) {{ openDropdown.classList.remove("open"); openDropdown = null; }}
|
|
481
|
+
}}
|
|
482
|
+
|
|
483
|
+
document.addEventListener("click", e => {{
|
|
484
|
+
if (!e.target.closest(".filter-dropdown") && !e.target.closest(".filter-btn")) closeDropdown();
|
|
485
|
+
}});
|
|
486
|
+
|
|
487
|
+
function sortBy(col) {{
|
|
488
|
+
if (sortCol === col) sortDir *= -1; else {{ sortCol = col; sortDir = 1; }}
|
|
489
|
+
COLS.forEach(c => {{
|
|
490
|
+
document.getElementById(`si-${{c}}`).textContent = "⇅";
|
|
491
|
+
document.querySelector(`[data-col="${{c}}"]`).classList.remove("sorted");
|
|
492
|
+
}});
|
|
493
|
+
document.getElementById(`si-${{col}}`).textContent = sortDir === 1 ? "↑" : "↓";
|
|
494
|
+
document.querySelector(`[data-col="${{col}}"]`).classList.add("sorted");
|
|
495
|
+
applyAll();
|
|
496
|
+
}}
|
|
497
|
+
|
|
498
|
+
function applyAll() {{
|
|
499
|
+
const q = document.getElementById("globalSearch").value.toLowerCase();
|
|
500
|
+
let rows = [...RAW];
|
|
501
|
+
COLS.forEach(col => {{
|
|
502
|
+
if (activeFilters[col]) rows = rows.filter(r => activeFilters[col].has(String(r[col])));
|
|
503
|
+
}});
|
|
504
|
+
if (q) rows = rows.filter(r => COLS.some(c => String(r[c]).toLowerCase().includes(q)));
|
|
505
|
+
if (sortCol) rows.sort((a,b) => {{
|
|
506
|
+
const va = a[sortCol], vb = b[sortCol];
|
|
507
|
+
if (va === null) return 1; if (vb === null) return -1;
|
|
508
|
+
return (va > vb ? 1 : va < vb ? -1 : 0) * sortDir;
|
|
509
|
+
}});
|
|
510
|
+
|
|
511
|
+
document.getElementById("tbody").innerHTML = rows.map(r =>
|
|
512
|
+
`<tr>${{COLS.map(c=>`<td class="${{numCols.has(c)?"num":""}}">${{fmt(r[c])}}</td>`).join("")}}</tr>`
|
|
513
|
+
).join("");
|
|
514
|
+
|
|
515
|
+
document.getElementById("meta").textContent = `${{rows.length}} / ${{RAW.length}} rows`;
|
|
516
|
+
document.getElementById("footerRight").textContent = `${{rows.length}} rows · ${{COLS.length}} cols`;
|
|
517
|
+
}}
|
|
518
|
+
|
|
519
|
+
buildHeader();
|
|
520
|
+
applyAll();
|
|
521
|
+
</script>
|
|
522
|
+
</body>
|
|
523
|
+
</html>"""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dfpretty.core
|
|
3
|
+
~~~~~~~~~~~~~
|
|
4
|
+
Main entry point: the pretty() function.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import json
|
|
9
|
+
import tempfile
|
|
10
|
+
import webbrowser
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from .themes import AVAILABLE_THEMES
|
|
16
|
+
from ._html import build_html
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def pretty(
|
|
20
|
+
df: pd.DataFrame,
|
|
21
|
+
title: str = "DataFrame",
|
|
22
|
+
theme: str = "dark",
|
|
23
|
+
locale: str = "en-US",
|
|
24
|
+
save: str | Path | None = None,
|
|
25
|
+
open_browser: bool = True,
|
|
26
|
+
) -> Path:
|
|
27
|
+
"""
|
|
28
|
+
Render a DataFrame as a styled interactive table in the browser.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
df : pd.DataFrame
|
|
33
|
+
The DataFrame to display.
|
|
34
|
+
title : str
|
|
35
|
+
Window / page title shown in the top bar.
|
|
36
|
+
theme : str
|
|
37
|
+
Initial colour theme. One of: 'dark', 'tableau', 'light',
|
|
38
|
+
'terminal', 'notion'. Can be changed live in the browser.
|
|
39
|
+
locale : str
|
|
40
|
+
BCP-47 locale string used by Intl.NumberFormat for number
|
|
41
|
+
formatting (e.g. 'en-US', 'es-ES', 'de-DE').
|
|
42
|
+
save : str | Path | None
|
|
43
|
+
If given, save the HTML file at this path instead of a
|
|
44
|
+
temporary file. The file persists after the browser closes.
|
|
45
|
+
open_browser : bool
|
|
46
|
+
If False, build the file but don't launch the browser.
|
|
47
|
+
Useful for testing or headless environments.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
Path
|
|
52
|
+
Path to the generated HTML file.
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> import pandas as pd
|
|
57
|
+
>>> from dfpretty import pretty
|
|
58
|
+
>>> df = pd.DataFrame({"x": [1, 2], "y": ["a", "b"]})
|
|
59
|
+
>>> pretty(df)
|
|
60
|
+
>>> pretty(df, theme="tableau", title="My Data")
|
|
61
|
+
>>> pretty(df, save="output.html", open_browser=False)
|
|
62
|
+
"""
|
|
63
|
+
if theme not in AVAILABLE_THEMES:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Unknown theme '{theme}'. "
|
|
66
|
+
f"Choose one of: {', '.join(AVAILABLE_THEMES)}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
data_json = df.to_json(orient="records")
|
|
70
|
+
cols_json = json.dumps(list(df.columns))
|
|
71
|
+
|
|
72
|
+
html = build_html(
|
|
73
|
+
data_json=data_json,
|
|
74
|
+
cols_json=cols_json,
|
|
75
|
+
title=title,
|
|
76
|
+
theme=theme,
|
|
77
|
+
locale=locale,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if save is not None:
|
|
81
|
+
out_path = Path(save)
|
|
82
|
+
out_path.write_text(html, encoding="utf-8")
|
|
83
|
+
else:
|
|
84
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
85
|
+
mode="w",
|
|
86
|
+
suffix=".html",
|
|
87
|
+
delete=False,
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
prefix="dfpretty_",
|
|
90
|
+
)
|
|
91
|
+
tmp.write(html)
|
|
92
|
+
tmp.close()
|
|
93
|
+
out_path = Path(tmp.name)
|
|
94
|
+
|
|
95
|
+
if open_browser:
|
|
96
|
+
webbrowser.open(out_path.as_uri())
|
|
97
|
+
print(f"✓ dfpretty [{theme}] → {out_path}")
|
|
98
|
+
|
|
99
|
+
return out_path
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dfpretty.themes
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
CSS variable definitions for each built-in theme.
|
|
5
|
+
Adding a custom theme: add a new key to THEMES with the required variables.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
THEMES: dict[str, dict[str, str]] = {
|
|
9
|
+
"dark": {
|
|
10
|
+
"bg": "#0f172a",
|
|
11
|
+
"bg2": "#111827",
|
|
12
|
+
"surface": "#1e293b",
|
|
13
|
+
"surface2": "#334155",
|
|
14
|
+
"border": "#334155",
|
|
15
|
+
"border2": "#1e293b",
|
|
16
|
+
"text": "#cbd5e1",
|
|
17
|
+
"text_dim": "#475569",
|
|
18
|
+
"text_muted": "#94a3b8",
|
|
19
|
+
"text_bright": "#f1f5f9",
|
|
20
|
+
"accent": "#3b82f6",
|
|
21
|
+
"accent2": "#2563eb",
|
|
22
|
+
"num_color": "#93c5fd",
|
|
23
|
+
"row_odd": "#111827",
|
|
24
|
+
"row_even": "#0f172a",
|
|
25
|
+
"row_hover": "#1e293b",
|
|
26
|
+
"header_bg": "#1e293b",
|
|
27
|
+
"header_text": "#94a3b8",
|
|
28
|
+
"th_border": "#3b82f6",
|
|
29
|
+
"font_body": "'IBM Plex Sans', sans-serif",
|
|
30
|
+
"font_mono": "'IBM Plex Mono', monospace",
|
|
31
|
+
},
|
|
32
|
+
"tableau": {
|
|
33
|
+
"bg": "#f5f5f2",
|
|
34
|
+
"bg2": "#eeede9",
|
|
35
|
+
"surface": "#ffffff",
|
|
36
|
+
"surface2": "#e8e7e3",
|
|
37
|
+
"border": "#d1cfc9",
|
|
38
|
+
"border2": "#e8e7e3",
|
|
39
|
+
"text": "#3b3935",
|
|
40
|
+
"text_dim": "#8a8780",
|
|
41
|
+
"text_muted": "#6b6966",
|
|
42
|
+
"text_bright": "#1a1917",
|
|
43
|
+
"accent": "#1f6bb0",
|
|
44
|
+
"accent2": "#174f82",
|
|
45
|
+
"num_color": "#1f6bb0",
|
|
46
|
+
"row_odd": "#ffffff",
|
|
47
|
+
"row_even": "#f5f5f2",
|
|
48
|
+
"row_hover": "#eaf2fb",
|
|
49
|
+
"header_bg": "#3b3935",
|
|
50
|
+
"header_text": "#e8e7e3",
|
|
51
|
+
"th_border": "#e8702a",
|
|
52
|
+
"font_body": "'Merriweather Sans', sans-serif",
|
|
53
|
+
"font_mono": "'IBM Plex Mono', monospace",
|
|
54
|
+
},
|
|
55
|
+
"light": {
|
|
56
|
+
"bg": "#f8fafc",
|
|
57
|
+
"bg2": "#f1f5f9",
|
|
58
|
+
"surface": "#ffffff",
|
|
59
|
+
"surface2": "#e2e8f0",
|
|
60
|
+
"border": "#e2e8f0",
|
|
61
|
+
"border2": "#f1f5f9",
|
|
62
|
+
"text": "#334155",
|
|
63
|
+
"text_dim": "#94a3b8",
|
|
64
|
+
"text_muted": "#64748b",
|
|
65
|
+
"text_bright": "#0f172a",
|
|
66
|
+
"accent": "#6366f1",
|
|
67
|
+
"accent2": "#4f46e5",
|
|
68
|
+
"num_color": "#6366f1",
|
|
69
|
+
"row_odd": "#ffffff",
|
|
70
|
+
"row_even": "#f8fafc",
|
|
71
|
+
"row_hover": "#eef2ff",
|
|
72
|
+
"header_bg": "#f1f5f9",
|
|
73
|
+
"header_text": "#475569",
|
|
74
|
+
"th_border": "#6366f1",
|
|
75
|
+
"font_body": "'IBM Plex Sans', sans-serif",
|
|
76
|
+
"font_mono": "'IBM Plex Mono', monospace",
|
|
77
|
+
},
|
|
78
|
+
"terminal": {
|
|
79
|
+
"bg": "#0d0d0d",
|
|
80
|
+
"bg2": "#111111",
|
|
81
|
+
"surface": "#1a1a1a",
|
|
82
|
+
"surface2": "#2a2a2a",
|
|
83
|
+
"border": "#2a2a2a",
|
|
84
|
+
"border2": "#1a1a1a",
|
|
85
|
+
"text": "#a3e635",
|
|
86
|
+
"text_dim": "#4d7c0f",
|
|
87
|
+
"text_muted": "#65a30d",
|
|
88
|
+
"text_bright": "#d9f99d",
|
|
89
|
+
"accent": "#4ade80",
|
|
90
|
+
"accent2": "#22c55e",
|
|
91
|
+
"num_color": "#86efac",
|
|
92
|
+
"row_odd": "#111111",
|
|
93
|
+
"row_even": "#0d0d0d",
|
|
94
|
+
"row_hover": "#1a2e1a",
|
|
95
|
+
"header_bg": "#1a1a1a",
|
|
96
|
+
"header_text": "#4ade80",
|
|
97
|
+
"th_border": "#4ade80",
|
|
98
|
+
"font_body": "'Geist Mono', monospace",
|
|
99
|
+
"font_mono": "'Geist Mono', monospace",
|
|
100
|
+
},
|
|
101
|
+
"notion": {
|
|
102
|
+
"bg": "#ffffff",
|
|
103
|
+
"bg2": "#f7f6f3",
|
|
104
|
+
"surface": "#ffffff",
|
|
105
|
+
"surface2": "#f1f1ef",
|
|
106
|
+
"border": "#e9e9e7",
|
|
107
|
+
"border2": "#f1f1ef",
|
|
108
|
+
"text": "#37352f",
|
|
109
|
+
"text_dim": "#9b9a97",
|
|
110
|
+
"text_muted": "#6b6a68",
|
|
111
|
+
"text_bright": "#1a1a1a",
|
|
112
|
+
"accent": "#2eaadc",
|
|
113
|
+
"accent2": "#0e8fc4",
|
|
114
|
+
"num_color": "#0e8fc4",
|
|
115
|
+
"row_odd": "#ffffff",
|
|
116
|
+
"row_even": "#f7f6f3",
|
|
117
|
+
"row_hover": "#f1f1ef",
|
|
118
|
+
"header_bg": "#f7f6f3",
|
|
119
|
+
"header_text": "#9b9a97",
|
|
120
|
+
"th_border": "#e9e9e7",
|
|
121
|
+
"font_body": "'IBM Plex Sans', sans-serif",
|
|
122
|
+
"font_mono": "'IBM Plex Mono', monospace",
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
AVAILABLE_THEMES = list(THEMES.keys())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_css_vars(theme_name: str) -> str:
|
|
130
|
+
"""Return a CSS :root block with the variables for *theme_name*."""
|
|
131
|
+
if theme_name not in THEMES:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Unknown theme '{theme_name}'. "
|
|
134
|
+
f"Available: {', '.join(AVAILABLE_THEMES)}"
|
|
135
|
+
)
|
|
136
|
+
t = THEMES[theme_name]
|
|
137
|
+
lines = [" :root {"]
|
|
138
|
+
for k, v in t.items():
|
|
139
|
+
css_key = "--" + k.replace("_", "-")
|
|
140
|
+
lines.append(f" {css_key}: {v};")
|
|
141
|
+
lines.append(" }")
|
|
142
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dfpretty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pretty-print pandas DataFrames as styled interactive HTML tables in your browser
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 YOUR_NAME
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/dfpretty
|
|
28
|
+
Project-URL: Repository, https://github.com/YOUR_USERNAME/dfpretty
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/dfpretty/issues
|
|
30
|
+
Keywords: pandas,dataframe,visualization,html,jupyter
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: pandas>=1.5
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
48
|
+
Requires-Dist: ruff; extra == "dev"
|
|
49
|
+
Requires-Dist: build; extra == "dev"
|
|
50
|
+
Requires-Dist: twine; extra == "dev"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# dfpretty
|
|
54
|
+
|
|
55
|
+
> Pretty-print pandas DataFrames as styled interactive HTML tables — with theme switcher and Excel-like column filters.
|
|
56
|
+
|
|
57
|
+
Opens a standalone browser window (no Jupyter required).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from dfpretty import pretty
|
|
61
|
+
pretty(df, theme="tableau", title="Sales Q1")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# pip
|
|
72
|
+
pip install dfpretty
|
|
73
|
+
|
|
74
|
+
# conda (once on conda-forge)
|
|
75
|
+
conda install -c conda-forge dfpretty
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import pandas as pd
|
|
84
|
+
from dfpretty import pretty
|
|
85
|
+
|
|
86
|
+
df = pd.read_csv("data.csv")
|
|
87
|
+
|
|
88
|
+
pretty(df) # dark theme, opens browser
|
|
89
|
+
pretty(df, theme="tableau", title="My Table") # Tableau style
|
|
90
|
+
pretty(df, theme="terminal") # green-on-black
|
|
91
|
+
pretty(df, locale="de-DE") # German number formatting
|
|
92
|
+
pretty(df, save="report.html", open_browser=False) # save without opening
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Parameters
|
|
96
|
+
|
|
97
|
+
| Parameter | Type | Default | Description |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `df` | `pd.DataFrame` | — | DataFrame to display |
|
|
100
|
+
| `title` | `str` | `"DataFrame"` | Title in the top bar |
|
|
101
|
+
| `theme` | `str` | `"dark"` | Initial colour theme |
|
|
102
|
+
| `locale` | `str` | `"en-US"` | BCP-47 locale for number formatting |
|
|
103
|
+
| `save` | `str \| Path \| None` | `None` | Save HTML to this path |
|
|
104
|
+
| `open_browser` | `bool` | `True` | Open browser automatically |
|
|
105
|
+
|
|
106
|
+
**Returns:** `Path` — path to the generated HTML file.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Themes
|
|
111
|
+
|
|
112
|
+
Themes can be switched live in the browser via the buttons in the top bar.
|
|
113
|
+
|
|
114
|
+
| Name | Style |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `dark` | Deep blue-slate, blue accents |
|
|
117
|
+
| `tableau` | Cream background, charcoal header, orange accent — Tableau-inspired |
|
|
118
|
+
| `light` | Clean white, indigo accents |
|
|
119
|
+
| `terminal` | Black, green-on-black Matrix style |
|
|
120
|
+
| `notion` | Soft white, editorial typography |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Features
|
|
125
|
+
|
|
126
|
+
- **Column filters** — click ▾ on any column header to filter by value (Excel-style)
|
|
127
|
+
- **Global search** — filter across all columns at once
|
|
128
|
+
- **Sort** — click any column name to sort ↑ ↓
|
|
129
|
+
- **Number formatting** — integers and floats formatted with locale-aware separators
|
|
130
|
+
- **Theme switcher** — switch themes live without reopening
|
|
131
|
+
- **Save to file** — export a standalone HTML report
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/YOUR_USERNAME/dfpretty
|
|
139
|
+
cd dfpretty
|
|
140
|
+
pip install -e ".[dev]"
|
|
141
|
+
pytest
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dfpretty/__init__.py
|
|
5
|
+
dfpretty/_html.py
|
|
6
|
+
dfpretty/core.py
|
|
7
|
+
dfpretty/themes.py
|
|
8
|
+
dfpretty.egg-info/PKG-INFO
|
|
9
|
+
dfpretty.egg-info/SOURCES.txt
|
|
10
|
+
dfpretty.egg-info/dependency_links.txt
|
|
11
|
+
dfpretty.egg-info/requires.txt
|
|
12
|
+
dfpretty.egg-info/top_level.txt
|
|
13
|
+
tests/test_pretty.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dfpretty
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dfpretty"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pretty-print pandas DataFrames as styled interactive HTML tables in your browser"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["pandas", "dataframe", "visualization", "html", "jupyter"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"pandas>=1.5",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=7",
|
|
32
|
+
"pytest-cov",
|
|
33
|
+
"ruff",
|
|
34
|
+
"build",
|
|
35
|
+
"twine",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/YOUR_USERNAME/dfpretty"
|
|
40
|
+
Repository = "https://github.com/YOUR_USERNAME/dfpretty"
|
|
41
|
+
"Bug Tracker" = "https://github.com/YOUR_USERNAME/dfpretty/issues"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
include = ["dfpretty*"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 88
|
|
49
|
+
target-version = "py39"
|
dfpretty-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Basic tests for dfpretty."""
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pytest
|
|
4
|
+
from dfpretty import pretty, AVAILABLE_THEMES
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def sample_df():
|
|
9
|
+
return pd.DataFrame({
|
|
10
|
+
"name": ["Alice", "Bob", "Carol"],
|
|
11
|
+
"score": [95.5, 82.0, 78.3],
|
|
12
|
+
"rank": [1, 2, 3],
|
|
13
|
+
"active": [True, False, True],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_returns_path(tmp_path, sample_df):
|
|
18
|
+
out = pretty(sample_df, save=tmp_path / "out.html", open_browser=False)
|
|
19
|
+
assert out.exists()
|
|
20
|
+
assert out.suffix == ".html"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_html_contains_title(tmp_path, sample_df):
|
|
24
|
+
out = pretty(sample_df, title="My Test", save=tmp_path / "out.html", open_browser=False)
|
|
25
|
+
content = out.read_text()
|
|
26
|
+
assert "My Test" in content
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_html_contains_data(tmp_path, sample_df):
|
|
30
|
+
out = pretty(sample_df, save=tmp_path / "out.html", open_browser=False)
|
|
31
|
+
content = out.read_text()
|
|
32
|
+
assert "Alice" in content
|
|
33
|
+
assert "95.5" in content
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_all_themes(tmp_path, sample_df):
|
|
37
|
+
for theme in AVAILABLE_THEMES:
|
|
38
|
+
out = pretty(sample_df, theme=theme, save=tmp_path / f"{theme}.html", open_browser=False)
|
|
39
|
+
assert out.exists()
|
|
40
|
+
content = out.read_text()
|
|
41
|
+
assert f'data-theme="{theme}"' in content
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_invalid_theme(sample_df):
|
|
45
|
+
with pytest.raises(ValueError, match="Unknown theme"):
|
|
46
|
+
pretty(sample_df, theme="neon_pink", open_browser=False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_available_themes_list():
|
|
50
|
+
assert "dark" in AVAILABLE_THEMES
|
|
51
|
+
assert "tableau" in AVAILABLE_THEMES
|
|
52
|
+
assert len(AVAILABLE_THEMES) == 5
|