vgplot 0.1.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.
Files changed (38) hide show
  1. {vgplot-0.1.0 → vgplot-0.29.1}/.gitignore +2 -7
  2. vgplot-0.29.1/CONTRIBUTING.md +46 -0
  3. vgplot-0.29.1/PKG-INFO +137 -0
  4. vgplot-0.29.1/README.md +128 -0
  5. vgplot-0.29.1/package.json +9 -0
  6. vgplot-0.29.1/pyproject.toml +37 -0
  7. vgplot-0.29.1/test/test_full_round_trip.py +74 -0
  8. vgplot-0.29.1/test/test_python_api.py +110 -0
  9. vgplot-0.29.1/test/test_schema_coverage.py +61 -0
  10. vgplot-0.29.1/vgplot/__init__.py +102 -0
  11. vgplot-0.29.1/vgplot/_generated/__init__.py +10 -0
  12. vgplot-0.29.1/vgplot/_generated/attributes.py +1293 -0
  13. vgplot-0.29.1/vgplot/_generated/encodings.py +252 -0
  14. vgplot-0.29.1/vgplot/_generated/marks.py +4124 -0
  15. vgplot-0.29.1/vgplot/_types.py +29 -0
  16. vgplot-0.29.1/vgplot/data.py +70 -0
  17. vgplot-0.29.1/vgplot/encodings.py +11 -0
  18. vgplot-0.29.1/vgplot/params.py +90 -0
  19. vgplot-0.29.1/vgplot/plot.py +550 -0
  20. vgplot-0.29.1/vgplot/spec.py +322 -0
  21. {vgplot-0.1.0/api → vgplot-0.29.1/vgplot}/util.py +2 -2
  22. vgplot-0.1.0/PKG-INFO +0 -27
  23. vgplot-0.1.0/README.md +0 -17
  24. vgplot-0.1.0/__init__.py +0 -1
  25. vgplot-0.1.0/api/__init__.py +0 -119
  26. vgplot-0.1.0/api/data.py +0 -27
  27. vgplot-0.1.0/api/plot.py +0 -266
  28. vgplot-0.1.0/api/spec.py +0 -90
  29. vgplot-0.1.0/pyproject.toml +0 -43
  30. vgplot-0.1.0/schema_wrapper/SchemaBase.py +0 -81
  31. vgplot-0.1.0/schema_wrapper/generate_schema_wrapper.py +0 -245
  32. vgplot-0.1.0/schema_wrapper/generated_classes.py +0 -5872
  33. vgplot-0.1.0/schema_wrapper/utils.py +0 -345
  34. vgplot-0.1.0/test/__init__.py +0 -0
  35. vgplot-0.1.0/test/mini_test.py +0 -661
  36. vgplot-0.1.0/test/test_full_round_trip.py +0 -65
  37. vgplot-0.1.0/test/test_generated_classes.py +0 -146
  38. {vgplot-0.1.0/schema_wrapper → vgplot-0.29.1/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
- # Test cache
34
- .pytest_cache/
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.1/PKG-INFO ADDED
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: vgplot
3
+ Version: 0.29.1
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
+ Requires-Dist: narwhals>=2.8.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # vgplot
11
+
12
+ [![PyPI](https://img.shields.io/pypi/v/vgplot.svg)](https://pypi.org/project/vgplot/)
13
+
14
+ 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.
15
+
16
+ `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.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install vgplot
22
+ ```
23
+
24
+ To render visualizations in the widget, install the widget:
25
+
26
+ ```bash
27
+ pip install mosaic-widget
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### File-based data
33
+
34
+ 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`:
35
+
36
+ ```python
37
+ import vgplot as vg
38
+
39
+ athletes = vg.parquet("data/athletes.parquet")
40
+
41
+ view = vg.plot(
42
+ vg.dot(athletes, x="weight", y="height", fill="sex", opacity=0.5),
43
+ vg.width(600),
44
+ vg.height(400),
45
+ )
46
+
47
+ view
48
+ ```
49
+
50
+ ### Interactive selections
51
+
52
+ Params and selections are first-class objects. Declare them before the view so they can be referenced by multiple marks or interactors:
53
+
54
+ ```python
55
+ import vgplot as vg
56
+
57
+ flights = vg.parquet("data/flights-200k.parquet")
58
+ brush = vg.selection.crossfilter()
59
+
60
+ view = vg.vconcat(
61
+ vg.plot(
62
+ vg.rect_y(flights, x=vg.bin("delay"), 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("Arrival Delay (min)"),
68
+ vg.height(200),
69
+ ),
70
+ vg.plot(
71
+ vg.rect_y(flights, x=vg.bin("time"), y=vg.count(),
72
+ filter_by=brush, fill="steelblue",
73
+ inset_left=0.5, inset_right=0.5),
74
+ vg.interval_x(bind=brush),
75
+ vg.x_domain("Fixed"),
76
+ vg.x_label("Departure Time (hour)"),
77
+ vg.height(200),
78
+ ),
79
+ )
80
+
81
+ view
82
+ ```
83
+
84
+ ### Scalar params and input widgets
85
+
86
+ Use `vg.param()` for scalar parameters bound to input controls:
87
+
88
+ ```python
89
+ import vgplot as vg
90
+
91
+ walk = vg.parquet("data/random-walk.parquet")
92
+ bias = vg.param(0)
93
+
94
+ view = vg.vconcat(
95
+ vg.slider(label="Bias", bind=bias, min=0, max=1000, step=1),
96
+ vg.plot(
97
+ vg.area_y(walk, x="t", y=vg.sql("v + $bias")),
98
+ vg.width(680),
99
+ vg.height(200),
100
+ ),
101
+ )
102
+
103
+ view
104
+ ```
105
+
106
+ ## Key concepts
107
+
108
+ | Concept | Python API |
109
+ |---|---|
110
+ | File-based data | `vg.parquet(path)`, `vg.csv(path)`, `vg.spatial(path)`, `vg.table(query)` |
111
+ | Mark | `vg.dot(...)`, `vg.bar_y(...)`, `vg.area_y(...)`, `vg.line_y(...)`, … |
112
+ | Aggregation / transform | `vg.count()`, `vg.bin("col")`, `vg.avg("col")`, `vg.sum("col")`, … |
113
+ | SQL expression | `vg.sql("expr")` — use `$param` to interpolate a param, e.g. `vg.sql("v + $bias")` |
114
+ | Plot attributes | `vg.width(n)`, `vg.height(n)`, `vg.x_label("…")`, `vg.color_scheme("…")`, … |
115
+ | Interactor | `vg.interval_x(...)`, `vg.interval_xy(...)`, `vg.region(...)`, `vg.highlight(...)`, … |
116
+ | Layout | `vg.vconcat(...)`, `vg.hconcat(...)`, `vg.vspace(n)`, `vg.hspace(n)` |
117
+ | Crossfilter selection | `vg.selection.crossfilter()` |
118
+ | Intersect / union selection | `vg.selection.intersect()`, `vg.selection.union()` |
119
+ | Scalar param | `vg.param(value)` |
120
+ | Input widgets | `vg.slider(...)`, `vg.menu(...)`, `vg.select(...)`, `vg.checkbox(...)` |
121
+ | Named data reference | `vg.source("table_name")` |
122
+ | Render | `view.show()` or `view` as last cell expression in notebooks |
123
+
124
+ 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`.
125
+
126
+ 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.
127
+
128
+ ## Exporting specs
129
+
130
+ `view.to_dict()` returns a plain Python dictionary. `view.to_json()` returns a JSON string:
131
+
132
+ ```python
133
+ from pprint import pprint
134
+
135
+ pprint(view.to_dict())
136
+ print(view.to_json(indent=2))
137
+ ```
@@ -0,0 +1,128 @@
1
+ # vgplot
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/vgplot.svg)](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,9 @@
1
+ {
2
+ "name": "vgplot-python",
3
+ "version": "0.29.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "prerelease": "uv run ruff check && uv run ruff format --check",
7
+ "release": "uv build && uvx twine upload --skip-existing ../../../dist/vgplot*"
8
+ }
9
+ }
@@ -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 = ["narwhals>=2.8.0"]
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,110 @@
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"
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"}
@@ -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}"