Repository: mwaskom/seaborn

Issue:
No easy way to set axis limits
This is a blocker for release.

Options:

- Use `norm` in the scale object (bleh)
- Have `Plot.limit` that works for any variable, working with the scale (interaction with `norm` needs definition)
- Another more general method (e.g. in `configure` that accepts `limits: dict[variable, tuple[float, float]]`

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2898?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) Report
> Merging [#2898](https://codecov.io/gh/mwaskom/seaborn/pull/2898?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) (8461bc0) 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/2898/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/2898?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    #2898   +/-   ##
=======================================
  Coverage   98.20%   98.20%           
=======================================
  Files          69       69           
  Lines       22561    22601   +40     
=======================================
+ Hits        22156    22196   +40     
  Misses        405      405           
```


| [Impacted Files](https://codecov.io/gh/mwaskom/seaborn/pull/2898?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) | Coverage Δ | |
|---|---|---|
| [seaborn/\_core/plot.py](https://codecov.io/gh/mwaskom/seaborn/pull/2898/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-c2VhYm9ybi9fY29yZS9wbG90LnB5) | `94.80% <100.00%> (+0.18%)` | :arrow_up: |
| [tests/\_core/test\_plot.py](https://codecov.io/gh/mwaskom/seaborn/pull/2898/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-dGVzdHMvX2NvcmUvdGVzdF9wbG90LnB5) | `98.42% <100.00%> (+0.02%)` | :arrow_up: |


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/plot.py b/seaborn/_core/plot.py
index 105c6fc6..357601e7 100644
--- a/seaborn/_core/plot.py
+++ b/seaborn/_core/plot.py
@@ -10,7 +10,7 @@ import itertools
 import textwrap
 from collections import abc
 from collections.abc import Callable, Generator, Hashable
-from typing import Any
+from typing import Any, cast
 
 import pandas as pd
 from pandas import DataFrame, Series, Index
@@ -142,11 +142,11 @@ class Plot:
     the plot without rendering it to access the lower-level representation.
 
     """
-    # TODO use TypedDict throughout?
-
     _data: PlotData
     _layers: list[Layer]
+
     _scales: dict[str, Scale]
+    _limits: dict[str, tuple[Any, Any]]
 
     _subplot_spec: dict[str, Any]  # TODO values type
     _facet_spec: FacetSpec
@@ -169,7 +169,9 @@ class Plot:
 
         self._data = PlotData(data, variables)
         self._layers = []
+
         self._scales = {}
+        self._limits = {}
 
         self._subplot_spec = {}
         self._facet_spec = {}
@@ -543,6 +545,23 @@ class Plot:
         new._scales.update(**scales)
         return new
 
+    def limit(self, **limits: tuple[Any, Any]) -> Plot:
+        """
+        Control the range of visible data.
+
+        Keywords correspond to variables defined in the plot, and values are a
+        (min, max) tuple (where either can be `None` to leave unset).
+
+        Limits apply only to the axis scale; data outside the limits are still
+        used in any stat transforms and added to the plot.
+
+        Behavior for non-coordinate variables is currently undefined.
+
+        """
+        new = self._clone()
+        new._limits.update(limits)
+        return new
+
     def configure(
         self,
         figsize: tuple[float, float] | None = None,
@@ -634,11 +653,8 @@ class Plot:
         for layer in layers:
             plotter._plot_layer(self, layer)
 
-        plotter._make_legend()
-
-        # TODO this should be configurable
-        if not plotter._figure.get_constrained_layout():
-            plotter._figure.set_tight_layout(True)
+        plotter._make_legend(self)
+        plotter._finalize_figure(self)
 
         return plotter
 
@@ -1379,7 +1395,7 @@ class Plotter:
 
         self._legend_contents.extend(contents)
 
-    def _make_legend(self) -> None:
+    def _make_legend(self, p: Plot) -> None:
         """Create the legend artist(s) and add onto the figure."""
         # Combine artists representing same information across layers
         # Input list has an entry for each distinct variable in each layer
@@ -1424,3 +1440,26 @@ class Plotter:
             else:
                 base_legend = legend
                 self._figure.legends.append(legend)
+
+    def _finalize_figure(self, p: Plot) -> None:
+
+        for sub in self._subplots:
+            ax = sub["ax"]
+            for axis in "xy":
+                axis_key = sub[axis]
+
+                # Axis limits
+                if axis_key in p._limits:
+                    convert_units = getattr(ax, f"{axis}axis").convert_units
+                    a, b = p._limits[axis_key]
+                    lo = a if a is None else convert_units(a)
+                    hi = b if b is None else convert_units(b)
+                    if isinstance(a, str):
+                        lo = cast(float, lo) - 0.5
+                    if isinstance(b, str):
+                        hi = cast(float, hi) + 0.5
+                    ax.set(**{f"{axis}lim": (lo, hi)})
+
+        # TODO this should be configurable
+        if not self._figure.get_constrained_layout():
+            self._figure.set_tight_layout(True)
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/_core/test_plot.py b/tests/_core/test_plot.py
index 062fd5d3..9cf7999b 100644
--- a/tests/_core/test_plot.py
+++ b/tests/_core/test_plot.py
@@ -995,6 +995,23 @@ class TestPlotting:
         assert ax.get_xlabel() == "a"
         assert ax.get_ylabel() == "b"
 
+    def test_limits(self, long_df):
+
+        limit = (-2, 24)
+        p = Plot(long_df, x="x", y="y").limit(x=limit).plot()
+        ax1 = p._figure.axes[0]
+        assert ax1.get_xlim() == limit
+
+        limit = (np.datetime64("2005-01-01"), np.datetime64("2008-01-01"))
+        p = Plot(long_df, x="d", y="y").limit(x=limit).plot()
+        ax = p._figure.axes[0]
+        assert ax.get_xlim() == tuple(mpl.dates.date2num(limit))
+
+        limit = ("b", "c")
+        p = Plot(x=["a", "b", "c", "d"], y=[1, 2, 3, 4]).limit(x=limit).plot()
+        ax = p._figure.axes[0]
+        assert ax.get_xlim() == (0.5, 2.5)
+
 
 class TestFacetInterface:
 
@@ -1382,6 +1399,13 @@ class TestPairInterface:
         with pytest.raises(RuntimeError, match=err):
             p.facet(col="a", row="b", order=["a", "b", "c"])
 
+    def test_limits(self, long_df):
+
+        limit = (-2, 24)
+        p = Plot(long_df, y="y").pair(x=["x", "z"]).limit(x1=limit).plot()
+        ax1 = p._figure.axes[1]
+        assert ax1.get_xlim() == limit
+
 
 class TestLabelVisibility:
 
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)