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.
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vgplot
3
- Version: 0.29.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgplot-python",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "prerelease": "uv run ruff check && uv run ruff format --check",
@@ -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 _collect_datadefs(node: Any) -> list[DataDef]:
45
- """Recursively collect all DataDef instances in a view tree."""
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 _collect_datadefs(node._view)
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(_collect_datadefs(v))
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(_collect_datadefs(item))
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→name mapping
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 _collect_datadefs(self.view):
132
- if id(obj) not in data_names:
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
- auto_name = f"_data{counter}"
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