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.
Files changed (81) hide show
  1. {plot3-0.5.0 → plot3-0.6.0}/PKG-INFO +1 -1
  2. plot3-0.6.0/plot3/__version__.py +1 -0
  3. {plot3-0.5.0 → plot3-0.6.0}/plot3/build.py +32 -7
  4. {plot3-0.5.0 → plot3-0.6.0}/plot3/geoms.py +18 -7
  5. {plot3-0.5.0 → plot3-0.6.0}/plot3/masking.py +26 -2
  6. {plot3-0.5.0 → plot3-0.6.0}/plot3/stat2d.py +1 -0
  7. {plot3-0.5.0 → plot3-0.6.0}/plot3/static.py +1 -1
  8. {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/PKG-INFO +1 -1
  9. {plot3-0.5.0 → plot3-0.6.0}/tests/test_facets_compose.py +46 -0
  10. {plot3-0.5.0 → plot3-0.6.0}/tests/test_grammar.py +38 -0
  11. {plot3-0.5.0 → plot3-0.6.0}/tests/test_masking.py +14 -0
  12. plot3-0.5.0/plot3/__version__.py +0 -1
  13. {plot3-0.5.0 → plot3-0.6.0}/LICENSE +0 -0
  14. {plot3-0.5.0 → plot3-0.6.0}/README.md +0 -0
  15. {plot3-0.5.0 → plot3-0.6.0}/plot3/__init__.py +0 -0
  16. {plot3-0.5.0 → plot3-0.6.0}/plot3/aesexpr.py +0 -0
  17. {plot3-0.5.0 → plot3-0.6.0}/plot3/calculus.py +0 -0
  18. {plot3-0.5.0 → plot3-0.6.0}/plot3/compose.py +0 -0
  19. {plot3-0.5.0 → plot3-0.6.0}/plot3/contour.py +0 -0
  20. {plot3-0.5.0 → plot3-0.6.0}/plot3/craft.py +0 -0
  21. {plot3-0.5.0 → plot3-0.6.0}/plot3/encode.py +0 -0
  22. {plot3-0.5.0 → plot3-0.6.0}/plot3/expr.py +0 -0
  23. {plot3-0.5.0 → plot3-0.6.0}/plot3/flip.py +0 -0
  24. {plot3-0.5.0 → plot3-0.6.0}/plot3/function.py +0 -0
  25. {plot3-0.5.0 → plot3-0.6.0}/plot3/ggplot.py +0 -0
  26. {plot3-0.5.0 → plot3-0.6.0}/plot3/io.py +0 -0
  27. {plot3-0.5.0 → plot3-0.6.0}/plot3/jupyter.py +0 -0
  28. {plot3-0.5.0 → plot3-0.6.0}/plot3/latexin.py +0 -0
  29. {plot3-0.5.0 → plot3-0.6.0}/plot3/mathtext.py +0 -0
  30. {plot3-0.5.0 → plot3-0.6.0}/plot3/payload.py +0 -0
  31. {plot3-0.5.0 → plot3-0.6.0}/plot3/remote.py +0 -0
  32. {plot3-0.5.0 → plot3-0.6.0}/plot3/scales.py +0 -0
  33. {plot3-0.5.0 → plot3-0.6.0}/plot3/scaling.py +0 -0
  34. {plot3-0.5.0 → plot3-0.6.0}/plot3/special.py +0 -0
  35. {plot3-0.5.0 → plot3-0.6.0}/plot3/stats3d.py +0 -0
  36. {plot3-0.5.0 → plot3-0.6.0}/plot3/table.py +0 -0
  37. {plot3-0.5.0 → plot3-0.6.0}/plot3/themes.py +0 -0
  38. {plot3-0.5.0 → plot3-0.6.0}/plot3/viewer.py +0 -0
  39. {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/SOURCES.txt +0 -0
  40. {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/dependency_links.txt +0 -0
  41. {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/requires.txt +0 -0
  42. {plot3-0.5.0 → plot3-0.6.0}/plot3.egg-info/top_level.txt +0 -0
  43. {plot3-0.5.0 → plot3-0.6.0}/pyproject.toml +0 -0
  44. {plot3-0.5.0 → plot3-0.6.0}/setup.cfg +0 -0
  45. {plot3-0.5.0 → plot3-0.6.0}/tests/test_2d_density.py +0 -0
  46. {plot3-0.5.0 → plot3-0.6.0}/tests/test_3d.py +0 -0
  47. {plot3-0.5.0 → plot3-0.6.0}/tests/test_animation.py +0 -0
  48. {plot3-0.5.0 → plot3-0.6.0}/tests/test_annotate.py +0 -0
  49. {plot3-0.5.0 → plot3-0.6.0}/tests/test_api_public.py +0 -0
  50. {plot3-0.5.0 → plot3-0.6.0}/tests/test_array_backend.py +0 -0
  51. {plot3-0.5.0 → plot3-0.6.0}/tests/test_arrow.py +0 -0
  52. {plot3-0.5.0 → plot3-0.6.0}/tests/test_build_contract.py +0 -0
  53. {plot3-0.5.0 → plot3-0.6.0}/tests/test_contour.py +0 -0
  54. {plot3-0.5.0 → plot3-0.6.0}/tests/test_everyday_ggplot2.py +0 -0
  55. {plot3-0.5.0 → plot3-0.6.0}/tests/test_expr.py +0 -0
  56. {plot3-0.5.0 → plot3-0.6.0}/tests/test_geom_function.py +0 -0
  57. {plot3-0.5.0 → plot3-0.6.0}/tests/test_geoms_ds.py +0 -0
  58. {plot3-0.5.0 → plot3-0.6.0}/tests/test_ggplot2_extras2.py +0 -0
  59. {plot3-0.5.0 → plot3-0.6.0}/tests/test_ggplot2_parity.py +0 -0
  60. {plot3-0.5.0 → plot3-0.6.0}/tests/test_ggplot_extras.py +0 -0
  61. {plot3-0.5.0 → plot3-0.6.0}/tests/test_latex_input.py +0 -0
  62. {plot3-0.5.0 → plot3-0.6.0}/tests/test_lidar_scene.py +0 -0
  63. {plot3-0.5.0 → plot3-0.6.0}/tests/test_math_layers.py +0 -0
  64. {plot3-0.5.0 → plot3-0.6.0}/tests/test_mathtext.py +0 -0
  65. {plot3-0.5.0 → plot3-0.6.0}/tests/test_payload.py +0 -0
  66. {plot3-0.5.0 → plot3-0.6.0}/tests/test_payload_display.py +0 -0
  67. {plot3-0.5.0 → plot3-0.6.0}/tests/test_point_cloud.py +0 -0
  68. {plot3-0.5.0 → plot3-0.6.0}/tests/test_r_oracle_parity.py +0 -0
  69. {plot3-0.5.0 → plot3-0.6.0}/tests/test_readme.py +0 -0
  70. {plot3-0.5.0 → plot3-0.6.0}/tests/test_remote_bridge.py +0 -0
  71. {plot3-0.5.0 → plot3-0.6.0}/tests/test_review_fixes.py +0 -0
  72. {plot3-0.5.0 → plot3-0.6.0}/tests/test_save_menu.py +0 -0
  73. {plot3-0.5.0 → plot3-0.6.0}/tests/test_scales_api.py +0 -0
  74. {plot3-0.5.0 → plot3-0.6.0}/tests/test_scales_themes.py +0 -0
  75. {plot3-0.5.0 → plot3-0.6.0}/tests/test_special.py +0 -0
  76. {plot3-0.5.0 → plot3-0.6.0}/tests/test_stat2d.py +0 -0
  77. {plot3-0.5.0 → plot3-0.6.0}/tests/test_static_save.py +0 -0
  78. {plot3-0.5.0 → plot3-0.6.0}/tests/test_stats_semantic.py +0 -0
  79. {plot3-0.5.0 → plot3-0.6.0}/tests/test_step3_geoms.py +0 -0
  80. {plot3-0.5.0 → plot3-0.6.0}/tests/test_table_backends.py +0 -0
  81. {plot3-0.5.0 → plot3-0.6.0}/tests/test_titles_theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -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 = _apply_fill(dict(base_mapping), geom.kind)
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 = _apply_fill(dict(g.mapping), geom.kind)
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
- force = _global_numeric_domains(g) if facet.scales == "fixed" else {}
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) or ``"free"`` (per-panel).
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
- if scales not in {"fixed", "free"}:
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
- if scales not in {"fixed", "free"}:
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._mask_selector(a) for a in node.args]
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._mask_selector(kw.value))
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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -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')"
@@ -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