vgplot 0.30.0__tar.gz → 0.32.0b0__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 (25) hide show
  1. {vgplot-0.30.0 → vgplot-0.32.0b0}/PKG-INFO +2 -2
  2. {vgplot-0.30.0 → vgplot-0.32.0b0}/package.json +1 -1
  3. {vgplot-0.30.0 → vgplot-0.32.0b0}/tests/test_full_round_trip.py +13 -9
  4. {vgplot-0.30.0 → vgplot-0.32.0b0}/tests/test_python_api.py +20 -19
  5. vgplot-0.32.0b0/tests/test_schema_coverage.py +65 -0
  6. {vgplot-0.30.0 → vgplot-0.32.0b0}/tests/test_unset_sentinel.py +2 -2
  7. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/__init__.py +35 -29
  8. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/_compat.py +2 -4
  9. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/_generated/__init__.py +5 -3
  10. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/_generated/attributes.py +12 -6
  11. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/_generated/encodings.py +4 -2
  12. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/_generated/marks.py +2 -0
  13. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/_types.py +6 -2
  14. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/params.py +6 -6
  15. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/plot.py +32 -18
  16. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/spec.py +24 -13
  17. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/util.py +2 -0
  18. vgplot-0.30.0/tests/test_schema_coverage.py +0 -56
  19. {vgplot-0.30.0 → vgplot-0.32.0b0}/.gitignore +0 -0
  20. {vgplot-0.30.0 → vgplot-0.32.0b0}/CONTRIBUTING.md +0 -0
  21. {vgplot-0.30.0 → vgplot-0.32.0b0}/README.md +0 -0
  22. {vgplot-0.30.0 → vgplot-0.32.0b0}/pyproject.toml +0 -0
  23. {vgplot-0.30.0 → vgplot-0.32.0b0}/tests/__init__.py +0 -0
  24. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/data.py +0 -0
  25. {vgplot-0.30.0 → vgplot-0.32.0b0}/vgplot/encodings.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: vgplot
3
- Version: 0.30.0
3
+ Version: 0.32.0-beta.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgplot-python",
3
- "version": "0.30.0",
3
+ "version": "0.32.0-beta.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "prerelease": "uv run ruff check && uv run ruff format --check",
@@ -4,6 +4,8 @@
4
4
  # in sync (same filenames).
5
5
  #
6
6
  # Run: pytest packages/vgplot/vgplot-python/tests/test_full_round_trip.py
7
+ from __future__ import annotations
8
+
7
9
  import json
8
10
  from pathlib import Path
9
11
  from typing import Any
@@ -16,27 +18,29 @@ ESM_DIR = SPEC_DIR / "esm"
16
18
  PYTHON_DIR = SPEC_DIR / "python"
17
19
 
18
20
 
19
- def load_json_fixture(name: str) -> dict:
21
+ def load_json_fixture(name: str) -> dict[str, Any]:
20
22
  return json.loads((JSON_DIR / f"{name}.json").read_text(encoding="utf-8"))
21
23
 
22
24
 
