Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions doc/changelog.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,21 @@ title: Changelog
layer(geom=geom_point())
```

- `geom.DEFAULT_PARAMS` now implicitly includes `stat="identity"`, `position="identity`
and `na_rm=False`.

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

### Bug Fixes

- Subclass geoms now inherit their parent geom's default parameters, so
parameters that a parent geom supports are no longer rejected.
[](:class:`~plotnine.geom_step`) accepts `lineend`, `linejoin` and `arrow`,
[](:class:`~plotnine.geom_quantile`) accepts `arrow`, and
[](:class:`~plotnine.geom_histogram`) accepts `just` and `width`.

- 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 >}})

- Fixed rendering of SVGs in jupyter notebooks.
Expand Down
27 changes: 26 additions & 1 deletion plotnine/_utils/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ..exceptions import PlotnineError

if TYPE_CHECKING:
from typing import TypeVar
from typing import Any, TypeVar

T = TypeVar("T")

Expand Down Expand Up @@ -108,3 +108,28 @@ def __init__(cls, name, bases, namespace):
for base in bases:
for base2 in base.mro()[:-2]:
cls._hierarchy[base2.__name__].append(name)


class _MergedDefaultParams:
"""
DEFAULT_PARAMS declarations merged down the class hierarchy

The merged result is cached per class and the cached dict is
shared, so consumers must only read it or combine it into a new
dict. Assumes single inheritance, which holds for all geoms &
stats (the only mixin is ABC on the root classes and it
contributes nothing).
"""

def __get__(self, obj: Any, cls: type | None = None) -> dict[str, Any]:
if cls is None:
cls = type(obj)
# cls.__dict__ (not getattr) so a parent's cache is never inherited
if "_default_params" not in cls.__dict__:
declared = cls.__dict__.get("DEFAULT_PARAMS", {})
# The parent's view is fully merged: this getattr recurses
# through this same descriptor and caches along the way.
# Terminates at the root (ABC has no default_params).
inherited = getattr(cls.__mro__[1], "default_params", {})
cls._default_params = inherited | declared
return cls.__dict__["_default_params"]
12 changes: 4 additions & 8 deletions plotnine/doctools.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,15 +429,13 @@ def document_geom(geom: type[geom]) -> type[geom]:
It replaces `{usage}`, `{common_parameters}` and
`{aesthetics}` with generated documentation.
"""
from plotnine.geoms.geom import _BASE_PARAMS

docstring = dedent(geom.__doc__ or "")
docstring = append_to_section(geom_kwargs, docstring, "Parameters")

# usage
signature = make_signature(
geom.__name__,
_BASE_PARAMS | geom.DEFAULT_PARAMS,
geom.default_params,
common_geom_params,
common_geom_param_values,
)
Expand All @@ -457,7 +455,7 @@ def document_geom(geom: type[geom]) -> type[geom]:
aesthetics_doc = indent(aesthetics_doc, " " * 4)

# common_parameters
d = geom.DEFAULT_PARAMS
d = geom.default_params
common_parameters = GEOM_PARAMS_TPL.format(
default_stat=default_class_name(d["stat"]),
default_position=default_class_name(d["position"]),
Expand All @@ -481,8 +479,6 @@ def document_stat(stat: type[stat]) -> type[stat]:
It replaces `{usage}`, `{common_parameters}` and
`{aesthetics}` with generated documentation.
"""
from plotnine.stats.stat import _BASE_PARAMS

# Dedented so that it lineups (in sphinx) with the part
# generated parts when put together
docstring = dedent(stat.__doc__ or "")
Expand All @@ -491,7 +487,7 @@ def document_stat(stat: type[stat]) -> type[stat]:
# usage:
signature = make_signature(
stat.__name__,
_BASE_PARAMS | stat.DEFAULT_PARAMS,
stat.default_params,
common_stat_params,
common_stat_param_values,
)
Expand All @@ -507,7 +503,7 @@ def document_stat(stat: type[stat]) -> type[stat]:
aesthetics_doc = indent(aesthetics_doc, " " * 4)

