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,380 @@
1
+ """Cartesian coordinates with interior axes (port of ggh4x ``coord_axes_inside.R``).
2
+
3
+ This module ports the R ggh4x ``coord_axes_inside()`` constructor, the
4
+ ``CoordAxesInside`` ggproto class and the ``replace_vp_coord()`` helper. The
5
+ coordinate system places the plot axes at interior positions (controlled by
6
+ ``xintercept``/``yintercept``); otherwise it behaves like
7
+ :func:`ggplot2_py.coord_cartesian` (or :func:`ggplot2_py.coord_fixed` when
8
+ ``ratio`` is set).
9
+
10
+ R source: ``ggh4x/R/coord_axes_inside.R``.
11
+
12
+ Notes
13
+ -----
14
+ * The constructor builds two :class:`~ggplot2_py.theme.Theme` objects:
15
+ ``outer_axes`` blanks the axis lines/ticks (and, when labels go inside, the
16
+ axis text + tick length) in the panel gutters, while ``inner_axes`` blanks the
17
+ axis text that should *not* be drawn inside the panel.
18
+ * :meth:`CoordAxesInside.render_bg` is the load-bearing override: it renders the
19
+ *inner* axes (with un-blanked content) and re-positions their viewports to the
20
+ interior NPC coordinate of the origin, then composites them into the panel
21
+ background grob. The *outer* (blanked) axes still occupy the gutters via the
22
+ :meth:`render_axis_h` / :meth:`render_axis_v` overrides.
23
+ * :func:`_replace_vp_coord` mirrors R's ``replace_vp_coord``: when a grob carries
24
+ no viewport it is returned unchanged; otherwise the single coordinate
25
+ (``x`` or ``y``) of its viewport is replaced. The axis grobs returned by
26
+ :func:`ggplot2_py._guide_axis.draw_axis` already carry a viewport, so the
27
+ coordinate replacement applies directly (via :func:`grid_py.edit_viewport`,
28
+ since :class:`grid_py.Viewport` coordinates are immutable).
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from typing import Any, Dict, Optional, Sequence, Union
34
+
35
+ import numpy as np
36
+ import pandas as pd
37
+
38
+ from ggplot2_py import ggproto_parent
39
+ from ggplot2_py.coord import CoordCartesian
40
+ from ggplot2_py.theme import theme
41
+ from ggplot2_py.theme_elements import element_blank
42
+
43
+ from grid_py import Unit, edit_viewport, grob_tree
44
+
45
+ from scales import oob_squish
46
+
47
+ from ggh4x._rlang import arg_match0
48
+
49
+ __all__ = [
50
+ "coord_axes_inside",
51
+ "CoordAxesInside",
52
+ "_replace_vp_coord",
53
+ ]
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Helper
58
+ # ---------------------------------------------------------------------------
59
+ def _replace_vp_coord(grob: Any, param: str = "x", value: Any = None) -> Any:
60
+ """Replace a single coordinate of a grob's viewport.
61
+
62
+ Port of R ``replace_vp_coord`` (``coord_axes_inside.R:164-170``). When the
63
+ grob carries no viewport (``grob.vp is None``) it is returned unchanged;
64
+ otherwise the ``param`` coordinate (``"x"`` or ``"y"``) of its viewport is
65
+ replaced with *value*.
66
+
67
+ Because :class:`grid_py.Viewport` coordinates are immutable, the replacement
68
+ is performed by :func:`grid_py.edit_viewport`, which returns an edited copy
69
+ of the viewport. The edited viewport is then re-attached to *grob*.
70
+
71
+ Parameters
72
+ ----------
73
+ grob : grid_py.Grob
74
+ A grob, typically an axis grob from
75
+ :func:`ggplot2_py._guide_axis.draw_axis`.
76
+ param : {"x", "y"}, default "x"
77
+ Which viewport coordinate to replace.
78
+ value : grid_py.Unit
79
+ The replacement coordinate (an NPC :class:`~grid_py.Unit`).
80
+
81
+ Returns
82
+ -------
83
+ grid_py.Grob
84
+ *grob* with its viewport's ``param`` coordinate replaced, or *grob*
85
+ unchanged when it carries no viewport.
86
+ """
87
+ vp = getattr(grob, "vp", None)
88
+ if vp is None:
89
+ return grob
90
+ grob.vp = edit_viewport(vp, **{param: value})
91
+ return grob
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # CoordAxesInside ggproto class
96
+ # ---------------------------------------------------------------------------
97
+ class CoordAxesInside(CoordCartesian):
98
+ """Cartesian coordinate system with interior axes.
99
+
100
+ Subclass of :class:`ggplot2_py.coord.CoordCartesian` ported from R
101
+ ``CoordAxesInside`` (``coord_axes_inside.R:122-160``).
102
+
103
+ Attributes
104
+ ----------
105
+ origin : pandas.DataFrame
106
+ A 1-row frame ``{"x": [xintercept], "y": [yintercept]}`` giving the
107
+ interior position where the axes meet.
108
+ outer_axes : ggplot2_py.theme.Theme
109
+ Theme additions blanking the gutter axis lines/ticks (and text/tick
110
+ length when labels are drawn inside).
111
+ inner_axes : ggplot2_py.theme.Theme
112
+ Theme additions blanking the axis text that should not appear inside the
113
+ panel.
114
+ """
115
+
116
+ def render_axis_h(self, panel_params: Dict[str, Any], theme: Any) -> Dict[str, Any]:
117
+ """Render the horizontal (top/bottom) gutter axes, blanked.
118
+
119
+ Port of R ``CoordAxesInside$render_axis_h``
120
+ (``coord_axes_inside.R:124-126``): delegate to
121
+ ``CoordCartesian$render_axis_h`` after adding ``self.outer_axes`` to the
122
+ theme so the gutter axis lines/ticks (and text, if labels are inside)
123
+ are blank.
124
+
125
+ Parameters
126
+ ----------
127
+ panel_params : dict
128
+ Panel parameters from :meth:`setup_panel_params`.
129
+ theme : ggplot2_py.theme.Theme
130
+ The active theme.
131
+
132
+ Returns
133
+ -------
134
+ dict
135
+ ``{"top": grob, "bottom": grob}``.
136
+ """
137
+ return ggproto_parent(CoordCartesian, self).render_axis_h(
138
+ panel_params, theme + self.outer_axes
139
+ )
140
+
141
+ def render_axis_v(self, panel_params: Dict[str, Any], theme: Any) -> Dict[str, Any]:
142
+ """Render the vertical (left/right) gutter axes, blanked.
143
+
144
+ Port of R ``CoordAxesInside$render_axis_v``
145
+ (``coord_axes_inside.R:127-129``): as :meth:`render_axis_h` but for the
146
+ vertical axes.
147
+
148
+ Parameters
149
+ ----------
150
+ panel_params : dict
151
+ Panel parameters from :meth:`setup_panel_params`.
152
+ theme : ggplot2_py.theme.Theme
153
+ The active theme.
154
+
155
+ Returns
156
+ -------
157
+ dict
158
+ ``{"left": grob, "right": grob}``.
159
+ """
160
+ return ggproto_parent(CoordCartesian, self).render_axis_v(
161
+ panel_params, theme + self.outer_axes
162
+ )
163
+
164
+ def render_bg(self, panel_params: Dict[str, Any], theme: Any) -> Any:
165
+ """Render the panel background with the interior axes injected.
166
+
167
+ Port of R ``CoordAxesInside$render_bg``
168
+ (``coord_axes_inside.R:130-152``). The *inner* axes are rendered here
169
+ (so their un-blanked content lands inside the panel background) and each
170
+ axis viewport is re-positioned to the interior NPC coordinate of the
171
+ transformed origin. The repositioned axes are composited into the
172
+ background grob with the grid/axis-b/axis-t/axis-l/axis-r names.
173
+
174
+ Parameters
175
+ ----------
176
+ panel_params : dict
177
+ Panel parameters from :meth:`setup_panel_params`.
178
+ theme : ggplot2_py.theme.Theme
179
+ The active theme.
180
+
181
+ Returns
182
+ -------
183
+ grid_py.Grob
184
+ A grob tree containing the grid background and the four repositioned
185
+ interior axes.
186
+ """
187
+ theme = theme + self.inner_axes
188
+ grid_grob = ggproto_parent(CoordCartesian, self).render_bg(panel_params, theme)
189
+ xaxes = ggproto_parent(CoordCartesian, self).render_axis_h(panel_params, theme)
190
+ yaxes = ggproto_parent(CoordCartesian, self).render_axis_v(panel_params, theme)
191
+
192
+ origin = self.transform(self.origin, panel_params)
193
+
194
+ x = Unit(oob_squish(float(origin["x"].iloc[0])), "npc")
195
+ y = Unit(oob_squish(float(origin["y"].iloc[0])), "npc")
196
+
197
+ xaxes["bottom"] = _replace_vp_coord(xaxes["bottom"], "y", y)
198
+ xaxes["top"] = _replace_vp_coord(xaxes["top"], "y", y)
199
+ yaxes["left"] = _replace_vp_coord(yaxes["left"], "x", x)
200
+ yaxes["right"] = _replace_vp_coord(yaxes["right"], "x", x)
201
+
202
+ # R grobTree names each child for later grid-path edits; mirror that by
203
+ # setting each grob's .name before assembling the tree (draw order:
204
+ # grid, axis-b, axis-t, axis-l, axis-r).
205
+ children = [
206
+ ("grid", grid_grob),
207
+ ("axis-b", xaxes["bottom"]),
208
+ ("axis-t", xaxes["top"]),
209
+ ("axis-l", yaxes["left"]),
210
+ ("axis-r", yaxes["right"]),
211
+ ]
212
+ for name, child in children:
213
+ if hasattr(child, "name"):
214
+ child.name = name
215
+
216
+ return grob_tree(*(child for _, child in children))
217
+
218
+ def is_free(self) -> bool:
219
+ """Whether the aspect ratio is free.
220
+
221
+ Port of R ``CoordAxesInside$is_free`` (``coord_axes_inside.R:154``):
222
+ ``self.ratio is None``. (Identical to
223
+ :meth:`CoordCartesian.is_free`; re-declared for parity.)
224
+
225
+ Returns
226
+ -------
227
+ bool
228
+ ``True`` when ``self.ratio`` is ``None``.
229
+ """
230
+ return self.ratio is None
231
+
232
+ def aspect(self, ranges: Any) -> Optional[float]:
233
+ """Compute the fixed aspect ratio, if any.
234
+
235
+ Port of R ``CoordAxesInside$aspect`` (``coord_axes_inside.R:156-159``):
236
+ ``None`` when ``ratio`` is ``None``, else
237
+ ``diff(y.range) / diff(x.range) * ratio``.
238
+
239
+ Parameters
240
+ ----------
241
+ ranges : dict
242
+ Must expose ``y.range`` / ``x.range`` (or ``y_range`` / ``x_range``).
243
+
244
+ Returns
245
+ -------
246
+ float or None
247
+ The aspect ratio, or ``None`` when ``ratio`` is unset.
248
+ """
249
+ if self.ratio is None:
250
+ return None
251
+ y_range = ranges.get("y.range") or ranges.get("y_range", [0, 1])
252
+ x_range = ranges.get("x.range") or ranges.get("x_range", [0, 1])
253
+ return (y_range[1] - y_range[0]) / (x_range[1] - x_range[0]) * self.ratio
254
+
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Constructor
258
+ # ---------------------------------------------------------------------------
259
+ def coord_axes_inside(
260
+ xlim: Optional[Sequence[float]] = None,
261
+ ylim: Optional[Sequence[float]] = None,
262
+ xintercept: float = 0,
263
+ yintercept: float = 0,
264
+ labels_inside: Union[bool, str] = False,
265
+ ratio: Optional[float] = None,
266
+ expand: bool = True,
267
+ default: bool = False,
268
+ clip: str = "on",
269
+ ) -> CoordAxesInside:
270
+ """Create a Cartesian coordinate system with interior axes.
271
+
272
+ Port of R ``coord_axes_inside()`` (``coord_axes_inside.R:49-114``). Other
273
+ than placing the axes at interior positions, this behaves like
274
+ :func:`ggplot2_py.coord_cartesian` (or :func:`ggplot2_py.coord_fixed` when
275
+ ``ratio`` is set).
276
+
277
+ Parameters
278
+ ----------
279
+ xlim, ylim : sequence of float, optional
280
+ Coordinate limits (zoom, does not filter data).
281
+ xintercept, yintercept : float, optional
282
+ Positions where the orthogonal axes should be placed. When outside the
283
+ limits, the axes snap to the nearest extreme. Default ``0``.
284
+ labels_inside : bool or {"x", "y", "both", "none"}, optional
285
+ The axes whose labels are placed inside the panel. ``True`` maps to
286
+ ``"both"`` and ``False`` (default) maps to ``"none"``.
287
+ ratio : float, optional
288
+ Fixed aspect ratio expressed as ``y / x``, or ``None`` for a free ratio.
289
+ expand : bool, default True
290
+ Whether to expand limits to avoid data/axis overlap.
291
+ default : bool, default False
292
+ Whether this is the plot's default coordinate system.
293
+ clip : str, default "on"
294
+ Clipping: ``"on"`` or ``"off"``.
295
+
296
+ Returns
297
+ -------
298
+ CoordAxesInside
299
+ A coordinate object that can be added to a plot.
300
+
301
+ Examples
302
+ --------
303
+ >>> isinstance(coord_axes_inside(xintercept=1), CoordAxesInside)
304
+ True
305
+ """
306
+ if isinstance(labels_inside, str):
307
+ labels_inside = arg_match0(
308
+ labels_inside, ("x", "y", "none", "both"), arg_name="labels_inside"
309
+ )
310
+ else:
311
+ labels_inside = "both" if labels_inside is True else "none"
312
+
313
+ inner_axes = theme()
314
+ outer_axes = theme(
315
+ **{
316
+ "axis.line.x.bottom": element_blank(),
317
+ "axis.line.x.top": element_blank(),
318
+ "axis.ticks.x.bottom": element_blank(),
319
+ "axis.ticks.x.top": element_blank(),
320
+ "axis.line.y.left": element_blank(),
321
+ "axis.line.y.right": element_blank(),
322
+ "axis.ticks.y.left": element_blank(),
323
+ "axis.ticks.y.right": element_blank(),
324
+ }
325
+ )
326
+
327
+ if labels_inside in ("x", "both"):
328
+ outer_axes = outer_axes + theme(
329
+ **{
330
+ "axis.text.x.bottom": element_blank(),
331
+ "axis.text.x.top": element_blank(),
332
+ "axis.ticks.length.x.bottom": Unit(0, "pt"),
333
+ "axis.ticks.length.x.top": Unit(0, "pt"),
334
+ }
335
+ )
336
+ else:
337
+ inner_axes = inner_axes + theme(
338
+ **{
339
+ "axis.text.x.bottom": element_blank(),
340
+ "axis.text.x.top": element_blank(),
341
+ }
342
+ )
343
+
344
+ if labels_inside in ("y", "both"):
345
+ outer_axes = outer_axes + theme(
346
+ **{
347
+ "axis.text.y.left": element_blank(),
348
+ "axis.text.y.right": element_blank(),
349
+ "axis.ticks.length.y.left": Unit(0, "pt"),
350
+ "axis.ticks.length.y.right": Unit(0, "pt"),
351
+ }
352
+ )
353
+ else:
354
+ inner_axes = inner_axes + theme(
355
+ **{
356
+ "axis.text.y.left": element_blank(),
357
+ "axis.text.y.right": element_blank(),
358
+ }
359
+ )
360
+
361
+ return CoordAxesInside(
362
+ limits={"x": list(xlim) if xlim is not None else None,
363
+ "y": list(ylim) if ylim is not None else None},
364
+ expand=expand,
365
+ default=default,
366
+ clip=clip,
367
+ ratio=ratio,
368
+ # R: data_frame0(x = xintercept[1], y = yintercept[1]) takes the FIRST
369
+ # element of a (possibly vector) intercept; np.atleast_1d handles both a
370
+ # scalar and a vector uniformly (previously a vector was stored verbatim
371
+ # and crashed downstream in float(origin["x"])).
372
+ origin=pd.DataFrame(
373
+ {
374
+ "x": [np.atleast_1d(xintercept)[0]],
375
+ "y": [np.atleast_1d(yintercept)[0]],
376
+ }
377
+ ),
378
+ outer_axes=outer_axes,
379
+ inner_axes=inner_axes,
380
+ )