Skip to content

Commit

Permalink
Add (untested/incomplete) prototype of marker mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Waskom committed Sep 12, 2021
1 parent 4ef6d61 commit 0bf214d
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 4 deletions.
51 changes: 48 additions & 3 deletions seaborn/_core/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@ def setup(self, data: Series, scale: Scale | None) -> SemanticMapping:

def __call__(self, x): # TODO types; will need to overload (wheee)
# TODO this is a hack to get things working
# We are missing numeric maps and lots of other things
if isinstance(x, pd.Series):
if x.dtype.name == "category": # TODO! possible pandas bug
x = x.astype(object)
# TODO where is best place to ensure that LUT values are rgba tuples?
return np.stack(x.map(self.lookup_table).map(to_rgb))
return np.stack(x.map(self.lookup_table))
else:
return to_rgb(self.lookup_table[x])
return self.lookup_table[x]


# TODO Currently, the SemanticMapping objects are also the source of the information
Expand Down Expand Up @@ -74,6 +73,17 @@ def __init__(self, palette: PaletteSpec = None):

self._input_palette = palette

def __call__(self, x): # TODO types; will need to overload (wheee)
# TODO this is a hack to get things working
# We are missing numeric maps and lots of other things
if isinstance(x, pd.Series):
if x.dtype.name == "category": # TODO! possible pandas bug
x = x.astype(object)
# TODO where is best place to ensure that LUT values are rgba tuples?
return np.stack(x.map(self.lookup_table).map(to_rgb))
else:
return to_rgb(self.lookup_table[x])

def setup(
self,
data: Series, # TODO generally rename Series arguments to distinguish from DF?
Expand Down Expand Up @@ -249,3 +259,38 @@ def _setup_numeric(
lookup_table = dict(zip(levels, cmap(norm(levels))))

return levels, lookup_table, norm, cmap


class MarkerMapping(SemanticMapping):

def __init__(self, shapes: list | dict | None = None): # TODO full types

# TODO fill or filled parameter?
# allow singletons? e.g. map_marker(shapes="o", filled=[True, False])?
# allow full matplotlib fillstyle API?

self._input_shapes = shapes

def setup(
self,
data: Series, # TODO generally rename Series arguments to distinguish from DF?
scale: Scale | None = None, # TODO or always have a Scale?
) -> MarkerMapping:

shapes = self._input_shapes
order = None if scale is None else scale.order
levels = categorical_order(data, order)

# TODO input checking; generalize across mappings

# TODO default shapes

if isinstance(shapes, dict):
shapes = {k: mpl.markers.MarkerStyle(s) for k, s in shapes.items()}
elif isinstance(shapes, list):
shapes = {k: mpl.markers.MarkerStyle(s) for k, s in zip(levels, shapes)}

self.levels = levels
self.lookup_table = shapes

return self
11 changes: 10 additions & 1 deletion seaborn/_core/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from seaborn._core.rules import categorical_order, variable_type
from seaborn._core.data import PlotData
from seaborn._core.subplots import Subplots
from seaborn._core.mappings import GroupMapping, ColorMapping
from seaborn._core.mappings import GroupMapping, ColorMapping, MarkerMapping
from seaborn._core.scales import (
ScaleWrapper,
CategoricalScale,
Expand Down Expand Up @@ -67,6 +67,7 @@ def __init__(
"color": ColorMapping(),
"facecolor": ColorMapping(),
"edgecolor": ColorMapping(),
"marker": MarkerMapping(),
}

# TODO is using "unknown" here the best approach?
Expand Down Expand Up @@ -283,6 +284,14 @@ def map_edgecolor(
self._mappings["edgecolor"] = ColorMapping(palette)
return self

def map_marker(
self,
shapes: list | dict | None = None,
) -> Plot:

self._mappings["marker"] = MarkerMapping(shapes)
return self

# TODO have map_gradient?
# This could be used to add another color-like dimension
# and also the basis for what mappings like stat.density -> rgba do
Expand Down
5 changes: 5 additions & 0 deletions seaborn/_marks/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ def _plot_split(self, keys, data, ax, mappings, kws):
func = getattr(points, f"set_{var}")
func(mappings[var](data[var]))

if "marker" in data:
markers = mappings["marker"](data["marker"])
paths = [m.get_path().transformed(m.get_transform()) for m in markers]
points.set_paths(paths)

# TODO note that when scaling this up we'll need to catch the artist
# and update its attributes (like in scatterplot) to allow marker variation

Expand Down
1 change: 1 addition & 0 deletions seaborn/relational.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ def plot(self, ax, kws):
# See https://github.com/matplotlib/matplotlib/issues/17849 for context
m = kws.get("marker", mpl.rcParams.get("marker", "o"))
if not isinstance(m, mpl.markers.MarkerStyle):
# TODO in more recent matplotlib (which?) can pass a MarkerStyle here
m = mpl.markers.MarkerStyle(m)
if m.is_filled():
kws.setdefault("edgecolor", "w")
Expand Down

0 comments on commit 0bf214d

Please sign in to comment.