23
- def run_spec_file_with_exec(path: Path) -> dict:
24
- namespace: dict[str, Any] = {
25
+ def run_spec_file_with_exec(path: Path) -> dict[str, Any]:
26
+ namespace = {
25
27
  "__name__": "__spec_exec__",
26
28
  "__file__": str(path),
27
29
  "__builtins__": __builtins__,
28
30
  }
29
31
  code = compile(path.read_text(encoding="utf-8"), str(path), "exec")
30
- exec(code, namespace)
32
+ exec(code, namespace) # ruff: ignore[exec-builtin]
31
33
 
32
34
  view = namespace.get("view")
33
35
  if view is None:
34
- raise AssertionError(f"`{path.name}` must define a top-level `view` variable")
36
+ msg = f"`{path.name}` must define a top-level `view` variable"
37
+ raise AssertionError(msg)
35
38
  if not hasattr(view, "to_dict"):
36
- raise AssertionError(f"`view` in `{path.name}` does not expose to_dict()")
39
+ msg = f"`view` in `{path.name}` does not expose to_dict()"
40
+ raise AssertionError(msg)
37
41
  # Execute to_dict() within the spec namespace so frame inspection finds the right variables
38
42
  namespace["__view__"] = view
39
- exec("__result__ = __view__.to_dict()", namespace)
43
+ exec("__result__ = __view__.to_dict()", namespace) # ruff: ignore[exec-builtin]
40
44
  return namespace["__result__"]
41
45
 
42
46
 
@@ -45,13 +49,13 @@ ESM_EXAMPLES = sorted(path.stem for path in ESM_DIR.glob("*.js"))
45
49
  PYTHON_EXAMPLES = sorted(path.stem for path in PYTHON_DIR.glob("*.py"))
46
50
 
47
51
 
48
- def test_generated_examples_stay_in_sync():
52
+ def test_generated_examples_stay_in_sync() -> None:
49
53
  assert PYTHON_EXAMPLES == JSON_EXAMPLES
50
54
  assert ESM_EXAMPLES == JSON_EXAMPLES
51
55
 
52
56
 
53
57
  @pytest.mark.parametrize("example_name", PYTHON_EXAMPLES)
54
- def test_python_round_trip(example_name: str):
58
+ def test_python_round_trip(example_name: str) -> None:
55
59
  generated = run_spec_file_with_exec(PYTHON_DIR / f"{example_name}.py")
56
60
  original = load_json_fixture(example_name)
57
61
 
@@ -1,9 +1,10 @@
1
1
  # Unit tests for the vgplot Python API, covering behaviors that the
2
2
  # generated-spec round-trip suite does not exercise directly.
3
+ from __future__ import annotations
4
+
3
5
  from typing import TYPE_CHECKING
4
6
 
5
7
  import pytest
6
-
7
8
  import vgplot as vg
8
9
  from vgplot.plot import Mark
9
10
 
@@ -12,7 +13,7 @@ class TestGeneratedMarks:
12
13
  """Marks are generated from the schema; there is no dynamic fallback, so
13
14
  unknown names fail loudly rather than being silently fabricated."""
14
15
 
15
- def test_positional_data_with_encodings_is_kept(self):
16
+ def test_positional_data_with_encodings_is_kept(self) -> None:
16
17
  # A leading positional arg is the data source even when the call also
17
18
  # has keyword encodings.
18
19
  mark = vg.waffle_y("athletes", x="a", y={"count": ""})
@@ -22,45 +23,45 @@ class TestGeneratedMarks:
22
23
  assert d["data"] == {"from": "athletes"}
23
24
  assert d["x"] == "a"
24
25
 
25
- def test_explicit_none_channel_is_preserved(self):
26
+ def test_explicit_none_channel_is_preserved(self) -> None:
26
27
  # Passing a channel explicitly as None keeps it in the output (distinct
27
28
  # from not passing it at all).
28
29
  assert vg.line_y("t", x="a", z=None).to_dict()["z"] is None
29
30
  assert "z" not in vg.line_y("t", x="a").to_dict()
30
31
 
31
- def test_unknown_name_raises(self):
32
+ def test_unknown_name_raises(self) -> None:
32
33
  # No __getattr__ fallback: a missing or mis-typed API name is an error.
33
34
  with pytest.raises(AttributeError):
34
- vg.some_custom_mark # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
35
+ _ = vg.some_custom_mark # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
35
36
  with pytest.raises(AttributeError):
36
- vg.definitely_not_a_real_directive # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
37
+ _ = vg.definitely_not_a_real_directive # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
37
38
 
38
39
 
39
40
  class TestDataHelpers:
40
- def test_json_inline_data(self):
41
+ def test_json_inline_data(self) -> None:
41
42
  d = vg.json([{"a": 1}, {"a": 2}])
42
43
  assert d.to_dict() == {"type": "json", "data": [{"a": 1}, {"a": 2}]}
43
44
 
44
- def test_json_file(self):
45
+ def test_json_file(self) -> None:
45
46
  assert vg.json(file="x.json").to_dict() == {"type": "json", "file": "x.json"}
46
47
 
47
48
 
48
49
  class TestParams:
49
- def test_date_param(self):
50
+ def test_date_param(self) -> None:
50
51
  from datetime import date
51
52
 
52
53
  assert vg.param(date(2013, 5, 13)).param_def() == {"date": "2013-05-13"}
53
54
 
54
- def test_datetime_param(self):
55
+ def test_datetime_param(self) -> None:
55
56
  from datetime import datetime
56
57
 
57
- assert vg.param(datetime(2013, 5, 13, 10, 30)).param_def() == {
58
+ assert vg.param(datetime(2013, 5, 13, 10, 30)).param_def() == { # ruff: ignore[call-datetime-without-tzinfo]
58
59
  "date": "2013-05-13T10:30:00"
59
60
  }
60
61
 
61
62
 
62
63
  class TestAutoNaming:
63
- def test_auto_param_name_skips_explicit_name(self):
64
+ def test_auto_param_name_skips_explicit_name(self) -> None:
64
65
  from vgplot.spec import Spec
65
66
 
66
67
  named = vg.param(1)
@@ -79,7 +80,7 @@ class TestDataFrames:
79
80
  referenced as a table, and carried in the spec's data section for the host
80
81
  (e.g. the widget) to register."""
81
82
 
82
- def test_frame_is_referenced_and_named_after_variable(self):
83
+ def test_frame_is_referenced_and_named_after_variable(self) -> None:
83
84
  pytest.importorskip("pyarrow")
84
85
  import pyarrow as pa
85
86
 
@@ -90,7 +91,7 @@ class TestDataFrames:
90
91
  assert d["plot"][0]["data"] == {"from": "weather"}
91
92
  assert d["data"]["weather"] is weather
92
93
 
93
- def test_frame_and_datadef_share_the_data_section(self):
94
+ def test_frame_and_datadef_share_the_data_section(self) -> None:
94
95
  pytest.importorskip("pyarrow")
95
96
  import pyarrow as pa
96
97
 
@@ -109,11 +110,11 @@ if TYPE_CHECKING:
109
110
 
110
111
  def typing_dunder_all() -> None:
111
112
  # Ok
112
- vg.arrow
113
- vg.errorbar_x
114
- vg.mark
113
+ _ = vg.arrow
114
+ _ = vg.errorbar_x
115
+ _ = vg.mark
115
116
 
116
117
  # TODO @dangotbanned: Get pyright to error on these
117
118
  # Error (`_generated` modules)
118
- vg.attributes # ty: ignore[unresolved-attribute]
119
- vg.marks # ty: ignore[unresolved-attribute]
119
+ _ = vg.attributes # ty: ignore[unresolved-attribute]
120
+ _ = vg.marks # ty: ignore[unresolved-attribute]
@@ -0,0 +1,65 @@
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
+ from __future__ import annotations
6
+
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import vgplot as vg
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+
17
+ from typing_extensions import TypedDict
18
+
19
+ class Schema(TypedDict, closed=True, total=False):
20
+ properties: dict[str, dict[str, Any]]
21
+ anyOf: Sequence[Schema]
22
+
23
+ class Definitions(TypedDict, extra_items=Schema):
24
+ PlotAttributes: Schema
25
+
26
+
27
+ ROOT = Path(__file__).resolve().parents[4]
28
+ SCHEMA = ROOT / "docs" / "public" / "schema" / "latest.json"
29
+
30
+ # Schema names whose Python export is renamed or intentionally absent.
31
+ RENAME = {"table": "table_input"} # `table` input -> vg.table_input (vg.table is data)
32
+ IGNORE = {"value"} # internal selection const, not a user-facing interactor
33
+
34
+
35
+ def _snake(name: str) -> str:
36
+ return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
37
+
38
+
39
+ def get_discriminator_values(defs: Definitions, key: str) -> set[str]:
40
+ out: set[str] = set()
41
+ for d in defs.values():
42
+ if (p := d.get("properties")) and (c := p.get(key, {}).get("const")):
43
+ out.add(c)
44
+ elif u := d.get("anyOf"):
45
+ out.update(
46
+ c
47
+ for member in u
48
+ if (p := member.get("properties"))
49
+ and (c := p.get(key, {}).get("const"))
50
+ )
51
+ return out
52
+
53
+
54
+ def test_schema_surface_is_exported() -> None:
55
+ defs: Definitions = json.loads(SCHEMA.read_text("utf-8"))["definitions"]
56
+ names = (
57
+ get_discriminator_values(defs, "mark")
58
+ | get_discriminator_values(defs, "select")
59
+ | get_discriminator_values(defs, "input")
60
+ | set(defs["PlotAttributes"].get("properties", ()))
61
+ ) - IGNORE
62
+
63
+ exported = set(vg.__all__)
64
+ missing = sorted(n for n in names if RENAME.get(n, _snake(n)) not in exported)
65
+ assert not missing, f"schema names with no exported vgplot function: {missing}"
@@ -32,7 +32,7 @@ def test_unset_pickle() -> None:
32
32
  import pickle
33
33
 
34
34
  unset = _import_unset()
35
- assert pickle.loads(pickle.dumps(unset)) is unset # noqa: S301
35
+ assert pickle.loads(pickle.dumps(unset)) is unset
36
36
 
37
37
 
38
38
  def test_unset_type_expression_union() -> None:
@@ -62,7 +62,7 @@ if TYPE_CHECKING:
62
62
  from typing_extensions import assert_type
63
63
 
64
64
  def typing_unset(
65
- a: UNSET, b: str | UNSET, c: Any | UNSET, d: int | None | UNSET = UNSET
65
+ a: UNSET, b: str | UNSET, c: Any | UNSET, d: int | UNSET | None = UNSET
66
66
  ) -> None:
67
67
  assert_type(a, UNSET)
68
68
  assert_type(b, str | UNSET)
@@ -1,50 +1,54 @@
1
- from .spec import spec, Spec, View
2
- from .data import parquet, csv, spatial, table, json, data
1
+ from __future__ import annotations
2
+
3
+ from ._generated import * # noqa: F403
4
+ from ._generated import __all__ as _generated_all
5
+ from .data import csv, data, json, parquet, spatial, table
6
+ from .encodings import channels, sql
3
7
  from .params import param, selection
4
- from .encodings import sql, channels
5
8
  from .plot import (
6
- plot,
7
- source,
8
- mark,
9
+ brush,
10
+ checkbox,
11
+ color_legend,
9
12
  directive,
10
- margins,
11
- vconcat,
12
13
  hconcat,
13
- vspace,
14
+ highlight,
14
15
  hspace,
15
- slider,
16
- select,
17
- checkbox,
18
- menu,
19
- search,
20
- table_input,
21
16
  input,
22
- option,
23
- brush,
24
- sort,
25
17
  interval_x,
26
- interval_y,
27
18
  interval_xy,
28
- toggle,
29
- toggle_x,
30
- toggle_y,
31
- toggle_color,
19
+ interval_y,
20
+ margins,
21
+ mark,
22
+ menu,
23
+ nearest,
32
24
  nearest_x,
33
25
  nearest_y,
34
- region,
35
- highlight,
26
+ opacity_legend,
27
+ option,
36
28
  pan,
37
29
  pan_x,
38
30
  pan_y,
39
31
  pan_zoom,
40
32
  pan_zoom_x,
41
33
  pan_zoom_y,
42
- color_legend,
43
- opacity_legend,
34
+ plot,
35
+ region,
36
+ search,
37
+ select,
38
+ slider,
39
+ sort,
40
+ source,
44
41
  symbol_legend,
42
+ table_input,
43
+ toggle,
44
+ toggle_color,
45
+ toggle_x,
46
+ toggle_y,
47
+ toggle_z,
48
+ vconcat,
49
+ vspace,
45
50
  )
46
- from ._generated import * # noqa: F401, F403
47
- from ._generated import __all__ as _generated_all
51
+ from .spec import Spec, View, spec
48
52
 
49
53
  __all__ = [
50
54
  "Spec",
@@ -67,6 +71,7 @@ __all__ = [
67
71
  "margins",
68
72
  "mark",
69
73
  "menu",
74
+ "nearest",
70
75
  "nearest_x",
71
76
  "nearest_y",
72
77
  "opacity_legend",
@@ -97,6 +102,7 @@ __all__ = [
97
102
  "toggle_color",
98
103
  "toggle_x",
99
104
  "toggle_y",
105
+ "toggle_z",
100
106
  "vconcat",
101
107
  "vspace",
102
108
  ] + list(_generated_all) # pyright: ignore[reportUnsupportedDunderAll]
@@ -19,8 +19,6 @@ from __future__ import annotations
19
19
 
20
20
  import sys
21
21
  import typing
22
-
23
- # ruff: noqa: A002
24
22
  from contextlib import suppress
25
23
  from importlib.util import find_spec
26
24
  from typing import TYPE_CHECKING, Any, ClassVar
@@ -92,8 +90,8 @@ def _sentinel_backport_pre_py_3_15() -> Any:
92
90
 
93
91
  if TYPE_CHECKING:
94
92
  # Was renamed in https://github.com/python/typing_extensions/releases/tag/4.16.0
95
- from typing_extensions import Sentinel as sentinel # noqa: N813
96
- else: # noqa: PLR5501
93
+ from typing_extensions import Sentinel as sentinel
94
+ else: # ruff: ignore[collapsible-else-if]
97
95
  if sys.version_info >= (3, 15):
98
96
  from builtins import sentinel
99
97
  else:
@@ -1,10 +1,12 @@
1
1
  # DO NOT EDIT. Generated from the Mosaic JSON schema by bin/generate-python-api.js.
2
2
  # Regenerate with: pnpm run generate:python-api
3
+ from __future__ import annotations
4
+
3
5
  from .attributes import * # noqa: F403
4
- from .encodings import * # noqa: F403
5
- from .marks import * # noqa: F403
6
6
  from .attributes import __all__ as _attr_all
7
+ from .encodings import * # noqa: F403
7
8
  from .encodings import __all__ as _enc_all
9
+ from .marks import * # noqa: F403
8
10
  from .marks import __all__ as _mark_all
9
11
 
10
- __all__ = [*_mark_all, *_attr_all, *_enc_all] # pyright: ignore[reportUnsupportedDunderAll]
12
+ __all__ = [*_mark_all, *_attr_all, *_enc_all] # pyright: ignore[reportUnsupportedDunderAll] # ruff: ignore[invalid-all-object]
@@ -1,9 +1,15 @@
1
1
  # DO NOT EDIT. Generated from the Mosaic JSON schema by bin/generate-python-api.js.
2
2
  # Regenerate with: pnpm run generate:python-api
3
3
 
4
- from .._types import AttrValue
4
+ from __future__ import annotations
5
+
6
+ from typing import TYPE_CHECKING
7
+
5
8
  from ..plot import Directive
6
9
 
10
+ if TYPE_CHECKING:
11
+ from .._types import AttrValue
12
+
7
13
 
8
14
  def align(value: AttrValue) -> Directive:
9
15
  """How to distribute unused space in the **range** for *point* and *band* scales."""
@@ -1130,9 +1136,9 @@ __all__ = [
1130
1136
  "fx_tick_format",
1131
1137
  "fx_tick_padding",
1132
1138
  "fx_tick_rotate",
1133
- "fx_ticks",
1134
1139
  "fx_tick_size",
1135
1140
  "fx_tick_spacing",
1141
+ "fx_ticks",
1136
1142
  "fy_align",
1137
1143
  "fy_aria_description",
1138
1144
  "fy_aria_label",
@@ -1156,9 +1162,9 @@ __all__ = [
1156
1162
  "fy_tick_format",
1157
1163
  "fy_tick_padding",
1158
1164
  "fy_tick_rotate",
1159
- "fy_ticks",
1160
1165
  "fy_tick_size",
1161
1166
  "fy_tick_spacing",
1167
+ "fy_ticks",
1162
1168
  "grid",
1163
1169
  "height",
1164
1170
  "inset",
@@ -1250,11 +1256,11 @@ __all__ = [
1250
1256
  "x_tick_format",
1251
1257
  "x_tick_padding",
1252
1258
  "x_tick_rotate",
1253
- "x_ticks",
1254
1259
  "x_tick_size",
1255
1260
  "x_tick_spacing",
1256
- "xy_domain",
1261
+ "x_ticks",
1257
1262
  "x_zero",
1263
+ "xy_domain",
1258
1264
  "y_align",
1259
1265
  "y_aria_description",
1260
1266
  "y_aria_label",
@@ -1286,8 +1292,8 @@ __all__ = [
1286
1292
  "y_tick_format",
1287
1293
  "y_tick_padding",
1288
1294
  "y_tick_rotate",
1289
- "y_ticks",
1290
1295
  "y_tick_size",
1291
1296
  "y_tick_spacing",
1297
+ "y_ticks",
1292
1298
  "y_zero",
1293
1299
  ]
@@ -1,6 +1,8 @@
1
1
  # DO NOT EDIT. Generated from the Mosaic JSON schema by bin/generate-python-api.js.
2
2
  # Regenerate with: pnpm run generate:python-api
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  from typing import Any
5
7
 
6
8
  from .._types import UNSET, TransformArg
@@ -150,7 +152,7 @@ def mode(col: TransformArg, **options: Any) -> dict[str, Any]:
150
152
 
151
153
 
152
154
  def nth_value(
153
- col: TransformArg, offset: TransformArg | UNSET = UNSET, **options: Any
155
+ col: TransformArg, offset: TransformArg, **options: Any
154
156
  ) -> dict[str, Any]:
155
157
  """Get the nth value of the given column in the current window frame, counting from one."""
156
158
  return _transform("nth_value", (col, offset), options)
@@ -247,6 +249,6 @@ __all__ = [
247
249
  "stddev",
248
250
  "stddev_pop",
249
251
  "sum",
250
- "variance",
251
252
  "var_pop",
253
+ "variance",
252
254
  ]
@@ -1,6 +1,8 @@
1
1
  # DO NOT EDIT. Generated from the Mosaic JSON schema by bin/generate-python-api.js.
2
2
  # Regenerate with: pnpm run generate:python-api
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  from typing import Any
5
7
 
6
8
  from .._types import UNSET, ChannelValue, MarkData
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Sequence
2
4
  from datetime import date
3
- from typing import Any
5
+ from typing import Any, TypeAlias
4
6
 
5
7
  from vgplot._compat import sentinel
6
8
 
@@ -18,7 +20,9 @@ AttrValue = (
18
20
  str | float | bool | date | dict[str, Any] | Sequence[Any] | _ParamBase | None
19
21
  )
20
22
  MarkData = str | FromRef | DataDef | dict[str, Any] | Sequence[Any] | _ParamBase | None
21
- TransformArg = str | float | bool | _ParamBase
23
+ TransformArg = str | float | bool | dict[str, Any] | _ParamBase
22
24
 
23
25
  UNSET = sentinel("UNSET")
24
26
  """Sentinel for mark channels that were not passed (distinct from None)."""
27
+
28
+ MimeBundle: TypeAlias = dict[str, str] | tuple[dict[str, Any], dict[str, Any]] | None
@@ -39,10 +39,10 @@ class ParamValue(_ParamBase):
39
39
 
40
40
 
41
41
  class ParamArray(_ParamBase):
42
- def __init__(self, values: list) -> None:
43
- self._values = list(values)
42
+ def __init__(self, values: list[Any]) -> None:
43
+ self._values: list[Any] = list(values)
44
44
 
45
- def param_def(self, *, param_names: dict[int, str] | None = None) -> Any:
45
+ def param_def(self, *, param_names: dict[int, str] | None = None) -> list[Any]:
46
46
  return [_resolve(v, param_names) for v in self._values]
47
47
 
48
48
  def __repr__(self) -> str:
@@ -54,8 +54,8 @@ class SelectionDef(_ParamBase):
54
54
  self._select = select
55
55
  self._opts = {k: v for k, v in opts.items() if v is not None}
56
56
 
57
- def param_def(self, *, param_names: dict[int, str] | None = None) -> Any:
58
- d: dict = {"select": self._select}
57
+ def param_def(self, *, param_names: dict[int, str] | None = None) -> dict[str, Any]:
58
+ d: dict[str, Any] = {"select": self._select}
59
59
  d.update(_resolve(self._opts, param_names))
60
60
  return d
61
61
 
@@ -70,7 +70,7 @@ def param(value: Any = None) -> _ParamBase:
70
70
  return ParamValue(value)
71
71
 
72
72
 
73
- class selection:
73
+ class selection: # ruff: ignore[invalid-class-name]
74
74
  """Namespace for creating selection params."""
75
75
 
76
76
  @staticmethod
@@ -2,15 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Mapping
4
4
  from dataclasses import dataclass
5
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
- from .util import camelize, omit_none
8
- from .params import _ParamBase
9
7
  from .data import DataDef, is_frame
8
+ from .params import _ParamBase
9
+ from .util import camelize, omit_none
10
+
11
+ if TYPE_CHECKING:
12
+ from vgplot.spec import View
10
13
 
11
14
 
12
15
  class FromRef:
13
- def __init__(self, name: str, **opts: Any):
16
+ def __init__(self, name: str, **opts: Any) -> None:
14
17
  self.name = name
15
18
  self.opts = {camelize(k): v for k, v in opts.items() if v is not None}
16
19
 
@@ -27,7 +30,7 @@ class Directive:
27
30
  key: str
28
31
  value: Any
29
32
 
30
- def to_kv(self):
33
+ def to_kv(self) -> tuple[str, Any]:
31
34
  return camelize(self.key), self.value
32
35
 
33
36
 
@@ -95,7 +98,7 @@ def plot(
95
98
  *items: Mark | Directive | Mapping[str, Any],
96
99
  param_names: dict[int, str] | None = None,
97
100
  **kwargs: Any,
98
- ) -> Any:
101
+ ) -> View:
99
102
  from .spec import View
100
103
 
101
104
  marks: list[dict[str, Any]] = []
@@ -109,7 +112,8 @@ def plot(
109
112
  elif isinstance(item, (dict, Mapping)):
110
113
  marks.append({k: encode_value(v, param_names) for k, v in item.items()})
111
114
  else:
112
- raise TypeError(f"Unsupported plot item: {item}")
115
+ msg = f"Unsupported plot item: {item}"
116
+ raise TypeError(msg)
113
117
  root: dict[str, Any] = {"plot": marks}
114
118
  root.update(directives)
115
119
  return View(root, **kwargs)
@@ -172,7 +176,7 @@ def _encode_component(
172
176
  # Layout helpers
173
177
  def vconcat(
174
178
  *items: Any, param_names: dict[int, str] | None = None, **kwargs: Any
175
- ) -> Any:
179
+ ) -> View:
176
180
  from .spec import View
177
181
 
178
182
  return View(
@@ -182,7 +186,7 @@ def vconcat(
182
186
 
183
187
  def hconcat(
184
188
  *items: Any, param_names: dict[int, str] | None = None, **kwargs: Any
185
- ) -> Any:
189
+ ) -> View:
186
190
  from .spec import View
187
191
 
188
192
  return View(
@@ -190,13 +194,13 @@ def hconcat(
190
194
  )
191
195
 
192
196
 
193
- def hspace(value: int | str) -> Any:
197
+ def hspace(value: int | str) -> View:
194
198
  from .spec import View
195
199
 
196
200
  return View({"hspace": value})
197
201
 
198
202
 
199
- def vspace(value: int | str) -> Any:
203
+ def vspace(value: int | str) -> View:
200
204
  from .spec import View
201
205
 
202
206
  return View({"vspace": value})
@@ -297,6 +301,16 @@ def toggle_color(bind: Any = None, peers: Any = None) -> dict[str, Any]:
297
301
  return _interactor("toggleColor", **{"as": bind, "peers": peers})
298
302
 
299
303
 
304
+ def toggle_z(bind: Any = None, peers: Any = None) -> dict[str, Any]:
305
+ return _interactor("toggleZ", **{"as": bind, "peers": peers})
306
+
307
+
308
+ def nearest(
309
+ bind: Any = None, field: Any = None, channels: Any = None
310
+ ) -> dict[str, Any]:
311
+ return _interactor("nearest", **{"as": bind, "field": field, "channels": channels})
312
+
313
+
300
314
  def nearest_x(
301
315
  bind: Any = None, field: Any = None, channels: Any = None
302
316
  ) -> dict[str, Any]:
@@ -417,7 +431,7 @@ def symbol_legend(
417
431
  _INPUT_ALIASES = {"bind": "as", "source": "from"}
418
432
 
419
433
 
420
- def input(kind: str, **opts: Any) -> Any:
434
+ def input(kind: str, **opts: Any) -> View:
421
435
  from .spec import View
422
436
 
423
437
  payload: dict[str, Any] = {"input": kind}
@@ -443,7 +457,7 @@ def slider(
443
457
  step: Any = None,
444
458
  value: Any = None,
445
459
  **opts: Any,
446
- ) -> Any:
460
+ ) -> View:
447
461
  return input(
448
462
  "slider",
449
463
  label=label,
@@ -463,7 +477,7 @@ def select(
463
477
  multiple: bool = False,
464
478
  value: Any = None,
465
479
  **opts: Any,
466
- ) -> Any:
480
+ ) -> View:
467
481
  return input(
468
482
  "select",
469
483
  label=label,
@@ -475,7 +489,7 @@ def select(
475
489
  )
476
490
 
477
491
 
478
- def checkbox(label: str, bind: Any, value: bool = False, **opts: Any) -> Any:
492
+ def checkbox(label: str, bind: Any, value: bool = False, **opts: Any) -> View:
479
493
  return input("checkbox", label=label, bind=bind, value=value, **opts)
480
494
 
481
495
 
@@ -488,7 +502,7 @@ def menu(
488
502
  column: Any = None,
489
503
  filter_by: Any = None,
490
504
  **opts: Any,
491
- ) -> Any:
505
+ ) -> View:
492
506
  return input(
493
507
  "menu",
494
508
  label=label,
@@ -510,7 +524,7 @@ def search(
510
524
  filter_by: Any = None,
511
525
  type: Any = None,
512
526
  **opts: Any,
513
- ) -> Any:
527
+ ) -> View:
514
528
  return input(
515
529
  "search",
516
530
  label=label,
@@ -534,7 +548,7 @@ def table_input(
534
548
  row_batch: Any = None,
535
549
  align: Any = None,
536
550
  **opts: Any,
537
- ) -> Any:
551
+ ) -> View:
538
552
  return input(
539
553
  "table",
540
554
  source=source,
@@ -2,12 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  import json
5
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
- from .util import omit_none
8
- from .params import _ParamBase
9
7
  from .data import DataDef, is_frame
8
+ from .params import _ParamBase
10
9
  from .plot import _encode_component
10
+ from .util import omit_none
11
+
12
+ if TYPE_CHECKING:
13
+ from duckdb import DuckDBPyConnection
14
+ from narwhals.typing import IntoFrame
15
+
16
+ from vgplot._types import MimeBundle
11
17
 
12
18
 
13
19
  def _caller_locals() -> dict[str, Any]:
@@ -80,10 +86,10 @@ class Spec:
80
86
  data: dict[str, Any] | None = None,
81
87
  data_names: dict[int, str] | None = None,
82
88
  params: dict[str, Any] | None = None,
83
- plotDefaults: dict[str, Any] | None = None,
89
+ plotDefaults: dict[str, Any] | None = None, # ruff: ignore[invalid-argument-name]
84
90
  plot_defaults: dict[str, Any] | None = None,
85
91
  config: dict[str, Any] | None = None,
86
- view: dict[str, Any] | None = None,
92
+ view: dict[str, Any] | View | None = None,
87
93
  **extra: Any,
88
94
  ) -> None:
89
95
  # Serialize any DataDef values in data and build id->name mapping
@@ -170,7 +176,7 @@ class Spec:
170
176
  def to_json(self, **kwargs: Any) -> str:
171
177
  return json.dumps(self.to_dict(), **kwargs)
172
178
 
173
- def _repr_mimebundle_(self, **kwargs):
179
+ def _repr_mimebundle_(self, **kwargs: Any) -> MimeBundle:
174
180
  try:
175
181
  from mosaic_widget import MosaicWidget
176
182
  except ImportError:
@@ -178,12 +184,17 @@ class Spec:
178
184
  widget = MosaicWidget(self.to_dict())
179
185
  return widget._repr_mimebundle_(**kwargs)
180
186
 
181
- def show(self, con=None, data=None):
187
+ def show(
188
+ self,
189
+ con: DuckDBPyConnection | None = None,
190
+ data: dict[str, IntoFrame] | None = None,
191
+ ) -> None:
182
192
  try:
183
- from mosaic_widget import MosaicWidget
184
193
  from IPython.display import display
194
+ from mosaic_widget import MosaicWidget
185
195
  except ImportError as e:
186
- raise ImportError("pip install mosaic-widget") from e
196
+ msg = "pip install mosaic-widget"
197
+ raise ImportError(msg) from e
187
198
  widget = MosaicWidget(self.to_dict(), con=con, data=data)
188
199
  display(widget)
189
200
 
@@ -200,7 +211,7 @@ class View:
200
211
  view: dict[str, Any],
201
212
  *,
202
213
  data: dict[str, Any] | None = None,
203
- plotDefaults: dict[str, Any] | None = None,
214
+ plotDefaults: dict[str, Any] | None = None, # ruff: ignore[invalid-argument-name]
204
215
  plot_defaults: dict[str, Any] | None = None,
205
216
  config: dict[str, Any] | None = None,
206
217
  **extra: Any,
@@ -249,7 +260,7 @@ class View:
249
260
  def show(self, con: Any = None, data: Any = None) -> None:
250
261
  self._build_spec(_caller_locals()).show(con=con, data=data)
251
262
 
252
- def _repr_mimebundle_(self, **kwargs: Any):
263
+ def _repr_mimebundle_(self, **kwargs: Any) -> MimeBundle:
253
264
  # Walk up frames to find the one where this View object lives
254
265
  frame = inspect.currentframe()
255
266
  frame = frame.f_back if frame is not None else None
@@ -268,10 +279,10 @@ def spec(
268
279
  *args: Any,
269
280
  data: dict[str, Any] | None = None,
270
281
  params: dict[str, Any] | None = None,
271
- plotDefaults: dict[str, Any] | None = None,
282
+ plotDefaults: dict[str, Any] | None = None, # ruff: ignore[invalid-argument-name]
272
283
  plot_defaults: dict[str, Any] | None = None,
273
284
  config: dict[str, Any] | None = None,
274
- view: dict[str, Any] | None = None,
285
+ view: dict[str, Any] | View | None = None,
275
286
  **extra: Any,
276
287
  ) -> Spec:
277
288
  """Assemble a Spec from a view plus data sources and params.
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Any
2
4
 
3
5
 
@@ -1,56 +0,0 @@
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
- from pathlib import Path
8
-
9
- import vgplot as vg
10
-
11
- ROOT = Path(__file__).resolve().parents[4]
12
- SCHEMA = ROOT / "docs" / "public" / "schema" / "latest.json"
13
-
14
- # Schema names whose Python export is renamed or intentionally absent.
15
- RENAME = {"table": "table_input"} # `table` input -> vg.table_input (vg.table is data)
16
- IGNORE = {"value"} # internal selection const, not a user-facing interactor
17
-
18
-
19
- def _snake(name: str) -> str:
20
- return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
21
-
22
-
23
- def _const(node: dict, key: str):
24
- prop = node.get("properties", {}).get(key)
25
- return prop["const"] if isinstance(prop, dict) and "const" in prop else None
26
-
27
-
28
- def _consts(defs: dict, key: str) -> set[str]:
29
- """Const values for `key`, including intersection defs (anyOf/allOf) whose
30
- branches all agree on a single const (e.g. the densityX mark)."""
31
- out = set()
32
- for d in defs.values():
33
- c = _const(d, key)
34
- if c is not None:
35
- out.add(c)
36
- continue
37
- branch_consts = {
38
- _const(b, key) for b in (d.get("anyOf") or d.get("allOf") or [])
39
- } - {None}
40
- if len(branch_consts) == 1:
41
- out |= branch_consts
42
- return out
43
-
44
-
45
- def test_schema_surface_is_exported():
46
- defs = json.loads(SCHEMA.read_text("utf-8"))["definitions"]
47
- names = (
48
- _consts(defs, "mark")
49
- | _consts(defs, "select")
50
- | _consts(defs, "input")
51
- | set(defs["PlotAttributes"]["properties"])
52
- ) - IGNORE
53
-
54
- exported = set(vg.__all__)
55
- missing = sorted(n for n in names if RENAME.get(n, _snake(n)) not in exported)
56
- assert not missing, f"schema names with no exported vgplot function: {missing}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes