Skip to content

Commit 995910f

Browse files
committed
Add polygon shapes
1 parent 1b192d9 commit 995910f

11 files changed

Lines changed: 199 additions & 189 deletions

File tree

doc/changelog.qmd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ title: Changelog
8888

8989
- Gained new helper function [](:func:`~plotnine.helpers.get_aesthetic_limits`).
9090

91+
- [](:class:`~plotnine.geom_point`) gained the ability to handle shapes of the form
92+
`Sequence[tuple[float, float]]` e.g.
93+
94+
```python
95+
((-2, -4), (-2, -1), (0, 1), (2, -1), (2, -4), (-2, -4))
96+
```
97+
98+
Which declares the vertices of a polygon shape.
99+
100+
91101
### Enhancements
92102

93103
- Included datasets [](:attr:`~plotnine.data.mpg`), [](:attr:`~plotnine.data.msleep`) and

plotnine/_utils/__init__.py

Lines changed: 4 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
from contextlib import suppress
1313
from copy import deepcopy
1414
from dataclasses import field
15-
from typing import TYPE_CHECKING, cast, overload
15+
from typing import TYPE_CHECKING
1616
from warnings import warn
1717

18+
import mizani._colors.utils as color_utils
1819
import numpy as np
1920
import pandas as pd
2021
from pandas.core.groupby import DataFrameGroupBy
@@ -26,12 +27,10 @@
2627
from typing import Any, Callable, Literal, TypeVar
2728

2829
import numpy.typing as npt
29-
from matplotlib.typing import ColorType
3030
from typing_extensions import TypeGuard
3131

3232
from plotnine.typing import (
3333
AnyArrayLike,
34-
AnySeries,
3534
DataLike,
3635
FloatArray,
3736
FloatArrayLike,
@@ -60,6 +59,8 @@
6059
"centre": (0.5, 0.5),
6160
}
6261

62+
to_rgba = color_utils.to_rgba
63+
6364

