Skip to content

Commit

Permalink
Cover full extent of data in Band/Range when not given min/max explic…
Browse files Browse the repository at this point in the history
…itly (#3056)

* Cover full extent of data in Range when not provided with min/max variables

* Cover full extent of data in Band when not provided with min/max variables

* Add example for Band and update release notes
  • Loading branch information
mwaskom committed Oct 8, 2022
1 parent f033f5d commit 6d6f8d3
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 17 deletions.
30 changes: 27 additions & 3 deletions doc/_docstrings/objects.Band.ipynb
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
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
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
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
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
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
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

0 comments on commit 6d6f8d3

Please sign in to comment.