# common_parameters
d = stat.DEFAULT_PARAMS
d = stat.default_params
common_parameters = STAT_PARAMS_TPL.format(
default_geom=default_class_name(d["geom"]),
default_position=default_class_name(d["position"]),
Expand Down
32 changes: 19 additions & 13 deletions plotnine/geoms/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
data_mapping_as_kwargs,
remove_missing,
)
from .._utils.registry import Register
from .._utils.registry import Register, _MergedDefaultParams
from ..exceptions import PlotnineError
from ..layer import layer
from ..mapping.aes import rename_aesthetics
Expand All @@ -33,13 +33,6 @@
from plotnine.typing import DataLike


_BASE_PARAMS: dict[str, Any] = {
"stat": "identity",
"position": "identity",
"na_rm": False,
}


class geom(ABC, metaclass=Register):
"""Base class of all Geoms"""

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

DEFAULT_PARAMS: dict[str, Any] = {}
"""Required parameters for the geom"""
DEFAULT_PARAMS: dict[str, Any] = {
"stat": "identity",
"position": "identity",
"na_rm": False,
}
"""
Default values for the parameters that the geom accepts

A geom inherits the parameters of its parent geom; declare only
those that are new or have a different default value.
"""

data: DataLike
"""Geom/layer specific dataframe"""

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

# All recognized parameters and their default values
default_params = _MergedDefaultParams()

aes_params: dict[str, Any] = {} # setting of aesthetic
params: dict[str, Any] # parameter settings

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

# separate aesthetics and parameters
possible_params = _BASE_PARAMS | self.DEFAULT_PARAMS
self.aes_params = {
ae: kwargs[ae] for ae in self.aesthetics() & set(kwargs)
}
self.params = possible_params | {
k: v for k, v in kwargs.items() if k in possible_params
self.params = {
**self.default_params,
**{k: kwargs[k] for k in overridden_params},
}
self.mapping = kwargs["mapping"]
self.data = kwargs["data"]
Expand Down
2 changes: 1 addition & 1 deletion plotnine/geoms/geom_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def setup_data(self, data: pd.DataFrame) -> pd.DataFrame:
else:
data["width"] = resolution(data["x"], False) * 0.9

just = self.params.get("just", 0.5)
just = self.params["just"]

bool_idx = data["y"] < 0

Expand Down
2 changes: 1 addition & 1 deletion plotnine/geoms/geom_col.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ class geom_col(geom_bar):

REQUIRED_AES = {"x", "y"}
NON_MISSING_AES = {"xmin", "xmax", "ymin", "ymax"}
DEFAULT_PARAMS = {"position": "stack", "just": 0.5, "width": None}
DEFAULT_PARAMS = {"stat": "identity"}
2 changes: 1 addition & 1 deletion plotnine/geoms/geom_density.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ class geom_density(geom_area):
"weight": 1,
}

DEFAULT_PARAMS = {"stat": "density", "outline_type": "upper"}
DEFAULT_PARAMS = {"stat": "density", "position": "identity"}
7 changes: 1 addition & 6 deletions plotnine/geoms/geom_freqpoly.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,4 @@ class geom_freqpoly(geom_path):
of the parameters.
"""

DEFAULT_PARAMS = {
"stat": "bin",
"lineend": "butt",
"linejoin": "round",
"arrow": None,
}
DEFAULT_PARAMS = {"stat": "bin"}
2 changes: 1 addition & 1 deletion plotnine/geoms/geom_histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ class geom_histogram(geom_bar):
plotnine.geom_bar : The default `stat` for this `geom`.
"""

DEFAULT_PARAMS = {"stat": "bin", "position": "stack"}
DEFAULT_PARAMS = {"stat": "bin"}
1 change: 0 additions & 1 deletion plotnine/geoms/geom_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ class geom_label(geom_text):

