table3 0.3.0__py3-none-any.whl
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/__init__.py +153 -0
- table3/__version__.py +1 -0
- table3/_format.py +81 -0
- table3/_html.py +125 -0
- table3/_pager.py +106 -0
- table3/py.typed +0 -0
- table3-0.3.0.dist-info/METADATA +77 -0
- table3-0.3.0.dist-info/RECORD +11 -0
- table3-0.3.0.dist-info/WHEEL +5 -0
- table3-0.3.0.dist-info/licenses/LICENSE +21 -0
- table3-0.3.0.dist-info/top_level.txt +1 -0
table3/__init__.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Paged, theme-aware tables in notebook output.
|
|
2
|
+
|
|
3
|
+
``table3.html(data)`` is a table of up to *n* rows shown ten at a time, with
|
|
4
|
+
Previous / page / Next buttons and, for wide data, as many columns as fit
|
|
5
|
+
the cell plus arrows to the rest: what a package returns from its frames'
|
|
6
|
+
``_repr_html_`` so they print as R notebooks print data frames.
|
|
7
|
+
``table3.show(data)`` displays the same table and returns *data* unchanged.
|
|
8
|
+
|
|
9
|
+
*data* is a polars DataFrame, LazyFrame or Series, a pandas DataFrame or
|
|
10
|
+
Series, or any object with a ``__table3__`` method::
|
|
11
|
+
|
|
12
|
+
def __table3__(self, n: int) -> tuple[polars.DataFrame, int | None, str | None]:
|
|
13
|
+
# the first n rows, the whole row count (None if not counted),
|
|
14
|
+
# and a caption shown above the table (None for none)
|
|
15
|
+
|
|
16
|
+
so a package's own frames show without table3 importing that package.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import polars as pl
|
|
24
|
+
|
|
25
|
+
from table3.__version__ import __version__
|
|
26
|
+
from table3._html import table_html
|
|
27
|
+
|
|
28
|
+
__all__ = ["__version__", "html", "show"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _from_pandas(df: Any) -> pl.DataFrame:
|
|
32
|
+
try:
|
|
33
|
+
return pl.from_pandas(df)
|
|
34
|
+
except Exception: # no pyarrow, or a dtype polars can't take: show the text
|
|
35
|
+
return pl.DataFrame({str(c): df[c].astype(str).tolist() for c in df.columns})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parts(data: Any, n: int) -> tuple[pl.DataFrame, int | None, str | None]:
|
|
39
|
+
if hasattr(type(data), "__table3__"):
|
|
40
|
+
return data.__table3__(n)
|
|
41
|
+
if isinstance(data, pl.Series):
|
|
42
|
+
data = data.to_frame()
|
|
43
|
+
if isinstance(data, pl.LazyFrame):
|
|
44
|
+
head, count = pl.collect_all([data.head(n), data.select(pl.len())])
|
|
45
|
+
return head, int(count.item()), None
|
|
46
|
+
if isinstance(data, pl.DataFrame):
|
|
47
|
+
return data.head(n), data.height, None
|
|
48
|
+
# pandas, duck-typed so table3 never imports it
|
|
49
|
+
if hasattr(data, "to_frame") and hasattr(data, "iloc") and not hasattr(data, "columns"):
|
|
50
|
+
data = data.to_frame()
|
|
51
|
+
if hasattr(data, "columns") and hasattr(data, "iloc"):
|
|
52
|
+
return _from_pandas(data.head(n)), len(data), None
|
|
53
|
+
raise TypeError(
|
|
54
|
+
f"table3 can't show a {type(data).__name__}: pass a polars or pandas "
|
|
55
|
+
"DataFrame or Series, or an object with a __table3__ method"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _positive_int(value: Any) -> bool:
|
|
60
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check(n: Any, pages: Any, page_size: Any, height: Any, digits: Any) -> None:
|
|
64
|
+
if not isinstance(n, int) or isinstance(n, bool) or n < 0:
|
|
65
|
+
raise ValueError("table3: n must be a non-negative integer")
|
|
66
|
+
if not isinstance(pages, bool):
|
|
67
|
+
raise TypeError("table3: pages must be True or False")
|
|
68
|
+
if not _positive_int(page_size):
|
|
69
|
+
raise ValueError("table3: page_size must be a positive integer")
|
|
70
|
+
if height is not None and not _positive_int(height):
|
|
71
|
+
raise ValueError("table3: height must be a positive integer or None")
|
|
72
|
+
if digits is not None and digits != "full" and not _positive_int(digits):
|
|
73
|
+
raise ValueError('table3: digits must be a positive integer, "full", or None')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def html(
|
|
77
|
+
data: Any,
|
|
78
|
+
*,
|
|
79
|
+
n: int = 1000,
|
|
80
|
+
pages: bool = True,
|
|
81
|
+
page_size: int = 10,
|
|
82
|
+
height: int | None = 300,
|
|
83
|
+
digits: int | str | None = None,
|
|
84
|
+
caption: str | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""The table's HTML for the first *n* rows of *data*.
|
|
87
|
+
|
|
88
|
+
*page_size* rows show per page. ``pages=False``, or a notebook that
|
|
89
|
+
doesn't run output scripts, shows every row instead in a box *height*
|
|
90
|
+
pixels tall that scrolls; ``height=None`` grows it to fit.
|
|
91
|
+
|
|
92
|
+
Floats print as polars' text preview prints them (up to six decimals,
|
|
93
|
+
following ``pl.Config(float_precision=)``) unless *digits* says
|
|
94
|
+
otherwise: ``digits=6`` keeps six significant digits, as R's
|
|
95
|
+
``signif()`` does, so small numbers keep their precision (0.000123457);
|
|
96
|
+
``digits="full"`` shows every stored digit. *caption* replaces the one
|
|
97
|
+
``__table3__`` gives.
|
|
98
|
+
"""
|
|
99
|
+
_check(n, pages, page_size, height, digits)
|
|
100
|
+
df, total, own_caption = _parts(data, n)
|
|
101
|
+
return table_html(
|
|
102
|
+
df,
|
|
103
|
+
total,
|
|
104
|
+
own_caption if caption is None else caption,
|
|
105
|
+
pages=pages,
|
|
106
|
+
page_size=page_size,
|
|
107
|
+
height=height,
|
|
108
|
+
digits=digits,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _text(data: Any, n: int, digits: int | str | None = None) -> str:
|
|
113
|
+
df, total, caption = _parts(data, n)
|
|
114
|
+
# Polars prints no significant digits; digits=k applies to the table only.
|
|
115
|
+
full = {"fmt_float": "full"} if digits == "full" else {}
|
|
116
|
+
with pl.Config(tbl_rows=max(df.height, 1), tbl_hide_dataframe_shape=True, **full):
|
|
117
|
+
lines = [f"shape: ({'?' if total is None else total}, {df.width})", repr(df)]
|
|
118
|
+
if caption:
|
|
119
|
+
lines.insert(0, caption)
|
|
120
|
+
if total is None or total > df.height:
|
|
121
|
+
lines.append(f"… first {df.height:,} rows")
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def show(
|
|
126
|
+
data: Any,
|
|
127
|
+
*,
|
|
128
|
+
n: int = 1000,
|
|
129
|
+
pages: bool = True,
|
|
130
|
+
page_size: int = 10,
|
|
131
|
+
height: int | None = 300,
|
|
132
|
+
digits: int | str | None = None,
|
|
133
|
+
caption: str | None = None,
|
|
134
|
+
) -> Any:
|
|
135
|
+
"""Show up to *n* rows of *data* in a paged table and return *data*.
|
|
136
|
+
|
|
137
|
+
Arguments as in ``html()``. Outside a notebook it prints the rows as
|
|
138
|
+
text. As a cell's last line the notebook also prints *data* itself; end
|
|
139
|
+
the line with ``;`` to hide it.
|
|
140
|
+
"""
|
|
141
|
+
_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:
|
|
151
|
+
print(_text(data, n, digits))
|
|
152
|
+
return data
|
|
153
|
+
|
table3/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
table3/_format.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Cell text: values, types and floats as polars' text preview prints them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html as _html
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
MAX_CHARS = 40 # longer values are cut, with the full value on hover
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def escape(v: object) -> str:
|
|
13
|
+
if v is None:
|
|
14
|
+
return "null"
|
|
15
|
+
return _html.escape(str(v))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cell(v: object) -> str:
|
|
19
|
+
text = "null" if v is None else str(v)
|
|
20
|
+
if len(text) <= MAX_CHARS:
|
|
21
|
+
return escape(v)
|
|
22
|
+
short = _html.escape(text[: MAX_CHARS - 1]) + "…"
|
|
23
|
+
return f"<span title='{_html.escape(text)}'>{short}</span>"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dtype_short(t: pl.DataType) -> str:
|
|
27
|
+
"""Polars-style short dtype name ("i64"); falls back to str(t)."""
|
|
28
|
+
try:
|
|
29
|
+
from polars._plr import dtype_str_repr
|
|
30
|
+
|
|
31
|
+
return dtype_str_repr(t)
|
|
32
|
+
except Exception:
|
|
33
|
+
return str(t)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def significant(v: float, digits: int) -> str:
|
|
37
|
+
"""*v* to *digits* significant digits, as R's ``signif()`` keeps them.
|
|
38
|
+
|
|
39
|
+
Written as polars writes floats: "2.0" not "2", "1.5e-7" not "1.5e-07".
|
|
40
|
+
"""
|
|
41
|
+
if v != v:
|
|
42
|
+
return "NaN"
|
|
43
|
+
if v in (float("inf"), float("-inf")):
|
|
44
|
+
return "inf" if v > 0 else "-inf"
|
|
45
|
+
text = format(v, f".{digits}g")
|
|
46
|
+
if "e" in text:
|
|
47
|
+
mantissa, exponent = text.split("e")
|
|
48
|
+
return f"{mantissa}e{int(exponent)}"
|
|
49
|
+
return text if "." in text else text + ".0"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def float_text(s: pl.Series, digits: int | str | None = None) -> list[object]:
|
|
53
|
+
"""Floats as polars' text preview prints them ("3.3", "1.2346e8").
|
|
54
|
+
|
|
55
|
+
Read off polars' own printer, so ``pl.Config(float_precision=)`` applies.
|
|
56
|
+
*digits* = k keeps k significant digits instead; "full" every stored digit.
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(digits, int):
|
|
59
|
+
return [None if v is None else significant(v, digits) for v in s.to_list()]
|
|
60
|
+
full = {"fmt_float": "full"} if digits == "full" else {}
|
|
61
|
+
with pl.Config(
|
|
62
|
+
tbl_formatting="NOTHING",
|
|
63
|
+
tbl_hide_column_names=True,
|
|
64
|
+
tbl_hide_column_data_types=True,
|
|
65
|
+
tbl_hide_dataframe_shape=True,
|
|
66
|
+
tbl_rows=-1,
|
|
67
|
+
tbl_width_chars=10_000,
|
|
68
|
+
**full,
|
|
69
|
+
):
|
|
70
|
+
lines = [line.strip() for line in repr(s.to_frame()).splitlines()]
|
|
71
|
+
if len(lines) != len(s):
|
|
72
|
+
return s.to_list()
|
|
73
|
+
return [None if v is None else t for v, t in zip(s.to_list(), lines)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def display_rows(df: pl.DataFrame, digits: int | str | None = None) -> list[tuple]:
|
|
77
|
+
cols = [
|
|
78
|
+
float_text(s, digits) if s.dtype.is_float() else s.to_list()
|
|
79
|
+
for s in df.get_columns()
|
|
80
|
+
]
|
|
81
|
+
return list(zip(*cols))
|
table3/_html.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The table's HTML: inline styles only.
|
|
2
|
+
|
|
3
|
+
CRAFT republishes remote output through ``display_pub.publish``, which drops
|
|
4
|
+
``<style>`` blocks, so every element carries its own style (as in tidy3's
|
|
5
|
+
``display.py``). The pinned header needs a solid background: SolveIt's card
|
|
6
|
+
colour (``--card``, an HSL triplet) is drawn as a gradient over a plain colour
|
|
7
|
+
taken from VS Code, then JupyterLab, then the browser. Where ``--card`` is
|
|
8
|
+
undefined the gradient drops out alone.
|
|
9
|
+
|
|
10
|
+
Paging is a script over this table. Where output scripts don't run, the
|
|
11
|
+
table stays a box of fixed height that scrolls down and sideways, with the
|
|
12
|
+
column names and types pinned at the top.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import html as _html
|
|
18
|
+
import uuid
|
|
19
|
+
|
|
20
|
+
import polars as pl
|
|
21
|
+
|
|
22
|
+
from table3._format import cell, display_rows, dtype_short, escape
|
|
23
|
+
from table3._pager import PAGER_JS
|
|
24
|
+
|
|
25
|
+
MONO = "12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace"
|
|
26
|
+
RULE = "border-bottom:1px solid rgba(127,127,127,.45)"
|
|
27
|
+
# The font is set once on the table: a cell's style repeats on up to
|
|
28
|
+
# n x columns cells, and the font list is half its length.
|
|
29
|
+
CELL = "font:inherit;padding:3px 10px;text-align:right;white-space:pre;border:0"
|
|
30
|
+
HEAD_BG = (
|
|
31
|
+
"background-color:var(--vscode-editor-background,"
|
|
32
|
+
"var(--jp-layout-color0,Canvas));"
|
|
33
|
+
"background-image:linear-gradient(hsl(var(--card)),hsl(var(--card)))"
|
|
34
|
+
)
|
|
35
|
+
ZEBRA = "background:rgba(127,127,127,.08)"
|
|
36
|
+
SCROLLBAR = "scrollbar-color:rgba(127,127,127,.5) transparent"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _plural(k: int) -> str:
|
|
40
|
+
return f"{k:,} row" + ("s" if k != 1 else "")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def rows_label(shown: int, total: int | None) -> str:
|
|
44
|
+
"""Footer for the scroll box: "35 rows", or how many of how many show."""
|
|
45
|
+
if total is not None and total <= shown:
|
|
46
|
+
return _plural(total)
|
|
47
|
+
of = "more" if total is None else f"{total:,}"
|
|
48
|
+
return f"showing first {shown:,} of {of} rows"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pages_label(shown: int, total: int | None) -> str:
|
|
52
|
+
"""What the pager counts rows of: "344 rows", or the first 100 of more."""
|
|
53
|
+
if total is not None and total <= shown:
|
|
54
|
+
return _plural(shown)
|
|
55
|
+
of = "more" if total is None else f"{total:,}"
|
|
56
|
+
return f"{_plural(shown)} (first {shown:,} of {of})"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def table_html(
|
|
60
|
+
df: pl.DataFrame,
|
|
61
|
+
total: int | None,
|
|
62
|
+
caption: str | None,
|
|
63
|
+
*,
|
|
64
|
+
pages: bool,
|
|
65
|
+
page_size: int,
|
|
66
|
+
height: int | None,
|
|
67
|
+
digits: int | str | None = None,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""*df* is the first rows; *total* the whole row count, None if not counted."""
|
|
70
|
+
|
|
71
|
+
def _tr(cells: list[str], style: str, row_style: str = "") -> str:
|
|
72
|
+
tds = "".join(f"<td style='{style}'>{c}</td>" for c in cells)
|
|
73
|
+
return f"<tr style='{row_style}'>{tds}</tr>"
|
|
74
|
+
|
|
75
|
+
head = _tr([f"<b>{escape(c)}</b>" for c in df.columns], f"{CELL};{RULE}")
|
|
76
|
+
types = _tr(
|
|
77
|
+
[escape(dtype_short(t)) for t in df.dtypes], f"{CELL};opacity:.55;{RULE}"
|
|
78
|
+
)
|
|
79
|
+
body = [
|
|
80
|
+
_tr([cell(v) for v in row], CELL, ZEBRA if i % 2 else "")
|
|
81
|
+
for i, row in enumerate(display_rows(df, digits))
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
shape = f"shape: ({'?' if total is None else total}, {df.width})"
|
|
85
|
+
box = f"overflow:auto;{SCROLLBAR}"
|
|
86
|
+
if height is not None:
|
|
87
|
+
box = f"max-height:{height}px;{box}"
|
|
88
|
+
small = f"font:{MONO};opacity:.65"
|
|
89
|
+
cap = ""
|
|
90
|
+
if caption:
|
|
91
|
+
cap = (
|
|
92
|
+
f"<div style='font:{MONO};opacity:.85;margin:0 0 2px 0'>"
|
|
93
|
+
f"{_html.escape(caption)}</div>"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
root = "<div>"
|
|
97
|
+
pager = script = ""
|
|
98
|
+
if pages:
|
|
99
|
+
uid = f"table3-{uuid.uuid4().hex}"
|
|
100
|
+
label = _html.escape(pages_label(df.height, total))
|
|
101
|
+
root = f"<div id='{uid}' data-page-size='{page_size}' data-label='{label}'>"
|
|
102
|
+
pager = (
|
|
103
|
+
f"<div data-pager style='font:{MONO};margin:4px 0 0 0;display:none'>"
|
|
104
|
+
"</div>"
|
|
105
|
+
)
|
|
106
|
+
script = f"<script>{PAGER_JS.replace('__ID__', uid)}</script>"
|
|
107
|
+
return (
|
|
108
|
+
root
|
|
109
|
+
+ cap
|
|
110
|
+
+ f"<small style='{small}'>{shape}</small>"
|
|
111
|
+
f"<div data-box style='{box}'>"
|
|
112
|
+
"<table style='border-collapse:separate;border-spacing:0;margin:2px 0;"
|
|
113
|
+
f"font:{MONO}'>"
|
|
114
|
+
f"<thead style='position:sticky;top:0;z-index:1;{HEAD_BG}'>"
|
|
115
|
+
+ head
|
|
116
|
+
+ types
|
|
117
|
+
+ "</thead><tbody>"
|
|
118
|
+
+ "".join(body)
|
|
119
|
+
+ "</tbody></table></div>"
|
|
120
|
+
f"<small data-foot style='{small}'>"
|
|
121
|
+
f"{rows_label(df.height, total)}</small>"
|
|
122
|
+
+ pager
|
|
123
|
+
+ "</div>"
|
|
124
|
+
+ script
|
|
125
|
+
)
|
table3/_pager.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The script that pages a table3 table in place."""
|
|
2
|
+
|
|
3
|
+
# Column widths are measured once over all rows, so a column page keeps its
|
|
4
|
+
# columns whichever row page is shown. __ID__ is the table's root element.
|
|
5
|
+
PAGER_JS = """
|
|
6
|
+
(function () {
|
|
7
|
+
var root = document.getElementById("__ID__");
|
|
8
|
+
if (!root || root.dataset.paged) return;
|
|
9
|
+
root.dataset.paged = "1";
|
|
10
|
+
var box = root.querySelector("[data-box]");
|
|
11
|
+
var table = box.querySelector("table");
|
|
12
|
+
var head = [].slice.call(table.tHead.rows);
|
|
13
|
+
var body = [].slice.call(table.tBodies[0].rows);
|
|
14
|
+
var foot = root.querySelector("[data-foot]");
|
|
15
|
+
var pager = root.querySelector("[data-pager]");
|
|
16
|
+
var size = +root.dataset.pageSize, label = root.dataset.label;
|
|
17
|
+
var widths = [].map.call(head[0].cells, function (c) { return c.offsetWidth; });
|
|
18
|
+
var page = 0, cpage = 0, groups = [], seenWidth = -1;
|
|
19
|
+
var BTN = "font:inherit;color:inherit;background:none;cursor:pointer;" +
|
|
20
|
+
"border:1px solid transparent;border-radius:4px;padding:1px 7px;margin:0 1px";
|
|
21
|
+
|
|
22
|
+
function group() {
|
|
23
|
+
var avail = box.clientWidth || Infinity, cur = [], w = 0;
|
|
24
|
+
seenWidth = box.clientWidth;
|
|
25
|
+
groups = [];
|
|
26
|
+
widths.forEach(function (x, j) {
|
|
27
|
+
if (cur.length && w + x > avail) { groups.push(cur); cur = []; w = 0; }
|
|
28
|
+
cur.push(j); w += x;
|
|
29
|
+
});
|
|
30
|
+
groups.push(cur);
|
|
31
|
+
cpage = Math.min(cpage, groups.length - 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function button(text, on, go, current) {
|
|
35
|
+
var b = document.createElement("button");
|
|
36
|
+
b.type = "button"; b.textContent = text; b.style.cssText = BTN;
|
|
37
|
+
if (current) b.style.borderColor = "rgba(127,127,127,.6)";
|
|
38
|
+
if (!on) { b.disabled = true; b.style.opacity = ".35"; b.style.cursor = "default"; }
|
|
39
|
+
else b.onclick = go;
|
|
40
|
+
pager.appendChild(b);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function gap(text) {
|
|
44
|
+
var s = document.createElement("span");
|
|
45
|
+
s.textContent = text || "…"; s.style.cssText = "opacity:.55;padding:0 4px";
|
|
46
|
+
pager.appendChild(s);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pageList(n, cur) {
|
|
50
|
+
if (n <= 7) return Array.from({ length: n }, function (_, i) { return i; });
|
|
51
|
+
var keep = [0, n - 1, cur - 1, cur, cur + 1].filter(function (i) {
|
|
52
|
+
return i >= 0 && i < n;
|
|
53
|
+
});
|
|
54
|
+
return keep.filter(function (v, i) { return keep.indexOf(v) === i; })
|
|
55
|
+
.sort(function (a, b) { return a - b; });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function render() {
|
|
59
|
+
var npages = Math.max(1, Math.ceil(body.length / size));
|
|
60
|
+
page = Math.min(page, npages - 1);
|
|
61
|
+
var lo = page * size, hi = Math.min(lo + size, body.length);
|
|
62
|
+
var cols = groups[cpage], keep = {};
|
|
63
|
+
cols.forEach(function (j) { keep[j] = 1; });
|
|
64
|
+
body.forEach(function (tr, i) {
|
|
65
|
+
var on = i >= lo && i < hi;
|
|
66
|
+
tr.style.display = on ? "" : "none";
|
|
67
|
+
if (on) tr.style.background = (i - lo) % 2 ? "rgba(127,127,127,.08)" : "";
|
|
68
|
+
});
|
|
69
|
+
head.concat(body.slice(lo, hi)).forEach(function (tr) {
|
|
70
|
+
[].forEach.call(tr.cells, function (td, j) { td.style.display = keep[j] ? "" : "none"; });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
var text = body.length > size ? (lo + 1) + "–" + hi + " of " + label : label;
|
|
74
|
+
if (groups.length > 1)
|
|
75
|
+
text += " | " + (cols[0] + 1) + "–" + (cols[cols.length - 1] + 1) +
|
|
76
|
+
" of " + widths.length + " columns";
|
|
77
|
+
foot.textContent = text;
|
|
78
|
+
|
|
79
|
+
pager.textContent = "";
|
|
80
|
+
if (groups.length > 1) {
|
|
81
|
+
button("◂ columns", cpage > 0, function () { cpage--; render(); });
|
|
82
|
+
button("columns ▸", cpage < groups.length - 1, function () { cpage++; render(); });
|
|
83
|
+
if (npages > 1) gap("|");
|
|
84
|
+
}
|
|
85
|
+
if (npages > 1) {
|
|
86
|
+
button("Previous", page > 0, function () { page--; render(); });
|
|
87
|
+
var last = -1;
|
|
88
|
+
pageList(npages, page).forEach(function (p) {
|
|
89
|
+
if (p > last + 1) gap();
|
|
90
|
+
button(String(p + 1), p !== page, function () { page = p; render(); }, p === page);
|
|
91
|
+
last = p;
|
|
92
|
+
});
|
|
93
|
+
button("Next", page < npages - 1, function () { page++; render(); });
|
|
94
|
+
}
|
|
95
|
+
pager.style.display = pager.childNodes.length ? "" : "none";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
group();
|
|
99
|
+
box.style.maxHeight = "none";
|
|
100
|
+
render();
|
|
101
|
+
if (window.ResizeObserver)
|
|
102
|
+
new ResizeObserver(function () {
|
|
103
|
+
if (box.clientWidth !== seenWidth) { group(); render(); }
|
|
104
|
+
}).observe(box);
|
|
105
|
+
})();
|
|
106
|
+
"""
|
table3/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: table3
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
|
|
5
|
+
Author: Rigoberto Leyva Salmeron
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rleyvasal/table3
|
|
8
|
+
Keywords: notebooks,jupyter,polars,tables,viewer
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: IPython
|
|
11
|
+
Classifier: Framework :: Jupyter
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: polars>=1.39
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: ipython>=8.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pandas>=2.1.4; extra == "dev"
|
|
21
|
+
Requires-Dist: pyarrow>=15.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# table3
|
|
25
|
+
|
|
26
|
+
Paged, theme-aware tables in notebook output, the way R notebooks print a
|
|
27
|
+
data frame. It is the shared table display of
|
|
28
|
+
[tidy3](https://github.com/rleyvasal/tidy3),
|
|
29
|
+
[tidy3a](https://github.com/rleyvasal/tidy3a) and
|
|
30
|
+
[stats3](https://github.com/rleyvasal/stats3), released on its own so each
|
|
31
|
+
can show tables without depending on the others. It needs only polars.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import polars as pl
|
|
35
|
+
import table3
|
|
36
|
+
|
|
37
|
+
penguins = pl.read_csv(
|
|
38
|
+
"https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv",
|
|
39
|
+
null_values="NA",
|
|
40
|
+
)
|
|
41
|
+
table3.show(penguins); # ten rows a page, every row up to n=1000
|
|
42
|
+
table3.show(penguins, page_size=25); # bigger pages
|
|
43
|
+
table3.show(penguins, pages=False, height=300); # one box that scrolls
|
|
44
|
+
table3.show(penguins, digits=4); # four significant digits
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Column names and types sit in the header, missing values show as `null`,
|
|
48
|
+
floats print as polars' text preview prints them (or to `digits=`
|
|
49
|
+
significant digits, or `digits="full"`), and long values are cut with the
|
|
50
|
+
full value on hover. Wide tables show as many columns as fit, with
|
|
51
|
+
arrows to the rest. The table follows the notebook's light or dark theme in
|
|
52
|
+
VS Code, JupyterLab and SolveIt.
|
|
53
|
+
|
|
54
|
+
`show()` returns what it was given, so it can sit in the middle of a pipe.
|
|
55
|
+
As a cell's last line the notebook also prints that value; end the line with
|
|
56
|
+
`;` to hide it.
|
|
57
|
+
|
|
58
|
+
## Your own frames
|
|
59
|
+
|
|
60
|
+
A package makes its frames print this way by returning `table3.html(self)`
|
|
61
|
+
from `_repr_html_`. An object with a `__table3__` method shows without table3
|
|
62
|
+
importing its package:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
def __table3__(self, n):
|
|
66
|
+
return first_n_rows_as_polars, total_rows_or_None, "caption or None"
|
|
67
|
+
|
|
68
|
+
def _repr_html_(self):
|
|
69
|
+
return table3.html(self, n=100)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## More
|
|
73
|
+
|
|
74
|
+
[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.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
table3/__init__.py,sha256=HuQnXQ5FKPEFe_JpbeCs0II-4FpqRdutcolaGZZMxAo,5814
|
|
2
|
+
table3/__version__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
|
|
3
|
+
table3/_format.py,sha256=vQ8w6FZHZyS-Z3cuKT10OPnws7Xlf54yv9rIgOHSTFw,2536
|
|
4
|
+
table3/_html.py,sha256=aCDUmSGAu85rfG6BbTbmrhqwgaI2KuWlVp3QfdKnr3s,4406
|
|
5
|
+
table3/_pager.py,sha256=P2FGXTxgclpFBySTqgKeBUFZSbGK99BnosTsgMlKPLc,4107
|
|
6
|
+
table3/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
table3-0.3.0.dist-info/licenses/LICENSE,sha256=sjpfmtLhkcM74AL9VMpifL0CRdeiMoLyUO9OKZ_hg9I,1082
|
|
8
|
+
table3-0.3.0.dist-info/METADATA,sha256=s8TqvQ_jfpaNBpttexj3HOyWGqKO6MRU72p7eNOUYRs,2873
|
|
9
|
+
table3-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
table3-0.3.0.dist-info/top_level.txt,sha256=UrtGfSKMg2UGL__F7_pu_MAGsOEj1KtBbmUs2NSgIjY,7
|
|
11
|
+
table3-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rigoberto Leyva Salmeron
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
table3
|