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.
Files changed (30) hide show
  1. table3-0.5.0/CHANGELOG.md +81 -0
  2. {table3-0.3.0/src/table3.egg-info → table3-0.5.0}/PKG-INFO +26 -5
  3. {table3-0.3.0 → table3-0.5.0}/README.md +23 -4
  4. {table3-0.3.0 → table3-0.5.0}/docs/design.md +11 -6
  5. table3-0.5.0/docs/explorer.md +145 -0
  6. {table3-0.3.0 → table3-0.5.0}/pyproject.toml +3 -0
  7. {table3-0.3.0 → table3-0.5.0}/src/table3/__init__.py +65 -10
  8. table3-0.5.0/src/table3/__version__.py +1 -0
  9. table3-0.5.0/src/table3/_explore.py +119 -0
  10. table3-0.5.0/src/table3/_explorer_js.py +281 -0
  11. {table3-0.3.0 → table3-0.5.0}/src/table3/_format.py +10 -0
  12. {table3-0.3.0 → table3-0.5.0}/src/table3/_html.py +16 -7
  13. {table3-0.3.0 → table3-0.5.0}/src/table3/_pager.py +25 -1
  14. table3-0.5.0/src/table3/_solveit.py +93 -0
  15. {table3-0.3.0 → table3-0.5.0/src/table3.egg-info}/PKG-INFO +26 -5
  16. {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/SOURCES.txt +8 -0
  17. {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/requires.txt +3 -0
  18. table3-0.5.0/tests/test_explore.py +123 -0
  19. table3-0.5.0/tests/test_explorer_browser.py +185 -0
  20. table3-0.5.0/tests/test_pager_browser.py +88 -0
  21. table3-0.5.0/tests/test_solveit.py +110 -0
  22. {table3-0.3.0 → table3-0.5.0}/tests/test_table3.py +37 -6
  23. table3-0.3.0/CHANGELOG.md +0 -40
  24. table3-0.3.0/src/table3/__version__.py +0 -1
  25. {table3-0.3.0 → table3-0.5.0}/LICENSE +0 -0
  26. {table3-0.3.0 → table3-0.5.0}/MANIFEST.in +0 -0
  27. {table3-0.3.0 → table3-0.5.0}/setup.cfg +0 -0
  28. {table3-0.3.0 → table3-0.5.0}/src/table3/py.typed +0 -0
  29. {table3-0.3.0 → table3-0.5.0}/src/table3.egg-info/dependency_links.txt +0 -0
  30. {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.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
- `show()` returns what it was given, so it can sit in the middle of a pipe.
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. table3 was first released as view3 0.1 and 0.2, where `show()` was
76
- `view()`, and its versions continue from them. The name `view` is kept for a
77
- data explorer, as R's `View()` opens one.
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
- `show()` returns what it was given, so it can sit in the middle of a pipe.
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. table3 was first released as view3 0.1 and 0.2, where `show()` was
53
- `view()`, and its versions continue from them. The name `view` is kept for a
54
- data explorer, as R's `View()` opens one.
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.** Every cell carries its style, about 100 bytes a cell: 1,000 rows
80
- of 12 columns add about 1.2 MB to a saved notebook. The font is set once
81
- on the table to halve that.
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: tidy3's ordinary notebook table formats through table3 (its floats
123
- print in full today); loading further rows while paging.
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
- try:
143
- from IPython import get_ipython
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
+ )