DEFAULT_AES = {**geom_text.DEFAULT_AES, "fill": "white"}
DEFAULT_PARAMS = {
**geom_text.DEFAULT_PARAMS,
# boxstyle is one of
# circle, larrow, rarrow, round, round4,
# roundtooth, sawtooth, square
Expand Down
6 changes: 1 addition & 5 deletions plotnine/geoms/geom_quantile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,4 @@ class geom_quantile(geom_path):
"linetype": "solid",
"size": 0.5,
}
DEFAULT_PARAMS = {
"stat": "quantile",
"lineend": "butt",
"linejoin": "round",
}
DEFAULT_PARAMS = {"stat": "quantile"}
20 changes: 10 additions & 10 deletions plotnine/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def __init__(
if stat is not None:
stat_ref = _lookup_stat(stat)
if isinstance(stat_ref, type):
geom = stat_ref.DEFAULT_PARAMS["geom"]
geom = stat_ref.default_params["geom"]
else:
geom = stat_ref.params["geom"]
# Forward stat instance's kwargs to the geom
Expand All @@ -117,19 +117,19 @@ def __init__(
self._verify_arguments(_geom, _stat)

# Layer params: prefer explicit kwargs, fall back to
# geom._raw_kwargs, then geom.DEFAULT_PARAMS
# geom._raw_kwargs, then geom.default_params
raw = _geom._raw_kwargs
self.inherit_aes = raw.get(
"inherit_aes",
_geom.DEFAULT_PARAMS.get("inherit_aes", inherit_aes),
_geom.default_params.get("inherit_aes", inherit_aes),
)
self.show_legend = raw.get(
"show_legend",
_geom.DEFAULT_PARAMS.get("show_legend", show_legend),
_geom.default_params.get("show_legend", show_legend),
)
self.raster = raw.get(
"raster",
_geom.DEFAULT_PARAMS.get("raster", raster),
_geom.default_params.get("raster", raster),
)

self.geom = _geom
Expand All @@ -148,9 +148,9 @@ def _verify_arguments(geom: geom, stat: stat) -> None:
unknown = (
geom_stat_args
- geom.aesthetics()
- geom.DEFAULT_PARAMS.keys()
- geom.default_params.keys()
- stat.aesthetics()
- stat.DEFAULT_PARAMS.keys()
- stat.default_params.keys()
- {
"data",
"mapping",
Expand Down Expand Up @@ -647,7 +647,7 @@ def _resolve_geom(
for param in set(geom_spec.aesthetics()) & set(kwargs):
geom_spec.aes_params[param] = kwargs[param]

for param in set(geom_spec.DEFAULT_PARAMS) & set(kwargs):
for param in set(geom_spec.default_params) & set(kwargs):
geom_spec.params[param] = kwargs[param]
return geom_spec

Expand Down Expand Up @@ -728,15 +728,15 @@ def _resolve_stat(
for param in set(result.aesthetics()) & set(kwargs):
result.aes_params[param] = kwargs[param]

for param in set(result.DEFAULT_PARAMS) & set(kwargs):
for param in set(result.default_params) & set(kwargs):
result.params[param] = kwargs[param]
return result

# It's a class — instantiate with filtered geom kwargs
klass = result
kwargs = geom_obj._raw_kwargs
valid_kwargs = (
klass.aesthetics() | klass.DEFAULT_PARAMS.keys()
klass.aesthetics() | klass.default_params.keys()
) & kwargs.keys()
params = {k: kwargs[k] for k in valid_kwargs}
return klass(**params)
Expand Down
6 changes: 3 additions & 3 deletions plotnine/qplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,14 @@ def get_facet_type(facets: str) -> Literal["grid", "wrap", "null"]:
for g in geom:
geom_name = f"geom_{g}"
geom_klass = Registry[geom_name]
s = geom_klass.DEFAULT_PARAMS.get("stat", "identity")
s = geom_klass.default_params["stat"]
stat_name = f"stat_{s}"
stat_klass = Registry[stat_name]
# find params
recognized = kwargs.keys() & (
geom_klass.DEFAULT_PARAMS.keys()
geom_klass.default_params.keys()
| geom_klass.aesthetics()
| stat_klass.DEFAULT_PARAMS.keys()
| stat_klass.default_params.keys()
| stat_klass.aesthetics()
)
recognized = recognized - aesthetics.keys()
Expand Down
Loading
Loading