Skip to content

Commit 6d6f8d3

Browse files
authored
Cover full extent of data in Band/Range when not given min/max explicitly (#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
1 parent f033f5d commit 6d6f8d3

File tree

7 files changed

+111
-17
lines changed

7 files changed

+111
-17
lines changed

doc/_docstrings/objects.Band.ipynb

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"source": [
1414
"import seaborn.objects as so\n",
1515
"from seaborn import load_dataset\n",
16-
"fmri = load_dataset(\"fmri\")\n",
16+
"fmri = load_dataset(\"fmri\").query(\"region == 'parietal'\")\n",
1717
"seaice = (\n",
1818
" load_dataset(\"seaice\")\n",
1919
" .assign(\n",
@@ -22,7 +22,7 @@
2222
" )\n",
2323
" .query(\"Year >= 1980\")\n",
2424
" .astype({\"Year\": str})\n",
25-
" .pivot(\"Day\", \"Year\", \"Extent\")\n",
25+
" .pivot(index=\"Day\", columns=\"Year\", values=\"Extent\")\n",
2626
" .filter([\"1980\", \"2019\"])\n",
2727
" .dropna()\n",
2828
" .reset_index()\n",
@@ -90,8 +90,32 @@
9090
},
9191
{
9292
"cell_type": "raw",
93-
"id": "4e817cdd-09a3-4cf6-8602-e9665607bfe1",
93+
"id": "9f0c82bf-3457-4ac5-ba48-8930bac03d75",
9494
"metadata": {},
95+
"source": [
96+
"When min/max values are not explicitly assigned or added in a transform, the band will cover the full extent of the data:"
97+
]
98+
},
99+
{
100+
"cell_type": "code",
101+
"execution_count": null,
102+
"id": "309f578e-da3d-4dc5-b6ac-a354321334c8",
103+
"metadata": {},
104+
"outputs": [],
105+
"source": [
106+
"(\n",
107+
" so.Plot(fmri, x=\"timepoint\", y=\"signal\", color=\"event\")\n",
108+
" .add(so.Line(linewidth=.5), group=\"subject\")\n",
109+
" .add(so.Band())\n",
110+
")"
111+
]
112+
},
113+
{
114+
"cell_type": "code",
115+
"execution_count": null,
116+
"id": "4330a3cd-63fe-470a-8e83-09e9606643b5",
117+
"metadata": {},
118+
"outputs": [],
95119
"source": []
96120
}
97121
],

