ggh4x-python 0.3.1.9000__py3-none-any.whl
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.
- ggh4x/__init__.py +140 -0
- ggh4x/_aimed_text_grob.py +432 -0
- ggh4x/_borrowed_ggplot2.py +273 -0
- ggh4x/_cli.py +84 -0
- ggh4x/_datasets.py +106 -0
- ggh4x/_download.py +111 -0
- ggh4x/_facet_helpers.py +313 -0
- ggh4x/_facet_utils.py +649 -0
- ggh4x/_gap_grobs.py +606 -0
- ggh4x/_registry.py +10 -0
- ggh4x/_rlang.py +93 -0
- ggh4x/_utils.py +150 -0
- ggh4x/_vctrs.py +233 -0
- ggh4x/conveniences.py +601 -0
- ggh4x/coord_axes_inside.py +380 -0
- ggh4x/element_part_rect.py +545 -0
- ggh4x/facet_grid2.py +1018 -0
- ggh4x/facet_manual.py +901 -0
- ggh4x/facet_nested.py +776 -0
- ggh4x/facet_nested_wrap.py +193 -0
- ggh4x/facet_wrap2.py +896 -0
- ggh4x/geom_box.py +536 -0
- ggh4x/geom_outline_point.py +444 -0
- ggh4x/geom_pointpath.py +259 -0
- ggh4x/geom_polygonraster.py +252 -0
- ggh4x/geom_rectrug.py +489 -0
- ggh4x/geom_text_aimed.py +279 -0
- ggh4x/guide_stringlegend.py +354 -0
- ggh4x/help_secondary.py +549 -0
- ggh4x/multiscale/__init__.py +51 -0
- ggh4x/multiscale/_multiscale_add.py +207 -0
- ggh4x/multiscale/scale_listed.py +167 -0
- ggh4x/multiscale/scale_manual.py +478 -0
- ggh4x/multiscale/scale_multi.py +393 -0
- ggh4x/panel_scales/__init__.py +58 -0
- ggh4x/panel_scales/at_panel.py +115 -0
- ggh4x/panel_scales/facetted_pos_scales.py +647 -0
- ggh4x/panel_scales/force_panelsize.py +411 -0
- ggh4x/panel_scales/scale_facet.py +222 -0
- ggh4x/position_disjoint_ranges.py +229 -0
- ggh4x/position_lineartrans.py +242 -0
- ggh4x/py.typed +0 -0
- ggh4x/resources/faithful.csv +273 -0
- ggh4x/resources/iris.csv +151 -0
- ggh4x/resources/mtcars.csv +33 -0
- ggh4x/resources/pressure.csv +20 -0
- ggh4x/resources/volcano.csv +87 -0
- ggh4x/save.py +255 -0
- ggh4x/stat_difference.py +388 -0
- ggh4x/stat_funxy.py +436 -0
- ggh4x/stat_rle.py +290 -0
- ggh4x/stat_rollingkernel.py +369 -0
- ggh4x/stat_theodensity.py +681 -0
- ggh4x/strip_nested.py +448 -0
- ggh4x/strip_split.py +687 -0
- ggh4x/strip_tag.py +636 -0
- ggh4x/strip_themed.py +232 -0
- ggh4x/strip_vanilla.py +1464 -0
- ggh4x/themes.py +31 -0
- ggh4x/themes_ggh4x.py +67 -0
- ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
- ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
- ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
- ggh4x_python-0.3.1.9000.dist-info/licenses/LICENSE +3 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Segregate overlapping ranges (port of ggh4x ``position_disjoint_ranges.R``).
|
|
2
|
+
|
|
3
|
+
This module ports the R ggh4x ``position_disjoint_ranges()`` constructor and the
|
|
4
|
+
``PositionDisjointRanges`` ggproto class. One-dimensional ranged data in the
|
|
5
|
+
x-direction (``xmin``/``xmax``) is segregated in the y-direction so that no two
|
|
6
|
+
ranges overlap in two-dimensional space. Overlapping ranges are assigned to
|
|
7
|
+
different y-direction *bins* (lower bins filled first), preserving x-information.
|
|
8
|
+
|
|
9
|
+
R source: ``ggh4x/R/position_disjoint_ranges.R``.
|
|
10
|
+
|
|
11
|
+
Notes
|
|
12
|
+
-----
|
|
13
|
+
* The disjoint-bin assignment is a sweep-line algorithm inspired by
|
|
14
|
+
``IRanges::disjointBins()`` but generalised to any numeric (not just integer)
|
|
15
|
+
ranges. This port reimplements it in pure Python (no Bioconductor dependency).
|
|
16
|
+
* **Boundary semantics (load-bearing):** R uses a *strict* ``<`` test
|
|
17
|
+
(``track_bins < dat$xmin``). A range is placed into an existing bin only when
|
|
18
|
+
that bin's running ``xmax`` is *strictly less* than the new ``xmin``; touching
|
|
19
|
+
endpoints (``xmax == xmin`` after extension) are treated as overlapping and
|
|
20
|
+
forced into a new bin. Free bins are filled lowest-index-first; bins are
|
|
21
|
+
1-based.
|
|
22
|
+
* The returned ``data`` preserves the original row order; only the internal
|
|
23
|
+
``ranges`` table is sorted.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Dict, Optional
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import pandas as pd
|
|
32
|
+
|
|
33
|
+
from ggplot2_py.position import Position
|
|
34
|
+
|
|
35
|
+
from ggh4x._cli import cli_warn
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"position_disjoint_ranges",
|
|
39
|
+
"PositionDisjointRanges",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PositionDisjointRanges(Position):
|
|
44
|
+
"""Segregate overlapping x-ranges into disjoint y-direction bins.
|
|
45
|
+
|
|
46
|
+
Subclass of :class:`ggplot2_py.position.Position` ported from R
|
|
47
|
+
``PositionDisjointRanges`` (``position_disjoint_ranges.R:58-129``).
|
|
48
|
+
|
|
49
|
+
Attributes
|
|
50
|
+
----------
|
|
51
|
+
extend : float or None
|
|
52
|
+
How far (in total) a range is extended when computing overlaps. A
|
|
53
|
+
positive value leaves space between ranges sharing a bin.
|
|
54
|
+
stepsize : float or None
|
|
55
|
+
Vertical space added between bins. Positive grows bins bottom-to-top,
|
|
56
|
+
negative grows them top-to-bottom.
|
|
57
|
+
required_aes : tuple of str
|
|
58
|
+
``("xmin", "xmax", "ymin", "ymax")``.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
extend: Optional[float] = None
|
|
62
|
+
stepsize: Optional[float] = None
|
|
63
|
+
required_aes = ("xmin", "xmax", "ymin", "ymax")
|
|
64
|
+
|
|
65
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
66
|
+
"""Store constructor members directly on the instance.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
**kwargs : Any
|
|
71
|
+
Members (``extend``, ``stepsize``) assigned verbatim, mirroring the
|
|
72
|
+
``ggproto(NULL, PositionDisjointRanges, ...)`` clone.
|
|
73
|
+
"""
|
|
74
|
+
for k, v in kwargs.items():
|
|
75
|
+
setattr(self, k, v)
|
|
76
|
+
|
|
77
|
+
def setup_params(self, data: pd.DataFrame) -> Dict[str, Any]:
|
|
78
|
+
"""Validate the x-direction ranges and collect parameters.
|
|
79
|
+
|
|
80
|
+
Port of R ``PositionDisjointRanges$setup_params``
|
|
81
|
+
(``position_disjoint_ranges.R:64-73``). Emits a :func:`cli_warn` when
|
|
82
|
+
``xmin``/``xmax`` are absent, then returns ``extend``/``stepsize``.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
data : pandas.DataFrame
|
|
87
|
+
Layer data.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
dict
|
|
92
|
+
``{"extend": ..., "stepsize": ...}``.
|
|
93
|
+
"""
|
|
94
|
+
if "xmin" not in data.columns or "xmax" not in data.columns:
|
|
95
|
+
cli_warn(
|
|
96
|
+
"Undefined ranges in the x-direction.\n"
|
|
97
|
+
"i Please supply xmin and xmax."
|
|
98
|
+
)
|
|
99
|
+
return {"extend": self.extend, "stepsize": self.stepsize}
|
|
100
|
+
|
|
101
|
+
def compute_panel(
|
|
102
|
+
self,
|
|
103
|
+
data: pd.DataFrame,
|
|
104
|
+
params: Dict[str, Any],
|
|
105
|
+
scales: Any = None,
|
|
106
|
+
) -> pd.DataFrame:
|
|
107
|
+
"""Assign disjoint y-bins to overlapping x-ranges within a panel.
|
|
108
|
+
|
|
109
|
+
Port of R ``PositionDisjointRanges$compute_panel``
|
|
110
|
+
(``position_disjoint_ranges.R:74-128``).
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
data : pandas.DataFrame
|
|
115
|
+
Panel data with ``xmin``/``xmax``/``group`` (and optionally
|
|
116
|
+
``ymin``/``ymax``).
|
|
117
|
+
params : dict
|
|
118
|
+
Carries ``extend`` and ``stepsize``.
|
|
119
|
+
scales : Any, optional
|
|
120
|
+
Panel scales (unused; signature parity with the base class).
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
pandas.DataFrame
|
|
125
|
+
*data* with ``ymin``/``ymax`` shifted by ``stepsize * (bin - 1)`` per
|
|
126
|
+
assigned bin, in the original row order.
|
|
127
|
+
"""
|
|
128
|
+
group = data["group"].to_numpy()
|
|
129
|
+
|
|
130
|
+
# --- Simplify groups to ranges (R L76-92) -----------------------------
|
|
131
|
+
if len(np.unique(group)) > 1:
|
|
132
|
+
# One range per group: (min(xmin), max(xmax)) collapsed per group.
|
|
133
|
+
# ``sort=False`` keeps groups in order of first appearance, matching
|
|
134
|
+
# R ``by()`` which iterates over ``sort(unique(group))`` -> but the
|
|
135
|
+
# subsequent sort by xmin and ``match`` make the iteration order
|
|
136
|
+
# irrelevant; we only require the per-group aggregate to be correct.
|
|
137
|
+
agg = (
|
|
138
|
+
data.groupby("group", sort=False)
|
|
139
|
+
.agg(xmin=("xmin", "min"), xmax=("xmax", "max"))
|
|
140
|
+
.reset_index()
|
|
141
|
+
)
|
|
142
|
+
ranges = agg[["xmin", "xmax", "group"]].copy()
|
|
143
|
+
elif np.all(group == -1):
|
|
144
|
+
# One range per row, synthetic 1-based group ids (R ``row(data)[,1]``).
|
|
145
|
+
ranges = data[["xmin", "xmax"]].copy()
|
|
146
|
+
ranges["group"] = np.arange(1, len(data) + 1)
|
|
147
|
+
group = ranges["group"].to_numpy()
|
|
148
|
+
else:
|
|
149
|
+
# Single non-(-1) group -> nothing to disjoin; return unchanged.
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
# --- Extend & sort ranges (R L95-98) ----------------------------------
|
|
153
|
+
extend = params["extend"]
|
|
154
|
+
stepsize = params["stepsize"]
|
|
155
|
+
ranges["xmin"] = ranges["xmin"].to_numpy(dtype="float64") - 0.5 * extend
|
|
156
|
+
ranges["xmax"] = ranges["xmax"].to_numpy(dtype="float64") + 0.5 * extend
|
|
157
|
+
order = np.argsort(ranges["xmin"].to_numpy(), kind="stable")
|
|
158
|
+
ranges = ranges.iloc[order].reset_index(drop=True)
|
|
159
|
+
|
|
160
|
+
# --- Sweep-line disjoint bins (R L102-118) ----------------------------
|
|
161
|
+
# Strict ``<`` boundary: a range reuses a bin only if its running xmax is
|
|
162
|
+
# strictly less than the new xmin; touching ranges overlap -> new bin.
|
|
163
|
+
xmin = ranges["xmin"].to_numpy(dtype="float64")
|
|
164
|
+
xmax = ranges["xmax"].to_numpy(dtype="float64")
|
|
165
|
+
track_bins = [xmax[0]]
|
|
166
|
+
bins = [1]
|
|
167
|
+
for i in range(1, len(ranges)):
|
|
168
|
+
free = [k for k, end in enumerate(track_bins) if end < xmin[i]]
|
|
169
|
+
if free:
|
|
170
|
+
ans = free[0]
|
|
171
|
+
track_bins[ans] = xmax[i]
|
|
172
|
+
bins.append(ans + 1) # 1-based
|
|
173
|
+
else:
|
|
174
|
+
track_bins.append(xmax[i])
|
|
175
|
+
bins.append(len(track_bins))
|
|
176
|
+
ranges["bin"] = bins
|
|
177
|
+
|
|
178
|
+
# --- Map back & shift (R L120-127) ------------------------------------
|
|
179
|
+
# R ``match(group, ranges$group)`` -> positional index into the sorted
|
|
180
|
+
# ``ranges`` (first match, 1-based); ``ranges$bin[map]`` then indexes by
|
|
181
|
+
# position. pandas Index.get_indexer gives the 0-based positional index.
|
|
182
|
+
map_idx = pd.Index(ranges["group"].to_numpy()).get_indexer(group)
|
|
183
|
+
bin_vals = ranges["bin"].to_numpy()[map_idx]
|
|
184
|
+
|
|
185
|
+
if "ymin" in data.columns and "ymax" in data.columns:
|
|
186
|
+
data = data.copy()
|
|
187
|
+
shift = stepsize * (bin_vals - 1)
|
|
188
|
+
data["ymax"] = data["ymax"].to_numpy(dtype="float64") + shift
|
|
189
|
+
data["ymin"] = data["ymin"].to_numpy(dtype="float64") + shift
|
|
190
|
+
|
|
191
|
+
return data
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def position_disjoint_ranges(
|
|
195
|
+
extend: float = 1,
|
|
196
|
+
stepsize: float = 1,
|
|
197
|
+
) -> PositionDisjointRanges:
|
|
198
|
+
"""Create a disjoint-ranges position adjustment.
|
|
199
|
+
|
|
200
|
+
Port of R ``position_disjoint_ranges()``
|
|
201
|
+
(``position_disjoint_ranges.R:48-50``). Segregates one-dimensional ranged
|
|
202
|
+
data (``xmin``/``xmax``) in the y-direction so that no two ranges overlap.
|
|
203
|
+
This positioning is most useful when y-coordinates carry no relevant
|
|
204
|
+
information; it pairs well with ``geom_rect`` / ``geom_tile``.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
extend : float, optional
|
|
209
|
+
Total amount by which a range is extended when computing overlaps. A
|
|
210
|
+
positive value leaves space between ranges in the same bin. Default
|
|
211
|
+
``1``.
|
|
212
|
+
stepsize : float, optional
|
|
213
|
+
Vertical space added between bins. Positive grows bins bottom-to-top,
|
|
214
|
+
negative grows them top-to-bottom. Default ``1``.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
PositionDisjointRanges
|
|
219
|
+
A position object that can be passed to a layer's ``position`` argument.
|
|
220
|
+
|
|
221
|
+
Examples
|
|
222
|
+
--------
|
|
223
|
+
>>> position_disjoint_ranges(extend=0.1) # doctest: +ELLIPSIS
|
|
224
|
+
<ggh4x.position_disjoint_ranges.PositionDisjointRanges object at ...>
|
|
225
|
+
"""
|
|
226
|
+
return PositionDisjointRanges(
|
|
227
|
+
extend=extend,
|
|
228
|
+
stepsize=stepsize,
|
|
229
|
+
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Linearly transform coordinates (port of ggh4x ``position_lineartrans.R``).
|
|
2
|
+
|
|
3
|
+
This module ports the R ggh4x ``position_lineartrans()`` constructor and the
|
|
4
|
+
``PositionLinearTrans`` ggproto class. The position adjustment applies a 2x2
|
|
5
|
+
linear transformation matrix ``M`` to the ``x``/``y`` coordinates of a layer.
|
|
6
|
+
|
|
7
|
+
The transformation matrix is either supplied directly (``M``) or assembled from
|
|
8
|
+
convenience arguments ``scale``, ``shear`` and ``angle`` in the fixed order
|
|
9
|
+
*scaling -> shearing -> rotating* (R ``position_lineartrans.R:18-23``).
|
|
10
|
+
|
|
11
|
+
R source: ``ggh4x/R/position_lineartrans.R``.
|
|
12
|
+
|
|
13
|
+
Notes
|
|
14
|
+
-----
|
|
15
|
+
* :meth:`PositionLinearTrans.setup_params` reconstructs R's **column-major**
|
|
16
|
+
matrix builds exactly. R's ``matrix(c(1, shear, 1), ncol = 2)`` with
|
|
17
|
+
``shear = c(s0, s1)`` fills column-by-column, giving ``[[1, s1], [s0, 1]]``;
|
|
18
|
+
the rotation ``matrix(c(cos, sin, -sin, cos), ncol = 2)`` gives
|
|
19
|
+
``[[cos, -sin], [sin, cos]]``. ``M * scale`` is an **elementwise column
|
|
20
|
+
broadcast** (multiplying column ``j`` of ``M`` by ``scale[j]``), not a matrix
|
|
21
|
+
product.
|
|
22
|
+
* :meth:`PositionLinearTrans.compute_layer` overrides the base ``compute_layer``
|
|
23
|
+
wholesale (no per-``PANEL`` split), mirroring R: it applies the transform to
|
|
24
|
+
every row at once via ``xy @ M.T`` (equivalent to R's ``t(M %*% t(coord))``).
|
|
25
|
+
* :meth:`PositionLinearTrans.setup_data` is an identity pass-through, suppressing
|
|
26
|
+
the base required-aesthetic check (there are no required aesthetics).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Any, Dict, Optional, Sequence, Union
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
from ggplot2_py.position import Position
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"position_lineartrans",
|
|
40
|
+
"PositionLinearTrans",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
ArrayLike = Union[Sequence[float], np.ndarray]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PositionLinearTrans(Position):
|
|
47
|
+
"""Apply a 2x2 linear transformation to ``x``/``y`` coordinates.
|
|
48
|
+
|
|
49
|
+
Subclass of :class:`ggplot2_py.position.Position` ported from R
|
|
50
|
+
``PositionLinearTrans`` (``position_lineartrans.R:115-152``).
|
|
51
|
+
|
|
52
|
+
Attributes
|
|
53
|
+
----------
|
|
54
|
+
scale : sequence of float or None
|
|
55
|
+
Length-2 multipliers for the ``x`` and ``y`` coordinates respectively.
|
|
56
|
+
shear : sequence of float or None
|
|
57
|
+
Length-2 shear amounts. The first number is the vertical shear, the
|
|
58
|
+
second the horizontal shear.
|
|
59
|
+
angle : float or None
|
|
60
|
+
Clockwise rotation angle in degrees.
|
|
61
|
+
M : array-like or None
|
|
62
|
+
An explicit 2x2 transformation matrix. When supplied it overrides
|
|
63
|
+
``scale``/``shear``/``angle``.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
scale: Optional[ArrayLike] = (1, 1)
|
|
67
|
+
shear: Optional[ArrayLike] = (0, 0)
|
|
68
|
+
angle: Optional[float] = 0
|
|
69
|
+
M: Optional[ArrayLike] = None
|
|
70
|
+
|
|
71
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
72
|
+
"""Store constructor members directly on the instance.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
**kwargs : Any
|
|
77
|
+
Members (``scale``, ``shear``, ``angle``, ``M``) assigned verbatim,
|
|
78
|
+
mirroring the ``ggproto(NULL, PositionLinearTrans, ...)`` clone.
|
|
79
|
+
"""
|
|
80
|
+
for k, v in kwargs.items():
|
|
81
|
+
setattr(self, k, v)
|
|
82
|
+
|
|
83
|
+
def setup_params(self, data: pd.DataFrame) -> Dict[str, Any]:
|
|
84
|
+
"""Build the 2x2 transformation matrix ``M``.
|
|
85
|
+
|
|
86
|
+
Port of R ``PositionLinearTrans$setup_params``
|
|
87
|
+
(``position_lineartrans.R:127-151``). When ``self.M`` is supplied it is
|
|
88
|
+
returned verbatim. Otherwise the matrix is assembled in the order
|
|
89
|
+
scale -> shear -> rotate, reproducing R's column-major
|
|
90
|
+
``matrix(..., ncol = 2)`` fills exactly.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
data : pandas.DataFrame
|
|
95
|
+
Layer data (unused; present for signature parity).
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
dict
|
|
100
|
+
``{"M": ndarray}`` with a float64 2x2 transformation matrix.
|
|
101
|
+
"""
|
|
102
|
+
if self.M is not None:
|
|
103
|
+
return {"M": np.asarray(self.M, dtype="float64")}
|
|
104
|
+
|
|
105
|
+
M = np.eye(2, dtype="float64")
|
|
106
|
+
|
|
107
|
+
# Scale: R ``M <- M * self$scale`` recycles a length-2 ``scale`` down
|
|
108
|
+
# the columns => multiply column j of M by scale[j]. Broadcasting a
|
|
109
|
+
# row vector across rows does exactly this for a 2x2 M.
|
|
110
|
+
if self.scale is not None:
|
|
111
|
+
scale = np.asarray(self.scale, dtype="float64")
|
|
112
|
+
M = M * scale
|
|
113
|
+
|
|
114
|
+
# Shear: R ``matrix(c(1, shear, 1), ncol = 2)`` with shear = c(s0, s1)
|
|
115
|
+
# fills column-major => column 0 = (1, s0), column 1 = (s1, 1), i.e.
|
|
116
|
+
# [[1, s1], [s0, 1]]. Then M <- M %*% shear_matrix.
|
|
117
|
+
if self.shear is not None:
|
|
118
|
+
shear = np.asarray(self.shear, dtype="float64")
|
|
119
|
+
if shear.shape == (2,):
|
|
120
|
+
s0, s1 = float(shear[0]), float(shear[1])
|
|
121
|
+
shear_mat = np.array([[1.0, s1], [s0, 1.0]], dtype="float64")
|
|
122
|
+
M = M @ shear_mat
|
|
123
|
+
|
|
124
|
+
# Rotation: theta = -angle * pi / 180; R builds
|
|
125
|
+
# matrix(c(cos, sin, -sin, cos), ncol = 2) (column-major) =>
|
|
126
|
+
# [[cos, -sin], [sin, cos]]; then M <- rotation %*% M.
|
|
127
|
+
if self.angle is not None:
|
|
128
|
+
theta = -float(self.angle) * np.pi / 180.0
|
|
129
|
+
c, s = np.cos(theta), np.sin(theta)
|
|
130
|
+
rot = np.array([[c, -s], [s, c]], dtype="float64")
|
|
131
|
+
M = rot @ M
|
|
132
|
+
|
|
133
|
+
return {"M": M}
|
|
134
|
+
|
|
135
|
+
def setup_data(
|
|
136
|
+
self, data: pd.DataFrame, params: Dict[str, Any]
|
|
137
|
+
) -> pd.DataFrame:
|
|
138
|
+
"""Return *data* unchanged (identity pass-through).
|
|
139
|
+
|
|
140
|
+
Port of R ``PositionLinearTrans$setup_data``
|
|
141
|
+
(``position_lineartrans.R:124-126``). Overriding the base
|
|
142
|
+
:meth:`ggplot2_py.position.Position.setup_data` suppresses the base
|
|
143
|
+
required-aesthetic check; there are no required aesthetics here.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
data : pandas.DataFrame
|
|
148
|
+
Layer data.
|
|
149
|
+
params : dict
|
|
150
|
+
Position parameters (unused).
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
pandas.DataFrame
|
|
155
|
+
*data* unchanged.
|
|
156
|
+
"""
|
|
157
|
+
return data
|
|
158
|
+
|
|
159
|
+
def compute_layer(
|
|
160
|
+
self,
|
|
161
|
+
data: pd.DataFrame,
|
|
162
|
+
params: Dict[str, Any],
|
|
163
|
+
layout: Any,
|
|
164
|
+
) -> pd.DataFrame:
|
|
165
|
+
"""Apply the linear transform to every row's ``x``/``y``.
|
|
166
|
+
|
|
167
|
+
Port of R ``PositionLinearTrans$compute_layer``
|
|
168
|
+
(``position_lineartrans.R:117-123``):
|
|
169
|
+
``coord <- t(params$M %*% t(coord))`` which equals ``xy @ M.T``. This
|
|
170
|
+
override bypasses the base per-``PANEL`` split, transforming all rows at
|
|
171
|
+
once exactly as R does.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
data : pandas.DataFrame
|
|
176
|
+
Layer data containing ``x`` and ``y`` columns.
|
|
177
|
+
params : dict
|
|
178
|
+
Position parameters carrying the transformation matrix ``M``.
|
|
179
|
+
layout : Any
|
|
180
|
+
Plot layout (unused; present for signature parity).
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
pandas.DataFrame
|
|
185
|
+
*data* with ``x``/``y`` replaced by the transformed coordinates.
|
|
186
|
+
"""
|
|
187
|
+
M = np.asarray(params["M"], dtype="float64")
|
|
188
|
+
xy = data[["x", "y"]].to_numpy(dtype="float64")
|
|
189
|
+
out = xy @ M.T # == (M @ xy.T).T == R's t(M %*% t(coord))
|
|
190
|
+
data = data.copy()
|
|
191
|
+
data["x"] = out[:, 0]
|
|
192
|
+
data["y"] = out[:, 1]
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def position_lineartrans(
|
|
197
|
+
scale: Optional[ArrayLike] = (1, 1),
|
|
198
|
+
shear: Optional[ArrayLike] = (0, 0),
|
|
199
|
+
angle: float = 0,
|
|
200
|
+
M: Optional[ArrayLike] = None,
|
|
201
|
+
) -> PositionLinearTrans:
|
|
202
|
+
"""Create a linear-transformation position adjustment.
|
|
203
|
+
|
|
204
|
+
Port of R ``position_lineartrans()`` (``position_lineartrans.R:102-107``).
|
|
205
|
+
Transforms ``x``/``y`` coordinates in two dimensions for layers with an
|
|
206
|
+
``x``/``y`` parametrisation.
|
|
207
|
+
|
|
208
|
+
Linear transformation matrices are 2x2 real matrices. ``scale``, ``shear``
|
|
209
|
+
and ``angle`` are convenience arguments combined in the order
|
|
210
|
+
*scaling -> shearing -> rotating*. To apply transformations in another
|
|
211
|
+
order, build a custom ``M``.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
scale : sequence of float, optional
|
|
216
|
+
Length-2 relative units multiplying the ``x`` and ``y`` coordinates
|
|
217
|
+
respectively. Default ``(1, 1)``.
|
|
218
|
+
shear : sequence of float, optional
|
|
219
|
+
Length-2 relative shear units. The first number shears vertically, the
|
|
220
|
+
second horizontally. Default ``(0, 0)``.
|
|
221
|
+
angle : float, optional
|
|
222
|
+
Angle in degrees by which to rotate the input clockwise. Default ``0``.
|
|
223
|
+
M : array-like, optional
|
|
224
|
+
A 2x2 real transformation matrix. Overrides ``scale``/``shear``/
|
|
225
|
+
``angle`` when provided.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
PositionLinearTrans
|
|
230
|
+
A position object that can be passed to a layer's ``position`` argument.
|
|
231
|
+
|
|
232
|
+
Examples
|
|
233
|
+
--------
|
|
234
|
+
>>> position_lineartrans(angle=30) # doctest: +ELLIPSIS
|
|
235
|
+
<ggh4x.position_lineartrans.PositionLinearTrans object at ...>
|
|
236
|
+
"""
|
|
237
|
+
return PositionLinearTrans(
|
|
238
|
+
scale=scale,
|
|
239
|
+
shear=shear,
|
|
240
|
+
angle=angle,
|
|
241
|
+
M=M,
|
|
242
|
+
)
|
ggh4x/py.typed
ADDED
|
File without changes
|