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,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
|
+
)
|
ggh4x/geom_pointpath.py
ADDED
|
@@ -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
|
+
)
|