Skip to content

Commit 4d7db09

Browse files
committed
BUG: Inherit DEFAULT_PARAMS down the geom and stat class hierarchy
Subclasses now declare only the parameters that differ from their parent, and the effective merged set is exposed as `default_params`. This fixes geoms rejecting parameters that their parent geom supports: geom_step now accepts lineend, linejoin and arrow, geom_quantile accepts arrow, and geom_histogram accepts just and width. geom_step also renders its corners with the documented round joinstyle, hence the updated baseline images.
1 parent e846b3a commit 4d7db09

18 files changed

Lines changed: 160 additions & 70 deletions

File tree

doc/changelog.qmd

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,21 @@ title: Changelog
6666
layer(geom=geom_point())
6767
```
6868

69-
- `geom.DEFAULT_PARAMS` now implicitly includes `stat="identity"`, `position="identity`
70-
and `na_rm=False`.
71-
72-
- `stat.DEFAULT_PARAMS` now implicitly includes `geom="blank"`, `position="identity`
73-
and `na_rm=False`.
69+
- `DEFAULT_PARAMS` declarations are now inherited down the class hierarchy;
70+
a subclass geom or stat only declares the parameters that differ from its
71+
parent. The base `geom` class declares `stat="identity"`,
72+
`position="identity"` and `na_rm=False`, and the base `stat` class declares
73+
`geom="blank"`, `position="identity"` and `na_rm=False`, so all geoms and
74+
stats include them automatically.
7475

7576
### Bug Fixes
7677

78+
- Subclass geoms now inherit their parent geom's default parameters, so
79+
parameters that a parent geom supports are no longer rejected.
80+
[](:class:`~plotnine.geom_step`) accepts `lineend`, `linejoin` and `arrow`,
81+
[](:class:`~plotnine.geom_quantile`) accepts `arrow`, and
82+
[](:class:`~plotnine.geom_histogram`) accepts `just` and `width`.
83+
7784
- Fixed [](:class:`~plotnine.geom_smooth`) / [](:class:`~plotnine.stat_smooth`) when using a linear model via "lm" with weights for the model to do a weighted regression. This bug did not affect the formula API of the linear model. ({{< issue 1005 >}})
7885

7986
- Fixed rendering of SVGs in jupyter notebooks.

plotnine/_utils/registry.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ..exceptions import PlotnineError
99

1010
if TYPE_CHECKING:
11-
from typing import TypeVar
11+
from typing import Any, TypeVar
1212

1313
T = TypeVar("T")
1414

@@ -108,3 +108,28 @@ def __init__(cls, name, bases, namespace):
108108
for base in bases:
109109
for base2 in base.mro()[:-2]:
110110
cls._hierarchy[base2.__name__].append(name)
111+
112+
113+
class _MergedDefaultParams:
114+
"""
115+
DEFAULT_PARAMS declarations merged down the class hierarchy
116+
117+
The merged result is cached per class and the cached dict is
118+
shared, so consumers must only read it or combine it into a new
119+
dict. Assumes single inheritance, which holds for all geoms &
120+
stats (the only mixin is ABC on the root classes and it
121+
contributes nothing).
122+
"""
123+
124+
def __get__(self, obj: Any, cls: type | None = None) -> dict[str, Any]:
125+
if cls is None:
126+
cls = type(obj)
127+
# cls.__dict__ (not getattr) so a parent's cache is never inherited
128+
if "_default_params" not in cls.__dict__:
129+
declared = cls.__dict__.get("DEFAULT_PARAMS", {})
130+
# The parent's view is fully merged: this getattr recurses
131+
# through this same descriptor and caches along the way.
132+
# Terminates at the root (ABC has no default_params).
133+
inherited = getattr(cls.__mro__[1], "default_params", {})
134+
cls._default_params = inherited | declared
135+
return cls.__dict__["_default_params"]

plotnine/doctools.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -429,15 +429,13 @@ def document_geom(geom: type[geom]) -> type[geom]:
429429
It replaces `{usage}`, `{common_parameters}` and
430430
`{aesthetics}` with generated documentation.
431431
"""
432-
from plotnine.geoms.geom import _BASE_PARAMS
433-
434432
docstring = dedent(geom.__doc__ or "")
435433
docstring = append_to_section(geom_kwargs, docstring, "Parameters")
436434

437435
# usage
438436
signature = make_signature(
439437
geom.__name__,
440-
_BASE_PARAMS | geom.DEFAULT_PARAMS,
438+
geom.default_params,
441439
common_geom_params,
442440
common_geom_param_values,
443441
)
@@ -457,7 +455,7 @@ def document_geom(geom: type[geom]) -> type[geom]:
457455
aesthetics_doc = indent(aesthetics_doc, " " * 4)
458456

459457
# common_parameters
460-
d = geom.DEFAULT_PARAMS
458+
d = geom.default_params
461459
common_parameters = GEOM_PARAMS_TPL.format(
462460
default_stat=default_class_name(d["stat"]),
463461
default_position=default_class_name(d["position"]),
@@ -481,8 +479,6 @@ def document_stat(stat: type[stat]) -> type[stat]:
481479
It replaces `{usage}`, `{common_parameters}` and
482480
`{aesthetics}` with generated documentation.
483481
"""
484-
from plotnine.stats.stat import _BASE_PARAMS
485-
486482
# Dedented so that it lineups (in sphinx) with the part
487483
# generated parts when put together
488484
docstring = dedent(stat.__doc__ or "")
@@ -491,7 +487,7 @@ def document_stat(stat: type[stat]) -> type[stat]:
491487
# usage:
492488
signature = make_signature(
493489
stat.__name__,
494-
_BASE_PARAMS | stat.DEFAULT_PARAMS,
490+
stat.default_params,
495491
common_stat_params,
496492
common_stat_param_values,
497493
)
@@ -507,7 +503,7 @@ def document_stat(stat: type[stat]) -> type[stat]:
507503
aesthetics_doc = indent(aesthetics_doc, " " * 4)
508504

509505
# common_parameters
510-
d = stat.DEFAULT_PARAMS
506+
d = stat.default_params
511507
common_parameters = STAT_PARAMS_TPL.format(
512508
default_geom=default_class_name(d["geom"]),
513509
default_position=default_class_name(d["position"]),

plotnine/geoms/geom.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
data_mapping_as_kwargs,
1313
remove_missing,
1414
)
15-
from .._utils.registry import Register
15+
from .._utils.registry import Register, _MergedDefaultParams
1616
from ..exceptions import PlotnineError
1717
from ..layer import layer
1818
from ..mapping.aes import rename_aesthetics
@@ -33,13 +33,6 @@
3333
from plotnine.typing import DataLike
3434

3535

36-
_BASE_PARAMS: dict[str, Any] = {
37-
"stat": "identity",
38-
"position": "identity",
39-
"na_rm": False,
40-
}
41-
42-
4336
class geom(ABC, metaclass=Register):
4437
"""Base class of all Geoms"""
4538

@@ -52,15 +45,27 @@ class geom(ABC, metaclass=Register):
5245
NON_MISSING_AES: set[str] = set()
5346
"""Required aesthetics for the geom"""
5447

55-
DEFAULT_PARAMS: dict[str, Any] = {}
56-
"""Required parameters for the geom"""
48+
DEFAULT_PARAMS: dict[str, Any] = {
49+
"stat": "identity",
50+
"position": "identity",
51+
"na_rm": False,
52+
}
53+
"""
54+
Default values for the parameters that the geom accepts
55+
56+
A geom inherits the parameters of its parent geom; declare only
57+
those that are new or have a different default value.
58+
"""
5759

5860
data: DataLike
5961
"""Geom/layer specific dataframe"""
6062

6163
mapping: aes
6264
"""Mappings i.e. `aes(x="col1", fill="col2")`{.py}"""
6365

66+
# All recognized parameters and their default values
67+
default_params = _MergedDefaultParams()
68+
6469
aes_params: dict[str, Any] = {} # setting of aesthetic
6570
params: dict[str, Any] # parameter settings
6671

@@ -87,14 +92,15 @@ def __init__(
8792
kwargs = rename_aesthetics(kwargs)
8893
kwargs = data_mapping_as_kwargs((data, mapping), kwargs)
8994
self._raw_kwargs = kwargs # Will be used to create stat & layer
95+
overridden_params = self.default_params.keys() & kwargs.keys()
9096

9197
# separate aesthetics and parameters
92-
possible_params = _BASE_PARAMS | self.DEFAULT_PARAMS
9398
self.aes_params = {
9499
ae: kwargs[ae] for ae in self.aesthetics() & set(kwargs)
95100
}
96-
self.params = possible_params | {
97-
k: v for k, v in kwargs.items() if k in possible_params
101+
self.params = {
102+
**self.default_params,
103+
**{k: kwargs[k] for k in overridden_params},
98104
}
99105
self.mapping = kwargs["mapping"]
100106
self.data = kwargs["data"]

plotnine/geoms/geom_bar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def setup_data(self, data: pd.DataFrame) -> pd.DataFrame:
5151
else:
5252
data["width"] = resolution(data["x"], False) * 0.9
5353

54-
just = self.params.get("just", 0.5)
54+
just = self.params["just"]
5555

5656
bool_idx = data["y"] < 0
5757

plotnine/geoms/geom_col.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ class geom_col(geom_bar):
3333

3434
REQUIRED_AES = {"x", "y"}
3535
NON_MISSING_AES = {"xmin", "xmax", "ymin", "ymax"}
36-
DEFAULT_PARAMS = {"position": "stack", "just": 0.5, "width": None}
36+
DEFAULT_PARAMS = {"stat": "identity"}

plotnine/geoms/geom_density.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ class geom_density(geom_area):
2525
"weight": 1,
2626
}
2727

28-
DEFAULT_PARAMS = {"stat": "density", "outline_type": "upper"}
28+
DEFAULT_PARAMS = {"stat": "density", "position": "identity"}

plotnine/geoms/geom_freqpoly.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,4 @@ class geom_freqpoly(geom_path):
1313
of the parameters.
1414
"""
1515

16-
DEFAULT_PARAMS = {
17-
"stat": "bin",
18-
"lineend": "butt",
19-
"linejoin": "round",
20-
"arrow": None,
21-
}
16+
DEFAULT_PARAMS = {"stat": "bin"}

plotnine/geoms/geom_histogram.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ class geom_histogram(geom_bar):
1818
plotnine.geom_bar : The default `stat` for this `geom`.
1919
"""
2020

21-
DEFAULT_PARAMS = {"stat": "bin", "position": "stack"}
21+
DEFAULT_PARAMS = {"stat": "bin"}

plotnine/geoms/geom_label.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ class geom_label(geom_text):
6161

6262
DEFAULT_AES = {**geom_text.DEFAULT_AES, "fill": "white"}
6363
DEFAULT_PARAMS = {
64-
**geom_text.DEFAULT_PARAMS,
6564
# boxstyle is one of
6665
# circle, larrow, rarrow, round, round4,
6766
# roundtooth, sawtooth, square

0 commit comments

Comments
 (0)