itables 2.5.2.dev0__tar.gz → 2.6.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.
Files changed (62) hide show
  1. {itables-2.5.2.dev0 → itables-2.6.1}/.gitignore +6 -0
  2. {itables-2.5.2.dev0 → itables-2.6.1}/PKG-INFO +85 -35
  3. {itables-2.5.2.dev0 → itables-2.6.1}/README.md +4 -6
  4. {itables-2.5.2.dev0 → itables-2.6.1}/itables/__init__.py +12 -0
  5. itables-2.6.1/itables/datatables_format.py +211 -0
  6. {itables-2.5.2.dev0 → itables-2.6.1}/itables/downsample.py +66 -51
  7. {itables-2.5.2.dev0 → itables-2.6.1}/itables/html/dt_bundle.css +1 -1
  8. {itables-2.5.2.dev0 → itables-2.6.1}/itables/itables_for_streamlit/asset-manifest.json +3 -3
  9. {itables-2.5.2.dev0 → itables-2.6.1}/itables/itables_for_streamlit/index.html +1 -1
  10. itables-2.5.2.dev0/itables/itables_for_streamlit/static/css/main.66715262.css → itables-2.6.1/itables/itables_for_streamlit/static/css/main.d9f03cb9.css +2 -2
  11. itables-2.6.1/itables/itables_for_streamlit/static/css/main.d9f03cb9.css.map +1 -0
  12. {itables-2.5.2.dev0 → itables-2.6.1}/itables/itables_for_streamlit/static/js/main.978ec6e6.js.map +1 -1
  13. {itables-2.5.2.dev0 → itables-2.6.1}/itables/javascript.py +220 -79
  14. {itables-2.5.2.dev0 → itables-2.6.1}/itables/options.py +19 -4
  15. itables-2.6.1/itables/sample_dfs.py +32 -0
  16. itables-2.5.2.dev0/itables/sample_dfs.py → itables-2.6.1/itables/sample_pandas_dfs.py +4 -53
  17. itables-2.6.1/itables/sample_polars_dfs.py +230 -0
  18. {itables-2.5.2.dev0 → itables-2.6.1}/itables/shiny.py +2 -4
  19. {itables-2.5.2.dev0 → itables-2.6.1}/itables/streamlit.py +1 -2
  20. {itables-2.5.2.dev0 → itables-2.6.1}/itables/typing.py +84 -5
  21. itables-2.6.1/itables/version.py +4 -0
  22. {itables-2.5.2.dev0 → itables-2.6.1}/itables/widget/__init__.py +2 -2
  23. {itables-2.5.2.dev0 → itables-2.6.1}/itables/widget/static/widget.css +1 -1
  24. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/ITable.py +1 -1
  25. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/__init__.py +2 -2
  26. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/async-ITable.js +1 -1
  27. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/async-ITable.js.map +1 -1
  28. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/itables_for_dash.min.js +1 -1
  29. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/itables_for_dash.min.js.map +1 -1
  30. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/properties.py +1 -2
  31. {itables-2.5.2.dev0 → itables-2.6.1}/pyproject.toml +79 -20
  32. itables-2.5.2.dev0/itables/datatables_format.py +0 -160
  33. itables-2.5.2.dev0/itables/itables_for_streamlit/static/css/main.66715262.css.map +0 -1
  34. itables-2.5.2.dev0/itables/version.py +0 -3
  35. {itables-2.5.2.dev0 → itables-2.6.1}/LICENSE +0 -0
  36. {itables-2.5.2.dev0 → itables-2.6.1}/itables/config.py +0 -0
  37. {itables-2.5.2.dev0 → itables-2.6.1}/itables/dash.py +0 -0
  38. {itables-2.5.2.dev0 → itables-2.6.1}/itables/html/datatables_template.html +0 -0
  39. {itables-2.5.2.dev0 → itables-2.6.1}/itables/html/datatables_template_offline.html +0 -0
  40. {itables-2.5.2.dev0 → itables-2.6.1}/itables/html/dt_bundle.js +0 -0
  41. {itables-2.5.2.dev0 → itables-2.6.1}/itables/html/init_datatables.html +0 -0
  42. {itables-2.5.2.dev0 → itables-2.6.1}/itables/html/init_notebook_offline.html +0 -0
  43. {itables-2.5.2.dev0 → itables-2.6.1}/itables/interactive.py +0 -0
  44. {itables-2.5.2.dev0 → itables-2.6.1}/itables/itables_for_streamlit/static/js/main.978ec6e6.js +0 -0
  45. {itables-2.5.2.dev0 → itables-2.6.1}/itables/itables_for_streamlit/static/js/main.978ec6e6.js.LICENSE.txt +0 -0
  46. {itables-2.5.2.dev0 → itables-2.6.1}/itables/logo/loading.svg +0 -0
  47. {itables-2.5.2.dev0 → itables-2.6.1}/itables/logo/logo.svg +0 -0
  48. {itables-2.5.2.dev0 → itables-2.6.1}/itables/logo/text.svg +0 -0
  49. {itables-2.5.2.dev0 → itables-2.6.1}/itables/logo/wide.svg +0 -0
  50. {itables-2.5.2.dev0 → itables-2.6.1}/itables/logo/with_text.svg +0 -0
  51. {itables-2.5.2.dev0 → itables-2.6.1}/itables/py.typed +0 -0
  52. {itables-2.5.2.dev0 → itables-2.6.1}/itables/samples/countries.csv +0 -0
  53. {itables-2.5.2.dev0 → itables-2.6.1}/itables/samples/indicators.csv +0 -0
  54. {itables-2.5.2.dev0 → itables-2.6.1}/itables/samples/population.csv +0 -0
  55. {itables-2.5.2.dev0 → itables-2.6.1}/itables/show_config.py +0 -0
  56. {itables-2.5.2.dev0 → itables-2.6.1}/itables/utils.py +0 -0
  57. {itables-2.5.2.dev0 → itables-2.6.1}/itables/widget/static/widget.js +0 -0
  58. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/_imports_.py +0 -0
  59. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/async-ITable.js.LICENSE.txt +0 -0
  60. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/metadata.json +0 -0
  61. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/package-info.json +0 -0
  62. {itables-2.5.2.dev0 → itables-2.6.1}/itables_for_dash/py.typed +0 -0
@@ -2,11 +2,17 @@
2
2
  .idea
3
3
  .vscode
4
4
 
5
+ # Pixi
6
+ .pixi/
7
+
5
8
  # Python
6
9
  *.egg-info
7
10
  *.pyc
11
+
12
+ # Build artifacts
8
13
  build
9
14
  dist
15
+ conda-dist
10
16
 
11
17
  # Notebooks
12
18
  *.ipynb
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itables
3
- Version: 2.5.2.dev0
4
- Summary: Pandas and Polar DataFrames as interactive DataTables
3
+ Version: 2.6.1
4
+ Summary: Python DataFrames as interactive DataTables
5
5
  Project-URL: Homepage, https://mwouts.github.io/itables/
6
6
  Project-URL: Documentation, https://mwouts.github.io/itables
7
7
  Project-URL: Repository, https://github.com/mwouts/itables.git
@@ -31,7 +31,7 @@ License: MIT License
31
31
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
32
  SOFTWARE.
33
33
  License-File: LICENSE
34
- Keywords: DataTables,Interactive,Javascript,Pandas,Polars
34
+ Keywords: DataTables,Interactive,Javascript,Narwhals,Pandas,Polars
35
35
  Classifier: Development Status :: 5 - Production/Stable
36
36
  Classifier: Framework :: Dash
37
37
  Classifier: Framework :: Jupyter
@@ -45,15 +45,18 @@ Classifier: Programming Language :: Python :: 3.10
45
45
  Classifier: Programming Language :: Python :: 3.11
46
46
  Classifier: Programming Language :: Python :: 3.12
47
47
  Classifier: Programming Language :: Python :: 3.13
48
+ Classifier: Programming Language :: Python :: 3.14
48
49
  Classifier: Typing :: Typed
49
50
  Requires-Python: >=3.9
50
- Requires-Dist: ipython
51
- Requires-Dist: numpy
52
- Requires-Dist: pandas
53
51
  Provides-Extra: all
54
52
  Requires-Dist: anywidget; extra == 'all'
55
53
  Requires-Dist: dash; extra == 'all'
54
+ Requires-Dist: ipython; extra == 'all'
55
+ Requires-Dist: marimo; extra == 'all'
56
56
  Requires-Dist: matplotlib; extra == 'all'
57
+ Requires-Dist: modin[dask]; extra == 'all'
58
+ Requires-Dist: narwhals; extra == 'all'
59
+ Requires-Dist: pandas; extra == 'all'
57
60
  Requires-Dist: platformdirs; extra == 'all'
58
61
  Requires-Dist: polars; extra == 'all'
59
62
  Requires-Dist: pyarrow; extra == 'all'
@@ -64,6 +67,7 @@ Requires-Dist: streamlit; extra == 'all'
64
67
  Requires-Dist: tomli; (python_version < '3.11') and extra == 'all'
65
68
  Requires-Dist: traitlets; extra == 'all'
66
69
  Requires-Dist: typeguard>=4.4.1; extra == 'all'
70
+ Requires-Dist: typing-extensions; extra == 'all'
67
71
  Requires-Dist: world-bank-data; extra == 'all'
68
72
  Provides-Extra: check-type
69
73
  Requires-Dist: typeguard>=4.4.1; extra == 'check-type'
@@ -72,20 +76,28 @@ Requires-Dist: platformdirs; extra == 'config'
72
76
  Requires-Dist: tomli; (python_version < '3.11') and extra == 'config'
73
77
  Provides-Extra: dash
74
78
  Requires-Dist: dash; extra == 'dash'
79
+ Requires-Dist: typing-extensions; extra == 'dash'
75
80
  Provides-Extra: dev
76
81
  Requires-Dist: anywidget; extra == 'dev'
77
82
  Requires-Dist: dash; extra == 'dev'
78
83
  Requires-Dist: ipykernel; extra == 'dev'
84
+ Requires-Dist: ipython; extra == 'dev'
79
85
  Requires-Dist: jupyterlab; extra == 'dev'
80
86
  Requires-Dist: jupytext; extra == 'dev'
81
87
  Requires-Dist: marimo; extra == 'dev'
82
88
  Requires-Dist: matplotlib; extra == 'dev'
89
+ Requires-Dist: modin[dask]; extra == 'dev'
90
+ Requires-Dist: narwhals; extra == 'dev'
83
91
  Requires-Dist: nbconvert; extra == 'dev'
92
+ Requires-Dist: pandas; extra == 'dev'
93
+ Requires-Dist: pandas-stubs; extra == 'dev'
84
94
  Requires-Dist: platformdirs; extra == 'dev'
85
95
  Requires-Dist: polars; extra == 'dev'
86
96
  Requires-Dist: pyarrow; extra == 'dev'
97
+ Requires-Dist: pyright; extra == 'dev'
87
98
  Requires-Dist: pytest; extra == 'dev'
88
99
  Requires-Dist: pytest-cov; extra == 'dev'
100
+ Requires-Dist: pytest-xdist; extra == 'dev'
89
101
  Requires-Dist: pytz; extra == 'dev'
90
102
  Requires-Dist: requests; extra == 'dev'
91
103
  Requires-Dist: shiny; extra == 'dev'
@@ -94,11 +106,23 @@ Requires-Dist: streamlit; extra == 'dev'
94
106
  Requires-Dist: tomli; (python_version < '3.11') and extra == 'dev'
95
107
  Requires-Dist: traitlets; extra == 'dev'
96
108
  Requires-Dist: typeguard>=4.4.1; extra == 'dev'
109
+ Requires-Dist: typing-extensions; extra == 'dev'
97
110
  Requires-Dist: watchfiles; extra == 'dev'
98
111
  Requires-Dist: world-bank-data; extra == 'dev'
112
+ Provides-Extra: marimo
113
+ Requires-Dist: marimo; extra == 'marimo'
114
+ Provides-Extra: narwhals
115
+ Requires-Dist: narwhals; extra == 'narwhals'
116
+ Provides-Extra: notebook
117
+ Requires-Dist: ipython; extra == 'notebook'
118
+ Provides-Extra: other-dataframes
119
+ Requires-Dist: modin[dask]; extra == 'other-dataframes'
120
+ Requires-Dist: narwhals; extra == 'other-dataframes'
121
+ Requires-Dist: pyarrow; extra == 'other-dataframes'
122
+ Provides-Extra: pandas
123
+ Requires-Dist: pandas; extra == 'pandas'
99
124
  Provides-Extra: polars
100
125
  Requires-Dist: polars; extra == 'polars'
101
- Requires-Dist: pyarrow; extra == 'polars'
102
126
  Provides-Extra: samples
103
127
  Requires-Dist: pytz; extra == 'samples'
104
128
  Requires-Dist: world-bank-data; extra == 'samples'
@@ -109,28 +133,56 @@ Provides-Extra: streamlit
109
133
  Requires-Dist: streamlit; extra == 'streamlit'
110
134
  Provides-Extra: style
111
135
  Requires-Dist: matplotlib; extra == 'style'
