table3 0.3.0__tar.gz → 0.5.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.
- table3-0.5.0/CHANGELOG.md +81 -0
- {table3-0.3.0/src/table3.egg-info → table3-0.5.0}/PKG-INFO +26 -5
- {table3-0.3.0 → table3-0.5.0}/README.md +23 -4
- {table3-0.3.0 → table3-0.5.0}/docs/design.md +11 -6
- table3-0.5.0/docs/explorer.md +145 -0
- {table3-0.3.0 → table3-0.5.0}/pyproject.toml +3 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3/__init__.py +65 -10
- table3-0.5.0/src/table3/__version__.py +1 -0
- table3-0.5.0/src/table3/_explore.py +119 -0
- table3-0.5.0/src/table3/_explorer_js.py +281 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3/_format.py +10 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3/_html.py +16 -7
- {table3-0.3.0 → table3-0.5.0}/src/table3/_pager.py +25 -1
- table3-0.5.0/src/table3/_solveit.py +93 -0
- {table3-0.3.0 → table3-0.5.0/src/table3.egg-info}/PKG-INFO +26 -5
- {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/SOURCES.txt +8 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/requires.txt +3 -0
- table3-0.5.0/tests/test_explore.py +123 -0
- table3-0.5.0/tests/test_explorer_browser.py +185 -0
- table3-0.5.0/tests/test_pager_browser.py +88 -0
- table3-0.5.0/tests/test_solveit.py +110 -0
- {table3-0.3.0 → table3-0.5.0}/tests/test_table3.py +37 -6
- table3-0.3.0/CHANGELOG.md +0 -40
- table3-0.3.0/src/table3/__version__.py +0 -1
- {table3-0.3.0 → table3-0.5.0}/LICENSE +0 -0
- {table3-0.3.0 → table3-0.5.0}/MANIFEST.in +0 -0
- {table3-0.3.0 → table3-0.5.0}/setup.cfg +0 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3/py.typed +0 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/dependency_links.txt +0 -0
- {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- In SolveIt, `view()` hides its cell from the AI (SolveIt's red eye), as
|
|
8
|
+
plot3 does for plots, so the explorer's rows don't fill the model's
|
|
9
|
+
context. SolveIt hides the cell's code with its output. `view(hide=False)`
|
|
10
|
+
keeps one cell visible, and `table3.autohide(False)` turns it off for the
|
|
11
|
+
session. `show()` and printed tables are not hidden. Outside SolveIt
|
|
12
|
+
nothing changes.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- A paged table writes only its first page as styled cells, and every row
|
|
17
|
+
as compact JSON its script draws into the other pages: about a quarter
|
|
18
|
+
of the size (a printed 8-column frame of 100 rows, 23 KB instead of
|
|
19
|
+
86 KB), which matters in SolveIt, where a cell's output goes to the AI.
|
|
20
|
+
Where scripts don't run, the first page shows. `pages=False` is
|
|
21
|
+
unchanged.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The explorer's headers show that they sort before the first click: a
|
|
26
|
+
faint ↕ beside each name, which turns into ▲ or ▼ on the sorted column,
|
|
27
|
+
and "Click to sort by …" on hover.
|
|
28
|
+
|
|
29
|
+
## 0.4.0 — 2026-11-03
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `table3.view(data)` opens a data explorer over the first 10,000 rows, as
|
|
34
|
+
R's `View()`: one grid to scroll, with row numbers, a search box, headers
|
|
35
|
+
that sort on a click (ascending, descending, off; nulls last), and a filter
|
|
36
|
+
under each column: from–to for numbers and dates (`2008` matches a year),
|
|
37
|
+
"contains" for text, and a pick-list for booleans and columns of at most
|
|
38
|
+
50 values. The status line counts the rows that pass ("120 of 344 rows").
|
|
39
|
+
It runs in the notebook's browser, so it works in saved notebooks; the
|
|
40
|
+
rows are JSON drawn a screen at a time, about 1 MB for 10,000 rows of a
|
|
41
|
+
dozen columns. Where output scripts don't run, it shows the first 100
|
|
42
|
+
rows as `show()` does. [docs/explorer.md](docs/explorer.md) has the design.
|
|
43
|
+
|
|
44
|
+
## 0.3.0 — 2026-11-02
|
|
45
|
+
|
|
46
|
+
table3 is view3 renamed: the table is how a frame prints in a notebook, as
|
|
47
|
+
R's `print()`, and `view` is kept for a data explorer, as R's `View()`. Its
|
|
48
|
+
versions continue view3's, whose entries are below; this is table3's first
|
|
49
|
+
release on PyPI.
|
|
50
|
+
|
|
51
|
+
### Changed from view3 0.2.0
|
|
52
|
+
|
|
53
|
+
- `view3.view()` is `table3.show()`, and there is no `view()`. Nothing used
|
|
54
|
+
`view()` yet.
|
|
55
|
+
- The hook is `__table3__`, not `__view3__`.
|
|
56
|
+
- The footer no longer names `n=`: "1–10 of 100 rows (first 100 of 344)".
|
|
57
|
+
A package showing the table as its frames' notebook display sets *n*
|
|
58
|
+
itself.
|
|
59
|
+
|
|
60
|
+
## view3 0.2.0 — 2026-11-02
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- `digits=` chooses how floats print. `digits=6` keeps six significant
|
|
65
|
+
digits, as R's `signif()` does, so small numbers keep their precision
|
|
66
|
+
(`0.000123457`, `1.5e-7`) where the default shows `0.000123`;
|
|
67
|
+
`digits="full"` shows every stored digit. The default, `None`, prints as
|
|
68
|
+
polars' text preview does, as before.
|
|
69
|
+
|
|
70
|
+
## view3 0.1.0 — 2026-11-02
|
|
71
|
+
|
|
72
|
+
Phase 1 of [the design](docs/design.md).
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- `view3.view(data)` shows up to `n` rows in a paged table and returns
|
|
77
|
+
`data`; `view3.html(data)` returns the HTML. Ten rows a page
|
|
78
|
+
(`page_size=`), column pages for wide tables, and a scroll box with pinned
|
|
79
|
+
names and types where output scripts don't run or with `pages=False`.
|
|
80
|
+
- polars DataFrames, LazyFrames and Series, pandas DataFrames and Series, and
|
|
81
|
+
any object with a `__view3__(n)` method.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: table3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,6 +19,8 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
19
19
|
Requires-Dist: ipython>=8.0; extra == "dev"
|
|
20
20
|
Requires-Dist: pandas>=2.1.4; extra == "dev"
|
|
21
21
|
Requires-Dist: pyarrow>=15.0; extra == "dev"
|
|
22
|
+
Provides-Extra: browser
|
|
23
|
+
Requires-Dist: pytest-playwright>=0.5; extra == "browser"
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# table3
|
|
@@ -51,7 +53,26 @@ full value on hover. Wide tables show as many columns as fit, with
|
|
|
51
53
|
arrows to the rest. The table follows the notebook's light or dark theme in
|
|
52
54
|
VS Code, JupyterLab and SolveIt.
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
## Explore
|
|
57
|
+
|
|
58
|
+
`table3.view(penguins)` opens a data explorer, as R's `View()` does: one
|
|
59
|
+
grid of up to 10,000 rows with a search box, headers that sort on a click,
|
|
60
|
+
and a filter under each column (from–to for numbers and dates, "contains"
|
|
61
|
+
for text, a pick-list for columns of few values).
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
table3.view(penguins);
|
|
65
|
+
table3.view(penguins, n=50_000, height=600); # more rows, a taller grid
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
In SolveIt, which sends every cell's code and output to its AI, `view()`
|
|
69
|
+
hides its cell from the AI (the red eye), so the explorer's rows don't fill
|
|
70
|
+
the model's context; the cell's code is hidden with them. `view(hide=False)`
|
|
71
|
+
keeps a cell visible, and `table3.autohide(False)` stops hiding for the
|
|
72
|
+
session.
|
|
73
|
+
|
|
74
|
+
`show()` and `view()` return what they were given, so they can sit in the
|
|
75
|
+
middle of a pipe.
|
|
55
76
|
As a cell's last line the notebook also prints that value; end the line with
|
|
56
77
|
`;` to hide it.
|
|
57
78
|
|
|
@@ -72,6 +93,6 @@ def _repr_html_(self):
|
|
|
72
93
|
## More
|
|
73
94
|
|
|
74
95
|
[docs/design.md](docs/design.md) records the design and what each package
|
|
75
|
-
adds
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
adds, and [docs/explorer.md](docs/explorer.md) the explorer's. table3 was
|
|
97
|
+
first released as view3 0.1 and 0.2, where `show()` was `view()`; its
|
|
98
|
+
versions continue from them, and `view()` is now the explorer.
|
|
@@ -28,7 +28,26 @@ full value on hover. Wide tables show as many columns as fit, with
|
|
|
28
28
|
arrows to the rest. The table follows the notebook's light or dark theme in
|
|
29
29
|
VS Code, JupyterLab and SolveIt.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
## Explore
|
|
32
|
+
|
|
33
|
+
`table3.view(penguins)` opens a data explorer, as R's `View()` does: one
|
|
34
|
+
grid of up to 10,000 rows with a search box, headers that sort on a click,
|
|
35
|
+
and a filter under each column (from–to for numbers and dates, "contains"
|
|
36
|
+
for text, a pick-list for columns of few values).
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
table3.view(penguins);
|
|
40
|
+
table3.view(penguins, n=50_000, height=600); # more rows, a taller grid
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
In SolveIt, which sends every cell's code and output to its AI, `view()`
|
|
44
|
+
hides its cell from the AI (the red eye), so the explorer's rows don't fill
|
|
45
|
+
the model's context; the cell's code is hidden with them. `view(hide=False)`
|
|
46
|
+
keeps a cell visible, and `table3.autohide(False)` stops hiding for the
|
|
47
|
+
session.
|
|
48
|
+
|
|
49
|
+
`show()` and `view()` return what they were given, so they can sit in the
|
|
50
|
+
middle of a pipe.
|
|
32
51
|
As a cell's last line the notebook also prints that value; end the line with
|
|
33
52
|
`;` to hide it.
|
|
34
53
|
|
|
@@ -49,6 +68,6 @@ def _repr_html_(self):
|
|
|
49
68
|
## More
|
|
50
69
|
|
|
51
70
|
[docs/design.md](docs/design.md) records the design and what each package
|
|
52
|
-
adds
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
adds, and [docs/explorer.md](docs/explorer.md) the explorer's. table3 was
|
|
72
|
+
first released as view3 0.1 and 0.2, where `show()` was `view()`; its
|
|
73
|
+
versions continue from them, and `view()` is now the explorer.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Status: accepted 2026-11-02. Released first as view3 (0.1.0 phase 1 of §6,
|
|
4
4
|
0.2.0 `digits=` of §2.1); renamed table3 for 0.3.0, with `show()` and the
|
|
5
|
-
table as tidy3's notebook display (§5).
|
|
5
|
+
table as tidy3's notebook display (§5), which tidy3 0.18.0 uses (phase 2).
|
|
6
6
|
|
|
7
7
|
tidy3 previews showed ten rows. To see more, a notebook user had to `pull()`
|
|
8
8
|
a column and call `.to_list()` on the parenthesized pipe, losing column
|
|
@@ -76,9 +76,13 @@ significant digits and converting the column would print its type as `str`.
|
|
|
76
76
|
scripts don't run, the table stays a box `height` pixels tall (default
|
|
77
77
|
300) that scrolls both ways under a pinned header. `pages=False` asks for
|
|
78
78
|
that box everywhere.
|
|
79
|
-
- **Size.**
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
- **Size.** A styled cell is about 100 bytes, and SolveIt sends a cell's
|
|
80
|
+
output to its AI, so a paged table writes only its first page as cells
|
|
81
|
+
and every row as compact JSON, which its script draws into the other
|
|
82
|
+
pages (0.5.0): `penguins`, 8 columns of 100 rows, is 23 KB, not 86 KB.
|
|
83
|
+
Without scripts the first page shows. `pages=False` writes every row as
|
|
84
|
+
cells, about 1.2 MB for 1,000 rows of 12 columns; the font is set once on
|
|
85
|
+
the table to halve that.
|
|
82
86
|
|
|
83
87
|
## 4. Interface
|
|
84
88
|
|
|
@@ -119,5 +123,6 @@ leaves the names alone.
|
|
|
119
123
|
2. tidy3: dependency, `__table3__`, the table as `_repr_html_`, reference docs,
|
|
120
124
|
release.
|
|
121
125
|
3. stats3 and tidy3a: hooks and docs.
|
|
122
|
-
4. Later:
|
|
123
|
-
|
|
126
|
+
4. Later: loading further rows while paging.
|
|
127
|
+
5. A data explorer, `table3.view()`, as R's `View()`: planned in
|
|
128
|
+
[explorer.md](explorer.md).
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# A data explorer: `table3.view()`
|
|
2
|
+
|
|
3
|
+
Status: accepted 2026-11-03; phase 1 released in table3 0.4.0.
|
|
4
|
+
|
|
5
|
+
A printed frame shows as a paged table (design.md). R has a second way to
|
|
6
|
+
look at data: `View(df)` opens RStudio's data viewer, a spreadsheet-like
|
|
7
|
+
grid where you search, sort and filter without writing code. This document
|
|
8
|
+
plans that viewer for table3: `table3.view(data)`, and a `view()` verb in
|
|
9
|
+
tidy3.
|
|
10
|
+
|
|
11
|
+
## 1. What R's `View()` does
|
|
12
|
+
|
|
13
|
+
- One grid of every row, scrolled rather than paged, with row numbers and a
|
|
14
|
+
pinned header.
|
|
15
|
+
- **Search** across every column in one box.
|
|
16
|
+
- **Sort** by clicking a column header: ascending, descending, off.
|
|
17
|
+
- **Filter** per column: a range for numbers and dates, text match for
|
|
18
|
+
strings, a pick-list for factors.
|
|
19
|
+
- A status line: "Showing 1 to 120 of 344 entries, 8 total columns".
|
|
20
|
+
- It reads only what it shows from the R session, so millions of rows work.
|
|
21
|
+
|
|
22
|
+
## 2. Where filtering runs
|
|
23
|
+
|
|
24
|
+
**Phase 1: in the browser.** `view()` writes the first *n* rows into the
|
|
25
|
+
output once, as compact JSON, and a script draws the grid and runs search,
|
|
26
|
+
sort and filters on them.
|
|
27
|
+
|
|
28
|
+
- It works where the paged table works: SolveIt runs output scripts and runs
|
|
29
|
+
them again when a dialog is reloaded (probed 2026-11-02); VS Code and
|
|
30
|
+
trusted Jupyter notebooks run them too. A saved notebook reopens with the
|
|
31
|
+
explorer working.
|
|
32
|
+
- No new dependency, no kernel connection.
|
|
33
|
+
- It explores only the rows it was given: `n=10_000` by default, about 1 MB
|
|
34
|
+
for a dozen columns. The status line says when *n* cut the rows ("120 of
|
|
35
|
+
10,000 rows (first 10,000 of 52,341)").
|
|
36
|
+
- Rows are JSON, not HTML: 10,000 rows of HTML table cells with inline
|
|
37
|
+
styles would be about 12 MB. The script draws only the rows in view
|
|
38
|
+
(virtual scrolling), so the grid stays fast at 10,000 rows.
|
|
39
|
+
|
|
40
|
+
**Phase 3 (later, if needed): through the kernel.** A live widget (anywidget)
|
|
41
|
+
sends each search, sort or filter to polars and gets one screen of rows
|
|
42
|
+
back, so it explores any number of rows lazily. It needs a live kernel and
|
|
43
|
+
shows nothing in a saved notebook, and SolveIt's and CRAFT's support for
|
|
44
|
+
widgets was unknown. It would be an optional extra (`table3[explore]`), and a
|
|
45
|
+
probe like the script probe decided whether to build it.
|
|
46
|
+
|
|
47
|
+
Probed 2026-11-03: SolveIt does not draw Jupyter widgets. An anywidget whose
|
|
48
|
+
button sends a click to the kernel printed as its text,
|
|
49
|
+
`<__main__.Probe object at 0x…>`, with no button, so a widget has no kernel
|
|
50
|
+
connection there. The kernel-backed mode is not built: the in-browser
|
|
51
|
+
explorer is the only one, and `n=` is how to explore more rows. A widget
|
|
52
|
+
mode for VS Code and Jupyter alone would split the explorer by notebook, so
|
|
53
|
+
it waits for a need that `n=` can't meet.
|
|
54
|
+
|
|
55
|
+
## 3. Interface
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
table3.view(data, *, n=10_000, height=500, digits=None, caption=None, hide=None) # shows, returns data
|
|
59
|
+
table3.autohide(on=True) # whether view() hides its cell from SolveIt's AI
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
*data* is anything `html()` and `show()` take, `__table3__` objects
|
|
63
|
+
included. It returns *data* unchanged, like `show()`; `;` hides the
|
|
64
|
+
notebook's own print of it. Outside a notebook it prints the first rows as
|
|
65
|
+
`show()` does.
|
|
66
|
+
|
|
67
|
+
In SolveIt, which sends each cell's code and output to its AI, `view()`
|
|
68
|
+
then marks its cell hidden from the AI (`dialoghelper.update_msg(skipped=1)`,
|
|
69
|
+
SolveIt's red eye), as plot3 does for plots: the rows would fill the
|
|
70
|
+
model's context. SolveIt has no flag for an output alone, so the cell's code
|
|
71
|
+
is hidden too; a `view()` cell is usually one line. `show()` and printed
|
|
72
|
+
tables are not hidden, since most analysis cells end in a frame and hiding
|
|
73
|
+
those would hide their code. `hide=False`, or `autohide(False)` for the
|
|
74
|
+
session, keeps cells visible. Added in 0.5.0.
|
|
75
|
+
|
|
76
|
+
tidy3 adds `view()` as a verb and method, as `glimpse()`: `penguins >> view()`,
|
|
77
|
+
`view(penguins)`, `penguins.view()`, passing the frame through. It is the
|
|
78
|
+
only bare `view` in the family; tidy3a and stats3 document `table3.view(x)`.
|
|
79
|
+
|
|
80
|
+
## 4. The grid
|
|
81
|
+
|
|
82
|
+
- **Header**, pinned: name, type (`i64`, `str`), and a sort arrow; clicking
|
|
83
|
+
cycles ascending, descending, off. One sort column at a time, as R.
|
|
84
|
+
- **Filter row** under the header, one control per column by type:
|
|
85
|
+
- numbers and dates: two boxes, from and to, either blank;
|
|
86
|
+
- strings: contains, case-insensitive;
|
|
87
|
+
- strings with at most 50 distinct values, booleans and categoricals: a
|
|
88
|
+
pick-list of their values, with `null` as one choice.
|
|
89
|
+
- **Search box** above: rows where any cell's text contains it.
|
|
90
|
+
- **Row numbers** on the left: the row's place in *data*, kept through sorts
|
|
91
|
+
and filters, as R shows them.
|
|
92
|
+
- **Status line**: "120 of 344 rows | 8 columns", plus the *n* note.
|
|
93
|
+
- **Cells** as the table formats them: `null`, floats by `digits=`, long
|
|
94
|
+
values cut with the full value on hover. Sorting and filters use the raw
|
|
95
|
+
values, so numbers sort as numbers and nulls go last.
|
|
96
|
+
- **Wide frames** scroll sideways with the row numbers pinned at the left.
|
|
97
|
+
- **Theme**: table3's colours and translucent greys, every style inline (set
|
|
98
|
+
by the script), no `<style>` block.
|
|
99
|
+
|
|
100
|
+
**Without scripts** (an untrusted Jupyter notebook, perhaps CRAFT's
|
|
101
|
+
republish) the output is `show()`'s table of the first 100 rows, with a line
|
|
102
|
+
saying the explorer needs output scripts.
|
|
103
|
+
|
|
104
|
+
## 5. Data in the output
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{"columns": [{"name": "species", "type": "str", "kind": "text", "choices": ["Adelie", "Chinstrap", "Gentoo"]},
|
|
108
|
+
{"name": "body_mass_g", "type": "i64", "kind": "number"}],
|
|
109
|
+
"rows": [["Adelie", 3750], ["Adelie", null]],
|
|
110
|
+
"shown": [null, ["3750"]],
|
|
111
|
+
"total": 344, "n": 10000}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `rows` holds raw values for sorting and filtering: numbers as numbers,
|
|
115
|
+
dates and times as ISO text (which sorts in order), everything else as
|
|
116
|
+
text, `null` for missing.
|
|
117
|
+
- `shown` holds a column's display text only where it differs from the raw
|
|
118
|
+
value: floats (`digits=`) and values cut at 40 characters. Other columns
|
|
119
|
+
show their raw text.
|
|
120
|
+
- `kind` picks the filter: `number`, `date`, `text`, `choice`, `bool`.
|
|
121
|
+
- The JSON goes in a `<script type="application/json">` element beside the
|
|
122
|
+
grid, escaped so a value holding `</script>` can't end it.
|
|
123
|
+
|
|
124
|
+
## 6. Tests
|
|
125
|
+
|
|
126
|
+
- **pytest**: the JSON (kinds, choices, raw and shown values, escaping, the
|
|
127
|
+
*n* cut, `__table3__` objects, pandas), `view()` returning its input, the
|
|
128
|
+
no-script fallback.
|
|
129
|
+
- **pytest with Playwright** (headless Chromium): load the output in a page
|
|
130
|
+
and drive it: sort cycles, each filter kind, search, row numbers kept,
|
|
131
|
+
status line counts, virtual scrolling at 10,000 rows, a value holding
|
|
132
|
+
`</script>`. Its own CI job (`pip install pytest-playwright`, `playwright
|
|
133
|
+
install chromium`) and a `scripts/check.sh` step, as neither the Mac nor
|
|
134
|
+
gpudev has a headless browser installed.
|
|
135
|
+
- **By eye**: `docs/check.ipynb` cells for the explorer, in SolveIt (dark
|
|
136
|
+
theme) and VS Code.
|
|
137
|
+
|
|
138
|
+
## 7. Phases
|
|
139
|
+
|
|
140
|
+
1. table3 0.4.0: `table3.view()` in the browser (§2, §4–§6).
|
|
141
|
+
2. tidy3: the `view()` verb and method, reference docs, release.
|
|
142
|
+
3. Probe SolveIt and CRAFT for widgets; if they work and in-browser limits
|
|
143
|
+
bite, a kernel-backed mode as `table3[explore]`. Done 2026-11-03: SolveIt
|
|
144
|
+
does not draw widgets (§2), so there is no kernel-backed mode.
|
|
145
|
+
4. tidy3a and stats3: document `table3.view(x)`.
|
|
@@ -27,6 +27,9 @@ dependencies = [
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
28
|
# pandas and pyarrow: the pandas input tests.
|
|
29
29
|
dev = ["pytest>=7.0", "ipython>=8.0", "pandas>=2.1.4", "pyarrow>=15.0"]
|
|
30
|
+
# The explorer's and the pages' scripts in headless Chromium (tests/test_*_browser.py);
|
|
31
|
+
# also run: python -m playwright install chromium
|
|
32
|
+
browser = ["pytest-playwright>=0.5"]
|
|
30
33
|
|
|
31
34
|
[project.urls]
|
|
32
35
|
Homepage = "https://github.com/rleyvasal/table3"
|
|
@@ -5,6 +5,8 @@ Previous / page / Next buttons and, for wide data, as many columns as fit
|
|
|
5
5
|
the cell plus arrows to the rest: what a package returns from its frames'
|
|
6
6
|
``_repr_html_`` so they print as R notebooks print data frames.
|
|
7
7
|
``table3.show(data)`` displays the same table and returns *data* unchanged.
|
|
8
|
+
``table3.view(data)`` opens a data explorer instead, as R's ``View()``: a
|
|
9
|
+
grid of up to 10,000 rows to search, sort and filter.
|
|
8
10
|
|
|
9
11
|
*data* is a polars DataFrame, LazyFrame or Series, a pandas DataFrame or
|
|
10
12
|
Series, or any object with a ``__table3__`` method::
|
|
@@ -24,8 +26,10 @@ import polars as pl
|
|
|
24
26
|
|
|
25
27
|
from table3.__version__ import __version__
|
|
26
28
|
from table3._html import table_html
|
|
29
|
+
from table3 import _solveit
|
|
30
|
+
from table3._solveit import autohide
|
|
27
31
|
|
|
28
|
-
__all__ = ["__version__", "html", "show"]
|
|
32
|
+
__all__ = ["__version__", "autohide", "html", "show", "view"]
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
def _from_pandas(df: Any) -> pl.DataFrame:
|
|
@@ -109,6 +113,19 @@ def html(
|
|
|
109
113
|
)
|
|
110
114
|
|
|
111
115
|
|
|
116
|
+
def _display(html_text: str) -> bool:
|
|
117
|
+
"""Show *html_text* in a notebook; False outside one."""
|
|
118
|
+
try:
|
|
119
|
+
from IPython import get_ipython
|
|
120
|
+
from IPython.display import HTML, display
|
|
121
|
+
except ImportError:
|
|
122
|
+
return False
|
|
123
|
+
if get_ipython() is None:
|
|
124
|
+
return False
|
|
125
|
+
display(HTML(html_text))
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
112
129
|
def _text(data: Any, n: int, digits: int | str | None = None) -> str:
|
|
113
130
|
df, total, caption = _parts(data, n)
|
|
114
131
|
# Polars prints no significant digits; digits=k applies to the table only.
|
|
@@ -139,15 +156,53 @@ def show(
|
|
|
139
156
|
the line with ``;`` to hide it.
|
|
140
157
|
"""
|
|
141
158
|
_check(n, pages, page_size, height, digits)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
from IPython.display import HTML, display
|
|
145
|
-
except ImportError:
|
|
146
|
-
get_ipython = None
|
|
147
|
-
if get_ipython is not None and get_ipython() is not None:
|
|
148
|
-
options = {"n": n, "pages": pages, "page_size": page_size, "height": height}
|
|
149
|
-
display(HTML(html(data, digits=digits, caption=caption, **options)))
|
|
150
|
-
else:
|
|
159
|
+
options = {"n": n, "pages": pages, "page_size": page_size, "height": height}
|
|
160
|
+
if not _display(html(data, digits=digits, caption=caption, **options)):
|
|
151
161
|
print(_text(data, n, digits))
|
|
152
162
|
return data
|
|
153
163
|
|
|
164
|
+
|
|
165
|
+
def view(
|
|
166
|
+
data: Any,
|
|
167
|
+
*,
|
|
168
|
+
n: int = 10_000,
|
|
169
|
+
height: int = 500,
|
|
170
|
+
digits: int | str | None = None,
|
|
171
|
+
caption: str | None = None,
|
|
172
|
+
hide: bool | None = None,
|
|
173
|
+
) -> Any:
|
|
174
|
+
"""Open a data explorer over the first *n* rows of *data*, and return *data*.
|
|
175
|
+
|
|
176
|
+
As R's ``View()``: one grid to scroll, a search box, headers that sort
|
|
177
|
+
on a click, and a filter under each column (from-to for numbers and
|
|
178
|
+
dates, "contains" for text, a pick-list for columns of few values).
|
|
179
|
+
The rows go into the notebook's output, so the explorer works in a saved
|
|
180
|
+
notebook, about 1 MB for 10,000 rows of a dozen columns. *height* is the
|
|
181
|
+
grid's height in pixels; *digits* and *caption* as in ``html()``. Where
|
|
182
|
+
output scripts don't run, it shows the first 100 rows as ``show()``.
|
|
183
|
+
Outside a notebook it prints the first rows.
|
|
184
|
+
|
|
185
|
+
In SolveIt, which sends every cell's code and output to its AI, the
|
|
186
|
+
cell is then hidden from the AI (SolveIt's red eye), so the rows don't
|
|
187
|
+
fill its context. *hide* says whether to, for this call: ``True`` or
|
|
188
|
+
``False``; ``None`` follows ``table3.autohide()``, on by default.
|
|
189
|
+
SolveIt hides the cell's code with its output.
|
|
190
|
+
"""
|
|
191
|
+
_check(n, True, 10, height, digits)
|
|
192
|
+
if hide is not None and not isinstance(hide, bool):
|
|
193
|
+
raise TypeError("table3: hide must be True, False or None")
|
|
194
|
+
if height is None:
|
|
195
|
+
raise ValueError("table3: view() height must be a positive integer")
|
|
196
|
+
from table3._explore import explorer_html
|
|
197
|
+
|
|
198
|
+
df, total, own_caption = _parts(data, n)
|
|
199
|
+
page = explorer_html(
|
|
200
|
+
df, total, own_caption if caption is None else caption,
|
|
201
|
+
n=n, height=height, digits=digits,
|
|
202
|
+
)
|
|
203
|
+
if not _display(page):
|
|
204
|
+
print(_text(data, min(n, 20), digits))
|
|
205
|
+
elif _solveit.AUTOHIDE if hide is None else hide:
|
|
206
|
+
_solveit.hide_calling_cell()
|
|
207
|
+
return data
|
|
208
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The explorer's output: its rows as JSON, a script that draws the grid, and a
|
|
2
|
+
plain table for notebooks that don't run output scripts (docs/explorer.md)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import html as _html
|
|
7
|
+
import math
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
from table3._explorer_js import EXPLORER_JS
|
|
13
|
+
from table3._format import dtype_short, float_text, json_for_script
|
|
14
|
+
from table3._html import MONO, table_html
|
|
15
|
+
|
|
16
|
+
CHOICES_MAX = 50 # a text column with at most this many values filters by pick-list
|
|
17
|
+
FALLBACK_ROWS = 100
|
|
18
|
+
_MAX_FLOAT = 1.7976931348623157e308 # inf's stand-in: JSON has no infinity
|
|
19
|
+
_SAFE_INT = 2**53 # beyond this a JavaScript number loses digits
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _kind(s: pl.Series) -> str:
|
|
23
|
+
t = s.dtype
|
|
24
|
+
if t == pl.Boolean:
|
|
25
|
+
return "bool"
|
|
26
|
+
if t.is_numeric():
|
|
27
|
+
return "number"
|
|
28
|
+
if t.is_temporal():
|
|
29
|
+
return "date"
|
|
30
|
+
if t in (pl.String, pl.Categorical) or isinstance(t, pl.Enum):
|
|
31
|
+
return "choice" if s.n_unique() <= CHOICES_MAX else "text"
|
|
32
|
+
return "text"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _float_raw(v: float | None) -> float | None:
|
|
36
|
+
if v is None or math.isnan(v):
|
|
37
|
+
return None
|
|
38
|
+
if math.isinf(v):
|
|
39
|
+
return _MAX_FLOAT if v > 0 else -_MAX_FLOAT
|
|
40
|
+
return v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _column(s: pl.Series, digits: int | str | None) -> tuple[dict, list, list | None]:
|
|
44
|
+
"""The column's header entry, its raw values, and its display text where it differs."""
|
|
45
|
+
kind = _kind(s)
|
|
46
|
+
head = {"name": s.name, "type": dtype_short(s.dtype), "kind": kind}
|
|
47
|
+
values = s.to_list()
|
|
48
|
+
shown = None
|
|
49
|
+
if s.dtype.is_float():
|
|
50
|
+
raw = [_float_raw(v) for v in values]
|
|
51
|
+
shown = float_text(s, digits)
|
|
52
|
+
elif s.dtype.is_integer():
|
|
53
|
+
raw = values
|
|
54
|
+
if any(v is not None and abs(v) > _SAFE_INT for v in values):
|
|
55
|
+
raw = [None if v is None else float(v) for v in values]
|
|
56
|
+
shown = [None if v is None else str(v) for v in values]
|
|
57
|
+
elif s.dtype.is_decimal():
|
|
58
|
+
raw = [None if v is None else float(v) for v in values]
|
|
59
|
+
shown = [None if v is None else str(v) for v in values]
|
|
60
|
+
elif kind == "bool":
|
|
61
|
+
raw = values
|
|
62
|
+
else:
|
|
63
|
+
raw = [None if v is None else str(v) for v in values]
|
|
64
|
+
if kind == "choice":
|
|
65
|
+
head["choices"] = sorted({v for v in raw if v is not None})
|
|
66
|
+
return head, raw, shown
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def payload(df: pl.DataFrame, total: int | None, n: int, digits: int | str | None) -> dict:
|
|
70
|
+
"""What the script needs: columns, rows of raw values, display text, counts."""
|
|
71
|
+
columns, raws, shown = [], [], []
|
|
72
|
+
for s in df.get_columns():
|
|
73
|
+
head, raw, text = _column(s, digits)
|
|
74
|
+
columns.append(head)
|
|
75
|
+
raws.append(raw)
|
|
76
|
+
shown.append(text)
|
|
77
|
+
return {
|
|
78
|
+
"columns": columns,
|
|
79
|
+
"rows": [list(r) for r in zip(*raws)] if raws else [],
|
|
80
|
+
"shown": shown,
|
|
81
|
+
"total": total,
|
|
82
|
+
"n": n,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def explorer_html(
|
|
87
|
+
df: pl.DataFrame,
|
|
88
|
+
total: int | None,
|
|
89
|
+
caption: str | None,
|
|
90
|
+
*,
|
|
91
|
+
n: int,
|
|
92
|
+
height: int,
|
|
93
|
+
digits: int | str | None,
|
|
94
|
+
) -> str:
|
|
95
|
+
uid = f"table3-explore-{uuid.uuid4().hex}"
|
|
96
|
+
fallback = table_html(
|
|
97
|
+
df.head(FALLBACK_ROWS), total, None, pages=False, page_size=10,
|
|
98
|
+
height=300, digits=digits,
|
|
99
|
+
)
|
|
100
|
+
note = (
|
|
101
|
+
f"<div style='font:{MONO};opacity:.65;margin:0 0 4px 0'>The explorer needs "
|
|
102
|
+
f"the notebook to run output scripts; the first {min(df.height, FALLBACK_ROWS):,} "
|
|
103
|
+
"rows show here instead.</div>"
|
|
104
|
+
)
|
|
105
|
+
cap = ""
|
|
106
|
+
if caption:
|
|
107
|
+
cap = (
|
|
108
|
+
f"<div style='font:{MONO};opacity:.85;margin:0 0 4px 0'>"
|
|
109
|
+
f"{_html.escape(caption)}</div>"
|
|
110
|
+
)
|
|
111
|
+
data = json_for_script(payload(df, total, n, digits))
|
|
112
|
+
return (
|
|
113
|
+
f"<div id='{uid}' data-height='{height}' style='font:{MONO}'>"
|
|
114
|
+
+ cap
|
|
115
|
+
+ f"<div data-fallback>{note}{fallback}</div>"
|
|
116
|
+
+ f"<script type='application/json'>{data}</script>"
|
|
117
|
+
+ "</div>"
|
|
118
|
+
+ f"<script>{EXPLORER_JS.replace('__ID__', uid)}</script>"
|
|
119
|
+
)
|