ggplot2-python 4.0.2.9000__tar.gz → 4.0.2.9000.post1__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 (56) hide show
  1. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/PKG-INFO +3 -1
  2. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/README.md +2 -0
  3. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/__init__.py +1 -1
  4. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/plot_render.py +147 -12
  5. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/theme_elements.py +14 -4
  6. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/pyproject.toml +1 -1
  7. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/.gitattributes +0 -0
  8. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/.gitignore +0 -0
  9. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/LICENSE +0 -0
  10. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/_compat.py +0 -0
  11. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/_plugins.py +0 -0
  12. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/_utils.py +0 -0
  13. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/aes.py +0 -0
  14. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/annotation.py +0 -0
  15. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/coord.py +0 -0
  16. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/coords/__init__.py +0 -0
  17. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/datasets.py +0 -0
  18. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/draw_key.py +0 -0
  19. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/facet.py +0 -0
  20. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/fortify.py +0 -0
  21. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/geom.py +0 -0
  22. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/geoms/__init__.py +0 -0
  23. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/ggproto.py +0 -0
  24. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/guide.py +0 -0
  25. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/guide_axis.py +0 -0
  26. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/guide_colourbar.py +0 -0
  27. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/guide_legend.py +0 -0
  28. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/guides/__init__.py +0 -0
  29. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/labeller.py +0 -0
  30. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/labels.py +0 -0
  31. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/layer.py +0 -0
  32. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/layout.py +0 -0
  33. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/limits.py +0 -0
  34. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/plot.py +0 -0
  35. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/position.py +0 -0
  36. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/protocols.py +0 -0
  37. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/py.typed +0 -0
  38. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/qplot.py +0 -0
  39. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/diamonds.csv +0 -0
  40. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/economics.csv +0 -0
  41. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/economics_long.csv +0 -0
  42. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/faithfuld.csv +0 -0
  43. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/luv_colours.csv +0 -0
  44. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/midwest.csv +0 -0
  45. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/mpg.csv +0 -0
  46. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/msleep.csv +0 -0
  47. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/presidential.csv +0 -0
  48. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/seals.csv +0 -0
  49. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/resources/txhousing.csv +0 -0
  50. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/save.py +0 -0
  51. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/scale.py +0 -0
  52. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/scales/__init__.py +0 -0
  53. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/stat.py +0 -0
  54. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/stats/__init__.py +0 -0
  55. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/theme.py +0 -0
  56. {ggplot2_python-4.0.2.9000 → ggplot2_python-4.0.2.9000.post1}/ggplot2_py/theme_defaults.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ggplot2-python
3
- Version: 4.0.2.9000
3
+ Version: 4.0.2.9000.post1
4
4
  Summary: Python port of the R ggplot2 package (tracks R ggplot2 4.0.2.9000)
5
5
  Project-URL: Homepage, https://github.com/Bio-Babel/ggplot2-python
6
6
  Project-URL: Repository, https://github.com/Bio-Babel/ggplot2-python
@@ -50,6 +50,8 @@ Description-Content-Type: text/markdown
50
50
 
51
51
  # ggplot2_py <a href="https://github.com/R2pyBioinformatics/ggplot2_py"><img src="assets/ggplot2_py_logo.png" align="right" height="138" alt="ggplot2_py logo" /></a>
52
52
 
