plot3 0.4.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.
- plot3/__init__.py +301 -0
- plot3/__version__.py +1 -0
- plot3/aesexpr.py +271 -0
- plot3/build.py +3948 -0
- plot3/calculus.py +1179 -0
- plot3/compose.py +285 -0
- plot3/contour.py +476 -0
- plot3/craft.py +142 -0
- plot3/encode.py +68 -0
- plot3/expr.py +1557 -0
- plot3/flip.py +245 -0
- plot3/function.py +1301 -0
- plot3/geoms.py +2558 -0
- plot3/ggplot.py +713 -0
- plot3/io.py +76 -0
- plot3/jupyter.py +514 -0
- plot3/latexin.py +616 -0
- plot3/masking.py +494 -0
- plot3/mathtext.py +842 -0
- plot3/payload.py +216 -0
- plot3/remote.py +220 -0
- plot3/scales.py +387 -0
- plot3/scaling.py +636 -0
- plot3/special.py +407 -0
- plot3/stat2d.py +1539 -0
- plot3/static.py +3760 -0
- plot3/stats3d.py +462 -0
- plot3/table.py +775 -0
- plot3/themes.py +104 -0
- plot3/viewer.py +3354 -0
- plot3-0.4.0.dist-info/METADATA +504 -0
- plot3-0.4.0.dist-info/RECORD +35 -0
- plot3-0.4.0.dist-info/WHEEL +5 -0
- plot3-0.4.0.dist-info/licenses/LICENSE +21 -0
- plot3-0.4.0.dist-info/top_level.txt +1 -0
plot3/payload.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Serializable PlotPayload — the contract between stats and the viewer.
|
|
2
|
+
|
|
3
|
+
Remote/local split:
|
|
4
|
+
|
|
5
|
+
* **Data / stats side** (local or GPU) produces a ``PlotPayload`` via
|
|
6
|
+
:func:`build_payload` / :meth:`ggplot.to_payload`.
|
|
7
|
+
* **Viewer side** turns that payload into HTML via :func:`render_payload`
|
|
8
|
+
or a payload-backed figure via :meth:`ggplot.from_payload`.
|
|
9
|
+
|
|
10
|
+
A payload does not hold a DataFrame. It holds the already-encoded wire format
|
|
11
|
+
that the three.js template consumes (JSON spec + base64 binary blobs).
|
|
12
|
+
|
|
13
|
+
Schema (``kind == "figure"``)::
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
"v": 1,
|
|
17
|
+
"kind": "figure",
|
|
18
|
+
"spec": { ... }, # same dict as build_spec()[0]
|
|
19
|
+
"blobs": { "p0x": "<b64>", ... },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Faceted figures are still assembled by :func:`plot3.build.build_doc` as a
|
|
23
|
+
grid of independent figure payloads (each panel is a normal figure payload).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
# Wire-format version for PlotPayload (independent of spec["v"]).
|
|
35
|
+
PAYLOAD_VERSION = 1
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"PAYLOAD_VERSION",
|
|
39
|
+
"build_payload",
|
|
40
|
+
"render_payload",
|
|
41
|
+
"validate_payload",
|
|
42
|
+
"payload_from_spec",
|
|
43
|
+
"payload_blobs_list",
|
|
44
|
+
"save_payload",
|
|
45
|
+
"load_payload",
|
|
46
|
+
"display_payload",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def payload_from_spec(
|
|
51
|
+
spec: dict[str, Any],
|
|
52
|
+
payloads: list[tuple[str, str]],
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""Build a figure PlotPayload from :func:`plot3.build.build_spec` output."""
|
|
55
|
+
blobs = {str(pid): str(b64) for pid, b64 in payloads}
|
|
56
|
+
return {
|
|
57
|
+
"v": PAYLOAD_VERSION,
|
|
58
|
+
"kind": "figure",
|
|
59
|
+
"spec": spec,
|
|
60
|
+
"blobs": blobs,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def payload_blobs_list(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
65
|
+
"""Restore ``build_spec``-style ``[(id, b64), ...]`` from a payload."""
|
|
66
|
+
blobs = payload.get("blobs") or {}
|
|
67
|
+
if not isinstance(blobs, dict):
|
|
68
|
+
raise TypeError("payload['blobs'] must be a dict")
|
|
69
|
+
return [(str(pid), str(b64)) for pid, b64 in blobs.items()]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def validate_payload(payload: Any) -> dict[str, Any]:
|
|
73
|
+
"""Validate a PlotPayload dict; return it on success.
|
|
74
|
+
|
|
75
|
+
Raises ``TypeError`` / ``ValueError`` with actionable messages.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(payload, dict):
|
|
78
|
+
raise TypeError(f"PlotPayload must be a dict, got {type(payload).__name__}")
|
|
79
|
+
if payload.get("v") != PAYLOAD_VERSION:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"unsupported PlotPayload version {payload.get('v')!r}; "
|
|
82
|
+
f"expected v={PAYLOAD_VERSION}"
|
|
83
|
+
)
|
|
84
|
+
kind = payload.get("kind")
|
|
85
|
+
if kind != "figure":
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"unsupported PlotPayload kind {kind!r}; expected 'figure'"
|
|
88
|
+
)
|
|
89
|
+
spec = payload.get("spec")
|
|
90
|
+
if not isinstance(spec, dict):
|
|
91
|
+
raise TypeError("PlotPayload['spec'] must be a dict")
|
|
92
|
+
for key in ("v", "is3d", "theme", "labs", "scales", "layers"):
|
|
93
|
+
if key not in spec:
|
|
94
|
+
raise ValueError(f"PlotPayload spec missing required key {key!r}")
|
|
95
|
+
if not isinstance(spec["layers"], list):
|
|
96
|
+
raise TypeError("PlotPayload spec['layers'] must be a list")
|
|
97
|
+
blobs = payload.get("blobs")
|
|
98
|
+
if not isinstance(blobs, dict):
|
|
99
|
+
raise TypeError("PlotPayload['blobs'] must be a dict")
|
|
100
|
+
for pid, b64 in blobs.items():
|
|
101
|
+
if not isinstance(pid, str) or not isinstance(b64, str):
|
|
102
|
+
raise TypeError("PlotPayload blobs must map str id -> str base64")
|
|
103
|
+
return payload
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_payload(g) -> dict[str, Any]:
|
|
107
|
+
"""Compute a PlotPayload for a single-panel (non-faceted) figure.
|
|
108
|
+
|
|
109
|
+
Runs stats / encoding where the data lives. Does not produce HTML.
|
|
110
|
+
|
|
111
|
+
Faceted figures (``facet_wrap``) should use :func:`plot3.build.build_doc`,
|
|
112
|
+
which builds one payload per panel. Calling this on a faceted ggplot
|
|
113
|
+
raises ``ValueError``.
|
|
114
|
+
"""
|
|
115
|
+
if getattr(g, "facet", None) is not None:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"to_payload() / build_payload() do not support facet_wrap(); "
|
|
118
|
+
"use build_doc() / fig.html() which assembles panel payloads"
|
|
119
|
+
)
|
|
120
|
+
from plot3.build import build_spec
|
|
121
|
+
|
|
122
|
+
spec, payloads = build_spec(g)
|
|
123
|
+
return payload_from_spec(spec, payloads)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def script_json(value) -> str:
|
|
127
|
+
"""JSON safe inside an HTML ``<script>``: ``</script>`` or ``<!--`` in a
|
|
128
|
+
title or a category cannot end the script, so data never becomes markup."""
|
|
129
|
+
text = json.dumps(value, separators=(",", ":"))
|
|
130
|
+
return (
|
|
131
|
+
text.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
|
|
132
|
+
.replace("\u2028", "\\u2028").replace("\u2029", "\\u2029")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def render_payload(
|
|
137
|
+
payload: dict[str, Any],
|
|
138
|
+
*,
|
|
139
|
+
log: bool = True,
|
|
140
|
+
) -> str:
|
|
141
|
+
"""Turn a validated figure PlotPayload into a standalone HTML document.
|
|
142
|
+
|
|
143
|
+
This is the **viewer** half: no DataFrame access, only ``spec`` + ``blobs``.
|
|
144
|
+
"""
|
|
145
|
+
from plot3.viewer import _DOC_TEMPLATE as DOC_TEMPLATE
|
|
146
|
+
from plot3.viewer import _KATEX_BOOT
|
|
147
|
+
|
|
148
|
+
payload = validate_payload(payload)
|
|
149
|
+
spec = payload["spec"]
|
|
150
|
+
blobs = payload["blobs"]
|
|
151
|
+
|
|
152
|
+
# Preserve insertion order from build_spec (viewer looks up by id).
|
|
153
|
+
blocks = "\n".join(
|
|
154
|
+
f'<script type="text/plain" id="{pid}">{b64}</script>'
|
|
155
|
+
for pid, b64 in blobs.items()
|
|
156
|
+
)
|
|
157
|
+
parts = {
|
|
158
|
+
"SPEC": script_json(spec),
|
|
159
|
+
"PAYLOADS": blocks,
|
|
160
|
+
"KATEX": _KATEX_BOOT if spec.get("math") else "",
|
|
161
|
+
}
|
|
162
|
+
# One pass: text from the figure (a title that says "__KATEX__") is
|
|
163
|
+
# never searched again for placeholders.
|
|
164
|
+
doc = re.sub(r"__(SPEC|PAYLOADS|KATEX)__", lambda m: parts[m.group(1)], DOC_TEMPLATE)
|
|
165
|
+
if log:
|
|
166
|
+
kb = len(doc) // 1024
|
|
167
|
+
# Sizes are opt-in (PLOT3_VERBOSE=1), so a notebook cell is just
|
|
168
|
+
# the plot.
|
|
169
|
+
if os.environ.get("PLOT3_VERBOSE", "").strip() == "1":
|
|
170
|
+
rows = sum(int(sp.get("n", 0)) for sp in spec.get("layers", []))
|
|
171
|
+
n_layers = len(spec.get("layers", []))
|
|
172
|
+
print(
|
|
173
|
+
f"plot3: {n_layers} layer(s), {rows:,} rows -> {kb:,} KB "
|
|
174
|
+
f"portable HTML{' (3D)' if spec.get('is3d') else ''}"
|
|
175
|
+
)
|
|
176
|
+
if kb > 1500:
|
|
177
|
+
print("plot3: the figure is over 1.5 MB of HTML")
|
|
178
|
+
return doc
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def save_payload(payload: dict[str, Any], path: str | Path) -> str:
|
|
182
|
+
"""Write a PlotPayload to a JSON file (remote → local handoff artifact)."""
|
|
183
|
+
payload = validate_payload(payload)
|
|
184
|
+
path = Path(path)
|
|
185
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
path.write_text(
|
|
187
|
+
json.dumps(payload, separators=(",", ":"), ensure_ascii=False),
|
|
188
|
+
encoding="utf-8",
|
|
189
|
+
)
|
|
190
|
+
return str(path.resolve())
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def load_payload(path: str | Path) -> dict[str, Any]:
|
|
194
|
+
"""Load and validate a PlotPayload JSON file."""
|
|
195
|
+
path = Path(path)
|
|
196
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
197
|
+
return validate_payload(data)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def display_payload(
|
|
201
|
+
payload: dict[str, Any],
|
|
202
|
+
*,
|
|
203
|
+
browser: bool | None = None,
|
|
204
|
+
path: str | Path | None = None,
|
|
205
|
+
height: str | int = "480px",
|
|
206
|
+
hide: bool | None = None,
|
|
207
|
+
):
|
|
208
|
+
"""Show a PlotPayload without a live DataFrame (local viewer half).
|
|
209
|
+
|
|
210
|
+
Builds a payload-backed :class:`~plot3.ggplot.ggplot` and calls
|
|
211
|
+
:meth:`~plot3.ggplot.ggplot.show`.
|
|
212
|
+
"""
|
|
213
|
+
from plot3.ggplot import ggplot
|
|
214
|
+
|
|
215
|
+
fig = ggplot.from_payload(payload, height=height, hide=hide)
|
|
216
|
+
return fig.show(browser=browser, path=path)
|
plot3/remote.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""CRAFT / GPU bridge: stats on the remote, render on the host.
|
|
2
|
+
|
|
3
|
+
Phase D of the remote/local split.
|
|
4
|
+
|
|
5
|
+
Host (SolveIt + CRAFT connected)
|
|
6
|
+
``remote_run_`` executes Python on the GPU kernel.
|
|
7
|
+
:func:`fetch_remote_payload` runs a ggplot expression there, pulls only
|
|
8
|
+
the compact PlotPayload (via temp file + SSH), and the host calls
|
|
9
|
+
:func:`plot3.payload.display_payload` / :func:`render_payload`.
|
|
10
|
+
|
|
11
|
+
Remote kernel (``%gpu`` cell body)
|
|
12
|
+
``ggplot(...)._repr_mimebundle_`` includes
|
|
13
|
+
``application/vnd.plot3.v1+json`` so a future host can intercept the
|
|
14
|
+
payload; HTML remains the fallback for display.
|
|
15
|
+
|
|
16
|
+
Environment
|
|
17
|
+
``PLOT3_REMOTE=1`` forces remote-kernel display behaviour (payload mime
|
|
18
|
+
preferred, lighter show path).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shlex
|
|
26
|
+
import uuid
|
|
27
|
+
from typing import Any, Callable
|
|
28
|
+
|
|
29
|
+
# Custom Jupyter mime type for PlotPayload handoff (host may ignore → HTML).
|
|
30
|
+
MIME_PLOT3 = "application/vnd.plot3.v1+json"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"MIME_PLOT3",
|
|
34
|
+
"is_remote_kernel",
|
|
35
|
+
"has_craft_host",
|
|
36
|
+
"fetch_remote_payload",
|
|
37
|
+
"remote_ggplot_payload",
|
|
38
|
+
"show_remote",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_remote_kernel() -> bool:
|
|
43
|
+
"""True when this process should act as the GPU/remote side of the bridge.
|
|
44
|
+
|
|
45
|
+
Heuristics (any match):
|
|
46
|
+
|
|
47
|
+
* ``PLOT3_REMOTE=1`` / ``true`` / ``yes``
|
|
48
|
+
* ``CRAFT_REMOTE=1`` or ``GPU_KERNEL=1``
|
|
49
|
+
* IPython ns has CRAFT remote markers but **not** host ``remote_run_``
|
|
50
|
+
"""
|
|
51
|
+
for key in ("PLOT3_REMOTE", "CRAFT_REMOTE", "GPU_KERNEL"):
|
|
52
|
+
raw = (os.environ.get(key) or "").strip().lower()
|
|
53
|
+
if raw in ("1", "true", "yes", "on"):
|
|
54
|
+
return True
|
|
55
|
+
try:
|
|
56
|
+
from IPython import get_ipython
|
|
57
|
+
|
|
58
|
+
ip = get_ipython()
|
|
59
|
+
ns = (getattr(ip, "user_ns", None) or {}) if ip is not None else {}
|
|
60
|
+
except Exception:
|
|
61
|
+
ns = {}
|
|
62
|
+
# Host CRAFT always exposes remote_run_ for talking *to* the GPU.
|
|
63
|
+
if callable(ns.get("remote_run_")):
|
|
64
|
+
return False
|
|
65
|
+
# Remote kernel often has GPU-oriented markers without host helpers.
|
|
66
|
+
for key in ("_craft_remote", "IS_GPU_KERNEL", "cuda", "CUDA_VISIBLE_DEVICES"):
|
|
67
|
+
if key in ns:
|
|
68
|
+
return True
|
|
69
|
+
if os.environ.get("CUDA_VISIBLE_DEVICES") is not None and not callable(
|
|
70
|
+
ns.get("remote_run_")
|
|
71
|
+
):
|
|
72
|
+
# Weak signal — only if explicitly on a GPU box without host CRAFT.
|
|
73
|
+
if (os.environ.get("PLOT3_ASSUME_REMOTE") or "").strip().lower() in (
|
|
74
|
+
"1",
|
|
75
|
+
"true",
|
|
76
|
+
"yes",
|
|
77
|
+
):
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def has_craft_host() -> bool:
|
|
83
|
+
"""True when the host IPython session can call ``remote_run_``."""
|
|
84
|
+
try:
|
|
85
|
+
from IPython import get_ipython
|
|
86
|
+
|
|
87
|
+
ip = get_ipython()
|
|
88
|
+
ns = (getattr(ip, "user_ns", None) or {}) if ip is not None else {}
|
|
89
|
+
except Exception:
|
|
90
|
+
return False
|
|
91
|
+
return callable(ns.get("remote_run_"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _remote_run() -> Callable[..., str]:
|
|
95
|
+
from IPython import get_ipython
|
|
96
|
+
|
|
97
|
+
ip = get_ipython()
|
|
98
|
+
if ip is None:
|
|
99
|
+
raise RuntimeError("fetch_remote_payload requires IPython")
|
|
100
|
+
rr = (ip.user_ns or {}).get("remote_run_")
|
|
101
|
+
if not callable(rr):
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
"remote_run_ missing — load CRAFT and run %gpu on the host first"
|
|
104
|
+
)
|
|
105
|
+
return rr
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def fetch_remote_payload(
|
|
109
|
+
remote_source: str,
|
|
110
|
+
*,
|
|
111
|
+
max_chars: int = 8000,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""Execute *remote_source* on the GPU kernel and return a PlotPayload.
|
|
114
|
+
|
|
115
|
+
*remote_source* must leave a dict named ``_plot3_payload`` in scope
|
|
116
|
+
(or assign the final expression result). Implementation writes JSON to a
|
|
117
|
+
temp file on the remote and streams it back with SSH (same pattern as
|
|
118
|
+
:func:`plot3.jupyter.remote_df`), so large blobs are not limited by
|
|
119
|
+
``remote_run_`` stdout caps.
|
|
120
|
+
"""
|
|
121
|
+
from plot3.io import ssh_bytes
|
|
122
|
+
from plot3.payload import validate_payload
|
|
123
|
+
|
|
124
|
+
rr = _remote_run()
|
|
125
|
+
tmp = f"/tmp/plot3_payload_{uuid.uuid4().hex}.json"
|
|
126
|
+
# Indent remote body for the embedded script.
|
|
127
|
+
body = remote_source.strip()
|
|
128
|
+
if not body.endswith("\n"):
|
|
129
|
+
body += "\n"
|
|
130
|
+
code = f"""
|
|
131
|
+
import json as _json
|
|
132
|
+
from pathlib import Path as _Path
|
|
133
|
+
{body}
|
|
134
|
+
if "_plot3_payload" not in dir() and "_plot3_payload" not in locals():
|
|
135
|
+
raise RuntimeError(
|
|
136
|
+
"remote plot source must set _plot3_payload = fig.to_payload()"
|
|
137
|
+
)
|
|
138
|
+
_p = _Path({tmp!r})
|
|
139
|
+
_p.write_text(_json.dumps(_plot3_payload, separators=(",", ":")), encoding="utf-8")
|
|
140
|
+
print(_p.as_posix())
|
|
141
|
+
print("plot3_payload_bytes", _p.stat().st_size)
|
|
142
|
+
"""
|
|
143
|
+
out = (rr(code, max_chars=max_chars) or "").strip()
|
|
144
|
+
# Path line: absolute or relative *.json written by the remote helper.
|
|
145
|
+
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
|
|
146
|
+
path_line = None
|
|
147
|
+
for ln in reversed(lines):
|
|
148
|
+
if not ln.endswith(".json"):
|
|
149
|
+
continue
|
|
150
|
+
if "plot3_payload" in ln or ln.startswith("/") or ln[1:3] == ":\\":
|
|
151
|
+
path_line = ln
|
|
152
|
+
break
|
|
153
|
+
if not path_line:
|
|
154
|
+
for ln in reversed(lines):
|
|
155
|
+
if ln.endswith(".json") and not ln.startswith("plot3_payload"):
|
|
156
|
+
path_line = ln
|
|
157
|
+
break
|
|
158
|
+
if not path_line:
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
"remote payload path not found in remote_run_ output:\n"
|
|
161
|
+
+ (out[-800:] if out else "(empty)")
|
|
162
|
+
)
|
|
163
|
+
try:
|
|
164
|
+
raw = ssh_bytes("cat -- " + shlex.quote(path_line))
|
|
165
|
+
finally:
|
|
166
|
+
try:
|
|
167
|
+
ssh_bytes("rm -f -- " + shlex.quote(path_line))
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
try:
|
|
171
|
+
data = json.loads(raw.decode("utf-8"))
|
|
172
|
+
except Exception as e:
|
|
173
|
+
raise RuntimeError(f"invalid remote PlotPayload JSON: {e}") from e
|
|
174
|
+
return validate_payload(data)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def remote_ggplot_payload(
|
|
178
|
+
expr: str,
|
|
179
|
+
*,
|
|
180
|
+
max_chars: int = 8000,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""Evaluate a ggplot expression on the remote kernel; return PlotPayload.
|
|
183
|
+
|
|
184
|
+
Example::
|
|
185
|
+
|
|
186
|
+
payload = remote_ggplot_payload(
|
|
187
|
+
"ggplot(df, aes(x='wt', y='mpg')) + geom_point()"
|
|
188
|
+
)
|
|
189
|
+
"""
|
|
190
|
+
expr = expr.strip()
|
|
191
|
+
source = f"""
|
|
192
|
+
_fig = ({expr})
|
|
193
|
+
if not hasattr(_fig, "to_payload"):
|
|
194
|
+
raise TypeError(
|
|
195
|
+
f"remote expression must produce a ggplot, got {{type(_fig).__name__}}"
|
|
196
|
+
)
|
|
197
|
+
_plot3_payload = _fig.to_payload()
|
|
198
|
+
"""
|
|
199
|
+
return fetch_remote_payload(source, max_chars=max_chars)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def show_remote(
|
|
203
|
+
expr: str,
|
|
204
|
+
*,
|
|
205
|
+
browser: bool | None = None,
|
|
206
|
+
height: str | int = "480px",
|
|
207
|
+
hide: bool | None = None,
|
|
208
|
+
max_chars: int = 8000,
|
|
209
|
+
):
|
|
210
|
+
"""Run *expr* on the remote kernel and display the figure on the host.
|
|
211
|
+
|
|
212
|
+
Stats / encoding run where the data lives; only the PlotPayload crosses
|
|
213
|
+
the wire. Viewer HTML is built locally.
|
|
214
|
+
"""
|
|
215
|
+
from plot3.payload import display_payload
|
|
216
|
+
|
|
217
|
+
payload = remote_ggplot_payload(expr, max_chars=max_chars)
|
|
218
|
+
return display_payload(
|
|
219
|
+
payload, browser=browser, height=height, hide=hide
|
|
220
|
+
)
|