Repository: mwaskom/seaborn

Issue:
Use extent clipping (and sticky edges) for Area mark
Give the `Area` mark the same behavior as we added to `Bar` in #2889

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2896?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) Report
> Merging [#2896](https://codecov.io/gh/mwaskom/seaborn/pull/2896?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) (0db68f1) into [master](https://codecov.io/gh/mwaskom/seaborn/commit/3c8447fc92e73b353202bacefc007784460b225e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) (3c8447f) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mwaskom/seaborn/pull/2896/graphs/tree.svg?width=650&height=150&src=pr&token=SLuzHnA9Bl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom)](https://codecov.io/gh/mwaskom/seaborn/pull/2896?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom)

```diff
@@           Coverage Diff           @@
##           master    #2896   +/-   ##
=======================================
  Coverage   98.20%   98.20%           
=======================================
  Files          69       69           
  Lines       22561    22573   +12     
=======================================
+ Hits        22156    22168   +12     
  Misses        405      405           
```


| [Impacted Files](https://codecov.io/gh/mwaskom/seaborn/pull/2896?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) | Coverage Δ | |
|---|---|---|
| [seaborn/\_marks/area.py](https://codecov.io/gh/mwaskom/seaborn/pull/2896/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-c2VhYm9ybi9fbWFya3MvYXJlYS5weQ==) | `97.29% <100.00%> (+0.68%)` | :arrow_up: |
| [tests/\_marks/test\_area.py](https://codecov.io/gh/mwaskom/seaborn/pull/2896/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-dGVzdHMvX21hcmtzL3Rlc3RfYXJlYS5weQ==) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/doc/introduction.ipynb b/doc/introduction.ipynb
index 3fca871f..e6b32738 100644
--- a/doc/introduction.ipynb
+++ b/doc/introduction.ipynb
@@ -400,7 +400,7 @@
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "When you're ready to share or publish your work, you'll probably want to polish the figure beyond what the defaults achieve. Seaborn allows for several levels of customization. It defines multiple built-in :doc:`themes <tutorial/aesthetics>` that apply to all figures, its functions have standardized parameters that can modify the semantic mappings for each plot, and additional keyword arguments are passed down to the underlying matplotlib artsts, allowing even more control. Once you've created a plot, its properties can be modified through both the seaborn API and by dropping down to the matplotlib layer for fine-grained tweaking:"
+    "When you're ready to share or publish your work, you'll probably want to polish the figure beyond what the defaults achieve. Seaborn allows for several levels of customization. It defines multiple built-in :doc:`themes <tutorial/aesthetics>` that apply to all figures, its functions have standardized parameters that can modify the semantic mappings for each plot, and additional keyword arguments are passed down to the underlying matplotlib artists, allowing even more control. Once you've created a plot, its properties can be modified through both the seaborn API and by dropping down to the matplotlib layer for fine-grained tweaking:"
    ]
   },
   {
diff --git a/doc/releases/v0.12.0.txt b/doc/releases/v0.12.0.txt
index 5f481deb..bbeba0df 100644
--- a/doc/releases/v0.12.0.txt
+++ b/doc/releases/v0.12.0.txt
@@ -80,7 +80,7 @@ Other updates
 
 - |Fix| Fixed a bug in :func:`violinplot` where inner boxes/points could be missing with unpaired split violins (:pr:`2814`).
 
-- |Fix| Fixed a bug in :class:`PairGrid` where and error would be raised when defining `hue` only in the mapping methods (:pr:`2847`).
+- |Fix| Fixed a bug in :class:`PairGrid` where an error would be raised when defining `hue` only in the mapping methods (:pr:`2847`).
 
 - |Fix| Fixed a bug in :func:`scatterplot` where an error would be raised when `hue_order` was a subset of the hue levels (:pr:`2848`).
 
diff --git a/doc/tutorial/color_palettes.ipynb b/doc/tutorial/color_palettes.ipynb
index 05f73ac2..5eb78a9c 100644
--- a/doc/tutorial/color_palettes.ipynb
+++ b/doc/tutorial/color_palettes.ipynb
@@ -320,7 +320,7 @@
     "\n",
     "The most important function for working with color palettes is, aptly, :func:`color_palette`. This function provides an interface to most of the possible ways that one can generate color palettes in seaborn. And it's used internally by any function that has a ``palette`` argument.\n",
     "\n",
-    "The primary argument to :func:`color_palette` is usually a string: either the a name of a specific palette or the name of a family and additional arguments to select a specific member. In the latter case, :func:`color_palette` will delegate to more specific function, such as :func:`cubehelix_palette`. It's also possible to pass a list of colors specified any way that matplotlib accepts (an RGB tuple, a hex code, or a name in the X11 table). The return value is an object that wraps a list of RGB tuples with a few useful methods, such as conversion to hex codes and a rich HTML representation.\n",
+    "The primary argument to :func:`color_palette` is usually a string: either the name of a specific palette or the name of a family and additional arguments to select a specific member. In the latter case, :func:`color_palette` will delegate to more specific function, such as :func:`cubehelix_palette`. It's also possible to pass a list of colors specified any way that matplotlib accepts (an RGB tuple, a hex code, or a name in the X11 table). The return value is an object that wraps a list of RGB tuples with a few useful methods, such as conversion to hex codes and a rich HTML representation.\n",
     "\n",
     "Calling :func:`color_palette` with no arguments will return the current default color palette that matplotlib (and most seaborn functions) will use if colors are not otherwise specified. This default palette can be set with the corresponding :func:`set_palette` function, which calls :func:`color_palette` internally and accepts the same arguments.\n",
     "\n",
@@ -423,7 +423,7 @@
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "Many people find the moderated hues of the default ``\"deep\"`` palette to be aesthetically pleasing, but they are also less distinct. As a result, they may be more difficult to discriminate in some contexts, which is something to keep in mind when making publication graphics. `This comparison <https://gist.github.com/mwaskom/b35f6ebc2d4b340b4f64a4e28e778486>`_ can be helpful for estimating how the the seaborn color palettes perform when simulating different forms of colorblindess."
+    "Many people find the moderated hues of the default ``\"deep\"`` palette to be aesthetically pleasing, but they are also less distinct. As a result, they may be more difficult to discriminate in some contexts, which is something to keep in mind when making publication graphics. `This comparison <https://gist.github.com/mwaskom/b35f6ebc2d4b340b4f64a4e28e778486>`_ can be helpful for estimating how the seaborn color palettes perform when simulating different forms of colorblindess."
    ]
   },
   {
diff --git a/doc/tutorial/data_structure.ipynb b/doc/tutorial/data_structure.ipynb
index cef161af..054b3168 100644
--- a/doc/tutorial/data_structure.ipynb
+++ b/doc/tutorial/data_structure.ipynb
@@ -339,7 +339,7 @@
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "Many pandas operations, such as a the split-apply-combine operations of a group-by, will produce a dataframe where information has moved from the columns of the input dataframe to the index of the output. So long as the name is retained, you can still reference the data as normal:"
+    "Many pandas operations, such as the split-apply-combine operations of a group-by, will produce a dataframe where information has moved from the columns of the input dataframe to the index of the output. So long as the name is retained, you can still reference the data as normal:"
    ]
   },
   {
diff --git a/doc/tutorial/error_bars.ipynb b/doc/tutorial/error_bars.ipynb
index 2a72dc9e..4d00f377 100644
--- a/doc/tutorial/error_bars.ipynb
+++ b/doc/tutorial/error_bars.ipynb
@@ -145,7 +145,7 @@
     "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n",
     "\n",
     "Percentile intervals also represent the range where some amount of the data fall, but they do so by \n",
-    "computing those percentiles directly from your sample. By default, `errorbar=\"pi\"` will show a 95% interval, ranging from the 2.5 to the 97.5 percentiles. You can chose a different range by passing a size parameter, e.g., to show the inter-quartile range:"
+    "computing those percentiles directly from your sample. By default, `errorbar=\"pi\"` will show a 95% interval, ranging from the 2.5 to the 97.5 percentiles. You can choose a different range by passing a size parameter, e.g., to show the inter-quartile range:"
    ]
   },
   {
@@ -229,7 +229,7 @@
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "The nonparametric bootstrap has advantages similar to those of the percentile interval: it will naturally adapt to skwewed and bounded data in a way that a standard error interval cannot. It is also more general. While the standard error formula is specific to the mean, error bars can be computed using the boootstrap for any estimator:"
+    "The nonparametric bootstrap has advantages similar to those of the percentile interval: it will naturally adapt to skewed and bounded data in a way that a standard error interval cannot. It is also more general. While the standard error formula is specific to the mean, error bars can be computed using the bootstrap for any estimator:"
    ]
   },
   {
diff --git a/seaborn/_core/scales.py b/seaborn/_core/scales.py
index a3cdfb28..477750a3 100644
--- a/seaborn/_core/scales.py
+++ b/seaborn/_core/scales.py
@@ -496,7 +496,7 @@ class Continuous(ContinuousBase):
         unit : str or (str, str) tuple
             Use  SI prefixes with these units (e.g., with `unit="g"`, a tick value
             of 5000 will appear as `5 kg`). When a tuple, the first element gives the
-            seperator between the number and unit.
+            separator between the number and unit.
 
         Returns
         -------
diff --git a/seaborn/_marks/area.py b/seaborn/_marks/area.py
index 27324f25..4c932f02 100644
--- a/seaborn/_marks/area.py
+++ b/seaborn/_marks/area.py
@@ -21,34 +21,40 @@ class AreaBase:
 
     def _plot(self, split_gen, scales, orient):
 
-        kws = {}
+        patches = defaultdict(list)
 
         for keys, data, ax in split_gen():
 
-            kws.setdefault(ax, defaultdict(list))
-
+            kws = {}
             data = self._standardize_coordinate_parameters(data, orient)
             resolved = resolve_properties(self, keys, scales)
             verts = self._get_verts(data, orient)
-
             ax.update_datalim(verts)
-            kws[ax]["verts"].append(verts)
 
-            # TODO fill= is not working here properly
-            # We could hack a fix, but would be better to handle fill in resolve_color
+            # TODO should really move this logic into resolve_color
+            fc = resolve_color(self, keys, "", scales)
+            if not resolved["fill"]:
+                fc = mpl.colors.to_rgba(fc, 0)
+
+            kws["facecolor"] = fc
+            kws["edgecolor"] = resolve_color(self, keys, "edge", scales)
+            kws["linewidth"] = resolved["edgewidth"]
+            kws["linestyle"] = resolved["edgestyle"]
 
-            kws[ax]["facecolors"].append(resolve_color(self, keys, "", scales))
-            kws[ax]["edgecolors"].append(resolve_color(self, keys, "edge", scales))
+            patches[ax].append(mpl.patches.Polygon(verts, **kws))
 
-            kws[ax]["linewidth"].append(resolved["edgewidth"])
-            kws[ax]["linestyle"].append(resolved["edgestyle"])
+        for ax, ax_patches in patches.items():
 
-        for ax, ax_kws in kws.items():
-            ax.add_collection(mpl.collections.PolyCollection(**ax_kws))
+            for patch in ax_patches:
+                self._postprocess_artist(patch, ax, orient)
+                ax.add_patch(patch)
 
     def _standardize_coordinate_parameters(self, data, orient):
         return data
 
+    def _postprocess_artist(self, artist, ax, orient):
+        pass
+
     def _get_verts(self, data, orient):
 
         dv = {"x": "y", "y": "x"}[orient]
@@ -66,8 +72,12 @@ class AreaBase:
         keys = {v: value for v in variables}
         resolved = resolve_properties(self, keys, scales)
 
+        fc = resolve_color(self, keys, "", scales)
+        if not resolved["fill"]:
+            fc = mpl.colors.to_rgba(fc, 0)
+
         return mpl.patches.Patch(
-            facecolor=resolve_color(self, keys, "", scales),
+            facecolor=fc,
             edgecolor=resolve_color(self, keys, "edge", scales),
             linewidth=resolved["edgewidth"],
             linestyle=resolved["edgestyle"],
@@ -95,6 +105,25 @@ class Area(AreaBase, Mark):
         dv = {"x": "y", "y": "x"}[orient]
         return data.rename(columns={"baseline": f"{dv}min", dv: f"{dv}max"})
 
+    def _postprocess_artist(self, artist, ax, orient):
+
+        # TODO copying a lot of code from Bar, let's abstract this
+        # See comments there, I am not going to repeat them too
+
+        artist.set_linewidth(artist.get_linewidth() * 2)
+
+        linestyle = artist.get_linestyle()
+        if linestyle[1]:
+            linestyle = (linestyle[0], tuple(x / 2 for x in linestyle[1]))
+        artist.set_linestyle(linestyle)
+
+        artist.set_clip_path(artist.get_path(), artist.get_transform() + ax.transData)
+        if self.artist_kws.get("clip_on", True):
+            artist.set_clip_box(ax.bbox)
+
+        val_idx = ["y", "x"].index(orient)
+        artist.sticky_edges[val_idx][:] = (0, np.inf)
+
 
 @dataclass
 class Ribbon(AreaBase, Mark):
diff --git a/seaborn/_oldcore.py b/seaborn/_oldcore.py
index 8afd10f0..192af381 100644
--- a/seaborn/_oldcore.py
+++ b/seaborn/_oldcore.py
@@ -650,7 +650,7 @@ class VectorPlotter:
 
     @classmethod
     def get_semantics(cls, kwargs, semantics=None):
-        """Subset a dictionary` arguments with known semantic variables."""
+        """Subset a dictionary arguments with known semantic variables."""
         # TODO this should be get_variables since we have included x and y
         if semantics is None:
             semantics = cls.semantics
diff --git a/seaborn/algorithms.py b/seaborn/algorithms.py
index 0bcc6c1e..2867e9a0 100644
--- a/seaborn/algorithms.py
+++ b/seaborn/algorithms.py
@@ -137,6 +137,6 @@ def _handle_random_seed(seed=None):
             elif seed is None:
                 rng = np.random.RandomState()
             else:
-                err = "{} cannot be used to seed the randomn number generator"
+                err = "{} cannot be used to seed the random number generator"
                 raise ValueError(err.format(seed))
     return rng
diff --git a/seaborn/axisgrid.py b/seaborn/axisgrid.py
index ae6c9aac..d5e2109b 100644
--- a/seaborn/axisgrid.py
+++ b/seaborn/axisgrid.py
@@ -1120,7 +1120,7 @@ class PairGrid(Grid):
     This object maps each variable in a dataset onto a column and row in a
     grid of multiple axes. Different axes-level plotting functions can be
     used to draw bivariate plots in the upper and lower triangles, and the
-    the marginal distribution of each variable can be shown on the diagonal.
+    marginal distribution of each variable can be shown on the diagonal.
 
     Several different common plots can be generated in a single line using
     :func:`pairplot`. Use :class:`PairGrid` when you need more flexibility.
diff --git a/seaborn/distributions.py b/seaborn/distributions.py
index f9e5b787..a6f6ab5c 100644
--- a/seaborn/distributions.py
+++ b/seaborn/distributions.py
@@ -1539,7 +1539,7 @@ pthresh : number or None
     transparent.
 pmax : number or None
     A value in [0, 1] that sets that saturation point for the colormap at a value
-    such that cells below is constistute this proportion of the total count (or
+    such that cells below constitute this proportion of the total count (or
     other statistic, when used).
 {params.dist.cbar}
 {params.dist.cbar_ax}


Test patch:
diff --git a/tests/_marks/test_area.py b/tests/_marks/test_area.py
index 70f31541..dcc3f5a8 100644
--- a/tests/_marks/test_area.py
+++ b/tests/_marks/test_area.py
@@ -1,6 +1,6 @@
 
 import matplotlib as mpl
-from matplotlib.colors import to_rgba_array
+from matplotlib.colors import to_rgba, to_rgba_array
 
 from numpy.testing import assert_array_equal
 
@@ -15,8 +15,8 @@ class TestAreaMarks:
         x, y = [1, 2, 3], [1, 2, 1]
         p = Plot(x=x, y=y).add(Area()).plot()
         ax = p._figure.axes[0]
-        poly, = ax.collections
-        verts = poly.get_paths()[0].vertices.T
+        poly = ax.patches[0]
+        verts = poly.get_path().vertices.T
 
         expected_x = [1, 2, 3, 3, 2, 1, 1]
         assert_array_equal(verts[0], expected_x)
@@ -25,13 +25,13 @@ class TestAreaMarks:
         assert_array_equal(verts[1], expected_y)
 
         fc = poly.get_facecolor()
-        assert_array_equal(fc, to_rgba_array("C0", .2))
+        assert_array_equal(fc, to_rgba("C0", .2))
 
         ec = poly.get_edgecolor()
-        assert_array_equal(ec, to_rgba_array("C0", 1))
+        assert_array_equal(ec, to_rgba("C0", 1))
 
         lw = poly.get_linewidth()
-        assert_array_equal(lw, mpl.rcParams["patch.linewidth"])
+        assert_array_equal(lw, mpl.rcParams["patch.linewidth"] * 2)
 
     def test_direct_parameters(self):
 
@@ -46,20 +46,20 @@ class TestAreaMarks:
         )
         p = Plot(x=x, y=y).add(mark).plot()
         ax = p._figure.axes[0]
-        poly, = ax.collections
+        poly = ax.patches[0]
 
         fc = poly.get_facecolor()
-        assert_array_equal(fc, to_rgba_array(mark.color, mark.alpha))
+        assert_array_equal(fc, to_rgba(mark.color, mark.alpha))
 
         ec = poly.get_edgecolor()
-        assert_array_equal(ec, to_rgba_array(mark.edgecolor, mark.edgealpha))
+        assert_array_equal(ec, to_rgba(mark.edgecolor, mark.edgealpha))
 
         lw = poly.get_linewidth()
-        assert_array_equal(lw, mark.edgewidth)
+        assert_array_equal(lw, mark.edgewidth * 2)
 
         ls = poly.get_linestyle()
         dash_on, dash_off = mark.edgestyle[1]
-        expected = [(0, [mark.edgewidth * dash_on, mark.edgewidth * dash_off])]
+        expected = (0, (mark.edgewidth * dash_on / 4, mark.edgewidth * dash_off / 4))
         assert ls == expected
 
     def test_mapped(self):
@@ -68,33 +68,38 @@ class TestAreaMarks:
         g = ["a", "a", "a", "b", "b", "b"]
         p = Plot(x=x, y=y, color=g, edgewidth=g).add(Area()).plot()
         ax = p._figure.axes[0]
-        polys, = ax.collections
 
-        paths = polys.get_paths()
         expected_x = [1, 2, 3, 3, 2, 1, 1], [2, 3, 4, 4, 3, 2, 2]
         expected_y = [0, 0, 0, 1, 2, 1, 0], [0, 0, 0, 2, 3, 1, 0]
 
-        for i, path in enumerate(paths):
-            verts = path.vertices.T
+        for i, poly in enumerate(ax.patches):
+            verts = poly.get_path().vertices.T
             assert_array_equal(verts[0], expected_x[i])
             assert_array_equal(verts[1], expected_y[i])
 
-        fc = polys.get_facecolor()
-        assert_array_equal(fc, to_rgba_array(["C0", "C1"], .2))
+        fcs = [p.get_facecolor() for p in ax.patches]
+        assert_array_equal(fcs, to_rgba_array(["C0", "C1"], .2))
 
-        ec = polys.get_edgecolor()
-        assert_array_equal(ec, to_rgba_array(["C0", "C1"], 1))
+        ecs = [p.get_edgecolor() for p in ax.patches]
+        assert_array_equal(ecs, to_rgba_array(["C0", "C1"], 1))
 
-        lw = polys.get_linewidths()
-        assert lw[0] > lw[1]
+        lws = [p.get_linewidth() for p in ax.patches]
+        assert lws[0] > lws[1]
+
+    def test_unfilled(self):
+
+        x, y = [1, 2, 3], [1, 2, 1]
+        p = Plot(x=x, y=y).add(Area(fill=False)).plot()
+        ax = p._figure.axes[0]
+        poly = ax.patches[0]
+        assert poly.get_facecolor() == to_rgba("C0", 0)
 
     def test_ribbon(self):
 
         x, ymin, ymax = [1, 2, 4], [2, 1, 4], [3, 3, 5]
         p = Plot(x=x, ymin=ymin, ymax=ymax).add(Ribbon()).plot()
         ax = p._figure.axes[0]
-        poly, = ax.collections
-        verts = poly.get_paths()[0].vertices.T
+        verts = ax.patches[0].get_path().vertices.T
 
         expected_x = [1, 2, 4, 4, 2, 1, 1]
         assert_array_equal(verts[0], expected_x)