plot3 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plot3-0.5.0 → plot3-0.6.0}/PKG-INFO +1 -1
- plot3-0.6.0/plot3/__version__.py +1 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/build.py +32 -7
- {plot3-0.5.0 → plot3-0.6.0}/plot3/geoms.py +18 -7
- {plot3-0.5.0 → plot3-0.6.0}/plot3/masking.py +26 -2
- {plot3-0.5.0 → plot3-0.6.0}/plot3/stat2d.py +1 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/static.py +1 -1
- {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/PKG-INFO +1 -1
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_facets_compose.py +46 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_grammar.py +38 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_masking.py +14 -0
- plot3-0.5.0/plot3/__version__.py +0 -1
- {plot3-0.5.0 → plot3-0.6.0}/LICENSE +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/README.md +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/__init__.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/aesexpr.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/calculus.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/compose.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/contour.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/craft.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/encode.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/expr.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/flip.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/function.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/ggplot.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/io.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/jupyter.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/latexin.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/mathtext.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/payload.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/remote.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/scales.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/scaling.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/special.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/stats3d.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/table.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/themes.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3/viewer.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/SOURCES.txt +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/dependency_links.txt +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/requires.txt +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/top_level.txt +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/pyproject.toml +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/setup.cfg +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_2d_density.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_3d.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_animation.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_annotate.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_api_public.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_array_backend.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_arrow.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_build_contract.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_contour.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_everyday_ggplot2.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_expr.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_geom_function.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_geoms_ds.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_ggplot2_extras2.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_ggplot2_parity.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_ggplot_extras.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_latex_input.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_lidar_scene.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_math_layers.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_mathtext.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_payload.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_payload_display.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_point_cloud.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_r_oracle_parity.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_readme.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_remote_bridge.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_review_fixes.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_save_menu.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_scales_api.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_scales_themes.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_special.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_stat2d.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_static_save.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_stats_semantic.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_step3_geoms.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_table_backends.py +0 -0
- {plot3-0.5.0 → plot3-0.6.0}/tests/test_titles_theme.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -440,6 +440,22 @@ def _apply_fill(mapping: dict, kind: str) -> dict:
|
|
|
440
440
|
return out
|
|
441
441
|
|
|
442
442
|
|
|
443
|
+
def _layer_mapping(base_mapping, geom) -> dict:
|
|
444
|
+
"""The plot's mapping with the layer's own on top, fill resolved per side.
|
|
445
|
+
|
|
446
|
+
A set colour (``geom_point(colour="black")`` under ``aes(colour=g)``)
|
|
447
|
+
replaces the inherited colour mapping for that layer, as in ggplot2, so
|
|
448
|
+
the layer neither recolours nor needs the column. Filled layers keep
|
|
449
|
+
it: there ``colour=`` is the outline.
|
|
450
|
+
"""
|
|
451
|
+
kind = getattr(geom, "kind", None)
|
|
452
|
+
mapping = _apply_fill(dict(base_mapping), kind)
|
|
453
|
+
if getattr(geom, "const_color", None) is not None and kind not in _FILL_KINDS:
|
|
454
|
+
mapping.pop("color", None)
|
|
455
|
+
mapping.update(_apply_fill(dict(geom.mapping), kind))
|
|
456
|
+
return mapping
|
|
457
|
+
|
|
458
|
+
|
|
443
459
|
_REF_KINDS = frozenset({"hline", "vline", "abline"})
|
|
444
460
|
_MM_TO_PX = 96.0 / 25.4
|
|
445
461
|
|
|
@@ -571,8 +587,7 @@ def expand_stat_geom(
|
|
|
571
587
|
layers = expand_vector_field(geom, transition, slider)
|
|
572
588
|
return layers[0] if len(layers) == 1 else layers
|
|
573
589
|
# Resolve fill per mapping, so a layer's own colour beats a base fill.
|
|
574
|
-
mapping =
|
|
575
|
-
mapping.update(_apply_fill(dict(geom.mapping), geom.kind))
|
|
590
|
+
mapping = _layer_mapping(base_mapping, geom)
|
|
576
591
|
if geom.kind == "box3d":
|
|
577
592
|
from plot3.stats3d import box3d_layers
|
|
578
593
|
|
|
@@ -1964,8 +1979,7 @@ def build_spec(g: ggplot) -> tuple[dict, list[tuple[str, str]]]:
|
|
|
1964
1979
|
if getattr(geom, "_replace_mapping", False):
|
|
1965
1980
|
m = dict(geom.mapping)
|
|
1966
1981
|
else:
|
|
1967
|
-
m =
|
|
1968
|
-
m.update(_apply_fill(dict(geom.mapping), geom.kind))
|
|
1982
|
+
m = _layer_mapping(g.mapping, geom)
|
|
1969
1983
|
if "x" not in m or "y" not in m:
|
|
1970
1984
|
raise ValueError("aes(x=, y=) are required (bar/histogram/density supply y)")
|
|
1971
1985
|
if geom.kind == "surface" and "z" not in m:
|
|
@@ -3717,7 +3731,13 @@ def facet_cells(g: ggplot) -> dict:
|
|
|
3717
3731
|
facet = g.facet
|
|
3718
3732
|
if g.data is None:
|
|
3719
3733
|
raise ValueError("ggplot has no data")
|
|
3720
|
-
|
|
3734
|
+
# ggplot2: "fixed" shares every axis, "free" none, "free_x" / "free_y"
|
|
3735
|
+
# all but that one.
|
|
3736
|
+
freed = {"fixed": set(), "free_x": {"x"}, "free_y": {"y"}}.get(facet.scales)
|
|
3737
|
+
force = (
|
|
3738
|
+
{ax: dom for ax, dom in _global_numeric_domains(g).items() if ax not in freed}
|
|
3739
|
+
if freed is not None else {}
|
|
3740
|
+
)
|
|
3721
3741
|
colour_levels = _facet_colour_levels(g)
|
|
3722
3742
|
|
|
3723
3743
|
def child(panel):
|
|
@@ -3731,6 +3751,9 @@ def facet_cells(g: ggplot) -> dict:
|
|
|
3731
3751
|
header = {
|
|
3732
3752
|
key: g.labs.get(key) for key in ("title", "subtitle", "caption", "tag") if g.labs.get(key)
|
|
3733
3753
|
}
|
|
3754
|
+
# Panels hide their own legends; the figure draws one unless
|
|
3755
|
+
# theme(legend_position="none") asks for none.
|
|
3756
|
+
legend = _legend_position_spec(getattr(g, "legend_position", None))
|
|
3734
3757
|
cells: list[dict] = []
|
|
3735
3758
|
if not isinstance(facet, _facet_grid):
|
|
3736
3759
|
column = facet.variable
|
|
@@ -3750,7 +3773,8 @@ def facet_cells(g: ggplot) -> dict:
|
|
|
3750
3773
|
row, col = divmod(index, ncol)
|
|
3751
3774
|
cells.append({"row": row, "col": col, "fig": panel, "strip": label})
|
|
3752
3775
|
return {"ncol": ncol, "nrow": nrow, "cells": cells, "col_strips": None,
|
|
3753
|
-
"row_strips": None, "header": header, "kind": "wrap"
|
|
3776
|
+
"row_strips": None, "header": header, "kind": "wrap",
|
|
3777
|
+
"legend": legend}
|
|
3754
3778
|
|
|
3755
3779
|
for column in (facet.rows, facet.cols):
|
|
3756
3780
|
if column is not None and not has_column(g.data, column):
|
|
@@ -3779,6 +3803,7 @@ def facet_cells(g: ggplot) -> dict:
|
|
|
3779
3803
|
"row_strips": [_strip_text(facet, facet.rows, v) for v in row_levels] if facet.rows else None,
|
|
3780
3804
|
"header": header,
|
|
3781
3805
|
"kind": "grid",
|
|
3806
|
+
"legend": legend,
|
|
3782
3807
|
}
|
|
3783
3808
|
|
|
3784
3809
|
|
|
@@ -3860,7 +3885,7 @@ def _build_doc_faceted(g: ggplot, facet) -> str:
|
|
|
3860
3885
|
first_spec, _pairs = build_spec(first)
|
|
3861
3886
|
shared_x = str((first_spec.get("labs") or {}).get("x") or "")
|
|
3862
3887
|
shared_y = str((first_spec.get("labs") or {}).get("y") or "")
|
|
3863
|
-
legend_html = _facet_legend_html(first_spec, theme)
|
|
3888
|
+
legend_html = "" if layout.get("legend") == "none" else _facet_legend_html(first_spec, theme)
|
|
3864
3889
|
cells: list[str] = []
|
|
3865
3890
|
total_kb = 0
|
|
3866
3891
|
count = 0
|
|
@@ -1184,13 +1184,14 @@ class geom_smooth(_Geom):
|
|
|
1184
1184
|
``method="loess"`` (default, local quadratic with ``span``) or ``"lm"``
|
|
1185
1185
|
(a straight line). ``se=True`` shades the ``level`` confidence band,
|
|
1186
1186
|
using Student's t like R. ``aes(colour=)`` fits each group.
|
|
1187
|
+
``linetype`` dashes the fitted line (``"dashed"``, ``"dotted"``, …).
|
|
1187
1188
|
"""
|
|
1188
1189
|
|
|
1189
1190
|
kind = "smooth"
|
|
1190
1191
|
|
|
1191
1192
|
def __init__(
|
|
1192
1193
|
self, mapping=None, *, method="loess", se=True, level=0.95,
|
|
1193
|
-
span=0.75, n=80, linewidth=2.0, **kw,
|
|
1194
|
+
span=0.75, n=80, linewidth=2.0, linetype=None, **kw,
|
|
1194
1195
|
):
|
|
1195
1196
|
super().__init__(mapping, **kw)
|
|
1196
1197
|
self.method = method
|
|
@@ -1199,6 +1200,8 @@ class geom_smooth(_Geom):
|
|
|
1199
1200
|
self.span = float(span)
|
|
1200
1201
|
self.n = int(n)
|
|
1201
1202
|
self.linewidth = float(linewidth)
|
|
1203
|
+
dash_pattern(linetype)
|
|
1204
|
+
self.linetype = linetype
|
|
1202
1205
|
|
|
1203
1206
|
|
|
1204
1207
|
class stat_summary(_Geom):
|
|
@@ -1751,6 +1754,14 @@ class geom_violin(_Geom):
|
|
|
1751
1754
|
self.linewidth = float(linewidth)
|
|
1752
1755
|
|
|
1753
1756
|
|
|
1757
|
+
_FACET_SCALES = ("fixed", "free", "free_x", "free_y")
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
def _check_facet_scales(scales) -> None:
|
|
1761
|
+
if scales not in _FACET_SCALES:
|
|
1762
|
+
raise ValueError("scales must be 'fixed', 'free', 'free_x', or 'free_y'")
|
|
1763
|
+
|
|
1764
|
+
|
|
1754
1765
|
class facet_wrap:
|
|
1755
1766
|
"""Wrap panels by a discrete column (ggplot2 ``facet_wrap``).
|
|
1756
1767
|
|
|
@@ -1762,7 +1773,8 @@ class facet_wrap:
|
|
|
1762
1773
|
Panel grid size. If both are omitted, the grid is ggplot2's: 3 panels
|
|
1763
1774
|
in a row, 4 in 2 x 2, 5 or 6 in 2 rows of 3, 7 to 9 in 3 x 3.
|
|
1764
1775
|
scales:
|
|
1765
|
-
``"fixed"`` (shared domains across panels)
|
|
1776
|
+
``"fixed"`` (shared domains across panels), ``"free"`` (per-panel),
|
|
1777
|
+
or ``"free_x"`` / ``"free_y"`` (only that axis per-panel).
|
|
1766
1778
|
"""
|
|
1767
1779
|
|
|
1768
1780
|
def __init__(
|
|
@@ -1792,8 +1804,7 @@ class facet_wrap:
|
|
|
1792
1804
|
f"(got {facets!r})"
|
|
1793
1805
|
)
|
|
1794
1806
|
name = candidates[0]
|
|
1795
|
-
|
|
1796
|
-
raise ValueError("scales must be 'fixed' or 'free'")
|
|
1807
|
+
_check_facet_scales(scales)
|
|
1797
1808
|
if ncol is not None and ncol < 1:
|
|
1798
1809
|
raise ValueError("ncol must be positive")
|
|
1799
1810
|
if nrow is not None and nrow < 1:
|
|
@@ -1812,7 +1823,8 @@ class facet_grid:
|
|
|
1812
1823
|
``facet_grid(rows="sex", cols="day")`` or the formula ``"sex ~ day"``
|
|
1813
1824
|
(``". ~ day"`` for columns only). Column labels sit above the top row and
|
|
1814
1825
|
row labels to the right, as in ggplot2. ``scales="fixed"`` (default)
|
|
1815
|
-
shares axes across panels; ``"free"`` lets each panel fit its data
|
|
1826
|
+
shares axes across panels; ``"free"`` lets each panel fit its data, and
|
|
1827
|
+
``"free_x"`` / ``"free_y"`` free one axis only.
|
|
1816
1828
|
"""
|
|
1817
1829
|
|
|
1818
1830
|
def __init__(self, facets: str | None = None, *, rows=None, cols=None, scales: str = "fixed",
|
|
@@ -1839,8 +1851,7 @@ class facet_grid:
|
|
|
1839
1851
|
cols = _as_column_name(cols) if cols is not None else None
|
|
1840
1852
|
if rows is None and cols is None:
|
|
1841
1853
|
raise ValueError("facet_grid() needs rows=, cols=, or both")
|
|
1842
|
-
|
|
1843
|
-
raise ValueError("scales must be 'fixed' or 'free'")
|
|
1854
|
+
_check_facet_scales(scales)
|
|
1844
1855
|
self.rows = rows
|
|
1845
1856
|
self.cols = cols
|
|
1846
1857
|
self.scales = scales
|
|
@@ -263,6 +263,30 @@ class Plot3MaskTransformer(ast.NodeTransformer):
|
|
|
263
263
|
def _mask_selector(self, node: ast.AST) -> ast.AST:
|
|
264
264
|
return MaskSelectors(self._known()).visit(node)
|
|
265
265
|
|
|
266
|
+
def _mask_aes_value(self, node: ast.AST) -> ast.AST:
|
|
267
|
+
"""``colour = factor(cyl)`` → ``colour = "factor(cyl)"``.
|
|
268
|
+
|
|
269
|
+
aes() reads expressions over columns as strings (``"log10(pop)"``,
|
|
270
|
+
``"dose > 5"``). An expression whose bare names are all columns is
|
|
271
|
+
passed whole; quoting each name (``factor("cyl")``) breaks it.
|
|
272
|
+
Expressions over notebook variables or modules are left alone.
|
|
273
|
+
"""
|
|
274
|
+
if isinstance(node, (ast.Name, ast.Constant)) or _is_bt_call(node):
|
|
275
|
+
return self._mask_selector(node)
|
|
276
|
+
known = self._known()
|
|
277
|
+
called = {
|
|
278
|
+
id(call.func) for call in ast.walk(node)
|
|
279
|
+
if isinstance(call, ast.Call) and isinstance(call.func, ast.Name)
|
|
280
|
+
}
|
|
281
|
+
names = [
|
|
282
|
+
item for item in ast.walk(node)
|
|
283
|
+
if isinstance(item, ast.Name) and id(item) not in called
|
|
284
|
+
]
|
|
285
|
+
backticks = any(_is_bt_call(item) for item in ast.walk(node))
|
|
286
|
+
if names and not backticks and all(item.id not in known for item in names):
|
|
287
|
+
return ast.copy_location(ast.Constant(value=ast.unparse(node)), node)
|
|
288
|
+
return self._mask_selector(node)
|
|
289
|
+
|
|
266
290
|
def visit_Call(self, node: ast.Call) -> ast.AST:
|
|
267
291
|
# Always recurse so nested aes(...) inside geom_point(aes(...)) is seen.
|
|
268
292
|
if not isinstance(node.func, ast.Name):
|
|
@@ -276,9 +300,9 @@ class Plot3MaskTransformer(ast.NodeTransformer):
|
|
|
276
300
|
|
|
277
301
|
if name == "aes":
|
|
278
302
|
# All positional + keyword values are column selectors.
|
|
279
|
-
node.args = [self.
|
|
303
|
+
node.args = [self._mask_aes_value(a) for a in node.args]
|
|
280
304
|
node.keywords = [
|
|
281
|
-
ast.keyword(arg=kw.arg, value=self.
|
|
305
|
+
ast.keyword(arg=kw.arg, value=self._mask_aes_value(kw.value))
|
|
282
306
|
for kw in node.keywords
|
|
283
307
|
]
|
|
284
308
|
return node
|
|
@@ -609,6 +609,7 @@ def smooth(geom: _Geom, mapping: dict, data: Any):
|
|
|
609
609
|
curve = _layer(
|
|
610
610
|
"line", pd.concat(lines, ignore_index=True), line_map, geom,
|
|
611
611
|
_groups=starts, linewidth=float(getattr(geom, "linewidth", 2.0) or 2.0),
|
|
612
|
+
linetype=getattr(geom, "linetype", None),
|
|
612
613
|
)
|
|
613
614
|
_title(curve, "y", ycol)
|
|
614
615
|
_title(curve, "x", xcol)
|
|
@@ -659,7 +659,7 @@ def _draw_facets(panels, layout, first_spec, theme, width, height, base_pt, comm
|
|
|
659
659
|
# One legend for the figure, from the first panel (colour levels are
|
|
660
660
|
# shared, so every panel has the same keys).
|
|
661
661
|
legend_spec = dict(first_spec, legendPosition="right")
|
|
662
|
-
metrics = _legend_metrics(
|
|
662
|
+
metrics = None if layout.get("legend") == "none" else _legend_metrics(
|
|
663
663
|
legend_spec, theme, fonts, raw_labs.get("color") or "",
|
|
664
664
|
max_box=(width * 0.3, height - head - foot),
|
|
665
665
|
)
|
|
@@ -163,3 +163,49 @@ def test_composition_saves_static_and_html(tmp_path, plots):
|
|
|
163
163
|
assert doc.count("<iframe") == 3
|
|
164
164
|
assert "Overview" in doc
|
|
165
165
|
assert re.search(r"flex-direction:row", doc) and re.search(r"flex-direction:column", doc)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _facet_frame():
|
|
169
|
+
import pandas as pd
|
|
170
|
+
|
|
171
|
+
return pd.DataFrame({
|
|
172
|
+
"g": ["a"] * 3 + ["b"] * 3,
|
|
173
|
+
"x": [1, 2, 3, 10, 20, 30],
|
|
174
|
+
"y": [1.0, 2.0, 3.0, 100.0, 200.0, 300.0],
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_facet_free_y_frees_only_y():
|
|
179
|
+
from plot3 import aes, facet_wrap, geom_point, ggplot
|
|
180
|
+
from plot3.build import facet_cells
|
|
181
|
+
|
|
182
|
+
layout = facet_cells(ggplot(_facet_frame(), aes(x="x", y="y")) + geom_point() + facet_wrap("g", scales="free_y"))
|
|
183
|
+
forced = [cell["fig"]._force_scales for cell in layout["cells"]]
|
|
184
|
+
assert all(set(f) == {"x"} and f["x"] == (1.0, 30.0) for f in forced)
|
|
185
|
+
layout = facet_cells(ggplot(_facet_frame(), aes(x="x", y="y")) + geom_point() + facet_wrap("g", scales="free_x"))
|
|
186
|
+
assert all(set(cell["fig"]._force_scales) == {"y"} for cell in layout["cells"])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_facet_scales_rejects_unknown_values():
|
|
190
|
+
import pytest
|
|
191
|
+
|
|
192
|
+
from plot3 import facet_grid, facet_wrap
|
|
193
|
+
|
|
194
|
+
with pytest.raises(ValueError, match="free_y"):
|
|
195
|
+
facet_wrap("g", scales="loose")
|
|
196
|
+
facet_grid("g ~ .", scales="free_x")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_facet_legend_position_none_hides_the_figure_legend(tmp_path):
|
|
200
|
+
from plot3 import aes, facet_wrap, geom_point, ggplot, ggsave, theme
|
|
201
|
+
from plot3.build import build_doc
|
|
202
|
+
|
|
203
|
+
p = (ggplot(_facet_frame(), aes(x="x", y="y", colour="g")) + geom_point()
|
|
204
|
+
+ facet_wrap("g") + theme(legend_position="none"))
|
|
205
|
+
assert "id='flegend'" not in build_doc(p)
|
|
206
|
+
out = tmp_path / "f.svg"
|
|
207
|
+
ggsave(str(out), p)
|
|
208
|
+
assert ">a<" in out.read_text() # strip label
|
|
209
|
+
shown = tmp_path / "shown.svg"
|
|
210
|
+
ggsave(str(shown), ggplot(_facet_frame(), aes(x="x", y="y", colour="g")) + geom_point() + facet_wrap("g"))
|
|
211
|
+
assert shown.read_text().count(">a<") > out.read_text().count(">a<") # legend key too
|
|
@@ -97,3 +97,41 @@ def test_scale_colour_viridis_on_points(cloud):
|
|
|
97
97
|
def test_unknown_add_raises(cars):
|
|
98
98
|
with pytest.raises(TypeError, match="cannot add"):
|
|
99
99
|
_ = ggplot(cars, aes(x="wt", y="mpg")) + 123
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_set_colour_replaces_inherited_colour_mapping():
|
|
103
|
+
# ggplot2: geom_point(colour = "black") under aes(colour = g) draws black,
|
|
104
|
+
# and the layer's own data need not have g.
|
|
105
|
+
import pandas as pd
|
|
106
|
+
|
|
107
|
+
from plot3 import aes, geom_boxplot, geom_point, ggplot
|
|
108
|
+
from plot3.build import build_spec
|
|
109
|
+
|
|
110
|
+
df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3], "g": ["a", "b", "c"]})
|
|
111
|
+
centre = pd.DataFrame({"x": [2], "y": [2]})
|
|
112
|
+
spec, _ = build_spec(
|
|
113
|
+
ggplot(df, aes(x="x", y="y", colour="g")) + geom_point()
|
|
114
|
+
+ geom_point(data=centre, colour="black", shape="cross")
|
|
115
|
+
)
|
|
116
|
+
assert spec["layers"][0].get("color") is not None
|
|
117
|
+
assert spec["layers"][1].get("color") is None
|
|
118
|
+
assert spec["layers"][1]["constColor"] == "#000000"
|
|
119
|
+
# A filled layer keeps its mapped fill under a set outline colour.
|
|
120
|
+
spec, _ = build_spec(
|
|
121
|
+
ggplot(df, aes(x="g", y="y", fill="g")) + geom_boxplot(colour="black")
|
|
122
|
+
)
|
|
123
|
+
assert spec["layers"][0].get("color") is not None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_geom_smooth_takes_a_linetype():
|
|
127
|
+
import pandas as pd
|
|
128
|
+
import pytest
|
|
129
|
+
|
|
130
|
+
from plot3 import aes, geom_smooth, ggplot
|
|
131
|
+
from plot3.build import build_spec
|
|
132
|
+
|
|
133
|
+
df = pd.DataFrame({"x": [1, 2, 3, 4], "y": [1.0, 2.5, 2.9, 4.2]})
|
|
134
|
+
spec, _ = build_spec(ggplot(df, aes(x="x", y="y")) + geom_smooth(method="lm", se=False, linetype="dashed"))
|
|
135
|
+
assert [layer.get("dash") for layer in spec["layers"] if layer["kind"] == "line"] == [[4.0, 4.0]]
|
|
136
|
+
with pytest.raises(ValueError):
|
|
137
|
+
geom_smooth(linetype="wavy")
|
|
@@ -183,3 +183,17 @@ def test_facet_wrap_masked_end_to_end(cars):
|
|
|
183
183
|
fig = ggplot(cars, aes(x="wt", y="mpg")) + geom_point() + fac
|
|
184
184
|
html = fig._repr_html_()
|
|
185
185
|
assert "iframe" in html or "three" in html.lower() or len(html) > 100
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_aes_expression_over_columns_passes_whole():
|
|
189
|
+
# aes(colour = factor(cyl)) in a notebook: aes() reads "factor(cyl)";
|
|
190
|
+
# quoting each name (factor("cyl")) would break it.
|
|
191
|
+
from plot3.masking import apply_masking, default_known_names
|
|
192
|
+
|
|
193
|
+
known = default_known_names({"np", "scale"})
|
|
194
|
+
assert apply_masking("aes(colour=factor(cyl))", known=known) == "aes(colour='factor(cyl)')"
|
|
195
|
+
assert apply_masking("aes(x=log10(pop), y=lifeExp)", known=known) == "aes(x='log10(pop)', y='lifeExp')"
|
|
196
|
+
assert apply_masking("aes(colour=cyl > 4)", known=known) == "aes(colour='cyl > 4')"
|
|
197
|
+
# Notebook variables and modules keep the old behaviour.
|
|
198
|
+
assert apply_masking("aes(x=np.log(x))", known=known) == "aes(x=np.log('x'))"
|
|
199
|
+
assert apply_masking("aes(x=wt)", known=known) == "aes(x='wt')"
|
plot3-0.5.0/plot3/__version__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.0"
|
|
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
|
|
File without changes
|