vgplot 0.1.0__tar.gz → 0.29.0__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.1.0 → vgplot-0.29.0}/.gitignore +2 -7
- vgplot-0.29.0/CONTRIBUTING.md +46 -0
- vgplot-0.29.0/PKG-INFO +136 -0
- vgplot-0.29.0/README.md +128 -0
- vgplot-0.29.0/package.json +9 -0
- vgplot-0.29.0/pyproject.toml +37 -0
- vgplot-0.29.0/test/test_full_round_trip.py +74 -0
- vgplot-0.29.0/test/test_python_api.py +81 -0
- vgplot-0.29.0/test/test_schema_coverage.py +61 -0
- vgplot-0.29.0/vgplot/__init__.py +102 -0
- vgplot-0.29.0/vgplot/_generated/__init__.py +10 -0
- vgplot-0.29.0/vgplot/_generated/attributes.py +1293 -0
- vgplot-0.29.0/vgplot/_generated/encodings.py +252 -0
- vgplot-0.29.0/vgplot/_generated/marks.py +4124 -0
- vgplot-0.29.0/vgplot/_types.py +29 -0
- vgplot-0.29.0/vgplot/data.py +55 -0
- vgplot-0.29.0/vgplot/encodings.py +11 -0
- vgplot-0.29.0/vgplot/params.py +90 -0
- vgplot-0.29.0/vgplot/plot.py +546 -0
- vgplot-0.29.0/vgplot/spec.py +304 -0
- {vgplot-0.1.0/api → vgplot-0.29.0/vgplot}/util.py +2 -2
- vgplot-0.1.0/PKG-INFO +0 -27
- vgplot-0.1.0/README.md +0 -17
- vgplot-0.1.0/__init__.py +0 -1
- vgplot-0.1.0/api/__init__.py +0 -119
- vgplot-0.1.0/api/data.py +0 -27
- vgplot-0.1.0/api/plot.py +0 -266
- vgplot-0.1.0/api/spec.py +0 -90
- vgplot-0.1.0/pyproject.toml +0 -43
- vgplot-0.1.0/schema_wrapper/SchemaBase.py +0 -81
- vgplot-0.1.0/schema_wrapper/generate_schema_wrapper.py +0 -245
- vgplot-0.1.0/schema_wrapper/generated_classes.py +0 -5872
- vgplot-0.1.0/schema_wrapper/utils.py +0 -345
- vgplot-0.1.0/test/__init__.py +0 -0
- vgplot-0.1.0/test/mini_test.py +0 -661
- vgplot-0.1.0/test/test_full_round_trip.py +0 -65
- vgplot-0.1.0/test/test_generated_classes.py +0 -146
- {vgplot-0.1.0/schema_wrapper → vgplot-0.29.0/test}/__init__.py +0 -0
|
@@ -27,12 +27,7 @@ venv/
|
|
|
27
27
|
conda-env/
|
|
28
28
|
|
|
29
29
|
# Python cache
|
|
30
|
-
__pycache__/
|
|
31
30
|
*.pyc
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.coverage
|
|
36
|
-
|
|
37
|
-
# Python API Generated classes
|
|
38
|
-
packages/vgplot-python/schema_wrapper/generated_classes.py
|
|
32
|
+
.pnpm-store
|
|
33
|
+
dev/notebooks/__marimo__
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Contributing to the vgplot Python API
|
|
2
|
+
|
|
3
|
+
Most of the API — every **mark**, **plot attribute**, and **encoding
|
|
4
|
+
transform** (aggregates, window functions, column transforms) — is generated
|
|
5
|
+
from the Mosaic JSON schema, which is itself generated from the JavaScript
|
|
6
|
+
spec ([`Spec.ts`]). The JS API is the source of truth.
|
|
7
|
+
|
|
8
|
+
Hand-written (not generated): the runtime core (`plot.py`, `spec.py`), data
|
|
9
|
+
sources (`data.py`), params (`params.py`), the shared value types
|
|
10
|
+
(`_types.py`), the `sql()` / `channels()` helpers (`encodings.py`), and the
|
|
11
|
+
interactor / input / legend helpers.
|
|
12
|
+
|
|
13
|
+
[`Spec.ts`]: ../../../packages/vgplot/spec/src/spec/Spec.ts
|
|
14
|
+
|
|
15
|
+
## Regenerating the API
|
|
16
|
+
|
|
17
|
+
After any change to the vgplot spec types (or the generator), regenerate:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm run generate:python-api
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This rebuilds the schema, runs [`bin/generate-python-api.js`], and formats the
|
|
24
|
+
output into [`vgplot/_generated/`]. Commit the regenerated files.
|
|
25
|
+
|
|
26
|
+
CI fails if the committed [`vgplot/_generated/`] output does not match a fresh
|
|
27
|
+
regeneration, so the generated code always matches the schema.
|
|
28
|
+
|
|
29
|
+
[`bin/generate-python-api.js`]: ../../../bin/generate-python-api.js
|
|
30
|
+
[`vgplot/_generated/`]: ./vgplot/_generated/__init__.py
|
|
31
|
+
|
|
32
|
+
## Adding a new mark, attribute, or transform
|
|
33
|
+
|
|
34
|
+
Add it on the JS side ([`Spec.ts`]), then run `pnpm run generate:python-api`.
|
|
35
|
+
Nothing in the Python package needs hand-editing — there is no dynamic
|
|
36
|
+
fallback, so a name that is missing from the generated API raises
|
|
37
|
+
`AttributeError` (and is flagged by type checkers) rather than silently
|
|
38
|
+
appearing.
|
|
39
|
+
|
|
40
|
+
## Tests
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
cd packages/vgplot/vgplot-python
|
|
44
|
+
uv run --group dev pytest # round-trip + API + schema-coverage tests
|
|
45
|
+
uv run --group dev ty check
|
|
46
|
+
```
|
vgplot-0.29.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](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
|
+
```
|
vgplot-0.29.0/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# vgplot
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/vgplot/)
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
`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.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install vgplot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To render visualizations in the widget, install the widget:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install mosaic-widget
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### File-based data
|
|
24
|
+
|
|
25
|
+
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`:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import vgplot as vg
|
|
29
|
+
|
|
30
|
+
athletes = vg.parquet("data/athletes.parquet")
|
|
31
|
+
|
|
32
|
+
view = vg.plot(
|
|
33
|
+
vg.dot(athletes, x="weight", y="height", fill="sex", opacity=0.5),
|
|
34
|
+
vg.width(600),
|
|
35
|
+
vg.height(400),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
view
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Interactive selections
|
|
42
|
+
|
|
43
|
+
Params and selections are first-class objects. Declare them before the view so they can be referenced by multiple marks or interactors:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import vgplot as vg
|
|
47
|
+
|
|
48
|
+
flights = vg.parquet("data/flights-200k.parquet")
|
|
49
|
+
brush = vg.selection.crossfilter()
|
|
50
|
+
|
|
51
|
+
view = vg.vconcat(
|
|
52
|
+
vg.plot(
|
|
53
|
+
vg.rect_y(flights, x=vg.bin("delay"), y=vg.count(),
|
|
54
|
+
filter_by=brush, fill="steelblue",
|
|
55
|
+
inset_left=0.5, inset_right=0.5),
|
|
56
|
+
vg.interval_x(bind=brush),
|
|
57
|
+
vg.x_domain("Fixed"),
|
|
58
|
+
vg.x_label("Arrival Delay (min)"),
|
|
59
|
+
vg.height(200),
|
|
60
|
+
),
|
|
61
|
+
vg.plot(
|
|
62
|
+
vg.rect_y(flights, x=vg.bin("time"), y=vg.count(),
|
|
63
|
+
filter_by=brush, fill="steelblue",
|
|
64
|
+
inset_left=0.5, inset_right=0.5),
|
|
65
|
+
vg.interval_x(bind=brush),
|
|
66
|
+
vg.x_domain("Fixed"),
|
|
67
|
+
vg.x_label("Departure Time (hour)"),
|
|
68
|
+
vg.height(200),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
view
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Scalar params and input widgets
|
|
76
|
+
|
|
77
|
+
Use `vg.param()` for scalar parameters bound to input controls:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import vgplot as vg
|
|
81
|
+
|
|
82
|
+
walk = vg.parquet("data/random-walk.parquet")
|
|
83
|
+
bias = vg.param(0)
|
|
84
|
+
|
|
85
|
+
view = vg.vconcat(
|
|
86
|
+
vg.slider(label="Bias", bind=bias, min=0, max=1000, step=1),
|
|
87
|
+
vg.plot(
|
|
88
|
+
vg.area_y(walk, x="t", y=vg.sql("v + $bias")),
|
|
89
|
+
vg.width(680),
|
|
90
|
+
vg.height(200),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
view
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Key concepts
|
|
98
|
+
|
|
99
|
+
| Concept | Python API |
|
|
100
|
+
|---|---|
|
|
101
|
+
| File-based data | `vg.parquet(path)`, `vg.csv(path)`, `vg.spatial(path)`, `vg.table(query)` |
|
|
102
|
+
| Mark | `vg.dot(...)`, `vg.bar_y(...)`, `vg.area_y(...)`, `vg.line_y(...)`, … |
|
|
103
|
+
| Aggregation / transform | `vg.count()`, `vg.bin("col")`, `vg.avg("col")`, `vg.sum("col")`, … |
|
|
104
|
+
| SQL expression | `vg.sql("expr")` — use `$param` to interpolate a param, e.g. `vg.sql("v + $bias")` |
|
|
105
|
+
| Plot attributes | `vg.width(n)`, `vg.height(n)`, `vg.x_label("…")`, `vg.color_scheme("…")`, … |
|
|
106
|
+
| Interactor | `vg.interval_x(...)`, `vg.interval_xy(...)`, `vg.region(...)`, `vg.highlight(...)`, … |
|
|
107
|
+
| Layout | `vg.vconcat(...)`, `vg.hconcat(...)`, `vg.vspace(n)`, `vg.hspace(n)` |
|
|
108
|
+
| Crossfilter selection | `vg.selection.crossfilter()` |
|
|
109
|
+
| Intersect / union selection | `vg.selection.intersect()`, `vg.selection.union()` |
|
|
110
|
+
| Scalar param | `vg.param(value)` |
|
|
111
|
+
| Input widgets | `vg.slider(...)`, `vg.menu(...)`, `vg.select(...)`, `vg.checkbox(...)` |
|
|
112
|
+
| Named data reference | `vg.source("table_name")` |
|
|
113
|
+
| Render | `view.show()` or `view` as last cell expression in notebooks |
|
|
114
|
+
|
|
115
|
+
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`.
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
## Exporting specs
|
|
120
|
+
|
|
121
|
+
`view.to_dict()` returns a plain Python dictionary. `view.to_json()` returns a JSON string:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from pprint import pprint
|
|
125
|
+
|
|
126
|
+
pprint(view.to_dict())
|
|
127
|
+
print(view.to_json(indent=2))
|
|
128
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vgplot"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "A Python API for authoring Mosaic vgplot visualizations"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[project.urls]
|
|
10
|
+
homepage = "https://github.com/uwdata/mosaic"
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"mosaic-widget",
|
|
19
|
+
"pytest",
|
|
20
|
+
"pytest-cov",
|
|
21
|
+
"ruff",
|
|
22
|
+
"ty",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
"mosaic-widget" = { workspace = true }
|
|
27
|
+
|
|
28
|
+
[tool.hatch.version]
|
|
29
|
+
path = "package.json"
|
|
30
|
+
pattern = "\"version\": \"(?P<version>.+?)\""
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["vgplot"]
|
|
34
|
+
|
|
35
|
+
[tool.pyright]
|
|
36
|
+
ignore = ["../**/.venv/"] # Do not display diagnostics in external source code
|
|
37
|
+
typeCheckingMode = "standard"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Tests that every generated Python spec (specs/python/*.py) round-trips
|
|
2
|
+
# correctly: running the file produces JSON identical to the reference fixture
|
|
3
|
+
# in specs/json/. Also checks that Python, JSON, and ESM example sets stay
|
|
4
|
+
# in sync (same filenames).
|
|
5
|
+
#
|
|
6
|
+
# Run: pytest packages/vgplot/vgplot-python/test/test_full_round_trip.py
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[4] # mosaic project root
|
|
15
|
+
SPEC_DIR = ROOT / "specs"
|
|
16
|
+
JSON_DIR = SPEC_DIR / "json"
|
|
17
|
+
ESM_DIR = SPEC_DIR / "esm"
|
|
18
|
+
PYTHON_DIR = SPEC_DIR / "python"
|
|
19
|
+
|
|
20
|
+
VGPLOT_PKG = ROOT / "packages" / "vgplot" / "vgplot-python"
|
|
21
|
+
|
|
22
|
+
for p in (VGPLOT_PKG, ROOT):
|
|
23
|
+
if str(p) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(p))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_json_fixture(name: str) -> dict:
|
|
28
|
+
return json.loads((JSON_DIR / f"{name}.json").read_text(encoding="utf-8"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_spec_file_with_exec(path: Path) -> dict:
|
|
32
|
+
namespace: dict[str, Any] = {
|
|
33
|
+
"__name__": "__spec_exec__",
|
|
34
|
+
"__file__": str(path),
|
|
35
|
+
"__builtins__": __builtins__,
|
|
36
|
+
}
|
|
37
|
+
code = compile(path.read_text(encoding="utf-8"), str(path), "exec")
|
|
38
|
+
exec(code, namespace)
|
|
39
|
+
|
|
40
|
+
view = namespace.get("view")
|
|
41
|
+
if view is None:
|
|
42
|
+
raise AssertionError(f"`{path.name}` must define a top-level `view` variable")
|
|
43
|
+
if not hasattr(view, "to_dict"):
|
|
44
|
+
raise AssertionError(f"`view` in `{path.name}` does not expose to_dict()")
|
|
45
|
+
# Execute to_dict() within the spec namespace so frame inspection finds the right variables
|
|
46
|
+
namespace["__view__"] = view
|
|
47
|
+
exec("__result__ = __view__.to_dict()", namespace)
|
|
48
|
+
return namespace["__result__"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
JSON_EXAMPLES = sorted(path.stem for path in JSON_DIR.glob("*.json"))
|
|
52
|
+
ESM_EXAMPLES = sorted(path.stem for path in ESM_DIR.glob("*.js"))
|
|
53
|
+
PYTHON_EXAMPLES = sorted(path.stem for path in PYTHON_DIR.glob("*.py"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_generated_examples_stay_in_sync():
|
|
57
|
+
assert PYTHON_EXAMPLES == JSON_EXAMPLES
|
|
58
|
+
assert ESM_EXAMPLES == JSON_EXAMPLES
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize("example_name", PYTHON_EXAMPLES)
|
|
62
|
+
def test_python_round_trip(example_name: str):
|
|
63
|
+
generated = run_spec_file_with_exec(PYTHON_DIR / f"{example_name}.py")
|
|
64
|
+
original = load_json_fixture(example_name)
|
|
65
|
+
|
|
66
|
+
# meta is documentation-only and not emitted by the Python DSL
|
|
67
|
+
original.pop("meta", None)
|
|
68
|
+
generated.pop("meta", None)
|
|
69
|
+
|
|
70
|
+
assert generated == original, (
|
|
71
|
+
f"Round-trip Python mismatch for '{example_name}'.\n"
|
|
72
|
+
f"> original: {original}\n"
|
|
73
|
+
f"> generated: {generated}"
|
|
74
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Unit tests for the vgplot Python API, covering behaviors that the
|
|
2
|
+
# generated-spec round-trip suite does not exercise directly.
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[4]
|
|
9
|
+
VGPLOT_PKG = ROOT / "packages" / "vgplot" / "vgplot-python"
|
|
10
|
+
for p in (VGPLOT_PKG, ROOT):
|
|
11
|
+
if str(p) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(p))
|
|
13
|
+
|
|
14
|
+
import vgplot as vg # noqa: E402
|
|
15
|
+
from vgplot.plot import Mark # noqa: E402
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestGeneratedMarks:
|
|
19
|
+
"""Marks are generated from the schema; there is no dynamic fallback, so
|
|
20
|
+
unknown names fail loudly rather than being silently fabricated."""
|
|
21
|
+
|
|
22
|
+
def test_positional_data_with_encodings_is_kept(self):
|
|
23
|
+
# A leading positional arg is the data source even when the call also
|
|
24
|
+
# has keyword encodings.
|
|
25
|
+
mark = vg.waffle_y("athletes", x="a", y={"count": ""})
|
|
26
|
+
assert isinstance(mark, Mark)
|
|
27
|
+
d = mark.to_dict()
|
|
28
|
+
assert d["mark"] == "waffleY"
|
|
29
|
+
assert d["data"] == {"from": "athletes"}
|
|
30
|
+
assert d["x"] == "a"
|
|
31
|
+
|
|
32
|
+
def test_explicit_none_channel_is_preserved(self):
|
|
33
|
+
# Passing a channel explicitly as None keeps it in the output (distinct
|
|
34
|
+
# from not passing it at all).
|
|
35
|
+
assert vg.line_y("t", x="a", z=None).to_dict()["z"] is None
|
|
36
|
+
assert "z" not in vg.line_y("t", x="a").to_dict()
|
|
37
|
+
|
|
38
|
+
def test_unknown_name_raises(self):
|
|
39
|
+
# No __getattr__ fallback: a missing or mis-typed API name is an error.
|
|
40
|
+
with pytest.raises(AttributeError):
|
|
41
|
+
vg.some_custom_mark # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
|
42
|
+
with pytest.raises(AttributeError):
|
|
43
|
+
vg.definitely_not_a_real_directive # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestDataHelpers:
|
|
47
|
+
def test_json_inline_data(self):
|
|
48
|
+
d = vg.json([{"a": 1}, {"a": 2}])
|
|
49
|
+
assert d.to_dict() == {"type": "json", "data": [{"a": 1}, {"a": 2}]}
|
|
50
|
+
|
|
51
|
+
def test_json_file(self):
|
|
52
|
+
assert vg.json(file="x.json").to_dict() == {"type": "json", "file": "x.json"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestParams:
|
|
56
|
+
def test_date_param(self):
|
|
57
|
+
from datetime import date
|
|
58
|
+
|
|
59
|
+
assert vg.param(date(2013, 5, 13)).param_def() == {"date": "2013-05-13"}
|
|
60
|
+
|
|
61
|
+
def test_datetime_param(self):
|
|
62
|
+
from datetime import datetime
|
|
63
|
+
|
|
64
|
+
assert vg.param(datetime(2013, 5, 13, 10, 30)).param_def() == {
|
|
65
|
+
"date": "2013-05-13T10:30:00"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestAutoNaming:
|
|
70
|
+
def test_auto_param_name_skips_explicit_name(self):
|
|
71
|
+
from vgplot.spec import Spec
|
|
72
|
+
|
|
73
|
+
named = vg.param(1)
|
|
74
|
+
unnamed = vg.param(2)
|
|
75
|
+
view = vg.plot(vg.dot("t", x=unnamed))
|
|
76
|
+
# The explicit param already occupies "_param0"; the in-view param must
|
|
77
|
+
# not collide with it.
|
|
78
|
+
d = Spec(params={"_param0": named}, view=view).to_dict()
|
|
79
|
+
assert d["params"]["_param0"] == 1
|
|
80
|
+
assert d["params"]["_param1"] == 2
|
|
81
|
+
assert d["plot"][0]["x"] == "$_param1"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Ensures the exported vgplot API covers every mark, attribute, interactor, and
|
|
2
|
+
# input in the Mosaic JSON schema (the source of truth). Marks and attributes
|
|
3
|
+
# are generated; interactors and inputs are hand-written. This guards against a
|
|
4
|
+
# schema addition that was never generated or hand-added.
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[4]
|
|
11
|
+
for p in (ROOT / "packages" / "vgplot" / "vgplot-python", ROOT):
|
|
12
|
+
if str(p) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(p))
|
|
14
|
+
|
|
15
|
+
import vgplot as vg # noqa: E402
|
|
16
|
+
|
|
17
|
+
SCHEMA = ROOT / "docs" / "public" / "schema" / "latest.json"
|
|
18
|
+
|
|
19
|
+
# Schema names whose Python export is renamed or intentionally absent.
|
|
20
|
+
RENAME = {"table": "table_input"} # `table` input -> vg.table_input (vg.table is data)
|
|
21
|
+
IGNORE = {"value"} # internal selection const, not a user-facing interactor
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _snake(name: str) -> str:
|
|
25
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _const(node: dict, key: str):
|
|
29
|
+
prop = node.get("properties", {}).get(key)
|
|
30
|
+
return prop["const"] if isinstance(prop, dict) and "const" in prop else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _consts(defs: dict, key: str) -> set[str]:
|
|
34
|
+
"""Const values for `key`, including intersection defs (anyOf/allOf) whose
|
|
35
|
+
branches all agree on a single const (e.g. the densityX mark)."""
|
|
36
|
+
out = set()
|
|
37
|
+
for d in defs.values():
|
|
38
|
+
c = _const(d, key)
|
|
39
|
+
if c is not None:
|
|
40
|
+
out.add(c)
|
|
41
|
+
continue
|
|
42
|
+
branch_consts = {
|
|
43
|
+
_const(b, key) for b in (d.get("anyOf") or d.get("allOf") or [])
|
|
44
|
+
} - {None}
|
|
45
|
+
if len(branch_consts) == 1:
|
|
46
|
+
out |= branch_consts
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_schema_surface_is_exported():
|
|
51
|
+
defs = json.loads(SCHEMA.read_text("utf-8"))["definitions"]
|
|
52
|
+
names = (
|
|
53
|
+
_consts(defs, "mark")
|
|
54
|
+
| _consts(defs, "select")
|
|
55
|
+
| _consts(defs, "input")
|
|
56
|
+
| set(defs["PlotAttributes"]["properties"])
|
|
57
|
+
) - IGNORE
|
|
58
|
+
|
|
59
|
+
exported = set(vg.__all__)
|
|
60
|
+
missing = sorted(n for n in names if RENAME.get(n, _snake(n)) not in exported)
|
|
61
|
+
assert not missing, f"schema names with no exported vgplot function: {missing}"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from .spec import spec, Spec, View
|
|
2
|
+
from .data import parquet, csv, spatial, table, json, data
|
|
3
|
+
from .params import param, selection
|
|
4
|
+
from .encodings import sql, channels
|
|
5
|
+
from .plot import (
|
|
6
|
+
plot,
|
|
7
|
+
source,
|
|
8
|
+
mark,
|
|
9
|
+
directive,
|
|
10
|
+
margins,
|
|
11
|
+
vconcat,
|
|
12
|
+
hconcat,
|
|
13
|
+
vspace,
|
|
14
|
+
hspace,
|
|
15
|
+
slider,
|
|
16
|
+
select,
|
|
17
|
+
checkbox,
|
|
18
|
+
menu,
|
|
19
|
+
search,
|
|
20
|
+
table_input,
|
|
21
|
+
input,
|
|
22
|
+
option,
|
|
23
|
+
brush,
|
|
24
|
+
sort,
|
|
25
|
+
interval_x,
|
|
26
|
+
interval_y,
|
|
27
|
+
interval_xy,
|
|
28
|
+
toggle,
|
|
29
|
+
toggle_x,
|
|
30
|
+
toggle_y,
|
|
31
|
+
toggle_color,
|
|
32
|
+
nearest_x,
|
|
33
|
+
nearest_y,
|
|
34
|
+
region,
|
|
35
|
+
highlight,
|
|
36
|
+
pan,
|
|
37
|
+
pan_x,
|
|
38
|
+
pan_y,
|
|
39
|
+
pan_zoom,
|
|
40
|
+
pan_zoom_x,
|
|
41
|
+
pan_zoom_y,
|
|
42
|
+
color_legend,
|
|
43
|
+
opacity_legend,
|
|
44
|
+
symbol_legend,
|
|
45
|
+
)
|
|
46
|
+
from ._generated import * # noqa: F401, F403
|
|
47
|
+
from ._generated import __all__ as _generated_all
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Spec",
|
|
51
|
+
"View",
|
|
52
|
+
"brush",
|
|
53
|
+
"channels",
|
|
54
|
+
"checkbox",
|
|
55
|
+
"color_legend",
|
|
56
|
+
"csv",
|
|
57
|
+
"data",
|
|
58
|
+
"directive",
|
|
59
|
+
"hconcat",
|
|
60
|
+
"highlight",
|
|
61
|
+
"hspace",
|
|
62
|
+
"input",
|
|
63
|
+
"interval_x",
|
|
64
|
+
"interval_xy",
|
|
65
|
+
"interval_y",
|
|
66
|
+
"json",
|
|
67
|
+
"margins",
|
|
68
|
+
"mark",
|
|
69
|
+
"menu",
|
|
70
|
+
"nearest_x",
|
|
71
|
+
"nearest_y",
|
|
72
|
+
"opacity_legend",
|
|
73
|
+
"option",
|
|
74
|
+
"pan",
|
|
75
|
+
"pan_x",
|
|
76
|
+
"pan_y",
|
|
77
|
+
"pan_zoom",
|
|
78
|
+
"pan_zoom_x",
|
|
79
|
+
"pan_zoom_y",
|
|
80
|
+
"param",
|
|
81
|
+
"parquet",
|
|
82
|
+
"plot",
|
|
83
|
+
"region",
|
|
84
|
+
"search",
|
|
85
|
+
"select",
|
|
86
|
+
"selection",
|
|
87
|
+
"slider",
|
|
88
|
+
"sort",
|
|
89
|
+
"source",
|
|
90
|
+
"spatial",
|
|
91
|
+
"spec",
|
|
92
|
+
"sql",
|
|
93
|
+
"symbol_legend",
|
|
94
|
+
"table",
|
|
95
|
+
"table_input",
|
|
96
|
+
"toggle",
|
|
97
|
+
"toggle_color",
|
|
98
|
+
"toggle_x",
|
|
99
|
+
"toggle_y",
|
|
100
|
+
"vconcat",
|
|
101
|
+
"vspace",
|
|
102
|
+
] + list(_generated_all) # pyright: ignore[reportUnsupportedDunderAll]
|