plot3 0.6.5__tar.gz → 0.6.7__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.
- {plot3-0.6.5 → plot3-0.6.7}/PKG-INFO +12 -1
- {plot3-0.6.5 → plot3-0.6.7}/README.md +11 -0
- plot3-0.6.7/plot3/__version__.py +1 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/build.py +32 -21
- {plot3-0.6.5 → plot3-0.6.7}/plot3/function.py +10 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/geoms.py +8 -5
- {plot3-0.6.5 → plot3-0.6.7}/plot3/ggplot.py +29 -2
- {plot3-0.6.5 → plot3-0.6.7}/plot3/jupyter.py +29 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/masking.py +132 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3.egg-info/PKG-INFO +12 -1
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_animation.py +30 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_masking.py +41 -0
- plot3-0.6.5/plot3/__version__.py +0 -1
- {plot3-0.6.5 → plot3-0.6.7}/LICENSE +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/__init__.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/aesexpr.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/calculus.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/compose.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/contour.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/craft.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/encode.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/expr.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/flip.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/io.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/latexin.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/mathtext.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/payload.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/remote.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/scales.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/scaling.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/special.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/stat2d.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/static.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/stats3d.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/table.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/themes.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3/viewer.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3.egg-info/SOURCES.txt +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3.egg-info/dependency_links.txt +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3.egg-info/requires.txt +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/plot3.egg-info/top_level.txt +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/pyproject.toml +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/setup.cfg +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_2d_density.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_3d.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_annotate.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_api_public.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_array_backend.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_arrow.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_build_contract.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_contour.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_everyday_ggplot2.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_expr.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_facets_compose.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_geom_function.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_geoms_ds.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_ggplot2_extras2.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_ggplot2_parity.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_ggplot_extras.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_grammar.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_latex_input.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_lidar_scene.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_math_layers.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_mathtext.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_payload.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_payload_display.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_point_cloud.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_r_oracle_parity.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_readme.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_remote_bridge.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_review_fixes.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_save_menu.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_scales_api.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_scales_themes.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_special.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_stat2d.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_static_save.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_stats_semantic.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_step3_geoms.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_table_backends.py +0 -0
- {plot3-0.6.5 → plot3-0.6.7}/tests/test_titles_theme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plot3
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.7
|
|
4
4
|
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -260,6 +260,17 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
|
|
|
260
260
|
- **Jupyter / SolveIt**: bare column names and backticks work in `aes()`,
|
|
261
261
|
`facet_wrap()`, and `facet_grid()` (`aes(x=`First Name`)`). Toggle with
|
|
262
262
|
`enable_r_style()` / `disable_r_style()`. `.py` files keep quoted strings.
|
|
263
|
+
- **Layers on their own lines**, as in R, with no parentheses:
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
ggplot(gapminder, aes(x=gdpPercap, y=lifeExp, colour=continent))
|
|
267
|
+
+ geom_point(alpha=0.7) # comments are fine
|
|
268
|
+
+ scale_x_log10()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
A line starting with `+` and a plot3 function continues the plot above
|
|
272
|
+
it. Import plot3 in an earlier cell: IPython reads a whole cell before
|
|
273
|
+
running any of it. `.py` files keep the outer parentheses.
|
|
263
274
|
- **SolveIt** draws figures inline and hides their HTML from the model's
|
|
264
275
|
context (`autohide(False)` to opt out).
|
|
265
276
|
- **VS Code notebooks** block WebGL in output cells, so plot3 opens figures
|
|
@@ -233,6 +233,17 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
|
|
|
233
233
|
- **Jupyter / SolveIt**: bare column names and backticks work in `aes()`,
|
|
234
234
|
`facet_wrap()`, and `facet_grid()` (`aes(x=`First Name`)`). Toggle with
|
|
235
235
|
`enable_r_style()` / `disable_r_style()`. `.py` files keep quoted strings.
|
|
236
|
+
- **Layers on their own lines**, as in R, with no parentheses:
|
|
237
|
+
|
|
238
|
+
```text
|
|
239
|
+
ggplot(gapminder, aes(x=gdpPercap, y=lifeExp, colour=continent))
|
|
240
|
+
+ geom_point(alpha=0.7) # comments are fine
|
|
241
|
+
+ scale_x_log10()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A line starting with `+` and a plot3 function continues the plot above
|
|
245
|
+
it. Import plot3 in an earlier cell: IPython reads a whole cell before
|
|
246
|
+
running any of it. `.py` files keep the outer parentheses.
|
|
236
247
|
- **SolveIt** draws figures inline and hides their HTML from the model's
|
|
237
248
|
context (`autohide(False)` to opt out).
|
|
238
249
|
- **VS Code notebooks** block WebGL in output cells, so plot3 opens figures
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.7"
|
|
@@ -1970,6 +1970,9 @@ def build_spec(g: ggplot) -> tuple[dict, list[tuple[str, str]]]:
|
|
|
1970
1970
|
"slider() cannot be combined with transition_time() "
|
|
1971
1971
|
"or transition_states()"
|
|
1972
1972
|
)
|
|
1973
|
+
if slider is not None:
|
|
1974
|
+
# Default steps shrink to fit while layers expand; keep that per render.
|
|
1975
|
+
slider = copy.copy(slider)
|
|
1973
1976
|
domains = None
|
|
1974
1977
|
if any(getattr(layer, "kind", None) == "function" for layer in layers_in):
|
|
1975
1978
|
from plot3.function import data_domains
|
|
@@ -1978,27 +1981,35 @@ def build_spec(g: ggplot) -> tuple[dict, list[tuple[str, str]]]:
|
|
|
1978
1981
|
addon_map: dict[int, list] = {}
|
|
1979
1982
|
for index, addon in getattr(g, "_addons", None) or []:
|
|
1980
1983
|
addon_map.setdefault(int(index), []).append(addon)
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
1984
|
+
def expand_all() -> list:
|
|
1985
|
+
out = []
|
|
1986
|
+
for index, geom in enumerate(layers_in):
|
|
1987
|
+
layer_data = getattr(geom, "layer_data", None)
|
|
1988
|
+
if getattr(geom, "kind", None) == "box3d":
|
|
1989
|
+
# Box classes take the figure theme's group colours.
|
|
1990
|
+
geom = copy.copy(geom)
|
|
1991
|
+
geom._palette = list(theme["cat"])
|
|
1992
|
+
result = expand_stat_geom(
|
|
1993
|
+
geom,
|
|
1994
|
+
g.mapping,
|
|
1995
|
+
data if layer_data is None else layer_data,
|
|
1996
|
+
domains,
|
|
1997
|
+
transition,
|
|
1998
|
+
slider,
|
|
1999
|
+
coord,
|
|
2000
|
+
addon_map.get(index),
|
|
2001
|
+
)
|
|
2002
|
+
if isinstance(result, list):
|
|
2003
|
+
out.extend(result)
|
|
2004
|
+
else:
|
|
2005
|
+
out.append(result)
|
|
2006
|
+
return out
|
|
2007
|
+
|
|
2008
|
+
steps_before = getattr(slider, "steps", None)
|
|
2009
|
+
expanded = expand_all()
|
|
2010
|
+
if getattr(slider, "steps", None) != steps_before:
|
|
2011
|
+
# A later layer lowered the default steps: every layer shares one grid.
|
|
2012
|
+
expanded = expand_all()
|
|
2002
2013
|
from plot3.calculus import mark_intersections
|
|
2003
2014
|
|
|
2004
2015
|
expanded = mark_intersections(expanded)
|
|
@@ -938,6 +938,16 @@ def _guard_slider(source: Any, n_samples: int, what: str) -> None:
|
|
|
938
938
|
cells = int(n_samples) * n_frames
|
|
939
939
|
if cells <= _SLIDER_CELL_CAP:
|
|
940
940
|
return
|
|
941
|
+
if getattr(source, "auto_steps", False):
|
|
942
|
+
# Default steps: take the largest count that fits. build_spec hands
|
|
943
|
+
# each render its own copy and re-expands if this lowered it.
|
|
944
|
+
count = len(source.ranges)
|
|
945
|
+
steps = int(source.steps)
|
|
946
|
+
while steps > 2 and int(n_samples) * steps**count > _SLIDER_CELL_CAP:
|
|
947
|
+
steps -= 1
|
|
948
|
+
if int(n_samples) * steps**count <= _SLIDER_CELL_CAP:
|
|
949
|
+
source.steps = steps
|
|
950
|
+
return
|
|
941
951
|
raise ValueError(
|
|
942
952
|
f"slider() would sample {n_frames} frames of {n_samples} {what} "
|
|
943
953
|
f"({cells} values). Pass a smaller steps= or n=."
|
|
@@ -2181,20 +2181,23 @@ class slider:
|
|
|
2181
2181
|
|
|
2182
2182
|
ggplot() + geom_function("y = a sin(k x)") + slider(a=(0, 3), k=(1, 5))
|
|
2183
2183
|
|
|
2184
|
-
``steps`` defaults to 25 per parameter
|
|
2185
|
-
|
|
2186
|
-
``steps``
|
|
2184
|
+
``steps`` defaults to 25 per parameter, lowered as far as needed for
|
|
2185
|
+
the whole grid to fit (three parameters on a curve get 9). An explicit
|
|
2186
|
+
``steps`` is kept as given and refused if it is too large; pass a
|
|
2187
|
+
smaller ``steps`` or ``n``. Do not combine with ``transition_time()``.
|
|
2187
2188
|
"""
|
|
2188
2189
|
|
|
2189
2190
|
kind = "slider"
|
|
2190
2191
|
|
|
2191
|
-
def __init__(self, *, steps: int =
|
|
2192
|
+
def __init__(self, *, steps: int | None = None, **ranges):
|
|
2192
2193
|
if not ranges:
|
|
2193
2194
|
raise TypeError(
|
|
2194
2195
|
"slider() needs a parameter range, for example slider(a=(0, 3))"
|
|
2195
2196
|
)
|
|
2197
|
+
# Only the default shrinks to fit; an explicit count is the user's.
|
|
2198
|
+
self.auto_steps = steps is None
|
|
2196
2199
|
try:
|
|
2197
|
-
nsteps = int(steps)
|
|
2200
|
+
nsteps = 25 if steps is None else int(steps)
|
|
2198
2201
|
except (TypeError, ValueError):
|
|
2199
2202
|
raise TypeError(
|
|
2200
2203
|
"slider() steps= must be an integer, for example steps=25"
|
|
@@ -42,6 +42,25 @@ from plot3.table import as_table, detect_backend
|
|
|
42
42
|
from plot3.themes import _THEMES
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _error_text(exc: BaseException) -> str:
|
|
46
|
+
return f"plot3 could not draw this figure.\n{type(exc).__name__}: {exc}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _error_html(exc: BaseException) -> str:
|
|
50
|
+
import traceback
|
|
51
|
+
|
|
52
|
+
trace = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
53
|
+
return (
|
|
54
|
+
'<div class="plot3-error" style="border-left:4px solid #d9534f;'
|
|
55
|
+
"padding:8px 12px;font-family:ui-monospace,Menlo,monospace;"
|
|
56
|
+
'font-size:13px;white-space:pre-wrap">'
|
|
57
|
+
"<strong>plot3 could not draw this figure.</strong>\n"
|
|
58
|
+
f"{_htmlesc.escape(type(exc).__name__)}: {_htmlesc.escape(str(exc))}"
|
|
59
|
+
'<details style="margin-top:6px"><summary>Traceback</summary>'
|
|
60
|
+
f"{_htmlesc.escape(trace)}</details></div>"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
def _env_flag(name: str) -> bool | None:
|
|
46
65
|
raw = os.environ.get(name)
|
|
47
66
|
if raw is None or raw == "":
|
|
@@ -422,7 +441,10 @@ class ggplot:
|
|
|
422
441
|
# Used by hosts that only understand HTML reprs (and by tests).
|
|
423
442
|
# Prefer ``display(fig)`` / ``fig.show()`` so VS Code can open a browser.
|
|
424
443
|
self._maybe_hide_from_ai()
|
|
425
|
-
|
|
444
|
+
try:
|
|
445
|
+
return self._iframe()
|
|
446
|
+
except Exception as exc:
|
|
447
|
+
return _error_html(exc)
|
|
426
448
|
|
|
427
449
|
def _repr_mimebundle_(self, include=None, exclude=None):
|
|
428
450
|
"""HTML plus optional PlotPayload mime for CRAFT host handoff.
|
|
@@ -435,7 +457,12 @@ class ggplot:
|
|
|
435
457
|
from plot3.remote import MIME_PLOT3
|
|
436
458
|
|
|
437
459
|
self._maybe_hide_from_ai()
|
|
438
|
-
|
|
460
|
+
try:
|
|
461
|
+
bundle: dict = {"text/html": self._iframe()}
|
|
462
|
+
except Exception as exc:
|
|
463
|
+
# Notebook hosts swallow repr errors and fall back to the bare
|
|
464
|
+
# object repr, so the reason a plot is missing must be the output.
|
|
465
|
+
return {"text/html": _error_html(exc), "text/plain": _error_text(exc)}
|
|
439
466
|
try:
|
|
440
467
|
if self.facet is None and (
|
|
441
468
|
self._payload is not None or self.data is not None or self.layers
|
|
@@ -22,6 +22,7 @@ from plot3.masking import (
|
|
|
22
22
|
is_mask_transformer,
|
|
23
23
|
is_tidy3_backtick_transformer,
|
|
24
24
|
plot3_backtick_transform,
|
|
25
|
+
plot3_layer_transform,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
# R-style bare-name / backtick masking for aes() / facet_wrap().
|
|
@@ -463,6 +464,28 @@ def run_plot3_from_magic(line: str = ""):
|
|
|
463
464
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
464
465
|
|
|
465
466
|
|
|
467
|
+
def enable_layer_lines(ipython: Any | None = None) -> bool:
|
|
468
|
+
"""Let ``+ geom_point()`` lines continue the ggplot above them in a cell.
|
|
469
|
+
|
|
470
|
+
``ggplot(df, aes(...))`` then ``+ geom_point()`` on the next lines, with
|
|
471
|
+
no parentheses, as in R. Only a ``+`` followed by a plot3 name joins, so
|
|
472
|
+
ordinary Python is never rewritten.
|
|
473
|
+
"""
|
|
474
|
+
if ipython is None:
|
|
475
|
+
try:
|
|
476
|
+
ipython = get_ipython() if get_ipython is not None else None
|
|
477
|
+
except Exception:
|
|
478
|
+
ipython = None
|
|
479
|
+
transformers = getattr(ipython, "input_transformers_cleanup", None)
|
|
480
|
+
if not isinstance(transformers, list):
|
|
481
|
+
return False
|
|
482
|
+
transformers[:] = [
|
|
483
|
+
t for t in transformers if getattr(t, "__name__", "") != "plot3_layer_transform"
|
|
484
|
+
]
|
|
485
|
+
transformers.append(plot3_layer_transform)
|
|
486
|
+
return True
|
|
487
|
+
|
|
488
|
+
|
|
466
489
|
def register_plot3(*, quiet=True, r_style: bool = True) -> bool:
|
|
467
490
|
if get_ipython is None:
|
|
468
491
|
return False
|
|
@@ -495,6 +518,12 @@ def register_plot3(*, quiet=True, r_style: bool = True) -> bool:
|
|
|
495
518
|
ns[name] = getattr(plot3_pkg, name)
|
|
496
519
|
except Exception:
|
|
497
520
|
pass
|
|
521
|
+
# ggplot(...) then "+ geom_point()" lines, without parentheses
|
|
522
|
+
try:
|
|
523
|
+
enable_layer_lines(ip)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
if not quiet:
|
|
526
|
+
print(f"plot3: multi-line layers not enabled: {e}")
|
|
498
527
|
# R-style bare names / backticks in aes() (ggplot2 parity with tidy3)
|
|
499
528
|
if r_style:
|
|
500
529
|
try:
|
|
@@ -179,6 +179,138 @@ def plot3_backtick_transform(lines: list[str]) -> list[str]:
|
|
|
179
179
|
return parts or [out]
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
_OPENERS = frozenset("([{")
|
|
183
|
+
_CLOSERS = frozenset(")]}")
|
|
184
|
+
_SKIP_TOKENS = frozenset({
|
|
185
|
+
tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, tokenize.INDENT,
|
|
186
|
+
tokenize.DEDENT, tokenize.ENCODING, tokenize.ENDMARKER,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _plot3_names() -> set[str]:
|
|
191
|
+
try:
|
|
192
|
+
import plot3
|
|
193
|
+
|
|
194
|
+
return {str(name) for name in getattr(plot3, "__all__", ())}
|
|
195
|
+
except Exception:
|
|
196
|
+
return set()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _logical_lines(text: str) -> list[list[tokenize.TokenInfo]] | None:
|
|
200
|
+
"""Token lists, one per logical line; ``None`` if *text* cannot tokenize."""
|
|
201
|
+
lines: list[list[tokenize.TokenInfo]] = []
|
|
202
|
+
current: list[tokenize.TokenInfo] = []
|
|
203
|
+
try:
|
|
204
|
+
for token in tokenize.generate_tokens(io.StringIO(text).readline):
|
|
205
|
+
if token.type in _SKIP_TOKENS:
|
|
206
|
+
if token.type == tokenize.NEWLINE and current:
|
|
207
|
+
lines.append(current)
|
|
208
|
+
current = []
|
|
209
|
+
continue
|
|
210
|
+
current.append(token)
|
|
211
|
+
except (tokenize.TokenError, IndentationError, SyntaxError):
|
|
212
|
+
return None
|
|
213
|
+
if current:
|
|
214
|
+
lines.append(current)
|
|
215
|
+
return lines
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _value_start(tokens: list[tokenize.TokenInfo]) -> tuple[int, int]:
|
|
219
|
+
"""Where the expression starts: after a top-level ``=``, else the start."""
|
|
220
|
+
depth = 0
|
|
221
|
+
for index, token in enumerate(tokens):
|
|
222
|
+
if token.string in _OPENERS:
|
|
223
|
+
depth += 1
|
|
224
|
+
elif token.string in _CLOSERS:
|
|
225
|
+
depth -= 1
|
|
226
|
+
elif depth == 0 and token.type == tokenize.OP and token.string == "=":
|
|
227
|
+
if index + 1 < len(tokens):
|
|
228
|
+
return tokens[index + 1].start
|
|
229
|
+
return tokens[0].start
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def join_layer_lines(source: str) -> str | None:
|
|
233
|
+
"""Join ``+`` lines that add plot3 layers to the statement above them.
|
|
234
|
+
|
|
235
|
+
In a notebook, ``ggplot(df, aes(...))`` followed by lines that start
|
|
236
|
+
with ``+ geom_point()`` reads like R, but to Python each ``+`` line is
|
|
237
|
+
a statement of its own (unary plus, or an IndentationError when
|
|
238
|
+
indented). A line whose first token is ``+`` followed by a plot3 name
|
|
239
|
+
continues the top-level statement above it, across comments and blank
|
|
240
|
+
lines; the expression gets parentheses on the lines as typed, so error
|
|
241
|
+
line numbers still match. Returns ``None`` when nothing needs joining,
|
|
242
|
+
or when the result would not parse. Pipes (``>>``) are tidy3's.
|
|
243
|
+
"""
|
|
244
|
+
if not source or "+" not in source:
|
|
245
|
+
return None
|
|
246
|
+
raw = source.split("\n")
|
|
247
|
+
if not any(line.lstrip().startswith("+") for line in raw):
|
|
248
|
+
return None
|
|
249
|
+
names = _plot3_names()
|
|
250
|
+
# Indented "+ geom_point()" lines must not trip the tokenizer.
|
|
251
|
+
shift = [len(line) - len(line.lstrip()) if line.lstrip().startswith("+") else 0 for line in raw]
|
|
252
|
+
analysed = "\n".join(line[n:] for line, n in zip(raw, shift))
|
|
253
|
+
logical = _logical_lines(analysed)
|
|
254
|
+
if not logical:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def is_layer(tokens: list[tokenize.TokenInfo]) -> bool:
|
|
258
|
+
return (
|
|
259
|
+
len(tokens) > 1
|
|
260
|
+
and tokens[0].type == tokenize.OP
|
|
261
|
+
and tokens[0].string == "+"
|
|
262
|
+
and tokens[1].type == tokenize.NAME
|
|
263
|
+
and tokens[1].string in names
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
groups: list[list[list[tokenize.TokenInfo]]] = []
|
|
267
|
+
for tokens in logical:
|
|
268
|
+
previous = groups[-1] if groups else None
|
|
269
|
+
head = previous[0][0] if previous else None
|
|
270
|
+
if (
|
|
271
|
+
previous is not None
|
|
272
|
+
and is_layer(tokens)
|
|
273
|
+
and head.start[1] == 0
|
|
274
|
+
and head.type != tokenize.OP
|
|
275
|
+
and head.string not in {"if", "for", "while", "with", "def", "class", "try", "elif", "else", "except", "finally", "return", "import", "from"}
|
|
276
|
+
):
|
|
277
|
+
previous.append(tokens)
|
|
278
|
+
else:
|
|
279
|
+
groups.append([tokens])
|
|
280
|
+
|
|
281
|
+
edits: list[tuple[int, int, str]] = []
|
|
282
|
+
for group in groups:
|
|
283
|
+
if len(group) < 2:
|
|
284
|
+
continue
|
|
285
|
+
opening = _value_start(group[0])
|
|
286
|
+
closing = group[-1][-1].end
|
|
287
|
+
edits.append((opening[0], opening[1] + shift[opening[0] - 1], "("))
|
|
288
|
+
edits.append((closing[0], closing[1] + shift[closing[0] - 1], ")"))
|
|
289
|
+
if not edits:
|
|
290
|
+
return None
|
|
291
|
+
lines = list(raw)
|
|
292
|
+
for row, col, piece in sorted(edits, reverse=True):
|
|
293
|
+
line = lines[row - 1]
|
|
294
|
+
lines[row - 1] = line[:col] + piece + line[col:]
|
|
295
|
+
rewritten = "\n".join(lines)
|
|
296
|
+
try:
|
|
297
|
+
ast.parse(rewrite_backticks(rewritten))
|
|
298
|
+
except SyntaxError:
|
|
299
|
+
return None
|
|
300
|
+
return rewritten
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def plot3_layer_transform(lines: list[str]) -> list[str]:
|
|
304
|
+
"""IPython input transformer: ``+`` lines continue a ggplot (see join_layer_lines)."""
|
|
305
|
+
if not lines:
|
|
306
|
+
return lines
|
|
307
|
+
source = "".join(lines)
|
|
308
|
+
out = join_layer_lines(source)
|
|
309
|
+
if out is None:
|
|
310
|
+
return lines
|
|
311
|
+
return out.splitlines(keepends=True)
|
|
312
|
+
|
|
313
|
+
|
|
182
314
|
def _is_bt_call(node: ast.AST) -> bool:
|
|
183
315
|
return (
|
|
184
316
|
isinstance(node, ast.Call)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plot3
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.7
|
|
4
4
|
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -260,6 +260,17 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
|
|
|
260
260
|
- **Jupyter / SolveIt**: bare column names and backticks work in `aes()`,
|
|
261
261
|
`facet_wrap()`, and `facet_grid()` (`aes(x=`First Name`)`). Toggle with
|
|
262
262
|
`enable_r_style()` / `disable_r_style()`. `.py` files keep quoted strings.
|
|
263
|
+
- **Layers on their own lines**, as in R, with no parentheses:
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
ggplot(gapminder, aes(x=gdpPercap, y=lifeExp, colour=continent))
|
|
267
|
+
+ geom_point(alpha=0.7) # comments are fine
|
|
268
|
+
+ scale_x_log10()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
A line starting with `+` and a plot3 function continues the plot above
|
|
272
|
+
it. Import plot3 in an earlier cell: IPython reads a whole cell before
|
|
273
|
+
running any of it. `.py` files keep the outer parentheses.
|
|
263
274
|
- **SolveIt** draws figures inline and hides their HTML from the model's
|
|
264
275
|
context (`autohide(False)` to opt out).
|
|
265
276
|
- **VS Code notebooks** block WebGL in output cells, so plot3 opens figures
|
|
@@ -671,3 +671,33 @@ def test_swept_parameter_is_not_frozen_into_labels():
|
|
|
671
671
|
)
|
|
672
672
|
assert "t = 0" not in spec
|
|
673
673
|
assert "(a = 2)" in spec # fixed coefficients still show their value
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def test_default_slider_steps_shrink_to_fit():
|
|
677
|
+
from plot3 import geom_function
|
|
678
|
+
|
|
679
|
+
g = (
|
|
680
|
+
ggplot()
|
|
681
|
+
+ geom_function("y = a*(x-h)^2 + k", xlim=(-5, 5))
|
|
682
|
+
+ slider(a=(0.2, 3), h=(-3, 3), k=(-3, 3))
|
|
683
|
+
)
|
|
684
|
+
spec, _ = build_spec(g)
|
|
685
|
+
assert [p["n"] for p in spec["slider"]["params"]] == [9, 9, 9]
|
|
686
|
+
assert spec["slider"]["nFrames"] == 729
|
|
687
|
+
# The figure's own slider keeps its default for later renders.
|
|
688
|
+
assert g.slider.steps == 25
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def test_render_error_is_shown_instead_of_the_bare_repr():
|
|
692
|
+
from plot3 import geom_function
|
|
693
|
+
|
|
694
|
+
g = (
|
|
695
|
+
ggplot()
|
|
696
|
+
+ geom_function("y = a x + k")
|
|
697
|
+
+ slider(a=(0, 1), k=(0, 1), steps=2000)
|
|
698
|
+
)
|
|
699
|
+
bundle = g._repr_mimebundle_()
|
|
700
|
+
assert "plot3-error" in bundle["text/html"]
|
|
701
|
+
assert "smaller steps" in bundle["text/html"]
|
|
702
|
+
assert "ValueError" in bundle["text/plain"]
|
|
703
|
+
assert "smaller steps" in g._repr_html_()
|
|
@@ -197,3 +197,44 @@ def test_aes_expression_over_columns_passes_whole():
|
|
|
197
197
|
# Notebook variables and modules keep the old behaviour.
|
|
198
198
|
assert apply_masking("aes(x=np.log(x))", known=known) == "aes(x=np.log('x'))"
|
|
199
199
|
assert apply_masking("aes(x=wt)", known=known) == "aes(x='wt')"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_plus_lines_continue_a_ggplot():
|
|
203
|
+
# ggplot(...) then "+ geom_point()" lines, as in R: to Python each "+"
|
|
204
|
+
# line is its own statement (unary plus, or an IndentationError).
|
|
205
|
+
import ast
|
|
206
|
+
|
|
207
|
+
from plot3.masking import join_layer_lines
|
|
208
|
+
|
|
209
|
+
cell = (
|
|
210
|
+
'ggplot(df, aes(x="a", y="b"))\n'
|
|
211
|
+
' + geom_point(alpha=0.7)\n'
|
|
212
|
+
' + scale_x_log10()'
|
|
213
|
+
)
|
|
214
|
+
joined = join_layer_lines(cell)
|
|
215
|
+
assert joined == (
|
|
216
|
+
'(ggplot(df, aes(x="a", y="b"))\n'
|
|
217
|
+
' + geom_point(alpha=0.7)\n'
|
|
218
|
+
' + scale_x_log10())'
|
|
219
|
+
)
|
|
220
|
+
tree = ast.parse(joined)
|
|
221
|
+
assert len(tree.body) == 1
|
|
222
|
+
|
|
223
|
+
# An assignment, with comments and a blank line between layers.
|
|
224
|
+
cell = 'p = ggplot(df) # base\n# layers\n+ geom_point() # points\n\n+ theme_bw()\np'
|
|
225
|
+
joined = join_layer_lines(cell)
|
|
226
|
+
assert joined.startswith("p = (ggplot(df)") and "+ theme_bw())" in joined
|
|
227
|
+
assert len(ast.parse(joined).body) == 2
|
|
228
|
+
|
|
229
|
+
# patchwork: a composition takes "+ plot_layout(...)" lines too.
|
|
230
|
+
assert join_layer_lines("fig = (a | b)\n+ plot_layout(widths=[3, 2])").startswith("fig = ((a | b)")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_plus_lines_leave_other_python_alone():
|
|
234
|
+
from plot3.masking import join_layer_lines
|
|
235
|
+
|
|
236
|
+
assert join_layer_lines("x = 1\n+2") is None # unary plus
|
|
237
|
+
assert join_layer_lines("y = 1\n+ foo(2)") is None # not a plot3 name
|
|
238
|
+
assert join_layer_lines("df\n>> ggplot()\n+ geom_point()") is None # tidy3's pipe
|
|
239
|
+
assert join_layer_lines("for i in r:\n p = ggplot(df)\n + geom_point()") is None
|
|
240
|
+
assert join_layer_lines("p = ggplot(df) + geom_point()") is None # already one line
|
plot3-0.6.5/plot3/__version__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|