ggplot2-python 4.0.2.9000.post4__tar.gz → 4.0.2.9000.post5__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.
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/PKG-INFO +6 -1
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/README.md +5 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/__init__.py +11 -3
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_compat.py +66 -9
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/_env.py +160 -0
- ggplot2_python-4.0.2.9000.post4/ggplot2_py/guide_axis.py → ggplot2_python-4.0.2.9000.post5/ggplot2_py/_guide_axis.py +20 -9
- ggplot2_python-4.0.2.9000.post4/ggplot2_py/guide_colourbar.py → ggplot2_python-4.0.2.9000.post5/ggplot2_py/_guide_colourbar.py +1 -1
- ggplot2_python-4.0.2.9000.post4/ggplot2_py/guide_legend.py → ggplot2_python-4.0.2.9000.post5/ggplot2_py/_guide_legend.py +22 -27
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/coord.py +3 -3
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/extension/__init__.py +365 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/facet.py +2 -2
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/geom.py +3 -6
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/ggproto.py +653 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/guide.py +645 -39
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/layer.py +8 -3
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/plot.py +169 -4
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/plot_render.py +58 -551
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/scale.py +111 -78
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/theme_elements.py +107 -54
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/pyproject.toml +1 -1
- ggplot2_python-4.0.2.9000.post4/ggplot2_py/ggproto.py +0 -329
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/.gitattributes +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/.gitignore +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/LICENSE +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_defaults.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_make_constructor.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_plugins.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_utils.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/aes.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/annotation.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/autoplot.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/coords/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/datasets.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/draw_key.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/fortify.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/geoms/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/guides/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/labeller.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/labels.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/layout.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/limits.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/position.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/protocols.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/py.typed +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/qplot.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/diamonds.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/economics.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/economics_long.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/faithfuld.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/luv_colours.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/midwest.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/mpg.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/msleep.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/presidential.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/seals.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/txhousing.csv +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/save.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/scales/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/stat.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/stats/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/theme.py +0 -0
- {ggplot2_python-4.0.2.9000.post4 → ggplot2_python-4.0.2.9000.post5}/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.post5
|
|
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
|
|
@@ -218,5 +218,10 @@ ggplot2-python is designed as an **extensible platform**. The following table su
|
|
|
218
218
|
| Custom `+` types | `@update_ggplot.register(MyClass)` | Register any Python class for the `+` operator |
|
|
219
219
|
| Custom plot types | `@ggplot_build.register(MyPlot)` | Override the entire build pipeline |
|
|
220
220
|
| Build hooks | `plot.add_build_hook(timing, stage, fn)` | Intercept data at any pipeline stage |
|
|
221
|
+
| Per-plot `+` hooks | `register_pre_add_hook(plot, hook)` | Per-plot transformer fires on the next `+`; supports stateful, self-removing hooks (R: `+.<dynamic_class>`) |
|
|
222
|
+
| Plot-env constructor injection | `plot.plot_env.push({...})` | Override `find_scale` / `add_defaults` per-plot — install a custom `scale_<aes>_<type>` constructor (R: `find_global`) |
|
|
223
|
+
| Instance-as-parent ggproto | `ggproto("Name", instance, method=fn)` | Clone a Geom/Stat/Scale instance with method overrides (R: `ggproto(NULL, inst, method = fn)`) |
|
|
224
|
+
| Explicit method binding | `bind_method(obj, name, fn)` | Bind an arbitrary callable as a method when its first arg isn't named `self` |
|
|
225
|
+
| Extension toolkit | `from ggplot2_py.extension import …` | Pre-built helpers (`clone_layer`, `rename_aes_in_*`, `protect`, `palette_for_aes`, …) for cross-cutting extensions |
|
|
221
226
|
| Protocol validation | `isinstance(obj, GeomProtocol)` | Verify structural conformance |
|
|
222
227
|
| Scoped defaults | `with ggplot_defaults(theme=...):` | Thread-safe scoped defaults |
|
|
@@ -168,5 +168,10 @@ ggplot2-python is designed as an **extensible platform**. The following table su
|
|
|
168
168
|
| Custom `+` types | `@update_ggplot.register(MyClass)` | Register any Python class for the `+` operator |
|
|
169
169
|
| Custom plot types | `@ggplot_build.register(MyPlot)` | Override the entire build pipeline |
|
|
170
170
|
| Build hooks | `plot.add_build_hook(timing, stage, fn)` | Intercept data at any pipeline stage |
|
|
171
|
+
| Per-plot `+` hooks | `register_pre_add_hook(plot, hook)` | Per-plot transformer fires on the next `+`; supports stateful, self-removing hooks (R: `+.<dynamic_class>`) |
|
|
172
|
+
| Plot-env constructor injection | `plot.plot_env.push({...})` | Override `find_scale` / `add_defaults` per-plot — install a custom `scale_<aes>_<type>` constructor (R: `find_global`) |
|
|
173
|
+
| Instance-as-parent ggproto | `ggproto("Name", instance, method=fn)` | Clone a Geom/Stat/Scale instance with method overrides (R: `ggproto(NULL, inst, method = fn)`) |
|
|
174
|
+
| Explicit method binding | `bind_method(obj, name, fn)` | Bind an arbitrary callable as a method when its first arg isn't named `self` |
|
|
175
|
+
| Extension toolkit | `from ggplot2_py.extension import …` | Pre-built helpers (`clone_layer`, `rename_aes_in_*`, `protect`, `palette_for_aes`, …) for cross-cutting extensions |
|
|
171
176
|
| Protocol validation | `isinstance(obj, GeomProtocol)` | Verify structural conformance |
|
|
172
177
|
| Scoped defaults | `with ggplot_defaults(theme=...):` | Thread-safe scoped defaults |
|
|
@@ -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.post5"
|
|
11
11
|
__r_commit__ = "c02c05a"
|
|
12
12
|
|
|
13
13
|
# ---------------------------------------------------------------------------
|
|
@@ -23,7 +23,10 @@ from ggplot2_py.ggproto import (
|
|
|
23
23
|
ggproto,
|
|
24
24
|
ggproto_parent,
|
|
25
25
|
is_ggproto,
|
|
26
|
+
fetch_ggproto,
|
|
27
|
+
bind_method,
|
|
26
28
|
)
|
|
29
|
+
from ggplot2_py._env import PlotEnv
|
|
27
30
|
|
|
28
31
|
# ---------------------------------------------------------------------------
|
|
29
32
|
# Aesthetics
|
|
@@ -62,6 +65,8 @@ from ggplot2_py.plot import (
|
|
|
62
65
|
ggplotGrob,
|
|
63
66
|
ggplot_add,
|
|
64
67
|
add_gg,
|
|
68
|
+
register_pre_add_hook,
|
|
69
|
+
unregister_pre_add_hook,
|
|
65
70
|
get_last_plot,
|
|
66
71
|
set_last_plot,
|
|
67
72
|
last_plot,
|
|
@@ -734,7 +739,8 @@ __all__ = [
|
|
|
734
739
|
# Version
|
|
735
740
|
"__version__",
|
|
736
741
|
# Core
|
|
737
|
-
"GGProto", "ggproto", "ggproto_parent", "is_ggproto",
|
|
742
|
+
"GGProto", "ggproto", "ggproto_parent", "is_ggproto", "fetch_ggproto",
|
|
743
|
+
"bind_method", "PlotEnv",
|
|
738
744
|
"Waiver", "waiver", "is_waiver",
|
|
739
745
|
# Aesthetics
|
|
740
746
|
"aes", "after_stat", "after_scale", "stage", "vars",
|
|
@@ -745,7 +751,9 @@ __all__ = [
|
|
|
745
751
|
"Layer", "layer", "is_layer",
|
|
746
752
|
# Plot
|
|
747
753
|
"ggplot", "is_ggplot", "ggplot_build", "ggplot_gtable", "ggplotGrob",
|
|
748
|
-
"ggplot_add", "add_gg",
|
|
754
|
+
"ggplot_add", "add_gg",
|
|
755
|
+
"register_pre_add_hook", "unregister_pre_add_hook",
|
|
756
|
+
"get_last_plot", "set_last_plot", "last_plot",
|
|
749
757
|
"print_plot", "get_alt_text", "update_ggplot",
|
|
750
758
|
# Introspection
|
|
751
759
|
"get_layer_data", "get_layer_grob", "get_panel_scales",
|
|
@@ -8,6 +8,8 @@ ggplot2 relies on, adapted for idiomatic Python usage.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import importlib
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
11
13
|
import warnings
|
|
12
14
|
from typing import Any, NoReturn, Optional
|
|
13
15
|
|
|
@@ -41,6 +43,40 @@ __all__ = [
|
|
|
41
43
|
# ---------------------------------------------------------------------------
|
|
42
44
|
# CLI messaging helpers (rlang / cli replacements)
|
|
43
45
|
# ---------------------------------------------------------------------------
|
|
46
|
+
#
|
|
47
|
+
# R distinguishes three output channels:
|
|
48
|
+
#
|
|
49
|
+
# * ``cli::cli_abort`` — error stream (``stop``) → Python ``raise``
|
|
50
|
+
# * ``cli::cli_warn`` — warning stream (``warning``) → ``warnings.warn(..., UserWarning)``
|
|
51
|
+
# * ``cli::cli_inform``— message stream (``message``) → ``logging.INFO``
|
|
52
|
+
#
|
|
53
|
+
# In R these are three separate streams: ``suppressWarnings`` does not
|
|
54
|
+
# silence messages, ``suppressMessages`` does not silence warnings, and
|
|
55
|
+
# the test framework's ``expect_message`` / ``expect_warning`` discriminate
|
|
56
|
+
# between them. This Python port matches that split — informational
|
|
57
|
+
# output (e.g. ``ggsave``'s "Saving …", ``stat_bin``'s "using bins = 30")
|
|
58
|
+
# routes through the standard :mod:`logging` module at INFO level so
|
|
59
|
+
# pytest does not count it as a warning, ``warnings.catch_warnings``
|
|
60
|
+
# does not capture it, and ``logging.getLogger("ggplot2_py").setLevel(
|
|
61
|
+
# logging.WARNING)`` mirrors R's ``suppressMessages``.
|
|
62
|
+
|
|
63
|
+
#: Package-level logger. Library convention says "do not configure the
|
|
64
|
+
#: root logger" — instead we attach a single :class:`logging.StreamHandler`
|
|
65
|
+
#: to *this* logger and set :attr:`Logger.propagate` to ``False``, so the
|
|
66
|
+
#: handler runs even when the user has not called
|
|
67
|
+
#: :func:`logging.basicConfig` and ours never duplicates onto theirs.
|
|
68
|
+
_logger: logging.Logger = logging.getLogger("ggplot2_py")
|
|
69
|
+
if not _logger.handlers:
|
|
70
|
+
_handler = logging.StreamHandler(stream=sys.stderr)
|
|
71
|
+
# R's ``message()`` prints the bare message text (no level/timestamp
|
|
72
|
+
# prefix); mirror that to keep ggplot2_py output indistinguishable
|
|
73
|
+
# from the R original.
|
|
74
|
+
_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
75
|
+
_handler.setLevel(logging.INFO)
|
|
76
|
+
_logger.addHandler(_handler)
|
|
77
|
+
_logger.setLevel(logging.INFO)
|
|
78
|
+
_logger.propagate = False
|
|
79
|
+
|
|
44
80
|
|
|
45
81
|
def cli_abort(
|
|
46
82
|
message: str,
|
|
@@ -81,7 +117,13 @@ def cli_warn(
|
|
|
81
117
|
call: Optional[str] = None,
|
|
82
118
|
**kwargs: Any,
|
|
83
119
|
) -> None:
|
|
84
|
-
"""Issue a ``UserWarning``
|
|
120
|
+
"""Issue a ``UserWarning`` — R *warning* stream equivalent.
|
|
121
|
+
|
|
122
|
+
Counterpart of :func:`cli_inform`: this function targets the
|
|
123
|
+
Python warning stream (matching R ``cli::cli_warn`` / ``warning()``),
|
|
124
|
+
while informational output goes through :mod:`logging` at INFO
|
|
125
|
+
level. Keeping the two streams separate preserves R's
|
|
126
|
+
``suppressWarnings`` / ``suppressMessages`` granularity.
|
|
85
127
|
|
|
86
128
|
Parameters
|
|
87
129
|
----------
|
|
@@ -105,24 +147,39 @@ def cli_inform(
|
|
|
105
147
|
call: Optional[str] = None,
|
|
106
148
|
**kwargs: Any,
|
|
107
149
|
) -> None:
|
|
108
|
-
"""Emit an informational message
|
|
150
|
+
"""Emit an informational message at ``logging.INFO`` level.
|
|
151
|
+
|
|
152
|
+
R analogue: ``cli::cli_inform`` / ``rlang::inform`` write to R's
|
|
153
|
+
*message* stream — informational output that is **distinct from**
|
|
154
|
+
R's *warning* stream (see :func:`cli_warn`). Python's natural
|
|
155
|
+
equivalent is the :mod:`logging` module at INFO level:
|
|
109
156
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
``suppressMessages
|
|
157
|
+
* pytest does not count INFO records as warnings (its
|
|
158
|
+
``--strict-warnings`` flag and "warnings summary" only track
|
|
159
|
+
:mod:`warnings`);
|
|
160
|
+
* ``warnings.catch_warnings`` does not capture them;
|
|
161
|
+
* the user opt-out parallels R's ``suppressMessages`` —
|
|
162
|
+
``logging.getLogger("ggplot2_py").setLevel(logging.WARNING)``
|
|
163
|
+
silences cli_inform output without affecting cli_warn.
|
|
164
|
+
|
|
165
|
+
By default the package logger has a :class:`logging.StreamHandler`
|
|
166
|
+
attached at INFO level (R-faithful: ``cli_inform`` messages are
|
|
167
|
+
visible to stderr unless explicitly suppressed).
|
|
115
168
|
|
|
116
169
|
Parameters
|
|
117
170
|
----------
|
|
118
171
|
message : str
|
|
119
|
-
Informational message.
|
|
172
|
+
Informational message. May contain ``{name}``-style placeholders.
|
|
120
173
|
call : str, optional
|
|
121
174
|
Name of the calling function (unused; matches R signature).
|
|
122
175
|
**kwargs : Any
|
|
123
176
|
Substitution values for placeholders in *message*.
|
|
124
177
|
"""
|
|
125
|
-
|
|
178
|
+
try:
|
|
179
|
+
formatted = message.format(**kwargs) if kwargs else message
|
|
180
|
+
except (KeyError, IndexError):
|
|
181
|
+
formatted = message
|
|
182
|
+
_logger.info(formatted)
|
|
126
183
|
|
|
127
184
|
|
|
128
185
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlotEnv — layered namespace lookup for scale/guide constructors.
|
|
3
|
+
|
|
4
|
+
Port of R's ``find_global(name, env, mode = "function")`` (R ref:
|
|
5
|
+
``ggplot2/R/scale-type.R:39-54``). Quoting the R source:
|
|
6
|
+
|
|
7
|
+
Look for object first in parent environment and if not found, then in
|
|
8
|
+
ggplot2 namespace environment. This makes it possible to override
|
|
9
|
+
default scales by setting them in the parent environment.
|
|
10
|
+
|
|
11
|
+
In R, ``plot@plot_env`` is the environment where the plot was constructed
|
|
12
|
+
(typically ``parent.frame()`` at ``ggplot()`` call time), and a chain of
|
|
13
|
+
environments terminates in ``asNamespace("ggplot2")``. ggnewscale
|
|
14
|
+
exploits this by **injecting** ``scale_<bumped_aes>_<type>`` functions
|
|
15
|
+
into ``plot@plot_env`` (R ref:
|
|
16
|
+
``ggnewscale/R/rename-aes.R:87-104``), so that when build-time
|
|
17
|
+
default-scale resolution runs, the injected constructor wins.
|
|
18
|
+
|
|
19
|
+
In Python, the closest equivalent is a list-of-namespaces walked in
|
|
20
|
+
order, with the ``ggplot2_py.scales`` module as the implicit fallback
|
|
21
|
+
layer. :class:`PlotEnv` encapsulates that — call sites pass a
|
|
22
|
+
``PlotEnv`` to :func:`find_scale` / ``ScalesList.add_defaults`` /
|
|
23
|
+
``ScalesList.add_missing``, and lookups walk the user-pushed layers
|
|
24
|
+
first.
|
|
25
|
+
|
|
26
|
+
A *namespace* is anything that responds to attribute access **or**
|
|
27
|
+
mapping ``__getitem__``. ``dict``, ``types.ModuleType``,
|
|
28
|
+
``types.SimpleNamespace``, ``argparse.Namespace``, and arbitrary
|
|
29
|
+
objects implementing ``__getattr__`` all work.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Any, Callable, List, Optional
|
|
35
|
+
|
|
36
|
+
__all__ = ["PlotEnv"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlotEnv:
|
|
40
|
+
"""Layered scale/guide-constructor lookup table for a plot.
|
|
41
|
+
|
|
42
|
+
Mirrors the *effective* behaviour of R's ``plot@plot_env`` + the
|
|
43
|
+
``find_global`` walk: lookups proceed through the user-pushed
|
|
44
|
+
layers in **last-pushed-first-checked** order (LIFO), then fall
|
|
45
|
+
back to the ``ggplot2_py.scales`` module.
|
|
46
|
+
|
|
47
|
+
Each layer may be any object that supports either attribute access
|
|
48
|
+
(``getattr(layer, name)``) or mapping access (``layer[name]``); the
|
|
49
|
+
first that yields a non-``None`` value wins.
|
|
50
|
+
|
|
51
|
+
Examples
|
|
52
|
+
--------
|
|
53
|
+
>>> env = PlotEnv()
|
|
54
|
+
>>> env.push({"scale_colour_continuous": my_factory})
|
|
55
|
+
>>> env.lookup("scale_colour_continuous") is my_factory
|
|
56
|
+
True
|
|
57
|
+
|
|
58
|
+
The implicit fallback to ``ggplot2_py.scales`` is intentionally
|
|
59
|
+
**not** included in :attr:`layers` so that :meth:`clone` produces
|
|
60
|
+
an env with the same user-visible chain.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, *layers: Any) -> None:
|
|
64
|
+
# Layers are stored in **push order**. Lookup walks them in
|
|
65
|
+
# reverse so that the most recently pushed layer wins, matching
|
|
66
|
+
# R's environment-chain semantics (the immediate parent is
|
|
67
|
+
# checked before more ancestral ones).
|
|
68
|
+
self._layers: List[Any] = [l for l in layers if l is not None]
|
|
69
|
+
|
|
70
|
+
# -- Mutation ----------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def push(self, layer: Any) -> None:
|
|
73
|
+
"""Add a new lookup layer. Last-pushed wins on conflicts.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
layer : object
|
|
78
|
+
Any dict-like or attribute-bearing namespace.
|
|
79
|
+
"""
|
|
80
|
+
if layer is None:
|
|
81
|
+
return
|
|
82
|
+
self._layers.append(layer)
|
|
83
|
+
|
|
84
|
+
def pop(self) -> Optional[Any]:
|
|
85
|
+
"""Remove and return the most-recently-pushed layer, or ``None``
|
|
86
|
+
if the chain is empty.
|
|
87
|
+
"""
|
|
88
|
+
if not self._layers:
|
|
89
|
+
return None
|
|
90
|
+
return self._layers.pop()
|
|
91
|
+
|
|
92
|
+
# -- Query -------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def lookup(self, name: str) -> Optional[Callable]:
|
|
95
|
+
"""Resolve *name* through the layer chain, then the
|
|
96
|
+
``ggplot2_py.scales`` fallback module.
|
|
97
|
+
|
|
98
|
+
Returns the first hit, or ``None`` if nothing matches.
|
|
99
|
+
|
|
100
|
+
Notes
|
|
101
|
+
-----
|
|
102
|
+
The fallback layer is searched **last** even when the lookup
|
|
103
|
+
chain is non-empty — matching R's
|
|
104
|
+
``c(env, list(as_namespace("ggplot2")))`` (R ref:
|
|
105
|
+
``scale-type.R:44``).
|
|
106
|
+
"""
|
|
107
|
+
for layer in reversed(self._layers):
|
|
108
|
+
val = _ns_get(layer, name)
|
|
109
|
+
if val is not None:
|
|
110
|
+
return val
|
|
111
|
+
# Fallback: the package's own scale-constructor module. Import
|
|
112
|
+
# lazily to avoid an import cycle with scale.py at load time —
|
|
113
|
+
# the cycle only matters during module construction; at call
|
|
114
|
+
# time every module is loaded so the import never raises.
|
|
115
|
+
from ggplot2_py import scales as _scales_mod
|
|
116
|
+
return getattr(_scales_mod, name, None)
|
|
117
|
+
|
|
118
|
+
def __contains__(self, name: str) -> bool:
|
|
119
|
+
return self.lookup(name) is not None
|
|
120
|
+
|
|
121
|
+
# -- Plumbing ----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def clone(self) -> "PlotEnv":
|
|
124
|
+
"""Return a new :class:`PlotEnv` with the same layer chain.
|
|
125
|
+
|
|
126
|
+
The layers themselves are **not** deep-copied — mirrors R env
|
|
127
|
+
semantics where ``plot_clone`` does not snapshot the
|
|
128
|
+
environment contents.
|
|
129
|
+
"""
|
|
130
|
+
new = PlotEnv()
|
|
131
|
+
new._layers = list(self._layers)
|
|
132
|
+
return new
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def layers(self) -> List[Any]:
|
|
136
|
+
"""Read-only view of the layer chain (in push order)."""
|
|
137
|
+
return list(self._layers)
|
|
138
|
+
|
|
139
|
+
def __repr__(self) -> str:
|
|
140
|
+
return f"<PlotEnv layers={len(self._layers)}>"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _ns_get(layer: Any, name: str) -> Any:
|
|
144
|
+
"""Resolve *name* on *layer*, trying mapping then attribute access.
|
|
145
|
+
|
|
146
|
+
Returns the found value, or ``None`` if absent. Catches only the
|
|
147
|
+
natural "key absent" / "unsupported indexing" cases — anything else
|
|
148
|
+
propagates so real bugs surface (per the project rule against
|
|
149
|
+
over-broad fallback logic).
|
|
150
|
+
"""
|
|
151
|
+
# Mapping path (dict and dict-like). ``str``/``bytes`` are
|
|
152
|
+
# excluded because their ``__getitem__`` is positional and would
|
|
153
|
+
# raise ``TypeError`` for non-integer keys.
|
|
154
|
+
if hasattr(layer, "__getitem__") and not isinstance(layer, (str, bytes)):
|
|
155
|
+
try:
|
|
156
|
+
return layer[name]
|
|
157
|
+
except (KeyError, TypeError):
|
|
158
|
+
pass
|
|
159
|
+
# Attribute path (modules, SimpleNamespace, regular objects).
|
|
160
|
+
return getattr(layer, name, None)
|
|
@@ -55,8 +55,8 @@ _PT: float = 72.27 / 25.4
|
|
|
55
55
|
# Helpers
|
|
56
56
|
# ---------------------------------------------------------------------------
|
|
57
57
|
|
|
58
|
-
def _unit_to_cm(u: Unit) -> float:
|
|
59
|
-
"""Convert a Unit (possibly compound/sum) to cm
|
|
58
|
+
def _unit_to_cm(u: Unit, axis: str = "height") -> float:
|
|
59
|
+
"""Convert a Unit (possibly compound/sum) to cm along *axis*.
|
|
60
60
|
|
|
61
61
|
``convert_height/width`` can't handle compound ``"sum"`` units
|
|
62
62
|
without a viewport context. This helper decomposes them into
|
|
@@ -70,8 +70,17 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
70
70
|
|
|
71
71
|
The ``data`` element is itself a multi-element Unit with the
|
|
72
72
|
individual operands.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
u : Unit
|
|
77
|
+
Unit to convert.
|
|
78
|
+
axis : {"height", "width"}
|
|
79
|
+
Axis to measure along — needed for viewport-relative units
|
|
80
|
+
(``"npc"``) which resolve differently per axis in non-square
|
|
81
|
+
viewports. Compound sums propagate the axis to children.
|
|
73
82
|
"""
|
|
74
|
-
from grid_py import convert_height
|
|
83
|
+
from grid_py import convert_height, convert_width
|
|
75
84
|
|
|
76
85
|
units = getattr(u, "_units", None)
|
|
77
86
|
values = getattr(u, "_values", None)
|
|
@@ -79,6 +88,8 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
79
88
|
if units is None or values is None:
|
|
80
89
|
return 0.0
|
|
81
90
|
|
|
91
|
+
fn = convert_width if axis == "width" else convert_height
|
|
92
|
+
|
|
82
93
|
total_cm = 0.0
|
|
83
94
|
n = len(u)
|
|
84
95
|
for i in range(n):
|
|
@@ -88,7 +99,7 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
88
99
|
# The operands are stored as a multi-element Unit in data[i]
|
|
89
100
|
inner = data[i] if data and i < len(data) else None
|
|
90
101
|
if inner is not None and isinstance(inner, Unit):
|
|
91
|
-
total_cm += _unit_to_cm(inner)
|
|
102
|
+
total_cm += _unit_to_cm(inner, axis)
|
|
92
103
|
continue
|
|
93
104
|
|
|
94
105
|
# Skip context-dependent units we can't resolve statically
|
|
@@ -99,7 +110,7 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
99
110
|
val = float(values[i]) if i < len(values) else 0.0
|
|
100
111
|
leaf = Unit(val, unit_type)
|
|
101
112
|
try:
|
|
102
|
-
cm =
|
|
113
|
+
cm = fn(leaf, "cm", valueOnly=True)
|
|
103
114
|
total_cm += float(np.sum(cm))
|
|
104
115
|
except Exception:
|
|
105
116
|
pass
|
|
@@ -125,12 +136,12 @@ def _width_cm(x: Any) -> float:
|
|
|
125
136
|
# For compound (sum) units, convert_width returns bogus results;
|
|
126
137
|
# decompose and convert leaf-by-leaf instead.
|
|
127
138
|
if _has_sum_unit(u):
|
|
128
|
-
return _unit_to_cm(u)
|
|
139
|
+
return _unit_to_cm(u, "width")
|
|
129
140
|
try:
|
|
130
141
|
result = convert_width(u, "cm", valueOnly=True)
|
|
131
142
|
return float(np.sum(result))
|
|
132
143
|
except Exception:
|
|
133
|
-
return _unit_to_cm(u)
|
|
144
|
+
return _unit_to_cm(u, "width")
|
|
134
145
|
|
|
135
146
|
|
|
136
147
|
def _height_cm(x: Any) -> float:
|
|
@@ -143,12 +154,12 @@ def _height_cm(x: Any) -> float:
|
|
|
143
154
|
else:
|
|
144
155
|
return 0.0
|
|
145
156
|
if _has_sum_unit(u):
|
|
146
|
-
return _unit_to_cm(u)
|
|
157
|
+
return _unit_to_cm(u, "height")
|
|
147
158
|
try:
|
|
148
159
|
result = convert_height(u, "cm", valueOnly=True)
|
|
149
160
|
return float(np.sum(result))
|
|
150
161
|
except Exception:
|
|
151
|
-
return _unit_to_cm(u)
|
|
162
|
+
return _unit_to_cm(u, "height")
|
|
152
163
|
|
|
153
164
|
|
|
154
165
|
# ---------------------------------------------------------------------------
|
|
@@ -681,7 +681,7 @@ def assemble_colourbar(
|
|
|
681
681
|
gt = gtable_add_grob(gt, label_tree, t=1, l=1, clip="off", name="labels")
|
|
682
682
|
|
|
683
683
|
# Add title
|
|
684
|
-
from ggplot2_py.
|
|
684
|
+
from ggplot2_py._guide_legend import add_legend_title
|
|
685
685
|
gt = add_legend_title(gt, title_grob, position="top")
|
|
686
686
|
|
|
687
687
|
# Add padding
|
|
@@ -25,6 +25,8 @@ from grid_py import (
|
|
|
25
25
|
Gpar,
|
|
26
26
|
Unit,
|
|
27
27
|
Viewport,
|
|
28
|
+
convert_height,
|
|
29
|
+
convert_width,
|
|
28
30
|
null_grob,
|
|
29
31
|
rect_grob,
|
|
30
32
|
text_grob,
|
|
@@ -930,8 +932,8 @@ def package_legend_box(
|
|
|
930
932
|
max_width_cm = 0.0
|
|
931
933
|
heights_cm: List[float] = []
|
|
932
934
|
for lg in legends:
|
|
933
|
-
max_width_cm = max(max_width_cm, _gtable_total_cm(lg.widths))
|
|
934
|
-
heights_cm.append(_gtable_total_cm(lg.heights))
|
|
935
|
+
max_width_cm = max(max_width_cm, _gtable_total_cm(lg.widths, "width"))
|
|
936
|
+
heights_cm.append(_gtable_total_cm(lg.heights, "height"))
|
|
935
937
|
guides = gtable_col(
|
|
936
938
|
name="guides",
|
|
937
939
|
grobs=legends,
|
|
@@ -944,8 +946,8 @@ def package_legend_box(
|
|
|
944
946
|
max_height_cm = 0.0
|
|
945
947
|
widths_cm: List[float] = []
|
|
946
948
|
for lg in legends:
|
|
947
|
-
max_height_cm = max(max_height_cm, _gtable_total_cm(lg.heights))
|
|
948
|
-
widths_cm.append(_gtable_total_cm(lg.widths))
|
|
949
|
+
max_height_cm = max(max_height_cm, _gtable_total_cm(lg.heights, "height"))
|
|
950
|
+
widths_cm.append(_gtable_total_cm(lg.widths, "width"))
|
|
949
951
|
guides = gtable_row(
|
|
950
952
|
name="guides",
|
|
951
953
|
grobs=legends,
|
|
@@ -1040,30 +1042,23 @@ def _interleave(
|
|
|
1040
1042
|
return result
|
|
1041
1043
|
|
|
1042
1044
|
|
|
1043
|
-
def _gtable_total_cm(unit: Optional[Unit]) -> float:
|
|
1044
|
-
"""Sum a Unit vector
|
|
1045
|
+
def _gtable_total_cm(unit: Optional[Unit], axis: str = "height") -> float:
|
|
1046
|
+
"""Sum a Unit vector along *axis* (``"height"`` or ``"width"``),
|
|
1047
|
+
returning cm as a float.
|
|
1045
1048
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1049
|
+
Delegates to :func:`grid_py.convert_height` / :func:`grid_py.convert_width`
|
|
1050
|
+
so font-relative units (``"lines"``, ``"char"``, ``"strwidth"``,
|
|
1051
|
+
``"strheight"``) resolve against the active gp / device — matching R's
|
|
1052
|
+
``sum(convertHeight(unit, "cm", valueOnly=TRUE))`` / ``convertWidth``.
|
|
1053
|
+
|
|
1054
|
+
Axis matters for viewport-relative units (``"npc"``): for a
|
|
1055
|
+
non-square viewport, ``convert_height`` and ``convert_width`` resolve
|
|
1056
|
+
``unit(0.5, "npc")`` to different cm values. Callers must pass the
|
|
1057
|
+
axis the unit is being measured along — typically ``"width"`` for
|
|
1058
|
+
gtable widths, ``"height"`` for gtable heights.
|
|
1048
1059
|
"""
|
|
1049
1060
|
if unit is None or len(unit) == 0:
|
|
1050
1061
|
return 0.0
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
vals = part.values if hasattr(part, "values") else [0.0]
|
|
1055
|
-
units = part.units if hasattr(part, "units") else ["cm"]
|
|
1056
|
-
v = vals[0] if vals else 0.0
|
|
1057
|
-
u = units[0] if units else "cm"
|
|
1058
|
-
if u == "cm":
|
|
1059
|
-
total += v
|
|
1060
|
-
elif u == "mm":
|
|
1061
|
-
total += v / 10.0
|
|
1062
|
-
elif u == "inches":
|
|
1063
|
-
total += v * 2.54
|
|
1064
|
-
elif u == "pt" or u == "points":
|
|
1065
|
-
total += v / 72.27 * 2.54
|
|
1066
|
-
else:
|
|
1067
|
-
# null, npc, etc. — use the numeric value as a rough estimate
|
|
1068
|
-
total += v
|
|
1069
|
-
return total
|
|
1062
|
+
fn = convert_width if axis == "width" else convert_height
|
|
1063
|
+
cm = fn(unit, "cm", valueOnly=True)
|
|
1064
|
+
return float(np.sum(np.asarray(cm)))
|
|
@@ -1061,7 +1061,7 @@ class CoordCartesian(Coord):
|
|
|
1061
1061
|
Mirrors R's ``CoordCartesian$render_axis_h``.
|
|
1062
1062
|
"""
|
|
1063
1063
|
from grid_py import null_grob
|
|
1064
|
-
from ggplot2_py.
|
|
1064
|
+
from ggplot2_py._guide_axis import draw_axis
|
|
1065
1065
|
|
|
1066
1066
|
breaks = panel_params.get("x_major", np.array([]))
|
|
1067
1067
|
labels = panel_params.get("x_labels", [])
|
|
@@ -1086,7 +1086,7 @@ class CoordCartesian(Coord):
|
|
|
1086
1086
|
Mirrors R's ``CoordCartesian$render_axis_v``.
|
|
1087
1087
|
"""
|
|
1088
1088
|
from grid_py import null_grob
|
|
1089
|
-
from ggplot2_py.
|
|
1089
|
+
from ggplot2_py._guide_axis import draw_axis
|
|
1090
1090
|
|
|
1091
1091
|
breaks = panel_params.get("y_major", np.array([]))
|
|
1092
1092
|
labels = panel_params.get("y_labels", [])
|
|
@@ -1131,7 +1131,7 @@ def _resolve_element(element_name: str, theme: Any, fallback: dict) -> dict:
|
|
|
1131
1131
|
|
|
1132
1132
|
|
|
1133
1133
|
# NOTE: _render_axis has been removed and replaced by guide_axis.draw_axis.
|
|
1134
|
-
# See
|
|
1134
|
+
# See _guide_axis.py and the render_axis_h/render_axis_v methods above.
|
|
1135
1135
|
|
|
1136
1136
|
|
|
1137
1137
|
# ---------------------------------------------------------------------------
|