Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Scale.label interface for formatting ticks #2877

Merged
merged 17 commits into from
Jun 26, 2022
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
5 changes: 0 additions & 5 deletions pytest.ini

This file was deleted.

40 changes: 20 additions & 20 deletions seaborn/_core/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from seaborn._stats.base import Stat
from seaborn._core.data import PlotData
from seaborn._core.moves import Move
from seaborn._core.scales import ScaleSpec, Scale
from seaborn._core.scales import Scale
from seaborn._core.subplots import Subplots
from seaborn._core.groupby import GroupBy
from seaborn._core.properties import PROPERTIES, Property, Coordinate
Expand Down Expand Up @@ -146,7 +146,7 @@ class Plot:

_data: PlotData
_layers: list[Layer]
_scales: dict[str, ScaleSpec]
_scales: dict[str, Scale]

_subplot_spec: dict[str, Any] # TODO values type
_facet_spec: FacetSpec
Expand Down Expand Up @@ -520,7 +520,7 @@ def facet(

# TODO def twin()?

def scale(self, **scales: ScaleSpec) -> Plot:
def scale(self, **scales: Scale) -> Plot:
"""
Control mappings from data units to visual properties.

Expand Down Expand Up @@ -873,7 +873,7 @@ def _transform_coords(self, p: Plot, common: PlotData, layers: list[Layer]) -> N
var_df = pd.DataFrame(columns=cols)

prop = Coordinate(axis)
scale_spec = self._get_scale(p, prefix, prop, var_df[var])
scale = self._get_scale(p, prefix, prop, var_df[var])

# Shared categorical axes are broken on matplotlib<3.4.0.
# https://github.com/matplotlib/matplotlib/pull/18308
Expand All @@ -882,7 +882,7 @@ def _transform_coords(self, p: Plot, common: PlotData, layers: list[Layer]) -> N
if Version(mpl.__version__) < Version("3.4.0"):
from seaborn._core.scales import Nominal
paired_axis = axis in p._pair_spec
cat_scale = isinstance(scale_spec, Nominal)
cat_scale = isinstance(scale, Nominal)
ok_dim = {"x": "col", "y": "row"}[axis]
shared_axes = share_state not in [False, "none", ok_dim]
if paired_axis and cat_scale and shared_axes:
Expand All @@ -897,7 +897,7 @@ def _transform_coords(self, p: Plot, common: PlotData, layers: list[Layer]) -> N
# Setup the scale on all of the data and plug it into self._scales
# We do this because by the time we do self._setup_scales, coordinate data
# will have been converted to floats already, so scale inference fails
self._scales[var] = scale_spec.setup(var_df[var], prop)
self._scales[var] = scale._setup(var_df[var], prop)

# Set up an empty series to receive the transformed values.
# We need this to handle piecemeal tranforms of categories -> floats.
Expand Down Expand Up @@ -927,7 +927,7 @@ def _transform_coords(self, p: Plot, common: PlotData, layers: list[Layer]) -> N

seed_values = var_df.loc[idx, var]

scale = scale_spec.setup(seed_values, prop, axis=axis_obj)
scale = scale._setup(seed_values, prop, axis=axis_obj)

for layer, new_series in zip(layers, transformed_data):
layer_df = layer["data"].frame
Expand All @@ -936,7 +936,7 @@ def _transform_coords(self, p: Plot, common: PlotData, layers: list[Layer]) -> N
new_series.loc[idx] = scale(layer_df.loc[idx, var])

# TODO need decision about whether to do this or modify axis transform
set_scale_obj(view["ax"], axis, scale.matplotlib_scale)
set_scale_obj(view["ax"], axis, scale._matplotlib_scale)

# Now the transformed data series are complete, set update the layer data
for layer, new_series in zip(layers, transformed_data):
Expand Down Expand Up @@ -1000,11 +1000,11 @@ def _compute_stats(self, spec: Plot, layers: list[Layer]) -> None:

def _get_scale(
self, spec: Plot, var: str, prop: Property, values: Series
) -> ScaleSpec:
) -> Scale:

if var in spec._scales:
arg = spec._scales[var]
if arg is None or isinstance(arg, ScaleSpec):
if arg is None or isinstance(arg, Scale):
scale = arg
else:
scale = prop.infer_scale(arg, values)
Expand Down Expand Up @@ -1052,28 +1052,28 @@ def _setup_scales(self, p: Plot, layers: list[Layer]) -> None:
axis = m["axis"]

prop = PROPERTIES.get(var if axis is None else axis, Property())
scale_spec = self._get_scale(p, var, prop, var_values)
scale = self._get_scale(p, var, prop, var_values)

# Initialize the data-dependent parameters of the scale
# Note that this returns a copy and does not mutate the original
# This dictionary is used by the semantic mappings
if scale_spec is None:
if scale is None:
# TODO what is the cleanest way to implement identity scale?
# We don't really need a ScaleSpec, and Identity() will be
# We don't really need a Scale, and Identity() will be
# overloaded anyway (but maybe a general Identity object
# that can be used as Scale/Mark/Stat/Move?)
# Note that this may not be the right spacer to use
# (but that is only relevant for coordinates, where identity scale
# doesn't make sense or is poorly defined, since we don't use pixels.)
self._scales[var] = Scale([], lambda x: x, None, "identity", None)
self._scales[var] = Scale._identity()
else:
scale = scale_spec.setup(var_values, prop)
scale = scale._setup(var_values, prop)
if isinstance(prop, Coordinate):
# If we have a coordinate here, we didn't assign a scale for it
# in _transform_coords, which means it was added during compute_stat
# This allows downstream orientation inference to work properly.
# But it feels a little hacky, so perhaps revisit.
scale.scale_type = "computed"
scale._priority = 0 # type: ignore
self._scales[var] = scale

def _plot_layer(self, p: Plot, layer: Layer) -> None:
Expand All @@ -1097,14 +1097,14 @@ def get_order(var):
# sorted unique numbers will correctly reconstruct intended order
# TODO This is tricky, make sure we add some tests for this
if var not in "xy" and var in scales:
return scales[var].order
return getattr(scales[var], "order", None)

if "width" in mark._mappable_props:
width = mark._resolve(df, "width", None)
else:
width = df.get("width", 0.8) # TODO what default
if orient in df:
df["width"] = width * scales[orient].spacing(df[orient])
df["width"] = width * scales[orient]._spacing(df[orient])

if "baseline" in mark._mappable_props:
# TODO what marks should have this?
Expand Down Expand Up @@ -1277,7 +1277,7 @@ def _setup_split_generator(
v for v in grouping_vars if v in df and v not in ["col", "row"]
]
for var in grouping_vars:
order = self._scales[var].order
order = getattr(self._scales[var], "order", None)
if order is None:
order = categorical_order(df[var])
grouping_keys.append(order)
Expand Down Expand Up @@ -1357,7 +1357,7 @@ def _update_legend_contents(
]] = []
schema = []
for var in legend_vars:
var_legend = scales[var].legend
var_legend = scales[var]._legend
if var_legend is not None:
values, labels = var_legend
for (_, part_id), part_vars, _ in schema:
Expand Down
24 changes: 12 additions & 12 deletions seaborn/_core/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from matplotlib.colors import to_rgb, to_rgba, to_rgba_array
from matplotlib.path import Path

from seaborn._core.scales import ScaleSpec, Nominal, Continuous, Temporal
from seaborn._core.scales import Scale, Nominal, Continuous, Temporal
from seaborn._core.rules import categorical_order, variable_type
from seaborn._compat import MarkerStyle
from seaborn.palettes import QUAL_PALETTES, color_palette, blend_palette
Expand Down Expand Up @@ -59,7 +59,7 @@ def __init__(self, variable: str | None = None):
variable = self.__class__.__name__.lower()
self.variable = variable

def default_scale(self, data: Series) -> ScaleSpec:
def default_scale(self, data: Series) -> Scale:
"""Given data, initialize appropriate scale class."""
# TODO allow variable_type to be "boolean" if that's a scale?
# TODO how will this handle data with units that can be treated as numeric
Expand All @@ -75,7 +75,7 @@ def default_scale(self, data: Series) -> ScaleSpec:
else:
return Nominal()

def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
def infer_scale(self, arg: Any, data: Series) -> Scale:
"""Given data and a scaling argument, initialize appropriate scale class."""
# TODO put these somewhere external for validation
# TODO putting this here won't pick it up if subclasses define infer_scale
Expand All @@ -86,7 +86,7 @@ def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
if isinstance(arg, str):
if any(arg.startswith(k) for k in trans_args):
# TODO validate numeric type? That should happen centrally somewhere
return Continuous(transform=arg)
return Continuous(trans=arg)
else:
msg = f"Unknown magic arg for {self.variable} scale: '{arg}'."
raise ValueError(msg)
Expand All @@ -96,7 +96,7 @@ def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
raise TypeError(msg)

def get_mapping(
self, scale: ScaleSpec, data: Series
self, scale: Scale, data: Series
) -> Callable[[ArrayLike], ArrayLike]:
"""Return a function that maps from data domain to property range."""
def identity(x):
Expand Down Expand Up @@ -176,7 +176,7 @@ def _inverse(self, values: ArrayLike) -> ArrayLike:
"""Transform applied to results of mapping that returns to native values."""
return values

def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
def infer_scale(self, arg: Any, data: Series) -> Scale:
"""Given data and a scaling argument, initialize appropriate scale class."""

# TODO infer continuous based on log/sqrt etc?
Expand All @@ -192,7 +192,7 @@ def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
return Continuous(arg)

def get_mapping(
self, scale: ScaleSpec, data: ArrayLike
self, scale: Scale, data: ArrayLike
) -> Callable[[ArrayLike], ArrayLike]:
"""Return a function that maps from data domain to property range."""
if isinstance(scale, Nominal):
Expand Down Expand Up @@ -325,7 +325,7 @@ def infer_scale(self, arg: Any, data: Series) -> Nominal:
return Nominal(arg)

def get_mapping(
self, scale: ScaleSpec, data: Series,
self, scale: Scale, data: Series,
) -> Callable[[ArrayLike], list]:
"""Define mapping as lookup into list of object values."""
order = getattr(scale, "order", None)
Expand Down Expand Up @@ -532,7 +532,7 @@ def has_alpha(x):
else:
return to_rgba_array(colors)[:, :3]

def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
def infer_scale(self, arg: Any, data: Series) -> Scale:
# TODO when inferring Continuous without data, verify type

# TODO need to rethink the variable type system
Expand Down Expand Up @@ -617,7 +617,7 @@ def mapping(x):
return mapping

def get_mapping(
self, scale: ScaleSpec, data: Series
self, scale: Scale, data: Series
) -> Callable[[ArrayLike], ArrayLike]:
"""Return a function that maps from data domain to color values."""
# TODO what is best way to do this conditional?
Expand Down Expand Up @@ -690,13 +690,13 @@ def default_scale(self, data: Series) -> Nominal:
"""Given data, initialize appropriate scale class."""
return Nominal()

def infer_scale(self, arg: Any, data: Series) -> ScaleSpec:
def infer_scale(self, arg: Any, data: Series) -> Scale:
"""Given data and a scaling argument, initialize appropriate scale class."""
# TODO infer Boolean where possible?
return Nominal(arg)

def get_mapping(
self, scale: ScaleSpec, data: Series
self, scale: Scale, data: Series
) -> Callable[[ArrayLike], ArrayLike]:
"""Return a function that maps each data value to True or False."""
# TODO categorical_order is going to return [False, True] for booleans,
Expand Down
Loading