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

Cover full extent of data in Band/Range when not given min/max explicitly #3056

Merged
merged 3 commits into from
Oct 8, 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
30 changes: 27 additions & 3 deletions doc/_docstrings/objects.Band.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"source": [
"import seaborn.objects as so\n",
"from seaborn import load_dataset\n",
"fmri = load_dataset(\"fmri\")\n",
"fmri = load_dataset(\"fmri\").query(\"region == 'parietal'\")\n",
"seaice = (\n",
" load_dataset(\"seaice\")\n",
" .assign(\n",
Expand All @@ -22,7 +22,7 @@
" )\n",
" .query(\"Year >= 1980\")\n",
" .astype({\"Year\": str})\n",
" .pivot(\"Day\", \"Year\", \"Extent\")\n",
" .pivot(index=\"Day\", columns=\"Year\", values=\"Extent\")\n",
" .filter([\"1980\", \"2019\"])\n",
" .dropna()\n",
" .reset_index()\n",
Expand Down Expand Up @@ -90,8 +90,32 @@
},
{
"cell_type": "raw",
"id": "4e817cdd-09a3-4cf6-8602-e9665607bfe1",
"id": "9f0c82bf-3457-4ac5-ba48-8930bac03d75",
"metadata": {},
"source": [
"When min/max values are not explicitly assigned or added in a transform, the band will cover the full extent of the data:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "309f578e-da3d-4dc5-b6ac-a354321334c8",
"metadata": {},
"outputs": [],
"source": [
"(\n",
" so.Plot(fmri, x=\"timepoint\", y=\"signal\", color=\"event\")\n",
" .add(so.Line(linewidth=.5), group=\"subject\")\n",
" .add(so.Band())\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4330a3cd-63fe-470a-8e83-09e9606643b5",
"metadata": {},
"outputs": [],
"source": []
}
],
Expand Down
38 changes: 28 additions & 10 deletions doc/_docstrings/objects.Range.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
" so.Plot(penguins, x=\"sex\", y=\"body_mass_g\", linestyle=\"species\")\n",
" .facet(\"species\")\n",
" .add(so.Line(marker=\"o\"), so.Agg())\n",
" .add(so.Range(), so.Est(errorbar=\"pi\"))\n",
" .add(so.Range(), so.Est(errorbar=\"sd\"))\n",
")"
]
},
Expand All @@ -78,21 +78,39 @@
"source": [
"(\n",
" penguins\n",
" .rename_axis(\"penguin\")\n",
" .pipe(so.Plot, ymin=\"bill_depth_mm\", ymax=\"bill_length_mm\", x=\"penguin\")\n",
" .add(so.Range(), color=\"island\", linewidth=\"body_mass_g\")\n",
" .scale(x=so.Continuous().tick(count=0), linewidth=(.5, 1.5))\n",
" .facet(row=\"species\", col=\"sex\")\n",
" .layout(size=(8, 4))\n",
" .share(x=False)\n",
" .label(x=\"\", y=\"Size (mm)\")\n",
" .rename_axis(index=\"penguin\")\n",
" .pipe(so.Plot, x=\"penguin\", ymin=\"bill_depth_mm\", ymax=\"bill_length_mm\")\n",
" .add(so.Range(), color=\"island\")\n",
")"
]
},
{
"cell_type": "markdown",
"id": "2191bec6-a02e-48e0-b92c-69c38826049d",
"metadata": {},
"source": [
"When `min`/`max` variables are neither computed as part of a transform or explicitly assigned, the range will cover the full extent of the data at each unique observation on the orient axis:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "63c6352e-4ef5-4cff-940e-35fa5804b2c7",
"metadata": {},
"outputs": [],
"source": [
"(\n",
" so.Plot(penguins, x=\"sex\", y=\"body_mass_g\")\n",
" .facet(\"species\")\n",
" .add(so.Dots(pointsize=6))\n",
" .add(so.Range(linewidth=2))\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "08751ee7-d0a0-4e70-92b4-c1b38ea28890",
"id": "c215deb1-e510-4631-b999-737f5f41cae2",
"metadata": {},
"outputs": [],
"source": []
Expand Down
2 changes: 2 additions & 0 deletions doc/whatsnew/v0.12.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ v0.12.1 (Unreleased)

- |Feature| Added the :class:`objects.Text` mark (:pr:`3051`).

- |Feature| The :class:`Band` and :class:`Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).

- |Fix| Make :class:`objects.PolyFit` robust to missing data (:pr:`3010`).

- |Fix| Fixed a bug that caused an exception when more than two layers with the same mappings were added (:pr:`3055`).
Expand Down
5 changes: 5 additions & 0 deletions seaborn/_marks/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,9 @@ class Band(AreaBase, Mark):
def _standardize_coordinate_parameters(self, data, orient):
# dv = {"x": "y", "y": "x"}[orient]
# TODO assert that all(ymax >= ymin)?
# TODO what if only one exist?
other = {"x": "y", "y": "x"}[orient]
if not set(data.columns) & {f"{other}min", f"{other}max"}:
agg = {f"{other}min": (other, "min"), f"{other}max": (other, "max")}
data = data.groupby(orient).agg(**agg).reset_index()
return data
12 changes: 10 additions & 2 deletions seaborn/_marks/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ def _plot(self, split_gen, scales, orient):
# Handle datalim update manually
# https://github.com/matplotlib/matplotlib/issues/23129
ax.add_collection(lines, autolim=False)
xy = np.concatenate(ax_data["segments"])
ax.update_datalim(xy)
if ax_data["segments"]:
xy = np.concatenate(ax_data["segments"])
ax.update_datalim(xy)

def _legend_artist(self, variables, value, scales):

Expand Down Expand Up @@ -270,9 +271,16 @@ def _setup_lines(self, split_gen, scales, orient):
"linestyles": [],
}

# TODO better checks on what variables we have

vals = resolve_properties(self, keys, scales)
vals["color"] = resolve_color(self, keys, scales=scales)

# TODO what if only one exist?
if not set(data.columns) & {f"{other}min", f"{other}max"}:
agg = {f"{other}min": (other, "min"), f"{other}max": (other, "max")}
data = data.groupby(orient).agg(**agg).reset_index()

cols = [orient, f"{other}min", f"{other}max"]
data = data[cols].melt(orient, value_name=other)[["x", "y"]]
segments = [d.to_numpy() for _, d in data.groupby(orient)]
Expand Down
21 changes: 19 additions & 2 deletions tests/_marks/test_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from seaborn._marks.area import Area, Band


class TestAreaMarks:
class TestArea:

def test_single_defaults(self):

Expand Down Expand Up @@ -97,7 +97,10 @@ def test_unfilled(self):
poly = ax.patches[0]
assert poly.get_facecolor() == to_rgba(c, 0)

def test_band(self):

class TestBand:

def test_range(self):

x, ymin, ymax = [1, 2, 4], [2, 1, 4], [3, 3, 5]
p = Plot(x=x, ymin=ymin, ymax=ymax).add(Band()).plot()
Expand All @@ -109,3 +112,17 @@ def test_band(self):

expected_y = [2, 1, 4, 5, 3, 3, 2]
assert_array_equal(verts[1], expected_y)

def test_auto_range(self):

x = [1, 1, 2, 2, 2]
y = [1, 2, 3, 4, 5]
p = Plot(x=x, y=y).add(Band()).plot()
ax = p._figure.axes[0]
verts = ax.patches[0].get_path().vertices.T

expected_x = [1, 2, 2, 1, 1]
assert_array_equal(verts[0], expected_x)

expected_y = [1, 3, 5, 2, 1]
assert_array_equal(verts[1], expected_y)
20 changes: 20 additions & 0 deletions tests/_marks/test_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ def test_xy_data(self):
assert_array_equal(verts[0], [2, 5])
assert_array_equal(verts[1], [3, 4])

def test_single_orient_value(self):

x = [1, 1, 1]
y = [1, 2, 3]
p = Plot(x, y).add(Lines()).plot()
lines, = p._figure.axes[0].collections
paths, = lines.get_paths()
assert paths.vertices.shape == (0, 2)


class TestRange:

Expand All @@ -263,6 +272,17 @@ def test_xy_data(self):
assert_array_equal(verts[0], [x[i], x[i]])
assert_array_equal(verts[1], [ymin[i], ymax[i]])

def test_auto_range(self):

x = [1, 1, 2, 2, 2]
y = [1, 2, 3, 4, 5]

p = Plot(x=x, y=y).add(Range()).plot()
lines, = p._figure.axes[0].collections
paths = lines.get_paths()
assert_array_equal(paths[0].vertices, [(1, 1), (1, 2)])
assert_array_equal(paths[1].vertices, [(2, 3), (2, 5)])

def test_mapped_color(self):

x = [1, 2, 1, 2]
Expand Down