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.
Files changed (64) hide show
  1. ggh4x/__init__.py +140 -0
  2. ggh4x/_aimed_text_grob.py +432 -0
  3. ggh4x/_borrowed_ggplot2.py +273 -0
  4. ggh4x/_cli.py +84 -0
  5. ggh4x/_datasets.py +106 -0
  6. ggh4x/_download.py +111 -0
  7. ggh4x/_facet_helpers.py +313 -0
  8. ggh4x/_facet_utils.py +649 -0
  9. ggh4x/_gap_grobs.py +606 -0
  10. ggh4x/_registry.py +10 -0
  11. ggh4x/_rlang.py +93 -0
  12. ggh4x/_utils.py +150 -0
  13. ggh4x/_vctrs.py +233 -0
  14. ggh4x/conveniences.py +601 -0
  15. ggh4x/coord_axes_inside.py +380 -0
  16. ggh4x/element_part_rect.py +545 -0
  17. ggh4x/facet_grid2.py +1018 -0
  18. ggh4x/facet_manual.py +901 -0
  19. ggh4x/facet_nested.py +776 -0
  20. ggh4x/facet_nested_wrap.py +193 -0
  21. ggh4x/facet_wrap2.py +896 -0
  22. ggh4x/geom_box.py +536 -0
  23. ggh4x/geom_outline_point.py +444 -0
  24. ggh4x/geom_pointpath.py +259 -0
  25. ggh4x/geom_polygonraster.py +252 -0
  26. ggh4x/geom_rectrug.py +489 -0
  27. ggh4x/geom_text_aimed.py +279 -0
  28. ggh4x/guide_stringlegend.py +354 -0
  29. ggh4x/help_secondary.py +549 -0
  30. ggh4x/multiscale/__init__.py +51 -0
  31. ggh4x/multiscale/_multiscale_add.py +207 -0
  32. ggh4x/multiscale/scale_listed.py +167 -0
  33. ggh4x/multiscale/scale_manual.py +478 -0
  34. ggh4x/multiscale/scale_multi.py +393 -0
  35. ggh4x/panel_scales/__init__.py +58 -0
  36. ggh4x/panel_scales/at_panel.py +115 -0
  37. ggh4x/panel_scales/facetted_pos_scales.py +647 -0
  38. ggh4x/panel_scales/force_panelsize.py +411 -0
  39. ggh4x/panel_scales/scale_facet.py +222 -0
  40. ggh4x/position_disjoint_ranges.py +229 -0
  41. ggh4x/position_lineartrans.py +242 -0
  42. ggh4x/py.typed +0 -0
  43. ggh4x/resources/faithful.csv +273 -0
  44. ggh4x/resources/iris.csv +151 -0
  45. ggh4x/resources/mtcars.csv +33 -0
  46. ggh4x/resources/pressure.csv +20 -0
  47. ggh4x/resources/volcano.csv +87 -0
  48. ggh4x/save.py +255 -0
  49. ggh4x/stat_difference.py +388 -0
  50. ggh4x/stat_funxy.py +436 -0
  51. ggh4x/stat_rle.py +290 -0
  52. ggh4x/stat_rollingkernel.py +369 -0
  53. ggh4x/stat_theodensity.py +681 -0
  54. ggh4x/strip_nested.py +448 -0
  55. ggh4x/strip_split.py +687 -0
  56. ggh4x/strip_tag.py +636 -0
  57. ggh4x/strip_themed.py +232 -0
  58. ggh4x/strip_vanilla.py +1464 -0
  59. ggh4x/themes.py +31 -0
  60. ggh4x/themes_ggh4x.py +67 -0
  61. ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
  62. ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
  63. ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
  64. 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