perspective-python 4.2.0__cp311-abi3-win_amd64.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.
- perspective/__init__.py +396 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +11 -0
- perspective/handlers/aiohttp.py +61 -0
- perspective/handlers/starlette.py +55 -0
- perspective/handlers/tornado.py +184 -0
- perspective/perspective.pyd +0 -0
- perspective/templates/exported_widget.html.template +35 -0
- perspective/tests/__init__.py +11 -0
- perspective/tests/async/test_async_client.py +83 -0
- perspective/tests/async/test_websocket_client.py +124 -0
- perspective/tests/conftest.py +272 -0
- perspective/tests/core/__init__.py +11 -0
- perspective/tests/core/test_async.py +351 -0
- perspective/tests/multi_threaded/__init__.py +11 -0
- perspective/tests/multi_threaded/test_multi_threaded.py +201 -0
- perspective/tests/server/__init__.py +11 -0
- perspective/tests/server/test_server.py +1016 -0
- perspective/tests/server/test_session.py +110 -0
- perspective/tests/table/__init__.py +11 -0
- perspective/tests/table/arrow/date32.arrow +0 -0
- perspective/tests/table/arrow/date64.arrow +0 -0
- perspective/tests/table/arrow/dict.arrow +0 -0
- perspective/tests/table/arrow/dict_update.arrow +0 -0
- perspective/tests/table/arrow/int_float_str.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
- perspective/tests/table/object_sequence.py +402 -0
- perspective/tests/table/test_column_paths.py +89 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_exception.py +65 -0
- perspective/tests/table/test_leaks.py +54 -0
- perspective/tests/table/test_ports.py +178 -0
- perspective/tests/table/test_remove.py +102 -0
- perspective/tests/table/test_table.py +641 -0
- perspective/tests/table/test_table_arrow.py +503 -0
- perspective/tests/table/test_table_datetime.py +2409 -0
- perspective/tests/table/test_table_infer.py +201 -0
- perspective/tests/table/test_table_limit.py +45 -0
- perspective/tests/table/test_table_numpy.py +1022 -0
- perspective/tests/table/test_table_pandas.py +1018 -0
- perspective/tests/table/test_table_polars.py +251 -0
- perspective/tests/table/test_table_view_table.py +130 -0
- perspective/tests/table/test_to_arrow.py +417 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_format.py +1024 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +545 -0
- perspective/tests/table/test_update_arrow.py +980 -0
- perspective/tests/table/test_update_pandas.py +211 -0
- perspective/tests/table/test_view.py +2261 -0
- perspective/tests/table/test_view_expression.py +1940 -0
- perspective/tests/test_dependencies.py +53 -0
- perspective/tests/viewer/__init__.py +11 -0
- perspective/tests/viewer/test_viewer.py +246 -0
- perspective/tests/widget/__init__.py +11 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +134 -0
- perspective/virtual_servers/clickhouse.py +245 -0
- perspective/virtual_servers/duckdb.py +236 -0
- perspective/widget/__init__.py +349 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/validate.py +22 -0
- perspective/widget/viewer/viewer.py +343 -0
- perspective/widget/viewer/viewer_traitlets.py +101 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/install.json +5 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/package.json +71 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js +2 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js.LICENSE.txt +25 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/523.c030af5d3c4f67ff83f6.js +1 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/remoteEntry.95a8ea1b44d96032833f.js +1 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/style.js +4 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/third-party-licenses.json +16 -0
- perspective_python-4.2.0.dist-info/METADATA +27 -0
- perspective_python-4.2.0.dist-info/RECORD +79 -0
- perspective_python-4.2.0.dist-info/WHEEL +4 -0
- perspective_python-4.2.0.dist-info/licenses/LICENSE.md +193 -0
- perspective_python-4.2.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +17395 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pytest
|
|
16
|
+
from perspective.widget import PerspectiveWidget
|
|
17
|
+
import perspective as psp
|
|
18
|
+
|
|
19
|
+
client = psp.Server().new_local_client()
|
|
20
|
+
Table = client.table
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
pytest.skip(allow_module_level=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestWidgetPandas:
|
|
27
|
+
def test_widget_load_table_df(self, superstore):
|
|
28
|
+
table = Table(superstore)
|
|
29
|
+
widget = PerspectiveWidget(table)
|
|
30
|
+
assert widget.table.schema() == {
|
|
31
|
+
"index": "integer",
|
|
32
|
+
"Country": "string",
|
|
33
|
+
"Region": "string",
|
|
34
|
+
"Category": "string",
|
|
35
|
+
"City": "string",
|
|
36
|
+
"Customer ID": "string",
|
|
37
|
+
"Discount": "float",
|
|
38
|
+
"Order Date": "date",
|
|
39
|
+
"Order ID": "string",
|
|
40
|
+
"Postal Code": "string",
|
|
41
|
+
"Product ID": "string",
|
|
42
|
+
"Profit": "float",
|
|
43
|
+
"Quantity": "integer",
|
|
44
|
+
"Row ID": "integer",
|
|
45
|
+
"Sales": "integer",
|
|
46
|
+
"Segment": "string",
|
|
47
|
+
"Ship Date": "date",
|
|
48
|
+
"Ship Mode": "string",
|
|
49
|
+
"State": "string",
|
|
50
|
+
"Sub-Category": "string",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
assert sorted(widget.columns) == sorted(
|
|
54
|
+
[
|
|
55
|
+
"index",
|
|
56
|
+
"Category",
|
|
57
|
+
"City",
|
|
58
|
+
"Country",
|
|
59
|
+
"Customer ID",
|
|
60
|
+
"Discount",
|
|
61
|
+
"Order Date",
|
|
62
|
+
"Order ID",
|
|
63
|
+
"Postal Code",
|
|
64
|
+
"Product ID",
|
|
65
|
+
"Profit",
|
|
66
|
+
"Quantity",
|
|
67
|
+
"Region",
|
|
68
|
+
"Row ID",
|
|
69
|
+
"Sales",
|
|
70
|
+
"Segment",
|
|
71
|
+
"Ship Date",
|
|
72
|
+
"Ship Mode",
|
|
73
|
+
"State",
|
|
74
|
+
"Sub-Category",
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
view = widget.table.view()
|
|
78
|
+
assert view.num_rows() == len(superstore)
|
|
79
|
+
assert view.num_columns() == len(superstore.columns) + 1 # index
|
|
80
|
+
|
|
81
|
+
def test_widget_load_data_df(self, superstore):
|
|
82
|
+
widget = PerspectiveWidget(superstore)
|
|
83
|
+
assert sorted(widget.columns) == sorted(
|
|
84
|
+
[
|
|
85
|
+
"index",
|
|
86
|
+
"Category",
|
|
87
|
+
"City",
|
|
88
|
+
"Country",
|
|
89
|
+
"Customer ID",
|
|
90
|
+
"Discount",
|
|
91
|
+
"Order Date",
|
|
92
|
+
"Order ID",
|
|
93
|
+
"Postal Code",
|
|
94
|
+
"Product ID",
|
|
95
|
+
"Profit",
|
|
96
|
+
"Quantity",
|
|
97
|
+
"Region",
|
|
98
|
+
"Row ID",
|
|
99
|
+
"Sales",
|
|
100
|
+
"Segment",
|
|
101
|
+
"Ship Date",
|
|
102
|
+
"Ship Mode",
|
|
103
|
+
"State",
|
|
104
|
+
"Sub-Category",
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
view = widget.table.view()
|
|
108
|
+
assert view.num_rows() == len(superstore)
|
|
109
|
+
assert view.num_columns() == 20
|
|
110
|
+
|
|
111
|
+
def test_widget_load_series(self, superstore):
|
|
112
|
+
series = pd.Series(superstore["Profit"].values, name="profit")
|
|
113
|
+
widget = PerspectiveWidget(series)
|
|
114
|
+
assert widget.table.schema() == {"index": "integer", "profit": "float"}
|
|
115
|
+
|
|
116
|
+
assert sorted(widget.columns) == sorted(["index", "profit"])
|
|
117
|
+
view = widget.table.view()
|
|
118
|
+
assert view.num_rows() == len(superstore)
|
|
119
|
+
assert view.num_columns() == 2
|
|
120
|
+
|
|
121
|
+
def test_widget_load_pivot_table(self, superstore):
|
|
122
|
+
pivot_table = pd.pivot_table(
|
|
123
|
+
superstore,
|
|
124
|
+
values="Discount",
|
|
125
|
+
index=["Country", "Region"],
|
|
126
|
+
columns=["Category", "Segment"],
|
|
127
|
+
)
|
|
128
|
+
widget = PerspectiveWidget(pivot_table)
|
|
129
|
+
assert widget.group_by == ["Country", "Region"]
|
|
130
|
+
assert widget.split_by == ["Category", "Segment"]
|
|
131
|
+
assert widget.columns == ["value"]
|
|
132
|
+
# table should host flattened data
|
|
133
|
+
view = widget.table.view()
|
|
134
|
+
assert view.num_rows() == 60
|
|
135
|
+
assert view.num_columns() == 6
|
|
136
|
+
|
|
137
|
+
def test_widget_load_pivot_table_with_user_pivots(self, superstore):
|
|
138
|
+
pivot_table = pd.pivot_table(
|
|
139
|
+
superstore,
|
|
140
|
+
values="Discount",
|
|
141
|
+
index=["Country", "Region"],
|
|
142
|
+
columns="Category",
|
|
143
|
+
)
|
|
144
|
+
widget = PerspectiveWidget(pivot_table, group_by=["Category", "Segment"])
|
|
145
|
+
assert widget.group_by == ["Category", "Segment"]
|
|
146
|
+
assert widget.split_by == []
|
|
147
|
+
assert widget.columns == [
|
|
148
|
+
"index",
|
|
149
|
+
"Country",
|
|
150
|
+
"Region",
|
|
151
|
+
"Financials",
|
|
152
|
+
"Industrials",
|
|
153
|
+
"Technology",
|
|
154
|
+
]
|
|
155
|
+
# table should host flattened data
|
|
156
|
+
view = widget.table.view()
|
|
157
|
+
assert view.num_rows() == 5
|
|
158
|
+
assert view.num_columns() == 6
|
|
159
|
+
|
|
160
|
+
def test_widget_load_group_by(self, superstore):
|
|
161
|
+
df_pivoted = superstore.set_index(["Country", "Region"])
|
|
162
|
+
widget = PerspectiveWidget(df_pivoted)
|
|
163
|
+
assert widget.group_by == ["Country", "Region"]
|
|
164
|
+
assert widget.split_by == []
|
|
165
|
+
assert sorted(widget.columns) == sorted(
|
|
166
|
+
[
|
|
167
|
+
"index",
|
|
168
|
+
"Category",
|
|
169
|
+
"Country",
|
|
170
|
+
"City",
|
|
171
|
+
"Customer ID",
|
|
172
|
+
"Discount",
|
|
173
|
+
"Order Date",
|
|
174
|
+
"Order ID",
|
|
175
|
+
"Postal Code",
|
|
176
|
+
"Product ID",
|
|
177
|
+
"Profit",
|
|
178
|
+
"Quantity",
|
|
179
|
+
"Region",
|
|
180
|
+
"Row ID",
|
|
181
|
+
"Sales",
|
|
182
|
+
"Segment",
|
|
183
|
+
"Ship Date",
|
|
184
|
+
"Ship Mode",
|
|
185
|
+
"State",
|
|
186
|
+
"Sub-Category",
|
|
187
|
+
]
|
|
188
|
+
)
|
|
189
|
+
assert widget.table.size() == 100
|
|
190
|
+
view = widget.table.view()
|
|
191
|
+
assert view.num_rows() == len(superstore)
|
|
192
|
+
assert view.num_columns() == len(superstore.columns) + 1 # index
|
|
193
|
+
|
|
194
|
+
def test_widget_load_group_by_with_user_pivots(self, superstore):
|
|
195
|
+
df_pivoted = superstore.set_index(["Country", "Region"])
|
|
196
|
+
widget = PerspectiveWidget(df_pivoted, group_by=["Category", "Segment"])
|
|
197
|
+
assert widget.group_by == ["Category", "Segment"]
|
|
198
|
+
assert widget.split_by == []
|
|
199
|
+
assert sorted(widget.columns) == sorted(
|
|
200
|
+
[
|
|
201
|
+
"index",
|
|
202
|
+
"Category",
|
|
203
|
+
"Country",
|
|
204
|
+
"City",
|
|
205
|
+
"Customer ID",
|
|
206
|
+
"Discount",
|
|
207
|
+
"Order Date",
|
|
208
|
+
"Order ID",
|
|
209
|
+
"Postal Code",
|
|
210
|
+
"Product ID",
|
|
211
|
+
"Profit",
|
|
212
|
+
"Quantity",
|
|
213
|
+
"Region",
|
|
214
|
+
"Row ID",
|
|
215
|
+
"Sales",
|
|
216
|
+
"Segment",
|
|
217
|
+
"Ship Date",
|
|
218
|
+
"Ship Mode",
|
|
219
|
+
"State",
|
|
220
|
+
"Sub-Category",
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
assert widget.table.size() == 100
|
|
224
|
+
view = widget.table.view()
|
|
225
|
+
assert view.num_rows() == len(superstore)
|
|
226
|
+
assert view.num_columns() == len(superstore.columns) + 1 # index
|
|
227
|
+
|
|
228
|
+
def test_widget_load_split_by(self, superstore):
|
|
229
|
+
arrays = [
|
|
230
|
+
np.array(
|
|
231
|
+
[
|
|
232
|
+
"bar",
|
|
233
|
+
"bar",
|
|
234
|
+
"bar",
|
|
235
|
+
"bar",
|
|
236
|
+
"baz",
|
|
237
|
+
"baz",
|
|
238
|
+
"baz",
|
|
239
|
+
"baz",
|
|
240
|
+
"foo",
|
|
241
|
+
"foo",
|
|
242
|
+
"foo",
|
|
243
|
+
"foo",
|
|
244
|
+
"qux",
|
|
245
|
+
"qux",
|
|
246
|
+
"qux",
|
|
247
|
+
"qux",
|
|
248
|
+
]
|
|
249
|
+
),
|
|
250
|
+
np.array(
|
|
251
|
+
[
|
|
252
|
+
"one",
|
|
253
|
+
"one",
|
|
254
|
+
"two",
|
|
255
|
+
"two",
|
|
256
|
+
"one",
|
|
257
|
+
"one",
|
|
258
|
+
"two",
|
|
259
|
+
"two",
|
|
260
|
+
"one",
|
|
261
|
+
"one",
|
|
262
|
+
"two",
|
|
263
|
+
"two",
|
|
264
|
+
"one",
|
|
265
|
+
"one",
|
|
266
|
+
"two",
|
|
267
|
+
"two",
|
|
268
|
+
]
|
|
269
|
+
),
|
|
270
|
+
np.array(
|
|
271
|
+
[
|
|
272
|
+
"X",
|
|
273
|
+
"Y",
|
|
274
|
+
"X",
|
|
275
|
+
"Y",
|
|
276
|
+
"X",
|
|
277
|
+
"Y",
|
|
278
|
+
"X",
|
|
279
|
+
"Y",
|
|
280
|
+
"X",
|
|
281
|
+
"Y",
|
|
282
|
+
"X",
|
|
283
|
+
"Y",
|
|
284
|
+
"X",
|
|
285
|
+
"Y",
|
|
286
|
+
"X",
|
|
287
|
+
"Y",
|
|
288
|
+
]
|
|
289
|
+
),
|
|
290
|
+
]
|
|
291
|
+
tuples = list(zip(*arrays))
|
|
292
|
+
index = pd.MultiIndex.from_tuples(tuples, names=["first", "second", "third"])
|
|
293
|
+
df_both = pd.DataFrame(
|
|
294
|
+
np.random.randn(3, 16), index=["A", "B", "C"], columns=index
|
|
295
|
+
)
|
|
296
|
+
widget = PerspectiveWidget(df_both)
|
|
297
|
+
assert widget.columns == ["value"]
|
|
298
|
+
assert widget.split_by == ["first", "second", "third"]
|
|
299
|
+
assert widget.group_by == ["index"]
|
|
300
|
+
|
|
301
|
+
def test_widget_load_split_by_preserve_user_settings(self, superstore):
|
|
302
|
+
arrays = [
|
|
303
|
+
np.array(
|
|
304
|
+
[
|
|
305
|
+
"bar",
|
|
306
|
+
"bar",
|
|
307
|
+
"bar",
|
|
308
|
+
"bar",
|
|
309
|
+
"baz",
|
|
310
|
+
"baz",
|
|
311
|
+
"baz",
|
|
312
|
+
"baz",
|
|
313
|
+
"foo",
|
|
314
|
+
"foo",
|
|
315
|
+
"foo",
|
|
316
|
+
"foo",
|
|
317
|
+
"qux",
|
|
318
|
+
"qux",
|
|
319
|
+
"qux",
|
|
320
|
+
"qux",
|
|
321
|
+
]
|
|
322
|
+
),
|
|
323
|
+
np.array(
|
|
324
|
+
[
|
|
325
|
+
"one",
|
|
326
|
+
"one",
|
|
327
|
+
"two",
|
|
328
|
+
"two",
|
|
329
|
+
"one",
|
|
330
|
+
"one",
|
|
331
|
+
"two",
|
|
332
|
+
"two",
|
|
333
|
+
"one",
|
|
334
|
+
"one",
|
|
335
|
+
"two",
|
|
336
|
+
"two",
|
|
337
|
+
"one",
|
|
338
|
+
"one",
|
|
339
|
+
"two",
|
|
340
|
+
"two",
|
|
341
|
+
]
|
|
342
|
+
),
|
|
343
|
+
np.array(
|
|
344
|
+
[
|
|
345
|
+
"X",
|
|
346
|
+
"Y",
|
|
347
|
+
"X",
|
|
348
|
+
"Y",
|
|
349
|
+
"X",
|
|
350
|
+
"Y",
|
|
351
|
+
"X",
|
|
352
|
+
"Y",
|
|
353
|
+
"X",
|
|
354
|
+
"Y",
|
|
355
|
+
"X",
|
|
356
|
+
"Y",
|
|
357
|
+
"X",
|
|
358
|
+
"Y",
|
|
359
|
+
"X",
|
|
360
|
+
"Y",
|
|
361
|
+
]
|
|
362
|
+
),
|
|
363
|
+
]
|
|
364
|
+
tuples = list(zip(*arrays))
|
|
365
|
+
index = pd.MultiIndex.from_tuples(tuples, names=["first", "second", "third"])
|
|
366
|
+
df_both = pd.DataFrame(
|
|
367
|
+
np.random.randn(3, 16), index=["A", "B", "C"], columns=index
|
|
368
|
+
)
|
|
369
|
+
widget = PerspectiveWidget(df_both, columns=["first", "third"])
|
|
370
|
+
assert widget.columns == ["first", "third"]
|
|
371
|
+
assert widget.split_by == ["first", "second", "third"]
|
|
372
|
+
assert widget.group_by == ["index"]
|
|
373
|
+
|
|
374
|
+
def test_pivottable_values_index(self, superstore):
|
|
375
|
+
arrays = {
|
|
376
|
+
"A": [
|
|
377
|
+
"bar",
|
|
378
|
+
"bar",
|
|
379
|
+
"bar",
|
|
380
|
+
"bar",
|
|
381
|
+
"baz",
|
|
382
|
+
"baz",
|
|
383
|
+
"baz",
|
|
384
|
+
"baz",
|
|
385
|
+
"foo",
|
|
386
|
+
"foo",
|
|
387
|
+
"foo",
|
|
388
|
+
"foo",
|
|
389
|
+
"qux",
|
|
390
|
+
"qux",
|
|
391
|
+
"qux",
|
|
392
|
+
"qux",
|
|
393
|
+
],
|
|
394
|
+
"B": [
|
|
395
|
+
"one",
|
|
396
|
+
"one",
|
|
397
|
+
"two",
|
|
398
|
+
"two",
|
|
399
|
+
"one",
|
|
400
|
+
"one",
|
|
401
|
+
"two",
|
|
402
|
+
"two",
|
|
403
|
+
"one",
|
|
404
|
+
"one",
|
|
405
|
+
"two",
|
|
406
|
+
"two",
|
|
407
|
+
"one",
|
|
408
|
+
"one",
|
|
409
|
+
"two",
|
|
410
|
+
"two",
|
|
411
|
+
],
|
|
412
|
+
"C": [
|
|
413
|
+
"X",
|
|
414
|
+
"Y",
|
|
415
|
+
"X",
|
|
416
|
+
"Y",
|
|
417
|
+
"X",
|
|
418
|
+
"Y",
|
|
419
|
+
"X",
|
|
420
|
+
"Y",
|
|
421
|
+
"X",
|
|
422
|
+
"Y",
|
|
423
|
+
"X",
|
|
424
|
+
"Y",
|
|
425
|
+
"X",
|
|
426
|
+
"Y",
|
|
427
|
+
"X",
|
|
428
|
+
"Y",
|
|
429
|
+
],
|
|
430
|
+
"D": np.arange(16),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
df = pd.DataFrame(arrays)
|
|
434
|
+
df_pivot = df.pivot_table(
|
|
435
|
+
values=["D"], index=["A"], columns=["B", "C"], aggfunc={"D": "count"}
|
|
436
|
+
)
|
|
437
|
+
widget = PerspectiveWidget(df_pivot)
|
|
438
|
+
assert widget.columns == ["value"]
|
|
439
|
+
assert widget.split_by == ["B", "C"]
|
|
440
|
+
assert widget.group_by == ["A"]
|
|
441
|
+
|
|
442
|
+
def test_pivottable_multi_values(self, superstore):
|
|
443
|
+
pt = pd.pivot_table(
|
|
444
|
+
superstore,
|
|
445
|
+
values=["Discount", "Sales"],
|
|
446
|
+
index=["Country", "Region"],
|
|
447
|
+
aggfunc={"Discount": "count", "Sales": "sum"},
|
|
448
|
+
columns=["State", "Quantity"],
|
|
449
|
+
)
|
|
450
|
+
widget = PerspectiveWidget(pt)
|
|
451
|
+
assert widget.columns == ["Discount", "Sales"]
|
|
452
|
+
assert widget.split_by == ["State", "Quantity"]
|
|
453
|
+
assert widget.group_by == ["Country", "Region"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VirtualSessionModel:
|
|
15
|
+
"""
|
|
16
|
+
An interface for implementing a Perspective `VirtualServer`. It operates
|
|
17
|
+
thusly:
|
|
18
|
+
|
|
19
|
+
- A table is selected by name (validated via `get_hosted_tables`).
|
|
20
|
+
|
|
21
|
+
- The UI will ask the model to create a temporary table with the results
|
|
22
|
+
of querying this table with a specific query `config`, a simple struct
|
|
23
|
+
which reflects the UI configurable fields (see `get_features`).
|
|
24
|
+
|
|
25
|
+
- The UI will query slices of the temporary table as it needs them to
|
|
26
|
+
render. This may be a rectangular slice, a whole column or the entire
|
|
27
|
+
set, and it is returned from teh model via a custom push-only
|
|
28
|
+
struct `PerspectiveColumn` for now, though in the future we will support
|
|
29
|
+
e.g. Polars and other arrow-native formats directly.
|
|
30
|
+
|
|
31
|
+
- The UI will delete its own temporary tables via `view_delete` but it is
|
|
32
|
+
ok for them to die intermittently, the UI will recover automatically.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def get_features(self):
|
|
36
|
+
"""
|
|
37
|
+
[OPTIONAL] Toggle UI features through data model support. For example,
|
|
38
|
+
setting `"group_by": False` would hide the "Group By" UI control, as
|
|
39
|
+
well as prevent this field from appearing in `config` dicts later
|
|
40
|
+
provided to `table_make_view`.
|
|
41
|
+
|
|
42
|
+
This API defaults to just "columns", e.g. a simple flat datagrid in
|
|
43
|
+
which you can just scroll, select and format columns.
|
|
44
|
+
|
|
45
|
+
# Example
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
return {
|
|
49
|
+
"group_by": True,
|
|
50
|
+
"split_by": True,
|
|
51
|
+
"sort": True,
|
|
52
|
+
"expressions": True,
|
|
53
|
+
"filter_ops": {
|
|
54
|
+
"integer": ["==", "<"],
|
|
55
|
+
},
|
|
56
|
+
"aggregates": {
|
|
57
|
+
"string": ["count"],
|
|
58
|
+
"float": ["count", "sum"],
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def get_hosted_tables(self) -> list[str]:
|
|
67
|
+
"""
|
|
68
|
+
List of `Table` names available to query from.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def table_schema(self, table_name):
|
|
74
|
+
"""
|
|
75
|
+
Get the _Perspective Schema_ for a `Table`, a mapping of column name to
|
|
76
|
+
Perspective column types, a simplified set of six visually-relevant
|
|
77
|
+
types mapped from DuckDB's much richer type system. Optionally,
|
|
78
|
+
a model may also implement `view_schema` which describes temporary
|
|
79
|
+
tables, but for DuckDB this method is identical.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def table_size(self, table_name):
|
|
85
|
+
"""
|
|
86
|
+
Get a table's row count. Optionally, a model may also implement the
|
|
87
|
+
`view_size` method to get the row count for temporary tables, but for
|
|
88
|
+
DuckDB this method is identical.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def view_schema(self, view_name, config):
|
|
94
|
+
return self.table_schema(view_name)
|
|
95
|
+
|
|
96
|
+
def view_size(self, view_name):
|
|
97
|
+
return self.table_size(view_name)
|
|
98
|
+
|
|
99
|
+
def table_make_view(self, table_name, view_name, config):
|
|
100
|
+
"""
|
|
101
|
+
Create a temporary table `view_name` from the results of querying
|
|
102
|
+
`table_name` with a query configuration `config`.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def table_validate_expression(self, view_name, expression):
|
|
108
|
+
"""
|
|
109
|
+
[OPTIONAL] Given a temporary table `view_name`, validate the type of
|
|
110
|
+
a column expression string `expression`, or raise an error if the
|
|
111
|
+
expression is invalid. This is enabeld by `"expressions"` via
|
|
112
|
+
`get_features` and defaults to allow all expressions.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
def view_delete(self, view_name):
|
|
118
|
+
"""
|
|
119
|
+
Delete a temporary table. The UI will do this automatically, and it
|
|
120
|
+
can recover.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def view_get_data(self, view_name, config, viewport, data):
|
|
126
|
+
"""
|
|
127
|
+
Serialize a rectangular slice `viewport` from temporary table
|
|
128
|
+
`view_name`, into the `PerspectiveColumn` serialization API injected
|
|
129
|
+
via `data`. The push-only `PerspectiveColumn` type can handle casting
|
|
130
|
+
Python types as input, but once a type is pushed to a column name it
|
|
131
|
+
must not be changed.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
pass
|