doc/_docstrings/objects.Range.ipynb

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
" so.Plot(penguins, x=\"sex\", y=\"body_mass_g\", linestyle=\"species\")\n",
5858
" .facet(\"species\")\n",
5959
" .add(so.Line(marker=\"o\"), so.Agg())\n",
60-
" .add(so.Range(), so.Est(errorbar=\"pi\"))\n",
60+
" .add(so.Range(), so.Est(errorbar=\"sd\"))\n",
6161
")"
6262
]
6363
},
@@ -78,21 +78,39 @@
7878
"source": [
7979
"(\n",
8080
" penguins\n",
81-
" .rename_axis(\"penguin\")\n",
82-
" .pipe(so.Plot, ymin=\"bill_depth_mm\", ymax=\"bill_length_mm\", x=\"penguin\")\n",
83-
" .add(so.Range(), color=\"island\", linewidth=\"body_mass_g\")\n",
84-
" .scale(x=so.Continuous().tick(count=0), linewidth=(.5, 1.5))\n",
85-
" .facet(row=\"species\", col=\"sex\")\n",
86-
" .layout(size=(8, 4))\n",
87-
" .share(x=False)\n",
88-
" .label(x=\"\", y=\"Size (mm)\")\n",
81+
" .rename_axis(index=\"penguin\")\n",
82+
" .pipe(so.Plot, x=\"penguin\", ymin=\"bill_depth_mm\", ymax=\"bill_length_mm\")\n",
83+
" .add(so.Range(), color=\"island\")\n",
84+
")"
85+
]
86+
},
87+
{
88+
"cell_type": "markdown",
89+
"id": "2191bec6-a02e-48e0-b92c-69c38826049d",
90+
"metadata": {},
91+
"source": [
92+
"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:"
93+
]
94+
},
95+
{
96+
"cell_type": "code",
97+
"execution_count": null,
98+
"id": "63c6352e-4ef5-4cff-940e-35fa5804b2c7",
99+
"metadata": {},
100+
"outputs": [],
101+
"source": [
102+
"(\n",
103+
" so.Plot(penguins, x=\"sex\", y=\"body_mass_g\")\n",
104+
" .facet(\"species\")\n",
105+
" .add(so.Dots(pointsize=6))\n",
106+
" .add(so.Range(linewidth=2))\n",
89107
")"
90108
]
91109
},
92110
{
93111
"cell_type": "code",
94112
"execution_count": null,
95-
"id": "08751ee7-d0a0-4e70-92b4-c1b38ea28890",
113+
"id": "c215deb1-e510-4631-b999-737f5f41cae2",
96114
"metadata": {},
97115
"outputs": [],
98116
"source": []

doc/whatsnew/v0.12.1.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ v0.12.1 (Unreleased)
44

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

7+
- |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`).
8+
79
- |Fix| Make :class:`objects.PolyFit` robust to missing data (:pr:`3010`).
810

911
- |Fix| Fixed a bug that caused an exception when more than two layers with the same mappings were added (:pr:`3055`).

seaborn/_marks/area.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,9 @@ class Band(AreaBase, Mark):
162162
def _standardize_coordinate_parameters(self, data, orient):
163163
# dv = {"x": "y", "y": "x"}[orient]
164164
# TODO assert that all(ymax >= ymin)?
165+
# TODO what if only one exist?
166+
other = {"x": "y", "y": "x"}[orient]
167+
if not set(data.columns) & {f"{other}min", f"{other}max"}:
168+
agg = {f"{other}min": (other, "min"), f"{other}max": (other, "max")}
169+
data = data.groupby(orient).agg(**agg).reset_index()
165170
return data

seaborn/_marks/line.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,9 @@ def _plot(self, split_gen, scales, orient):
204204
# Handle datalim update manually
205205
# https://github.com/matplotlib/matplotlib/issues/23129
206206
ax.add_collection(lines, autolim=False)
207-
xy = np.concatenate(ax_data["segments"])
208-
ax.update_datalim(xy)
207+
if ax_data["segments"]:
208+
xy = np.concatenate(ax_data["segments"])
209+
ax.update_datalim(xy)
209210

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

@@ -270,9 +271,16 @@ def _setup_lines(self, split_gen, scales, orient):
270271
"linestyles": [],
271272
}
272273

274+
# TODO better checks on what variables we have
275+
273276
vals = resolve_properties(self, keys, scales)
274277
vals["color"] = resolve_color(self, keys, scales=scales)
275278

279+
# TODO what if only one exist?
280+
if not set(data.columns) & {f"{other}min", f"{other}max"}:
281+
agg = {f"{other}min": (other, "min"), f"{other}max": (other, "max")}
282+
data = data.groupby(orient).agg(**agg).reset_index()
283+
276284
cols = [orient, f"{other}min", f"{other}max"]
277285
data = data[cols].melt(orient, value_name=other)[["x", "y"]]
278286
segments = [d.to_numpy() for _, d in data.groupby(orient)]

tests/_marks/test_area.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from seaborn._marks.area import Area, Band
99

1010

11-
class TestAreaMarks:
11+
class TestArea:
1212

1313
def test_single_defaults(self):
1414

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

100-
def test_band(self):
100+
101+
class TestBand:
102+
103+
def test_range(self):
101104

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

110113
expected_y = [2, 1, 4, 5, 3, 3, 2]
111114
assert_array_equal(verts[1], expected_y)
115+
116+
def test_auto_range(self):
117+
118+
x = [1, 1, 2, 2, 2]
119+
y = [1, 2, 3, 4, 5]
120+
p = Plot(x=x, y=y).add(Band()).plot()
121+
ax = p._figure.axes[0]
122+
verts = ax.patches[0].get_path().vertices.T
123+
124+
expected_x = [1, 2, 2, 1, 1]
125+
assert_array_equal(verts[0], expected_x)
126+
127+
expected_y = [1, 3, 5, 2, 1]
128+
assert_array_equal(verts[1], expected_y)

tests/_marks/test_line.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ def test_xy_data(self):
246246
assert_array_equal(verts[0], [2, 5])
247247
assert_array_equal(verts[1], [3, 4])
248248

249+
def test_single_orient_value(self):
250+
251+
x = [1, 1, 1]
252+
y = [1, 2, 3]
253+
p = Plot(x, y).add(Lines()).plot()
254+
lines, = p._figure.axes[0].collections
255+
paths, = lines.get_paths()
256+
assert paths.vertices.shape == (0, 2)
257+
249258

250259
class TestRange:
251260

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

275+
def test_auto_range(self):
276+
277+
x = [1, 1, 2, 2, 2]
278+
y = [1, 2, 3, 4, 5]
279+
280+
p = Plot(x=x, y=y).add(Range()).plot()
281+
lines, = p._figure.axes[0].collections
282+
paths = lines.get_paths()
283+
assert_array_equal(paths[0].vertices, [(1, 1), (1, 2)])
284+
assert_array_equal(paths[1].vertices, [(2, 3), (2, 5)])
285+
266286
def test_mapped_color(self):
267287

268288
x = [1, 2, 1, 2]

0 commit comments

Comments
 (0)