vgplot 0.29.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.
vgplot/spec.py ADDED
@@ -0,0 +1,304 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import json
5
+ from typing import Any
6
+
7
+ from .util import omit_none
8
+ from .params import _ParamBase
9
+ from .data import DataDef
10
+ from .plot import _encode_component
11
+
12
+
13
+ def _caller_locals() -> dict[str, Any]:
14
+ """Local variables of the caller of the method invoking this helper.
15
+
16
+ Returns an empty mapping when the frame is unavailable (e.g. on Python
17
+ implementations without ``sys._getframe`` support).
18
+ """
19
+ frame = inspect.currentframe()
20
+ method = frame.f_back if frame is not None else None
21
+ caller = method.f_back if method is not None else None
22
+ return caller.f_locals if caller is not None else {}
23
+
24
+
25
+ def _collect_params(node: Any) -> list[_ParamBase]:
26
+ """Recursively collect all _ParamBase instances in a view tree."""
27
+ if isinstance(node, _ParamBase):
28
+ return [node]
29
+ if isinstance(node, View):
30
+ return _collect_params(node._view)
31
+ if isinstance(node, dict):
32
+ found = []
33
+ for v in node.values():
34
+ found.extend(_collect_params(v))
35
+ return found
36
+ if isinstance(node, list):
37
+ found = []
38
+ for item in node:
39
+ found.extend(_collect_params(item))
40
+ return found
41
+ return []
42
+
43
+
44
+ def _collect_datadefs(node: Any) -> list[DataDef]:
45
+ """Recursively collect all DataDef instances in a view tree."""
46
+ if isinstance(node, DataDef):
47
+ return [node]
48
+ if isinstance(node, View):
49
+ return _collect_datadefs(node._view)
50
+ if isinstance(node, dict):
51
+ found = []
52
+ for v in node.values():
53
+ found.extend(_collect_datadefs(v))
54
+ return found
55
+ if isinstance(node, list):
56
+ found = []
57
+ for item in node:
58
+ found.extend(_collect_datadefs(item))
59
+ return found
60
+ return []
61
+
62
+
63
+ class Spec:
64
+ def __init__(
65
+ self,
66
+ *,
67
+ data: dict[str, Any] | None = None,
68
+ data_names: dict[int, str] | None = None,
69
+ params: dict[str, Any] | None = None,
70
+ plotDefaults: dict[str, Any] | None = None,
71
+ plot_defaults: dict[str, Any] | None = None,
72
+ config: dict[str, Any] | None = None,
73
+ view: dict[str, Any] | None = None,
74
+ **extra: Any,
75
+ ) -> None:
76
+ # Serialize any DataDef values in data and build id→name mapping
77
+ resolved_data: dict[str, Any] = {}
78
+ resolved_data_names: dict[int, str] = dict(data_names or {})
79
+ for name, val in (data or {}).items():
80
+ if isinstance(val, DataDef):
81
+ resolved_data_names[id(val)] = name
82
+ resolved_data[name] = val.to_dict()
83
+ else:
84
+ resolved_data[name] = val
85
+ self.data = resolved_data or None
86
+ self.data_names = resolved_data_names
87
+ self.params = params
88
+ self.plotDefaults = plotDefaults or plot_defaults
89
+ self.config = config
90
+ self.view = view or {}
91
+ self.extra = extra
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ # Build a reverse lookup: id(param_object) -> param_name, so that
95
+ # _ParamBase instances appearing in the view resolve to "$name" refs.
96
+ param_names: dict[int, str] = {}
97
+ serialized_params: dict[str, Any] = {}
98
+ # First pass: register all named params
99
+ for name, p in (self.params or {}).items():
100
+ if isinstance(p, _ParamBase):
101
+ param_names[id(p)] = name
102
+
103
+ # Second pass: auto-name any _ParamBase in the view not yet registered,
104
+ # skipping names already taken by explicit params.
105
+ used = set(param_names.values()) | set(self.params or {})
106
+ counter = 0
107
+ for obj in _collect_params(self.view):
108
+ if id(obj) not in param_names:
109
+ while f"_param{counter}" in used:
110
+ counter += 1
111
+ param_names[id(obj)] = f"_param{counter}"
112
+ counter += 1
113
+
114
+ # Third pass: serialize all known params
115
+ view_params = {id(obj): obj for obj in _collect_params(self.view)}
116
+ all_params = dict(self.params or {})
117
+ for obj_id, name in param_names.items():
118
+ if name not in all_params and obj_id in view_params:
119
+ all_params[name] = view_params[obj_id]
120
+ for name, p in all_params.items():
121
+ if isinstance(p, _ParamBase):
122
+ serialized_params[name] = p.param_def(param_names=param_names)
123
+ else:
124
+ serialized_params[name] = p
125
+
126
+ # Build data_names lookup and auto-register any unnamed DataDefs in the view
127
+ data_names = dict(self.data_names)
128
+ extra_data: dict[str, Any] = {}
129
+ used = set(data_names.values()) | set(self.data or {})
130
+ counter = 0
131
+ for obj in _collect_datadefs(self.view):
132
+ if id(obj) not in data_names:
133
+ while f"_data{counter}" in used:
134
+ counter += 1
135
+ auto_name = f"_data{counter}"
136
+ data_names[id(obj)] = auto_name
137
+ extra_data[auto_name] = obj.to_dict()
138
+ counter += 1
139
+ merged_data = {**(self.data or {}), **extra_data} or None
140
+
141
+ base: dict[str, Any] = {}
142
+ if self.config:
143
+ base["config"] = self.config
144
+ if merged_data:
145
+ base["data"] = merged_data
146
+ if serialized_params:
147
+ base["params"] = serialized_params
148
+ if self.plotDefaults:
149
+ base["plotDefaults"] = self.plotDefaults
150
+ base.update(_encode_component(self.view, param_names, data_names))
151
+ base.update(omit_none(self.extra))
152
+ return base
153
+
154
+ def to_json(self, **kwargs: Any) -> str:
155
+ return json.dumps(self.to_dict(), **kwargs)
156
+
157
+ def _repr_mimebundle_(self, **kwargs):
158
+ try:
159
+ from mosaic_widget import MosaicWidget
160
+ except ImportError:
161
+ return {"text/plain": repr(self)}
162
+ widget = MosaicWidget(self.to_dict())
163
+ return widget._repr_mimebundle_(**kwargs)
164
+
165
+ def show(self, con=None, data=None):
166
+ try:
167
+ from mosaic_widget import MosaicWidget
168
+ from IPython.display import display
169
+ except ImportError as e:
170
+ raise ImportError("pip install mosaic-widget") from e
171
+ widget = MosaicWidget(self.to_dict(), con=con, data=data)
172
+ display(widget)
173
+
174
+
175
+ class View:
176
+ """
177
+ A composable view returned by plot(), vconcat(), hconcat() etc.
178
+ Behaves like Spec but discovers data and params from the caller's
179
+ frame at display/export time rather than at construction time.
180
+ """
181
+
182
+ def __init__(
183
+ self,
184
+ view: dict[str, Any],
185
+ *,
186
+ data: dict[str, Any] | None = None,
187
+ plotDefaults: dict[str, Any] | None = None,
188
+ plot_defaults: dict[str, Any] | None = None,
189
+ config: dict[str, Any] | None = None,
190
+ **extra: Any,
191
+ ) -> None:
192
+ self._view = view
193
+ self._data = data # explicit overrides for renamed data variables
194
+ self._plotDefaults = plotDefaults or plot_defaults
195
+ self._config = config
196
+ self._extra = extra
197
+
198
+ def _build_spec(self, caller_locals: dict[str, Any]) -> Spec:
199
+ explicit_ids = {
200
+ id(v) for v in (self._data or {}).values() if isinstance(v, DataDef)
201
+ }
202
+ frame_data: dict[str, Any] = {
203
+ name: obj
204
+ for name, obj in caller_locals.items()
205
+ if isinstance(obj, DataDef)
206
+ and not name.startswith("_")
207
+ and id(obj) not in explicit_ids
208
+ }
209
+ merged_data = {**(self._data or {}), **frame_data} or None
210
+ frame_params: dict[str, Any] = {
211
+ name: obj
212
+ for name, obj in caller_locals.items()
213
+ if isinstance(obj, _ParamBase) and not name.startswith("_")
214
+ }
215
+ return Spec(
216
+ data=merged_data,
217
+ params=frame_params or None,
218
+ plotDefaults=self._plotDefaults,
219
+ config=self._config,
220
+ view=self._view,
221
+ **self._extra,
222
+ )
223
+
224
+ def to_dict(self, _context: dict[str, Any] | None = None) -> dict[str, Any]:
225
+ locals_ = _context if _context is not None else _caller_locals()
226
+ return self._build_spec(locals_).to_dict()
227
+
228
+ def to_json(self, _context: dict[str, Any] | None = None, **kwargs: Any) -> str:
229
+ locals_ = _context if _context is not None else _caller_locals()
230
+ return self._build_spec(locals_).to_json(**kwargs)
231
+
232
+ def show(self, con: Any = None, data: Any = None) -> None:
233
+ self._build_spec(_caller_locals()).show(con=con, data=data)
234
+
235
+ def _repr_mimebundle_(self, **kwargs: Any):
236
+ # Walk up frames to find the one where this View object lives
237
+ frame = inspect.currentframe()
238
+ frame = frame.f_back if frame is not None else None
239
+ while frame is not None:
240
+ if any(v is self for v in frame.f_locals.values()):
241
+ break
242
+ frame = frame.f_back
243
+ locals_ = frame.f_locals if frame is not None else {}
244
+ return self._build_spec(locals_)._repr_mimebundle_(**kwargs)
245
+
246
+
247
+ _VIEW_KEYS = {"plot", "vconcat", "hconcat", "hspace", "vspace", "input"}
248
+
249
+
250
+ def spec(
251
+ *args: Any,
252
+ data: dict[str, Any] | None = None,
253
+ params: dict[str, Any] | None = None,
254
+ plotDefaults: dict[str, Any] | None = None,
255
+ plot_defaults: dict[str, Any] | None = None,
256
+ config: dict[str, Any] | None = None,
257
+ view: dict[str, Any] | None = None,
258
+ **extra: Any,
259
+ ) -> Spec:
260
+ """Assemble a Spec from a view plus data sources and params.
261
+
262
+ A positional dict holding a view key (plot/vconcat/hconcat/...) is used as
263
+ the view; any other positional dict is used as data. DataDef and param
264
+ objects bound to local variables in the caller are discovered automatically
265
+ by name unless passed explicitly via data= or params=.
266
+ """
267
+ for arg in args:
268
+ if isinstance(arg, dict) and _VIEW_KEYS.intersection(arg):
269
+ view = arg
270
+ elif isinstance(arg, dict):
271
+ data = arg
272
+
273
+ # Inspect caller frame to discover DataDef and _ParamBase objects by variable name.
274
+ caller_locals = _caller_locals()
275
+
276
+ # Collect DataDef objects not already covered by the explicit data= argument.
277
+ explicit_ids = {id(v) for v in (data or {}).values() if isinstance(v, DataDef)}
278
+ frame_data: dict[str, Any] = {
279
+ name: obj
280
+ for name, obj in caller_locals.items()
281
+ if isinstance(obj, DataDef)
282
+ and not name.startswith("_")
283
+ and id(obj) not in explicit_ids
284
+ }
285
+ merged_data = {**(data or {}), **frame_data} or None
286
+
287
+ # Collect _ParamBase objects when params were not passed explicitly.
288
+ if params is None:
289
+ frame_params: dict[str, Any] = {
290
+ name: obj
291
+ for name, obj in caller_locals.items()
292
+ if isinstance(obj, _ParamBase) and not name.startswith("_")
293
+ }
294
+ params = frame_params or None
295
+
296
+ return Spec(
297
+ data=merged_data,
298
+ params=params,
299
+ plotDefaults=plotDefaults,
300
+ plot_defaults=plot_defaults,
301
+ config=config,
302
+ view=view,
303
+ **extra,
304
+ )
vgplot/util.py ADDED
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+
4
+ def camelize(key: str) -> str:
5
+ """Convert snake_case to lowerCamelCase."""
6
+ if not key:
7
+ return key
8
+ parts = key.split("_")
9
+ return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:])
10
+
11
+
12
+ def omit_none(d: dict[str, Any]) -> dict[str, Any]:
13
+ """Return a copy without None values."""
14
+ return {k: v for k, v in d.items() if v is not None}
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: vgplot
3
+ Version: 0.29.0
4
+ Summary: A Python API for authoring Mosaic vgplot visualizations
5
+ Project-URL: homepage, https://github.com/uwdata/mosaic
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # vgplot
10
+
11
+ [![PyPI](https://img.shields.io/pypi/v/vgplot.svg)](https://pypi.org/project/vgplot/)
12
+
13
+ A Python API for authoring [Mosaic](https://uwdata.github.io/mosaic/) visualizations. Build declarative, interactive plots backed by DuckDB — in notebooks, as JSON specs, or as part of a data pipeline.
14
+
15
+ `vgplot` produces Mosaic `View` objects that can be rendered with [`mosaic-widget`](https://pypi.org/project/mosaic-widget/) or exported to JSON for use in a browser. Data sources and parameters defined as local variables are automatically discovered at render time.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install vgplot
21
+ ```
22
+
23
+ To render visualizations in the widget, install the widget:
24
+
25
+ ```bash
26
+ pip install mosaic-widget
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### File-based data
32
+
33
+ Load data from a file using `vg.parquet()`, `vg.csv()`, or `vg.table()`, pass it directly to a mark, then call `view.show()` or `view`:
34
+
35
+ ```python
36
+ import vgplot as vg
37
+
38
+ athletes = vg.parquet("data/athletes.parquet")
39
+
40
+ view = vg.plot(
41
+ vg.dot(athletes, x="weight", y="height", fill="sex", opacity=0.5),
42
+ vg.width(600),
43
+ vg.height(400),
44
+ )
45
+
46
+ view
47
+ ```
48
+
49
+ ### Interactive selections
50
+
51
+ Params and selections are first-class objects. Declare them before the view so they can be referenced by multiple marks or interactors:
52
+
53
+ ```python
54
+ import vgplot as vg
55
+
56
+ flights = vg.parquet("data/flights-200k.parquet")
57
+ brush = vg.selection.crossfilter()
58
+
59
+ view = vg.vconcat(
60
+ vg.plot(
61
+ vg.rect_y(flights, x=vg.bin("delay"), y=vg.count(),
62
+ filter_by=brush, fill="steelblue",
63
+ inset_left=0.5, inset_right=0.5),
64
+ vg.interval_x(bind=brush),
65
+ vg.x_domain("Fixed"),
66
+ vg.x_label("Arrival Delay (min)"),
67
+ vg.height(200),
68
+ ),
69
+ vg.plot(
70
+ vg.rect_y(flights, x=vg.bin("time"), y=vg.count(),
71
+ filter_by=brush, fill="steelblue",
72
+ inset_left=0.5, inset_right=0.5),
73
+ vg.interval_x(bind=brush),
74
+ vg.x_domain("Fixed"),
75
+ vg.x_label("Departure Time (hour)"),
76
+ vg.height(200),
77
+ ),
78
+ )
79
+
80
+ view
81
+ ```
82
+
83
+ ### Scalar params and input widgets
84
+
85
+ Use `vg.param()` for scalar parameters bound to input controls:
86
+
87
+ ```python
88
+ import vgplot as vg
89
+
90
+ walk = vg.parquet("data/random-walk.parquet")
91
+ bias = vg.param(0)
92
+
93
+ view = vg.vconcat(
94
+ vg.slider(label="Bias", bind=bias, min=0, max=1000, step=1),
95
+ vg.plot(
96
+ vg.area_y(walk, x="t", y=vg.sql("v + $bias")),
97
+ vg.width(680),
98
+ vg.height(200),
99
+ ),
100
+ )
101
+
102
+ view
103
+ ```
104
+
105
+ ## Key concepts
106
+
107
+ | Concept | Python API |
108
+ |---|---|
109
+ | File-based data | `vg.parquet(path)`, `vg.csv(path)`, `vg.spatial(path)`, `vg.table(query)` |
110
+ | Mark | `vg.dot(...)`, `vg.bar_y(...)`, `vg.area_y(...)`, `vg.line_y(...)`, … |
111
+ | Aggregation / transform | `vg.count()`, `vg.bin("col")`, `vg.avg("col")`, `vg.sum("col")`, … |
112
+ | SQL expression | `vg.sql("expr")` — use `$param` to interpolate a param, e.g. `vg.sql("v + $bias")` |
113
+ | Plot attributes | `vg.width(n)`, `vg.height(n)`, `vg.x_label("…")`, `vg.color_scheme("…")`, … |
114
+ | Interactor | `vg.interval_x(...)`, `vg.interval_xy(...)`, `vg.region(...)`, `vg.highlight(...)`, … |
115
+ | Layout | `vg.vconcat(...)`, `vg.hconcat(...)`, `vg.vspace(n)`, `vg.hspace(n)` |
116
+ | Crossfilter selection | `vg.selection.crossfilter()` |
117
+ | Intersect / union selection | `vg.selection.intersect()`, `vg.selection.union()` |
118
+ | Scalar param | `vg.param(value)` |
119
+ | Input widgets | `vg.slider(...)`, `vg.menu(...)`, `vg.select(...)`, `vg.checkbox(...)` |
120
+ | Named data reference | `vg.source("table_name")` |
121
+ | Render | `view.show()` or `view` as last cell expression in notebooks |
122
+
123
+ Option names match the [vgplot API reference](https://uwdata.github.io/mosaic/api/), but in snake_case. For example, `xDomain` → `x_domain`, `colorScheme` → `color_scheme`, `filterBy` → `filter_by`.
124
+
125
+ Any mark or directive not listed above is also accessible by its snake_case name — `vg.regression_y(...)` and `vg.x_tick_rotate(45)` work without explicit exports.
126
+
127
+ ## Exporting specs
128
+
129
+ `view.to_dict()` returns a plain Python dictionary. `view.to_json()` returns a JSON string:
130
+
131
+ ```python
132
+ from pprint import pprint
133
+
134
+ pprint(view.to_dict())
135
+ print(view.to_json(indent=2))
136
+ ```
@@ -0,0 +1,15 @@
1
+ vgplot/__init__.py,sha256=uALjBohruCjzzzXLOENDPbfYgCC5u7j2KLnTXaUcAHw,1678
2
+ vgplot/_types.py,sha256=cs5FgHnBKmcx9HaH4AWOaBZkMR3Vlg2CR4QXcv_qmWE,870
3
+ vgplot/data.py,sha256=VgG62a806C0bYo--sdpqK8r4pLZdeEKeUwhIJ3k_bTs,1493
4
+ vgplot/encodings.py,sha256=sb_TcBzClBL3c9IID8OSGVExCYYE7pB35bRaJt680ns,197
5
+ vgplot/params.py,sha256=d2copQfOHEu29cyS49_dgQSIe6ouU3_TSiafXUbFqg4,2724
6
+ vgplot/plot.py,sha256=2ScCoEDgYog3MZWMKXvNtCE1Cnd41xVyCI8guGCbehw,13583
7
+ vgplot/spec.py,sha256=MzI60-5rwu-UDCd1cn8oqj1Vl7FkCLwdEvPSWwF5MWs,11074
8
+ vgplot/util.py,sha256=3HQBJaXyCmM680xHvpmE4OaGSf1Nm7_PpbIUGs1WP4k,394
9
+ vgplot/_generated/__init__.py,sha256=S_iBHL2x70pcqk1OY-pw3xoXvSEcaxjpjpmN4n-zCxI,421
10
+ vgplot/_generated/attributes.py,sha256=Ghgp-geFLefbwD9awAs8ja2bHd0MiKTirrJyT5bSJMo,46270
11
+ vgplot/_generated/encodings.py,sha256=OcktD1tX2zqOb7X3XSZ6GNCpsNQW5B3pjF_vwzja_I4,8081
12
+ vgplot/_generated/marks.py,sha256=z_iM_BZ7FCtjCgWuv9NRnW8g1tFg56NKQpQ4wV8Q48Q,137136
13
+ vgplot-0.29.0.dist-info/METADATA,sha256=ZZqIFGtZaRQ2Ki-Acc8ZZ-4uE4XOeTOimdz4DoqYJYw,4414
14
+ vgplot-0.29.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
15
+ vgplot-0.29.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any