112
- Provides-Extra: test
113
- Requires-Dist: anywidget; extra == 'test'
114
- Requires-Dist: dash; extra == 'test'
115
- Requires-Dist: ipykernel; extra == 'test'
116
- Requires-Dist: jupytext; extra == 'test'
117
- Requires-Dist: marimo; extra == 'test'
118
- Requires-Dist: matplotlib; extra == 'test'
119
- Requires-Dist: nbconvert; extra == 'test'
120
- Requires-Dist: platformdirs; extra == 'test'
121
- Requires-Dist: polars; extra == 'test'
122
- Requires-Dist: pyarrow; extra == 'test'
123
- Requires-Dist: pytest; extra == 'test'
124
- Requires-Dist: pytest-cov; extra == 'test'
125
- Requires-Dist: pytz; extra == 'test'
126
- Requires-Dist: requests; extra == 'test'
127
- Requires-Dist: shiny; extra == 'test'
128
- Requires-Dist: shinywidgets; extra == 'test'
129
- Requires-Dist: streamlit; extra == 'test'
130
- Requires-Dist: tomli; (python_version < '3.11') and extra == 'test'
131
- Requires-Dist: traitlets; extra == 'test'
132
- Requires-Dist: typeguard>=4.4.1; extra == 'test'
133
- Requires-Dist: world-bank-data; extra == 'test'
136
+ Requires-Dist: pandas; extra == 'style'
137
+ Provides-Extra: test-all
138
+ Requires-Dist: anywidget; extra == 'test-all'
139
+ Requires-Dist: dash; extra == 'test-all'
140
+ Requires-Dist: ipykernel; extra == 'test-all'
141
+ Requires-Dist: ipython; extra == 'test-all'
142
+ Requires-Dist: jupytext; extra == 'test-all'
143
+ Requires-Dist: marimo; extra == 'test-all'
144
+ Requires-Dist: matplotlib; extra == 'test-all'
145
+ Requires-Dist: modin[dask]; extra == 'test-all'
146
+ Requires-Dist: narwhals; extra == 'test-all'
147
+ Requires-Dist: nbconvert; extra == 'test-all'
148
+ Requires-Dist: pandas; extra == 'test-all'
149
+ Requires-Dist: platformdirs; extra == 'test-all'
150
+ Requires-Dist: polars; extra == 'test-all'
151
+ Requires-Dist: pyarrow; extra == 'test-all'
152
+ Requires-Dist: pytest; extra == 'test-all'
153
+ Requires-Dist: pytest-cov; extra == 'test-all'
154
+ Requires-Dist: pytest-xdist; extra == 'test-all'
155
+ Requires-Dist: pytz; extra == 'test-all'
156
+ Requires-Dist: requests; extra == 'test-all'
157
+ Requires-Dist: shiny; extra == 'test-all'
158
+ Requires-Dist: shinywidgets; extra == 'test-all'
159
+ Requires-Dist: streamlit; extra == 'test-all'
160
+ Requires-Dist: tomli; (python_version < '3.11') and extra == 'test-all'
161
+ Requires-Dist: traitlets; extra == 'test-all'
162
+ Requires-Dist: typeguard>=4.4.1; extra == 'test-all'
163
+ Requires-Dist: typing-extensions; extra == 'test-all'
164
+ Requires-Dist: world-bank-data; extra == 'test-all'
165
+ Provides-Extra: test-base
166
+ Requires-Dist: pytest; extra == 'test-base'
167
+ Requires-Dist: pytest-cov; extra == 'test-base'
168
+ Requires-Dist: pytest-xdist; extra == 'test-base'
169
+ Provides-Extra: test-dash
170
+ Requires-Dist: dash; extra == 'test-dash'
171
+ Requires-Dist: pytest; extra == 'test-dash'
172
+ Requires-Dist: pytest-cov; extra == 'test-dash'
173
+ Requires-Dist: pytest-xdist; extra == 'test-dash'
174
+ Requires-Dist: typing-extensions; extra == 'test-dash'
175
+ Provides-Extra: test-notebook
176
+ Requires-Dist: anywidget; extra == 'test-notebook'
177
+ Requires-Dist: ipykernel; extra == 'test-notebook'
178
+ Requires-Dist: ipython; extra == 'test-notebook'
179
+ Requires-Dist: jupytext; extra == 'test-notebook'
180
+ Requires-Dist: nbconvert; extra == 'test-notebook'
181
+ Requires-Dist: pytest; extra == 'test-notebook'
182
+ Requires-Dist: pytest-cov; extra == 'test-notebook'
183
+ Requires-Dist: pytest-xdist; extra == 'test-notebook'
184
+ Requires-Dist: requests; extra == 'test-notebook'
185
+ Requires-Dist: traitlets; extra == 'test-notebook'
134
186
  Provides-Extra: widget
135
187
  Requires-Dist: anywidget; extra == 'widget'
136
188
  Requires-Dist: traitlets; extra == 'widget'
