Repository: mwaskom/seaborn

Issue:
Scatterplot fails with matplotlib==3.3.1
Matplotlib 3.3.1 has just been released and it breaks `scatterplot` (and `relplot(kind="scatter")`). The problem is the `marker=''` argument in the legend artists creation, which now raises an exception upon drawing (seems unintentional). See [https://github.com/matplotlib/matplotlib/issues/18254](https://github.com/matplotlib/matplotlib/issues/18254). Example code:

```python
import seaborn as sns

# Load the example mpg dataset
mpg = sns.load_dataset("mpg")

# Plot miles per gallon against horsepower with other semantics
sns.scatterplot(x="horsepower", y="mpg", hue="origin", data=mpg)
```

Until upstream issue is resolved I'd advocate using matplotlib==3.3.0.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=h1) Report
> Merging [#2229](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=desc) into [master](https://codecov.io/gh/mwaskom/seaborn/commit/eba6a57d67e056740c156dde1d26e12bbc7a8d75?el=desc) will **decrease** coverage by `0.05%`.
> The diff coverage is `95.45%`.

[![Impacted file tree graph](https://codecov.io/gh/mwaskom/seaborn/pull/2229/graphs/tree.svg?width=650&height=150&src=pr&token=SLuzHnA9Bl)](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2229      +/-   ##
==========================================
- Coverage   96.93%   96.87%   -0.06%     
==========================================
  Files          17       17              
  Lines        5864     5892      +28     
==========================================
+ Hits         5684     5708      +24     
- Misses        180      184       +4     
```


| [Impacted Files](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [seaborn/axisgrid.py](https://codecov.io/gh/mwaskom/seaborn/pull/2229/diff?src=pr&el=tree#diff-c2VhYm9ybi9heGlzZ3JpZC5weQ==) | `95.53% <80.00%> (-0.10%)` | :arrow_down: |
| [seaborn/relational.py](https://codecov.io/gh/mwaskom/seaborn/pull/2229/diff?src=pr&el=tree#diff-c2VhYm9ybi9yZWxhdGlvbmFsLnB5) | `99.42% <96.15%> (-0.28%)` | :arrow_down: |
| [seaborn/\_core.py](https://codecov.io/gh/mwaskom/seaborn/pull/2229/diff?src=pr&el=tree#diff-c2VhYm9ybi9fY29yZS5weQ==) | `98.12% <100.00%> (-0.32%)` | :arrow_down: |
| [seaborn/utils.py](https://codecov.io/gh/mwaskom/seaborn/pull/2229/diff?src=pr&el=tree#diff-c2VhYm9ybi91dGlscy5weQ==) | `88.61% <100.00%> (+0.50%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=footer). Last update [eba6a57...4ef0918](https://codecov.io/gh/mwaskom/seaborn/pull/2229?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/ci/deps_minimal.txt b/ci/deps_minimal.txt
index 021ddef1..8fed0418 100644
--- a/ci/deps_minimal.txt
+++ b/ci/deps_minimal.txt
@@ -1,4 +1,4 @@
 numpy
 scipy
-matplotlib!=3.3.1
+matplotlib
 pandas
diff --git a/doc/docstrings/lineplot.ipynb b/doc/docstrings/lineplot.ipynb
index b4fc8a22..88a5bde9 100644
--- a/doc/docstrings/lineplot.ipynb
+++ b/doc/docstrings/lineplot.ipynb
@@ -307,7 +307,7 @@
     "sns.lineplot(\n",
     "    data=dots.query(\"coherence > 0\"),\n",
     "    x=\"time\", y=\"firing_rate\", hue=\"coherence\", style=\"choice\",\n",
-    "     palette=\"viridis_r\", hue_norm=mpl.colors.LogNorm(),\n",
+    "     palette=\"rocket_r\", hue_norm=mpl.colors.LogNorm(),\n",
     ")"
    ]
   },
diff --git a/doc/docstrings/scatterplot.ipynb b/doc/docstrings/scatterplot.ipynb
index d821457c..24ddf22e 100644
--- a/doc/docstrings/scatterplot.ipynb
+++ b/doc/docstrings/scatterplot.ipynb
@@ -127,9 +127,24 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.scatterplot(\n",
-    "    data=tips, x=\"total_bill\", y=\"tip\", hue=\"size\", palette=\"deep\",\n",
-    ")"
+    "sns.scatterplot(data=tips, x=\"total_bill\", y=\"tip\", hue=\"size\", palette=\"deep\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "metadata": {},
+   "source": [
+    "If there are a large number of unique numeric values, the legend will show a representative, evenly-spaced set:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "tip_rate = tips.eval(\"tip / total_bill\").rename(\"tip_rate\")\n",
+    "sns.scatterplot(data=tips, x=\"total_bill\", y=\"tip\", hue=tip_rate)"
    ]
   },
   {
diff --git a/doc/releases/v0.11.0.txt b/doc/releases/v0.11.0.txt
index 79440d7a..185a4d2c 100644
--- a/doc/releases/v0.11.0.txt
+++ b/doc/releases/v0.11.0.txt
@@ -79,6 +79,8 @@ TODO organize by module.
 
 - Plots with a ``style`` semantic can now generate an infinite number of unique dashes and/or markers by default. Previously, an error would be raised if the ``style`` variable had more levels than could be mapped using the default lists. The existing defaults were slightly modified as part of this change; if you need to exactly reproduce plots from earlier versions, refer to the `old defaults <https://github.com/mwaskom/seaborn/blob/v0.10.1/seaborn/relational.py#L24>`_ (:pr:`2075`).
 
+- Reduced some of the surprising behavior of relational plot legends when using a numeric hue or size mapping. Added an "auto" mode (the new default) that chooses between "brief" and "full" based on the number of unique levels of the variable(s). Modified the ticking algorithm to only show values inside the limits of the data and to show up to 6 values. Changed the approach to the legend title: the normal matplotlib legend title is used when only one variable is assigned a semantic mapping, whereas the old approach of adding an invisible legend artist with a subtitle label is used only when multiple semantic variables are defined. The subtitles are now also left-aligned and set to the legend title font size. (:pr:`2229`).
+
 - Fixed a few issues with :func:`boxenplot` regarding the way the number of boxes are calculated. ``k_depth="tukey"`` is now the default boxes calculation method as the previous default (`"proportion"`) produces too many boxes when datasets are small. Added the option to specify the desired number of boxes as a scalar (e.g. ``k_depth=6``) or just plot boxes that will cover most of the data points (with ``k_depth="full"``). Added a new parameter ``trust_alpha`` to control the number of boxes when ``k_depth="trustworthy"``. Additionally, the visual appearance of :func:`boxenplot` now more closely resembles :func:`boxplot`, and thin boxes will remain visible when the edges are white. Finally, the ``lvplot`` function (the previously-deprecated name for :func:`boxenplot`) has been removed. (:pr:`2086`).
 
 - Added a ``tight_layout`` method to :class:`FacetGrid` and :class:`PairGrid`, which runs the :func:`matplotlib.pyplot.tight_layout` algorithm without interference from the external legend (:pr:`2073`).
@@ -87,7 +89,7 @@ TODO organize by module.
 
 - Adapted to a change in matplotlib that prevented passing vectors of literal values to ``c`` and ``s`` in :func:`scatterplot` (:pr:`2079`).
 
-- Added an explicit warning in :func:`swarmplot` when more than 2% of the points overlap in the "gutters" of the swarm (:pr:`2045`).
+- Added an explicit warning in :func:`swarmplot` when more than 5% of the points overlap in the "gutters" of the swarm (:pr:`2045`).
 
 - Added the ``axes_dict`` attribute to :class:`FacetGrid` for named access to the component axes (:pr:`2046`).
 
diff --git a/seaborn/_core.py b/seaborn/_core.py
index f4ef0676..18bc33ae 100644
--- a/seaborn/_core.py
+++ b/seaborn/_core.py
@@ -243,7 +243,7 @@ class HueMapping(SemanticMapping):
                 try:
                     cmap = mpl.cm.get_cmap(palette)
                 except (ValueError, TypeError):
-                    err = "Palette {} not understood"
+                    err = f"Palette {palette} not understood"
                     raise ValueError(err)
 
             # Now sort out the data normalization
diff --git a/seaborn/axisgrid.py b/seaborn/axisgrid.py
index b9e2742e..adb2f00c 100644
--- a/seaborn/axisgrid.py
+++ b/seaborn/axisgrid.py
@@ -11,7 +11,7 @@ import matplotlib.pyplot as plt
 
 from ._core import VectorPlotter, variable_type, categorical_order
 from . import utils
-from .utils import _check_argument
+from .utils import _check_argument, adjust_legend_subtitles
 from .palettes import color_palette, blend_palette
 from ._decorators import _deprecate_positional_args
 from ._docstrings import (
@@ -56,7 +56,7 @@ class Grid(object):
         self.fig.tight_layout(*args, **kwargs)
 
     def add_legend(self, legend_data=None, title=None, label_order=None,
-                   **kwargs):
+                   adjust_subtitles=False, **kwargs):
         """Draw a legend, maybe placing it outside axes and resizing the figure.
 
         Parameters
@@ -70,6 +70,9 @@ class Grid(object):
         label_order : list of labels
             The order that the legend entries should appear in. The default
             reads from ``self.hue_names``.
+        adjust_subtitles : bool
+            If True, modify entries with invisible artists to left-align
+            the labels and set the font size to that of a title.
         kwargs : key, value pairings
             Other keyword arguments are passed to the underlying legend methods
             on the Figure or Axes object.
@@ -123,6 +126,9 @@ class Grid(object):
             self._legend = figlegend
             figlegend.set_title(title, prop={"size": title_size})
 
+            if adjust_subtitles:
+                adjust_legend_subtitles(figlegend)
+
             # Draw the plot to set the bounding boxes correctly
             if hasattr(self.fig.canvas, "get_renderer"):
                 self.fig.draw(self.fig.canvas.get_renderer())
@@ -152,6 +158,9 @@ class Grid(object):
             ax = self.axes.flat[0]
             kwargs.setdefault("loc", "best")
 
+            if adjust_subtitles:
+                adjust_legend_subtitles(figlegend)
+
             leg = ax.legend(handles, labels, **kwargs)
             leg.set_title(title, prop={"size": title_size})
             self._legend = leg
diff --git a/seaborn/relational.py b/seaborn/relational.py
index e672e2cf..c2e0d081 100644
--- a/seaborn/relational.py
+++ b/seaborn/relational.py
@@ -11,6 +11,7 @@ from ._core import (
 from .utils import (
     ci_to_errsize,
     locator_to_legend_entries,
+    adjust_legend_subtitles,
     ci as ci_func
 )
 from .algorithms import bootstrap
@@ -156,11 +157,12 @@ seed : int, numpy.random.Generator, or numpy.random.RandomState
     Seed or random number generator for reproducible bootstrapping.
     """,
     legend="""
-legend : "brief", "full", or False
+legend : "auto", "brief", "full", or False
     How to draw the legend. If "brief", numeric ``hue`` and ``size``
     variables will be represented with a sample of evenly spaced values.
-    If "full", every group will get an entry in the legend. If ``False``,
-    no legend data is added and no legend is drawn.
+    If "full", every group will get an entry in the legend. If "auto",
+    choose between brief or full representation based on number of levels.
+    If ``False``, no legend data is added and no legend is drawn.
     """,
     ax_in="""
 ax : matplotlib Axes
@@ -193,14 +195,31 @@ class _RelationalPlotter(VectorPlotter):
     def add_legend_data(self, ax):
         """Add labeled artists to represent the different plot semantics."""
         verbosity = self.legend
-        if verbosity not in ["brief", "full"]:
-            err = "`legend` must be 'brief', 'full', or False"
+        if isinstance(verbosity, str) and verbosity not in ["auto", "brief", "full"]:
+            err = "`legend` must be 'auto', 'brief', 'full', or a boolean."
             raise ValueError(err)
+        elif verbosity is True:
+            verbosity = "auto"
 
         legend_kwargs = {}
         keys = []
 
-        title_kws = dict(color="w", s=0, linewidth=0, marker="", dashes="")
+        # Assign a legend title if there is only going to be one sub-legend,
+        # otherwise, subtitles will be inserted into the texts list with an
+        # invisible handle (which is a hack)
+        titles = {
+            title for title in
+            (self.variables.get(v, None) for v in ["hue", "size", "style"])
+            if title is not None
+        }
+        if len(titles) == 1:
+            legend_title = titles.pop()
+        else:
+            legend_title = ""
+
+        title_kws = dict(
+            visible=False, color="w", s=0, linewidth=0, marker="", dashes=""
+        )
 
         def update(var_name, val_name, **kws):
 
@@ -212,12 +231,19 @@ class _RelationalPlotter(VectorPlotter):
 
                 legend_kwargs[key] = dict(**kws)
 
+        # Define the maximum number of ticks to use for "brief" legends
+        brief_ticks = 6
+
         # -- Add a legend for hue semantics
-        if verbosity == "brief" and self._hue_map.map_type == "numeric":
+        brief_hue = self._hue_map.map_type == "numeric" and (
+            verbosity == "brief"
+            or (verbosity == "auto" and len(self._hue_map.levels) > brief_ticks)
+        )
+        if brief_hue:
             if isinstance(self._hue_map.norm, mpl.colors.LogNorm):
-                locator = mpl.ticker.LogLocator(numticks=3)
+                locator = mpl.ticker.LogLocator(numticks=brief_ticks)
             else:
-                locator = mpl.ticker.MaxNLocator(nbins=3)
+                locator = mpl.ticker.MaxNLocator(nbins=brief_ticks)
             limits = min(self._hue_map.levels), max(self._hue_map.levels)
             hue_levels, hue_formatted_levels = locator_to_legend_entries(
                 locator, limits, self.plot_data["hue"].infer_objects().dtype
@@ -228,7 +254,7 @@ class _RelationalPlotter(VectorPlotter):
             hue_levels = hue_formatted_levels = self._hue_map.levels
 
         # Add the hue semantic subtitle
-        if "hue" in self.variables and self.variables["hue"] is not None:
+        if not legend_title and self.variables.get("hue", None) is not None:
             update((self.variables["hue"], "title"),
                    self.variables["hue"], **title_kws)
 
@@ -239,13 +265,16 @@ class _RelationalPlotter(VectorPlotter):
                 update(self.variables["hue"], formatted_level, color=color)
 
         # -- Add a legend for size semantics
-
-        if verbosity == "brief" and self._size_map.map_type == "numeric":
+        brief_size = self._size_map.map_type == "numeric" and (
+            verbosity == "brief"
+            or (verbosity == "auto" and len(self._size_map.levels) > brief_ticks)
+        )
+        if brief_size:
             # Define how ticks will interpolate between the min/max data values
             if isinstance(self._size_map.norm, mpl.colors.LogNorm):
-                locator = mpl.ticker.LogLocator(numticks=3)
+                locator = mpl.ticker.LogLocator(numticks=brief_ticks)
             else:
-                locator = mpl.ticker.MaxNLocator(nbins=3)
+                locator = mpl.ticker.MaxNLocator(nbins=brief_ticks)
             # Define the min/max data values
             limits = min(self._size_map.levels), max(self._size_map.levels)
             size_levels, size_formatted_levels = locator_to_legend_entries(
@@ -257,7 +286,7 @@ class _RelationalPlotter(VectorPlotter):
             size_levels = size_formatted_levels = self._size_map.levels
 
         # Add the size semantic subtitle
-        if "size" in self.variables and self.variables["size"] is not None:
+        if not legend_title and self.variables.get("size", None) is not None:
             update((self.variables["size"], "title"),
                    self.variables["size"], **title_kws)
 
@@ -275,7 +304,7 @@ class _RelationalPlotter(VectorPlotter):
         # -- Add a legend for style semantics
 
         # Add the style semantic title
-        if "style" in self.variables and self.variables["style"] is not None:
+        if not legend_title and self.variables.get("style", None) is not None:
             update((self.variables["style"], "title"),
                    self.variables["style"], **title_kws)
 
@@ -311,6 +340,7 @@ class _RelationalPlotter(VectorPlotter):
             legend_data[key] = artist
             legend_order.append(key)
 
+        self.legend_title = legend_title
         self.legend_data = legend_data
         self.legend_order = legend_order
 
@@ -522,7 +552,8 @@ class _LinePlotter(_RelationalPlotter):
             self.add_legend_data(ax)
             handles, _ = ax.get_legend_handles_labels()
             if handles:
-                ax.legend()
+                legend = ax.legend(title=self.legend_title)
+                adjust_legend_subtitles(legend)
 
 
 class _ScatterPlotter(_RelationalPlotter):
@@ -625,7 +656,8 @@ class _ScatterPlotter(_RelationalPlotter):
             self.add_legend_data(ax)
             handles, _ = ax.get_legend_handles_labels()
             if handles:
-                ax.legend()
+                legend = ax.legend(title=self.legend_title)
+                adjust_legend_subtitles(legend)
 
 
 @_deprecate_positional_args
@@ -639,7 +671,7 @@ def lineplot(
     dashes=True, markers=None, style_order=None,
     units=None, estimator="mean", ci=95, n_boot=1000, seed=None,
     sort=True, err_style="band", err_kws=None,
-    legend="brief", ax=None, **kwargs
+    legend="auto", ax=None, **kwargs
 ):
 
     variables = _LinePlotter.get_semantics(locals())
@@ -755,7 +787,7 @@ def scatterplot(
     x_bins=None, y_bins=None,
     units=None, estimator=None, ci=95, n_boot=1000,
     alpha=None, x_jitter=None, y_jitter=None,
-    legend="brief", ax=None, **kwargs
+    legend="auto", ax=None, **kwargs
 ):
 
     variables = _ScatterPlotter.get_semantics(locals())
@@ -866,7 +898,7 @@ def relplot(
     palette=None, hue_order=None, hue_norm=None,
     sizes=None, size_order=None, size_norm=None,
     markers=None, dashes=None, style_order=None,
-    legend="brief", kind="scatter",
+    legend="auto", kind="scatter",
     height=5, aspect=1, facet_kws=None,
     units=None,
     **kwargs
@@ -996,7 +1028,9 @@ def relplot(
         p.add_legend_data(g.axes.flat[0])
         if p.legend_data:
             g.add_legend(legend_data=p.legend_data,
-                         label_order=p.legend_order)
+                         label_order=p.legend_order,
+                         title=p.legend_title,
+                         adjust_subtitles=True)
 
     return g
 
diff --git a/seaborn/utils.py b/seaborn/utils.py
index 300e068f..1d181b35 100644
--- a/seaborn/utils.py
+++ b/seaborn/utils.py
@@ -575,6 +575,9 @@ def locator_to_legend_entries(locator, limits, dtype):
     """Return levels and formatted levels for brief numeric legends."""
     raw_levels = locator.tick_values(*limits).astype(dtype)
 
+    # The locator can return ticks outside the limits, clip them here
+    raw_levels = [l for l in raw_levels if l >= limits[0] and l <= limits[1]]
+
     class dummy_axis:
         def get_view_interval(self):
             return limits
@@ -680,7 +683,7 @@ def _assign_default_kwargs(kws, call_func, source_func):
     # the signature of the axes-level function.
     # An alternative would be to  have a decorator on the method that sets its
     # defaults based on those defined in the axes-level function.
-    # Then the figuer-level function would not need to worry about defaults.
+    # Then the figure-level function would not need to worry about defaults.
     # I am not sure which is better.
     needed = inspect.signature(call_func).parameters
     defaults = inspect.signature(source_func).parameters
@@ -690,3 +693,18 @@ def _assign_default_kwargs(kws, call_func, source_func):
             kws[param] = defaults[param].default
 
     return kws
+
+
+def adjust_legend_subtitles(legend):
+    """Make invisible-handle "subtitles" entries look more like titles."""
+    # Legend title not in rcParams until 3.0
+    font_size = plt.rcParams.get("legend.title_fontsize", None)
+    hpackers = legend.findobj(mpl.offsetbox.VPacker)[0].get_children()
+    for hpack in hpackers:
+        draw_area, text_area = hpack.get_children()
+        handles = draw_area.get_children()
+        if not all(artist.get_visible() for artist in handles):
+            draw_area.set_width(0)
+            for text in text_area.get_children():
+                if font_size is not None:
+                    text.set_size(font_size)


Test patch:
diff --git a/ci/deps_latest.txt b/ci/deps_latest.txt
index deeeeecc..a8d32f99 100644
--- a/ci/deps_latest.txt
+++ b/ci/deps_latest.txt
@@ -1,5 +1,5 @@
 numpy
 scipy
-matplotlib!=3.3.1
+matplotlib
 pandas
 statsmodels
diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py
index bb3afbcf..ab3015b1 100644
--- a/seaborn/tests/test_relational.py
+++ b/seaborn/tests/test_relational.py
@@ -706,12 +706,12 @@ class TestRelationalPlotter(Helpers):
 
         g = relplot(data=long_df, x="x", y="y", hue="a")
         texts = [t.get_text() for t in g._legend.texts]
-        expected_texts = np.append(["a"], long_df["a"].unique())
+        expected_texts = long_df["a"].unique()
         assert_array_equal(texts, expected_texts)
 
         g = relplot(data=long_df, x="x", y="y", hue="s", size="s")
         texts = [t.get_text() for t in g._legend.texts]
-        assert_array_equal(texts[1:], np.sort(texts[1:]))
+        assert_array_equal(texts, np.sort(texts))
 
         g = relplot(data=long_df, x="x", y="y", hue="a", legend=False)
         assert g._legend is None
@@ -842,8 +842,8 @@ class TestLinePlotter(Helpers):
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
         colors = [h.get_color() for h in handles]
-        assert labels == ["a"] + p._hue_map.levels
-        assert colors == ["w"] + p._hue_map(p._hue_map.levels)
+        assert labels == p._hue_map.levels
+        assert colors == p._hue_map(p._hue_map.levels)
 
         # --
 
@@ -858,10 +858,10 @@ class TestLinePlotter(Helpers):
         handles, labels = ax.get_legend_handles_labels()
         colors = [h.get_color() for h in handles]
         markers = [h.get_marker() for h in handles]
-        assert labels == ["a"] + p._hue_map.levels
-        assert labels == ["a"] + p._style_map.levels
-        assert colors == ["w"] + p._hue_map(p._hue_map.levels)
-        assert markers == [""] + p._style_map(p._style_map.levels, "marker")
+        assert labels == p._hue_map.levels
+        assert labels == p._style_map.levels
+        assert colors == p._hue_map(p._hue_map.levels)
+        assert markers == p._style_map(p._style_map.levels, "marker")
 
         # --
 
@@ -905,10 +905,10 @@ class TestLinePlotter(Helpers):
         handles, labels = ax.get_legend_handles_labels()
         colors = [h.get_color() for h in handles]
         widths = [h.get_linewidth() for h in handles]
-        assert labels == ["a"] + p._hue_map.levels
-        assert labels == ["a"] + p._size_map.levels
-        assert colors == ["w"] + p._hue_map(p._hue_map.levels)
-        assert widths == [0] + p._size_map(p._size_map.levels)
+        assert labels == p._hue_map.levels
+        assert labels == p._size_map.levels
+        assert colors == p._hue_map(p._hue_map.levels)
+        assert widths == p._size_map(p._size_map.levels)
 
         # --
 
@@ -927,7 +927,7 @@ class TestLinePlotter(Helpers):
         p.legend = "brief"
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
-        assert len(labels) == 4
+        assert len(labels) < len(p._hue_map.levels)
 
         p = _LinePlotter(variables=dict(x=x, y=y, size=z))
 
@@ -941,7 +941,19 @@ class TestLinePlotter(Helpers):
         p.legend = "brief"
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
-        assert len(labels) == 4
+        assert len(labels) < len(p._size_map.levels)
+
+        ax.clear()
+        p.legend = "auto"
+        p.add_legend_data(ax)
+        handles, labels = ax.get_legend_handles_labels()
+        assert len(labels) < len(p._size_map.levels)
+
+        ax.clear()
+        p.legend = True
+        p.add_legend_data(ax)
+        handles, labels = ax.get_legend_handles_labels()
+        assert len(labels) < len(p._size_map.levels)
 
         ax.clear()
         p.legend = "bad_value"
@@ -956,7 +968,17 @@ class TestLinePlotter(Helpers):
         p.map_hue(norm=mpl.colors.LogNorm()),
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
-        assert float(labels[2]) / float(labels[1]) == 10
+        assert float(labels[1]) / float(labels[0]) == 10
+
+        ax.clear()
+        p = _LinePlotter(
+            variables=dict(x=x, y=y, hue=z % 2),
+            legend="auto"
+        )
+        p.map_hue(norm=mpl.colors.LogNorm()),
+        p.add_legend_data(ax)
+        handles, labels = ax.get_legend_handles_labels()
+        assert labels == ["0", "1"]
 
         ax.clear()
         p = _LinePlotter(
@@ -966,7 +988,7 @@ class TestLinePlotter(Helpers):
         p.map_size(norm=mpl.colors.LogNorm())
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
-        assert float(labels[2]) / float(labels[1]) == 10
+        assert float(labels[1]) / float(labels[0]) == 10
 
         ax.clear()
         p = _LinePlotter(
@@ -975,9 +997,9 @@ class TestLinePlotter(Helpers):
             legend="brief",
         )
         p.add_legend_data(ax)
-        expected_levels = ['0.20', '0.24', '0.28', '0.32']
+        expected_labels = ['0.20', '0.22', '0.24', '0.26', '0.28']
         handles, labels = ax.get_legend_handles_labels()
-        assert labels == ["f"] + expected_levels
+        assert labels == expected_labels
 
         ax.clear()
         p = _LinePlotter(
@@ -986,9 +1008,9 @@ class TestLinePlotter(Helpers):
             legend="brief",
         )
         p.add_legend_data(ax)
-        expected_levels = ['0.20', '0.24', '0.28', '0.32']
+        expected_levels = ['0.20', '0.22', '0.24', '0.26', '0.28']
         handles, labels = ax.get_legend_handles_labels()
-        assert labels == ["f"] + expected_levels
+        assert labels == expected_levels
 
     def test_plot(self, long_df, repeated_df):
 
@@ -1362,8 +1384,8 @@ class TestScatterPlotter(Helpers):
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
         colors = [h.get_facecolors()[0] for h in handles]
-        expected_colors = ["w"] + p._hue_map(p._hue_map.levels)
-        assert labels == ["a"] + p._hue_map.levels
+        expected_colors = p._hue_map(p._hue_map.levels)
+        assert labels == p._hue_map.levels
         assert self.colors_equal(colors, expected_colors)
 
         # --
@@ -1378,11 +1400,11 @@ class TestScatterPlotter(Helpers):
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
         colors = [h.get_facecolors()[0] for h in handles]
-        expected_colors = ["w"] + p._hue_map(p._hue_map.levels)
+        expected_colors = p._hue_map(p._hue_map.levels)
         paths = [h.get_paths()[0] for h in handles]
-        expected_paths = [null] + p._style_map(p._style_map.levels, "path")
-        assert labels == ["a"] + p._hue_map.levels
-        assert labels == ["a"] + p._style_map.levels
+        expected_paths = p._style_map(p._style_map.levels, "path")
+        assert labels == p._hue_map.levels
+        assert labels == p._style_map.levels
         assert self.colors_equal(colors, expected_colors)
         assert self.paths_equal(paths, expected_paths)
 
@@ -1424,11 +1446,11 @@ class TestScatterPlotter(Helpers):
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
         colors = [h.get_facecolors()[0] for h in handles]
-        expected_colors = ["w"] + p._hue_map(p._hue_map.levels)
+        expected_colors = p._hue_map(p._hue_map.levels)
         sizes = [h.get_sizes()[0] for h in handles]
-        expected_sizes = [0] + p._size_map(p._size_map.levels)
-        assert labels == ["a"] + p._hue_map.levels
-        assert labels == ["a"] + p._size_map.levels
+        expected_sizes = p._size_map(p._size_map.levels)
+        assert labels == p._hue_map.levels
+        assert labels == p._size_map.levels
         assert self.colors_equal(colors, expected_colors)
         assert sizes == expected_sizes
 
@@ -1445,8 +1467,8 @@ class TestScatterPlotter(Helpers):
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
         sizes = [h.get_sizes()[0] for h in handles]
-        expected_sizes = [0] + p._size_map(p._size_map.levels)
-        assert labels == ["s"] + [str(l) for l in p._size_map.levels]
+        expected_sizes = p._size_map(p._size_map.levels)
+        assert labels == [str(l) for l in p._size_map.levels]
         assert sizes == expected_sizes
 
         # --
@@ -1462,8 +1484,8 @@ class TestScatterPlotter(Helpers):
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
         sizes = [h.get_sizes()[0] for h in handles]
-        expected_sizes = [0] + p._size_map(p._size_map.levels)
-        assert labels == ["s"] + [str(l) for l in p._size_map.levels]
+        expected_sizes = p._size_map(p._size_map.levels)
+        assert labels == [str(l) for l in p._size_map.levels]
         assert sizes == expected_sizes
 
         # --
@@ -1485,7 +1507,7 @@ class TestScatterPlotter(Helpers):
         p.legend = "brief"
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
-        assert len(labels) == 4
+        assert len(labels) < len(p._hue_map.levels)
 
         p = _ScatterPlotter(
             variables=dict(x=x, y=y, size=z),
@@ -1501,7 +1523,7 @@ class TestScatterPlotter(Helpers):
         p.legend = "brief"
         p.add_legend_data(ax)
         handles, labels = ax.get_legend_handles_labels()
-        assert len(labels) == 4
+        assert len(labels) < len(p._size_map.levels)
 
         ax.clear()
         p.legend = "bad_value"
diff --git a/seaborn/tests/test_utils.py b/seaborn/tests/test_utils.py
index d91ec850..e01bc46a 100644
--- a/seaborn/tests/test_utils.py
+++ b/seaborn/tests/test_utils.py
@@ -357,30 +357,30 @@ def test_locator_to_legend_entries():
     levels, str_levels = utils.locator_to_legend_entries(
         locator, limits, float
     )
-    assert str_levels == ["0.00", "0.15", "0.30", "0.45"]
+    assert str_levels == ["0.15", "0.30"]
 
     limits = (0.8, 0.9)
     levels, str_levels = utils.locator_to_legend_entries(
         locator, limits, float
     )
-    assert str_levels == ["0.80", "0.84", "0.88", "0.92"]
+    assert str_levels == ["0.80", "0.84", "0.88"]
 
     limits = (1, 6)
     levels, str_levels = utils.locator_to_legend_entries(locator, limits, int)
-    assert str_levels == ["0", "2", "4", "6"]
+    assert str_levels == ["2", "4", "6"]
 
-    locator = mpl.ticker.LogLocator(numticks=3)
+    locator = mpl.ticker.LogLocator(numticks=5)
     limits = (5, 1425)
     levels, str_levels = utils.locator_to_legend_entries(locator, limits, int)
     if LooseVersion(mpl.__version__) >= "3.1":
-        assert str_levels == ['0', '1', '100', '10000', '1e+06']
+        assert str_levels == ['10', '100', '1000']
 
     limits = (0.00003, 0.02)
     levels, str_levels = utils.locator_to_legend_entries(
         locator, limits, float
     )
     if LooseVersion(mpl.__version__) >= "3.1":
-        assert str_levels == ['1e-07', '1e-05', '1e-03', '1e-01', '10']
+        assert str_levels == ['1e-04', '1e-03', '1e-02']
 
 
 def check_load_dataset(name):