vgplot 0.29.0__tar.gz → 0.29.1__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.
- {vgplot-0.29.0 → vgplot-0.29.1}/PKG-INFO +2 -1
- {vgplot-0.29.0 → vgplot-0.29.1}/package.json +1 -1
- {vgplot-0.29.0 → vgplot-0.29.1}/pyproject.toml +1 -1
- {vgplot-0.29.0 → vgplot-0.29.1}/test/test_python_api.py +29 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/data.py +15 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/plot.py +6 -2
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/spec.py +32 -14
- {vgplot-0.29.0 → vgplot-0.29.1}/.gitignore +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/CONTRIBUTING.md +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/README.md +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/test/__init__.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/test/test_full_round_trip.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/test/test_schema_coverage.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/__init__.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/_generated/__init__.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/_generated/attributes.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/_generated/encodings.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/_generated/marks.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/_types.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/encodings.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/params.py +0 -0
- {vgplot-0.29.0 → vgplot-0.29.1}/vgplot/util.py +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vgplot
|
|
3
|
-
Version: 0.29.
|
|
3
|
+
Version: 0.29.1
|
|
4
4
|
Summary: A Python API for authoring Mosaic vgplot visualizations
|
|
5
5
|
Project-URL: homepage, https://github.com/uwdata/mosaic
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: narwhals>=2.8.0
|
|
7
8
|
Description-Content-Type: text/markdown
|
|
8
9
|
|
|
9
10
|
# vgplot
|
|
@@ -4,7 +4,7 @@ dynamic = ["version"]
|
|
|
4
4
|
description = "A Python API for authoring Mosaic vgplot visualizations"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
|
-
dependencies = []
|
|
7
|
+
dependencies = ["narwhals>=2.8.0"]
|
|
8
8
|
|
|
9
9
|
[project.urls]
|
|
10
10
|
homepage = "https://github.com/uwdata/mosaic"
|
|
@@ -79,3 +79,32 @@ class TestAutoNaming:
|
|
|
79
79
|
assert d["params"]["_param0"] == 1
|
|
80
80
|
assert d["params"]["_param1"] == 2
|
|
81
81
|
assert d["plot"][0]["x"] == "$_param1"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestDataFrames:
|
|
85
|
+
"""A DataFrame passed directly to a mark is discovered by variable name,
|
|
86
|
+
referenced as a table, and carried in the spec's data section for the host
|
|
87
|
+
(e.g. the widget) to register."""
|
|
88
|
+
|
|
89
|
+
def test_frame_is_referenced_and_named_after_variable(self):
|
|
90
|
+
import pyarrow as pa
|
|
91
|
+
|
|
92
|
+
weather = pa.table({"a": [1, 2, 3]})
|
|
93
|
+
view = vg.plot(vg.dot(weather, x="a"))
|
|
94
|
+
d = view.to_dict()
|
|
95
|
+
|
|
96
|
+
assert d["plot"][0]["data"] == {"from": "weather"}
|
|
97
|
+
assert d["data"]["weather"] is weather
|
|
98
|
+
|
|
99
|
+
def test_frame_and_datadef_share_the_data_section(self):
|
|
100
|
+
import pyarrow as pa
|
|
101
|
+
|
|
102
|
+
weather = pa.table({"a": [1]})
|
|
103
|
+
athletes = vg.csv("athletes.csv")
|
|
104
|
+
view = vg.plot(vg.dot(weather, x="a"), vg.dot(athletes, x="a"))
|
|
105
|
+
d = view.to_dict()
|
|
106
|
+
|
|
107
|
+
assert d["plot"][0]["data"] == {"from": "weather"}
|
|
108
|
+
assert d["plot"][1]["data"] == {"from": "athletes"}
|
|
109
|
+
assert d["data"]["weather"] is weather
|
|
110
|
+
assert d["data"]["athletes"] == {"type": "csv", "file": "athletes.csv"}
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
import narwhals as nw
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
@dataclass
|
|
8
10
|
class DataDef:
|
|
@@ -12,6 +14,19 @@ class DataDef:
|
|
|
12
14
|
return self.payload
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def is_frame(obj: Any) -> bool:
|
|
18
|
+
"""True if `obj` is a dataframe-like object (pandas/polars/Arrow/...)."""
|
|
19
|
+
if obj is None or isinstance(
|
|
20
|
+
obj, (str, bytes, int, float, bool, dict, list, tuple)
|
|
21
|
+
):
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
nw.from_native(obj)
|
|
25
|
+
except TypeError:
|
|
26
|
+
return False
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
15
30
|
def parquet(file: str, select: Any = None, where: Any = None, **kwargs: Any) -> DataDef:
|
|
16
31
|
payload: dict[str, Any] = {"type": "parquet", "file": file}
|
|
17
32
|
if select is not None:
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from .util import camelize, omit_none
|
|
8
8
|
from .params import _ParamBase
|
|
9
|
-
from .data import DataDef
|
|
9
|
+
from .data import DataDef, is_frame
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class FromRef:
|
|
@@ -47,7 +47,7 @@ class Mark:
|
|
|
47
47
|
data_dict: Any = {"from": self.data}
|
|
48
48
|
elif isinstance(self.data, FromRef):
|
|
49
49
|
data_dict = dict(self.data.to_dict())
|
|
50
|
-
elif isinstance(self.data, DataDef):
|
|
50
|
+
elif isinstance(self.data, DataDef) or is_frame(self.data):
|
|
51
51
|
data_dict = {"from": self.data}
|
|
52
52
|
else:
|
|
53
53
|
data_dict = self.data
|
|
@@ -84,6 +84,10 @@ def encode_value(
|
|
|
84
84
|
return [encode_value(x, param_names, data_names) for x in v]
|
|
85
85
|
if isinstance(v, dict):
|
|
86
86
|
return {k: encode_value(val, param_names, data_names) for k, val in v.items()}
|
|
87
|
+
if is_frame(v):
|
|
88
|
+
if data_names and id(v) in data_names:
|
|
89
|
+
return data_names[id(v)]
|
|
90
|
+
return v
|
|
87
91
|
return v
|
|
88
92
|
|
|
89
93
|
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from .util import omit_none
|
|
8
8
|
from .params import _ParamBase
|
|
9
|
-
from .data import DataDef
|
|
9
|
+
from .data import DataDef, is_frame
|
|
10
10
|
from .plot import _encode_component
|
|
11
11
|
|
|
12
12
|
|
|
@@ -41,25 +41,38 @@ def _collect_params(node: Any) -> list[_ParamBase]:
|
|
|
41
41
|
return []
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""Recursively collect all
|
|
46
|
-
if isinstance(node, DataDef):
|
|
44
|
+
def _collect_data_sources(node: Any) -> list[Any]:
|
|
45
|
+
"""Recursively collect all data sources (DataDefs and DataFrames) in a view tree."""
|
|
46
|
+
if isinstance(node, DataDef) or is_frame(node):
|
|
47
47
|
return [node]
|
|
48
48
|
if isinstance(node, View):
|
|
49
|
-
return
|
|
49
|
+
return _collect_data_sources(node._view)
|
|
50
50
|
if isinstance(node, dict):
|
|
51
51
|
found = []
|
|
52
52
|
for v in node.values():
|
|
53
|
-
found.extend(
|
|
53
|
+
found.extend(_collect_data_sources(v))
|
|
54
54
|
return found
|
|
55
55
|
if isinstance(node, list):
|
|
56
56
|
found = []
|
|
57
57
|
for item in node:
|
|
58
|
-
found.extend(
|
|
58
|
+
found.extend(_collect_data_sources(item))
|
|
59
59
|
return found
|
|
60
60
|
return []
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _frame_data_names(caller_locals: dict[str, Any]) -> dict[int, str]:
|
|
64
|
+
"""Map id->name for DataFrames bound to non-underscore caller variables.
|
|
65
|
+
|
|
66
|
+
A frame bound to several variables resolves to just one name (the last),
|
|
67
|
+
which becomes its registered table name.
|
|
68
|
+
"""
|
|
69
|
+
return {
|
|
70
|
+
id(obj): name
|
|
71
|
+
for name, obj in caller_locals.items()
|
|
72
|
+
if is_frame(obj) and not name.startswith("_")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
63
76
|
class Spec:
|
|
64
77
|
def __init__(
|
|
65
78
|
self,
|
|
@@ -73,7 +86,7 @@ class Spec:
|
|
|
73
86
|
view: dict[str, Any] | None = None,
|
|
74
87
|
**extra: Any,
|
|
75
88
|
) -> None:
|
|
76
|
-
# Serialize any DataDef values in data and build id
|
|
89
|
+
# Serialize any DataDef values in data and build id->name mapping
|
|
77
90
|
resolved_data: dict[str, Any] = {}
|
|
78
91
|
resolved_data_names: dict[int, str] = dict(data_names or {})
|
|
79
92
|
for name, val in (data or {}).items():
|
|
@@ -123,19 +136,22 @@ class Spec:
|
|
|
123
136
|
else:
|
|
124
137
|
serialized_params[name] = p
|
|
125
138
|
|
|
126
|
-
# Build data_names lookup and auto-register any unnamed DataDefs in the view
|
|
127
139
|
data_names = dict(self.data_names)
|
|
128
140
|
extra_data: dict[str, Any] = {}
|
|
129
141
|
used = set(data_names.values()) | set(self.data or {})
|
|
130
142
|
counter = 0
|
|
131
|
-
for obj in
|
|
132
|
-
|
|
143
|
+
for obj in _collect_data_sources(self.view):
|
|
144
|
+
name = data_names.get(id(obj))
|
|
145
|
+
if name is None:
|
|
133
146
|
while f"_data{counter}" in used:
|
|
134
147
|
counter += 1
|
|
135
|
-
|
|
136
|
-
data_names[id(obj)] = auto_name
|
|
137
|
-
extra_data[auto_name] = obj.to_dict()
|
|
148
|
+
name = f"_data{counter}"
|
|
138
149
|
counter += 1
|
|
150
|
+
data_names[id(obj)] = name
|
|
151
|
+
used.add(name)
|
|
152
|
+
if name in (self.data or {}) or name in extra_data:
|
|
153
|
+
continue
|
|
154
|
+
extra_data[name] = obj.to_dict() if isinstance(obj, DataDef) else obj
|
|
139
155
|
merged_data = {**(self.data or {}), **extra_data} or None
|
|
140
156
|
|
|
141
157
|
base: dict[str, Any] = {}
|
|
@@ -214,6 +230,7 @@ class View:
|
|
|
214
230
|
}
|
|
215
231
|
return Spec(
|
|
216
232
|
data=merged_data,
|
|
233
|
+
data_names=_frame_data_names(caller_locals) or None,
|
|
217
234
|
params=frame_params or None,
|
|
218
235
|
plotDefaults=self._plotDefaults,
|
|
219
236
|
config=self._config,
|
|
@@ -295,6 +312,7 @@ def spec(
|
|
|
295
312
|
|
|
296
313
|
return Spec(
|
|
297
314
|
data=merged_data,
|
|
315
|
+
data_names=_frame_data_names(caller_locals) or None,
|
|
298
316
|
params=params,
|
|
299
317
|
plotDefaults=plotDefaults,
|
|
300
318
|
plot_defaults=plot_defaults,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|