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,444 @@
1
+ """Points with a shared outline.
2
+
3
+ Python port of ``geom_outline_point.R`` from the R package **ggh4x**.
4
+
5
+ This is a variant of the point geom in which overlapping points share a
6
+ common outline. It works by drawing an additional layer of points
7
+ *below* a regular layer of points, with a thicker stroke. The colour of
8
+ the lower (outline) layer is controlled by the new ``stroke_colour``
9
+ aesthetic, which can be mapped to a scale via
10
+ ``scale_colour_hue(aesthetics="stroke_colour")``.
11
+
12
+ R source
13
+ --------
14
+ ``ggh4x/R/geom_outline_point.R`` -- :func:`geom_outline_point`,
15
+ :class:`GeomOutlinePoint`, :func:`draw_key_outline_point`.
16
+
17
+ Notes
18
+ -----
19
+ Because of the two-layer implementation, the ``alpha`` aesthetic is
20
+ handled rather ungracefully (it is applied to *both* layers, so the
21
+ outline shows through semi-transparent fills). This mirrors the R
22
+ package behaviour exactly.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Dict, Mapping as TMapping, Optional
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+
32
+ from ggplot2_py import GeomPoint
33
+ from ggplot2_py.geom import (
34
+ PT,
35
+ STROKE,
36
+ FromTheme,
37
+ Gpar,
38
+ Mapping,
39
+ _coord_transform,
40
+ _fill_alpha,
41
+ grob_tree,
42
+ points_grob,
43
+ scales_alpha,
44
+ )
45
+
46
+ from ._rlang import value_or
47
+
48
+ __all__ = [
49
+ "GeomOutlinePoint",
50
+ "geom_outline_point",
51
+ "draw_key_outline_point",
52
+ ]
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Helpers
57
+ # ---------------------------------------------------------------------------
58
+ def _mix_ink_paper_half(geom_el: Any) -> Any:
59
+ """Compute ``col_mix(ink, paper)`` for the ``colour`` fallback.
60
+
61
+ R ``geom_outline_point.R:103``::
62
+
63
+ colour = from_theme(colour %||% col_mix(ink, paper))
64
+
65
+ ``col_mix`` with no explicit ratio defaults to ``0.5`` (an equal
66
+ blend of the ink and paper colours).
67
+
68
+ Parameters
69
+ ----------
70
+ geom_el : element_geom
71
+ The resolved ``element_geom`` carrying ``ink`` / ``paper``.
72
+
73
+ Returns
74
+ -------
75
+ str
76
+ The mixed colour as a hex string.
77
+ """
78
+ from scales import col_mix
79
+
80
+ return col_mix(geom_el.ink, geom_el.paper, 0.5)
81
+
82
+
83
+ def _to_float_array(values: Any) -> np.ndarray:
84
+ """Coerce a column/scalar to a float ``ndarray`` (NA-preserving).
85
+
86
+ Parameters
87
+ ----------
88
+ values : Any
89
+ A pandas Series, numpy array, list or scalar.
90
+
91
+ Returns
92
+ -------
93
+ numpy.ndarray
94
+ 1-D float array; non-finite entries become ``nan``.
95
+ """
96
+ arr = np.asarray(values, dtype="float64")
97
+ return np.atleast_1d(arr)
98
+
99
+
100
+ def _outline_point_gpars(
101
+ shape: Any,
102
+ size: Any,
103
+ stroke: Any,
104
+ colour: Any,
105
+ fill: Any,
106
+ stroke_colour: Any,
107
+ alpha: Any,
108
+ ) -> tuple:
109
+ """Compute the foreground and background ``gpar`` arguments.
110
+
111
+ This is the shared core of :meth:`GeomOutlinePoint.draw_panel` and
112
+ :func:`draw_key_outline_point`, ported verbatim from
113
+ ``geom_outline_point.R:60-91`` / ``:117-148``.
114
+
115
+ Parameters
116
+ ----------
117
+ shape : array-like
118
+ ``pch`` values (numeric).
119
+ size : array-like
120
+ Point sizes (mm).
121
+ stroke : array-like
122
+ Stroke widths; ``NA`` becomes ``0``.
123
+ colour, fill, stroke_colour : array-like
124
+ Colour specifications for the foreground colour, foreground fill
125
+ and background (outline) colour respectively.
126
+ alpha : array-like
127
+ Alpha values applied to all colours.
128
+
129
+ Returns
130
+ -------
131
+ tuple of (dict, dict)
132
+ ``(foreground_gp, background_gp)`` keyword dictionaries suitable
133
+ for :class:`grid_py.Gpar`.
134
+ """
135
+ shape_arr = _to_float_array(shape)
136
+
137
+ is_solid = shape_arr > 14
138
+ has_fill = shape_arr > 20
139
+
140
+ stroke_size = _to_float_array(stroke).astype("float64").copy()
141
+ stroke_size[np.isnan(stroke_size)] = 0.0
142
+
143
+ size_arr = _to_float_array(size)
144
+
145
+ # R: lwd <- ifelse(is_solid & !has_fill, 0, stroke_size * .stroke / 2)
146
+ lwd = np.where(is_solid & ~has_fill, 0.0, stroke_size * STROKE / 2.0)
147
+
148
+ foreground_gp: Dict[str, Any] = dict(
149
+ col=scales_alpha(colour, alpha),
150
+ fill=_fill_alpha(fill, alpha),
151
+ fontsize=size_arr * PT,
152
+ lwd=lwd,
153
+ )
154
+
155
+ # R: size <- coords$size * .pt + ifelse(is_solid, stroke_size * .stroke, 0)
156
+ bg_fontsize = size_arr * PT + np.where(is_solid, stroke_size * STROKE, 0.0)
157
+ # R: lwd <- lwd + ifelse(is_solid, 0, stroke_size * .stroke)
158
+ bg_lwd = lwd + np.where(is_solid, 0.0, stroke_size * STROKE)
159
+
160
+ background_gp: Dict[str, Any] = dict(
161
+ col=scales_alpha(stroke_colour, alpha),
162
+ fill=scales_alpha(stroke_colour, alpha),
163
+ lwd=bg_lwd,
164
+ fontsize=bg_fontsize,
165
+ )
166
+
167
+ return foreground_gp, background_gp
168
+
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Legend key
172
+ # ---------------------------------------------------------------------------
173
+ def draw_key_outline_point(
174
+ data: Any,
175
+ params: Dict[str, Any],
176
+ size: Any = None,
177
+ ) -> Any:
178
+ """Draw a legend key for :class:`GeomOutlinePoint`.
179
+
180
+ Port of R ``draw_key_outline_point`` (``geom_outline_point.R:59-94``).
181
+ Replicates the two-layer point glyph: a thick ``stroke_colour``
182
+ background under a normal foreground point, both centred at
183
+ ``(0.5, 0.5)`` in the key viewport.
184
+
185
+ Parameters
186
+ ----------
187
+ data : dict or DataFrame
188
+ Scaled aesthetics for a single legend entry. Must carry
189
+ ``shape``, ``size``, ``stroke``, ``colour``, ``fill``,
190
+ ``stroke_colour`` and ``alpha``.
191
+ params : dict
192
+ Extra layer parameters (unused, accepted for signature parity).
193
+ size : optional
194
+ Key dimensions (unused).
195
+
196
+ Returns
197
+ -------
198
+ grob
199
+ A :class:`grid_py.GTree` with the background drawn first and the
200
+ foreground on top.
201
+ """
202
+ shape = _key_get(data, "shape", 19)
203
+ pt_size = _key_get(data, "size", 1.5)
204
+ stroke = _key_get(data, "stroke", 0.5)
205
+ colour = _key_get(data, "colour", "black")
206
+ fill = _key_get(data, "fill", None)
207
+ stroke_colour = _key_get(data, "stroke_colour", "black")
208
+ alpha = _key_get(data, "alpha", None)
209
+
210
+ fg_gp, bg_gp = _outline_point_gpars(
211
+ shape=shape,
212
+ size=pt_size,
213
+ stroke=stroke,
214
+ colour=colour,
215
+ fill=fill,
216
+ stroke_colour=stroke_colour,
217
+ alpha=alpha,
218
+ )
219
+
220
+ foreground = points_grob(
221
+ x=0.5,
222
+ y=0.5,
223
+ pch=np.asarray(shape),
224
+ gp=Gpar(**fg_gp),
225
+ )
226
+ background = points_grob(
227
+ x=0.5,
228
+ y=0.5,
229
+ pch=np.asarray(shape),
230
+ gp=Gpar(**bg_gp),
231
+ )
232
+
233
+ return grob_tree(background, foreground)
234
+
235
+
236
+ def _key_get(data: Any, key: str, default: Any = None) -> Any:
237
+ """Fetch ``key`` from a dict- or DataFrame-like legend ``data``.
238
+
239
+ Mirrors ggplot2_py's ``draw_key`` accessor but is column-safe for
240
+ DataFrames (``getattr(df, "shape")`` would return the DataFrame's
241
+ ``.shape`` tuple, so column access must be explicit).
242
+
243
+ Parameters
244
+ ----------
245
+ data : dict or DataFrame
246
+ The legend entry data.
247
+ key : str
248
+ Aesthetic name.
249
+ default : Any
250
+ Value returned when ``key`` is absent.
251
+
252
+ Returns
253
+ -------
254
+ Any
255
+ The stored value (scalar for dicts; the first element for
256
+ DataFrame columns, matching a single-row legend key).
257
+ """
258
+ if isinstance(data, pd.DataFrame):
259
+ if key in data.columns:
260
+ col = data[key]
261
+ return col.iloc[0] if len(col) else default
262
+ return default
263
+ if isinstance(data, dict):
264
+ return data.get(key, default)
265
+ return getattr(data, key, default)
266
+
267
+
268
+ # ---------------------------------------------------------------------------
269
+ # ggproto class
270
+ # ---------------------------------------------------------------------------
271
+ class GeomOutlinePoint(GeomPoint):
272
+ """Point geom that draws a shared outline beneath the points.
273
+
274
+ Two stacked :func:`grid_py.points_grob` layers are emitted per panel:
275
+ a thicker *background* stroke layer (coloured by ``stroke_colour``)
276
+ and, on top of it, a normal *foreground* point layer. Overlapping
277
+ points therefore appear to share a single outline.
278
+
279
+ Subclasses :class:`ggplot2_py.GeomPoint`. Unlike its parent,
280
+ :meth:`draw_panel` does **not** call ``ggproto_parent`` -- it builds
281
+ both grobs directly.
282
+ """
283
+
284
+ # R geom_outline_point.R:101-109. Adds the brand-new ``stroke_colour``
285
+ # aesthetic and overrides the ``colour`` fallback to col_mix(ink, paper).
286
+ default_aes: Mapping = Mapping(
287
+ shape=FromTheme("pointshape"),
288
+ colour=FromTheme("colour", fallback=_mix_ink_paper_half),
289
+ size=FromTheme("pointsize"),
290
+ fill=FromTheme("fill"),
291
+ alpha=None,
292
+ stroke=FromTheme("borderwidth"),
293
+ stroke_colour=FromTheme("ink"),
294
+ )
295
+
296
+ draw_key = staticmethod(draw_key_outline_point)
297
+
298
+ def draw_panel(
299
+ self,
300
+ data: pd.DataFrame,
301
+ panel_params: Any,
302
+ coord: Any,
303
+ na_rm: bool = True,
304
+ **params: Any,
305
+ ) -> Any:
306
+ """Draw the outline + point layers for one panel.
307
+
308
+ Port of R ``GeomOutlinePoint$draw_panel``
309
+ (``geom_outline_point.R:113-155``). Note the R default
310
+ ``na.rm = TRUE`` (the parent :class:`GeomPoint` defaults to
311
+ ``FALSE``).
312
+
313
+ Parameters
314
+ ----------
315
+ data : DataFrame
316
+ Layer data with at least ``x``, ``y``, ``shape``, ``size``,
317
+ ``stroke``, ``colour``, ``fill``, ``stroke_colour``,
318
+ ``alpha``.
319
+ panel_params : Any
320
+ Panel parameters (ranges, etc.).
321
+ coord : Coord
322
+ The active coordinate system.
323
+ na_rm : bool, default True
324
+ Whether missing values are silently removed upstream.
325
+ **params : Any
326
+ Ignored extra parameters.
327
+
328
+ Returns
329
+ -------
330
+ grob
331
+ A :class:`grid_py.GTree` named ``outline_points`` with the
332
+ background layer drawn first and the foreground on top.
333
+ """
334
+ coords = _coord_transform(coord, data, panel_params)
335
+
336
+ def _col(name: str, default: Any) -> Any:
337
+ return coords[name].values if name in coords.columns else default
338
+
339
+ fg_gp, bg_gp = _outline_point_gpars(
340
+ shape=_col("shape", 19),
341
+ size=_col("size", 1.5),
342
+ stroke=_col("stroke", 0.5),
343
+ colour=_col("colour", "black"),
344
+ fill=_col("fill", None),
345
+ stroke_colour=_col("stroke_colour", "black"),
346
+ alpha=_col("alpha", None),
347
+ )
348
+
349
+ x = coords["x"].values
350
+ y = coords["y"].values
351
+ shape_vals = np.asarray(_col("shape", 19))
352
+
353
+ foreground = points_grob(
354
+ x=x,
355
+ y=y,
356
+ pch=shape_vals,
357
+ gp=Gpar(**fg_gp),
358
+ )
359
+
360
+ background = points_grob(
361
+ x=x,
362
+ y=y,
363
+ pch=shape_vals,
364
+ gp=Gpar(**bg_gp),
365
+ )
366
+
367
+ # R: grob <- grobTree(background, foreground); grob$name <- ...
368
+ grob = grob_tree(background, foreground)
369
+ grob.name = "outline_points"
370
+ return grob
371
+
372
+
373
+ # Make the class-level ``draw_key`` resolvable both as an unbound function
374
+ # (legend machinery calls ``draw_key_fn(data, params, size)``) and as an
375
+ # attribute lookup.
376
+ GeomOutlinePoint.draw_key = draw_key_outline_point
377
+
378
+
379
+ # ---------------------------------------------------------------------------
380
+ # Constructor
381
+ # ---------------------------------------------------------------------------
382
+ def geom_outline_point(
383
+ mapping: Optional[TMapping] = None,
384
+ data: Any = None,
385
+ stat: Any = "identity",
386
+ position: Any = "identity",
387
+ *,
388
+ na_rm: bool = False,
389
+ show_legend: Optional[bool] = None,
390
+ inherit_aes: bool = True,
391
+ **kwargs: Any,
392
+ ) -> Any:
393
+ """Points with a shared outline.
394
+
395
+ Port of R ``geom_outline_point`` (``geom_outline_point.R:32-55``). A
396
+ variant of :func:`ggplot2_py.geom_point` in which overlapping points
397
+ are given a shared outline by drawing a thicker stroke layer beneath
398
+ the regular points.
399
+
400
+ The outline colour can be mapped to a scale by setting the aesthetic
401
+ to ``"stroke_colour"`` and supplying e.g.
402
+ ``scale_colour_hue(aesthetics="stroke_colour")``.
403
+
404
+ Parameters
405
+ ----------
406
+ mapping : Mapping, optional
407
+ Aesthetic mapping (see :func:`ggplot2_py.aes`).
408
+ data : DataFrame or callable, optional
409
+ Layer data.
410
+ stat : str or Stat, default ``"identity"``
411
+ Statistical transformation.
412
+ position : str or Position, default ``"identity"``
413
+ Position adjustment.
414
+ na_rm : bool, default False
415
+ If ``False``, missing values are removed with a warning.
416
+ show_legend : bool, optional
417
+ Whether to include this layer in the legend. ``None`` mirrors
418
+ R's ``NA`` (include only mapped aesthetics).
419
+ inherit_aes : bool, default True
420
+ Whether to inherit the plot-level mapping.
421
+ **kwargs : Any
422
+ Other arguments passed on to the layer (e.g. ``size``,
423
+ ``stroke``, fixed aesthetics).
424
+
425
+ Returns
426
+ -------
427
+ ggplot2_py.Layer
428
+ A layer backed by :class:`GeomOutlinePoint`.
429
+ """
430
+ from ggplot2_py.layer import layer
431
+
432
+ return layer(
433
+ data=data,
434
+ mapping=mapping,
435
+ stat=stat,
436
+ geom=GeomOutlinePoint,
437
+ position=position,
438
+ show_legend=show_legend,
439
+ inherit_aes=inherit_aes,
440
+ params={
441
+ "na_rm": na_rm,
442
+ **kwargs,
443
+ },
444
+ )
@@ -0,0 +1,259 @@
1
+ """Point paths (port of ggh4x ``geom_pointpath.R``).
2
+
3
+ ``geom_pointpath()`` makes a scatterplot in which the points are connected by
4
+ line segments in data order, mimicking base R's ``type = "b"`` line plots. The
5
+ inter-point segments are *interrupted* by a gap around every point; crucially,
6
+ the gap is sized in absolute units at draw time so it does not deform under
7
+ different aspect ratios or device sizes.
8
+
9
+ R source: ``ggh4x/R/geom_pointpath.R``.
10
+
11
+ Notes
12
+ -----
13
+ * :meth:`GeomPointPath.draw_panel` first draws the underlying points via
14
+ ``ggproto_parent(GeomPoint, self).draw_panel`` (it keeps ``self`` in its
15
+ formals), then builds the inter-point segments, attaches a custom gap grob
16
+ and returns ``grob_tree(gap_grob, point_grob)`` so the **points sit on top**.
17
+ * The custom grob class is :class:`~ggh4x._gap_grobs.GapSegmentsGrob` under
18
+ linear coordinates and :class:`~ggh4x._gap_grobs.GapSegmentsChainGrob`
19
+ otherwise, exactly mirroring R's ``cl = if (coord$is_linear())`` switch.
20
+ * ``mult`` is a non-standard *mappable* aesthetic (default ``0.5``) that scales
21
+ the gap radius; it is declared in :attr:`GeomPointPath.default_aes` so
22
+ ``use_defaults`` broadcasts it.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Optional
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+
32
+ from ggplot2_py import ggproto_parent
33
+ from ggplot2_py.geom import (
34
+ GeomPoint,
35
+ FromTheme,
36
+ Gpar,
37
+ Mapping,
38
+ PT,
39
+ STROKE,
40
+ _ggname,
41
+ grob_tree,
42
+ scales_alpha,
43
+ )
44
+ from ggplot2_py.coord import coord_munch
45
+ from grid_py import Unit
46
+
47
+ from ._gap_grobs import GapSegmentsChainGrob, GapSegmentsGrob
48
+
49
+ __all__ = [
50
+ "geom_pointpath",
51
+ "GeomPointPath",
52
+ "GeomPointpath",
53
+ ]
54
+
55
+
56
+ class GeomPointPath(GeomPoint):
57
+ """Point geom whose points are connected by gapped line segments.
58
+
59
+ Subclass of :class:`ggplot2_py.GeomPoint` ported from R ``GeomPointPath``
60
+ (``geom_pointpath.R:71-140``). Adds the ``linewidth``, ``linetype`` and
61
+ ``mult`` aesthetics on top of the point defaults and overrides
62
+ :meth:`draw_panel` to emit the interrupted path beneath the points.
63
+ """
64
+
65
+ # R geom_pointpath.R:128-138. GeomPoint defaults + line aesthetics + the
66
+ # non-standard ``mult`` gap-scaling aesthetic (default 0.5).
67
+ default_aes: Mapping = Mapping(
68
+ shape=FromTheme("pointshape"),
69
+ colour=FromTheme("colour", fallback="ink"),
70
+ size=FromTheme("pointsize"),
71
+ fill=FromTheme("fill"),
72
+ alpha=None,
73
+ stroke=FromTheme("borderwidth"),
74
+ linewidth=FromTheme("linewidth"),
75
+ linetype=FromTheme("linetype"),
76
+ mult=0.5,
77
+ )
78
+ non_missing_aes = ("size", "colour")
79
+
80
+ def draw_panel(
81
+ self,
82
+ data: pd.DataFrame,
83
+ panel_params: Any,
84
+ coord: Any,
85
+ arrow: Any = None,
86
+ na_rm: bool = False,
87
+ **params: Any,
88
+ ) -> Any:
89
+ """Draw the points and the gapped inter-point path for one panel.
90
+
91
+ Port of R ``GeomPointPath$draw_panel`` (``geom_pointpath.R:73-126``).
92
+
93
+ Parameters
94
+ ----------
95
+ data : pandas.DataFrame
96
+ Layer data for one panel.
97
+ panel_params : Any
98
+ Panel scales / ranges.
99
+ coord : Any
100
+ Active coordinate system.
101
+ arrow : grid_py.Arrow or None, default ``None``
102
+ Optional arrow specification for the path ends.
103
+ na_rm : bool, default ``False``
104
+ Whether missing values are silently removed.
105
+ **params : Any
106
+ Ignored extra parameters.
107
+
108
+ Returns
109
+ -------
110
+ grid_py.Grob
111
+ ``grob_tree(gap_path, point_grob)`` with the points drawn on top,
112
+ or ``grob_tree(point_grob)`` when no inter-point segment survives.
113
+ """
114
+ # Default geom_point behaviour for the points themselves.
115
+ pointgrob = ggproto_parent(GeomPoint, self).draw_panel(
116
+ data, panel_params, coord, na_rm=na_rm
117
+ )
118
+
119
+ data = data.copy()
120
+ data["id"] = np.arange(1, len(data) + 1)
121
+ # order(group) is a stable sort in R.
122
+ data = data.sort_values("group", kind="stable").reset_index(drop=True)
123
+ data = coord_munch(coord, data, panel_params)
124
+ data = data.reset_index(drop=True)
125
+
126
+ x = data["x"].to_numpy(dtype="float64")
127
+ y = data["y"].to_numpy(dtype="float64")
128
+ group = data["group"].to_numpy()
129
+ n = len(data)
130
+
131
+ # transform: xend = c(tail(x, -1), NA); yend likewise;
132
+ # keep = c(group[-1] == head(group, -1), FALSE)
133
+ xend = np.concatenate([x[1:], [np.nan]]) if n > 0 else np.array([])
134
+ yend = np.concatenate([y[1:], [np.nan]]) if n > 0 else np.array([])
135
+ if n > 1:
136
+ keep = np.concatenate([group[1:] == group[:-1], [False]])
137
+ elif n == 1:
138
+ keep = np.array([False])
139
+ else:
140
+ keep = np.array([], dtype=bool)
141
+
142
+ data["xend"] = xend
143
+ data["yend"] = yend
144
+ sub = data.loc[keep].reset_index(drop=True)
145
+
146
+ if len(sub) < 1:
147
+ return _ggname("geom_pointpath", grob_tree(pointgrob))
148
+
149
+ size = sub["size"].to_numpy(dtype="float64") if "size" in sub else np.full(len(sub), 1.5)
150
+ stroke = (
151
+ sub["stroke"].to_numpy(dtype="float64")
152
+ if "stroke" in sub
153
+ else np.full(len(sub), 0.5)
154
+ )
155
+ mult_aes = (
156
+ sub["mult"].to_numpy(dtype="float64")
157
+ if "mult" in sub
158
+ else np.full(len(sub), 0.5)
159
+ )
160
+ mult = (size * PT + stroke * STROKE / 2.0) * mult_aes
161
+
162
+ colour = sub["colour"].to_numpy() if "colour" in sub else "black"
163
+ alpha = sub["alpha"].to_numpy() if "alpha" in sub else None
164
+ linewidth = (
165
+ sub["linewidth"].to_numpy(dtype="float64")
166
+ if "linewidth" in sub
167
+ else np.full(len(sub), 0.5)
168
+ )
169
+ linetype = sub["linetype"].to_numpy() if "linetype" in sub else 1
170
+
171
+ gp = Gpar(
172
+ col=scales_alpha(colour, alpha),
173
+ fill=scales_alpha(colour, alpha),
174
+ lwd=linewidth * PT,
175
+ lty=linetype,
176
+ lineend="butt",
177
+ linejoin="round",
178
+ linemitre=10,
179
+ )
180
+
181
+ grob_cls = GapSegmentsGrob if coord.is_linear() else GapSegmentsChainGrob
182
+ my_path = grob_cls(
183
+ x0=Unit(sub["x"].to_numpy(dtype="float64"), "npc"),
184
+ x1=Unit(sub["xend"].to_numpy(dtype="float64"), "npc"),
185
+ y0=Unit(sub["y"].to_numpy(dtype="float64"), "npc"),
186
+ y1=Unit(sub["yend"].to_numpy(dtype="float64"), "npc"),
187
+ mult=mult,
188
+ id=sub["id"].to_numpy(),
189
+ arrow=arrow,
190
+ gp=gp,
191
+ name="pointpath",
192
+ )
193
+
194
+ return _ggname("geom_pointpath", grob_tree(my_path, pointgrob))
195
+
196
+
197
+ # R geom_pointpath.R:146 ``GeomPointpath <- GeomPointPath``.
198
+ GeomPointpath = GeomPointPath
199
+
200
+
201
+ def geom_pointpath(
202
+ mapping: Optional[Mapping] = None,
203
+ data: Any = None,
204
+ stat: str = "identity",
205
+ position: str = "identity",
206
+ na_rm: bool = False,
207
+ show_legend: Any = None,
208
+ arrow: Any = None,
209
+ inherit_aes: bool = True,
210
+ **kwargs: Any,
211
+ ) -> Any:
212
+ """Create a point-path layer.
213
+
214
+ Port of R ``geom_pointpath()`` (``geom_pointpath.R:43-63``). Connects
215
+ points with gapped line segments in data order, à la base R's
216
+ ``type = "b"``.
217
+
218
+ Parameters
219
+ ----------
220
+ mapping : Mapping, optional
221
+ Aesthetic mapping created by :func:`ggplot2_py.aes`.
222
+ data : Any, optional
223
+ Layer data.
224
+ stat : str, default ``"identity"``
225
+ Statistical transformation.
226
+ position : str, default ``"identity"``
227
+ Position adjustment.
228
+ na_rm : bool, default ``False``
229
+ If ``True``, silently remove missing values.
230
+ show_legend : bool or None, default ``None``
231
+ Whether to show a legend for this layer.
232
+ arrow : grid_py.Arrow or None, default ``None``
233
+ Arrow specification (see :func:`grid_py.arrow`) for the path ends.
234
+ inherit_aes : bool, default ``True``
235
+ Whether to inherit the plot's default aesthetics.
236
+ **kwargs : Any
237
+ Additional aesthetic parameters passed to the layer.
238
+
239
+ Returns
240
+ -------
241
+ ggplot2_py.Layer
242
+ A layer object that can be added to a plot.
243
+ """
244
+ from ggplot2_py.layer import layer
245
+
246
+ return layer(
247
+ data=data,
248
+ mapping=mapping,
249
+ stat=stat,
250
+ geom=GeomPointPath,
251
+ position=position,
252
+ show_legend=show_legend,
253
+ inherit_aes=inherit_aes,
254
+ params={
255
+ "na_rm": na_rm,
256
+ "arrow": arrow,
257
+ **kwargs,
258
+ },
259
+ )