ggplot2-python 4.0.2.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.
- ggplot2_py/__init__.py +852 -0
- ggplot2_py/_compat.py +475 -0
- ggplot2_py/_plugins.py +129 -0
- ggplot2_py/_utils.py +544 -0
- ggplot2_py/aes.py +586 -0
- ggplot2_py/annotation.py +540 -0
- ggplot2_py/coord.py +2108 -0
- ggplot2_py/coords/__init__.py +49 -0
- ggplot2_py/datasets.py +265 -0
- ggplot2_py/draw_key.py +454 -0
- ggplot2_py/facet.py +1456 -0
- ggplot2_py/fortify.py +95 -0
- ggplot2_py/geom.py +4516 -0
- ggplot2_py/geoms/__init__.py +12 -0
- ggplot2_py/ggproto.py +279 -0
- ggplot2_py/guide.py +2925 -0
- ggplot2_py/guide_axis.py +615 -0
- ggplot2_py/guide_colourbar.py +657 -0
- ggplot2_py/guide_legend.py +1061 -0
- ggplot2_py/guides/__init__.py +8 -0
- ggplot2_py/labeller.py +296 -0
- ggplot2_py/labels.py +309 -0
- ggplot2_py/layer.py +954 -0
- ggplot2_py/layout.py +754 -0
- ggplot2_py/limits.py +314 -0
- ggplot2_py/plot.py +1401 -0
- ggplot2_py/plot_render.py +866 -0
- ggplot2_py/position.py +1269 -0
- ggplot2_py/protocols.py +171 -0
- ggplot2_py/py.typed +0 -0
- ggplot2_py/qplot.py +233 -0
- ggplot2_py/resources/diamonds.csv +53941 -0
- ggplot2_py/resources/economics.csv +575 -0
- ggplot2_py/resources/economics_long.csv +2871 -0
- ggplot2_py/resources/faithfuld.csv +5626 -0
- ggplot2_py/resources/luv_colours.csv +658 -0
- ggplot2_py/resources/midwest.csv +438 -0
- ggplot2_py/resources/mpg.csv +235 -0
- ggplot2_py/resources/msleep.csv +84 -0
- ggplot2_py/resources/presidential.csv +13 -0
- ggplot2_py/resources/seals.csv +1156 -0
- ggplot2_py/resources/txhousing.csv +8603 -0
- ggplot2_py/save.py +316 -0
- ggplot2_py/scale.py +2727 -0
- ggplot2_py/scales/__init__.py +4252 -0
- ggplot2_py/stat.py +6071 -0
- ggplot2_py/stats/__init__.py +9 -0
- ggplot2_py/theme.py +490 -0
- ggplot2_py/theme_defaults.py +1350 -0
- ggplot2_py/theme_elements.py +2052 -0
- ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
- ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
- ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
- ggplot2_python-4.0.2.9000.dist-info/licenses/LICENSE +3 -0
ggplot2_py/geom.py
ADDED
|
@@ -0,0 +1,4516 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geom classes and constructor functions for ggplot2_py.
|
|
3
|
+
|
|
4
|
+
This module contains the base ``Geom`` class (a ``GGProto`` subclass) and all
|
|
5
|
+
concrete geom implementations (``GeomPoint``, ``GeomPath``, ``GeomBar``, etc.)
|
|
6
|
+
together with their user-facing constructor functions (``geom_point``,
|
|
7
|
+
``geom_path``, ``geom_bar``, etc.).
|
|
8
|
+
|
|
9
|
+
Each ``Geom*`` class defines:
|
|
10
|
+
|
|
11
|
+
* ``required_aes`` -- tuple of required aesthetic names.
|
|
12
|
+
* ``non_missing_aes`` -- aesthetics whose ``NA`` values trigger row removal.
|
|
13
|
+
* ``optional_aes`` -- aesthetics accepted but not required.
|
|
14
|
+
* ``default_aes`` -- a :class:`Mapping` with default values.
|
|
15
|
+
* ``extra_params`` -- extra non-aesthetic parameter names.
|
|
16
|
+
* ``draw_key`` -- legend key drawing function.
|
|
17
|
+
* ``setup_params`` / ``setup_data`` -- data/parameter preprocessing.
|
|
18
|
+
* ``draw_panel`` or ``draw_group`` -- the actual grob-creation method.
|
|
19
|
+
|
|
20
|
+
Each ``geom_*()`` function is a thin wrapper that calls ``layer()``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import warnings
|
|
26
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import pandas as pd
|
|
30
|
+
|
|
31
|
+
from ggplot2_py.ggproto import GGProto, ggproto, ggproto_parent
|
|
32
|
+
from ggplot2_py._compat import Waiver, is_waiver, waiver, cli_abort, cli_warn
|
|
33
|
+
from ggplot2_py._utils import (
|
|
34
|
+
remove_missing,
|
|
35
|
+
resolution,
|
|
36
|
+
snake_class,
|
|
37
|
+
compact,
|
|
38
|
+
data_frame,
|
|
39
|
+
empty,
|
|
40
|
+
)
|
|
41
|
+
from ggplot2_py.aes import (
|
|
42
|
+
aes, Mapping, standardise_aes_names,
|
|
43
|
+
AfterScale, AfterStat, Stage, eval_aes_value,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Import draw_key functions
|
|
47
|
+
from ggplot2_py.draw_key import (
|
|
48
|
+
draw_key_point,
|
|
49
|
+
draw_key_path,
|
|
50
|
+
draw_key_rect,
|
|
51
|
+
draw_key_polygon,
|
|
52
|
+
draw_key_blank,
|
|
53
|
+
draw_key_boxplot,
|
|
54
|
+
draw_key_crossbar,
|
|
55
|
+
draw_key_dotplot,
|
|
56
|
+
draw_key_label,
|
|
57
|
+
draw_key_linerange,
|
|
58
|
+
draw_key_pointrange,
|
|
59
|
+
draw_key_smooth,
|
|
60
|
+
draw_key_text,
|
|
61
|
+
draw_key_abline,
|
|
62
|
+
draw_key_vline,
|
|
63
|
+
draw_key_timeseries,
|
|
64
|
+
draw_key_vpath,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# grid_py grob creation imports
|
|
68
|
+
from grid_py import (
|
|
69
|
+
points_grob,
|
|
70
|
+
rect_grob,
|
|
71
|
+
lines_grob,
|
|
72
|
+
segments_grob,
|
|
73
|
+
polygon_grob,
|
|
74
|
+
polyline_grob,
|
|
75
|
+
text_grob,
|
|
76
|
+
circle_grob,
|
|
77
|
+
raster_grob,
|
|
78
|
+
path_grob,
|
|
79
|
+
curve_grob,
|
|
80
|
+
null_grob,
|
|
81
|
+
Gpar,
|
|
82
|
+
Unit,
|
|
83
|
+
grob_tree,
|
|
84
|
+
GTree,
|
|
85
|
+
GList,
|
|
86
|
+
clip_grob,
|
|
87
|
+
Viewport,
|
|
88
|
+
edit_grob,
|
|
89
|
+
roundrect_grob,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
from scales import alpha as _scales_alpha_raw
|
|
93
|
+
|
|
94
|
+
import re as _re
|
|
95
|
+
|
|
96
|
+
def _r_col_to_mpl(c):
|
|
97
|
+
"""Convert R-style grey names to RGB tuples for matplotlib."""
|
|
98
|
+
if isinstance(c, str):
|
|
99
|
+
m = _re.match(r'^gr[ae]y(\d{1,3})$', c)
|
|
100
|
+
if m:
|
|
101
|
+
v = int(m.group(1)) / 100.0
|
|
102
|
+
return f"#{int(v*255):02x}{int(v*255):02x}{int(v*255):02x}"
|
|
103
|
+
return c
|
|
104
|
+
|
|
105
|
+
def scales_alpha(colour, alpha):
|
|
106
|
+
"""Apply alpha to colours, converting R colour names first."""
|
|
107
|
+
if isinstance(colour, (list, np.ndarray)):
|
|
108
|
+
colour = [_r_col_to_mpl(c) for c in colour]
|
|
109
|
+
elif isinstance(colour, str):
|
|
110
|
+
colour = _r_col_to_mpl(colour)
|
|
111
|
+
return _scales_alpha_raw(colour, alpha)
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
# Base class
|
|
115
|
+
"Geom",
|
|
116
|
+
# ggproto classes
|
|
117
|
+
"GeomPoint", "GeomPath", "GeomLine", "GeomStep",
|
|
118
|
+
"GeomBar", "GeomCol", "GeomRect", "GeomTile", "GeomRaster",
|
|
119
|
+
"GeomText", "GeomLabel",
|
|
120
|
+
"GeomBoxplot", "GeomViolin", "GeomDotplot",
|
|
121
|
+
"GeomRibbon", "GeomArea", "GeomSmooth",
|
|
122
|
+
"GeomPolygon",
|
|
123
|
+
"GeomErrorbar", "GeomErrorbarh", "GeomCrossbar", "GeomLinerange", "GeomPointrange",
|
|
124
|
+
"GeomSegment", "GeomCurve", "GeomSpoke",
|
|
125
|
+
"GeomDensity", "GeomDensity2d", "GeomDensity2dFilled",
|
|
126
|
+
"GeomContour", "GeomContourFilled",
|
|
127
|
+
"GeomHex", "GeomBin2d",
|
|
128
|
+
"GeomAbline", "GeomHline", "GeomVline",
|
|
129
|
+
"GeomRug",
|
|
130
|
+
"GeomBlank",
|
|
131
|
+
"GeomFunction",
|
|
132
|
+
"GeomFreqpoly", "GeomHistogram",
|
|
133
|
+
"GeomCount",
|
|
134
|
+
"GeomMap",
|
|
135
|
+
"GeomQuantile",
|
|
136
|
+
"GeomJitter",
|
|
137
|
+
"GeomSf", "GeomAnnotationMap", "GeomCustomAnn", "GeomRasterAnn", "GeomLogticks",
|
|
138
|
+
# Constructor functions
|
|
139
|
+
"geom_point", "geom_path", "geom_line", "geom_step",
|
|
140
|
+
"geom_bar", "geom_col", "geom_rect", "geom_tile", "geom_raster",
|
|
141
|
+
"geom_text", "geom_label",
|
|
142
|
+
"geom_boxplot", "geom_violin", "geom_dotplot",
|
|
143
|
+
"geom_ribbon", "geom_area", "geom_smooth",
|
|
144
|
+
"geom_polygon",
|
|
145
|
+
"geom_errorbar", "geom_errorbarh", "geom_crossbar", "geom_linerange", "geom_pointrange",
|
|
146
|
+
"geom_segment", "geom_curve", "geom_spoke",
|
|
147
|
+
"geom_density", "geom_density2d", "geom_density2d_filled",
|
|
148
|
+
"geom_density_2d", "geom_density_2d_filled",
|
|
149
|
+
"geom_contour", "geom_contour_filled",
|
|
150
|
+
"geom_hex", "geom_bin2d", "geom_bin_2d",
|
|
151
|
+
"geom_abline", "geom_hline", "geom_vline",
|
|
152
|
+
"geom_rug",
|
|
153
|
+
"geom_blank",
|
|
154
|
+
"geom_function",
|
|
155
|
+
"geom_freqpoly", "geom_histogram",
|
|
156
|
+
"geom_count",
|
|
157
|
+
"geom_map",
|
|
158
|
+
"geom_quantile",
|
|
159
|
+
"geom_jitter",
|
|
160
|
+
"geom_sf", "geom_sf_label", "geom_sf_text",
|
|
161
|
+
"geom_qq", "geom_qq_line",
|
|
162
|
+
# Utility
|
|
163
|
+
"is_geom",
|
|
164
|
+
"translate_shape_string",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ===========================================================================
|
|
169
|
+
# from_theme — theme-aware default aesthetics (R: properties.R / aes-delayed-eval.R)
|
|
170
|
+
# ===========================================================================
|
|
171
|
+
|
|
172
|
+
class FromTheme:
|
|
173
|
+
"""Marker for a default aesthetic that should be resolved from the theme.
|
|
174
|
+
|
|
175
|
+
R's ``from_theme()`` records an expression over the
|
|
176
|
+
``element_geom`` properties and re-evaluates it at draw time,
|
|
177
|
+
e.g. ``from_theme(fill %||% col_mix(ink, paper, 0.35))``.
|
|
178
|
+
|
|
179
|
+
Python equivalents::
|
|
180
|
+
|
|
181
|
+
FromTheme("pointsize") # R: from_theme(pointsize)
|
|
182
|
+
FromTheme("colour", fallback="ink") # R: from_theme(colour %||% ink)
|
|
183
|
+
FromTheme("fill", fallback=lambda g: col_mix(g.ink, g.paper, 0.35))
|
|
184
|
+
|
|
185
|
+
Any callable passed as ``fallback`` receives the resolved
|
|
186
|
+
``element_geom`` and must return the final value. ``%||%``
|
|
187
|
+
semantics apply — the fallback is only used when the primary
|
|
188
|
+
property is ``None``.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
__slots__ = ("_prop", "_fallback")
|
|
192
|
+
|
|
193
|
+
def __init__(self, prop: str, fallback: Any = None):
|
|
194
|
+
self._prop = prop
|
|
195
|
+
# fallback: None | str | callable(element_geom) -> value
|
|
196
|
+
self._fallback = fallback
|
|
197
|
+
|
|
198
|
+
def resolve(self, geom_el: Any) -> Any:
|
|
199
|
+
"""Evaluate against an ``element_geom``.
|
|
200
|
+
|
|
201
|
+
R semantics: ``x %||% y`` uses ``y`` only if ``x`` is ``NULL``.
|
|
202
|
+
When ``prop`` is a callable, it is invoked with the
|
|
203
|
+
``element_geom`` directly — used for expressions that are not
|
|
204
|
+
plain property lookups, e.g. ``from_theme(2 * linewidth)``
|
|
205
|
+
(geom-smooth.R:55).
|
|
206
|
+
"""
|
|
207
|
+
if callable(self._prop):
|
|
208
|
+
return self._prop(geom_el)
|
|
209
|
+
val = getattr(geom_el, self._prop, None)
|
|
210
|
+
if val is not None:
|
|
211
|
+
return val
|
|
212
|
+
fb = self._fallback
|
|
213
|
+
if fb is None:
|
|
214
|
+
return None
|
|
215
|
+
if callable(fb):
|
|
216
|
+
return fb(geom_el)
|
|
217
|
+
# String-name fallback: another property on the element_geom
|
|
218
|
+
return getattr(geom_el, fb, None)
|
|
219
|
+
|
|
220
|
+
def __repr__(self) -> str:
|
|
221
|
+
return f"FromTheme({self._prop!r})"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Default element_geom properties (R: theme-elements.R:356-363
|
|
225
|
+
# `.default_geom_element`). These are the *exact* values hardcoded
|
|
226
|
+
# in R; we match them verbatim.
|
|
227
|
+
_DEFAULT_GEOM_PROPS = {
|
|
228
|
+
"ink": "black",
|
|
229
|
+
"paper": "white",
|
|
230
|
+
"accent": "#3366FF",
|
|
231
|
+
"linewidth": 0.5,
|
|
232
|
+
"borderwidth": 0.5,
|
|
233
|
+
"linetype": 1,
|
|
234
|
+
"bordertype": 1,
|
|
235
|
+
"family": "",
|
|
236
|
+
"fontsize": 11,
|
|
237
|
+
"pointsize": 1.5,
|
|
238
|
+
"pointshape": 19,
|
|
239
|
+
"fill": None,
|
|
240
|
+
"colour": None,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class _DefaultGeomElement:
|
|
245
|
+
"""Fallback geom element when theme has no ``"geom"`` entry.
|
|
246
|
+
|
|
247
|
+
Mirrors R's ``.default_geom_element`` (theme-elements.R:356-363).
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
def __getattr__(self, name: str) -> Any:
|
|
251
|
+
if name in _DEFAULT_GEOM_PROPS:
|
|
252
|
+
return _DEFAULT_GEOM_PROPS[name]
|
|
253
|
+
raise AttributeError(name)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _eval_from_theme(default_aes: "Mapping", theme: Any) -> "Mapping":
|
|
257
|
+
"""Resolve any ``FromTheme`` markers in *default_aes* using *theme*.
|
|
258
|
+
|
|
259
|
+
Mirrors R's ``eval_from_theme()`` (geom-.R:471-498). Falls back
|
|
260
|
+
to ``_DefaultGeomElement`` when ``theme$geom`` is absent — matching
|
|
261
|
+
R which uses ``.default_geom_element`` in that case.
|
|
262
|
+
"""
|
|
263
|
+
has_themed = any(isinstance(v, FromTheme) for v in default_aes.values())
|
|
264
|
+
if not has_themed:
|
|
265
|
+
return default_aes
|
|
266
|
+
|
|
267
|
+
# Get the geom element from theme, else fall back to R's default
|
|
268
|
+
from ggplot2_py.theme_elements import calc_element, ElementGeom
|
|
269
|
+
theme_geom_el = None
|
|
270
|
+
if theme is not None:
|
|
271
|
+
theme_geom_el = calc_element("geom", theme)
|
|
272
|
+
# calc_element may return a partially-populated ElementGeom (user
|
|
273
|
+
# only set some props). Wrap it with defaults for missing props.
|
|
274
|
+
default_el = _DefaultGeomElement()
|
|
275
|
+
if theme_geom_el is None:
|
|
276
|
+
geom_el = default_el
|
|
277
|
+
elif isinstance(theme_geom_el, ElementGeom):
|
|
278
|
+
# Proxy: read from theme first, then .default_geom_element.
|
|
279
|
+
# Capturing ``theme_geom_el`` (not ``geom_el``) avoids the
|
|
280
|
+
# re-assignment masking the closure cell and recursing.
|
|
281
|
+
_src = theme_geom_el
|
|
282
|
+
_defaults = default_el
|
|
283
|
+
class _MergedGeomElement:
|
|
284
|
+
def __getattr__(self, name):
|
|
285
|
+
v = getattr(_src, name, None)
|
|
286
|
+
if v is not None:
|
|
287
|
+
return v
|
|
288
|
+
return getattr(_defaults, name)
|
|
289
|
+
geom_el = _MergedGeomElement()
|
|
290
|
+
else:
|
|
291
|
+
geom_el = theme_geom_el
|
|
292
|
+
|
|
293
|
+
resolved = {}
|
|
294
|
+
for key, val in default_aes.items():
|
|
295
|
+
if isinstance(val, FromTheme):
|
|
296
|
+
resolved[key] = val.resolve(geom_el)
|
|
297
|
+
else:
|
|
298
|
+
resolved[key] = val
|
|
299
|
+
return Mapping(**resolved)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ===========================================================================
|
|
303
|
+
# Graphical-unit constants
|
|
304
|
+
# ===========================================================================
|
|
305
|
+
|
|
306
|
+
#: Points per mm (``72.27 / 25.4``)
|
|
307
|
+
PT: float = 72.27 / 25.4
|
|
308
|
+
#: Stroke scale factor (``96 / 25.4``)
|
|
309
|
+
STROKE: float = 96 / 25.4
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ===========================================================================
|
|
313
|
+
# Utilities
|
|
314
|
+
# ===========================================================================
|
|
315
|
+
|
|
316
|
+
def _fill_alpha(fill: Any, alpha_val: Any) -> Any:
|
|
317
|
+
"""Apply alpha to a fill colour, passing through ``None``."""
|
|
318
|
+
if fill is None:
|
|
319
|
+
return None
|
|
320
|
+
try:
|
|
321
|
+
return scales_alpha(fill, alpha_val)
|
|
322
|
+
except Exception:
|
|
323
|
+
return fill
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _gg_par(**kwargs: Any) -> Gpar:
|
|
327
|
+
"""Build a :class:`Gpar` filtering out ``None`` entries and converting
|
|
328
|
+
``linewidth`` (mm) to ``lwd`` (pts) when needed."""
|
|
329
|
+
# Convert lwd from mm to pts
|
|
330
|
+
if "lwd" in kwargs and kwargs["lwd"] is not None:
|
|
331
|
+
try:
|
|
332
|
+
kwargs["lwd"] = np.asarray(kwargs["lwd"], dtype=float) * PT
|
|
333
|
+
except (TypeError, ValueError):
|
|
334
|
+
pass
|
|
335
|
+
filtered = {k: v for k, v in kwargs.items() if v is not None}
|
|
336
|
+
return Gpar(**filtered)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _ggname(prefix: str, grob: Any) -> Any:
|
|
340
|
+
"""Attach a name prefix to a grob (used for identification in grob trees)."""
|
|
341
|
+
try:
|
|
342
|
+
grob.name = prefix
|
|
343
|
+
except AttributeError:
|
|
344
|
+
pass
|
|
345
|
+
return grob
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Shape translation
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
_PCH_TABLE: Dict[str, int] = {
|
|
353
|
+
"square open": 0,
|
|
354
|
+
"circle open": 1,
|
|
355
|
+
"triangle open": 2,
|
|
356
|
+
"plus": 3,
|
|
357
|
+
"cross": 4,
|
|
358
|
+
"diamond open": 5,
|
|
359
|
+
"triangle down open": 6,
|
|
360
|
+
"square cross": 7,
|
|
361
|
+
"asterisk": 8,
|
|
362
|
+
"diamond plus": 9,
|
|
363
|
+
"circle plus": 10,
|
|
364
|
+
"star": 11,
|
|
365
|
+
"square plus": 12,
|
|
366
|
+
"circle cross": 13,
|
|
367
|
+
"square triangle": 14,
|
|
368
|
+
"triangle square": 14,
|
|
369
|
+
"square": 15,
|
|
370
|
+
"circle small": 16,
|
|
371
|
+
"triangle": 17,
|
|
372
|
+
"diamond": 18,
|
|
373
|
+
"circle": 19,
|
|
374
|
+
"bullet": 20,
|
|
375
|
+
"circle filled": 21,
|
|
376
|
+
"square filled": 22,
|
|
377
|
+
"diamond filled": 23,
|
|
378
|
+
"triangle filled": 24,
|
|
379
|
+
"triangle down filled": 25,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def translate_shape_string(shape: Any) -> Any:
|
|
384
|
+
"""Translate point shape names to integer pch codes.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
shape : str or array-like of str, or numeric
|
|
389
|
+
Shape specification. If numeric or single-character strings,
|
|
390
|
+
returned as-is.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
int or array-like
|
|
395
|
+
Integer pch values.
|
|
396
|
+
"""
|
|
397
|
+
if shape is None:
|
|
398
|
+
return 19 # default circle
|
|
399
|
+
if isinstance(shape, (int, float, np.integer, np.floating)):
|
|
400
|
+
return int(shape)
|
|
401
|
+
if isinstance(shape, str):
|
|
402
|
+
if len(shape) <= 1:
|
|
403
|
+
return shape
|
|
404
|
+
lower = shape.lower()
|
|
405
|
+
for name, code in _PCH_TABLE.items():
|
|
406
|
+
if name.startswith(lower):
|
|
407
|
+
return code
|
|
408
|
+
cli_abort(f"Shape aesthetic contains invalid value: {shape!r}.")
|
|
409
|
+
# array-like
|
|
410
|
+
if hasattr(shape, "__iter__"):
|
|
411
|
+
return np.array([translate_shape_string(s) for s in shape])
|
|
412
|
+
return shape
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def is_geom(x: Any) -> bool:
|
|
416
|
+
"""Return ``True`` if *x* is a ``Geom`` subclass or instance."""
|
|
417
|
+
if isinstance(x, type):
|
|
418
|
+
return issubclass(x, Geom)
|
|
419
|
+
return isinstance(x, Geom)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ===========================================================================
|
|
423
|
+
# Base Geom class
|
|
424
|
+
# ===========================================================================
|
|
425
|
+
|
|
426
|
+
class Geom(GGProto):
|
|
427
|
+
"""Base class for all geometry objects.
|
|
428
|
+
|
|
429
|
+
Subclasses must override at least ``draw_panel`` or ``draw_group``.
|
|
430
|
+
|
|
431
|
+
Attributes
|
|
432
|
+
----------
|
|
433
|
+
required_aes : tuple of str
|
|
434
|
+
Aesthetics that *must* be present.
|
|
435
|
+
non_missing_aes : tuple of str
|
|
436
|
+
Aesthetics that trigger row-removal if ``NA``.
|
|
437
|
+
optional_aes : tuple of str
|
|
438
|
+
Extra accepted aesthetics.
|
|
439
|
+
default_aes : Mapping
|
|
440
|
+
Default aesthetic values.
|
|
441
|
+
extra_params : tuple of str
|
|
442
|
+
Extra non-aesthetic parameters (e.g. ``"na_rm"``).
|
|
443
|
+
draw_key : callable
|
|
444
|
+
Legend key drawing function.
|
|
445
|
+
rename_size : bool
|
|
446
|
+
Whether to rename ``size`` to ``linewidth``.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
# --- Auto-registration registry (Python-exclusive) -------------------
|
|
450
|
+
_registry: Dict[str, Any] = {}
|
|
451
|
+
|
|
452
|
+
required_aes: Tuple[str, ...] = ()
|
|
453
|
+
non_missing_aes: Tuple[str, ...] = ()
|
|
454
|
+
optional_aes: Tuple[str, ...] = ()
|
|
455
|
+
default_aes: Mapping = Mapping()
|
|
456
|
+
extra_params: Tuple[str, ...] = ("na_rm",)
|
|
457
|
+
draw_key = draw_key_point
|
|
458
|
+
rename_size: bool = False
|
|
459
|
+
|
|
460
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
461
|
+
super().__init_subclass__(**kwargs)
|
|
462
|
+
# Auto-register: GeomPoint -> "point", GeomBar -> "bar", etc.
|
|
463
|
+
name = cls.__name__
|
|
464
|
+
if name.startswith("Geom") and len(name) > 4:
|
|
465
|
+
key = name[4:] # strip "Geom" prefix
|
|
466
|
+
# Store both CamelCase and lower-case keys
|
|
467
|
+
Geom._registry[key] = cls
|
|
468
|
+
Geom._registry[key.lower()] = cls
|
|
469
|
+
|
|
470
|
+
# -----------------------------------------------------------------------
|
|
471
|
+
# Setup hooks (run before position adjustments)
|
|
472
|
+
# -----------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
475
|
+
"""Modify or validate parameters given the data.
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
data : DataFrame
|
|
480
|
+
Layer data.
|
|
481
|
+
params : dict
|
|
482
|
+
Current parameters.
|
|
483
|
+
|
|
484
|
+
Returns
|
|
485
|
+
-------
|
|
486
|
+
dict
|
|
487
|
+
Possibly modified parameters.
|
|
488
|
+
"""
|
|
489
|
+
return params
|
|
490
|
+
|
|
491
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
492
|
+
"""Modify or validate data before defaults are applied.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
data : DataFrame
|
|
497
|
+
Layer data.
|
|
498
|
+
params : dict
|
|
499
|
+
Parameters from ``setup_params``.
|
|
500
|
+
|
|
501
|
+
Returns
|
|
502
|
+
-------
|
|
503
|
+
DataFrame
|
|
504
|
+
"""
|
|
505
|
+
return data
|
|
506
|
+
|
|
507
|
+
# -----------------------------------------------------------------------
|
|
508
|
+
# Missing-value handling
|
|
509
|
+
# -----------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
def handle_na(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
512
|
+
"""Remove rows with missing values in required aesthetics.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
data : DataFrame
|
|
517
|
+
params : dict
|
|
518
|
+
|
|
519
|
+
Returns
|
|
520
|
+
-------
|
|
521
|
+
DataFrame
|
|
522
|
+
"""
|
|
523
|
+
na_rm = params.get("na_rm", params.get("na.rm", False))
|
|
524
|
+
check_vars = list(self.required_aes) + list(self.non_missing_aes)
|
|
525
|
+
return remove_missing(data, vars=check_vars, na_rm=na_rm, name=snake_class(self))
|
|
526
|
+
|
|
527
|
+
# -----------------------------------------------------------------------
|
|
528
|
+
# use_defaults -- fill in default aesthetics
|
|
529
|
+
# -----------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
def use_defaults(
|
|
532
|
+
self,
|
|
533
|
+
data: pd.DataFrame,
|
|
534
|
+
params: Optional[Dict[str, Any]] = None,
|
|
535
|
+
modifiers: Optional[Mapping] = None,
|
|
536
|
+
default_aes: Optional[Mapping] = None,
|
|
537
|
+
theme: Any = None,
|
|
538
|
+
) -> pd.DataFrame:
|
|
539
|
+
"""Fill missing aesthetics with defaults and apply parameter overrides.
|
|
540
|
+
|
|
541
|
+
Parameters
|
|
542
|
+
----------
|
|
543
|
+
data : DataFrame
|
|
544
|
+
params : dict, optional
|
|
545
|
+
modifiers : Mapping, optional
|
|
546
|
+
default_aes : Mapping, optional
|
|
547
|
+
theme : optional
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
DataFrame
|
|
552
|
+
"""
|
|
553
|
+
if params is None:
|
|
554
|
+
params = {}
|
|
555
|
+
if modifiers is None:
|
|
556
|
+
modifiers = Mapping()
|
|
557
|
+
if default_aes is None:
|
|
558
|
+
default_aes = self.default_aes
|
|
559
|
+
|
|
560
|
+
# Resolve FromTheme markers using the theme (R: eval_from_theme)
|
|
561
|
+
default_aes = _eval_from_theme(default_aes, theme)
|
|
562
|
+
|
|
563
|
+
# Inherit size as linewidth when applicable
|
|
564
|
+
if self.rename_size:
|
|
565
|
+
if data is not None and "linewidth" not in data.columns and "size" in data.columns:
|
|
566
|
+
data = data.copy()
|
|
567
|
+
data["linewidth"] = data["size"]
|
|
568
|
+
if "linewidth" not in params and "size" in params:
|
|
569
|
+
params["linewidth"] = params["size"]
|
|
570
|
+
|
|
571
|
+
# Fill in missing aesthetics with their defaults
|
|
572
|
+
if data is not None and not data.empty:
|
|
573
|
+
for aes_name, default_val in default_aes.items():
|
|
574
|
+
if aes_name not in data.columns:
|
|
575
|
+
data[aes_name] = default_val
|
|
576
|
+
|
|
577
|
+
# Override with params
|
|
578
|
+
aes_params = set(self.aesthetics()) & set(params.keys())
|
|
579
|
+
if data is not None and not data.empty:
|
|
580
|
+
for ap in aes_params:
|
|
581
|
+
data[ap] = params[ap]
|
|
582
|
+
|
|
583
|
+
# Evaluate after_scale modifiers (R ref: geom-.R:243-265).
|
|
584
|
+
# R calls eval_aesthetics(substitute_aes(modifiers), data,
|
|
585
|
+
# mask=list(stage=stage_scaled)).
|
|
586
|
+
# In Python, modifiers is a dict of AfterScale/Stage objects whose
|
|
587
|
+
# after_scale slot should be evaluated against the now-complete data.
|
|
588
|
+
if modifiers and data is not None and not data.empty:
|
|
589
|
+
for aes_name, mod_val in modifiers.items():
|
|
590
|
+
target = None
|
|
591
|
+
if isinstance(mod_val, AfterScale):
|
|
592
|
+
target = mod_val.x
|
|
593
|
+
elif isinstance(mod_val, Stage) and mod_val.after_scale is not None:
|
|
594
|
+
as_obj = mod_val.after_scale
|
|
595
|
+
target = as_obj.x if isinstance(as_obj, AfterScale) else as_obj
|
|
596
|
+
if target is not None:
|
|
597
|
+
try:
|
|
598
|
+
result = eval_aes_value(target, data)
|
|
599
|
+
if result is not None:
|
|
600
|
+
data[aes_name] = result
|
|
601
|
+
except Exception:
|
|
602
|
+
# R: cli::cli_warn("Unable to apply staged modifications.")
|
|
603
|
+
import warnings
|
|
604
|
+
warnings.warn(
|
|
605
|
+
f"Unable to apply after_scale modifier for '{aes_name}'.",
|
|
606
|
+
stacklevel=2,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
return data
|
|
610
|
+
|
|
611
|
+
# -----------------------------------------------------------------------
|
|
612
|
+
# Drawing
|
|
613
|
+
# -----------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
def draw_layer(
|
|
616
|
+
self,
|
|
617
|
+
data: pd.DataFrame,
|
|
618
|
+
params: Dict[str, Any],
|
|
619
|
+
layout: Any,
|
|
620
|
+
coord: Any,
|
|
621
|
+
) -> List[Any]:
|
|
622
|
+
"""Orchestrate drawing for all panels.
|
|
623
|
+
|
|
624
|
+
Parameters
|
|
625
|
+
----------
|
|
626
|
+
data : DataFrame
|
|
627
|
+
params : dict
|
|
628
|
+
layout : Layout
|
|
629
|
+
coord : Coord
|
|
630
|
+
|
|
631
|
+
Returns
|
|
632
|
+
-------
|
|
633
|
+
list of grobs
|
|
634
|
+
"""
|
|
635
|
+
if data is None or (hasattr(data, "empty") and data.empty):
|
|
636
|
+
return [null_grob()]
|
|
637
|
+
|
|
638
|
+
# Split by PANEL
|
|
639
|
+
if "PANEL" in data.columns:
|
|
640
|
+
panels = {k: v for k, v in data.groupby("PANEL", observed=True)}
|
|
641
|
+
else:
|
|
642
|
+
panels = {1: data}
|
|
643
|
+
|
|
644
|
+
grobs = []
|
|
645
|
+
for panel_id, panel_data in panels.items():
|
|
646
|
+
if panel_data.empty:
|
|
647
|
+
grobs.append(null_grob())
|
|
648
|
+
continue
|
|
649
|
+
# PANEL is 1-based, panel_params list is 0-based
|
|
650
|
+
idx = int(panel_id) - 1 if isinstance(panel_id, (int, np.integer)) else panel_id
|
|
651
|
+
panel_params = layout.panel_params[idx]
|
|
652
|
+
grobs.append(self.draw_panel(panel_data, panel_params, coord, **params))
|
|
653
|
+
return grobs
|
|
654
|
+
|
|
655
|
+
def draw_panel(
|
|
656
|
+
self,
|
|
657
|
+
data: pd.DataFrame,
|
|
658
|
+
panel_params: Any,
|
|
659
|
+
coord: Any,
|
|
660
|
+
**params: Any,
|
|
661
|
+
) -> Any:
|
|
662
|
+
"""Draw the geom for a single panel.
|
|
663
|
+
|
|
664
|
+
The default implementation splits on ``group`` and delegates to
|
|
665
|
+
``draw_group``.
|
|
666
|
+
|
|
667
|
+
Parameters
|
|
668
|
+
----------
|
|
669
|
+
data : DataFrame
|
|
670
|
+
panel_params : panel parameters
|
|
671
|
+
coord : Coord
|
|
672
|
+
|
|
673
|
+
Returns
|
|
674
|
+
-------
|
|
675
|
+
grob
|
|
676
|
+
"""
|
|
677
|
+
if "group" not in data.columns:
|
|
678
|
+
return self.draw_group(data, panel_params, coord, **params)
|
|
679
|
+
|
|
680
|
+
groups = {k: v for k, v in data.groupby("group")}
|
|
681
|
+
grobs = []
|
|
682
|
+
for _, group_data in groups.items():
|
|
683
|
+
grobs.append(self.draw_group(group_data, panel_params, coord, **params))
|
|
684
|
+
|
|
685
|
+
return _ggname(
|
|
686
|
+
snake_class(self),
|
|
687
|
+
grob_tree(*grobs) if grobs else null_grob(),
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def draw_group(
|
|
691
|
+
self,
|
|
692
|
+
data: pd.DataFrame,
|
|
693
|
+
panel_params: Any,
|
|
694
|
+
coord: Any,
|
|
695
|
+
**params: Any,
|
|
696
|
+
) -> Any:
|
|
697
|
+
"""Draw the geom for a single group.
|
|
698
|
+
|
|
699
|
+
Must be overridden by subclasses that need per-group drawing.
|
|
700
|
+
|
|
701
|
+
Parameters
|
|
702
|
+
----------
|
|
703
|
+
data : DataFrame
|
|
704
|
+
panel_params : panel parameters
|
|
705
|
+
coord : Coord
|
|
706
|
+
|
|
707
|
+
Returns
|
|
708
|
+
-------
|
|
709
|
+
grob
|
|
710
|
+
"""
|
|
711
|
+
cli_abort(f"{snake_class(self)} has not implemented a draw_group method")
|
|
712
|
+
|
|
713
|
+
# -----------------------------------------------------------------------
|
|
714
|
+
# Utility methods
|
|
715
|
+
# -----------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
def parameters(self, extra: bool = False) -> List[str]:
|
|
718
|
+
"""List acceptable parameters for this geom.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
extra : bool
|
|
723
|
+
Whether to include ``extra_params``.
|
|
724
|
+
|
|
725
|
+
Returns
|
|
726
|
+
-------
|
|
727
|
+
list of str
|
|
728
|
+
"""
|
|
729
|
+
import inspect
|
|
730
|
+
sig = inspect.signature(self.draw_panel)
|
|
731
|
+
args = [p for p in sig.parameters if p not in ("self", "data", "panel_params", "coord")]
|
|
732
|
+
if extra:
|
|
733
|
+
args = list(set(args) | set(self.extra_params))
|
|
734
|
+
return args
|
|
735
|
+
|
|
736
|
+
def aesthetics(self) -> List[str]:
|
|
737
|
+
"""List all accepted aesthetics.
|
|
738
|
+
|
|
739
|
+
Returns
|
|
740
|
+
-------
|
|
741
|
+
list of str
|
|
742
|
+
"""
|
|
743
|
+
required = []
|
|
744
|
+
for aes_name in self.required_aes:
|
|
745
|
+
required.extend(aes_name.split("|"))
|
|
746
|
+
|
|
747
|
+
aes_names = list(dict.fromkeys(required + list(self.default_aes.keys())))
|
|
748
|
+
aes_names.extend(a for a in self.optional_aes if a not in aes_names)
|
|
749
|
+
if "group" not in aes_names:
|
|
750
|
+
aes_names.append("group")
|
|
751
|
+
return aes_names
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ===========================================================================
|
|
755
|
+
# Helper: _coord_transform
|
|
756
|
+
# ===========================================================================
|
|
757
|
+
|
|
758
|
+
def _coord_transform(coord: Any, data: pd.DataFrame, panel_params: Any) -> pd.DataFrame:
|
|
759
|
+
"""Safely apply coordinate transformation."""
|
|
760
|
+
if coord is not None and hasattr(coord, "transform"):
|
|
761
|
+
return coord.transform(data, panel_params)
|
|
762
|
+
return data
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
# ===========================================================================
|
|
766
|
+
# GeomPoint
|
|
767
|
+
# ===========================================================================
|
|
768
|
+
|
|
769
|
+
class GeomPoint(Geom):
|
|
770
|
+
"""Point geom (scatterplot)."""
|
|
771
|
+
|
|
772
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
773
|
+
non_missing_aes: Tuple[str, ...] = ("size", "shape", "colour")
|
|
774
|
+
# R: aes(shape=from_theme(pointshape), colour=from_theme(colour %||% ink),
|
|
775
|
+
# fill=from_theme(fill %||% NA), size=from_theme(pointsize),
|
|
776
|
+
# alpha=NA, stroke=from_theme(borderwidth))
|
|
777
|
+
default_aes: Mapping = Mapping(
|
|
778
|
+
shape=FromTheme("pointshape"),
|
|
779
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
780
|
+
fill=FromTheme("fill"),
|
|
781
|
+
size=FromTheme("pointsize"),
|
|
782
|
+
alpha=None,
|
|
783
|
+
stroke=FromTheme("borderwidth"),
|
|
784
|
+
)
|
|
785
|
+
draw_key = draw_key_point
|
|
786
|
+
|
|
787
|
+
def draw_panel(
|
|
788
|
+
self,
|
|
789
|
+
data: pd.DataFrame,
|
|
790
|
+
panel_params: Any,
|
|
791
|
+
coord: Any,
|
|
792
|
+
na_rm: bool = False,
|
|
793
|
+
**params: Any,
|
|
794
|
+
) -> Any:
|
|
795
|
+
"""Draw points.
|
|
796
|
+
|
|
797
|
+
Parameters
|
|
798
|
+
----------
|
|
799
|
+
data : DataFrame
|
|
800
|
+
panel_params : panel parameters
|
|
801
|
+
coord : Coord
|
|
802
|
+
na_rm : bool
|
|
803
|
+
|
|
804
|
+
Returns
|
|
805
|
+
-------
|
|
806
|
+
grob
|
|
807
|
+
"""
|
|
808
|
+
data = data.copy()
|
|
809
|
+
if "shape" in data.columns:
|
|
810
|
+
data["shape"] = data["shape"].apply(translate_shape_string)
|
|
811
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
812
|
+
|
|
813
|
+
# R (utilities-grid.R:35-43 gg_par):
|
|
814
|
+
# args$lwd = stroke * .stroke / 2
|
|
815
|
+
# args$fontsize = pointsize * .pt + stroke * .stroke / 2
|
|
816
|
+
size_arr = coords["size"].values if "size" in coords.columns else 1.5
|
|
817
|
+
stroke_arr = coords["stroke"].values if "stroke" in coords.columns else 0.5
|
|
818
|
+
return _ggname(
|
|
819
|
+
"geom_point",
|
|
820
|
+
points_grob(
|
|
821
|
+
x=coords["x"].values,
|
|
822
|
+
y=coords["y"].values,
|
|
823
|
+
pch=coords["shape"].values if "shape" in coords.columns else 19,
|
|
824
|
+
gp=Gpar(
|
|
825
|
+
col=scales_alpha(
|
|
826
|
+
coords["colour"].values if "colour" in coords.columns else "black",
|
|
827
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
828
|
+
),
|
|
829
|
+
fill=_fill_alpha(
|
|
830
|
+
coords["fill"].values if "fill" in coords.columns else None,
|
|
831
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
832
|
+
),
|
|
833
|
+
fontsize=size_arr * PT + stroke_arr * STROKE / 2,
|
|
834
|
+
lwd=stroke_arr * STROKE / 2,
|
|
835
|
+
),
|
|
836
|
+
),
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
# ===========================================================================
|
|
841
|
+
# GeomPath / GeomLine / GeomStep
|
|
842
|
+
# ===========================================================================
|
|
843
|
+
|
|
844
|
+
class GeomPath(Geom):
|
|
845
|
+
"""Path geom -- connects observations in data order."""
|
|
846
|
+
|
|
847
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
848
|
+
non_missing_aes: Tuple[str, ...] = ("linewidth", "colour", "linetype")
|
|
849
|
+
# R: aes(colour=from_theme(colour %||% ink), linewidth=from_theme(linewidth),
|
|
850
|
+
# linetype=from_theme(linetype), alpha=NA)
|
|
851
|
+
default_aes: Mapping = Mapping(
|
|
852
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
853
|
+
linewidth=FromTheme("linewidth"),
|
|
854
|
+
linetype=FromTheme("linetype"),
|
|
855
|
+
alpha=None,
|
|
856
|
+
)
|
|
857
|
+
draw_key = draw_key_path
|
|
858
|
+
rename_size: bool = True
|
|
859
|
+
|
|
860
|
+
def draw_panel(
|
|
861
|
+
self,
|
|
862
|
+
data: pd.DataFrame,
|
|
863
|
+
panel_params: Any,
|
|
864
|
+
coord: Any,
|
|
865
|
+
arrow: Any = None,
|
|
866
|
+
lineend: str = "butt",
|
|
867
|
+
linejoin: str = "round",
|
|
868
|
+
linemitre: float = 10,
|
|
869
|
+
na_rm: bool = False,
|
|
870
|
+
**params: Any,
|
|
871
|
+
) -> Any:
|
|
872
|
+
"""Draw connected paths.
|
|
873
|
+
|
|
874
|
+
R splits data by group and draws one polyline per group so
|
|
875
|
+
that each group can have its own colour/linetype/linewidth.
|
|
876
|
+
"""
|
|
877
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
878
|
+
|
|
879
|
+
if coords.empty or len(coords) < 2:
|
|
880
|
+
return null_grob()
|
|
881
|
+
|
|
882
|
+
# R semantics: split by group, draw each separately
|
|
883
|
+
# so per-group colour/lwd/lty are respected.
|
|
884
|
+
if "group" not in coords.columns:
|
|
885
|
+
coords["group"] = 0
|
|
886
|
+
|
|
887
|
+
children = []
|
|
888
|
+
for gid, gdata in coords.groupby("group", sort=True, observed=True):
|
|
889
|
+
if len(gdata) < 2:
|
|
890
|
+
continue
|
|
891
|
+
# Take first-row aesthetics for the whole group
|
|
892
|
+
row0 = gdata.iloc[0]
|
|
893
|
+
col_val = row0.get("colour", "black")
|
|
894
|
+
alpha_val = row0.get("alpha", None)
|
|
895
|
+
lwd_val = float(row0.get("linewidth", 0.5)) * PT
|
|
896
|
+
lty_val = row0.get("linetype", 1)
|
|
897
|
+
|
|
898
|
+
col_str = scales_alpha(col_val, alpha_val)
|
|
899
|
+
|
|
900
|
+
children.append(polyline_grob(
|
|
901
|
+
x=gdata["x"].values,
|
|
902
|
+
y=gdata["y"].values,
|
|
903
|
+
default_units="native",
|
|
904
|
+
gp=Gpar(
|
|
905
|
+
col=col_str,
|
|
906
|
+
lwd=lwd_val,
|
|
907
|
+
lty=lty_val,
|
|
908
|
+
lineend=lineend,
|
|
909
|
+
linejoin=linejoin,
|
|
910
|
+
linemitre=linemitre,
|
|
911
|
+
),
|
|
912
|
+
arrow=arrow,
|
|
913
|
+
name=f"path.{gid}",
|
|
914
|
+
))
|
|
915
|
+
|
|
916
|
+
if not children:
|
|
917
|
+
return null_grob()
|
|
918
|
+
return _ggname("geom_path", grob_tree(*children))
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
class GeomLine(GeomPath):
|
|
922
|
+
"""Line geom -- like path but sorted by x."""
|
|
923
|
+
|
|
924
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
925
|
+
|
|
926
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
927
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
928
|
+
return params
|
|
929
|
+
|
|
930
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
931
|
+
data = data.copy()
|
|
932
|
+
flipped = params.get("flipped_aes", False)
|
|
933
|
+
sort_col = "y" if flipped else "x"
|
|
934
|
+
group_cols = ["PANEL", "group"] if "group" in data.columns else ["PANEL"]
|
|
935
|
+
group_cols = [c for c in group_cols if c in data.columns]
|
|
936
|
+
if sort_col in data.columns:
|
|
937
|
+
data = data.sort_values(group_cols + [sort_col])
|
|
938
|
+
return data
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
class GeomStep(GeomPath):
|
|
942
|
+
"""Step geom -- stairstep connections."""
|
|
943
|
+
|
|
944
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
945
|
+
|
|
946
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
947
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
948
|
+
return params
|
|
949
|
+
|
|
950
|
+
def draw_panel(
|
|
951
|
+
self,
|
|
952
|
+
data: pd.DataFrame,
|
|
953
|
+
panel_params: Any,
|
|
954
|
+
coord: Any,
|
|
955
|
+
direction: str = "hv",
|
|
956
|
+
lineend: str = "butt",
|
|
957
|
+
linejoin: str = "round",
|
|
958
|
+
linemitre: float = 10,
|
|
959
|
+
arrow: Any = None,
|
|
960
|
+
flipped_aes: bool = False,
|
|
961
|
+
**params: Any,
|
|
962
|
+
) -> Any:
|
|
963
|
+
"""Draw step connections."""
|
|
964
|
+
data = data.copy()
|
|
965
|
+
data = _stairstep(data, direction=direction)
|
|
966
|
+
return GeomPath.draw_panel(
|
|
967
|
+
self, data, panel_params, coord,
|
|
968
|
+
lineend=lineend, linejoin=linejoin, linemitre=linemitre,
|
|
969
|
+
arrow=arrow,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _stairstep(data: pd.DataFrame, direction: str = "hv") -> pd.DataFrame:
|
|
974
|
+
"""Calculate stairstep coordinates for :class:`GeomStep`."""
|
|
975
|
+
if direction not in ("hv", "vh", "mid"):
|
|
976
|
+
cli_abort(f"direction must be 'hv', 'vh', or 'mid', not {direction!r}")
|
|
977
|
+
data = data.sort_values("x").reset_index(drop=True)
|
|
978
|
+
n = len(data)
|
|
979
|
+
if n <= 1:
|
|
980
|
+
return data.iloc[:0]
|
|
981
|
+
|
|
982
|
+
x = data["x"].values
|
|
983
|
+
y = data["y"].values
|
|
984
|
+
|
|
985
|
+
if direction == "hv":
|
|
986
|
+
xs = np.repeat(x, 2)[1:]
|
|
987
|
+
ys = np.repeat(y, 2)[:-1]
|
|
988
|
+
elif direction == "vh":
|
|
989
|
+
xs = np.repeat(x, 2)[:-1]
|
|
990
|
+
ys = np.repeat(y, 2)[1:]
|
|
991
|
+
else: # mid
|
|
992
|
+
gaps = np.diff(x)
|
|
993
|
+
mid_x = x[:-1] + gaps / 2
|
|
994
|
+
xs_idx = np.repeat(np.arange(n - 1), 2)
|
|
995
|
+
ys_idx = np.repeat(np.arange(n), 2)
|
|
996
|
+
xs_arr = np.concatenate([[x[0]], mid_x[xs_idx], [x[-1]]])
|
|
997
|
+
ys_arr = y[ys_idx]
|
|
998
|
+
result = data.iloc[[0]].copy()
|
|
999
|
+
result = pd.DataFrame({"x": xs_arr, "y": ys_arr})
|
|
1000
|
+
# carry forward other columns
|
|
1001
|
+
for col in data.columns:
|
|
1002
|
+
if col not in ("x", "y"):
|
|
1003
|
+
result[col] = data[col].iloc[0]
|
|
1004
|
+
return result
|
|
1005
|
+
|
|
1006
|
+
result = pd.DataFrame({"x": xs, "y": ys})
|
|
1007
|
+
for col in data.columns:
|
|
1008
|
+
if col not in ("x", "y"):
|
|
1009
|
+
result[col] = data[col].iloc[0]
|
|
1010
|
+
return result
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
# ===========================================================================
|
|
1014
|
+
# GeomRect / GeomTile / GeomRaster
|
|
1015
|
+
# ===========================================================================
|
|
1016
|
+
|
|
1017
|
+
def _mix_ink_paper(ratio: float):
|
|
1018
|
+
"""Build a fallback callable that computes ``col_mix(ink, paper, ratio)``.
|
|
1019
|
+
|
|
1020
|
+
Mirrors R's inline expressions such as ``col_mix(ink, paper, 0.35)``
|
|
1021
|
+
used in geom default aesthetics (geom-rect.R, geom-ribbon.R, etc.).
|
|
1022
|
+
"""
|
|
1023
|
+
from scales import col_mix as _cm
|
|
1024
|
+
def _fn(g):
|
|
1025
|
+
return _cm(g.ink, g.paper, ratio)
|
|
1026
|
+
return _fn
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
class GeomRect(Geom):
|
|
1030
|
+
"""Rectangle geom (defined by xmin, xmax, ymin, ymax)."""
|
|
1031
|
+
|
|
1032
|
+
required_aes: Tuple[str, ...] = ("xmin", "xmax", "ymin", "ymax")
|
|
1033
|
+
# R (geom-rect.R:6-11):
|
|
1034
|
+
# colour = from_theme(colour %||% NA),
|
|
1035
|
+
# fill = from_theme(fill %||% col_mix(ink, paper, 0.35)),
|
|
1036
|
+
# linewidth = from_theme(borderwidth),
|
|
1037
|
+
# linetype = from_theme(bordertype),
|
|
1038
|
+
default_aes: Mapping = Mapping(
|
|
1039
|
+
colour=FromTheme("colour"),
|
|
1040
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.35)),
|
|
1041
|
+
linewidth=FromTheme("borderwidth"),
|
|
1042
|
+
linetype=FromTheme("bordertype"),
|
|
1043
|
+
alpha=None,
|
|
1044
|
+
)
|
|
1045
|
+
draw_key = draw_key_polygon
|
|
1046
|
+
rename_size: bool = True
|
|
1047
|
+
|
|
1048
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1049
|
+
"""Resolve rect aesthetics from center + size if corners are missing."""
|
|
1050
|
+
if all(c in data.columns for c in ("xmin", "xmax", "ymin", "ymax")):
|
|
1051
|
+
return data
|
|
1052
|
+
data = data.copy()
|
|
1053
|
+
# Resolve x-dimension
|
|
1054
|
+
if "xmin" not in data.columns or "xmax" not in data.columns:
|
|
1055
|
+
if "x" in data.columns and "width" in data.columns:
|
|
1056
|
+
data["xmin"] = data["x"] - data["width"] / 2
|
|
1057
|
+
data["xmax"] = data["x"] + data["width"] / 2
|
|
1058
|
+
elif "x" in data.columns:
|
|
1059
|
+
w = params.get("width", 0.9)
|
|
1060
|
+
data["xmin"] = data["x"] - w / 2
|
|
1061
|
+
data["xmax"] = data["x"] + w / 2
|
|
1062
|
+
# Resolve y-dimension
|
|
1063
|
+
if "ymin" not in data.columns or "ymax" not in data.columns:
|
|
1064
|
+
if "y" in data.columns and "height" in data.columns:
|
|
1065
|
+
data["ymin"] = data["y"] - data["height"] / 2
|
|
1066
|
+
data["ymax"] = data["y"] + data["height"] / 2
|
|
1067
|
+
elif "y" in data.columns:
|
|
1068
|
+
h = params.get("height", 0.9)
|
|
1069
|
+
data["ymin"] = data["y"] - h / 2
|
|
1070
|
+
data["ymax"] = data["y"] + h / 2
|
|
1071
|
+
return data
|
|
1072
|
+
|
|
1073
|
+
def draw_panel(
|
|
1074
|
+
self,
|
|
1075
|
+
data: pd.DataFrame,
|
|
1076
|
+
panel_params: Any,
|
|
1077
|
+
coord: Any,
|
|
1078
|
+
lineend: str = "butt",
|
|
1079
|
+
linejoin: str = "mitre",
|
|
1080
|
+
**params: Any,
|
|
1081
|
+
) -> Any:
|
|
1082
|
+
"""Draw rectangles."""
|
|
1083
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1084
|
+
|
|
1085
|
+
return _ggname(
|
|
1086
|
+
"geom_rect",
|
|
1087
|
+
rect_grob(
|
|
1088
|
+
x=coords["xmin"].values,
|
|
1089
|
+
y=coords["ymax"].values,
|
|
1090
|
+
width=coords["xmax"].values - coords["xmin"].values,
|
|
1091
|
+
height=coords["ymax"].values - coords["ymin"].values,
|
|
1092
|
+
default_units="native",
|
|
1093
|
+
just=("left", "top"),
|
|
1094
|
+
gp=Gpar(
|
|
1095
|
+
col=coords["colour"].values if "colour" in coords.columns else None,
|
|
1096
|
+
fill=_fill_alpha(
|
|
1097
|
+
coords["fill"].values if "fill" in coords.columns else "grey35",
|
|
1098
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
1099
|
+
),
|
|
1100
|
+
lwd=(
|
|
1101
|
+
coords["linewidth"].values * PT
|
|
1102
|
+
if "linewidth" in coords.columns
|
|
1103
|
+
else 0.5 * PT
|
|
1104
|
+
),
|
|
1105
|
+
lty=coords["linetype"].values if "linetype" in coords.columns else 1,
|
|
1106
|
+
linejoin=linejoin,
|
|
1107
|
+
lineend=lineend,
|
|
1108
|
+
),
|
|
1109
|
+
),
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
class GeomTile(GeomRect):
|
|
1114
|
+
"""Tile geom -- rectangles parameterised by center and size."""
|
|
1115
|
+
|
|
1116
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
1117
|
+
non_missing_aes: Tuple[str, ...] = ("xmin", "xmax", "ymin", "ymax")
|
|
1118
|
+
# R (geom-tile.R:26-35):
|
|
1119
|
+
# fill = from_theme(fill %||% col_mix(ink, paper, 0.2)),
|
|
1120
|
+
# colour = from_theme(colour %||% NA),
|
|
1121
|
+
# linewidth = from_theme(linewidth), linetype = from_theme(linetype)
|
|
1122
|
+
default_aes: Mapping = Mapping(
|
|
1123
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.2)),
|
|
1124
|
+
colour=FromTheme("colour"),
|
|
1125
|
+
linewidth=FromTheme("linewidth"),
|
|
1126
|
+
linetype=FromTheme("linetype"),
|
|
1127
|
+
alpha=None,
|
|
1128
|
+
width=1,
|
|
1129
|
+
height=1,
|
|
1130
|
+
)
|
|
1131
|
+
draw_key = draw_key_polygon
|
|
1132
|
+
extra_params: Tuple[str, ...] = ("na_rm",)
|
|
1133
|
+
|
|
1134
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1135
|
+
data = data.copy()
|
|
1136
|
+
w = data["width"].values if "width" in data.columns else params.get("width", 1)
|
|
1137
|
+
h = data["height"].values if "height" in data.columns else params.get("height", 1)
|
|
1138
|
+
data["xmin"] = data["x"] - np.asarray(w) / 2
|
|
1139
|
+
data["xmax"] = data["x"] + np.asarray(w) / 2
|
|
1140
|
+
data["ymin"] = data["y"] - np.asarray(h) / 2
|
|
1141
|
+
data["ymax"] = data["y"] + np.asarray(h) / 2
|
|
1142
|
+
return data
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
class GeomRaster(Geom):
|
|
1146
|
+
"""Raster geom -- high-performance uniform tiles."""
|
|
1147
|
+
|
|
1148
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
1149
|
+
non_missing_aes: Tuple[str, ...] = ("fill",)
|
|
1150
|
+
default_aes: Mapping = Mapping(fill="grey35", alpha=None)
|
|
1151
|
+
draw_key = draw_key_polygon
|
|
1152
|
+
|
|
1153
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1154
|
+
hjust = params.get("hjust", 0.5)
|
|
1155
|
+
vjust = params.get("vjust", 0.5)
|
|
1156
|
+
data = data.copy()
|
|
1157
|
+
|
|
1158
|
+
x_vals = data["x"].values.astype(float)
|
|
1159
|
+
y_vals = data["y"].values.astype(float)
|
|
1160
|
+
x_diff = np.diff(np.sort(np.unique(x_vals)))
|
|
1161
|
+
y_diff = np.diff(np.sort(np.unique(y_vals)))
|
|
1162
|
+
w = x_diff[0] if len(x_diff) > 0 else 1
|
|
1163
|
+
h = y_diff[0] if len(y_diff) > 0 else 1
|
|
1164
|
+
|
|
1165
|
+
data["xmin"] = data["x"] - w * (1 - hjust)
|
|
1166
|
+
data["xmax"] = data["x"] + w * hjust
|
|
1167
|
+
data["ymin"] = data["y"] - h * (1 - vjust)
|
|
1168
|
+
data["ymax"] = data["y"] + h * vjust
|
|
1169
|
+
return data
|
|
1170
|
+
|
|
1171
|
+
def draw_panel(
|
|
1172
|
+
self,
|
|
1173
|
+
data: pd.DataFrame,
|
|
1174
|
+
panel_params: Any,
|
|
1175
|
+
coord: Any,
|
|
1176
|
+
interpolate: bool = False,
|
|
1177
|
+
hjust: float = 0.5,
|
|
1178
|
+
vjust: float = 0.5,
|
|
1179
|
+
**params: Any,
|
|
1180
|
+
) -> Any:
|
|
1181
|
+
"""Draw raster tiles."""
|
|
1182
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1183
|
+
|
|
1184
|
+
x_rng = (coords["xmin"].min(), coords["xmax"].max())
|
|
1185
|
+
y_rng = (coords["ymin"].min(), coords["ymax"].max())
|
|
1186
|
+
|
|
1187
|
+
return raster_grob(
|
|
1188
|
+
image=_fill_alpha(
|
|
1189
|
+
coords["fill"].values if "fill" in coords.columns else "grey35",
|
|
1190
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
1191
|
+
),
|
|
1192
|
+
x=np.mean(x_rng),
|
|
1193
|
+
y=np.mean(y_rng),
|
|
1194
|
+
width=x_rng[1] - x_rng[0],
|
|
1195
|
+
height=y_rng[1] - y_rng[0],
|
|
1196
|
+
default_units="native",
|
|
1197
|
+
interpolate=interpolate,
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
# ===========================================================================
|
|
1202
|
+
# GeomBar / GeomCol
|
|
1203
|
+
# ===========================================================================
|
|
1204
|
+
|
|
1205
|
+
class GeomBar(GeomRect):
|
|
1206
|
+
"""Bar geom -- rectangles with y anchored at zero."""
|
|
1207
|
+
|
|
1208
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
1209
|
+
non_missing_aes: Tuple[str, ...] = ("xmin", "xmax", "ymin", "ymax")
|
|
1210
|
+
# R (geom-bar.R:15):
|
|
1211
|
+
# default_aes = aes(!!!GeomRect$default_aes, width = 0.9)
|
|
1212
|
+
default_aes: Mapping = Mapping(
|
|
1213
|
+
colour=FromTheme("colour"),
|
|
1214
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.35)),
|
|
1215
|
+
linewidth=FromTheme("borderwidth"),
|
|
1216
|
+
linetype=FromTheme("bordertype"),
|
|
1217
|
+
alpha=None,
|
|
1218
|
+
width=0.9,
|
|
1219
|
+
)
|
|
1220
|
+
extra_params: Tuple[str, ...] = ("just", "na_rm", "orientation")
|
|
1221
|
+
rename_size: bool = False
|
|
1222
|
+
|
|
1223
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1224
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1225
|
+
return params
|
|
1226
|
+
|
|
1227
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1228
|
+
data = data.copy()
|
|
1229
|
+
width = params.get("width") or (data["width"].values if "width" in data.columns else 0.9)
|
|
1230
|
+
just = params.get("just", 0.5)
|
|
1231
|
+
|
|
1232
|
+
if isinstance(width, (int, float)):
|
|
1233
|
+
data["width"] = width
|
|
1234
|
+
data["ymin"] = np.minimum(data["y"].values, 0)
|
|
1235
|
+
data["ymax"] = np.maximum(data["y"].values, 0)
|
|
1236
|
+
data["xmin"] = data["x"] - data["width"] * just
|
|
1237
|
+
data["xmax"] = data["x"] + data["width"] * (1 - just)
|
|
1238
|
+
return data
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
class GeomCol(GeomBar):
|
|
1242
|
+
"""Column geom -- alias for GeomBar."""
|
|
1243
|
+
pass
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
# ===========================================================================
|
|
1247
|
+
# GeomText / GeomLabel
|
|
1248
|
+
# ===========================================================================
|
|
1249
|
+
|
|
1250
|
+
class GeomText(Geom):
|
|
1251
|
+
"""Text geom."""
|
|
1252
|
+
|
|
1253
|
+
required_aes: Tuple[str, ...] = ("x", "y", "label")
|
|
1254
|
+
non_missing_aes: Tuple[str, ...] = ("angle",)
|
|
1255
|
+
default_aes: Mapping = Mapping(
|
|
1256
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
1257
|
+
family="",
|
|
1258
|
+
size=3.88, # R GeomText$default_aes: literal 3.88 (mm), ≈ 11 pt when ×.pt
|
|
1259
|
+
angle=0,
|
|
1260
|
+
hjust=0.5,
|
|
1261
|
+
vjust=0.5,
|
|
1262
|
+
alpha=None,
|
|
1263
|
+
fontface=1,
|
|
1264
|
+
lineheight=1.2,
|
|
1265
|
+
)
|
|
1266
|
+
draw_key = draw_key_text
|
|
1267
|
+
|
|
1268
|
+
def draw_panel(
|
|
1269
|
+
self,
|
|
1270
|
+
data: pd.DataFrame,
|
|
1271
|
+
panel_params: Any,
|
|
1272
|
+
coord: Any,
|
|
1273
|
+
parse: bool = False,
|
|
1274
|
+
na_rm: bool = False,
|
|
1275
|
+
check_overlap: bool = False,
|
|
1276
|
+
size_unit: str = "mm",
|
|
1277
|
+
**params: Any,
|
|
1278
|
+
) -> Any:
|
|
1279
|
+
"""Draw text labels."""
|
|
1280
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1281
|
+
|
|
1282
|
+
size_mul = PT # default mm
|
|
1283
|
+
if size_unit == "pt":
|
|
1284
|
+
size_mul = 1
|
|
1285
|
+
elif size_unit == "cm":
|
|
1286
|
+
size_mul = PT * 10
|
|
1287
|
+
elif size_unit == "in":
|
|
1288
|
+
size_mul = 72.27
|
|
1289
|
+
elif size_unit == "pc":
|
|
1290
|
+
size_mul = 12
|
|
1291
|
+
|
|
1292
|
+
# R's textGrob handles vectorised parameters natively.
|
|
1293
|
+
# Our text_grob expects scalars, so we create one per row.
|
|
1294
|
+
children = []
|
|
1295
|
+
colours = scales_alpha(
|
|
1296
|
+
coords["colour"].values if "colour" in coords.columns else "black",
|
|
1297
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
1298
|
+
)
|
|
1299
|
+
if isinstance(colours, str):
|
|
1300
|
+
colours = [colours] * len(coords)
|
|
1301
|
+
|
|
1302
|
+
for i in range(len(coords)):
|
|
1303
|
+
row = coords.iloc[i]
|
|
1304
|
+
col_i = colours[i] if i < len(colours) else "black"
|
|
1305
|
+
children.append(text_grob(
|
|
1306
|
+
label=str(row.get("label", "")),
|
|
1307
|
+
x=float(row["x"]),
|
|
1308
|
+
y=float(row["y"]),
|
|
1309
|
+
default_units="native",
|
|
1310
|
+
hjust=float(row.get("hjust", 0.5)),
|
|
1311
|
+
vjust=float(row.get("vjust", 0.5)),
|
|
1312
|
+
rot=float(row.get("angle", 0)),
|
|
1313
|
+
gp=Gpar(
|
|
1314
|
+
col=col_i,
|
|
1315
|
+
fontsize=float(row.get("size", 3.88)) * size_mul,
|
|
1316
|
+
),
|
|
1317
|
+
name=f"text.{i}",
|
|
1318
|
+
))
|
|
1319
|
+
|
|
1320
|
+
return _ggname("geom_text", grob_tree(*children))
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
class GeomLabel(Geom):
|
|
1324
|
+
"""Label geom -- text with background rectangle."""
|
|
1325
|
+
|
|
1326
|
+
required_aes: Tuple[str, ...] = ("x", "y", "label")
|
|
1327
|
+
default_aes: Mapping = Mapping(
|
|
1328
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
1329
|
+
fill="white",
|
|
1330
|
+
family="",
|
|
1331
|
+
size=3.88, # R GeomLabel$default_aes: literal 3.88 (mm)
|
|
1332
|
+
angle=0,
|
|
1333
|
+
hjust=0.5,
|
|
1334
|
+
vjust=0.5,
|
|
1335
|
+
alpha=None,
|
|
1336
|
+
fontface=1,
|
|
1337
|
+
lineheight=1.2,
|
|
1338
|
+
linewidth=FromTheme("linewidth"),
|
|
1339
|
+
linetype=FromTheme("linetype"),
|
|
1340
|
+
)
|
|
1341
|
+
draw_key = draw_key_label
|
|
1342
|
+
|
|
1343
|
+
def draw_panel(
|
|
1344
|
+
self,
|
|
1345
|
+
data: pd.DataFrame,
|
|
1346
|
+
panel_params: Any,
|
|
1347
|
+
coord: Any,
|
|
1348
|
+
parse: bool = False,
|
|
1349
|
+
na_rm: bool = False,
|
|
1350
|
+
label_padding: Any = None,
|
|
1351
|
+
label_r: Any = None,
|
|
1352
|
+
size_unit: str = "mm",
|
|
1353
|
+
**params: Any,
|
|
1354
|
+
) -> Any:
|
|
1355
|
+
"""Draw labelled text."""
|
|
1356
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1357
|
+
size_mul = PT
|
|
1358
|
+
|
|
1359
|
+
grobs = []
|
|
1360
|
+
for i in range(len(coords)):
|
|
1361
|
+
row = coords.iloc[i]
|
|
1362
|
+
label = str(row["label"])
|
|
1363
|
+
x_val = row["x"]
|
|
1364
|
+
y_val = row["y"]
|
|
1365
|
+
|
|
1366
|
+
bg_grob = roundrect_grob(
|
|
1367
|
+
x=x_val,
|
|
1368
|
+
y=y_val,
|
|
1369
|
+
gp=Gpar(
|
|
1370
|
+
col=row.get("colour", "black"),
|
|
1371
|
+
fill=_fill_alpha(row.get("fill", "white"), row.get("alpha")),
|
|
1372
|
+
lwd=row.get("linewidth", 0.25) * PT,
|
|
1373
|
+
lty=row.get("linetype", 1),
|
|
1374
|
+
),
|
|
1375
|
+
)
|
|
1376
|
+
txt_grob = text_grob(
|
|
1377
|
+
label=label,
|
|
1378
|
+
x=x_val,
|
|
1379
|
+
y=y_val,
|
|
1380
|
+
gp=Gpar(
|
|
1381
|
+
col=scales_alpha(row.get("colour", "black"), row.get("alpha")),
|
|
1382
|
+
fontsize=row.get("size", 3.88) * size_mul,
|
|
1383
|
+
fontfamily=row.get("family", ""),
|
|
1384
|
+
fontface=row.get("fontface", 1),
|
|
1385
|
+
),
|
|
1386
|
+
)
|
|
1387
|
+
grobs.extend([bg_grob, txt_grob])
|
|
1388
|
+
|
|
1389
|
+
return _ggname("geom_label", grob_tree(*grobs) if grobs else null_grob())
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
# ===========================================================================
|
|
1393
|
+
# GeomPolygon
|
|
1394
|
+
# ===========================================================================
|
|
1395
|
+
|
|
1396
|
+
class GeomPolygon(Geom):
|
|
1397
|
+
"""Polygon geom."""
|
|
1398
|
+
|
|
1399
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
1400
|
+
# R (geom-polygon.R:73-80):
|
|
1401
|
+
# fill = from_theme(fill %||% col_mix(ink, paper, 0.2))
|
|
1402
|
+
default_aes: Mapping = Mapping(
|
|
1403
|
+
colour=FromTheme("colour"),
|
|
1404
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.2)),
|
|
1405
|
+
linewidth=FromTheme("linewidth"),
|
|
1406
|
+
linetype=FromTheme("linetype"),
|
|
1407
|
+
alpha=None,
|
|
1408
|
+
)
|
|
1409
|
+
draw_key = draw_key_polygon
|
|
1410
|
+
rename_size: bool = True
|
|
1411
|
+
|
|
1412
|
+
def handle_na(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1413
|
+
return data
|
|
1414
|
+
|
|
1415
|
+
def draw_panel(
|
|
1416
|
+
self,
|
|
1417
|
+
data: pd.DataFrame,
|
|
1418
|
+
panel_params: Any,
|
|
1419
|
+
coord: Any,
|
|
1420
|
+
rule: str = "evenodd",
|
|
1421
|
+
lineend: str = "butt",
|
|
1422
|
+
linejoin: str = "round",
|
|
1423
|
+
linemitre: float = 10,
|
|
1424
|
+
**params: Any,
|
|
1425
|
+
) -> Any:
|
|
1426
|
+
"""Draw filled polygons."""
|
|
1427
|
+
if len(data) <= 1:
|
|
1428
|
+
return null_grob()
|
|
1429
|
+
|
|
1430
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1431
|
+
# R does NOT sort by group here — group is only used as
|
|
1432
|
+
# polygon sub-id. Sorting would scramble vertex order.
|
|
1433
|
+
group_id = coords["group"].values if "group" in coords.columns else None
|
|
1434
|
+
|
|
1435
|
+
# Take first value per group for gpar
|
|
1436
|
+
return _ggname(
|
|
1437
|
+
"geom_polygon",
|
|
1438
|
+
polygon_grob(
|
|
1439
|
+
x=coords["x"].values,
|
|
1440
|
+
y=coords["y"].values,
|
|
1441
|
+
id=group_id,
|
|
1442
|
+
default_units="native",
|
|
1443
|
+
gp=Gpar(
|
|
1444
|
+
col=coords["colour"].iloc[0] if "colour" in coords.columns else None,
|
|
1445
|
+
fill=_fill_alpha(
|
|
1446
|
+
coords["fill"].iloc[0] if "fill" in coords.columns else "grey35",
|
|
1447
|
+
coords["alpha"].iloc[0] if "alpha" in coords.columns else None,
|
|
1448
|
+
),
|
|
1449
|
+
lwd=(
|
|
1450
|
+
coords["linewidth"].iloc[0] * PT
|
|
1451
|
+
if "linewidth" in coords.columns
|
|
1452
|
+
else 0.5 * PT
|
|
1453
|
+
),
|
|
1454
|
+
lty=coords["linetype"].iloc[0] if "linetype" in coords.columns else 1,
|
|
1455
|
+
lineend=lineend,
|
|
1456
|
+
linejoin=linejoin,
|
|
1457
|
+
linemitre=linemitre,
|
|
1458
|
+
),
|
|
1459
|
+
),
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
# ===========================================================================
|
|
1464
|
+
# GeomRibbon / GeomArea
|
|
1465
|
+
# ===========================================================================
|
|
1466
|
+
|
|
1467
|
+
class GeomRibbon(Geom):
|
|
1468
|
+
"""Ribbon geom -- shaded region between ymin and ymax."""
|
|
1469
|
+
|
|
1470
|
+
required_aes: Tuple[str, ...] = ("x", "ymin", "ymax")
|
|
1471
|
+
# R (geom-ribbon.R:6-13):
|
|
1472
|
+
# fill = from_theme(fill %||% col_mix(ink, paper, 0.2))
|
|
1473
|
+
default_aes: Mapping = Mapping(
|
|
1474
|
+
colour=FromTheme("colour"),
|
|
1475
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.2)),
|
|
1476
|
+
linewidth=FromTheme("linewidth"),
|
|
1477
|
+
linetype=FromTheme("linetype"),
|
|
1478
|
+
alpha=None,
|
|
1479
|
+
)
|
|
1480
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
1481
|
+
draw_key = draw_key_polygon
|
|
1482
|
+
rename_size: bool = True
|
|
1483
|
+
|
|
1484
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1485
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1486
|
+
return params
|
|
1487
|
+
|
|
1488
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1489
|
+
data = data.copy()
|
|
1490
|
+
sort_cols = ["PANEL", "group", "x"]
|
|
1491
|
+
sort_cols = [c for c in sort_cols if c in data.columns]
|
|
1492
|
+
if sort_cols:
|
|
1493
|
+
data = data.sort_values(sort_cols)
|
|
1494
|
+
if "y" not in data.columns:
|
|
1495
|
+
data["y"] = data.get("ymin", data.get("ymax", 0))
|
|
1496
|
+
return data
|
|
1497
|
+
|
|
1498
|
+
def draw_group(
|
|
1499
|
+
self,
|
|
1500
|
+
data: pd.DataFrame,
|
|
1501
|
+
panel_params: Any,
|
|
1502
|
+
coord: Any,
|
|
1503
|
+
lineend: str = "butt",
|
|
1504
|
+
linejoin: str = "round",
|
|
1505
|
+
linemitre: float = 10,
|
|
1506
|
+
na_rm: bool = False,
|
|
1507
|
+
flipped_aes: bool = False,
|
|
1508
|
+
outline_type: str = "both",
|
|
1509
|
+
**params: Any,
|
|
1510
|
+
) -> Any:
|
|
1511
|
+
"""Draw ribbon."""
|
|
1512
|
+
data = data.copy()
|
|
1513
|
+
|
|
1514
|
+
# Build polygon from upper + reversed lower
|
|
1515
|
+
upper = pd.DataFrame({"x": data["x"].values, "y": data["ymax"].values})
|
|
1516
|
+
lower = pd.DataFrame({"x": data["x"].values[::-1], "y": data["ymin"].values[::-1]})
|
|
1517
|
+
poly_data = pd.concat([upper, lower], ignore_index=True)
|
|
1518
|
+
|
|
1519
|
+
# Copy aesthetics
|
|
1520
|
+
for col in ("colour", "fill", "linewidth", "linetype", "alpha"):
|
|
1521
|
+
if col in data.columns:
|
|
1522
|
+
poly_data[col] = data[col].iloc[0]
|
|
1523
|
+
|
|
1524
|
+
coords = _coord_transform(coord, poly_data, panel_params)
|
|
1525
|
+
|
|
1526
|
+
fill_val = data["fill"].iloc[0] if "fill" in data.columns else "grey35"
|
|
1527
|
+
alpha_val = data["alpha"].iloc[0] if "alpha" in data.columns else None
|
|
1528
|
+
colour_val = data["colour"].iloc[0] if "colour" in data.columns else None
|
|
1529
|
+
lwd = data["linewidth"].iloc[0] * PT if "linewidth" in data.columns else 0.5 * PT
|
|
1530
|
+
lty = data["linetype"].iloc[0] if "linetype" in data.columns else 1
|
|
1531
|
+
|
|
1532
|
+
g_poly = polygon_grob(
|
|
1533
|
+
x=coords["x"].values,
|
|
1534
|
+
y=coords["y"].values,
|
|
1535
|
+
default_units="native",
|
|
1536
|
+
gp=Gpar(
|
|
1537
|
+
fill=_fill_alpha(fill_val, alpha_val),
|
|
1538
|
+
col=colour_val if outline_type == "full" else None,
|
|
1539
|
+
lwd=lwd if outline_type == "full" else 0,
|
|
1540
|
+
lty=lty if outline_type == "full" else 1,
|
|
1541
|
+
lineend=lineend,
|
|
1542
|
+
linejoin=linejoin,
|
|
1543
|
+
),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
if outline_type == "full":
|
|
1547
|
+
return _ggname("geom_ribbon", g_poly)
|
|
1548
|
+
|
|
1549
|
+
# R (geom-ribbon.R:187-198): polylineGrob with col=aes$colour.
|
|
1550
|
+
# When colour is NA (Python ``None``), R's gpar produces no
|
|
1551
|
+
# stroke — we must skip the outline grobs entirely, otherwise
|
|
1552
|
+
# Python's grid default fills in black (manifesting as the
|
|
1553
|
+
# double-line artifact on geom_smooth's confidence ribbon).
|
|
1554
|
+
if colour_val is None:
|
|
1555
|
+
return _ggname("geom_ribbon", g_poly)
|
|
1556
|
+
|
|
1557
|
+
# Draw outline lines
|
|
1558
|
+
upper_coords = _coord_transform(coord, pd.DataFrame({"x": data["x"].values, "y": data["ymax"].values}), panel_params)
|
|
1559
|
+
lower_coords = _coord_transform(coord, pd.DataFrame({"x": data["x"].values[::-1], "y": data["ymin"].values[::-1]}), panel_params)
|
|
1560
|
+
|
|
1561
|
+
line_gp = Gpar(col=colour_val, lwd=lwd, lty=lty, lineend=lineend, linejoin=linejoin)
|
|
1562
|
+
|
|
1563
|
+
line_grobs = []
|
|
1564
|
+
if outline_type in ("both", "upper"):
|
|
1565
|
+
line_grobs.append(
|
|
1566
|
+
lines_grob(x=upper_coords["x"].values, y=upper_coords["y"].values, default_units="native", gp=line_gp)
|
|
1567
|
+
)
|
|
1568
|
+
if outline_type in ("both", "lower"):
|
|
1569
|
+
line_grobs.append(
|
|
1570
|
+
lines_grob(x=lower_coords["x"].values, y=lower_coords["y"].values, default_units="native", gp=line_gp)
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
return _ggname("geom_ribbon", grob_tree(g_poly, *line_grobs))
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
class GeomArea(GeomRibbon):
|
|
1577
|
+
"""Area geom -- ribbon anchored at y=0."""
|
|
1578
|
+
|
|
1579
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
1580
|
+
|
|
1581
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1582
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1583
|
+
# R semantics: GeomArea uses outline.type = "upper" (not "both")
|
|
1584
|
+
params.setdefault("outline_type", "upper")
|
|
1585
|
+
return params
|
|
1586
|
+
|
|
1587
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1588
|
+
data = data.copy()
|
|
1589
|
+
sort_cols = [c for c in ["PANEL", "group", "x"] if c in data.columns]
|
|
1590
|
+
if sort_cols:
|
|
1591
|
+
data = data.sort_values(sort_cols)
|
|
1592
|
+
data["ymin"] = 0
|
|
1593
|
+
data["ymax"] = data["y"]
|
|
1594
|
+
return data
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
# ===========================================================================
|
|
1598
|
+
# GeomSmooth
|
|
1599
|
+
# ===========================================================================
|
|
1600
|
+
|
|
1601
|
+
class GeomSmooth(Geom):
|
|
1602
|
+
"""Smooth geom -- fitted line + optional confidence ribbon."""
|
|
1603
|
+
|
|
1604
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
1605
|
+
optional_aes: Tuple[str, ...] = ("ymin", "ymax")
|
|
1606
|
+
# R (geom-smooth.R:52-58):
|
|
1607
|
+
# colour = from_theme(colour %||% accent),
|
|
1608
|
+
# fill = from_theme(fill %||% col_mix(ink, paper, 0.6)),
|
|
1609
|
+
# linewidth = from_theme(2 * linewidth),
|
|
1610
|
+
# linetype = from_theme(linetype), weight = 1, alpha = 0.4
|
|
1611
|
+
default_aes: Mapping = Mapping(
|
|
1612
|
+
colour=FromTheme("colour", fallback="accent"),
|
|
1613
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.6)),
|
|
1614
|
+
linewidth=FromTheme(lambda g: 2.0 * g.linewidth),
|
|
1615
|
+
linetype=FromTheme("linetype"),
|
|
1616
|
+
weight=1,
|
|
1617
|
+
alpha=0.4,
|
|
1618
|
+
)
|
|
1619
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
1620
|
+
draw_key = draw_key_smooth
|
|
1621
|
+
rename_size: bool = True
|
|
1622
|
+
|
|
1623
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1624
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1625
|
+
if "se" not in params:
|
|
1626
|
+
params["se"] = all(c in data.columns for c in ("ymin", "ymax"))
|
|
1627
|
+
return params
|
|
1628
|
+
|
|
1629
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1630
|
+
return GeomLine.setup_data(GeomLine(), data, params)
|
|
1631
|
+
|
|
1632
|
+
def draw_group(
|
|
1633
|
+
self,
|
|
1634
|
+
data: pd.DataFrame,
|
|
1635
|
+
panel_params: Any,
|
|
1636
|
+
coord: Any,
|
|
1637
|
+
lineend: str = "butt",
|
|
1638
|
+
linejoin: str = "round",
|
|
1639
|
+
linemitre: float = 10,
|
|
1640
|
+
se: bool = False,
|
|
1641
|
+
flipped_aes: bool = False,
|
|
1642
|
+
**params: Any,
|
|
1643
|
+
) -> Any:
|
|
1644
|
+
"""Draw smooth line + optional ribbon."""
|
|
1645
|
+
ribbon_data = data.copy()
|
|
1646
|
+
if "colour" in ribbon_data.columns:
|
|
1647
|
+
ribbon_data["colour"] = None
|
|
1648
|
+
|
|
1649
|
+
path_data = data.copy()
|
|
1650
|
+
path_data["alpha"] = None
|
|
1651
|
+
|
|
1652
|
+
grobs = []
|
|
1653
|
+
has_ribbon = se and "ymin" in data.columns and "ymax" in data.columns
|
|
1654
|
+
if has_ribbon:
|
|
1655
|
+
grobs.append(
|
|
1656
|
+
GeomRibbon.draw_group(
|
|
1657
|
+
GeomRibbon(), ribbon_data, panel_params, coord,
|
|
1658
|
+
flipped_aes=flipped_aes,
|
|
1659
|
+
)
|
|
1660
|
+
)
|
|
1661
|
+
grobs.append(
|
|
1662
|
+
GeomLine.draw_panel(
|
|
1663
|
+
GeomLine(), path_data, panel_params, coord,
|
|
1664
|
+
lineend=lineend, linejoin=linejoin, linemitre=linemitre,
|
|
1665
|
+
)
|
|
1666
|
+
)
|
|
1667
|
+
return grob_tree(*grobs)
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
# ===========================================================================
|
|
1671
|
+
# GeomSegment / GeomCurve / GeomSpoke
|
|
1672
|
+
# ===========================================================================
|
|
1673
|
+
|
|
1674
|
+
class GeomSegment(Geom):
|
|
1675
|
+
"""Segment geom -- straight line between two points."""
|
|
1676
|
+
|
|
1677
|
+
required_aes: Tuple[str, ...] = ("x", "y", "xend", "yend")
|
|
1678
|
+
non_missing_aes: Tuple[str, ...] = ("linetype", "linewidth")
|
|
1679
|
+
default_aes: Mapping = Mapping(
|
|
1680
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
1681
|
+
linewidth=FromTheme("linewidth"),
|
|
1682
|
+
linetype=FromTheme("linetype"),
|
|
1683
|
+
alpha=None,
|
|
1684
|
+
)
|
|
1685
|
+
draw_key = draw_key_path
|
|
1686
|
+
rename_size: bool = True
|
|
1687
|
+
|
|
1688
|
+
def draw_panel(
|
|
1689
|
+
self,
|
|
1690
|
+
data: pd.DataFrame,
|
|
1691
|
+
panel_params: Any,
|
|
1692
|
+
coord: Any,
|
|
1693
|
+
arrow: Any = None,
|
|
1694
|
+
lineend: str = "butt",
|
|
1695
|
+
linejoin: str = "round",
|
|
1696
|
+
na_rm: bool = False,
|
|
1697
|
+
**params: Any,
|
|
1698
|
+
) -> Any:
|
|
1699
|
+
"""Draw line segments."""
|
|
1700
|
+
data = data.copy()
|
|
1701
|
+
if "xend" not in data.columns:
|
|
1702
|
+
data["xend"] = data["x"]
|
|
1703
|
+
if "yend" not in data.columns:
|
|
1704
|
+
data["yend"] = data["y"]
|
|
1705
|
+
|
|
1706
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1707
|
+
|
|
1708
|
+
if coords.empty:
|
|
1709
|
+
return null_grob()
|
|
1710
|
+
|
|
1711
|
+
return segments_grob(
|
|
1712
|
+
x0=coords["x"].values,
|
|
1713
|
+
y0=coords["y"].values,
|
|
1714
|
+
x1=coords["xend"].values,
|
|
1715
|
+
y1=coords["yend"].values,
|
|
1716
|
+
default_units="native",
|
|
1717
|
+
gp=Gpar(
|
|
1718
|
+
col=scales_alpha(
|
|
1719
|
+
coords["colour"].values if "colour" in coords.columns else "black",
|
|
1720
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
1721
|
+
),
|
|
1722
|
+
lwd=(
|
|
1723
|
+
coords["linewidth"].values * PT
|
|
1724
|
+
if "linewidth" in coords.columns
|
|
1725
|
+
else 0.5 * PT
|
|
1726
|
+
),
|
|
1727
|
+
lty=coords["linetype"].values if "linetype" in coords.columns else 1,
|
|
1728
|
+
lineend=lineend,
|
|
1729
|
+
linejoin=linejoin,
|
|
1730
|
+
),
|
|
1731
|
+
arrow=arrow,
|
|
1732
|
+
)
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
class GeomCurve(GeomSegment):
|
|
1736
|
+
"""Curve geom -- curved line between two points."""
|
|
1737
|
+
|
|
1738
|
+
def draw_panel(
|
|
1739
|
+
self,
|
|
1740
|
+
data: pd.DataFrame,
|
|
1741
|
+
panel_params: Any,
|
|
1742
|
+
coord: Any,
|
|
1743
|
+
curvature: float = 0.5,
|
|
1744
|
+
angle: float = 90,
|
|
1745
|
+
ncp: int = 5,
|
|
1746
|
+
shape: float = 0.5,
|
|
1747
|
+
arrow: Any = None,
|
|
1748
|
+
lineend: str = "butt",
|
|
1749
|
+
na_rm: bool = False,
|
|
1750
|
+
**params: Any,
|
|
1751
|
+
) -> Any:
|
|
1752
|
+
"""Draw curved segments."""
|
|
1753
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
1754
|
+
|
|
1755
|
+
if coords.empty:
|
|
1756
|
+
return null_grob()
|
|
1757
|
+
|
|
1758
|
+
return curve_grob(
|
|
1759
|
+
x1=coords["x"].values,
|
|
1760
|
+
y1=coords["y"].values,
|
|
1761
|
+
x2=coords["xend"].values if "xend" in coords.columns else coords["x"].values,
|
|
1762
|
+
y2=coords["yend"].values if "yend" in coords.columns else coords["y"].values,
|
|
1763
|
+
default_units="native",
|
|
1764
|
+
curvature=curvature,
|
|
1765
|
+
angle=angle,
|
|
1766
|
+
ncp=ncp,
|
|
1767
|
+
shape=shape,
|
|
1768
|
+
gp=Gpar(
|
|
1769
|
+
col=scales_alpha(
|
|
1770
|
+
coords["colour"].values if "colour" in coords.columns else "black",
|
|
1771
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
1772
|
+
),
|
|
1773
|
+
lwd=(
|
|
1774
|
+
coords["linewidth"].values * PT
|
|
1775
|
+
if "linewidth" in coords.columns
|
|
1776
|
+
else 0.5 * PT
|
|
1777
|
+
),
|
|
1778
|
+
lty=coords["linetype"].values if "linetype" in coords.columns else 1,
|
|
1779
|
+
lineend=lineend,
|
|
1780
|
+
),
|
|
1781
|
+
arrow=arrow,
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
class GeomSpoke(GeomSegment):
|
|
1786
|
+
"""Spoke geom -- segment parameterised by location, angle, and radius."""
|
|
1787
|
+
|
|
1788
|
+
required_aes: Tuple[str, ...] = ("x", "y", "angle", "radius")
|
|
1789
|
+
|
|
1790
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1791
|
+
data = data.copy()
|
|
1792
|
+
if "radius" not in data.columns:
|
|
1793
|
+
data["radius"] = params.get("radius", 1)
|
|
1794
|
+
if "angle" not in data.columns:
|
|
1795
|
+
data["angle"] = params.get("angle", 0)
|
|
1796
|
+
data["xend"] = data["x"] + np.cos(data["angle"]) * data["radius"]
|
|
1797
|
+
data["yend"] = data["y"] + np.sin(data["angle"]) * data["radius"]
|
|
1798
|
+
return data
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
# ===========================================================================
|
|
1802
|
+
# GeomErrorbar / GeomErrorbarh
|
|
1803
|
+
# ===========================================================================
|
|
1804
|
+
|
|
1805
|
+
class GeomErrorbar(Geom):
|
|
1806
|
+
"""Errorbar geom -- T-shaped error bars."""
|
|
1807
|
+
|
|
1808
|
+
required_aes: Tuple[str, ...] = ("x", "ymin", "ymax")
|
|
1809
|
+
default_aes: Mapping = Mapping(
|
|
1810
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
1811
|
+
linewidth=FromTheme("linewidth"),
|
|
1812
|
+
linetype=FromTheme("linetype"),
|
|
1813
|
+
width=0.9,
|
|
1814
|
+
alpha=None,
|
|
1815
|
+
)
|
|
1816
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
1817
|
+
draw_key = draw_key_path
|
|
1818
|
+
rename_size: bool = True
|
|
1819
|
+
|
|
1820
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1821
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1822
|
+
return params
|
|
1823
|
+
|
|
1824
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1825
|
+
data = data.copy()
|
|
1826
|
+
width = params.get("width") or (data["width"].values if "width" in data.columns else 0.9)
|
|
1827
|
+
if isinstance(width, (int, float)):
|
|
1828
|
+
data["width"] = width
|
|
1829
|
+
data["xmin"] = data["x"] - data["width"] / 2
|
|
1830
|
+
data["xmax"] = data["x"] + data["width"] / 2
|
|
1831
|
+
return data
|
|
1832
|
+
|
|
1833
|
+
def draw_panel(
|
|
1834
|
+
self,
|
|
1835
|
+
data: pd.DataFrame,
|
|
1836
|
+
panel_params: Any,
|
|
1837
|
+
coord: Any,
|
|
1838
|
+
lineend: str = "butt",
|
|
1839
|
+
width: Optional[float] = None,
|
|
1840
|
+
flipped_aes: bool = False,
|
|
1841
|
+
**params: Any,
|
|
1842
|
+
) -> Any:
|
|
1843
|
+
"""Draw error bars as T-shapes."""
|
|
1844
|
+
n = len(data)
|
|
1845
|
+
# Build the three segments per bar:
|
|
1846
|
+
# top cap, vertical, bottom cap
|
|
1847
|
+
x_vals = np.concatenate([
|
|
1848
|
+
np.column_stack([data["xmin"].values, data["xmax"].values, np.full(n, np.nan),
|
|
1849
|
+
data["x"].values, data["x"].values, np.full(n, np.nan),
|
|
1850
|
+
data["xmin"].values, data["xmax"].values]).ravel()
|
|
1851
|
+
])
|
|
1852
|
+
y_vals = np.concatenate([
|
|
1853
|
+
np.column_stack([data["ymax"].values, data["ymax"].values, np.full(n, np.nan),
|
|
1854
|
+
data["ymax"].values, data["ymin"].values, np.full(n, np.nan),
|
|
1855
|
+
data["ymin"].values, data["ymin"].values]).ravel()
|
|
1856
|
+
])
|
|
1857
|
+
|
|
1858
|
+
# Create a path-like data frame
|
|
1859
|
+
path_data = pd.DataFrame({
|
|
1860
|
+
"x": x_vals,
|
|
1861
|
+
"y": y_vals,
|
|
1862
|
+
"colour": np.repeat(data["colour"].values if "colour" in data.columns else "black", 8),
|
|
1863
|
+
"alpha": np.repeat(data["alpha"].values if "alpha" in data.columns else np.nan, 8),
|
|
1864
|
+
"linewidth": np.repeat(data["linewidth"].values if "linewidth" in data.columns else 0.5, 8),
|
|
1865
|
+
"linetype": np.repeat(data["linetype"].values if "linetype" in data.columns else 1, 8),
|
|
1866
|
+
"group": np.repeat(np.arange(n), 8),
|
|
1867
|
+
})
|
|
1868
|
+
|
|
1869
|
+
return GeomPath.draw_panel(GeomPath(), path_data, panel_params, coord, lineend=lineend)
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
class GeomErrorbarh(GeomErrorbar):
|
|
1873
|
+
"""Horizontal errorbar geom (deprecated -- use ``geom_errorbar(orientation='y')``)."""
|
|
1874
|
+
|
|
1875
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1876
|
+
warnings.warn(
|
|
1877
|
+
"geom_errorbarh() is deprecated. Use geom_errorbar(orientation='y').",
|
|
1878
|
+
FutureWarning,
|
|
1879
|
+
stacklevel=2,
|
|
1880
|
+
)
|
|
1881
|
+
return super().setup_params(data, params)
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
# ===========================================================================
|
|
1885
|
+
# GeomCrossbar
|
|
1886
|
+
# ===========================================================================
|
|
1887
|
+
|
|
1888
|
+
class GeomCrossbar(Geom):
|
|
1889
|
+
"""Crossbar geom -- box with median line."""
|
|
1890
|
+
|
|
1891
|
+
required_aes: Tuple[str, ...] = ("x", "y", "ymin", "ymax")
|
|
1892
|
+
default_aes: Mapping = Mapping(
|
|
1893
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
1894
|
+
fill=FromTheme("fill"),
|
|
1895
|
+
linewidth=FromTheme("linewidth"),
|
|
1896
|
+
linetype=FromTheme("linetype"),
|
|
1897
|
+
alpha=None,
|
|
1898
|
+
)
|
|
1899
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
1900
|
+
draw_key = draw_key_crossbar
|
|
1901
|
+
rename_size: bool = True
|
|
1902
|
+
|
|
1903
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1904
|
+
params.setdefault("fatten", 2.5)
|
|
1905
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1906
|
+
return params
|
|
1907
|
+
|
|
1908
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
1909
|
+
return GeomErrorbar.setup_data(GeomErrorbar(), data, params)
|
|
1910
|
+
|
|
1911
|
+
def draw_panel(
|
|
1912
|
+
self,
|
|
1913
|
+
data: pd.DataFrame,
|
|
1914
|
+
panel_params: Any,
|
|
1915
|
+
coord: Any,
|
|
1916
|
+
lineend: str = "butt",
|
|
1917
|
+
linejoin: str = "mitre",
|
|
1918
|
+
fatten: float = 2.5,
|
|
1919
|
+
width: Optional[float] = None,
|
|
1920
|
+
flipped_aes: bool = False,
|
|
1921
|
+
middle_gp: Optional[Dict] = None,
|
|
1922
|
+
box_gp: Optional[Dict] = None,
|
|
1923
|
+
**params: Any,
|
|
1924
|
+
) -> Any:
|
|
1925
|
+
"""Draw crossbar."""
|
|
1926
|
+
# Build box polygon
|
|
1927
|
+
n = len(data)
|
|
1928
|
+
boxes = []
|
|
1929
|
+
middles = []
|
|
1930
|
+
|
|
1931
|
+
for i in range(n):
|
|
1932
|
+
row = data.iloc[i]
|
|
1933
|
+
xmin = row.get("xmin", row["x"] - 0.45)
|
|
1934
|
+
xmax = row.get("xmax", row["x"] + 0.45)
|
|
1935
|
+
ymin_val = row["ymin"]
|
|
1936
|
+
ymax_val = row["ymax"]
|
|
1937
|
+
y_mid = row["y"]
|
|
1938
|
+
|
|
1939
|
+
box_df = pd.DataFrame({
|
|
1940
|
+
"x": [xmin, xmin, xmax, xmax, xmin],
|
|
1941
|
+
"y": [ymax_val, ymin_val, ymin_val, ymax_val, ymax_val],
|
|
1942
|
+
"colour": row.get("colour", "black"),
|
|
1943
|
+
"fill": row.get("fill"),
|
|
1944
|
+
"linewidth": row.get("linewidth", 0.5),
|
|
1945
|
+
"linetype": row.get("linetype", 1),
|
|
1946
|
+
"alpha": row.get("alpha"),
|
|
1947
|
+
"group": i,
|
|
1948
|
+
})
|
|
1949
|
+
boxes.append(box_df)
|
|
1950
|
+
|
|
1951
|
+
mid_df = pd.DataFrame({
|
|
1952
|
+
"x": [xmin],
|
|
1953
|
+
"y": [y_mid],
|
|
1954
|
+
"xend": [xmax],
|
|
1955
|
+
"yend": [y_mid],
|
|
1956
|
+
"colour": row.get("colour", "black"),
|
|
1957
|
+
"linewidth": row.get("linewidth", 0.5) * fatten,
|
|
1958
|
+
"linetype": row.get("linetype", 1),
|
|
1959
|
+
"alpha": [np.nan],
|
|
1960
|
+
})
|
|
1961
|
+
middles.append(mid_df)
|
|
1962
|
+
|
|
1963
|
+
box_data = pd.concat(boxes, ignore_index=True)
|
|
1964
|
+
mid_data = pd.concat(middles, ignore_index=True)
|
|
1965
|
+
|
|
1966
|
+
box_grob = GeomPolygon.draw_panel(
|
|
1967
|
+
GeomPolygon(), box_data, panel_params, coord,
|
|
1968
|
+
lineend=lineend, linejoin=linejoin,
|
|
1969
|
+
)
|
|
1970
|
+
mid_grob = GeomSegment.draw_panel(
|
|
1971
|
+
GeomSegment(), mid_data, panel_params, coord, lineend=lineend,
|
|
1972
|
+
)
|
|
1973
|
+
|
|
1974
|
+
return _ggname("geom_crossbar", grob_tree(box_grob, mid_grob))
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
# ===========================================================================
|
|
1978
|
+
# GeomLinerange / GeomPointrange
|
|
1979
|
+
# ===========================================================================
|
|
1980
|
+
|
|
1981
|
+
class GeomLinerange(Geom):
|
|
1982
|
+
"""Linerange geom -- vertical line segments."""
|
|
1983
|
+
|
|
1984
|
+
required_aes: Tuple[str, ...] = ("x", "ymin", "ymax")
|
|
1985
|
+
default_aes: Mapping = Mapping(
|
|
1986
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
1987
|
+
linewidth=FromTheme("linewidth"),
|
|
1988
|
+
linetype=FromTheme("linetype"),
|
|
1989
|
+
alpha=None,
|
|
1990
|
+
)
|
|
1991
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
1992
|
+
draw_key = draw_key_linerange
|
|
1993
|
+
rename_size: bool = True
|
|
1994
|
+
|
|
1995
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1996
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
1997
|
+
return params
|
|
1998
|
+
|
|
1999
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
2000
|
+
data = data.copy()
|
|
2001
|
+
data["flipped_aes"] = params.get("flipped_aes", False)
|
|
2002
|
+
return data
|
|
2003
|
+
|
|
2004
|
+
def draw_panel(
|
|
2005
|
+
self,
|
|
2006
|
+
data: pd.DataFrame,
|
|
2007
|
+
panel_params: Any,
|
|
2008
|
+
coord: Any,
|
|
2009
|
+
lineend: str = "butt",
|
|
2010
|
+
flipped_aes: bool = False,
|
|
2011
|
+
na_rm: bool = False,
|
|
2012
|
+
arrow: Any = None,
|
|
2013
|
+
**params: Any,
|
|
2014
|
+
) -> Any:
|
|
2015
|
+
"""Draw line ranges."""
|
|
2016
|
+
seg_data = data.copy()
|
|
2017
|
+
seg_data["xend"] = seg_data["x"]
|
|
2018
|
+
seg_data["yend"] = seg_data["ymax"]
|
|
2019
|
+
seg_data["y"] = seg_data["ymin"]
|
|
2020
|
+
grob = GeomSegment.draw_panel(
|
|
2021
|
+
GeomSegment(), seg_data, panel_params, coord,
|
|
2022
|
+
lineend=lineend, na_rm=na_rm, arrow=arrow,
|
|
2023
|
+
)
|
|
2024
|
+
return _ggname("geom_linerange", grob)
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
class GeomPointrange(Geom):
|
|
2028
|
+
"""Pointrange geom -- line range with point at y."""
|
|
2029
|
+
|
|
2030
|
+
required_aes: Tuple[str, ...] = ("x", "y", "ymin", "ymax")
|
|
2031
|
+
default_aes: Mapping = Mapping(
|
|
2032
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2033
|
+
size=0.5,
|
|
2034
|
+
linewidth=FromTheme("linewidth"),
|
|
2035
|
+
linetype=FromTheme("linetype"),
|
|
2036
|
+
shape=FromTheme("pointshape"),
|
|
2037
|
+
fill=FromTheme("fill"),
|
|
2038
|
+
alpha=None,
|
|
2039
|
+
stroke=FromTheme("borderwidth"),
|
|
2040
|
+
)
|
|
2041
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
2042
|
+
draw_key = draw_key_pointrange
|
|
2043
|
+
|
|
2044
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2045
|
+
params.setdefault("fatten", 4)
|
|
2046
|
+
return GeomLinerange.setup_params(GeomLinerange(), data, params)
|
|
2047
|
+
|
|
2048
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
2049
|
+
return GeomLinerange.setup_data(GeomLinerange(), data, params)
|
|
2050
|
+
|
|
2051
|
+
def draw_panel(
|
|
2052
|
+
self,
|
|
2053
|
+
data: pd.DataFrame,
|
|
2054
|
+
panel_params: Any,
|
|
2055
|
+
coord: Any,
|
|
2056
|
+
lineend: str = "butt",
|
|
2057
|
+
fatten: float = 4,
|
|
2058
|
+
flipped_aes: bool = False,
|
|
2059
|
+
na_rm: bool = False,
|
|
2060
|
+
arrow: Any = None,
|
|
2061
|
+
**params: Any,
|
|
2062
|
+
) -> Any:
|
|
2063
|
+
"""Draw point + range."""
|
|
2064
|
+
line_grob = GeomLinerange.draw_panel(
|
|
2065
|
+
GeomLinerange(), data, panel_params, coord,
|
|
2066
|
+
lineend=lineend, flipped_aes=flipped_aes, na_rm=na_rm, arrow=arrow,
|
|
2067
|
+
)
|
|
2068
|
+
pt_data = data.copy()
|
|
2069
|
+
if "size" in pt_data.columns:
|
|
2070
|
+
pt_data["size"] = pt_data["size"] * fatten
|
|
2071
|
+
point_grob = GeomPoint.draw_panel(GeomPoint(), pt_data, panel_params, coord, na_rm=na_rm)
|
|
2072
|
+
return _ggname("geom_pointrange", grob_tree(line_grob, point_grob))
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
# ===========================================================================
|
|
2076
|
+
# GeomBoxplot
|
|
2077
|
+
# ===========================================================================
|
|
2078
|
+
|
|
2079
|
+
class GeomBoxplot(Geom):
|
|
2080
|
+
"""Boxplot geom."""
|
|
2081
|
+
|
|
2082
|
+
required_aes: Tuple[str, ...] = ("x", "lower", "upper", "middle", "ymin", "ymax")
|
|
2083
|
+
default_aes: Mapping = Mapping(
|
|
2084
|
+
weight=1,
|
|
2085
|
+
colour="grey20",
|
|
2086
|
+
fill="white",
|
|
2087
|
+
size=FromTheme("pointsize"),
|
|
2088
|
+
alpha=None,
|
|
2089
|
+
shape=FromTheme("pointshape"),
|
|
2090
|
+
linetype=FromTheme("linetype"),
|
|
2091
|
+
linewidth=FromTheme("linewidth"),
|
|
2092
|
+
width=0.9,
|
|
2093
|
+
)
|
|
2094
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation", "outliers")
|
|
2095
|
+
draw_key = draw_key_boxplot
|
|
2096
|
+
rename_size: bool = True
|
|
2097
|
+
|
|
2098
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2099
|
+
params.setdefault("fatten", 2)
|
|
2100
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
2101
|
+
return params
|
|
2102
|
+
|
|
2103
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
2104
|
+
"""Prepare boxplot data: compute box width and outlier-inclusive ranges.
|
|
2105
|
+
|
|
2106
|
+
Mirrors R's ``GeomBoxplot$setup_data`` (geom-boxplot.R:257-286).
|
|
2107
|
+
Adds ``ymin_final``/``ymax_final`` columns that include outlier
|
|
2108
|
+
values, ensuring the y-scale is trained on the full data extent.
|
|
2109
|
+
"""
|
|
2110
|
+
data = data.copy()
|
|
2111
|
+
width = params.get("width") or (data["width"].values if "width" in data.columns else 0.9)
|
|
2112
|
+
if isinstance(width, (int, float)):
|
|
2113
|
+
data["width"] = width
|
|
2114
|
+
|
|
2115
|
+
# Compute ymin_final / ymax_final from outliers
|
|
2116
|
+
# (R: geom-boxplot.R:266-274)
|
|
2117
|
+
if "outliers" in data.columns:
|
|
2118
|
+
ymin_final = []
|
|
2119
|
+
ymax_final = []
|
|
2120
|
+
for _, row in data.iterrows():
|
|
2121
|
+
outliers = row.get("outliers", [])
|
|
2122
|
+
if outliers is None or (isinstance(outliers, float) and np.isnan(outliers)):
|
|
2123
|
+
outliers = []
|
|
2124
|
+
if isinstance(outliers, np.ndarray):
|
|
2125
|
+
outliers = outliers.tolist()
|
|
2126
|
+
if len(outliers) > 0:
|
|
2127
|
+
ymin_final.append(min(min(outliers), row.get("ymin", np.inf)))
|
|
2128
|
+
ymax_final.append(max(max(outliers), row.get("ymax", -np.inf)))
|
|
2129
|
+
else:
|
|
2130
|
+
ymin_final.append(row.get("ymin", np.nan))
|
|
2131
|
+
ymax_final.append(row.get("ymax", np.nan))
|
|
2132
|
+
data["ymin_final"] = ymin_final
|
|
2133
|
+
data["ymax_final"] = ymax_final
|
|
2134
|
+
|
|
2135
|
+
data["xmin"] = data["x"] - data["width"] / 2
|
|
2136
|
+
data["xmax"] = data["x"] + data["width"] / 2
|
|
2137
|
+
return data
|
|
2138
|
+
|
|
2139
|
+
def draw_group(
|
|
2140
|
+
self,
|
|
2141
|
+
data: pd.DataFrame,
|
|
2142
|
+
panel_params: Any,
|
|
2143
|
+
coord: Any,
|
|
2144
|
+
lineend: str = "butt",
|
|
2145
|
+
linejoin: str = "mitre",
|
|
2146
|
+
fatten: float = 2,
|
|
2147
|
+
outlier_gp: Optional[Dict] = None,
|
|
2148
|
+
whisker_gp: Optional[Dict] = None,
|
|
2149
|
+
staple_gp: Optional[Dict] = None,
|
|
2150
|
+
median_gp: Optional[Dict] = None,
|
|
2151
|
+
box_gp: Optional[Dict] = None,
|
|
2152
|
+
notch: bool = False,
|
|
2153
|
+
notchwidth: float = 0.5,
|
|
2154
|
+
staplewidth: float = 0,
|
|
2155
|
+
varwidth: bool = False,
|
|
2156
|
+
flipped_aes: bool = False,
|
|
2157
|
+
**params: Any,
|
|
2158
|
+
) -> Any:
|
|
2159
|
+
"""Draw a single boxplot."""
|
|
2160
|
+
if outlier_gp is None:
|
|
2161
|
+
outlier_gp = {}
|
|
2162
|
+
if whisker_gp is None:
|
|
2163
|
+
whisker_gp = {}
|
|
2164
|
+
|
|
2165
|
+
row = data.iloc[0] if len(data) > 0 else data
|
|
2166
|
+
|
|
2167
|
+
# Whiskers
|
|
2168
|
+
whisker_data = pd.DataFrame({
|
|
2169
|
+
"x": [row["x"], row["x"]],
|
|
2170
|
+
"y": [row["upper"], row["lower"]],
|
|
2171
|
+
"xend": [row["x"], row["x"]],
|
|
2172
|
+
"yend": [row["ymax"], row["ymin"]],
|
|
2173
|
+
"colour": whisker_gp.get("colour", row.get("colour", "grey20")),
|
|
2174
|
+
"linewidth": whisker_gp.get("linewidth", row.get("linewidth", 0.5)),
|
|
2175
|
+
"linetype": whisker_gp.get("linetype", row.get("linetype", 1)),
|
|
2176
|
+
"alpha": [np.nan, np.nan],
|
|
2177
|
+
})
|
|
2178
|
+
|
|
2179
|
+
# Box (simple rectangle)
|
|
2180
|
+
xmin = row.get("xmin", row["x"] - 0.45)
|
|
2181
|
+
xmax = row.get("xmax", row["x"] + 0.45)
|
|
2182
|
+
box_data = pd.DataFrame({
|
|
2183
|
+
"x": [xmin, xmin, xmax, xmax, xmin],
|
|
2184
|
+
"y": [row["upper"], row["lower"], row["lower"], row["upper"], row["upper"]],
|
|
2185
|
+
"colour": row.get("colour", "grey20"),
|
|
2186
|
+
"fill": row.get("fill", "white"),
|
|
2187
|
+
"linewidth": row.get("linewidth", 0.5),
|
|
2188
|
+
"linetype": row.get("linetype", 1),
|
|
2189
|
+
"alpha": row.get("alpha"),
|
|
2190
|
+
"group": 1,
|
|
2191
|
+
})
|
|
2192
|
+
|
|
2193
|
+
# Median line
|
|
2194
|
+
median_data = pd.DataFrame({
|
|
2195
|
+
"x": [xmin],
|
|
2196
|
+
"y": [row["middle"]],
|
|
2197
|
+
"xend": [xmax],
|
|
2198
|
+
"yend": [row["middle"]],
|
|
2199
|
+
"colour": (median_gp or {}).get("colour", row.get("colour", "grey20")),
|
|
2200
|
+
"linewidth": (median_gp or {}).get("linewidth", row.get("linewidth", 0.5)) * fatten,
|
|
2201
|
+
"linetype": (median_gp or {}).get("linetype", row.get("linetype", 1)),
|
|
2202
|
+
"alpha": [np.nan],
|
|
2203
|
+
})
|
|
2204
|
+
|
|
2205
|
+
grobs = [
|
|
2206
|
+
GeomSegment.draw_panel(GeomSegment(), whisker_data, panel_params, coord, lineend=lineend),
|
|
2207
|
+
GeomPolygon.draw_panel(GeomPolygon(), box_data, panel_params, coord, lineend=lineend, linejoin=linejoin),
|
|
2208
|
+
GeomSegment.draw_panel(GeomSegment(), median_data, panel_params, coord, lineend=lineend),
|
|
2209
|
+
]
|
|
2210
|
+
|
|
2211
|
+
# Outliers
|
|
2212
|
+
if "outliers" in data.columns and data["outliers"].iloc[0] is not None:
|
|
2213
|
+
outliers_list = data["outliers"].iloc[0]
|
|
2214
|
+
if hasattr(outliers_list, "__len__") and len(outliers_list) > 0:
|
|
2215
|
+
outlier_data = pd.DataFrame({
|
|
2216
|
+
"x": row["x"],
|
|
2217
|
+
"y": outliers_list,
|
|
2218
|
+
"colour": outlier_gp.get("colour", row.get("colour", "grey20")),
|
|
2219
|
+
"fill": None,
|
|
2220
|
+
"shape": outlier_gp.get("shape", 19),
|
|
2221
|
+
"size": outlier_gp.get("size", 1.5),
|
|
2222
|
+
"stroke": outlier_gp.get("stroke", 0.5),
|
|
2223
|
+
"alpha": outlier_gp.get("alpha", row.get("alpha")),
|
|
2224
|
+
})
|
|
2225
|
+
grobs.insert(0, GeomPoint.draw_panel(GeomPoint(), outlier_data, panel_params, coord))
|
|
2226
|
+
|
|
2227
|
+
return _ggname("geom_boxplot", grob_tree(*grobs))
|
|
2228
|
+
|
|
2229
|
+
|
|
2230
|
+
# ===========================================================================
|
|
2231
|
+
# GeomViolin
|
|
2232
|
+
# ===========================================================================
|
|
2233
|
+
|
|
2234
|
+
class GeomViolin(Geom):
|
|
2235
|
+
"""Violin geom."""
|
|
2236
|
+
|
|
2237
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
2238
|
+
default_aes: Mapping = Mapping(
|
|
2239
|
+
weight=1,
|
|
2240
|
+
colour="grey20",
|
|
2241
|
+
fill="white",
|
|
2242
|
+
linewidth=FromTheme("linewidth"),
|
|
2243
|
+
linetype=FromTheme("linetype"),
|
|
2244
|
+
alpha=None,
|
|
2245
|
+
width=0.9,
|
|
2246
|
+
)
|
|
2247
|
+
extra_params: Tuple[str, ...] = ("na_rm", "orientation")
|
|
2248
|
+
draw_key = draw_key_polygon
|
|
2249
|
+
rename_size: bool = True
|
|
2250
|
+
|
|
2251
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2252
|
+
params["flipped_aes"] = params.get("flipped_aes", False)
|
|
2253
|
+
return params
|
|
2254
|
+
|
|
2255
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
2256
|
+
data = data.copy()
|
|
2257
|
+
width = params.get("width") or (data["width"].values if "width" in data.columns else 0.9)
|
|
2258
|
+
if isinstance(width, (int, float)):
|
|
2259
|
+
data["width"] = width
|
|
2260
|
+
if "group" in data.columns:
|
|
2261
|
+
for grp, idx in data.groupby("group").groups.items():
|
|
2262
|
+
data.loc[idx, "xmin"] = data.loc[idx, "x"] - data.loc[idx, "width"] / 2
|
|
2263
|
+
data.loc[idx, "xmax"] = data.loc[idx, "x"] + data.loc[idx, "width"] / 2
|
|
2264
|
+
return data
|
|
2265
|
+
|
|
2266
|
+
def draw_group(
|
|
2267
|
+
self,
|
|
2268
|
+
data: pd.DataFrame,
|
|
2269
|
+
panel_params: Any,
|
|
2270
|
+
coord: Any,
|
|
2271
|
+
quantile_gp: Optional[Dict] = None,
|
|
2272
|
+
flipped_aes: bool = False,
|
|
2273
|
+
**params: Any,
|
|
2274
|
+
) -> Any:
|
|
2275
|
+
"""Draw a single violin."""
|
|
2276
|
+
data = data.copy()
|
|
2277
|
+
|
|
2278
|
+
# R semantics: filter out quantile marker rows (they have
|
|
2279
|
+
# non-NaN 'quantile' values) — only the density curve rows
|
|
2280
|
+
# form the violin polygon shape.
|
|
2281
|
+
if "quantile" in data.columns:
|
|
2282
|
+
data = data[data["quantile"].isna()].copy()
|
|
2283
|
+
|
|
2284
|
+
if "violinwidth" in data.columns:
|
|
2285
|
+
data["xminv"] = data["x"] - data["violinwidth"] * (data["x"] - data.get("xmin", data["x"] - 0.45))
|
|
2286
|
+
data["xmaxv"] = data["x"] + data["violinwidth"] * (data.get("xmax", data["x"] + 0.45) - data["x"])
|
|
2287
|
+
else:
|
|
2288
|
+
data["xminv"] = data.get("xmin", data["x"] - 0.45)
|
|
2289
|
+
data["xmaxv"] = data.get("xmax", data["x"] + 0.45)
|
|
2290
|
+
|
|
2291
|
+
# Build polygon: left side (sorted y ascending) + right side (descending)
|
|
2292
|
+
sorted_data = data.sort_values("y")
|
|
2293
|
+
upper = pd.DataFrame({"y": sorted_data["y"].values, "x": sorted_data["xminv"].values})
|
|
2294
|
+
lower = pd.DataFrame({"y": sorted_data["y"].values[::-1], "x": sorted_data["xmaxv"].values[::-1]})
|
|
2295
|
+
|
|
2296
|
+
newdata = pd.concat([upper, lower], ignore_index=True)
|
|
2297
|
+
newdata = pd.concat([newdata, newdata.iloc[:1]], ignore_index=True)
|
|
2298
|
+
|
|
2299
|
+
for col in ("colour", "fill", "linewidth", "linetype", "alpha"):
|
|
2300
|
+
if col in data.columns:
|
|
2301
|
+
newdata[col] = data[col].iloc[0]
|
|
2302
|
+
newdata["group"] = 1
|
|
2303
|
+
|
|
2304
|
+
return _ggname(
|
|
2305
|
+
"geom_violin",
|
|
2306
|
+
GeomPolygon.draw_panel(GeomPolygon(), newdata, panel_params, coord),
|
|
2307
|
+
)
|
|
2308
|
+
|
|
2309
|
+
|
|
2310
|
+
# ===========================================================================
|
|
2311
|
+
# GeomDotplot
|
|
2312
|
+
# ===========================================================================
|
|
2313
|
+
|
|
2314
|
+
class GeomDotplot(Geom):
|
|
2315
|
+
"""Dotplot geom."""
|
|
2316
|
+
|
|
2317
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
2318
|
+
non_missing_aes: Tuple[str, ...] = ("size", "shape")
|
|
2319
|
+
default_aes: Mapping = Mapping(
|
|
2320
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2321
|
+
fill="black",
|
|
2322
|
+
alpha=None,
|
|
2323
|
+
stroke=1.0,
|
|
2324
|
+
linetype=FromTheme("linetype"),
|
|
2325
|
+
weight=1,
|
|
2326
|
+
width=0.9,
|
|
2327
|
+
)
|
|
2328
|
+
draw_key = draw_key_dotplot
|
|
2329
|
+
|
|
2330
|
+
def draw_group(
|
|
2331
|
+
self,
|
|
2332
|
+
data: pd.DataFrame,
|
|
2333
|
+
panel_params: Any,
|
|
2334
|
+
coord: Any,
|
|
2335
|
+
lineend: str = "butt",
|
|
2336
|
+
na_rm: bool = False,
|
|
2337
|
+
binaxis: str = "x",
|
|
2338
|
+
stackdir: str = "up",
|
|
2339
|
+
stackratio: float = 1,
|
|
2340
|
+
dotsize: float = 1,
|
|
2341
|
+
stackgroups: bool = False,
|
|
2342
|
+
**params: Any,
|
|
2343
|
+
) -> Any:
|
|
2344
|
+
"""Draw dotplot."""
|
|
2345
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
2346
|
+
return _ggname(
|
|
2347
|
+
"geom_dotplot",
|
|
2348
|
+
points_grob(
|
|
2349
|
+
x=coords["x"].values,
|
|
2350
|
+
y=coords["y"].values,
|
|
2351
|
+
pch=21,
|
|
2352
|
+
gp=Gpar(
|
|
2353
|
+
col=scales_alpha(
|
|
2354
|
+
coords["colour"].values if "colour" in coords.columns else "black",
|
|
2355
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
2356
|
+
),
|
|
2357
|
+
fill=_fill_alpha(
|
|
2358
|
+
coords["fill"].values if "fill" in coords.columns else "black",
|
|
2359
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
2360
|
+
),
|
|
2361
|
+
),
|
|
2362
|
+
),
|
|
2363
|
+
)
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
# ===========================================================================
|
|
2367
|
+
# GeomDensity
|
|
2368
|
+
# ===========================================================================
|
|
2369
|
+
|
|
2370
|
+
class GeomDensity(GeomArea):
|
|
2371
|
+
"""Density geom -- smoothed histogram."""
|
|
2372
|
+
|
|
2373
|
+
default_aes: Mapping = Mapping(
|
|
2374
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2375
|
+
fill=FromTheme("fill"),
|
|
2376
|
+
weight=1,
|
|
2377
|
+
alpha=None,
|
|
2378
|
+
linewidth=FromTheme("linewidth"),
|
|
2379
|
+
linetype=FromTheme("linetype"),
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
# ===========================================================================
|
|
2384
|
+
# GeomHistogram / GeomFreqpoly
|
|
2385
|
+
# ===========================================================================
|
|
2386
|
+
|
|
2387
|
+
# GeomHistogram is just GeomBar with stat="bin"
|
|
2388
|
+
GeomHistogram = GeomBar # alias
|
|
2389
|
+
|
|
2390
|
+
# GeomFreqpoly is just GeomPath with stat="bin"
|
|
2391
|
+
GeomFreqpoly = GeomPath # alias
|
|
2392
|
+
|
|
2393
|
+
|
|
2394
|
+
# ===========================================================================
|
|
2395
|
+
# GeomAbline / GeomHline / GeomVline
|
|
2396
|
+
# ===========================================================================
|
|
2397
|
+
|
|
2398
|
+
class GeomAbline(Geom):
|
|
2399
|
+
"""Abline geom -- diagonal reference line."""
|
|
2400
|
+
|
|
2401
|
+
required_aes: Tuple[str, ...] = ("slope", "intercept")
|
|
2402
|
+
default_aes: Mapping = Mapping(
|
|
2403
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2404
|
+
linewidth=FromTheme("linewidth"),
|
|
2405
|
+
linetype=FromTheme("linetype"),
|
|
2406
|
+
alpha=None,
|
|
2407
|
+
)
|
|
2408
|
+
draw_key = draw_key_abline
|
|
2409
|
+
rename_size: bool = True
|
|
2410
|
+
|
|
2411
|
+
def draw_panel(
|
|
2412
|
+
self,
|
|
2413
|
+
data: pd.DataFrame,
|
|
2414
|
+
panel_params: Any,
|
|
2415
|
+
coord: Any,
|
|
2416
|
+
lineend: str = "butt",
|
|
2417
|
+
**params: Any,
|
|
2418
|
+
) -> Any:
|
|
2419
|
+
"""Draw diagonal lines."""
|
|
2420
|
+
# Get x-range from panel_params
|
|
2421
|
+
if hasattr(panel_params, "x") and hasattr(panel_params.x, "range"):
|
|
2422
|
+
x_rng = panel_params.x.range
|
|
2423
|
+
elif isinstance(panel_params, dict) and "x_range" in panel_params:
|
|
2424
|
+
x_rng = panel_params["x_range"]
|
|
2425
|
+
else:
|
|
2426
|
+
x_rng = (0, 1)
|
|
2427
|
+
|
|
2428
|
+
seg_data = data.copy()
|
|
2429
|
+
seg_data["x"] = x_rng[0]
|
|
2430
|
+
seg_data["xend"] = x_rng[1]
|
|
2431
|
+
seg_data["y"] = seg_data["slope"] * x_rng[0] + seg_data["intercept"]
|
|
2432
|
+
seg_data["yend"] = seg_data["slope"] * x_rng[1] + seg_data["intercept"]
|
|
2433
|
+
|
|
2434
|
+
return GeomSegment.draw_panel(GeomSegment(), seg_data, panel_params, coord, lineend=lineend)
|
|
2435
|
+
|
|
2436
|
+
|
|
2437
|
+
class GeomHline(Geom):
|
|
2438
|
+
"""Horizontal line geom."""
|
|
2439
|
+
|
|
2440
|
+
required_aes: Tuple[str, ...] = ("yintercept",)
|
|
2441
|
+
default_aes: Mapping = Mapping(
|
|
2442
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2443
|
+
linewidth=FromTheme("linewidth"),
|
|
2444
|
+
linetype=FromTheme("linetype"),
|
|
2445
|
+
alpha=None,
|
|
2446
|
+
)
|
|
2447
|
+
draw_key = draw_key_path
|
|
2448
|
+
rename_size: bool = True
|
|
2449
|
+
|
|
2450
|
+
def draw_panel(
|
|
2451
|
+
self,
|
|
2452
|
+
data: pd.DataFrame,
|
|
2453
|
+
panel_params: Any,
|
|
2454
|
+
coord: Any,
|
|
2455
|
+
lineend: str = "butt",
|
|
2456
|
+
**params: Any,
|
|
2457
|
+
) -> Any:
|
|
2458
|
+
"""Draw horizontal lines."""
|
|
2459
|
+
x_rng = (0, 1)
|
|
2460
|
+
if hasattr(panel_params, "x") and hasattr(panel_params.x, "range"):
|
|
2461
|
+
x_rng = panel_params.x.range
|
|
2462
|
+
elif isinstance(panel_params, dict) and "x_range" in panel_params:
|
|
2463
|
+
x_rng = panel_params["x_range"]
|
|
2464
|
+
|
|
2465
|
+
seg_data = data.copy()
|
|
2466
|
+
seg_data["x"] = x_rng[0]
|
|
2467
|
+
seg_data["xend"] = x_rng[1]
|
|
2468
|
+
seg_data["y"] = seg_data["yintercept"]
|
|
2469
|
+
seg_data["yend"] = seg_data["yintercept"]
|
|
2470
|
+
|
|
2471
|
+
return GeomSegment.draw_panel(GeomSegment(), seg_data, panel_params, coord, lineend=lineend)
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
class GeomVline(Geom):
|
|
2475
|
+
"""Vertical line geom."""
|
|
2476
|
+
|
|
2477
|
+
required_aes: Tuple[str, ...] = ("xintercept",)
|
|
2478
|
+
default_aes: Mapping = Mapping(
|
|
2479
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2480
|
+
linewidth=FromTheme("linewidth"),
|
|
2481
|
+
linetype=FromTheme("linetype"),
|
|
2482
|
+
alpha=None,
|
|
2483
|
+
)
|
|
2484
|
+
draw_key = draw_key_vline
|
|
2485
|
+
rename_size: bool = True
|
|
2486
|
+
|
|
2487
|
+
def draw_panel(
|
|
2488
|
+
self,
|
|
2489
|
+
data: pd.DataFrame,
|
|
2490
|
+
panel_params: Any,
|
|
2491
|
+
coord: Any,
|
|
2492
|
+
lineend: str = "butt",
|
|
2493
|
+
**params: Any,
|
|
2494
|
+
) -> Any:
|
|
2495
|
+
"""Draw vertical lines."""
|
|
2496
|
+
y_rng = (0, 1)
|
|
2497
|
+
if hasattr(panel_params, "y") and hasattr(panel_params.y, "range"):
|
|
2498
|
+
y_rng = panel_params.y.range
|
|
2499
|
+
elif isinstance(panel_params, dict) and "y_range" in panel_params:
|
|
2500
|
+
y_rng = panel_params["y_range"]
|
|
2501
|
+
|
|
2502
|
+
seg_data = data.copy()
|
|
2503
|
+
seg_data["x"] = seg_data["xintercept"]
|
|
2504
|
+
seg_data["xend"] = seg_data["xintercept"]
|
|
2505
|
+
seg_data["y"] = y_rng[0]
|
|
2506
|
+
seg_data["yend"] = y_rng[1]
|
|
2507
|
+
|
|
2508
|
+
return GeomSegment.draw_panel(GeomSegment(), seg_data, panel_params, coord, lineend=lineend)
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
# ===========================================================================
|
|
2512
|
+
# GeomRug
|
|
2513
|
+
# ===========================================================================
|
|
2514
|
+
|
|
2515
|
+
class GeomRug(Geom):
|
|
2516
|
+
"""Rug geom -- marginal tick marks."""
|
|
2517
|
+
|
|
2518
|
+
optional_aes: Tuple[str, ...] = ("x", "y")
|
|
2519
|
+
default_aes: Mapping = Mapping(
|
|
2520
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
2521
|
+
linewidth=FromTheme("linewidth"),
|
|
2522
|
+
linetype=FromTheme("linetype"),
|
|
2523
|
+
alpha=None,
|
|
2524
|
+
)
|
|
2525
|
+
draw_key = draw_key_path
|
|
2526
|
+
rename_size: bool = True
|
|
2527
|
+
|
|
2528
|
+
def setup_params(self, data: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
2529
|
+
params.setdefault("sides", "bl")
|
|
2530
|
+
return params
|
|
2531
|
+
|
|
2532
|
+
def draw_panel(
|
|
2533
|
+
self,
|
|
2534
|
+
data: pd.DataFrame,
|
|
2535
|
+
panel_params: Any,
|
|
2536
|
+
coord: Any,
|
|
2537
|
+
lineend: str = "butt",
|
|
2538
|
+
sides: str = "bl",
|
|
2539
|
+
outside: bool = False,
|
|
2540
|
+
length: Any = None,
|
|
2541
|
+
**params: Any,
|
|
2542
|
+
) -> Any:
|
|
2543
|
+
"""Draw rug marks."""
|
|
2544
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
2545
|
+
|
|
2546
|
+
gp = Gpar(
|
|
2547
|
+
col=scales_alpha(
|
|
2548
|
+
coords["colour"].values if "colour" in coords.columns else "black",
|
|
2549
|
+
coords["alpha"].values if "alpha" in coords.columns else None,
|
|
2550
|
+
),
|
|
2551
|
+
lty=coords["linetype"].values if "linetype" in coords.columns else 1,
|
|
2552
|
+
lwd=coords["linewidth"].values * PT if "linewidth" in coords.columns else 0.5 * PT,
|
|
2553
|
+
lineend=lineend,
|
|
2554
|
+
)
|
|
2555
|
+
|
|
2556
|
+
rug_len = 0.03 # fraction of npc
|
|
2557
|
+
grobs = []
|
|
2558
|
+
|
|
2559
|
+
if "x" in coords.columns:
|
|
2560
|
+
x_vals = coords["x"].values
|
|
2561
|
+
if "b" in sides:
|
|
2562
|
+
grobs.append(
|
|
2563
|
+
segments_grob(
|
|
2564
|
+
x0=x_vals, y0=np.zeros_like(x_vals),
|
|
2565
|
+
x1=x_vals, y1=np.full_like(x_vals, rug_len),
|
|
2566
|
+
default_units="native", gp=gp,
|
|
2567
|
+
)
|
|
2568
|
+
)
|
|
2569
|
+
if "t" in sides:
|
|
2570
|
+
grobs.append(
|
|
2571
|
+
segments_grob(
|
|
2572
|
+
x0=x_vals, y0=np.ones_like(x_vals),
|
|
2573
|
+
x1=x_vals, y1=np.full_like(x_vals, 1 - rug_len),
|
|
2574
|
+
default_units="native", gp=gp,
|
|
2575
|
+
)
|
|
2576
|
+
)
|
|
2577
|
+
|
|
2578
|
+
if "y" in coords.columns:
|
|
2579
|
+
y_vals = coords["y"].values
|
|
2580
|
+
if "l" in sides:
|
|
2581
|
+
grobs.append(
|
|
2582
|
+
segments_grob(
|
|
2583
|
+
x0=np.zeros_like(y_vals), y0=y_vals,
|
|
2584
|
+
x1=np.full_like(y_vals, rug_len), y1=y_vals,
|
|
2585
|
+
default_units="native", gp=gp,
|
|
2586
|
+
)
|
|
2587
|
+
)
|
|
2588
|
+
if "r" in sides:
|
|
2589
|
+
grobs.append(
|
|
2590
|
+
segments_grob(
|
|
2591
|
+
x0=np.ones_like(y_vals), y0=y_vals,
|
|
2592
|
+
x1=np.full_like(y_vals, 1 - rug_len), y1=y_vals,
|
|
2593
|
+
default_units="native", gp=gp,
|
|
2594
|
+
)
|
|
2595
|
+
)
|
|
2596
|
+
|
|
2597
|
+
return grob_tree(*grobs) if grobs else null_grob()
|
|
2598
|
+
|
|
2599
|
+
|
|
2600
|
+
# ===========================================================================
|
|
2601
|
+
# GeomBlank
|
|
2602
|
+
# ===========================================================================
|
|
2603
|
+
|
|
2604
|
+
class GeomBlank(Geom):
|
|
2605
|
+
"""Blank geom -- draws nothing."""
|
|
2606
|
+
|
|
2607
|
+
default_aes: Mapping = Mapping()
|
|
2608
|
+
|
|
2609
|
+
def handle_na(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
2610
|
+
return data
|
|
2611
|
+
|
|
2612
|
+
def draw_panel(self, data: pd.DataFrame = None, panel_params: Any = None,
|
|
2613
|
+
coord: Any = None, **params: Any) -> Any:
|
|
2614
|
+
return null_grob()
|
|
2615
|
+
|
|
2616
|
+
|
|
2617
|
+
# ===========================================================================
|
|
2618
|
+
# GeomContour / GeomContourFilled
|
|
2619
|
+
# ===========================================================================
|
|
2620
|
+
|
|
2621
|
+
class GeomContour(GeomPath):
|
|
2622
|
+
"""Contour geom -- contour lines of a 3D surface."""
|
|
2623
|
+
|
|
2624
|
+
default_aes: Mapping = Mapping(
|
|
2625
|
+
weight=1,
|
|
2626
|
+
colour="blue",
|
|
2627
|
+
linewidth=FromTheme("linewidth"),
|
|
2628
|
+
linetype=FromTheme("linetype"),
|
|
2629
|
+
alpha=None,
|
|
2630
|
+
)
|
|
2631
|
+
|
|
2632
|
+
|
|
2633
|
+
class GeomContourFilled(GeomPolygon):
|
|
2634
|
+
"""Filled contour geom."""
|
|
2635
|
+
pass
|
|
2636
|
+
|
|
2637
|
+
|
|
2638
|
+
# ===========================================================================
|
|
2639
|
+
# GeomDensity2d / GeomDensity2dFilled
|
|
2640
|
+
# ===========================================================================
|
|
2641
|
+
|
|
2642
|
+
class GeomDensity2d(GeomPath):
|
|
2643
|
+
"""2D density contour lines."""
|
|
2644
|
+
|
|
2645
|
+
default_aes: Mapping = Mapping(
|
|
2646
|
+
colour="blue",
|
|
2647
|
+
linewidth=FromTheme("linewidth"),
|
|
2648
|
+
linetype=FromTheme("linetype"),
|
|
2649
|
+
alpha=None,
|
|
2650
|
+
)
|
|
2651
|
+
|
|
2652
|
+
|
|
2653
|
+
class GeomDensity2dFilled(GeomPolygon):
|
|
2654
|
+
"""Filled 2D density contours."""
|
|
2655
|
+
pass
|
|
2656
|
+
|
|
2657
|
+
|
|
2658
|
+
# ===========================================================================
|
|
2659
|
+
# GeomHex
|
|
2660
|
+
# ===========================================================================
|
|
2661
|
+
|
|
2662
|
+
class GeomHex(Geom):
|
|
2663
|
+
"""Hexagonal bin geom."""
|
|
2664
|
+
|
|
2665
|
+
required_aes: Tuple[str, ...] = ("x", "y")
|
|
2666
|
+
default_aes: Mapping = Mapping(
|
|
2667
|
+
colour=FromTheme("colour"),
|
|
2668
|
+
fill="grey50",
|
|
2669
|
+
linewidth=FromTheme("linewidth"),
|
|
2670
|
+
linetype=FromTheme("linetype"),
|
|
2671
|
+
alpha=None,
|
|
2672
|
+
)
|
|
2673
|
+
draw_key = draw_key_polygon
|
|
2674
|
+
rename_size: bool = True
|
|
2675
|
+
|
|
2676
|
+
def draw_group(
|
|
2677
|
+
self,
|
|
2678
|
+
data: pd.DataFrame,
|
|
2679
|
+
panel_params: Any,
|
|
2680
|
+
coord: Any,
|
|
2681
|
+
lineend: str = "butt",
|
|
2682
|
+
linejoin: str = "mitre",
|
|
2683
|
+
linemitre: float = 10,
|
|
2684
|
+
**params: Any,
|
|
2685
|
+
) -> Any:
|
|
2686
|
+
"""Draw hexagons."""
|
|
2687
|
+
if data.empty:
|
|
2688
|
+
return null_grob()
|
|
2689
|
+
|
|
2690
|
+
# R semantics: stat_bin_hex maps fill=after_stat(count).
|
|
2691
|
+
# Apply count→fill mapping when fill is uniform (default).
|
|
2692
|
+
if "count" in data.columns and "fill" in data.columns:
|
|
2693
|
+
fills = data["fill"].values
|
|
2694
|
+
if len(set(str(f) for f in fills)) <= 1:
|
|
2695
|
+
# Map count to a blue gradient (matching R's default)
|
|
2696
|
+
counts = data["count"].values.astype(float)
|
|
2697
|
+
mn, mx = counts.min(), counts.max()
|
|
2698
|
+
if mx > mn:
|
|
2699
|
+
t = (counts - mn) / (mx - mn)
|
|
2700
|
+
else:
|
|
2701
|
+
t = np.full_like(counts, 0.5)
|
|
2702
|
+
# Viridis-like: dark blue → yellow
|
|
2703
|
+
from matplotlib.cm import viridis
|
|
2704
|
+
data = data.copy()
|
|
2705
|
+
data["fill"] = [
|
|
2706
|
+
f"#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}"
|
|
2707
|
+
for c in viridis(t)
|
|
2708
|
+
]
|
|
2709
|
+
|
|
2710
|
+
# R semantics (geom-hex.R:14-29): GeomHex builds hex vertices in
|
|
2711
|
+
# data coords using the stat's binwidth, then transforms to NPC.
|
|
2712
|
+
n = len(data)
|
|
2713
|
+
|
|
2714
|
+
# Use width/height from stat output (R: data$width, data$height).
|
|
2715
|
+
# Fall back to resolution-based estimate if not available.
|
|
2716
|
+
if "width" in data.columns:
|
|
2717
|
+
dx = float(data["width"].iloc[0]) / 2
|
|
2718
|
+
else:
|
|
2719
|
+
dx = resolution(data["x"].values, zero=False)
|
|
2720
|
+
if "height" in data.columns:
|
|
2721
|
+
dy = float(data["height"].iloc[0]) / np.sqrt(3) / 2
|
|
2722
|
+
else:
|
|
2723
|
+
dy = resolution(data["y"].values, zero=False) / np.sqrt(3) / 2 * 1.15
|
|
2724
|
+
|
|
2725
|
+
# R: hexbin::hexcoords(dx, dy) returns
|
|
2726
|
+
# x = c( dx, dx, 0, -dx, -dx, 0)
|
|
2727
|
+
# y = c( dy, -dy, -2dy, -dy, dy, 2dy)
|
|
2728
|
+
# (no ``/2`` divisor). With the divisor the hexagons render
|
|
2729
|
+
# at exactly HALF the intended size, leaving visible gaps
|
|
2730
|
+
# between neighbours — the user-reported "hex not aligned".
|
|
2731
|
+
hex_x = dx * np.array([1.0, 1.0, 0.0, -1.0, -1.0, 0.0])
|
|
2732
|
+
hex_y = dy * np.array([1.0, -1.0, -2.0, -1.0, 1.0, 2.0])
|
|
2733
|
+
|
|
2734
|
+
all_x = np.repeat(data["x"].values, 6) + np.tile(hex_x, n)
|
|
2735
|
+
all_y = np.repeat(data["y"].values, 6) + np.tile(hex_y, n)
|
|
2736
|
+
|
|
2737
|
+
hex_data = pd.DataFrame({"x": all_x, "y": all_y})
|
|
2738
|
+
hex_data["group"] = np.repeat(np.arange(n), 6)
|
|
2739
|
+
|
|
2740
|
+
for col in ("colour", "fill", "linewidth", "linetype", "alpha"):
|
|
2741
|
+
if col in data.columns:
|
|
2742
|
+
hex_data[col] = np.repeat(data[col].values, 6)
|
|
2743
|
+
|
|
2744
|
+
coords = _coord_transform(coord, hex_data, panel_params)
|
|
2745
|
+
|
|
2746
|
+
return _ggname(
|
|
2747
|
+
"geom_hex",
|
|
2748
|
+
polygon_grob(
|
|
2749
|
+
x=coords["x"].values,
|
|
2750
|
+
y=coords["y"].values,
|
|
2751
|
+
id=coords["group"].values,
|
|
2752
|
+
default_units="native",
|
|
2753
|
+
gp=Gpar(
|
|
2754
|
+
col=data["colour"].values if "colour" in data.columns else None,
|
|
2755
|
+
fill=_fill_alpha(
|
|
2756
|
+
data["fill"].values if "fill" in data.columns else "grey50",
|
|
2757
|
+
data["alpha"].values if "alpha" in data.columns else None,
|
|
2758
|
+
),
|
|
2759
|
+
lwd=data["linewidth"].values * PT if "linewidth" in data.columns else 0.5 * PT,
|
|
2760
|
+
lty=data["linetype"].values if "linetype" in data.columns else 1,
|
|
2761
|
+
),
|
|
2762
|
+
),
|
|
2763
|
+
)
|
|
2764
|
+
|
|
2765
|
+
|
|
2766
|
+
# ===========================================================================
|
|
2767
|
+
# GeomBin2d
|
|
2768
|
+
# ===========================================================================
|
|
2769
|
+
|
|
2770
|
+
class GeomBin2d(GeomTile):
|
|
2771
|
+
"""2D bin heatmap geom."""
|
|
2772
|
+
pass
|
|
2773
|
+
|
|
2774
|
+
|
|
2775
|
+
# ===========================================================================
|
|
2776
|
+
# GeomFunction
|
|
2777
|
+
# ===========================================================================
|
|
2778
|
+
|
|
2779
|
+
class GeomFunction(GeomPath):
|
|
2780
|
+
"""Function geom -- draw a mathematical function as a path."""
|
|
2781
|
+
|
|
2782
|
+
def draw_panel(
|
|
2783
|
+
self,
|
|
2784
|
+
data: pd.DataFrame,
|
|
2785
|
+
panel_params: Any,
|
|
2786
|
+
coord: Any,
|
|
2787
|
+
arrow: Any = None,
|
|
2788
|
+
lineend: str = "butt",
|
|
2789
|
+
linejoin: str = "round",
|
|
2790
|
+
linemitre: float = 10,
|
|
2791
|
+
na_rm: bool = False,
|
|
2792
|
+
**params: Any,
|
|
2793
|
+
) -> Any:
|
|
2794
|
+
return GeomPath.draw_panel(
|
|
2795
|
+
self, data, panel_params, coord,
|
|
2796
|
+
arrow=arrow, lineend=lineend, linejoin=linejoin,
|
|
2797
|
+
linemitre=linemitre, na_rm=na_rm,
|
|
2798
|
+
)
|
|
2799
|
+
|
|
2800
|
+
|
|
2801
|
+
# ===========================================================================
|
|
2802
|
+
# GeomMap
|
|
2803
|
+
# ===========================================================================
|
|
2804
|
+
|
|
2805
|
+
class GeomMap(GeomPolygon):
|
|
2806
|
+
"""Map polygon geom."""
|
|
2807
|
+
|
|
2808
|
+
required_aes: Tuple[str, ...] = ("map_id",)
|
|
2809
|
+
|
|
2810
|
+
def draw_panel(
|
|
2811
|
+
self,
|
|
2812
|
+
data: pd.DataFrame,
|
|
2813
|
+
panel_params: Any,
|
|
2814
|
+
coord: Any,
|
|
2815
|
+
lineend: str = "butt",
|
|
2816
|
+
linejoin: str = "round",
|
|
2817
|
+
linemitre: float = 10,
|
|
2818
|
+
map: Optional[pd.DataFrame] = None,
|
|
2819
|
+
**params: Any,
|
|
2820
|
+
) -> Any:
|
|
2821
|
+
"""Draw map polygons."""
|
|
2822
|
+
if map is None:
|
|
2823
|
+
return null_grob()
|
|
2824
|
+
|
|
2825
|
+
map_df = map.copy()
|
|
2826
|
+
if "lat" in map_df.columns:
|
|
2827
|
+
map_df["y"] = map_df["lat"]
|
|
2828
|
+
if "long" in map_df.columns:
|
|
2829
|
+
map_df["x"] = map_df["long"]
|
|
2830
|
+
if "region" in map_df.columns:
|
|
2831
|
+
map_df["id"] = map_df["region"]
|
|
2832
|
+
|
|
2833
|
+
# Merge aesthetics
|
|
2834
|
+
common = set(data["map_id"]) & set(map_df["id"])
|
|
2835
|
+
map_df = map_df[map_df["id"].isin(common)]
|
|
2836
|
+
data_subset = data[data["map_id"].isin(common)]
|
|
2837
|
+
|
|
2838
|
+
if map_df.empty:
|
|
2839
|
+
return null_grob()
|
|
2840
|
+
|
|
2841
|
+
# Assign aesthetics from data to map
|
|
2842
|
+
for col in ("colour", "fill", "linewidth", "linetype", "alpha"):
|
|
2843
|
+
if col in data_subset.columns:
|
|
2844
|
+
id_to_val = dict(zip(data_subset["map_id"], data_subset[col]))
|
|
2845
|
+
map_df[col] = map_df["id"].map(id_to_val)
|
|
2846
|
+
|
|
2847
|
+
map_df["group"] = map_df["id"]
|
|
2848
|
+
|
|
2849
|
+
return GeomPolygon.draw_panel(
|
|
2850
|
+
GeomPolygon(), map_df, panel_params, coord,
|
|
2851
|
+
lineend=lineend, linejoin=linejoin, linemitre=linemitre,
|
|
2852
|
+
)
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
# ===========================================================================
|
|
2856
|
+
# GeomQuantile
|
|
2857
|
+
# ===========================================================================
|
|
2858
|
+
|
|
2859
|
+
class GeomQuantile(GeomPath):
|
|
2860
|
+
"""Quantile regression lines."""
|
|
2861
|
+
|
|
2862
|
+
default_aes: Mapping = Mapping(
|
|
2863
|
+
weight=1,
|
|
2864
|
+
colour="blue",
|
|
2865
|
+
linewidth=FromTheme("linewidth"),
|
|
2866
|
+
linetype=FromTheme("linetype"),
|
|
2867
|
+
alpha=None,
|
|
2868
|
+
)
|
|
2869
|
+
|
|
2870
|
+
|
|
2871
|
+
# ===========================================================================
|
|
2872
|
+
# GeomSf and related
|
|
2873
|
+
# ===========================================================================
|
|
2874
|
+
|
|
2875
|
+
# ---------------------------------------------------------------------------
|
|
2876
|
+
# sf geometry type mapping (mirrors R's sf_types vector)
|
|
2877
|
+
# ---------------------------------------------------------------------------
|
|
2878
|
+
|
|
2879
|
+
_SF_TYPES: Dict[str, str] = {
|
|
2880
|
+
"Point": "point", "MultiPoint": "point",
|
|
2881
|
+
"LineString": "line", "MultiLineString": "line",
|
|
2882
|
+
"CircularString": "line", "CompoundCurve": "line",
|
|
2883
|
+
"MultiCurve": "line", "Curve": "line",
|
|
2884
|
+
"Polygon": "other", "MultiPolygon": "other",
|
|
2885
|
+
"CurvePolygon": "other", "MultiSurface": "other",
|
|
2886
|
+
"Surface": "other", "PolyhedralSurface": "other",
|
|
2887
|
+
"TIN": "other", "Triangle": "other",
|
|
2888
|
+
"GeometryCollection": "collection",
|
|
2889
|
+
"Geometry": "other",
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
# R's .pt and .stroke constants
|
|
2893
|
+
_PT = 72.27 / 25.4 # ≈ 2.845
|
|
2894
|
+
_STROKE = 96 / 25.4 # ≈ 3.78
|
|
2895
|
+
|
|
2896
|
+
|
|
2897
|
+
def _sf_geometry_to_grobs(
|
|
2898
|
+
geometry_series: Any,
|
|
2899
|
+
colour: Any,
|
|
2900
|
+
fill: Any,
|
|
2901
|
+
linewidth: Any,
|
|
2902
|
+
linetype: Any,
|
|
2903
|
+
point_size: Any,
|
|
2904
|
+
pch: Any,
|
|
2905
|
+
lineend: str = "butt",
|
|
2906
|
+
linejoin: str = "round",
|
|
2907
|
+
) -> Any:
|
|
2908
|
+
"""Convert a Series of shapely geometries to grid_py grobs.
|
|
2909
|
+
|
|
2910
|
+
This reimplements R's ``sf::st_as_grob()`` using shapely + grid_py.
|
|
2911
|
+
Each geometry is rendered as the appropriate grob type:
|
|
2912
|
+
- Point/MultiPoint → points_grob
|
|
2913
|
+
- LineString/MultiLineString → polyline_grob / lines_grob
|
|
2914
|
+
- Polygon/MultiPolygon → polygon_grob / path_grob
|
|
2915
|
+
|
|
2916
|
+
Returns a GTree containing all grobs.
|
|
2917
|
+
"""
|
|
2918
|
+
from shapely.geometry import (
|
|
2919
|
+
Point as ShapelyPoint,
|
|
2920
|
+
MultiPoint as ShapelyMultiPoint,
|
|
2921
|
+
LineString as ShapelyLineString,
|
|
2922
|
+
MultiLineString as ShapelyMultiLineString,
|
|
2923
|
+
Polygon as ShapelyPolygon,
|
|
2924
|
+
MultiPolygon as ShapelyMultiPolygon,
|
|
2925
|
+
GeometryCollection as ShapelyGeometryCollection,
|
|
2926
|
+
)
|
|
2927
|
+
|
|
2928
|
+
children = []
|
|
2929
|
+
|
|
2930
|
+
for i, geom in enumerate(geometry_series):
|
|
2931
|
+
if geom is None or geom.is_empty:
|
|
2932
|
+
continue
|
|
2933
|
+
|
|
2934
|
+
# Per-row graphical parameters
|
|
2935
|
+
col_i = colour[i] if hasattr(colour, "__getitem__") and len(colour) > i else colour
|
|
2936
|
+
fill_i = fill[i] if hasattr(fill, "__getitem__") and len(fill) > i else fill
|
|
2937
|
+
lwd_i = linewidth[i] if hasattr(linewidth, "__getitem__") and len(linewidth) > i else linewidth
|
|
2938
|
+
lty_i = linetype[i] if hasattr(linetype, "__getitem__") and len(linetype) > i else linetype
|
|
2939
|
+
sz_i = point_size[i] if hasattr(point_size, "__getitem__") and len(point_size) > i else point_size
|
|
2940
|
+
pch_i = pch[i] if hasattr(pch, "__getitem__") and len(pch) > i else pch
|
|
2941
|
+
|
|
2942
|
+
gp = Gpar(
|
|
2943
|
+
col=col_i, fill=fill_i, lwd=lwd_i, lty=lty_i,
|
|
2944
|
+
lineend=lineend, linejoin=linejoin,
|
|
2945
|
+
)
|
|
2946
|
+
|
|
2947
|
+
if isinstance(geom, (ShapelyPoint,)):
|
|
2948
|
+
x, y = geom.x, geom.y
|
|
2949
|
+
children.append(points_grob(
|
|
2950
|
+
x=[x], y=[y], pch=int(pch_i) if pch_i is not None else 19,
|
|
2951
|
+
size=Unit(float(sz_i) if sz_i is not None else 1, "char"),
|
|
2952
|
+
gp=gp, name=f"sf_point_{i}",
|
|
2953
|
+
))
|
|
2954
|
+
|
|
2955
|
+
elif isinstance(geom, (ShapelyMultiPoint,)):
|
|
2956
|
+
xs = [p.x for p in geom.geoms]
|
|
2957
|
+
ys = [p.y for p in geom.geoms]
|
|
2958
|
+
children.append(points_grob(
|
|
2959
|
+
x=xs, y=ys, pch=int(pch_i) if pch_i is not None else 19,
|
|
2960
|
+
size=Unit(float(sz_i) if sz_i is not None else 1, "char"),
|
|
2961
|
+
gp=gp, name=f"sf_mpoint_{i}",
|
|
2962
|
+
))
|
|
2963
|
+
|
|
2964
|
+
elif isinstance(geom, (ShapelyLineString,)):
|
|
2965
|
+
xs, ys = zip(*geom.coords) if len(geom.coords) > 0 else ([], [])
|
|
2966
|
+
children.append(lines_grob(
|
|
2967
|
+
x=list(xs), y=list(ys), gp=gp, name=f"sf_line_{i}",
|
|
2968
|
+
))
|
|
2969
|
+
|
|
2970
|
+
elif isinstance(geom, (ShapelyMultiLineString,)):
|
|
2971
|
+
for j, line in enumerate(geom.geoms):
|
|
2972
|
+
xs, ys = zip(*line.coords) if len(line.coords) > 0 else ([], [])
|
|
2973
|
+
children.append(lines_grob(
|
|
2974
|
+
x=list(xs), y=list(ys), gp=gp,
|
|
2975
|
+
name=f"sf_mline_{i}_{j}",
|
|
2976
|
+
))
|
|
2977
|
+
|
|
2978
|
+
elif isinstance(geom, (ShapelyPolygon,)):
|
|
2979
|
+
# Exterior ring
|
|
2980
|
+
xs, ys = geom.exterior.coords.xy
|
|
2981
|
+
children.append(polygon_grob(
|
|
2982
|
+
x=list(xs), y=list(ys), gp=gp, name=f"sf_poly_{i}",
|
|
2983
|
+
))
|
|
2984
|
+
|
|
2985
|
+
elif isinstance(geom, (ShapelyMultiPolygon,)):
|
|
2986
|
+
for j, poly in enumerate(geom.geoms):
|
|
2987
|
+
xs, ys = poly.exterior.coords.xy
|
|
2988
|
+
children.append(polygon_grob(
|
|
2989
|
+
x=list(xs), y=list(ys), gp=gp,
|
|
2990
|
+
name=f"sf_mpoly_{i}_{j}",
|
|
2991
|
+
))
|
|
2992
|
+
|
|
2993
|
+
elif isinstance(geom, (ShapelyGeometryCollection,)):
|
|
2994
|
+
# Recurse into collection
|
|
2995
|
+
sub_grobs = _sf_geometry_to_grobs(
|
|
2996
|
+
list(geom.geoms),
|
|
2997
|
+
colour=[col_i] * len(geom.geoms),
|
|
2998
|
+
fill=[fill_i] * len(geom.geoms),
|
|
2999
|
+
linewidth=[lwd_i] * len(geom.geoms),
|
|
3000
|
+
linetype=[lty_i] * len(geom.geoms),
|
|
3001
|
+
point_size=[sz_i] * len(geom.geoms),
|
|
3002
|
+
pch=[pch_i] * len(geom.geoms),
|
|
3003
|
+
lineend=lineend, linejoin=linejoin,
|
|
3004
|
+
)
|
|
3005
|
+
children.append(sub_grobs)
|
|
3006
|
+
|
|
3007
|
+
if not children:
|
|
3008
|
+
return null_grob()
|
|
3009
|
+
|
|
3010
|
+
return grob_tree(*children, name="sf_geometries")
|
|
3011
|
+
|
|
3012
|
+
|
|
3013
|
+
class GeomSf(Geom):
|
|
3014
|
+
"""Simple features geom.
|
|
3015
|
+
|
|
3016
|
+
Draws different geometric objects depending on the geometry type:
|
|
3017
|
+
points, lines, or polygons — mirroring R's ``geom_sf()``.
|
|
3018
|
+
"""
|
|
3019
|
+
|
|
3020
|
+
required_aes: Tuple[str, ...] = ("geometry",)
|
|
3021
|
+
default_aes: Mapping = Mapping(
|
|
3022
|
+
shape=None,
|
|
3023
|
+
colour=FromTheme("colour"),
|
|
3024
|
+
fill=FromTheme("fill"),
|
|
3025
|
+
size=None,
|
|
3026
|
+
linewidth=None,
|
|
3027
|
+
linetype=None,
|
|
3028
|
+
alpha=None,
|
|
3029
|
+
stroke=FromTheme("borderwidth"),
|
|
3030
|
+
)
|
|
3031
|
+
|
|
3032
|
+
def draw_panel(
|
|
3033
|
+
self,
|
|
3034
|
+
data: pd.DataFrame,
|
|
3035
|
+
panel_params: Any,
|
|
3036
|
+
coord: Any,
|
|
3037
|
+
legend: Any = None,
|
|
3038
|
+
lineend: str = "butt",
|
|
3039
|
+
linejoin: str = "round",
|
|
3040
|
+
linemitre: float = 10,
|
|
3041
|
+
arrow: Any = None,
|
|
3042
|
+
na_rm: bool = True,
|
|
3043
|
+
**params: Any,
|
|
3044
|
+
) -> Any:
|
|
3045
|
+
"""Draw sf geometries.
|
|
3046
|
+
|
|
3047
|
+
Mirrors R's ``GeomSf$draw_panel``: classifies each geometry
|
|
3048
|
+
as point/line/other, computes per-type graphical parameters,
|
|
3049
|
+
and renders via ``_sf_geometry_to_grobs``.
|
|
3050
|
+
"""
|
|
3051
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
3052
|
+
|
|
3053
|
+
if "geometry" not in coords.columns:
|
|
3054
|
+
return null_grob()
|
|
3055
|
+
|
|
3056
|
+
import shapely
|
|
3057
|
+
|
|
3058
|
+
n = len(coords)
|
|
3059
|
+
|
|
3060
|
+
# Classify geometry types (mirrors R's sf_types vector)
|
|
3061
|
+
types = coords["geometry"].apply(
|
|
3062
|
+
lambda g: _SF_TYPES.get(g.geom_type, "other") if g is not None else "other"
|
|
3063
|
+
)
|
|
3064
|
+
is_point = types == "point"
|
|
3065
|
+
is_line = types == "line"
|
|
3066
|
+
is_collection = types == "collection"
|
|
3067
|
+
|
|
3068
|
+
# Shape translation
|
|
3069
|
+
shape = coords.get("shape", pd.Series([19] * n))
|
|
3070
|
+
shape = shape.apply(
|
|
3071
|
+
lambda s: translate_shape_string(s) if isinstance(s, str) else (s if s is not None else 19)
|
|
3072
|
+
)
|
|
3073
|
+
|
|
3074
|
+
# Fill with alpha (mirrors R: fill_alpha for all, arrow.fill for lines)
|
|
3075
|
+
fill_raw = coords.get("fill", pd.Series([np.nan] * n))
|
|
3076
|
+
alpha_raw = coords.get("alpha", pd.Series([1.0] * n)).fillna(1.0)
|
|
3077
|
+
fill_vals = _fill_alpha(fill_raw, alpha_raw)
|
|
3078
|
+
|
|
3079
|
+
# Colour with alpha for points and lines
|
|
3080
|
+
colour = coords.get("colour", pd.Series(["black"] * n))
|
|
3081
|
+
|
|
3082
|
+
# Point size vs linewidth (R: point_size for points/collections,
|
|
3083
|
+
# linewidth for everything else)
|
|
3084
|
+
size_raw = coords.get("size", pd.Series([1.5] * n)).fillna(1.5)
|
|
3085
|
+
lw_raw = coords.get("linewidth", pd.Series([0.5] * n)).fillna(0.5)
|
|
3086
|
+
point_size = size_raw.copy()
|
|
3087
|
+
point_size[~(is_point | is_collection)] = lw_raw[~(is_point | is_collection)]
|
|
3088
|
+
|
|
3089
|
+
# Stroke
|
|
3090
|
+
stroke_raw = coords.get("stroke", pd.Series([0.5] * n)).fillna(0.5)
|
|
3091
|
+
stroke_vals = stroke_raw * _STROKE / 2
|
|
3092
|
+
font_size = point_size * _PT + stroke_vals
|
|
3093
|
+
|
|
3094
|
+
# Linewidth
|
|
3095
|
+
linewidth = lw_raw * _PT
|
|
3096
|
+
linewidth[is_point] = stroke_vals[is_point]
|
|
3097
|
+
|
|
3098
|
+
linetype = coords.get("linetype", pd.Series([1] * n))
|
|
3099
|
+
|
|
3100
|
+
return _sf_geometry_to_grobs(
|
|
3101
|
+
coords["geometry"],
|
|
3102
|
+
colour=colour.values,
|
|
3103
|
+
fill=fill_vals if hasattr(fill_vals, '__len__') else [fill_vals] * n,
|
|
3104
|
+
linewidth=linewidth.values,
|
|
3105
|
+
linetype=linetype.values,
|
|
3106
|
+
point_size=font_size.values,
|
|
3107
|
+
pch=shape.values,
|
|
3108
|
+
lineend=lineend,
|
|
3109
|
+
linejoin=linejoin,
|
|
3110
|
+
)
|
|
3111
|
+
|
|
3112
|
+
def draw_key(self, data: Any, params: Dict[str, Any], size: Any = None) -> Any:
|
|
3113
|
+
legend_type = params.get("legend", "other")
|
|
3114
|
+
if legend_type == "point":
|
|
3115
|
+
return draw_key_point(data, params, size)
|
|
3116
|
+
elif legend_type == "line":
|
|
3117
|
+
return draw_key_path(data, params, size)
|
|
3118
|
+
return draw_key_polygon(data, params, size)
|
|
3119
|
+
|
|
3120
|
+
|
|
3121
|
+
# Placeholder classes for annotation geoms
|
|
3122
|
+
class GeomAnnotationMap(GeomPolygon):
|
|
3123
|
+
"""Annotation map geom."""
|
|
3124
|
+
pass
|
|
3125
|
+
|
|
3126
|
+
|
|
3127
|
+
class GeomCustomAnn(Geom):
|
|
3128
|
+
"""Custom annotation geom."""
|
|
3129
|
+
|
|
3130
|
+
def draw_panel(self, data: pd.DataFrame = None, panel_params: Any = None,
|
|
3131
|
+
coord: Any = None, grob: Any = None, **params: Any) -> Any:
|
|
3132
|
+
return grob if grob is not None else null_grob()
|
|
3133
|
+
|
|
3134
|
+
|
|
3135
|
+
class GeomRasterAnn(Geom):
|
|
3136
|
+
"""Raster annotation geom."""
|
|
3137
|
+
|
|
3138
|
+
def draw_panel(self, data: pd.DataFrame = None, panel_params: Any = None,
|
|
3139
|
+
coord: Any = None, raster: Any = None, **params: Any) -> Any:
|
|
3140
|
+
if raster is not None:
|
|
3141
|
+
return raster_grob(image=raster)
|
|
3142
|
+
return null_grob()
|
|
3143
|
+
|
|
3144
|
+
|
|
3145
|
+
def _calc_logticks(
|
|
3146
|
+
base: float = 10,
|
|
3147
|
+
minpow: int = 0,
|
|
3148
|
+
maxpow: int = 1,
|
|
3149
|
+
start: float = 0.0,
|
|
3150
|
+
shortend: float = 0.1,
|
|
3151
|
+
midend: float = 0.2,
|
|
3152
|
+
longend: float = 0.3,
|
|
3153
|
+
) -> pd.DataFrame:
|
|
3154
|
+
"""Compute log tick mark positions and lengths.
|
|
3155
|
+
|
|
3156
|
+
Mirrors R's ``calc_logticks()`` from ``annotation-logticks.R``.
|
|
3157
|
+
|
|
3158
|
+
Returns
|
|
3159
|
+
-------
|
|
3160
|
+
pd.DataFrame
|
|
3161
|
+
Columns: ``value``, ``start``, ``end``.
|
|
3162
|
+
"""
|
|
3163
|
+
ticks_per_base = int(base) - 1
|
|
3164
|
+
reps = maxpow - minpow
|
|
3165
|
+
|
|
3166
|
+
if reps <= 0 or ticks_per_base <= 0:
|
|
3167
|
+
return pd.DataFrame({"value": [base ** maxpow], "start": [start], "end": [longend]})
|
|
3168
|
+
|
|
3169
|
+
ticknums = np.tile(np.linspace(1, base - 1, ticks_per_base), reps)
|
|
3170
|
+
powers = np.repeat(np.arange(minpow, maxpow), ticks_per_base)
|
|
3171
|
+
ticks = ticknums * (base ** powers)
|
|
3172
|
+
ticks = np.append(ticks, base ** maxpow)
|
|
3173
|
+
|
|
3174
|
+
tickend = np.full(len(ticks), shortend)
|
|
3175
|
+
cycle_idx = (ticknums - 1).astype(int)
|
|
3176
|
+
cycle_idx = np.append(cycle_idx, 0)
|
|
3177
|
+
|
|
3178
|
+
# Major ticks (at each power of base)
|
|
3179
|
+
tickend[cycle_idx == 0] = longend
|
|
3180
|
+
|
|
3181
|
+
# Mid ticks (at base/2, e.g. 5 for base 10)
|
|
3182
|
+
longtick_after = ticks_per_base // 2
|
|
3183
|
+
tickend[cycle_idx == longtick_after] = midend
|
|
3184
|
+
|
|
3185
|
+
return pd.DataFrame({
|
|
3186
|
+
"value": ticks,
|
|
3187
|
+
"start": np.full(len(ticks), start),
|
|
3188
|
+
"end": tickend,
|
|
3189
|
+
})
|
|
3190
|
+
|
|
3191
|
+
|
|
3192
|
+
class GeomLogticks(Geom):
|
|
3193
|
+
"""Log-scale tick marks geom.
|
|
3194
|
+
|
|
3195
|
+
Mirrors R's ``GeomLogticks`` from ``annotation-logticks.R``.
|
|
3196
|
+
Draws diminishing tick marks at log-spaced intervals on specified
|
|
3197
|
+
sides of the plot panel.
|
|
3198
|
+
"""
|
|
3199
|
+
|
|
3200
|
+
default_aes: Mapping = Mapping(
|
|
3201
|
+
colour=FromTheme("colour", fallback="ink"),
|
|
3202
|
+
linewidth=FromTheme("linewidth"),
|
|
3203
|
+
linetype=FromTheme("linetype"),
|
|
3204
|
+
alpha=1.0,
|
|
3205
|
+
)
|
|
3206
|
+
|
|
3207
|
+
def draw_panel(
|
|
3208
|
+
self,
|
|
3209
|
+
data: pd.DataFrame = None,
|
|
3210
|
+
panel_params: Any = None,
|
|
3211
|
+
coord: Any = None,
|
|
3212
|
+
base: float = 10,
|
|
3213
|
+
sides: str = "bl",
|
|
3214
|
+
outside: bool = False,
|
|
3215
|
+
scaled: bool = True,
|
|
3216
|
+
short: float = 0.1,
|
|
3217
|
+
mid: float = 0.2,
|
|
3218
|
+
long: float = 0.3,
|
|
3219
|
+
**params: Any,
|
|
3220
|
+
) -> Any:
|
|
3221
|
+
"""Draw log tick marks on panel edges.
|
|
3222
|
+
|
|
3223
|
+
Mirrors R's ``GeomLogticks$draw_panel``.
|
|
3224
|
+
"""
|
|
3225
|
+
if panel_params is None:
|
|
3226
|
+
return null_grob()
|
|
3227
|
+
|
|
3228
|
+
x_range = panel_params.get("x_range") or panel_params.get("x.range")
|
|
3229
|
+
y_range = panel_params.get("y_range") or panel_params.get("y.range")
|
|
3230
|
+
|
|
3231
|
+
# Extract gp from data row
|
|
3232
|
+
colour = "black"
|
|
3233
|
+
linewidth_val = 0.5
|
|
3234
|
+
linetype_val = 1
|
|
3235
|
+
alpha_val = 1.0
|
|
3236
|
+
if data is not None and len(data) > 0:
|
|
3237
|
+
row = data.iloc[0]
|
|
3238
|
+
colour = row.get("colour", "black")
|
|
3239
|
+
linewidth_val = float(row.get("linewidth", 0.5))
|
|
3240
|
+
linetype_val = row.get("linetype", 1)
|
|
3241
|
+
alpha_val = float(row.get("alpha", 1.0) or 1.0)
|
|
3242
|
+
|
|
3243
|
+
gp = Gpar(col=colour, lwd=linewidth_val, lty=linetype_val, alpha=alpha_val)
|
|
3244
|
+
|
|
3245
|
+
ticks_grobs = []
|
|
3246
|
+
|
|
3247
|
+
# X-axis ticks (bottom / top)
|
|
3248
|
+
if ("b" in sides or "t" in sides) and x_range is not None:
|
|
3249
|
+
xr = [float(x_range[0]), float(x_range[1])]
|
|
3250
|
+
if all(np.isfinite(xr)):
|
|
3251
|
+
xticks = _calc_logticks(
|
|
3252
|
+
base=base,
|
|
3253
|
+
minpow=int(np.floor(xr[0])),
|
|
3254
|
+
maxpow=int(np.ceil(xr[1])),
|
|
3255
|
+
start=0.0, shortend=short, midend=mid, longend=long,
|
|
3256
|
+
)
|
|
3257
|
+
if scaled:
|
|
3258
|
+
xticks["value"] = np.log(xticks["value"]) / np.log(base)
|
|
3259
|
+
|
|
3260
|
+
# Rescale to [0, 1] NPC
|
|
3261
|
+
span = xr[1] - xr[0]
|
|
3262
|
+
if span > 0:
|
|
3263
|
+
xticks["x"] = (xticks["value"] - xr[0]) / span
|
|
3264
|
+
xticks = xticks[(xticks["x"] >= 0) & (xticks["x"] <= 1)]
|
|
3265
|
+
|
|
3266
|
+
if outside:
|
|
3267
|
+
xticks["end"] = -xticks["end"]
|
|
3268
|
+
|
|
3269
|
+
if "b" in sides and len(xticks) > 0:
|
|
3270
|
+
ticks_grobs.append(segments_grob(
|
|
3271
|
+
x0=xticks["x"].values, y0=np.zeros(len(xticks)),
|
|
3272
|
+
x1=xticks["x"].values, y1=xticks["end"].values * 0.02,
|
|
3273
|
+
gp=gp, name="logtick_x_b",
|
|
3274
|
+
))
|
|
3275
|
+
|
|
3276
|
+
if "t" in sides and len(xticks) > 0:
|
|
3277
|
+
ticks_grobs.append(segments_grob(
|
|
3278
|
+
x0=xticks["x"].values, y0=np.ones(len(xticks)),
|
|
3279
|
+
x1=xticks["x"].values, y1=1.0 - xticks["end"].values * 0.02,
|
|
3280
|
+
gp=gp, name="logtick_x_t",
|
|
3281
|
+
))
|
|
3282
|
+
|
|
3283
|
+
# Y-axis ticks (left / right)
|
|
3284
|
+
if ("l" in sides or "r" in sides) and y_range is not None:
|
|
3285
|
+
yr = [float(y_range[0]), float(y_range[1])]
|
|
3286
|
+
if all(np.isfinite(yr)):
|
|
3287
|
+
yticks = _calc_logticks(
|
|
3288
|
+
base=base,
|
|
3289
|
+
minpow=int(np.floor(yr[0])),
|
|
3290
|
+
maxpow=int(np.ceil(yr[1])),
|
|
3291
|
+
start=0.0, shortend=short, midend=mid, longend=long,
|
|
3292
|
+
)
|
|
3293
|
+
if scaled:
|
|
3294
|
+
yticks["value"] = np.log(yticks["value"]) / np.log(base)
|
|
3295
|
+
|
|
3296
|
+
span = yr[1] - yr[0]
|
|
3297
|
+
if span > 0:
|
|
3298
|
+
yticks["y"] = (yticks["value"] - yr[0]) / span
|
|
3299
|
+
yticks = yticks[(yticks["y"] >= 0) & (yticks["y"] <= 1)]
|
|
3300
|
+
|
|
3301
|
+
if outside:
|
|
3302
|
+
yticks["end"] = -yticks["end"]
|
|
3303
|
+
|
|
3304
|
+
if "l" in sides and len(yticks) > 0:
|
|
3305
|
+
ticks_grobs.append(segments_grob(
|
|
3306
|
+
x0=np.zeros(len(yticks)),
|
|
3307
|
+
y0=yticks["y"].values,
|
|
3308
|
+
x1=yticks["end"].values * 0.02,
|
|
3309
|
+
y1=yticks["y"].values,
|
|
3310
|
+
gp=gp, name="logtick_y_l",
|
|
3311
|
+
))
|
|
3312
|
+
|
|
3313
|
+
if "r" in sides and len(yticks) > 0:
|
|
3314
|
+
ticks_grobs.append(segments_grob(
|
|
3315
|
+
x0=np.ones(len(yticks)),
|
|
3316
|
+
y0=yticks["y"].values,
|
|
3317
|
+
x1=1.0 - yticks["end"].values * 0.02,
|
|
3318
|
+
y1=yticks["y"].values,
|
|
3319
|
+
gp=gp, name="logtick_y_r",
|
|
3320
|
+
))
|
|
3321
|
+
|
|
3322
|
+
if not ticks_grobs:
|
|
3323
|
+
return null_grob()
|
|
3324
|
+
|
|
3325
|
+
return grob_tree(*ticks_grobs, name="logticks")
|
|
3326
|
+
|
|
3327
|
+
|
|
3328
|
+
# ===========================================================================
|
|
3329
|
+
# GeomCount / GeomJitter (trivial wrappers)
|
|
3330
|
+
# ===========================================================================
|
|
3331
|
+
|
|
3332
|
+
# GeomCount is GeomPoint + stat_sum
|
|
3333
|
+
GeomCount = GeomPoint # alias
|
|
3334
|
+
|
|
3335
|
+
# GeomJitter is GeomPoint + position_jitter
|
|
3336
|
+
GeomJitter = GeomPoint # alias
|
|
3337
|
+
|
|
3338
|
+
|
|
3339
|
+
# ===========================================================================
|
|
3340
|
+
# Constructor functions
|
|
3341
|
+
# ===========================================================================
|
|
3342
|
+
|
|
3343
|
+
def _layer_import():
|
|
3344
|
+
"""Lazy import of ``layer`` to avoid circular imports."""
|
|
3345
|
+
from ggplot2_py.layer import layer
|
|
3346
|
+
return layer
|
|
3347
|
+
|
|
3348
|
+
|
|
3349
|
+
# ---------------------------------------------------------------------------
|
|
3350
|
+
# Point / Path / Line / Step
|
|
3351
|
+
# ---------------------------------------------------------------------------
|
|
3352
|
+
|
|
3353
|
+
def geom_point(
|
|
3354
|
+
mapping: Optional[Mapping] = None,
|
|
3355
|
+
data: Any = None,
|
|
3356
|
+
stat: str = "identity",
|
|
3357
|
+
position: str = "identity",
|
|
3358
|
+
na_rm: bool = False,
|
|
3359
|
+
show_legend: Any = None,
|
|
3360
|
+
inherit_aes: bool = True,
|
|
3361
|
+
**kwargs: Any,
|
|
3362
|
+
) -> Any:
|
|
3363
|
+
"""Create a point (scatter) layer.
|
|
3364
|
+
|
|
3365
|
+
Parameters
|
|
3366
|
+
----------
|
|
3367
|
+
mapping : Mapping, optional
|
|
3368
|
+
data : DataFrame, optional
|
|
3369
|
+
stat, position : str
|
|
3370
|
+
na_rm : bool
|
|
3371
|
+
show_legend : bool or None
|
|
3372
|
+
inherit_aes : bool
|
|
3373
|
+
**kwargs : additional aesthetic or parameter overrides
|
|
3374
|
+
|
|
3375
|
+
Returns
|
|
3376
|
+
-------
|
|
3377
|
+
Layer
|
|
3378
|
+
"""
|
|
3379
|
+
layer = _layer_import()
|
|
3380
|
+
return layer(
|
|
3381
|
+
geom=GeomPoint, stat=stat, data=data, mapping=mapping,
|
|
3382
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3383
|
+
params={"na_rm": na_rm, **kwargs},
|
|
3384
|
+
)
|
|
3385
|
+
|
|
3386
|
+
|
|
3387
|
+
def geom_path(
|
|
3388
|
+
mapping: Optional[Mapping] = None,
|
|
3389
|
+
data: Any = None,
|
|
3390
|
+
stat: str = "identity",
|
|
3391
|
+
position: str = "identity",
|
|
3392
|
+
na_rm: bool = False,
|
|
3393
|
+
show_legend: Any = None,
|
|
3394
|
+
inherit_aes: bool = True,
|
|
3395
|
+
**kwargs: Any,
|
|
3396
|
+
) -> Any:
|
|
3397
|
+
"""Create a path layer (connects observations in data order)."""
|
|
3398
|
+
layer = _layer_import()
|
|
3399
|
+
return layer(
|
|
3400
|
+
geom=GeomPath, stat=stat, data=data, mapping=mapping,
|
|
3401
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3402
|
+
params={"na_rm": na_rm, **kwargs},
|
|
3403
|
+
)
|
|
3404
|
+
|
|
3405
|
+
|
|
3406
|
+
def geom_line(
|
|
3407
|
+
mapping: Optional[Mapping] = None,
|
|
3408
|
+
data: Any = None,
|
|
3409
|
+
stat: str = "identity",
|
|
3410
|
+
position: str = "identity",
|
|
3411
|
+
na_rm: bool = False,
|
|
3412
|
+
show_legend: Any = None,
|
|
3413
|
+
inherit_aes: bool = True,
|
|
3414
|
+
orientation: Any = None,
|
|
3415
|
+
**kwargs: Any,
|
|
3416
|
+
) -> Any:
|
|
3417
|
+
"""Create a line layer (connects observations sorted by x)."""
|
|
3418
|
+
layer = _layer_import()
|
|
3419
|
+
return layer(
|
|
3420
|
+
geom=GeomLine, stat=stat, data=data, mapping=mapping,
|
|
3421
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3422
|
+
params={"na_rm": na_rm, "orientation": orientation, **kwargs},
|
|
3423
|
+
)
|
|
3424
|
+
|
|
3425
|
+
|
|
3426
|
+
def geom_step(
|
|
3427
|
+
mapping: Optional[Mapping] = None,
|
|
3428
|
+
data: Any = None,
|
|
3429
|
+
stat: str = "identity",
|
|
3430
|
+
position: str = "identity",
|
|
3431
|
+
na_rm: bool = False,
|
|
3432
|
+
show_legend: Any = None,
|
|
3433
|
+
inherit_aes: bool = True,
|
|
3434
|
+
direction: str = "hv",
|
|
3435
|
+
orientation: Any = None,
|
|
3436
|
+
**kwargs: Any,
|
|
3437
|
+
) -> Any:
|
|
3438
|
+
"""Create a stairstep layer."""
|
|
3439
|
+
layer = _layer_import()
|
|
3440
|
+
return layer(
|
|
3441
|
+
geom=GeomStep, stat=stat, data=data, mapping=mapping,
|
|
3442
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3443
|
+
params={"na_rm": na_rm, "direction": direction, "orientation": orientation, **kwargs},
|
|
3444
|
+
)
|
|
3445
|
+
|
|
3446
|
+
|
|
3447
|
+
# ---------------------------------------------------------------------------
|
|
3448
|
+
# Bar / Col
|
|
3449
|
+
# ---------------------------------------------------------------------------
|
|
3450
|
+
|
|
3451
|
+
def geom_bar(
|
|
3452
|
+
mapping: Optional[Mapping] = None,
|
|
3453
|
+
data: Any = None,
|
|
3454
|
+
stat: str = "count",
|
|
3455
|
+
position: str = "stack",
|
|
3456
|
+
na_rm: bool = False,
|
|
3457
|
+
show_legend: Any = None,
|
|
3458
|
+
inherit_aes: bool = True,
|
|
3459
|
+
just: float = 0.5,
|
|
3460
|
+
orientation: Any = None,
|
|
3461
|
+
**kwargs: Any,
|
|
3462
|
+
) -> Any:
|
|
3463
|
+
"""Create a bar layer."""
|
|
3464
|
+
layer = _layer_import()
|
|
3465
|
+
return layer(
|
|
3466
|
+
geom=GeomBar, stat=stat, data=data, mapping=mapping,
|
|
3467
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3468
|
+
params={"na_rm": na_rm, "just": just, "orientation": orientation, **kwargs},
|
|
3469
|
+
)
|
|
3470
|
+
|
|
3471
|
+
|
|
3472
|
+
def geom_col(
|
|
3473
|
+
mapping: Optional[Mapping] = None,
|
|
3474
|
+
data: Any = None,
|
|
3475
|
+
stat: str = "identity",
|
|
3476
|
+
position: str = "stack",
|
|
3477
|
+
na_rm: bool = False,
|
|
3478
|
+
show_legend: Any = None,
|
|
3479
|
+
inherit_aes: bool = True,
|
|
3480
|
+
just: float = 0.5,
|
|
3481
|
+
**kwargs: Any,
|
|
3482
|
+
) -> Any:
|
|
3483
|
+
"""Create a column layer (bars with stat = identity)."""
|
|
3484
|
+
layer = _layer_import()
|
|
3485
|
+
return layer(
|
|
3486
|
+
geom=GeomCol, stat=stat, data=data, mapping=mapping,
|
|
3487
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3488
|
+
params={"na_rm": na_rm, "just": just, **kwargs},
|
|
3489
|
+
)
|
|
3490
|
+
|
|
3491
|
+
|
|
3492
|
+
# ---------------------------------------------------------------------------
|
|
3493
|
+
# Rect / Tile / Raster
|
|
3494
|
+
# ---------------------------------------------------------------------------
|
|
3495
|
+
|
|
3496
|
+
def geom_rect(
|
|
3497
|
+
mapping: Optional[Mapping] = None,
|
|
3498
|
+
data: Any = None,
|
|
3499
|
+
stat: str = "identity",
|
|
3500
|
+
position: str = "identity",
|
|
3501
|
+
na_rm: bool = False,
|
|
3502
|
+
show_legend: Any = None,
|
|
3503
|
+
inherit_aes: bool = True,
|
|
3504
|
+
**kwargs: Any,
|
|
3505
|
+
) -> Any:
|
|
3506
|
+
"""Create a rectangle layer."""
|
|
3507
|
+
layer = _layer_import()
|
|
3508
|
+
return layer(
|
|
3509
|
+
geom=GeomRect, stat=stat, data=data, mapping=mapping,
|
|
3510
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3511
|
+
params={"na_rm": na_rm, **kwargs},
|
|
3512
|
+
)
|
|
3513
|
+
|
|
3514
|
+
|
|
3515
|
+
def geom_tile(
|
|
3516
|
+
mapping: Optional[Mapping] = None,
|
|
3517
|
+
data: Any = None,
|
|
3518
|
+
stat: str = "identity",
|
|
3519
|
+
position: str = "identity",
|
|
3520
|
+
na_rm: bool = False,
|
|
3521
|
+
show_legend: Any = None,
|
|
3522
|
+
inherit_aes: bool = True,
|
|
3523
|
+
**kwargs: Any,
|
|
3524
|
+
) -> Any:
|
|
3525
|
+
"""Create a tile layer."""
|
|
3526
|
+
layer = _layer_import()
|
|
3527
|
+
return layer(
|
|
3528
|
+
geom=GeomTile, stat=stat, data=data, mapping=mapping,
|
|
3529
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3530
|
+
params={"na_rm": na_rm, **kwargs},
|
|
3531
|
+
)
|
|
3532
|
+
|
|
3533
|
+
|
|
3534
|
+
def geom_raster(
|
|
3535
|
+
mapping: Optional[Mapping] = None,
|
|
3536
|
+
data: Any = None,
|
|
3537
|
+
stat: str = "identity",
|
|
3538
|
+
position: str = "identity",
|
|
3539
|
+
na_rm: bool = False,
|
|
3540
|
+
show_legend: Any = None,
|
|
3541
|
+
inherit_aes: bool = True,
|
|
3542
|
+
hjust: float = 0.5,
|
|
3543
|
+
vjust: float = 0.5,
|
|
3544
|
+
interpolate: bool = False,
|
|
3545
|
+
**kwargs: Any,
|
|
3546
|
+
) -> Any:
|
|
3547
|
+
"""Create a raster layer."""
|
|
3548
|
+
layer = _layer_import()
|
|
3549
|
+
return layer(
|
|
3550
|
+
geom=GeomRaster, stat=stat, data=data, mapping=mapping,
|
|
3551
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3552
|
+
params={"na_rm": na_rm, "hjust": hjust, "vjust": vjust, "interpolate": interpolate, **kwargs},
|
|
3553
|
+
)
|
|
3554
|
+
|
|
3555
|
+
|
|
3556
|
+
# ---------------------------------------------------------------------------
|
|
3557
|
+
# Text / Label
|
|
3558
|
+
# ---------------------------------------------------------------------------
|
|
3559
|
+
|
|
3560
|
+
def geom_text(
|
|
3561
|
+
mapping: Optional[Mapping] = None,
|
|
3562
|
+
data: Any = None,
|
|
3563
|
+
stat: str = "identity",
|
|
3564
|
+
position: str = "nudge",
|
|
3565
|
+
na_rm: bool = False,
|
|
3566
|
+
show_legend: Any = None,
|
|
3567
|
+
inherit_aes: bool = True,
|
|
3568
|
+
parse: bool = False,
|
|
3569
|
+
check_overlap: bool = False,
|
|
3570
|
+
size_unit: str = "mm",
|
|
3571
|
+
**kwargs: Any,
|
|
3572
|
+
) -> Any:
|
|
3573
|
+
"""Create a text layer."""
|
|
3574
|
+
layer = _layer_import()
|
|
3575
|
+
return layer(
|
|
3576
|
+
geom=GeomText, stat=stat, data=data, mapping=mapping,
|
|
3577
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3578
|
+
params={"na_rm": na_rm, "parse": parse, "check_overlap": check_overlap, "size_unit": size_unit, **kwargs},
|
|
3579
|
+
)
|
|
3580
|
+
|
|
3581
|
+
|
|
3582
|
+
def geom_label(
|
|
3583
|
+
mapping: Optional[Mapping] = None,
|
|
3584
|
+
data: Any = None,
|
|
3585
|
+
stat: str = "identity",
|
|
3586
|
+
position: str = "nudge",
|
|
3587
|
+
na_rm: bool = False,
|
|
3588
|
+
show_legend: Any = None,
|
|
3589
|
+
inherit_aes: bool = True,
|
|
3590
|
+
parse: bool = False,
|
|
3591
|
+
size_unit: str = "mm",
|
|
3592
|
+
**kwargs: Any,
|
|
3593
|
+
) -> Any:
|
|
3594
|
+
"""Create a label layer (text with background box)."""
|
|
3595
|
+
layer = _layer_import()
|
|
3596
|
+
return layer(
|
|
3597
|
+
geom=GeomLabel, stat=stat, data=data, mapping=mapping,
|
|
3598
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3599
|
+
params={"na_rm": na_rm, "parse": parse, "size_unit": size_unit, **kwargs},
|
|
3600
|
+
)
|
|
3601
|
+
|
|
3602
|
+
|
|
3603
|
+
# ---------------------------------------------------------------------------
|
|
3604
|
+
# Boxplot / Violin / Dotplot
|
|
3605
|
+
# ---------------------------------------------------------------------------
|
|
3606
|
+
|
|
3607
|
+
def geom_boxplot(
|
|
3608
|
+
mapping: Optional[Mapping] = None,
|
|
3609
|
+
data: Any = None,
|
|
3610
|
+
stat: str = "boxplot",
|
|
3611
|
+
position: str = "dodge2",
|
|
3612
|
+
na_rm: bool = False,
|
|
3613
|
+
show_legend: Any = None,
|
|
3614
|
+
inherit_aes: bool = True,
|
|
3615
|
+
outliers: bool = True,
|
|
3616
|
+
notch: bool = False,
|
|
3617
|
+
notchwidth: float = 0.5,
|
|
3618
|
+
staplewidth: float = 0,
|
|
3619
|
+
varwidth: bool = False,
|
|
3620
|
+
orientation: Any = None,
|
|
3621
|
+
**kwargs: Any,
|
|
3622
|
+
) -> Any:
|
|
3623
|
+
"""Create a boxplot layer."""
|
|
3624
|
+
layer = _layer_import()
|
|
3625
|
+
return layer(
|
|
3626
|
+
geom=GeomBoxplot, stat=stat, data=data, mapping=mapping,
|
|
3627
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3628
|
+
params={
|
|
3629
|
+
"na_rm": na_rm, "outliers": outliers, "notch": notch,
|
|
3630
|
+
"notchwidth": notchwidth, "staplewidth": staplewidth,
|
|
3631
|
+
"varwidth": varwidth, "orientation": orientation, **kwargs,
|
|
3632
|
+
},
|
|
3633
|
+
)
|
|
3634
|
+
|
|
3635
|
+
|
|
3636
|
+
def geom_violin(
|
|
3637
|
+
mapping: Optional[Mapping] = None,
|
|
3638
|
+
data: Any = None,
|
|
3639
|
+
stat: str = "ydensity",
|
|
3640
|
+
position: str = "dodge",
|
|
3641
|
+
na_rm: bool = False,
|
|
3642
|
+
show_legend: Any = None,
|
|
3643
|
+
inherit_aes: bool = True,
|
|
3644
|
+
trim: bool = True,
|
|
3645
|
+
scale: str = "area",
|
|
3646
|
+
orientation: Any = None,
|
|
3647
|
+
**kwargs: Any,
|
|
3648
|
+
) -> Any:
|
|
3649
|
+
"""Create a violin layer."""
|
|
3650
|
+
layer = _layer_import()
|
|
3651
|
+
return layer(
|
|
3652
|
+
geom=GeomViolin, stat=stat, data=data, mapping=mapping,
|
|
3653
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3654
|
+
params={"na_rm": na_rm, "trim": trim, "scale": scale, "orientation": orientation, **kwargs},
|
|
3655
|
+
)
|
|
3656
|
+
|
|
3657
|
+
|
|
3658
|
+
def geom_dotplot(
|
|
3659
|
+
mapping: Optional[Mapping] = None,
|
|
3660
|
+
data: Any = None,
|
|
3661
|
+
stat: str = "bindot",
|
|
3662
|
+
position: str = "identity",
|
|
3663
|
+
na_rm: bool = False,
|
|
3664
|
+
show_legend: Any = None,
|
|
3665
|
+
inherit_aes: bool = True,
|
|
3666
|
+
binaxis: str = "x",
|
|
3667
|
+
method: str = "dotdensity",
|
|
3668
|
+
stackdir: str = "up",
|
|
3669
|
+
stackratio: float = 1,
|
|
3670
|
+
dotsize: float = 1,
|
|
3671
|
+
**kwargs: Any,
|
|
3672
|
+
) -> Any:
|
|
3673
|
+
"""Create a dotplot layer."""
|
|
3674
|
+
layer = _layer_import()
|
|
3675
|
+
return layer(
|
|
3676
|
+
geom=GeomDotplot, stat=stat, data=data, mapping=mapping,
|
|
3677
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3678
|
+
params={
|
|
3679
|
+
"na_rm": na_rm, "binaxis": binaxis, "method": method,
|
|
3680
|
+
"stackdir": stackdir, "stackratio": stackratio, "dotsize": dotsize,
|
|
3681
|
+
**kwargs,
|
|
3682
|
+
},
|
|
3683
|
+
)
|
|
3684
|
+
|
|
3685
|
+
|
|
3686
|
+
# ---------------------------------------------------------------------------
|
|
3687
|
+
# Ribbon / Area / Smooth
|
|
3688
|
+
# ---------------------------------------------------------------------------
|
|
3689
|
+
|
|
3690
|
+
def geom_ribbon(
|
|
3691
|
+
mapping: Optional[Mapping] = None,
|
|
3692
|
+
data: Any = None,
|
|
3693
|
+
stat: str = "identity",
|
|
3694
|
+
position: str = "identity",
|
|
3695
|
+
na_rm: bool = False,
|
|
3696
|
+
show_legend: Any = None,
|
|
3697
|
+
inherit_aes: bool = True,
|
|
3698
|
+
orientation: Any = None,
|
|
3699
|
+
outline_type: str = "both",
|
|
3700
|
+
**kwargs: Any,
|
|
3701
|
+
) -> Any:
|
|
3702
|
+
"""Create a ribbon layer."""
|
|
3703
|
+
layer = _layer_import()
|
|
3704
|
+
return layer(
|
|
3705
|
+
geom=GeomRibbon, stat=stat, data=data, mapping=mapping,
|
|
3706
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3707
|
+
params={"na_rm": na_rm, "orientation": orientation, "outline_type": outline_type, **kwargs},
|
|
3708
|
+
)
|
|
3709
|
+
|
|
3710
|
+
|
|
3711
|
+
def geom_area(
|
|
3712
|
+
mapping: Optional[Mapping] = None,
|
|
3713
|
+
data: Any = None,
|
|
3714
|
+
stat: str = "align",
|
|
3715
|
+
position: str = "stack",
|
|
3716
|
+
na_rm: bool = False,
|
|
3717
|
+
show_legend: Any = None,
|
|
3718
|
+
inherit_aes: bool = True,
|
|
3719
|
+
orientation: Any = None,
|
|
3720
|
+
outline_type: str = "upper",
|
|
3721
|
+
**kwargs: Any,
|
|
3722
|
+
) -> Any:
|
|
3723
|
+
"""Create an area layer."""
|
|
3724
|
+
layer = _layer_import()
|
|
3725
|
+
return layer(
|
|
3726
|
+
geom=GeomArea, stat=stat, data=data, mapping=mapping,
|
|
3727
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3728
|
+
params={"na_rm": na_rm, "orientation": orientation, "outline_type": outline_type, **kwargs},
|
|
3729
|
+
)
|
|
3730
|
+
|
|
3731
|
+
|
|
3732
|
+
def geom_smooth(
|
|
3733
|
+
mapping: Optional[Mapping] = None,
|
|
3734
|
+
data: Any = None,
|
|
3735
|
+
stat: str = "smooth",
|
|
3736
|
+
position: str = "identity",
|
|
3737
|
+
na_rm: bool = False,
|
|
3738
|
+
show_legend: Any = None,
|
|
3739
|
+
inherit_aes: bool = True,
|
|
3740
|
+
method: Any = None,
|
|
3741
|
+
formula: Any = None,
|
|
3742
|
+
se: bool = True,
|
|
3743
|
+
orientation: Any = None,
|
|
3744
|
+
**kwargs: Any,
|
|
3745
|
+
) -> Any:
|
|
3746
|
+
"""Create a smooth layer."""
|
|
3747
|
+
layer = _layer_import()
|
|
3748
|
+
params: Dict[str, Any] = {
|
|
3749
|
+
"na_rm": na_rm, "orientation": orientation, "se": se, **kwargs,
|
|
3750
|
+
}
|
|
3751
|
+
if stat == "smooth":
|
|
3752
|
+
params["method"] = method
|
|
3753
|
+
params["formula"] = formula
|
|
3754
|
+
return layer(
|
|
3755
|
+
geom=GeomSmooth, stat=stat, data=data, mapping=mapping,
|
|
3756
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3757
|
+
params=params,
|
|
3758
|
+
)
|
|
3759
|
+
|
|
3760
|
+
|
|
3761
|
+
# ---------------------------------------------------------------------------
|
|
3762
|
+
# Polygon
|
|
3763
|
+
# ---------------------------------------------------------------------------
|
|
3764
|
+
|
|
3765
|
+
def geom_polygon(
|
|
3766
|
+
mapping: Optional[Mapping] = None,
|
|
3767
|
+
data: Any = None,
|
|
3768
|
+
stat: str = "identity",
|
|
3769
|
+
position: str = "identity",
|
|
3770
|
+
na_rm: bool = False,
|
|
3771
|
+
show_legend: Any = None,
|
|
3772
|
+
inherit_aes: bool = True,
|
|
3773
|
+
rule: str = "evenodd",
|
|
3774
|
+
**kwargs: Any,
|
|
3775
|
+
) -> Any:
|
|
3776
|
+
"""Create a polygon layer."""
|
|
3777
|
+
layer = _layer_import()
|
|
3778
|
+
return layer(
|
|
3779
|
+
geom=GeomPolygon, stat=stat, data=data, mapping=mapping,
|
|
3780
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3781
|
+
params={"na_rm": na_rm, "rule": rule, **kwargs},
|
|
3782
|
+
)
|
|
3783
|
+
|
|
3784
|
+
|
|
3785
|
+
# ---------------------------------------------------------------------------
|
|
3786
|
+
# Errorbar / Crossbar / Linerange / Pointrange
|
|
3787
|
+
# ---------------------------------------------------------------------------
|
|
3788
|
+
|
|
3789
|
+
def geom_errorbar(
|
|
3790
|
+
mapping: Optional[Mapping] = None,
|
|
3791
|
+
data: Any = None,
|
|
3792
|
+
stat: str = "identity",
|
|
3793
|
+
position: str = "identity",
|
|
3794
|
+
na_rm: bool = False,
|
|
3795
|
+
show_legend: Any = None,
|
|
3796
|
+
inherit_aes: bool = True,
|
|
3797
|
+
orientation: Any = None,
|
|
3798
|
+
**kwargs: Any,
|
|
3799
|
+
) -> Any:
|
|
3800
|
+
"""Create an errorbar layer."""
|
|
3801
|
+
layer = _layer_import()
|
|
3802
|
+
return layer(
|
|
3803
|
+
geom=GeomErrorbar, stat=stat, data=data, mapping=mapping,
|
|
3804
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3805
|
+
params={"na_rm": na_rm, "orientation": orientation, **kwargs},
|
|
3806
|
+
)
|
|
3807
|
+
|
|
3808
|
+
|
|
3809
|
+
def geom_errorbarh(
|
|
3810
|
+
mapping: Optional[Mapping] = None,
|
|
3811
|
+
data: Any = None,
|
|
3812
|
+
orientation: str = "y",
|
|
3813
|
+
**kwargs: Any,
|
|
3814
|
+
) -> Any:
|
|
3815
|
+
"""Create a horizontal errorbar (deprecated -- use geom_errorbar)."""
|
|
3816
|
+
warnings.warn(
|
|
3817
|
+
"geom_errorbarh() is deprecated. Use geom_errorbar(orientation='y').",
|
|
3818
|
+
FutureWarning,
|
|
3819
|
+
stacklevel=2,
|
|
3820
|
+
)
|
|
3821
|
+
return geom_errorbar(mapping=mapping, data=data, orientation=orientation, **kwargs)
|
|
3822
|
+
|
|
3823
|
+
|
|
3824
|
+
def geom_crossbar(
|
|
3825
|
+
mapping: Optional[Mapping] = None,
|
|
3826
|
+
data: Any = None,
|
|
3827
|
+
stat: str = "identity",
|
|
3828
|
+
position: str = "identity",
|
|
3829
|
+
na_rm: bool = False,
|
|
3830
|
+
show_legend: Any = None,
|
|
3831
|
+
inherit_aes: bool = True,
|
|
3832
|
+
orientation: Any = None,
|
|
3833
|
+
**kwargs: Any,
|
|
3834
|
+
) -> Any:
|
|
3835
|
+
"""Create a crossbar layer."""
|
|
3836
|
+
layer = _layer_import()
|
|
3837
|
+
return layer(
|
|
3838
|
+
geom=GeomCrossbar, stat=stat, data=data, mapping=mapping,
|
|
3839
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3840
|
+
params={"na_rm": na_rm, "orientation": orientation, **kwargs},
|
|
3841
|
+
)
|
|
3842
|
+
|
|
3843
|
+
|
|
3844
|
+
def geom_linerange(
|
|
3845
|
+
mapping: Optional[Mapping] = None,
|
|
3846
|
+
data: Any = None,
|
|
3847
|
+
stat: str = "identity",
|
|
3848
|
+
position: str = "identity",
|
|
3849
|
+
na_rm: bool = False,
|
|
3850
|
+
show_legend: Any = None,
|
|
3851
|
+
inherit_aes: bool = True,
|
|
3852
|
+
orientation: Any = None,
|
|
3853
|
+
**kwargs: Any,
|
|
3854
|
+
) -> Any:
|
|
3855
|
+
"""Create a linerange layer."""
|
|
3856
|
+
layer = _layer_import()
|
|
3857
|
+
return layer(
|
|
3858
|
+
geom=GeomLinerange, stat=stat, data=data, mapping=mapping,
|
|
3859
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3860
|
+
params={"na_rm": na_rm, "orientation": orientation, **kwargs},
|
|
3861
|
+
)
|
|
3862
|
+
|
|
3863
|
+
|
|
3864
|
+
def geom_pointrange(
|
|
3865
|
+
mapping: Optional[Mapping] = None,
|
|
3866
|
+
data: Any = None,
|
|
3867
|
+
stat: str = "identity",
|
|
3868
|
+
position: str = "identity",
|
|
3869
|
+
na_rm: bool = False,
|
|
3870
|
+
show_legend: Any = None,
|
|
3871
|
+
inherit_aes: bool = True,
|
|
3872
|
+
orientation: Any = None,
|
|
3873
|
+
fatten: float = 4,
|
|
3874
|
+
**kwargs: Any,
|
|
3875
|
+
) -> Any:
|
|
3876
|
+
"""Create a pointrange layer."""
|
|
3877
|
+
layer = _layer_import()
|
|
3878
|
+
return layer(
|
|
3879
|
+
geom=GeomPointrange, stat=stat, data=data, mapping=mapping,
|
|
3880
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3881
|
+
params={"na_rm": na_rm, "orientation": orientation, "fatten": fatten, **kwargs},
|
|
3882
|
+
)
|
|
3883
|
+
|
|
3884
|
+
|
|
3885
|
+
# ---------------------------------------------------------------------------
|
|
3886
|
+
# Segment / Curve / Spoke
|
|
3887
|
+
# ---------------------------------------------------------------------------
|
|
3888
|
+
|
|
3889
|
+
def geom_segment(
|
|
3890
|
+
mapping: Optional[Mapping] = None,
|
|
3891
|
+
data: Any = None,
|
|
3892
|
+
stat: str = "identity",
|
|
3893
|
+
position: str = "identity",
|
|
3894
|
+
na_rm: bool = False,
|
|
3895
|
+
show_legend: Any = None,
|
|
3896
|
+
inherit_aes: bool = True,
|
|
3897
|
+
**kwargs: Any,
|
|
3898
|
+
) -> Any:
|
|
3899
|
+
"""Create a segment layer."""
|
|
3900
|
+
layer = _layer_import()
|
|
3901
|
+
return layer(
|
|
3902
|
+
geom=GeomSegment, stat=stat, data=data, mapping=mapping,
|
|
3903
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3904
|
+
params={"na_rm": na_rm, **kwargs},
|
|
3905
|
+
)
|
|
3906
|
+
|
|
3907
|
+
|
|
3908
|
+
def geom_curve(
|
|
3909
|
+
mapping: Optional[Mapping] = None,
|
|
3910
|
+
data: Any = None,
|
|
3911
|
+
stat: str = "identity",
|
|
3912
|
+
position: str = "identity",
|
|
3913
|
+
na_rm: bool = False,
|
|
3914
|
+
show_legend: Any = None,
|
|
3915
|
+
inherit_aes: bool = True,
|
|
3916
|
+
curvature: float = 0.5,
|
|
3917
|
+
angle: float = 90,
|
|
3918
|
+
ncp: int = 5,
|
|
3919
|
+
**kwargs: Any,
|
|
3920
|
+
) -> Any:
|
|
3921
|
+
"""Create a curve layer."""
|
|
3922
|
+
layer = _layer_import()
|
|
3923
|
+
return layer(
|
|
3924
|
+
geom=GeomCurve, stat=stat, data=data, mapping=mapping,
|
|
3925
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3926
|
+
params={"na_rm": na_rm, "curvature": curvature, "angle": angle, "ncp": ncp, **kwargs},
|
|
3927
|
+
)
|
|
3928
|
+
|
|
3929
|
+
|
|
3930
|
+
def geom_spoke(
|
|
3931
|
+
mapping: Optional[Mapping] = None,
|
|
3932
|
+
data: Any = None,
|
|
3933
|
+
stat: str = "identity",
|
|
3934
|
+
position: str = "identity",
|
|
3935
|
+
na_rm: bool = False,
|
|
3936
|
+
show_legend: Any = None,
|
|
3937
|
+
inherit_aes: bool = True,
|
|
3938
|
+
**kwargs: Any,
|
|
3939
|
+
) -> Any:
|
|
3940
|
+
"""Create a spoke layer."""
|
|
3941
|
+
layer = _layer_import()
|
|
3942
|
+
return layer(
|
|
3943
|
+
geom=GeomSpoke, stat=stat, data=data, mapping=mapping,
|
|
3944
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3945
|
+
params={"na_rm": na_rm, **kwargs},
|
|
3946
|
+
)
|
|
3947
|
+
|
|
3948
|
+
|
|
3949
|
+
# ---------------------------------------------------------------------------
|
|
3950
|
+
# Density
|
|
3951
|
+
# ---------------------------------------------------------------------------
|
|
3952
|
+
|
|
3953
|
+
def geom_density(
|
|
3954
|
+
mapping: Optional[Mapping] = None,
|
|
3955
|
+
data: Any = None,
|
|
3956
|
+
stat: str = "density",
|
|
3957
|
+
position: str = "identity",
|
|
3958
|
+
na_rm: bool = False,
|
|
3959
|
+
show_legend: Any = None,
|
|
3960
|
+
inherit_aes: bool = True,
|
|
3961
|
+
outline_type: str = "upper",
|
|
3962
|
+
**kwargs: Any,
|
|
3963
|
+
) -> Any:
|
|
3964
|
+
"""Create a density layer."""
|
|
3965
|
+
layer = _layer_import()
|
|
3966
|
+
return layer(
|
|
3967
|
+
geom=GeomDensity, stat=stat, data=data, mapping=mapping,
|
|
3968
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3969
|
+
params={"na_rm": na_rm, "outline_type": outline_type, **kwargs},
|
|
3970
|
+
)
|
|
3971
|
+
|
|
3972
|
+
|
|
3973
|
+
def geom_density_2d(
|
|
3974
|
+
mapping: Optional[Mapping] = None,
|
|
3975
|
+
data: Any = None,
|
|
3976
|
+
stat: str = "density_2d",
|
|
3977
|
+
position: str = "identity",
|
|
3978
|
+
na_rm: bool = False,
|
|
3979
|
+
show_legend: Any = None,
|
|
3980
|
+
inherit_aes: bool = True,
|
|
3981
|
+
contour_var: str = "density",
|
|
3982
|
+
**kwargs: Any,
|
|
3983
|
+
) -> Any:
|
|
3984
|
+
"""Create a 2D density contour layer."""
|
|
3985
|
+
layer = _layer_import()
|
|
3986
|
+
return layer(
|
|
3987
|
+
geom=GeomDensity2d, stat=stat, data=data, mapping=mapping,
|
|
3988
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
3989
|
+
params={"na_rm": na_rm, "contour_var": contour_var, **kwargs},
|
|
3990
|
+
)
|
|
3991
|
+
|
|
3992
|
+
|
|
3993
|
+
# Aliases
|
|
3994
|
+
geom_density2d = geom_density_2d
|
|
3995
|
+
|
|
3996
|
+
|
|
3997
|
+
def geom_density_2d_filled(
|
|
3998
|
+
mapping: Optional[Mapping] = None,
|
|
3999
|
+
data: Any = None,
|
|
4000
|
+
stat: str = "density_2d_filled",
|
|
4001
|
+
position: str = "identity",
|
|
4002
|
+
na_rm: bool = False,
|
|
4003
|
+
show_legend: Any = None,
|
|
4004
|
+
inherit_aes: bool = True,
|
|
4005
|
+
contour_var: str = "density",
|
|
4006
|
+
**kwargs: Any,
|
|
4007
|
+
) -> Any:
|
|
4008
|
+
"""Create a filled 2D density contour layer."""
|
|
4009
|
+
layer = _layer_import()
|
|
4010
|
+
return layer(
|
|
4011
|
+
geom=GeomDensity2dFilled, stat=stat, data=data, mapping=mapping,
|
|
4012
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4013
|
+
params={"na_rm": na_rm, "contour_var": contour_var, **kwargs},
|
|
4014
|
+
)
|
|
4015
|
+
|
|
4016
|
+
|
|
4017
|
+
geom_density2d_filled = geom_density_2d_filled
|
|
4018
|
+
|
|
4019
|
+
|
|
4020
|
+
# ---------------------------------------------------------------------------
|
|
4021
|
+
# Contour
|
|
4022
|
+
# ---------------------------------------------------------------------------
|
|
4023
|
+
|
|
4024
|
+
def geom_contour(
|
|
4025
|
+
mapping: Optional[Mapping] = None,
|
|
4026
|
+
data: Any = None,
|
|
4027
|
+
stat: str = "contour",
|
|
4028
|
+
position: str = "identity",
|
|
4029
|
+
na_rm: bool = False,
|
|
4030
|
+
show_legend: Any = None,
|
|
4031
|
+
inherit_aes: bool = True,
|
|
4032
|
+
bins: Optional[int] = None,
|
|
4033
|
+
binwidth: Optional[float] = None,
|
|
4034
|
+
breaks: Any = None,
|
|
4035
|
+
**kwargs: Any,
|
|
4036
|
+
) -> Any:
|
|
4037
|
+
"""Create a contour line layer."""
|
|
4038
|
+
layer = _layer_import()
|
|
4039
|
+
return layer(
|
|
4040
|
+
geom=GeomContour, stat=stat, data=data, mapping=mapping,
|
|
4041
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4042
|
+
params={"na_rm": na_rm, "bins": bins, "binwidth": binwidth, "breaks": breaks, **kwargs},
|
|
4043
|
+
)
|
|
4044
|
+
|
|
4045
|
+
|
|
4046
|
+
def geom_contour_filled(
|
|
4047
|
+
mapping: Optional[Mapping] = None,
|
|
4048
|
+
data: Any = None,
|
|
4049
|
+
stat: str = "contour_filled",
|
|
4050
|
+
position: str = "identity",
|
|
4051
|
+
na_rm: bool = False,
|
|
4052
|
+
show_legend: Any = None,
|
|
4053
|
+
inherit_aes: bool = True,
|
|
4054
|
+
bins: Optional[int] = None,
|
|
4055
|
+
binwidth: Optional[float] = None,
|
|
4056
|
+
breaks: Any = None,
|
|
4057
|
+
**kwargs: Any,
|
|
4058
|
+
) -> Any:
|
|
4059
|
+
"""Create a filled contour layer."""
|
|
4060
|
+
layer = _layer_import()
|
|
4061
|
+
return layer(
|
|
4062
|
+
geom=GeomContourFilled, stat=stat, data=data, mapping=mapping,
|
|
4063
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4064
|
+
params={"na_rm": na_rm, "bins": bins, "binwidth": binwidth, "breaks": breaks, **kwargs},
|
|
4065
|
+
)
|
|
4066
|
+
|
|
4067
|
+
|
|
4068
|
+
# ---------------------------------------------------------------------------
|
|
4069
|
+
# Hex / Bin2d
|
|
4070
|
+
# ---------------------------------------------------------------------------
|
|
4071
|
+
|
|
4072
|
+
def geom_hex(
|
|
4073
|
+
mapping: Optional[Mapping] = None,
|
|
4074
|
+
data: Any = None,
|
|
4075
|
+
stat: str = "binhex",
|
|
4076
|
+
position: str = "identity",
|
|
4077
|
+
na_rm: bool = False,
|
|
4078
|
+
show_legend: Any = None,
|
|
4079
|
+
inherit_aes: bool = True,
|
|
4080
|
+
**kwargs: Any,
|
|
4081
|
+
) -> Any:
|
|
4082
|
+
"""Create a hex bin layer."""
|
|
4083
|
+
layer = _layer_import()
|
|
4084
|
+
return layer(
|
|
4085
|
+
geom=GeomHex, stat=stat, data=data, mapping=mapping,
|
|
4086
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4087
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4088
|
+
)
|
|
4089
|
+
|
|
4090
|
+
|
|
4091
|
+
def geom_bin_2d(
|
|
4092
|
+
mapping: Optional[Mapping] = None,
|
|
4093
|
+
data: Any = None,
|
|
4094
|
+
stat: str = "bin2d",
|
|
4095
|
+
position: str = "identity",
|
|
4096
|
+
na_rm: bool = False,
|
|
4097
|
+
show_legend: Any = None,
|
|
4098
|
+
inherit_aes: bool = True,
|
|
4099
|
+
**kwargs: Any,
|
|
4100
|
+
) -> Any:
|
|
4101
|
+
"""Create a 2D bin heatmap layer."""
|
|
4102
|
+
layer = _layer_import()
|
|
4103
|
+
return layer(
|
|
4104
|
+
geom=GeomBin2d, stat=stat, data=data, mapping=mapping,
|
|
4105
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4106
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4107
|
+
)
|
|
4108
|
+
|
|
4109
|
+
|
|
4110
|
+
geom_bin2d = geom_bin_2d
|
|
4111
|
+
|
|
4112
|
+
|
|
4113
|
+
# ---------------------------------------------------------------------------
|
|
4114
|
+
# Abline / Hline / Vline
|
|
4115
|
+
# ---------------------------------------------------------------------------
|
|
4116
|
+
|
|
4117
|
+
def geom_abline(
|
|
4118
|
+
mapping: Optional[Mapping] = None,
|
|
4119
|
+
data: Any = None,
|
|
4120
|
+
stat: str = "identity",
|
|
4121
|
+
slope: Any = None,
|
|
4122
|
+
intercept: Any = None,
|
|
4123
|
+
na_rm: bool = False,
|
|
4124
|
+
show_legend: Any = None,
|
|
4125
|
+
inherit_aes: bool = False,
|
|
4126
|
+
**kwargs: Any,
|
|
4127
|
+
) -> Any:
|
|
4128
|
+
"""Create an abline layer."""
|
|
4129
|
+
layer = _layer_import()
|
|
4130
|
+
if slope is not None or intercept is not None:
|
|
4131
|
+
if slope is None:
|
|
4132
|
+
slope = 1
|
|
4133
|
+
if intercept is None:
|
|
4134
|
+
intercept = 0
|
|
4135
|
+
data = pd.DataFrame({"intercept": [intercept] if not hasattr(intercept, "__len__") else intercept,
|
|
4136
|
+
"slope": [slope] if not hasattr(slope, "__len__") else slope})
|
|
4137
|
+
mapping = Mapping(intercept="intercept", slope="slope")
|
|
4138
|
+
show_legend = False
|
|
4139
|
+
elif mapping is None:
|
|
4140
|
+
slope = 1
|
|
4141
|
+
intercept = 0
|
|
4142
|
+
data = pd.DataFrame({"intercept": [intercept], "slope": [slope]})
|
|
4143
|
+
mapping = Mapping(intercept="intercept", slope="slope")
|
|
4144
|
+
|
|
4145
|
+
return layer(
|
|
4146
|
+
geom=GeomAbline, stat=stat, data=data, mapping=mapping,
|
|
4147
|
+
position="identity", show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4148
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4149
|
+
)
|
|
4150
|
+
|
|
4151
|
+
|
|
4152
|
+
def geom_hline(
|
|
4153
|
+
mapping: Optional[Mapping] = None,
|
|
4154
|
+
data: Any = None,
|
|
4155
|
+
stat: str = "identity",
|
|
4156
|
+
position: str = "identity",
|
|
4157
|
+
yintercept: Any = None,
|
|
4158
|
+
na_rm: bool = False,
|
|
4159
|
+
show_legend: Any = None,
|
|
4160
|
+
inherit_aes: bool = False,
|
|
4161
|
+
**kwargs: Any,
|
|
4162
|
+
) -> Any:
|
|
4163
|
+
"""Create a horizontal line layer."""
|
|
4164
|
+
layer = _layer_import()
|
|
4165
|
+
if yintercept is not None:
|
|
4166
|
+
data = pd.DataFrame({"yintercept": [yintercept] if not hasattr(yintercept, "__len__") else yintercept})
|
|
4167
|
+
mapping = Mapping(yintercept="yintercept")
|
|
4168
|
+
show_legend = False
|
|
4169
|
+
|
|
4170
|
+
return layer(
|
|
4171
|
+
geom=GeomHline, stat=stat, data=data, mapping=mapping,
|
|
4172
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4173
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4174
|
+
)
|
|
4175
|
+
|
|
4176
|
+
|
|
4177
|
+
def geom_vline(
|
|
4178
|
+
mapping: Optional[Mapping] = None,
|
|
4179
|
+
data: Any = None,
|
|
4180
|
+
stat: str = "identity",
|
|
4181
|
+
position: str = "identity",
|
|
4182
|
+
xintercept: Any = None,
|
|
4183
|
+
na_rm: bool = False,
|
|
4184
|
+
show_legend: Any = None,
|
|
4185
|
+
inherit_aes: bool = False,
|
|
4186
|
+
**kwargs: Any,
|
|
4187
|
+
) -> Any:
|
|
4188
|
+
"""Create a vertical line layer."""
|
|
4189
|
+
layer = _layer_import()
|
|
4190
|
+
if xintercept is not None:
|
|
4191
|
+
data = pd.DataFrame({"xintercept": [xintercept] if not hasattr(xintercept, "__len__") else xintercept})
|
|
4192
|
+
mapping = Mapping(xintercept="xintercept")
|
|
4193
|
+
show_legend = False
|
|
4194
|
+
|
|
4195
|
+
return layer(
|
|
4196
|
+
geom=GeomVline, stat=stat, data=data, mapping=mapping,
|
|
4197
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4198
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4199
|
+
)
|
|
4200
|
+
|
|
4201
|
+
|
|
4202
|
+
# ---------------------------------------------------------------------------
|
|
4203
|
+
# Rug
|
|
4204
|
+
# ---------------------------------------------------------------------------
|
|
4205
|
+
|
|
4206
|
+
def geom_rug(
|
|
4207
|
+
mapping: Optional[Mapping] = None,
|
|
4208
|
+
data: Any = None,
|
|
4209
|
+
stat: str = "identity",
|
|
4210
|
+
position: str = "identity",
|
|
4211
|
+
na_rm: bool = False,
|
|
4212
|
+
show_legend: Any = None,
|
|
4213
|
+
inherit_aes: bool = True,
|
|
4214
|
+
sides: str = "bl",
|
|
4215
|
+
outside: bool = False,
|
|
4216
|
+
length: Any = None,
|
|
4217
|
+
**kwargs: Any,
|
|
4218
|
+
) -> Any:
|
|
4219
|
+
"""Create a rug layer."""
|
|
4220
|
+
layer = _layer_import()
|
|
4221
|
+
return layer(
|
|
4222
|
+
geom=GeomRug, stat=stat, data=data, mapping=mapping,
|
|
4223
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4224
|
+
params={"na_rm": na_rm, "sides": sides, "outside": outside, "length": length, **kwargs},
|
|
4225
|
+
)
|
|
4226
|
+
|
|
4227
|
+
|
|
4228
|
+
# ---------------------------------------------------------------------------
|
|
4229
|
+
# Blank
|
|
4230
|
+
# ---------------------------------------------------------------------------
|
|
4231
|
+
|
|
4232
|
+
def geom_blank(
|
|
4233
|
+
mapping: Optional[Mapping] = None,
|
|
4234
|
+
data: Any = None,
|
|
4235
|
+
stat: str = "identity",
|
|
4236
|
+
position: str = "identity",
|
|
4237
|
+
show_legend: Any = None,
|
|
4238
|
+
inherit_aes: bool = True,
|
|
4239
|
+
**kwargs: Any,
|
|
4240
|
+
) -> Any:
|
|
4241
|
+
"""Create a blank layer (draws nothing)."""
|
|
4242
|
+
layer = _layer_import()
|
|
4243
|
+
return layer(
|
|
4244
|
+
geom=GeomBlank, stat=stat, data=data, mapping=mapping,
|
|
4245
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4246
|
+
params=kwargs,
|
|
4247
|
+
)
|
|
4248
|
+
|
|
4249
|
+
|
|
4250
|
+
# ---------------------------------------------------------------------------
|
|
4251
|
+
# Function
|
|
4252
|
+
# ---------------------------------------------------------------------------
|
|
4253
|
+
|
|
4254
|
+
def geom_function(
|
|
4255
|
+
mapping: Optional[Mapping] = None,
|
|
4256
|
+
data: Any = None,
|
|
4257
|
+
stat: str = "function",
|
|
4258
|
+
position: str = "identity",
|
|
4259
|
+
na_rm: bool = False,
|
|
4260
|
+
show_legend: Any = None,
|
|
4261
|
+
inherit_aes: bool = True,
|
|
4262
|
+
**kwargs: Any,
|
|
4263
|
+
) -> Any:
|
|
4264
|
+
"""Create a function layer."""
|
|
4265
|
+
layer = _layer_import()
|
|
4266
|
+
return layer(
|
|
4267
|
+
geom=GeomFunction, stat=stat, data=data, mapping=mapping,
|
|
4268
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4269
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4270
|
+
)
|
|
4271
|
+
|
|
4272
|
+
|
|
4273
|
+
# ---------------------------------------------------------------------------
|
|
4274
|
+
# Histogram / Freqpoly
|
|
4275
|
+
# ---------------------------------------------------------------------------
|
|
4276
|
+
|
|
4277
|
+
def geom_histogram(
|
|
4278
|
+
mapping: Optional[Mapping] = None,
|
|
4279
|
+
data: Any = None,
|
|
4280
|
+
stat: str = "bin",
|
|
4281
|
+
position: str = "stack",
|
|
4282
|
+
na_rm: bool = False,
|
|
4283
|
+
show_legend: Any = None,
|
|
4284
|
+
inherit_aes: bool = True,
|
|
4285
|
+
binwidth: Any = None,
|
|
4286
|
+
bins: Optional[int] = None,
|
|
4287
|
+
orientation: Any = None,
|
|
4288
|
+
**kwargs: Any,
|
|
4289
|
+
) -> Any:
|
|
4290
|
+
"""Create a histogram layer."""
|
|
4291
|
+
layer = _layer_import()
|
|
4292
|
+
return layer(
|
|
4293
|
+
geom=GeomBar, stat=stat, data=data, mapping=mapping,
|
|
4294
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4295
|
+
params={"na_rm": na_rm, "binwidth": binwidth, "bins": bins, "orientation": orientation, **kwargs},
|
|
4296
|
+
)
|
|
4297
|
+
|
|
4298
|
+
|
|
4299
|
+
def geom_freqpoly(
|
|
4300
|
+
mapping: Optional[Mapping] = None,
|
|
4301
|
+
data: Any = None,
|
|
4302
|
+
stat: str = "bin",
|
|
4303
|
+
position: str = "identity",
|
|
4304
|
+
na_rm: bool = False,
|
|
4305
|
+
show_legend: Any = None,
|
|
4306
|
+
inherit_aes: bool = True,
|
|
4307
|
+
**kwargs: Any,
|
|
4308
|
+
) -> Any:
|
|
4309
|
+
"""Create a frequency polygon layer."""
|
|
4310
|
+
layer = _layer_import()
|
|
4311
|
+
params: Dict[str, Any] = {"na_rm": na_rm, **kwargs}
|
|
4312
|
+
if stat == "bin":
|
|
4313
|
+
params["pad"] = True
|
|
4314
|
+
return layer(
|
|
4315
|
+
geom=GeomPath, stat=stat, data=data, mapping=mapping,
|
|
4316
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4317
|
+
params=params,
|
|
4318
|
+
)
|
|
4319
|
+
|
|
4320
|
+
|
|
4321
|
+
# ---------------------------------------------------------------------------
|
|
4322
|
+
# Count / Jitter
|
|
4323
|
+
# ---------------------------------------------------------------------------
|
|
4324
|
+
|
|
4325
|
+
def geom_count(
|
|
4326
|
+
mapping: Optional[Mapping] = None,
|
|
4327
|
+
data: Any = None,
|
|
4328
|
+
stat: str = "sum",
|
|
4329
|
+
position: str = "identity",
|
|
4330
|
+
na_rm: bool = False,
|
|
4331
|
+
show_legend: Any = None,
|
|
4332
|
+
inherit_aes: bool = True,
|
|
4333
|
+
**kwargs: Any,
|
|
4334
|
+
) -> Any:
|
|
4335
|
+
"""Create a count layer (points sized by n at each location)."""
|
|
4336
|
+
layer = _layer_import()
|
|
4337
|
+
return layer(
|
|
4338
|
+
geom=GeomPoint, stat=stat, data=data, mapping=mapping,
|
|
4339
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4340
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4341
|
+
)
|
|
4342
|
+
|
|
4343
|
+
|
|
4344
|
+
def geom_jitter(
|
|
4345
|
+
mapping: Optional[Mapping] = None,
|
|
4346
|
+
data: Any = None,
|
|
4347
|
+
stat: str = "identity",
|
|
4348
|
+
position: str = "jitter",
|
|
4349
|
+
na_rm: bool = False,
|
|
4350
|
+
show_legend: Any = None,
|
|
4351
|
+
inherit_aes: bool = True,
|
|
4352
|
+
width: Optional[float] = None,
|
|
4353
|
+
height: Optional[float] = None,
|
|
4354
|
+
**kwargs: Any,
|
|
4355
|
+
) -> Any:
|
|
4356
|
+
"""Create a jittered point layer."""
|
|
4357
|
+
layer = _layer_import()
|
|
4358
|
+
if width is not None or height is not None:
|
|
4359
|
+
position = {"name": "jitter", "width": width, "height": height}
|
|
4360
|
+
return layer(
|
|
4361
|
+
geom=GeomPoint, stat=stat, data=data, mapping=mapping,
|
|
4362
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4363
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4364
|
+
)
|
|
4365
|
+
|
|
4366
|
+
|
|
4367
|
+
# ---------------------------------------------------------------------------
|
|
4368
|
+
# Map
|
|
4369
|
+
# ---------------------------------------------------------------------------
|
|
4370
|
+
|
|
4371
|
+
def geom_map(
|
|
4372
|
+
mapping: Optional[Mapping] = None,
|
|
4373
|
+
data: Any = None,
|
|
4374
|
+
stat: str = "identity",
|
|
4375
|
+
map: Optional[pd.DataFrame] = None,
|
|
4376
|
+
na_rm: bool = False,
|
|
4377
|
+
show_legend: Any = None,
|
|
4378
|
+
inherit_aes: bool = True,
|
|
4379
|
+
**kwargs: Any,
|
|
4380
|
+
) -> Any:
|
|
4381
|
+
"""Create a map polygon layer."""
|
|
4382
|
+
layer = _layer_import()
|
|
4383
|
+
return layer(
|
|
4384
|
+
geom=GeomMap, stat=stat, data=data, mapping=mapping,
|
|
4385
|
+
position="identity", show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4386
|
+
params={"na_rm": na_rm, "map": map, **kwargs},
|
|
4387
|
+
)
|
|
4388
|
+
|
|
4389
|
+
|
|
4390
|
+
# ---------------------------------------------------------------------------
|
|
4391
|
+
# Quantile
|
|
4392
|
+
# ---------------------------------------------------------------------------
|
|
4393
|
+
|
|
4394
|
+
def geom_quantile(
|
|
4395
|
+
mapping: Optional[Mapping] = None,
|
|
4396
|
+
data: Any = None,
|
|
4397
|
+
stat: str = "quantile",
|
|
4398
|
+
position: str = "identity",
|
|
4399
|
+
na_rm: bool = False,
|
|
4400
|
+
show_legend: Any = None,
|
|
4401
|
+
inherit_aes: bool = True,
|
|
4402
|
+
**kwargs: Any,
|
|
4403
|
+
) -> Any:
|
|
4404
|
+
"""Create a quantile regression line layer."""
|
|
4405
|
+
layer = _layer_import()
|
|
4406
|
+
return layer(
|
|
4407
|
+
geom=GeomQuantile, stat=stat, data=data, mapping=mapping,
|
|
4408
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4409
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4410
|
+
)
|
|
4411
|
+
|
|
4412
|
+
|
|
4413
|
+
# ---------------------------------------------------------------------------
|
|
4414
|
+
# Sf
|
|
4415
|
+
# ---------------------------------------------------------------------------
|
|
4416
|
+
|
|
4417
|
+
def geom_sf(
|
|
4418
|
+
mapping: Optional[Mapping] = None,
|
|
4419
|
+
data: Any = None,
|
|
4420
|
+
stat: str = "sf",
|
|
4421
|
+
position: str = "identity",
|
|
4422
|
+
na_rm: bool = False,
|
|
4423
|
+
show_legend: Any = None,
|
|
4424
|
+
inherit_aes: bool = True,
|
|
4425
|
+
**kwargs: Any,
|
|
4426
|
+
) -> Any:
|
|
4427
|
+
"""Create a simple-features layer."""
|
|
4428
|
+
layer = _layer_import()
|
|
4429
|
+
return layer(
|
|
4430
|
+
geom=GeomSf, stat=stat, data=data, mapping=mapping or Mapping(),
|
|
4431
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4432
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4433
|
+
)
|
|
4434
|
+
|
|
4435
|
+
|
|
4436
|
+
def geom_sf_text(
|
|
4437
|
+
mapping: Optional[Mapping] = None,
|
|
4438
|
+
data: Any = None,
|
|
4439
|
+
stat: str = "sf_coordinates",
|
|
4440
|
+
position: str = "nudge",
|
|
4441
|
+
na_rm: bool = False,
|
|
4442
|
+
show_legend: Any = None,
|
|
4443
|
+
inherit_aes: bool = True,
|
|
4444
|
+
parse: bool = False,
|
|
4445
|
+
check_overlap: bool = False,
|
|
4446
|
+
**kwargs: Any,
|
|
4447
|
+
) -> Any:
|
|
4448
|
+
"""Create a text layer for sf geometries."""
|
|
4449
|
+
layer = _layer_import()
|
|
4450
|
+
return layer(
|
|
4451
|
+
geom=GeomText, stat=stat, data=data, mapping=mapping or Mapping(),
|
|
4452
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4453
|
+
params={"na_rm": na_rm, "parse": parse, "check_overlap": check_overlap, **kwargs},
|
|
4454
|
+
)
|
|
4455
|
+
|
|
4456
|
+
|
|
4457
|
+
def geom_sf_label(
|
|
4458
|
+
mapping: Optional[Mapping] = None,
|
|
4459
|
+
data: Any = None,
|
|
4460
|
+
stat: str = "sf_coordinates",
|
|
4461
|
+
position: str = "nudge",
|
|
4462
|
+
na_rm: bool = False,
|
|
4463
|
+
show_legend: Any = None,
|
|
4464
|
+
inherit_aes: bool = True,
|
|
4465
|
+
parse: bool = False,
|
|
4466
|
+
**kwargs: Any,
|
|
4467
|
+
) -> Any:
|
|
4468
|
+
"""Create a label layer for sf geometries."""
|
|
4469
|
+
layer = _layer_import()
|
|
4470
|
+
return layer(
|
|
4471
|
+
geom=GeomLabel, stat=stat, data=data, mapping=mapping or Mapping(),
|
|
4472
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4473
|
+
params={"na_rm": na_rm, "parse": parse, **kwargs},
|
|
4474
|
+
)
|
|
4475
|
+
|
|
4476
|
+
|
|
4477
|
+
# ---------------------------------------------------------------------------
|
|
4478
|
+
# QQ (geom only -- delegates to stat_qq / stat_qq_line)
|
|
4479
|
+
# ---------------------------------------------------------------------------
|
|
4480
|
+
|
|
4481
|
+
def geom_qq(
|
|
4482
|
+
mapping: Optional[Mapping] = None,
|
|
4483
|
+
data: Any = None,
|
|
4484
|
+
stat: str = "qq",
|
|
4485
|
+
position: str = "identity",
|
|
4486
|
+
na_rm: bool = False,
|
|
4487
|
+
show_legend: Any = None,
|
|
4488
|
+
inherit_aes: bool = True,
|
|
4489
|
+
**kwargs: Any,
|
|
4490
|
+
) -> Any:
|
|
4491
|
+
"""Create a QQ-plot point layer."""
|
|
4492
|
+
layer = _layer_import()
|
|
4493
|
+
return layer(
|
|
4494
|
+
geom=GeomPoint, stat=stat, data=data, mapping=mapping,
|
|
4495
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4496
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4497
|
+
)
|
|
4498
|
+
|
|
4499
|
+
|
|
4500
|
+
def geom_qq_line(
|
|
4501
|
+
mapping: Optional[Mapping] = None,
|
|
4502
|
+
data: Any = None,
|
|
4503
|
+
stat: str = "qq_line",
|
|
4504
|
+
position: str = "identity",
|
|
4505
|
+
na_rm: bool = False,
|
|
4506
|
+
show_legend: Any = None,
|
|
4507
|
+
inherit_aes: bool = True,
|
|
4508
|
+
**kwargs: Any,
|
|
4509
|
+
) -> Any:
|
|
4510
|
+
"""Create a QQ-line layer."""
|
|
4511
|
+
layer = _layer_import()
|
|
4512
|
+
return layer(
|
|
4513
|
+
geom=GeomPath, stat=stat, data=data, mapping=mapping,
|
|
4514
|
+
position=position, show_legend=show_legend, inherit_aes=inherit_aes,
|
|
4515
|
+
params={"na_rm": na_rm, **kwargs},
|
|
4516
|
+
)
|