6465
def is_scalar(val):
6566
"""
@@ -530,105 +531,6 @@ def remove_missing(
530531
return data
531532

532533

533-
@overload
534-
def to_rgba(colors: ColorType, alpha: float) -> ColorType: ...
535-
536-
537-
@overload
538-
def to_rgba(
539-
colors: Sequence[ColorType], alpha: float
540-
) -> Sequence[ColorType] | ColorType: ...
541-
542-
543-
@overload
544-
def to_rgba(
545-
colors: AnySeries, alpha: AnySeries
546-
) -> Sequence[ColorType] | ColorType: ...
547-
548-
549-
def to_rgba(
550-
colors: Sequence[ColorType] | AnySeries | ColorType,
551-
alpha: float | Sequence[float] | AnySeries,
552-
) -> Sequence[ColorType] | ColorType:
553-
"""
554-
Convert hex colors to rgba values.
555-
556-
Parameters
557-
----------
558-
colors : iterable | str
559-
colors to convert
560-
alphas : iterable | float
561-
alpha values
562-
563-
Returns
564-
-------
565-
out : ndarray | tuple
566-
rgba color(s)
567-
568-
Notes
569-
-----
570-
Matplotlib plotting functions only accept scalar
571-
alpha values. Hence no two objects with different
572-
alpha values may be plotted in one call. This would
573-
make plots with continuous alpha values innefficient.
574-
However :), the colors can be rgba hex values or
575-
list-likes and the alpha dimension will be respected.
576-
"""
577-
578-
def is_iterable(var):
579-
return np.iterable(var) and not isinstance(var, str)
580-
581-
def has_alpha(c):
582-
return (isinstance(c, tuple) and len(c) == 4) or (
583-
isinstance(c, str) and len(c) == 9 and c[0] == "#"
584-
)
585-
586-
def no_color(c):
587-
return c is None or c.lower() in ("none", "")
588-
589-
def to_rgba_hex(c: ColorType, a: float) -> str:
590-
"""
591-
Convert rgb color to rgba hex value
592-
593-
If color c has an alpha channel, then alpha value
594-
a is ignored
595-
"""
596-
from matplotlib.colors import colorConverter, to_hex
597-
598-
if c in ("None", "none"):
599-
return c
600-
601-
_has_alpha = has_alpha(c)
602-
c = to_hex(c, keep_alpha=_has_alpha)
603-
604-
if not _has_alpha:
605-
arr = colorConverter.to_rgba(c, a)
606-
return to_hex(arr, keep_alpha=True)
607-
608-
return c
609-
610-
if is_iterable(colors):
611-
colors = cast("Sequence[ColorType]", colors)
612-
613-
if all(no_color(c) for c in colors):
614-
return "none"
615-
616-
if isinstance(alpha, (Sequence, pd.Series)):
617-
return [to_rgba_hex(c, a) for c, a in zip(colors, alpha)]
618-
else:
619-
return [to_rgba_hex(c, alpha) for c in colors]
620-
else:
621-
colors = cast("ColorType", colors)
622-
623-
if no_color(colors):
624-
return colors
625-
626-
if isinstance(alpha, (Sequence, pd.Series)):
627-
return [to_rgba_hex(colors, a) for a in alpha]
628-
else:
629-
return to_rgba_hex(colors, alpha)
630-
631-
632534
def groupby_apply(
633535
df: pd.DataFrame,
634536
cols: str | list[str],

plotnine/geoms/annotate.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@ class annotate:
6464
def __init__(
6565
self,
6666
geom: str | type[geom_base_class],
67-
x: float | None = None,
68-
y: float | None = None,
69-
xmin: float | None = None,
70-
xmax: float | None = None,
71-
xend: float | None = None,
72-
xintercept: float | None = None,
73-
ymin: float | None = None,
74-
ymax: float | None = None,
75-
yend: float | None = None,
76-
yintercept: float | None = None,
67+
x: float | list[float] | None = None,
68+
y: float | list[float] | None = None,
69+
xmin: float | list[float] | None = None,
70+
xmax: float | list[float] | None = None,
71+
xend: float | list[float] | None = None,
72+
xintercept: float | list[float] | None = None,
73+
ymin: float | list[float] | None = None,
74+
ymax: float | list[float] | None = None,
75+
yend: float | list[float] | None = None,
76+
yintercept: float | list[float] | None = None,
7777
**kwargs: Any,
7878
):
7979
variables = locals()

plotnine/geoms/geom.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77

88
from .._utils import (
99
data_mapping_as_kwargs,
10-
is_list_like,
1110
remove_missing,
1211
)
1312
from .._utils.registry import Register, Registry
1413
from ..exceptions import PlotnineError
1514
from ..layer import layer
16-
from ..mapping.aes import is_valid_aesthetic, rename_aesthetics
15+
from ..mapping.aes import RepeatAesthetic, rename_aesthetics
1716
from ..mapping.evaluation import evaluate
1817
from ..positions.position import position
1918
from ..stats.stat import stat
@@ -260,23 +259,28 @@ def use_defaults(
260259
data[ae] = evaled[ae]
261260

262261
num_panels = len(data["PANEL"].unique()) if "PANEL" in data else 1
262+
across_panels = num_panels > 1 and not self.params["inherit_aes"]
263263

264264
# Aesthetics set as parameters to the geom/stat
265265
for ae, value in self.aes_params.items():
266266
try:
267267
data[ae] = value
268268
except ValueError as e:
269-
# sniff out the special cases, like custom
270-
# tupled linetypes, shapes and colors
271-
if is_valid_aesthetic(value, ae):
272-
data[ae] = [value] * len(data)
273-
elif num_panels > 1 and is_list_like(value):
274-
data[ae] = list(chain(*repeat(value, num_panels)))
269+
# NOTE: Handling of the edgecases in this exception is not
270+
# foolproof.
271+
repeat_ae = getattr(RepeatAesthetic, ae, None)
272+
if across_panels:
273+
# Adding an annotation/abline/hline/vhline with multiple
274+
# items across to more than 1 panel
275+
value = list(chain(*repeat(value, num_panels)))
276+
data[ae] = value
277+
elif repeat_ae:
278+
# Some aesthetics may have valid values that are not
279+
# scalar. e.g. sequences. For such case, we need to
280+
# insert a sequence of the same value.
281+
data[ae] = repeat_ae(value, len(data))
275282
else:
276-
msg = (
277-
f"'{value}' does not look like a "
278-
f"valid value for `{ae}`"
279-
)
283+
msg = f"'{ae}={value}' does not look like a valid value"
280284
raise PlotnineError(msg) from e
281285

282286
return data

plotnine/layer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def _share_layer_params(self):
257257
"""
258258
self.geom.params["zorder"] = self.zorder
259259
self.geom.params["raster"] = self.raster
260+
self.geom.params["inherit_aes"] = self.inherit_aes
260261

261262
def compute_aesthetics(self, plot: ggplot):
262263
"""
@@ -330,6 +331,7 @@ def setup_data(self):
330331

331332
self.geom.params.update(self.stat.params)
332333
self.geom.setup_params(data)
334+
self.geom.setup_aes_params(data)
333335
data = self.geom.setup_data(data)
334336

335337
check_required_aesthetics(

0 commit comments

Comments
 (0)