tablelab 0.5.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.
- tablelab/__init__.py +72 -0
- tablelab/__main__.py +64 -0
- tablelab/app.py +431 -0
- tablelab/charts.py +242 -0
- tablelab/code_export.py +545 -0
- tablelab/column_inspector.py +303 -0
- tablelab/figures.py +101 -0
- tablelab/file_readers.py +129 -0
- tablelab/filepicker.py +195 -0
- tablelab/helpers/__init__.py +50 -0
- tablelab/helpers/data.py +30 -0
- tablelab/helpers/plotting.py +31 -0
- tablelab/helpers/widgets.py +5 -0
- tablelab/missing_values.py +97 -0
- tablelab/paged_preview.py +152 -0
- tablelab/pipeline.py +202 -0
- tablelab/query_app.py +1343 -0
- tablelab/query_expressions.py +91 -0
- tablelab/table.py +128 -0
- tablelab-0.5.0.dist-info/METADATA +465 -0
- tablelab-0.5.0.dist-info/RECORD +24 -0
- tablelab-0.5.0.dist-info/WHEEL +4 -0
- tablelab-0.5.0.dist-info/entry_points.txt +2 -0
- tablelab-0.5.0.dist-info/licenses/LICENSE +21 -0
tablelab/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""tablelab - interactive table inspection in Jupyter.
|
|
2
|
+
|
|
3
|
+
Load a table from a file, see what is in it, and read any one series as a chart
|
|
4
|
+
- either a column down the rows, or a row across the columns. A row is the
|
|
5
|
+
useful direction when each row of the table is a signal (ECG beats, sensor
|
|
6
|
+
sweeps, spectra); when the table carries a label column, the label names the
|
|
7
|
+
chart.
|
|
8
|
+
|
|
9
|
+
The app browses for the file, lists the table's columns in a menu, and offers
|
|
10
|
+
five views of whichever series is chosen: a run chart, a sorted run chart, a
|
|
11
|
+
histogram, a box plot and an ECDF. Where ipympl is installed the chart pans and
|
|
12
|
+
zooms, and grid lines are a checkbox.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
|
|
16
|
+
from tablelab import main
|
|
17
|
+
main() # empty app, load a file from the UI
|
|
18
|
+
main("data/ecg.csv") # open on a file
|
|
19
|
+
|
|
20
|
+
For table queries and pipelines:
|
|
21
|
+
|
|
22
|
+
from tablelab import query_main
|
|
23
|
+
app = query_main() # Select, Join, Filter, Group, Missing values
|
|
24
|
+
|
|
25
|
+
Or without widgets:
|
|
26
|
+
|
|
27
|
+
from tablelab import load_table, run_chart
|
|
28
|
+
df = load_table("data/ecg.csv")
|
|
29
|
+
run_chart(df.iloc[0, :-1], title="beat 0")
|
|
30
|
+
|
|
31
|
+
Building the widgets takes a moment, so it is deferred until `main` is actually
|
|
32
|
+
looked up rather than done on `import tablelab`.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__version__ = "0.5.0"
|
|
36
|
+
__all__ = ["__version__", "main", "TableApp", "query_main", "QueryApp", "load_table", "read_uploaded_dataframe",
|
|
37
|
+
"find_label_column", "describe", "summary_stats",
|
|
38
|
+
"run_chart", "sorted_run_chart", "histogram", "box_plot", "ecdf",
|
|
39
|
+
"draw", "KINDS"]
|
|
40
|
+
|
|
41
|
+
_LAZY = {
|
|
42
|
+
"main": "app",
|
|
43
|
+
"TableApp": "app",
|
|
44
|
+
"QueryApp": "query_app",
|
|
45
|
+
"load_table": "file_readers",
|
|
46
|
+
"read_uploaded_dataframe": "file_readers",
|
|
47
|
+
"find_label_column": "table",
|
|
48
|
+
"describe": "table",
|
|
49
|
+
"summary_stats": "table",
|
|
50
|
+
"run_chart": "charts",
|
|
51
|
+
"sorted_run_chart": "charts",
|
|
52
|
+
"histogram": "charts",
|
|
53
|
+
"box_plot": "charts",
|
|
54
|
+
"ecdf": "charts",
|
|
55
|
+
"draw": "charts",
|
|
56
|
+
"KINDS": "charts",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __getattr__(name): # PEP 562: keeps `import tablelab` cheap
|
|
61
|
+
if name == "query_main":
|
|
62
|
+
from .query_app import main
|
|
63
|
+
return main
|
|
64
|
+
module = _LAZY.get(name)
|
|
65
|
+
if module is None:
|
|
66
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
67
|
+
import importlib
|
|
68
|
+
return getattr(importlib.import_module(f".{module}", __name__), name)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __dir__():
|
|
72
|
+
return sorted(__all__)
|
tablelab/__main__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Command line front end, for when a notebook would be overkill.
|
|
2
|
+
|
|
3
|
+
python -m tablelab FILE --list
|
|
4
|
+
python -m tablelab FILE --col temperature
|
|
5
|
+
python -m tablelab FILE --col temperature --chart histogram --grid
|
|
6
|
+
python -m tablelab FILE --row 0 --out beat.png
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
|
|
12
|
+
from . import charts, __version__
|
|
13
|
+
from . import table as _table
|
|
14
|
+
from .file_readers import load_table
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv=None):
|
|
18
|
+
p = argparse.ArgumentParser(prog="tablelab", description=__doc__,
|
|
19
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
20
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
21
|
+
p.add_argument("file")
|
|
22
|
+
p.add_argument("--col", help="column to plot down the rows (name or index)")
|
|
23
|
+
p.add_argument("--row", type=int, help="row to plot across the columns")
|
|
24
|
+
p.add_argument("--label-col", help="override the detected label column")
|
|
25
|
+
p.add_argument("--chart", default="run", choices=[k for _, k in charts.KINDS],
|
|
26
|
+
help="which view of the series (default: run)")
|
|
27
|
+
p.add_argument("--centre", default="median", choices=["median", "mean", "none"])
|
|
28
|
+
p.add_argument("--grid", action="store_true",
|
|
29
|
+
help="vertical grid lines as well as the horizontal ones")
|
|
30
|
+
p.add_argument("--list", action="store_true", help="describe the table and exit")
|
|
31
|
+
p.add_argument("--out", help="save to this png instead of opening a window")
|
|
32
|
+
args = p.parse_args(argv)
|
|
33
|
+
|
|
34
|
+
df = load_table(args.file)
|
|
35
|
+
label_col = (_table.resolve_column(df, args.label_col) if args.label_col
|
|
36
|
+
else _table.find_label_column(df))
|
|
37
|
+
centre = None if args.centre == "none" else args.centre
|
|
38
|
+
grid = True if args.grid else "y"
|
|
39
|
+
|
|
40
|
+
if args.list or (args.col is None and args.row is None):
|
|
41
|
+
print(f"{args.file}\n{_table.describe(df)}")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if args.row is not None:
|
|
45
|
+
values = _table.row_series(df, args.row, label_col)
|
|
46
|
+
title = f"row {args.row}"
|
|
47
|
+
if label_col is not None:
|
|
48
|
+
title += f" · {_table.label_name(label_col)} = {df[label_col].iloc[args.row]}"
|
|
49
|
+
charts.draw(args.chart, values, title=title, xlabel="position",
|
|
50
|
+
ylabel="value", centre=centre, grid=grid)
|
|
51
|
+
else:
|
|
52
|
+
col = _table.resolve_column(df, args.col)
|
|
53
|
+
charts.draw(args.chart, df[col], title=str(col), xlabel="row",
|
|
54
|
+
ylabel=str(col), centre=centre, grid=grid)
|
|
55
|
+
|
|
56
|
+
if args.out:
|
|
57
|
+
plt.savefig(args.out, dpi=150)
|
|
58
|
+
print(f"saved {args.out}")
|
|
59
|
+
else:
|
|
60
|
+
plt.show()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
tablelab/app.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""The widget UI: load a table, choose a series, choose a view, read the chart.
|
|
2
|
+
|
|
3
|
+
Two ways to choose a series, because "a column" means different things depending
|
|
4
|
+
on how the table is laid out:
|
|
5
|
+
|
|
6
|
+
Column a column down the rows - each row is an observation
|
|
7
|
+
Row a row across the columns - each row is a signal
|
|
8
|
+
|
|
9
|
+
In Row mode the label column is dropped from the plotted values (it is not part
|
|
10
|
+
of the signal) and its value for that row becomes the chart's title.
|
|
11
|
+
|
|
12
|
+
The chart itself is live where ipympl is installed: the toolbar above it pans
|
|
13
|
+
and zooms, and the grid checkbox is applied to the figure in place rather than
|
|
14
|
+
by redrawing, so ticking it does not throw away the zoom.
|
|
15
|
+
"""
|
|
16
|
+
import os
|
|
17
|
+
from .file_readers import table_location
|
|
18
|
+
from html import escape
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import ipywidgets as widgets
|
|
22
|
+
from IPython.display import display
|
|
23
|
+
|
|
24
|
+
from .helpers import data as _table
|
|
25
|
+
from .helpers import plotting as charts
|
|
26
|
+
from .helpers import plotting as figures
|
|
27
|
+
from .helpers.data import load_table, read_uploaded_dataframe
|
|
28
|
+
from .helpers.widgets import FilePicker
|
|
29
|
+
|
|
30
|
+
ACCEPTED = ".csv,.tsv,.txt,.json,.jsonl,.parquet,.feather,.xlsx,.xls"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TableApp:
|
|
34
|
+
"""Several loaded tables, one chart. `app.df` is the active DataFrame."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, zoom=True):
|
|
37
|
+
self.df = None
|
|
38
|
+
self.tables = []
|
|
39
|
+
self._type_selection = None
|
|
40
|
+
self._setting_type = False
|
|
41
|
+
self.label_col = None
|
|
42
|
+
self._loading = False # suppress redraws while wiring up a new table
|
|
43
|
+
self._rows = [] # df positions behind the row slider
|
|
44
|
+
self._fig = None # the chart on screen, for the grid toggle
|
|
45
|
+
|
|
46
|
+
# Switching backends is notebook-wide, so it happens once, here, rather
|
|
47
|
+
# than every time a chart is drawn. With `zoom=False` the notebook's own
|
|
48
|
+
# backend is left alone - which may already be an interactive one.
|
|
49
|
+
self.zoom = figures.enable_zoom() if zoom else figures.zoom_active()
|
|
50
|
+
|
|
51
|
+
# -- where the table comes from
|
|
52
|
+
self.browse_btn = widgets.Button(description="Browse...", icon="folder-open",
|
|
53
|
+
tooltip="Pick a file the kernel can see",
|
|
54
|
+
layout=widgets.Layout(width="130px"))
|
|
55
|
+
self.path = widgets.Text(placeholder="local file path or https://… CSV URL",
|
|
56
|
+
layout=widgets.Layout(width="330px"))
|
|
57
|
+
self.load_btn = widgets.Button(description="Add table", icon="plus",
|
|
58
|
+
layout=widgets.Layout(width="100px"))
|
|
59
|
+
self.upload = widgets.FileUpload(accept=ACCEPTED, multiple=False,
|
|
60
|
+
description="Upload",
|
|
61
|
+
tooltip="Send a file from this browser "
|
|
62
|
+
"to the kernel")
|
|
63
|
+
self.picker = FilePicker(self._load_picked)
|
|
64
|
+
self.status = widgets.HTML(self._zoom_hint())
|
|
65
|
+
self.table = widgets.Dropdown(description="table:", disabled=True,
|
|
66
|
+
layout=widgets.Layout(width="520px"))
|
|
67
|
+
self.remove_btn = widgets.Button(description="Remove table", icon="trash",
|
|
68
|
+
disabled=True)
|
|
69
|
+
|
|
70
|
+
# -- which series
|
|
71
|
+
self.mode = widgets.ToggleButtons(
|
|
72
|
+
options=[("Column · down the rows", "column"),
|
|
73
|
+
("Row · across the columns", "row")],
|
|
74
|
+
value="column", style={"button_width": "190px"}, disabled=True)
|
|
75
|
+
self.column = widgets.Dropdown(description="column:", disabled=True,
|
|
76
|
+
layout=widgets.Layout(width="320px", height="32px"))
|
|
77
|
+
self.label_filter = widgets.Dropdown(description="label:", disabled=True,
|
|
78
|
+
layout=widgets.Layout(width="220px"))
|
|
79
|
+
self.row = widgets.IntSlider(description="row:", min=0, max=0,
|
|
80
|
+
continuous_update=False, disabled=True,
|
|
81
|
+
layout=widgets.Layout(width="420px"))
|
|
82
|
+
|
|
83
|
+
# -- which view of it
|
|
84
|
+
self.kind = widgets.Dropdown(options=charts.KINDS, value="run",
|
|
85
|
+
description="chart:",
|
|
86
|
+
layout=widgets.Layout(width="280px"))
|
|
87
|
+
self.centre = widgets.Dropdown(options=[("median", "median"), ("mean", "mean"),
|
|
88
|
+
("none", None)],
|
|
89
|
+
value="median", description="centre:",
|
|
90
|
+
layout=widgets.Layout(width="180px"))
|
|
91
|
+
self.grid = widgets.Checkbox(value=False, description="grid lines",
|
|
92
|
+
indent=False,
|
|
93
|
+
layout=widgets.Layout(width="120px"),
|
|
94
|
+
tooltip="Vertical grid lines as well as the "
|
|
95
|
+
"horizontal ones")
|
|
96
|
+
|
|
97
|
+
self.chart = widgets.Output()
|
|
98
|
+
self.text_preview = widgets.Output()
|
|
99
|
+
self.data_type = widgets.Dropdown(
|
|
100
|
+
description="Data type:", options=[], disabled=True,
|
|
101
|
+
layout=widgets.Layout(width="270px", height="32px"),
|
|
102
|
+
style={"description_width": "80px"},
|
|
103
|
+
tooltip="Automatically detected for each column. Choose Categorical to see counts.")
|
|
104
|
+
self.chart_tabs = widgets.Tab(children=[widgets.Box() for _ in charts.KINDS],
|
|
105
|
+
layout=widgets.Layout(display="none"))
|
|
106
|
+
for i, (label, _) in enumerate(charts.KINDS):
|
|
107
|
+
self.chart_tabs.set_title(i, label)
|
|
108
|
+
self.chart_tabs.children[0].children = [self.chart]
|
|
109
|
+
self.preview = widgets.Output()
|
|
110
|
+
self.stats = widgets.Output()
|
|
111
|
+
|
|
112
|
+
self.browse_btn.on_click(lambda _: self.picker.toggle())
|
|
113
|
+
self.load_btn.on_click(lambda _: self._load_path())
|
|
114
|
+
self.upload.observe(self._load_upload, names="value")
|
|
115
|
+
self.table.observe(lambda _: self._select_table(), names="value")
|
|
116
|
+
self.remove_btn.on_click(lambda _: self._remove_table())
|
|
117
|
+
for w in (self.mode, self.column, self.label_filter, self.row,
|
|
118
|
+
self.kind, self.centre):
|
|
119
|
+
w.observe(lambda _: self._draw(), names="value")
|
|
120
|
+
self.mode.observe(lambda _: self._show_controls(), names="value")
|
|
121
|
+
self.grid.observe(self._on_grid, names="value")
|
|
122
|
+
self.chart_tabs.observe(self._on_chart_tab, names="selected_index")
|
|
123
|
+
self.data_type.observe(self._on_data_type, names="value")
|
|
124
|
+
|
|
125
|
+
self._column_box = widgets.HBox([self.column, self.data_type],
|
|
126
|
+
layout=widgets.Layout(align_items="center"))
|
|
127
|
+
self._row_box = widgets.HBox([self.label_filter, self.row])
|
|
128
|
+
self._controls = widgets.Box([self._column_box])
|
|
129
|
+
self._view_box = widgets.HBox([self.centre, self.grid],
|
|
130
|
+
layout=widgets.Layout(display="none"))
|
|
131
|
+
|
|
132
|
+
preview_acc = widgets.Accordion(children=[self.preview], selected_index=None)
|
|
133
|
+
preview_acc.set_title(0, "First rows")
|
|
134
|
+
self.stats_acc = widgets.Accordion(children=[self.stats], selected_index=None,
|
|
135
|
+
layout=widgets.Layout(display="none"))
|
|
136
|
+
self.stats_acc.set_title(0, "Summary statistics")
|
|
137
|
+
self.ui = widgets.VBox([
|
|
138
|
+
widgets.HBox([self.browse_btn, self.path, self.load_btn, self.upload]),
|
|
139
|
+
self.picker.ui, widgets.HBox([self.table, self.remove_btn]), self.status,
|
|
140
|
+
preview_acc, self.mode, self._controls,
|
|
141
|
+
self._view_box, self.chart_tabs, self.text_preview, self.stats_acc,
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
# -- loading -------------------------------------------------------------
|
|
145
|
+
def _load_path(self):
|
|
146
|
+
if not self.path.value.strip():
|
|
147
|
+
self.picker.open()
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
path, filename = table_location(self.path.value)
|
|
151
|
+
except ValueError as exc:
|
|
152
|
+
self.status.value = escape(str(exc))
|
|
153
|
+
return
|
|
154
|
+
for i, entry in enumerate(self.tables):
|
|
155
|
+
if entry["source"] == path:
|
|
156
|
+
self.table.value = i
|
|
157
|
+
return
|
|
158
|
+
self._accept(lambda: load_table(path), name=filename, source=path)
|
|
159
|
+
|
|
160
|
+
def _load_picked(self, path):
|
|
161
|
+
self.path.value = path
|
|
162
|
+
self._load_path()
|
|
163
|
+
|
|
164
|
+
def _load_upload(self, change):
|
|
165
|
+
files = change["new"]
|
|
166
|
+
if not files:
|
|
167
|
+
return
|
|
168
|
+
f = files[0] if isinstance(files, (list, tuple)) else list(files.values())[0]
|
|
169
|
+
self.path.value = f["name"]
|
|
170
|
+
self._accept(lambda: read_uploaded_dataframe(bytes(f["content"]), f["name"]),
|
|
171
|
+
name=f["name"])
|
|
172
|
+
|
|
173
|
+
def _accept(self, read, name="Table", source=None):
|
|
174
|
+
try:
|
|
175
|
+
df = read()
|
|
176
|
+
except Exception as e:
|
|
177
|
+
self.status.value = ("<pre style='color:#b3261e'>"
|
|
178
|
+
f"{escape(type(e).__name__ + ': ' + str(e))}</pre>")
|
|
179
|
+
return
|
|
180
|
+
self.tables.append({"name": name, "source": source, "df": df})
|
|
181
|
+
self._fill_table_menu(len(self.tables) - 1)
|
|
182
|
+
|
|
183
|
+
def _fill_table_menu(self, selected):
|
|
184
|
+
self._loading = True
|
|
185
|
+
try:
|
|
186
|
+
self.table.options = [(f"{i + 1}: {entry['name']}", i)
|
|
187
|
+
for i, entry in enumerate(self.tables)]
|
|
188
|
+
self.table.value = selected
|
|
189
|
+
self.table.disabled = self.remove_btn.disabled = not self.tables
|
|
190
|
+
finally:
|
|
191
|
+
self._loading = False
|
|
192
|
+
self._select_table()
|
|
193
|
+
|
|
194
|
+
def _remove_table(self):
|
|
195
|
+
i = self.table.value
|
|
196
|
+
if i is None:
|
|
197
|
+
return
|
|
198
|
+
self.tables.pop(i)
|
|
199
|
+
self._fill_table_menu(min(i, len(self.tables) - 1) if self.tables else None)
|
|
200
|
+
|
|
201
|
+
def _select_table(self):
|
|
202
|
+
if self._loading:
|
|
203
|
+
return
|
|
204
|
+
self._type_selection = None
|
|
205
|
+
i = self.table.value
|
|
206
|
+
if i is None:
|
|
207
|
+
self.df = self.label_col = self._fig = None
|
|
208
|
+
self._rows = []
|
|
209
|
+
self._loading = True
|
|
210
|
+
try:
|
|
211
|
+
self.column.options = []
|
|
212
|
+
self.label_filter.options = [("all", None)]
|
|
213
|
+
self.row.value = self.row.max = 0
|
|
214
|
+
for w in (self.mode, self.column, self.label_filter, self.row):
|
|
215
|
+
w.disabled = True
|
|
216
|
+
finally:
|
|
217
|
+
self._loading = False
|
|
218
|
+
self._reset_data_type()
|
|
219
|
+
self._show_numeric_controls(False)
|
|
220
|
+
for output in (self.chart, self.preview, self.stats, self.text_preview):
|
|
221
|
+
output.outputs = ()
|
|
222
|
+
self.status.value = self._zoom_hint()
|
|
223
|
+
return
|
|
224
|
+
entry = self.tables[i]
|
|
225
|
+
self.df = entry["df"]
|
|
226
|
+
self.path.value = entry["source"] or entry["name"]
|
|
227
|
+
self.label_col = _table.find_label_column(self.df)
|
|
228
|
+
self.status.value = f"<pre>{escape(entry['name'])}\n{escape(_table.describe(self.df))}</pre>"
|
|
229
|
+
self._fig = None
|
|
230
|
+
for output in (self.chart, self.stats, self.text_preview):
|
|
231
|
+
output.outputs = ()
|
|
232
|
+
|
|
233
|
+
self._loading = True
|
|
234
|
+
try:
|
|
235
|
+
cols = _table.column_choices(self.df)
|
|
236
|
+
self.column.options = cols
|
|
237
|
+
# A Dropdown built without options does not adopt the first one when
|
|
238
|
+
# options arrive later - it stays on None. Choose explicitly.
|
|
239
|
+
self.column.value = cols[0] if cols else None
|
|
240
|
+
levels = ([("all", None)] + [(str(v), v) for v in
|
|
241
|
+
sorted(self.df[self.label_col].dropna().unique())]
|
|
242
|
+
if self.label_col is not None else [("all", None)])
|
|
243
|
+
self.label_filter.options = levels
|
|
244
|
+
self.label_filter.value = None # "all"
|
|
245
|
+
self.row.value = 0
|
|
246
|
+
for w in (self.mode, self.column, self.label_filter, self.row):
|
|
247
|
+
w.disabled = False
|
|
248
|
+
self.label_filter.disabled = self.label_col is None
|
|
249
|
+
self._set_rows()
|
|
250
|
+
finally:
|
|
251
|
+
self._loading = False
|
|
252
|
+
|
|
253
|
+
self._set_html(self.preview, self.df.head().to_html())
|
|
254
|
+
self._draw()
|
|
255
|
+
|
|
256
|
+
# -- controls ------------------------------------------------------------
|
|
257
|
+
def _show_controls(self):
|
|
258
|
+
self._column_box.children = ([self.column, self.data_type]
|
|
259
|
+
if self.mode.value == "column" else [])
|
|
260
|
+
self._row_box.children = ([self.data_type, self.label_filter, self.row]
|
|
261
|
+
if self.mode.value == "row" else
|
|
262
|
+
[self.label_filter, self.row])
|
|
263
|
+
self._controls.children = [
|
|
264
|
+
self._row_box if self.mode.value == "row" else self._column_box]
|
|
265
|
+
|
|
266
|
+
def _set_rows(self):
|
|
267
|
+
"""Which df positions the row slider walks, given the label filter."""
|
|
268
|
+
if self.df is None:
|
|
269
|
+
return
|
|
270
|
+
wanted = self.label_filter.value
|
|
271
|
+
if wanted is None or self.label_col is None:
|
|
272
|
+
self._rows = list(range(len(self.df)))
|
|
273
|
+
else:
|
|
274
|
+
mask = (self.df[self.label_col] == wanted).to_numpy()
|
|
275
|
+
self._rows = [i for i, keep in enumerate(mask) if keep]
|
|
276
|
+
self.row.max = max(len(self._rows) - 1, 0)
|
|
277
|
+
self.row.value = min(self.row.value, self.row.max)
|
|
278
|
+
|
|
279
|
+
# -- the series on screen -------------------------------------------------
|
|
280
|
+
def series(self):
|
|
281
|
+
"""The series the chart is showing, with its labels, or None."""
|
|
282
|
+
if self.df is None:
|
|
283
|
+
return None
|
|
284
|
+
if self.mode.value == "row":
|
|
285
|
+
self._set_rows()
|
|
286
|
+
if not self._rows:
|
|
287
|
+
return None
|
|
288
|
+
pos = self._rows[self.row.value]
|
|
289
|
+
title = f"row {pos}"
|
|
290
|
+
if self.label_col is not None:
|
|
291
|
+
title += (f" · {_table.label_name(self.label_col)} = "
|
|
292
|
+
f"{self.df[self.label_col].iloc[pos]}")
|
|
293
|
+
return _table.row_series(self.df, pos, self.label_col), title, "position", "value"
|
|
294
|
+
|
|
295
|
+
col = self.column.value
|
|
296
|
+
if col is None:
|
|
297
|
+
return None
|
|
298
|
+
return self.df[col], str(col), "row", str(col)
|
|
299
|
+
|
|
300
|
+
# -- drawing -------------------------------------------------------------
|
|
301
|
+
def _on_data_type(self, _change):
|
|
302
|
+
if not self._setting_type:
|
|
303
|
+
self._draw()
|
|
304
|
+
|
|
305
|
+
def _reset_data_type(self):
|
|
306
|
+
self._setting_type = True
|
|
307
|
+
try:
|
|
308
|
+
self.data_type.options = []
|
|
309
|
+
self.data_type.disabled = True
|
|
310
|
+
self._type_selection = None
|
|
311
|
+
finally:
|
|
312
|
+
self._setting_type = False
|
|
313
|
+
|
|
314
|
+
def _detect_data_type(self, values):
|
|
315
|
+
selection = (self.table.value, self.mode.value,
|
|
316
|
+
self.column.value if self.mode.value == "column" else None)
|
|
317
|
+
if selection == self._type_selection:
|
|
318
|
+
return
|
|
319
|
+
self._setting_type = True
|
|
320
|
+
try:
|
|
321
|
+
dtype = str(values.dtype)
|
|
322
|
+
options = [(f"Categorical ({dtype})", "categorical")]
|
|
323
|
+
numeric = _table.is_numeric(values)
|
|
324
|
+
if numeric:
|
|
325
|
+
options.insert(0, (f"Numeric ({dtype})", "numeric"))
|
|
326
|
+
self.data_type.options = options
|
|
327
|
+
self.data_type.value = "numeric" if numeric else "categorical"
|
|
328
|
+
self.data_type.disabled = not numeric
|
|
329
|
+
self.data_type.tooltip = (f"Detected: {_table.data_type(values)} ({dtype}). "
|
|
330
|
+
"Choose Categorical to see value counts." if numeric else
|
|
331
|
+
f"Detected: {_table.data_type(values)} ({dtype}). "
|
|
332
|
+
"Numeric drawings require numeric data.")
|
|
333
|
+
self._type_selection = selection
|
|
334
|
+
finally:
|
|
335
|
+
self._setting_type = False
|
|
336
|
+
|
|
337
|
+
def _on_chart_tab(self, change):
|
|
338
|
+
if change["new"] is not None:
|
|
339
|
+
self.kind.value = charts.KINDS[change["new"]][1]
|
|
340
|
+
|
|
341
|
+
def _show_numeric_controls(self, numeric):
|
|
342
|
+
for widget in (self._view_box, self.chart_tabs, self.stats_acc):
|
|
343
|
+
widget.layout.display = "" if numeric else "none"
|
|
344
|
+
self.text_preview.layout.display = "none" if numeric else ""
|
|
345
|
+
|
|
346
|
+
def _draw(self):
|
|
347
|
+
if self.df is None or self._loading:
|
|
348
|
+
return
|
|
349
|
+
chosen = self.series()
|
|
350
|
+
if chosen is None:
|
|
351
|
+
self._reset_data_type()
|
|
352
|
+
self._show_numeric_controls(False)
|
|
353
|
+
self._fig = None
|
|
354
|
+
self.text_preview.outputs = ()
|
|
355
|
+
return
|
|
356
|
+
values, title, xlabel, ylabel = chosen
|
|
357
|
+
self._detect_data_type(values)
|
|
358
|
+
numeric = self.data_type.value == "numeric"
|
|
359
|
+
self._show_numeric_controls(numeric)
|
|
360
|
+
|
|
361
|
+
if not numeric:
|
|
362
|
+
self._show_text_column(values, title)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
index = [key for _, key in charts.KINDS].index(self.kind.value)
|
|
366
|
+
for i, panel in enumerate(self.chart_tabs.children):
|
|
367
|
+
panel.children = [self.chart] if i == index else []
|
|
368
|
+
self.chart_tabs.selected_index = index
|
|
369
|
+
|
|
370
|
+
# A box plot draws its own median, so a centre line would repeat it.
|
|
371
|
+
self.centre.disabled = self.kind.value not in charts.CENTRED
|
|
372
|
+
|
|
373
|
+
fig = figures.figure()
|
|
374
|
+
charts.draw(self.kind.value, values, title=title, xlabel=xlabel,
|
|
375
|
+
ylabel=ylabel, centre=self.centre.value,
|
|
376
|
+
ax=fig.add_subplot(111),
|
|
377
|
+
grid=True if self.grid.value else "y")
|
|
378
|
+
self._fig = fig
|
|
379
|
+
figures.show_in_output(fig, self.chart)
|
|
380
|
+
self.text_preview.outputs = ()
|
|
381
|
+
self._set_html(self.stats, _table.summary_stats(values).to_html(
|
|
382
|
+
index=False, float_format=lambda v: f"{v:.4g}"))
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _set_html(output, html):
|
|
386
|
+
output.outputs = ({"output_type": "display_data",
|
|
387
|
+
"data": {"text/html": html}, "metadata": {}},)
|
|
388
|
+
|
|
389
|
+
def _show_text_column(self, values, title):
|
|
390
|
+
"""A text column has no chart to draw, but its values can still be counted."""
|
|
391
|
+
self._fig = None
|
|
392
|
+
self.chart.outputs = ()
|
|
393
|
+
counts = values.value_counts(dropna=False).rename_axis(title).reset_index(name="count")
|
|
394
|
+
html = (f"<p style='color:#52514e'><b>{escape(title)}</b>: "
|
|
395
|
+
f"{len(counts)} distinct values. Most common values:</p>"
|
|
396
|
+
+ counts.head(20).to_html(index=False))
|
|
397
|
+
self._set_html(self.text_preview, html)
|
|
398
|
+
self.stats.outputs = ()
|
|
399
|
+
|
|
400
|
+
def _on_grid(self, _change):
|
|
401
|
+
"""Toggle the grid on the figure already on screen.
|
|
402
|
+
|
|
403
|
+
Redrawing instead would reset the zoom, which is exactly what someone
|
|
404
|
+
ticking the box halfway through an inspection does not want. A static
|
|
405
|
+
figure is closed once shown, so there it does mean a redraw.
|
|
406
|
+
"""
|
|
407
|
+
if self._fig is not None and plt.fignum_exists(self._fig.number):
|
|
408
|
+
charts.apply_grid(self._fig, self.grid.value)
|
|
409
|
+
else:
|
|
410
|
+
self._draw()
|
|
411
|
+
|
|
412
|
+
def _zoom_hint(self):
|
|
413
|
+
if self.zoom:
|
|
414
|
+
return ("<span style='color:#52514e'>Pan and zoom with the toolbar "
|
|
415
|
+
"above the chart.</span>")
|
|
416
|
+
return ("<span style='color:#52514e'>Static charts - for the pan/zoom "
|
|
417
|
+
"toolbar: <code>pip install \"tablelab[zoom]\"</code></span>")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def main(path=None, zoom=True):
|
|
421
|
+
"""Display the app, optionally opening on `path`.
|
|
422
|
+
|
|
423
|
+
`zoom=False` leaves the notebook's matplotlib backend alone and draws static
|
|
424
|
+
charts, for when the interactive canvas is in the way.
|
|
425
|
+
"""
|
|
426
|
+
app = TableApp(zoom=zoom)
|
|
427
|
+
display(app.ui)
|
|
428
|
+
if path is not None:
|
|
429
|
+
app.path.value = str(path)
|
|
430
|
+
app._load_path()
|
|
431
|
+
return app
|