diff --git a/seaborn/_core/plot.py b/seaborn/_core/plot.py index 2abd64e79e..2c735a9287 100644 --- a/seaborn/_core/plot.py +++ b/seaborn/_core/plot.py @@ -1263,12 +1263,16 @@ def _setup_split_generator( order = categorical_order(df[var]) grouping_keys.append(order) - def split_generator() -> Generator: + def split_generator(dropna=True) -> Generator: for view in subplots: axes_df = self._filter_subplot_data(df, view) + if dropna: + with pd.option_context("mode.use_inf_as_null", True): + axes_df = axes_df.dropna() + subplot_keys = {} for dim in ["col", "row"]: if view[dim] is not None: diff --git a/seaborn/_core/properties.py b/seaborn/_core/properties.py index 68836cd045..95539dc6f4 100644 --- a/seaborn/_core/properties.py +++ b/seaborn/_core/properties.py @@ -312,6 +312,7 @@ class ObjectProperty(Property): normed = False # Object representing null data, should appear invisible when drawn by matplotlib + # Note that we now drop nulls in Plot._plot_layer and thus may not need this null_value: Any = None def _default_values(self, n: int) -> list: @@ -720,7 +721,11 @@ def get_mapping( raise TypeError(msg) def mapping(x): - return np.take(values, np.asarray(x, np.intp)) + ixs = np.asarray(x, np.intp) + return [ + values[ix] if np.isfinite(x_i) else False + for x_i, ix in zip(x, ixs) + ] return mapping diff --git a/seaborn/_marks/basic.py b/seaborn/_marks/basic.py index 2d5d8d38a5..a79e99bd96 100644 --- a/seaborn/_marks/basic.py +++ b/seaborn/_marks/basic.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +import numpy as np import matplotlib as mpl from seaborn._marks.base import ( @@ -35,13 +36,15 @@ class Line(Mark): def _plot(self, split_gen, scales, orient): - for keys, data, ax in split_gen(): + for keys, data, ax in split_gen(dropna=False): keys = resolve_properties(self, keys, scales) if self.sort: # TODO where to dropna? - data = data.dropna().sort_values(orient) + data = data.sort_values(orient) + else: + data.loc[data.isna().any(axis=1), ["x", "y"]] = np.nan line = mpl.lines.Line2D( data["x"].to_numpy(), diff --git a/seaborn/_marks/scatter.py b/seaborn/_marks/scatter.py index b5d4a0bcc8..85642ff33e 100644 --- a/seaborn/_marks/scatter.py +++ b/seaborn/_marks/scatter.py @@ -67,7 +67,7 @@ def _resolve_properties(self, data, scales): filled_marker = [m.is_filled() for m in resolved["marker"]] resolved["linewidth"] = resolved["stroke"] - resolved["fill"] = resolved["fill"] & filled_marker + resolved["fill"] = resolved["fill"] * filled_marker resolved["size"] = resolved["pointsize"] ** 2 resolved["edgecolor"] = resolve_color(self, data, "", scales) @@ -91,7 +91,6 @@ def _plot(self, split_gen, scales, orient): # (That should be solved upstream by defaulting to "" for unset x/y?) # (Be mindful of xmin/xmax, etc!) - # TODO pass scales *into* split_gen? for keys, data, ax in split_gen(): offsets = np.column_stack([data["x"], data["y"]]) diff --git a/seaborn/tests/_marks/test_scatter.py b/seaborn/tests/_marks/test_scatter.py index e5f0a6b8ca..9f751f95bb 100644 --- a/seaborn/tests/_marks/test_scatter.py +++ b/seaborn/tests/_marks/test_scatter.py @@ -1,5 +1,6 @@ from matplotlib.colors import to_rgba, to_rgba_array +import pytest from numpy.testing import assert_array_equal from seaborn._core.plot import Plot @@ -139,3 +140,15 @@ def test_filled_unfilled_mix(self): expected = [mark.edgewidth, mark.stroke] assert_array_equal(points.get_linewidths(), expected) + + @pytest.mark.parametrize("prop", ["color", "fill", "marker", "pointsize"]) + def test_missing_semantic_data(self, prop): + + x = [1, 2, 3] + y = [5, 3, 4] + z = ["a", float("nan"), "b"] + + p = Plot(x=x, y=y, **{prop: z}).add(Dot()).plot() + ax = p._figure.axes[0] + points, = ax.collections + self.check_offsets(points, [1, 3], [5, 4])