53
+ [![PyPI](https://img.shields.io/pypi/v/ggplot2-python)](https://pypi.org/project/ggplot2-python/)
54
+
53
55
  AI-assisted Python port of the R **ggplot2** package — Create Elegant Data Visualisations Using the Grammar of Graphics.
54
56
 
55
57
  ## Overview
@@ -1,5 +1,7 @@
1
1
  # ggplot2_py <a href="https://github.com/R2pyBioinformatics/ggplot2_py"><img src="assets/ggplot2_py_logo.png" align="right" height="138" alt="ggplot2_py logo" /></a>
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/ggplot2-python)](https://pypi.org/project/ggplot2-python/)
4
+
3
5
  AI-assisted Python port of the R **ggplot2** package — Create Elegant Data Visualisations Using the Grammar of Graphics.
4
6
 
5
7
  ## Overview
@@ -7,7 +7,7 @@ approach to creating statistical visualizations.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- __version__ = "4.0.2.9000"
10
+ __version__ = "4.0.2.9000.post1"
11
11
  __r_commit__ = "c02c05a"
12
12
 
13
13
  # ---------------------------------------------------------------------------
@@ -55,6 +55,130 @@ def _legend_label_width_cm(labels: List[Any], fontsize: float = 6.0) -> float:
55
55
  return max(max_w, 0.3) # minimum width 0.3 cm
56
56
 
57
57
 
58
+ # ---------------------------------------------------------------------------
59
+ # Layer-to-guide filtering (ports of R's matched_aes / include_layer_in_guide,
60
+ # ``ggplot2/R/guides-.R:871-912``).
61
+ #
62
+ # R's GuideLegend$process_layers only forwards layers whose aesthetic
63
+ # mapping actually maps one of the guide's aesthetics, unless the user
64
+ # explicitly set ``show.legend=TRUE``. Without this filter, a legend
65
+ # picks up ``draw_key`` from any convenient layer (e.g. a backbone
66
+ # ``geom_segment`` with a fixed black colour) and renders black path
67
+ # glyphs instead of the colour-scale dots it should show.
68
+ # ---------------------------------------------------------------------------
69
+
70
+ _AES_SYNONYMS: Dict[str, str] = {"color": "colour"}
71
+
72
+
73
+ def _canon_aes(name: str) -> str:
74
+ return _AES_SYNONYMS.get(name, name)
75
+
76
+
77
+ def _aes_key_set(obj: Any) -> set:
78
+ """Return the canonicalised set of aesthetic names in a mapping-like obj."""
79
+ if obj is None:
80
+ return set()
81
+ try:
82
+ keys = obj.keys() if hasattr(obj, "keys") else list(obj)
83
+ except Exception:
84
+ return set()
85
+ return {_canon_aes(str(k)) for k in keys}
86
+
87
+
88
+ def _matched_aes(layer: Any, guide_aes: set) -> set:
89
+ """Port of R's ``matched_aes`` (``guides-.R:871-880``).
90
+
91
+ Returns the canonical aesthetic names that are *mapped* by this
92
+ layer's ``aes()`` and also part of the guide's key columns, excluding
93
+ aesthetics that are fixed (``aes_params``/``computed_geom_params``).
94
+ """
95
+ mapping_keys = _aes_key_set(getattr(layer, "computed_mapping", None)
96
+ or getattr(layer, "mapping", None))
97
+ stat = getattr(layer, "stat", None)
98
+ stat_default = _aes_key_set(getattr(stat, "default_aes", None))
99
+ all_names = mapping_keys | stat_default
100
+
101
+ geom = getattr(layer, "geom", None)
102
+ geom_required = set()
103
+ geom_default = set()
104
+ if geom is not None:
105
+ req = getattr(geom, "required_aes", None)
106
+ if req is not None:
107
+ geom_required = {_canon_aes(str(a)) for a in req}
108
+ geom_default = _aes_key_set(getattr(geom, "default_aes", None))
109
+ geom_names = geom_required | geom_default
110
+ # R's rename_size shim: size-renaming geoms contribute to size
111
+ # legends even without mapping "size" explicitly.
112
+ if geom is not None and getattr(geom, "rename_size", False):
113
+ if "size" in all_names and "linewidth" not in all_names:
114
+ geom_names = geom_names | {"size"}
115
+
116
+ matched = (all_names & geom_names) & {_canon_aes(a) for a in guide_aes}
117
+ matched -= _aes_key_set(getattr(layer, "computed_geom_params", None))
118
+ matched -= _aes_key_set(getattr(layer, "aes_params", None))
119
+ return matched
120
+
121
+
122
+ def _include_layer_in_guide(layer: Any, matched: set) -> bool:
123
+ """Port of R's ``include_layer_in_guide`` (``guides-.R:885-912``)."""
124
+ show = getattr(layer, "show_legend", None)
125
+ # Non-logical values: R warns and treats as FALSE. Python accepts
126
+ # None (= NA) and bool; anything else is coerced to False.
127
+ if show is not None and not isinstance(show, (bool, np.bool_)):
128
+ # Named-dict form (``show.legend=c(colour=TRUE)``) — uncommon in
129
+ # ggplot2_py but supported for completeness.
130
+ if isinstance(show, dict):
131
+ if not matched:
132
+ return False
133
+ picks = {_canon_aes(k): v for k, v in show.items()}
134
+ vals = [picks[a] for a in matched if a in picks and picks[a] is not None]
135
+ return len(vals) == 0 or any(vals)
136
+ return False
137
+
138
+ if matched:
139
+ # Layer maps at least one of the guide's aesthetics:
140
+ # include unless show.legend is explicitly FALSE.
141
+ if show is None:
142
+ return True
143
+ return bool(show)
144
+ # Layer does not map any guide aesthetic: include only if show.legend
145
+ # is explicitly TRUE.
146
+ return show is True
147
+
148
+
149
+ def _resolve_draw_key_for_entry(
150
+ entry: Dict[str, Any], layers: Any,
151
+ ) -> tuple[Any, List[Any]]:
152
+ """Pick the ``draw_key`` and layer subset for a single legend entry.
153
+
154
+ Mirrors R's ``GuideLegend$process_layers`` filtering combined with
155
+ ``get_layer_key``'s first-layer-wins behaviour for glyph selection.
156
+ Returns ``(draw_key_fn, included_layers)`` — the included layer
157
+ list is forwarded to ``build_legend_decor`` so ``aes_params`` /
158
+ ``default_aes`` resolution also uses only qualifying layers.
159
+ """
160
+ from ggplot2_py.draw_key import draw_key_point as _draw_key_point
161
+
162
+ guide_aes = {_canon_aes(a) for a in (entry.get("aes_mapped") or {}).keys()}
163
+ if not guide_aes:
164
+ guide_aes = {_canon_aes(str(entry.get("aesthetic", "")))}
165
+
166
+ included: List[Any] = []
167
+ if layers:
168
+ for layer in layers:
169
+ matched = _matched_aes(layer, guide_aes)
170
+ if _include_layer_in_guide(layer, matched):
171
+ included.append(layer)
172
+
173
+ draw_key_fn = _draw_key_point
174
+ for layer in included:
175
+ geom = getattr(layer, "geom", None)
176
+ if geom is not None and hasattr(geom, "draw_key"):
177
+ draw_key_fn = geom.draw_key
178
+ break
179
+ return draw_key_fn, included
180
+
181
+
58
182
  @singledispatch
59
183
  def ggplot_gtable(data: Any) -> Any:
60
184
  """Convert a built ggplot to a gtable for rendering.
@@ -362,16 +486,11 @@ def _table_add_legends(
362
486
  PADDING_CM = 0.15 # R: legend.margin default padding
363
487
 
364
488
  # ------------------------------------------------------------------
365
- # 4. Determine draw_key function from layers
489
+ # 4. ``draw_key`` is now resolved per-entry inside the loop below
490
+ # (R's ``GuideLegend$process_layers`` filters layers against each
491
+ # guide's aesthetics via ``matched_aes`` / ``include_layer_in_guide``,
492
+ # ``guide-legend.R:219-231``). See ``_resolve_draw_key_for_entry``.
366
493
  # ------------------------------------------------------------------
367
- from ggplot2_py.draw_key import draw_key_point as _draw_key_point
368
- draw_key_fn = _draw_key_point
369
- if layers:
370
- for layer in layers:
371
- geom = getattr(layer, "geom", None)
372
- if geom is not None and hasattr(geom, "draw_key"):
373
- draw_key_fn = geom.draw_key
374
- break
375
494
 
376
495
  # ------------------------------------------------------------------
377
496
  # 5. Build each guide as an independent Gtable
@@ -537,11 +656,27 @@ def _table_add_legends(
537
656
  continue
538
657
 
539
658
  # --- Legend path: discrete scales ---
540
- nrow = min(n_breaks, 20)
541
- ncol = 1
659
+ # Mirror R ``GuideLegend$setup_params`` (``guide-legend.R:286-298``):
660
+ # vertical direction defaults to ``ncol = ceiling(n_breaks / 20)``,
661
+ # then ``nrow = ceiling(n_breaks / ncol)``. Previously hardcoded
662
+ # ``ncol = 1`` caused any legend with more than 20 entries to pile
663
+ # all wrapped entries into a single physical column (multi-column
664
+ # positions were computed by ``arrange_legend_layout`` but only
665
+ # one column width was allocated in the gtable), producing
666
+ # overlapping key + label glyphs per row.
667
+ ncol = max(1, math.ceil(n_breaks / 20))
668
+ nrow = max(1, math.ceil(n_breaks / ncol))
669
+
670
+ # Per-entry draw_key: mirror R's ``matched_aes`` /
671
+ # ``include_layer_in_guide`` so ``geom_segment`` / ``geom_path``
672
+ # layers that don't map the guide's aesthetic can't hijack the
673
+ # legend key glyph.
674
+ entry_draw_key_fn, entry_layers = _resolve_draw_key_for_entry(
675
+ entry, layers,
676
+ )
542
677
 
543
678
  decor = build_legend_decor(
544
- entry, draw_key_fn, layers,
679
+ entry, entry_draw_key_fn, entry_layers,
545
680
  key_width_cm=KEY_W_CM, key_height_cm=KEY_H_CM,
546
681
  theme=theme,
547
682
  )
@@ -1425,16 +1425,26 @@ class _TitleGrob(GTree):
1425
1425
  self._title_widths = widths
1426
1426
  self._title_heights = heights
1427
1427
 
1428
- # R: widthDetails.titleGrob sum(x$widths)
1428
+ # R: widthDetails.titleGrob <- function(x) sum(x$widths)
1429
+ # (``ggplot2/R/margins.R:199-201``). R's ``sum()`` on a unit vector
1430
+ # dispatches to ``Summary.unit`` which wraps the full vector in a
1431
+ # *single-element* L_SUM compound (``grid/R/unit.R:300-347``). That
1432
+ # is what ``evaluateGrobUnit`` expects at ``grid/src/unit.c:535``,
1433
+ # where it reads ``transformWidthtoINCHES(unitx, 0, ...)``. Using
1434
+ # Python's builtin ``sum`` here would leave a multi-element unit
1435
+ # unchanged (because ``0 + Unit`` short-circuits to identity),
1436
+ # which makes grid_py read only the left-margin element and
1437
+ # underestimate the grob's width.
1429
1438
  def width_details(self) -> Any:
1430
1439
  if self._title_widths is not None:
1431
- return sum(self._title_widths)
1440
+ from grid_py import unit_summary_sum
1441
+ return unit_summary_sum(self._title_widths)
1432
1442
  return Unit(0, "cm")
1433
1443
 
1434
- # R: heightDetails.titleGrob → sum(x$heights)
1435
1444
  def height_details(self) -> Any:
1436
1445
  if self._title_heights is not None:
1437
- return sum(self._title_heights)
1446
+ from grid_py import unit_summary_sum
1447
+ return unit_summary_sum(self._title_heights)
1438
1448
  return Unit(0, "cm")
1439
1449
 
1440
1450
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ggplot2-python"
7
- version = "4.0.2.9000"
7
+ version = "4.0.2.9000.post1"
8
8
  description = "Python port of the R ggplot2 package (tracks R ggplot2 4.0.2.9000)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"