@@ -150,16 +202,14 @@ Description-Content-Type: text/markdown
150
202
  [![Dash Component](https://img.shields.io/badge/Dash-Plotly-1098F7.svg?style=flat&logo=Plotly)](https://mwouts.github.io/itables/apps/dash.html)
151
203
  [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_red.svg)](https://itables.streamlit.app)
152
204
 
153
- This packages changes how Pandas and Polars DataFrames are rendered in Python notebooks and applications.
205
+ This package changes how Pandas and Polars DataFrames are rendered in Python notebooks and applications.
154
206
  With `itables` you can display your tables as interactive [DataTables](https://datatables.net/)
155
207
  that you can sort, paginate, scroll or filter.
156
208
 
157
209
  ITables is just about how tables are displayed. You can turn it on and off in just two lines,
158
210
  with no other impact on your data workflow.
159
211
 
160
- The `itables` package only depends on `numpy`, `pandas` and `IPython`
161
- which you must already have if you work with Pandas in Jupyter (add `polars`, `pyarrow` if you
162
- work with Polars DataFrames).
212
+ Since v2.6.0, ITables has no dependencies. It works out of the box with Pandas or Polars in Jupyter, Dash, Streamlit, or Marimo—you only need these packages installed. The Jupyter Widget is the sole exception, requiring `anywidget`. With Narwhals installed, ITables can also display DataFrames from other libraries like cuDF, Modin or PyArrow.
163
213
 
164
214
  ## Documentation
165
215
 
@@ -210,7 +260,7 @@ ITables is also available as
210
260
  ## Licence
211
261
 
212
262
  ITables is developed by [Marc Wouts](https://github.com/mwouts) on [GitHub](https://github.com/mwouts/itables),
213
- under a MIT license.
263
+ under an MIT license.
214
264
 
215
265
  ITables is a wrapper for [datatables.net](https://datatables.net/) which is developed by Allan Jardine
216
- [(sponsor him!)](https://github.com/sponsors/AllanJard), also under a MIT license.
266
+ [(sponsor him!)](https://github.com/sponsors/AllanJard), also under an MIT license.
@@ -12,16 +12,14 @@
12
12
  [![Dash Component](https://img.shields.io/badge/Dash-Plotly-1098F7.svg?style=flat&logo=Plotly)](https://mwouts.github.io/itables/apps/dash.html)
13
13
  [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_red.svg)](https://itables.streamlit.app)
14
14
 
15
- This packages changes how Pandas and Polars DataFrames are rendered in Python notebooks and applications.
15
+ This package changes how Pandas and Polars DataFrames are rendered in Python notebooks and applications.
16
16
  With `itables` you can display your tables as interactive [DataTables](https://datatables.net/)
17
17
  that you can sort, paginate, scroll or filter.
18
18
 
19
19
  ITables is just about how tables are displayed. You can turn it on and off in just two lines,
20
20
  with no other impact on your data workflow.
21
21
 
22
- The `itables` package only depends on `numpy`, `pandas` and `IPython`
23
- which you must already have if you work with Pandas in Jupyter (add `polars`, `pyarrow` if you
24
- work with Polars DataFrames).
22
+ Since v2.6.0, ITables has no dependencies. It works out of the box with Pandas or Polars in Jupyter, Dash, Streamlit, or Marimo—you only need these packages installed. The Jupyter Widget is the sole exception, requiring `anywidget`. With Narwhals installed, ITables can also display DataFrames from other libraries like cuDF, Modin or PyArrow.
25
23
 
26
24
  ## Documentation
27
25
 
@@ -72,7 +70,7 @@ ITables is also available as
72
70
  ## Licence
73
71
 
74
72
  ITables is developed by [Marc Wouts](https://github.com/mwouts) on [GitHub](https://github.com/mwouts/itables),
75
- under a MIT license.
73
+ under an MIT license.
76
74
 
77
75
  ITables is a wrapper for [datatables.net](https://datatables.net/) which is developed by Allan Jardine
78
- [(sponsor him!)](https://github.com/sponsors/AllanJard), also under a MIT license.
76
+ [(sponsor him!)](https://github.com/sponsors/AllanJard), also under an MIT license.
@@ -10,6 +10,16 @@ from .typing import (
10
10
  )
11
11
  from .version import __version__
12
12
 
13
+ try:
14
+ from itables import sample_pandas_dfs
15
+ except ImportError:
16
+ sample_pandas_dfs = None
17
+
18
+ try:
19
+ from itables import sample_polars_dfs
20
+ except ImportError:
21
+ sample_polars_dfs = None
22
+
13
23
  __all__ = [
14
24
  "__version__",
15
25
  "to_html_datatable",
@@ -24,4 +34,6 @@ __all__ = [
24
34
  "options",
25
35
  "downsample",
26
36
  "sample_dfs",
37
+ "sample_pandas_dfs",
38
+ "sample_polars_dfs",
27
39
  ]
@@ -0,0 +1,211 @@
1
+ import json
2
+ import math
3
+ import sys
4
+ import warnings
5
+ from typing import Any, Optional, Sequence
6
+
7
+ from .typing import DataFrameOrSeries, get_dataframe_module_name
8
+
9
+
10
+ def _format_pandas_series(x, escape_html: bool) -> Sequence[Any]:
11
+ dtype_kind = x.dtype.kind
12
+ if dtype_kind in ["b", "i"]:
13
+ return x
14
+
15
+ if dtype_kind == "s":
16
+ if escape_html:
17
+ return x
18
+ return [escape_html_chars(i) for i in x]
19
+
20
+ import pandas.io.formats.format as fmt
21
+
22
+ try:
23
+ x = fmt.format_array(x._values, None, justify="all", leading_space=False) # type: ignore
24
+ except TypeError:
25
+ # Older versions of Pandas don't have 'leading_space'
26
+ x = fmt.format_array(x._values, None, justify="all") # type: ignore
27
+
28
+ y: Sequence[Any] = x
29
+ if dtype_kind == "f":
30
+ np = sys.modules["numpy"]
31
+ try:
32
+ z = np.array(x).astype(float)
33
+ except ValueError:
34
+ z = x
35
+ pass
36
+
37
+ y = [escape_non_finite_float(f) for f in z]
38
+
39
+ if escape_html:
40
+ return [escape_html_chars(i) for i in y]
41
+
42
+ return y
43
+
44
+
45
+ def _format_polars_series(x, escape_html: bool) -> Sequence[Any]:
46
+ """Format a Polars Series for DataTables display"""
47
+ pl = sys.modules["polars"]
48
+ dtype = x.dtype
49
+
50
+ # Boolean and integer types - return as-is
51
+ if dtype in (
52
+ pl.Boolean,
53
+ pl.Int8,
54
+ pl.Int16,
55
+ pl.Int32,
56
+ pl.Int64,
57
+ pl.UInt8,
58
+ pl.UInt16,
59
+ pl.UInt32,
60
+ pl.UInt64,
61
+ ):
62
+ return x.to_list()
63
+
64
+ # Float types - format and handle non-finite values
65
+ if dtype in (pl.Float32, pl.Float64):
66
+ # Round floats according to Polars config
67
+ precision = pl.Config.state().get("set_float_precision")
68
+ if precision is None:
69
+ values = x.to_list()
70
+ else:
71
+ values = x.round(precision).to_list()
72
+ return [escape_non_finite_float(v) for v in values]
73
+
74
+ # Any other type: convert to string
75
+ try:
76
+ formatted = x.cast(str).to_list()
77
+ except pl.exceptions.InvalidOperationError:
78
+ formatted = [str(i) if i is not None else None for i in x.to_list()]
79
+
80
+ if escape_html:
81
+ return [escape_html_chars(i) for i in formatted]
82
+ return formatted
83
+
84
+
85
+ def _format_narwhals_series(x, escape_html: bool) -> Sequence[Any]:
86
+ """Format a Narwhals Series for DataTables display"""
87
+ nw = sys.modules["narwhals"]
88
+ dtype = x.dtype
89
+
90
+ # Boolean and integer types - return as-is
91
+ if dtype in (
92
+ nw.Boolean,
93
+ nw.Int8,
94
+ nw.Int16,
95
+ nw.Int32,
96
+ nw.Int64,
97
+ nw.UInt8,
98
+ nw.UInt16,
99
+ nw.UInt32,
100
+ nw.UInt64,
101
+ ):
102
+ return [v for v in x]
103
+
104
+ # Float types - format and handle non-finite values
105
+ if dtype in (nw.Float32, nw.Float64):
106
+ return [escape_non_finite_float(v) for v in x]
107
+
108
+ formatted = [str(v) for v in x]
109
+ if escape_html:
110
+ return [escape_html_chars(i) for i in formatted]
111
+ return formatted
112
+
113
+
114
+ def escape_non_finite_float(value: Any) -> Any:
115
+ """Encode non-finite float values to strings that will be parsed by parseJSON"""
116
+ if not isinstance(value, float):
117
+ return value
118
+ if math.isnan(value):
119
+ return "___NaN___"
120
+ if value == math.inf:
121
+ return "___Infinity___"
122
+ if value == -math.inf:
123
+ return "___-Infinity___"
124
+ return value
125
+
126
+
127
+ def escape_html_chars(value: Any) -> Any:
128
+ """Escape HTML special characters"""
129
+ if isinstance(value, str):
130
+ return value.replace("&", r"&amp;").replace("<", r"&lt;").replace(">", r"&gt;")
131
+ return value
132
+
133
+
134
+ def generate_encoder(warn_on_unexpected_types: bool = True) -> Any:
135
+ """Generate a JSON encoder that can handle special types like numpy"""
136
+
137
+ class TableValuesEncoder(json.JSONEncoder):
138
+ def default(self, o):
139
+ if isinstance(o, (bool, int, float, str)):
140
+ return json.JSONEncoder.default(self, o)
141
+ module = type(o).__module__
142
+ if module == "numpy":
143
+ import numpy as np
144
+
145
+ if isinstance(o, np.bool_):
146
+ return bool(o)
147
+ if isinstance(o, np.integer):
148
+ return int(o)
149
+ if isinstance(o, np.floating):
150
+ return float(o)
151
+ pd = sys.modules.get("pandas", None)
152
+ if pd is not None and o is pd.NA:
153
+ return str(o)
154
+
155
+ if warn_on_unexpected_types:
156
+ warnings.warn(
157
+ f"Unexpected type '{type(o)}' for '{o}'.\n"
158
+ "You can report this warning at https://github.com/mwouts/itables/issues\n"
159
+ "To silence this warning, please run:\n"
160
+ " itables.options.warn_on_unexpected_types = False",
161
+ category=RuntimeWarning,
162
+ )
163
+ return str(o)
164
+
165
+ return TableValuesEncoder
166
+
167
+
168
+ def datatables_rows(
169
+ df: DataFrameOrSeries,
170
+ column_count: Optional[int] = None,
171
+ warn_on_unexpected_types: bool = False,
172
+ escape_html: bool = True,
173
+ ) -> str:
174
+ """Format the values in the table and return the data, row by row, as requested by DataTables"""
175
+ # We iterate over columns using an index rather than the column name
176
+ # to avoid an issue in case of duplicated column names #89
177
+ if column_count is None or len(df.columns) == column_count:
178
+ empty_columns = []
179
+ else:
180
+ # When the header requires more columns (#141), we append empty columns on the left
181
+ missing_columns = column_count - len(df.columns)
182
+ assert missing_columns > 0
183
+ empty_columns = [[None] * len(df)] * missing_columns
184
+
185
+ df_module = get_dataframe_module_name(df)
186
+ if df_module == "pandas":
187
+ formatted_columns = [
188
+ _format_pandas_series(x, escape_html) for _, x in df.items()
189
+ ]
190
+ elif df_module == "polars":
191
+ formatted_columns = [
192
+ _format_polars_series(df[col], escape_html) for col in df.columns
193
+ ]
194
+ else:
195
+ # Other DataFrame types are handled via Narwhals, and are expected
196
+ # to have been converted to Narwhals already (except in tests)
197
+ import narwhals as nw
198
+
199
+ df = nw.from_native(df, eager_only=True, allow_series=True)
200
+
201
+ formatted_columns = [
202
+ _format_narwhals_series(df[col], escape_html) for col in df.columns
203
+ ]
204
+
205
+ data = list(zip(*(empty_columns + formatted_columns)))
206
+
207
+ return json.dumps(
208
+ data,
209
+ cls=generate_encoder(warn_on_unexpected_types),
210
+ allow_nan=False,
211
+ )
@@ -1,16 +1,23 @@
1
1
  import math
2
2
  from typing import Union
3
3
 
4
- import pandas as pd
4
+ from itables.typing import (
5
+ DataFrameModuleName,
6
+ DataFrameOrSeries,
7
+ get_dataframe_module_name,
8
+ )
5
9
 
6
- from itables.typing import DataFrameOrSeries
7
10
 
11
+ def nbytes(df: DataFrameOrSeries, df_module_name: DataFrameModuleName = None) -> int:
12
+ """Return an estimate for number of bytes used by the dataframe"""
13
+ if df_module_name is None:
14
+ df_module_name = get_dataframe_module_name(df)
15
+ assert df_module_name is not None
8
16
 
9
- def nbytes(df: DataFrameOrSeries) -> int:
10
- try:
17
+ if df_module_name in ["pandas", "numpy"]:
11
18
  return sum(x.values.nbytes for _, x in df.items())
12
- except AttributeError:
13
- # Polars DataFrame
19
+ else:
20
+ # Polars or Narwhalified DataFrame
14
21
  return df.estimated_size()
15
22
 
16
23
 
@@ -34,19 +41,31 @@ def as_nbytes(mem: Union[int, float, str]) -> int:
34
41
 
35
42
  def downsample(
36
43
  df: DataFrameOrSeries,
44
+ df_module_name: DataFrameModuleName = None,
37
45
  max_rows: int = 0,
38
46
  max_columns: int = 0,
39
47
  max_bytes: Union[int, str] = 0,
40
48
  ) -> tuple[DataFrameOrSeries, str]:
41
49
  """Return a subset of the dataframe that fits the limits"""
42
- org_rows, org_columns, org_bytes = len(df), len(df.columns), nbytes(df)
50
+ if df_module_name is None:
51
+ df_module_name = get_dataframe_module_name(df)
52
+
53
+ org_rows, org_columns, org_bytes = (
54
+ len(df),
55
+ len(df.columns),
56
+ nbytes(df, df_module_name),
57
+ )
43
58
  max_bytes_numeric = as_nbytes(max_bytes)
44
59
  df = _downsample(
45
- df, max_rows=max_rows, max_columns=max_columns, max_bytes=max_bytes_numeric
60
+ df,
61
+ df_module_name,
62
+ max_rows=max_rows,
63
+ max_columns=max_columns,
64
+ max_bytes=max_bytes_numeric,
46
65
  )
47
66
 
48
67
  if len(df) < org_rows or len(df.columns) < org_columns:
49
- link = '<a href="https://mwouts.github.io/itables/downsampling.html">downsampled</a>'
68
+ downsampled = '<a href="https://mwouts.github.io/itables/downsampling.html">downsampled</a>'
50
69
  reasons = []
51
70
  if org_rows > max_rows > 0:
52
71
  reasons.append("maxRows={}".format(max_rows))
@@ -55,16 +74,14 @@ def downsample(
55
74
  if org_bytes > max_bytes_numeric > 0:
56
75
  reasons.append("maxBytes={}".format(max_bytes))
57
76
 
58
- warning = "{} from {:,d}x{:,d} to {:,d}x{:,d} as {}".format(
59
- link,
60
- org_rows,
61
- org_columns,
62
- len(df),
63
- len(df.columns),
64
- " and ".join(reasons),
65
- )
77
+ if len(df.columns) < org_columns:
78
+ downsampled_warning = f"{downsampled} from {org_rows:,d}x{org_columns:,d} to {len(df):,d}x{len(df.columns):,d} as {' and '.join(reasons)}"
79
+ else:
80
+ downsampled_warning = (
81
+ f"{downsampled} from {org_rows:,d} rows as {' and '.join(reasons)}"
82
+ )
66
83
 
67
- return df, warning
84
+ return df, downsampled_warning
68
85
 
69
86
  return df, ""
70
87
 
@@ -98,41 +115,42 @@ def shrink_towards_target_aspect_ratio(
98
115
  return int(rows * row_shrink_factor), int(columns * column_shrink_factor)
99
116
 
100
117
 
101
- def _downsample(df, max_rows=0, max_columns=0, max_bytes=0, target_aspect_ratio=None):
118
+ def _downsample(
119
+ df,
120
+ df_module_name: DataFrameModuleName,
121
+ max_rows=0,
122
+ max_columns=0,
123
+ max_bytes=0,
124
+ target_aspect_ratio=None,
125
+ ):
102
126
  """Implementation of downsample - may be called recursively"""
103
127
  if len(df) > max_rows > 0:
104
128
  second_half = max_rows // 2
105
129
  first_half = max_rows - second_half
106
130
  assert first_half >= second_half
107
- if second_half:
108
- try:
109
- df = pd.concat((df.iloc[:first_half], df.iloc[-second_half:]))
110
- except AttributeError:
111
- df = df.head(first_half).vstack(df.tail(second_half))
131
+ rows = list(range(first_half)) + list(range(len(df) - second_half, len(df)))
132
+ if df_module_name == "pandas":
133
+ df = df.iloc[rows]
134
+ elif df_module_name == "polars":
135
+ df = df[rows]
112
136
  else:
113
- try:
114
- df = df.iloc[:first_half]
115
- except AttributeError:
116
- df = df.head(first_half)
137
+ raise TypeError(f"Unsupported DataFrame type: {df_module_name}")
117
138
 
118
139
  if len(df.columns) > max_columns > 0:
119
140
  second_half = max_columns // 2
120
141
  first_half = max_columns - second_half
121
142
  assert first_half >= second_half
122
- if second_half:
123
- if isinstance(df, pd.DataFrame):
124
- df = pd.concat(
125
- (df.iloc[:, :first_half], df.iloc[:, -second_half:]), axis=1
126
- )
127
- else:
128
- df = df[df.columns[:first_half]].hstack(df[df.columns[-second_half:]])
143
+ columns = list(range(first_half)) + list(
144
+ range(len(df.columns) - second_half, len(df.columns))
145
+ )
146
+ if df_module_name == "pandas":
147
+ df = df.iloc[:, columns]
148
+ elif df_module_name == "polars":
149
+ df = df[[df.columns[i] for i in columns]]
129
150
  else:
130
- if isinstance(df, pd.DataFrame):
131
- df = df.iloc[:, :first_half]
132
- else:
133
- df = df[df.columns[:first_half]]
151
+ raise TypeError(f"Unsupported DataFrame type: {df_module_name}")
134
152
 
135
- df_nbytes = nbytes(df)
153
+ df_nbytes = nbytes(df, df_module_name)
136
154
  if df_nbytes > max_bytes > 0:
137
155
  if target_aspect_ratio is None:
138
156
  if max_rows > 0 and max_columns > 0:
@@ -149,19 +167,16 @@ def _downsample(df, max_rows=0, max_columns=0, max_bytes=0, target_aspect_ratio=
149
167
 
150
168
  if max_rows > 0 and max_columns > 0:
151
169
  return _downsample(
152
- df, max_rows, max_columns, max_bytes, target_aspect_ratio
170
+ df,
171
+ df_module_name,
172
+ max_rows,
173
+ max_columns,
174
+ max_bytes,
175
+ target_aspect_ratio,
153
176
  )
154
177
 
155
178
  # max_bytes is smaller than the average size of one cell
156
- if isinstance(df, pd.DataFrame):
157
- return pd.DataFrame("...", index=df.index[:1], columns=df.columns[:1])
158
-
159
- else:
160
- import polars as pl # noqa
161
-
162
- assert isinstance(df, pl.DataFrame)
163
-
164
- df = pl.DataFrame({df.columns[0]: ["..."]})
165
- return df
179
+ # return a single cell with "..."
180
+ return type(df)({df.columns[0]: ["..."]})
166
181
 
167
182
  return df