Repository: mwaskom/seaborn

Issue:
Handle order in legend changes when labels are renamed
Here is a minimal example. In the second and third example the legend handles are reversed, which I think is unwanted behaviour:
```python
penguins = sns.load_dataset("penguins")
ax = sns.histplot(data=penguins, x="flipper_length_mm", hue="species")
```
![issue0](https://user-images.githubusercontent.com/49990506/195336548-0be477c8-6199-4538-b6ac-b0451b7b1592.png)
```python
ax = sns.histplot(data=penguins, x="flipper_length_mm", hue="species")
ax.legend(["Adelie", "Chinstrap", "Gentoo"])
```
![issue1](https://user-images.githubusercontent.com/49990506/195336552-552918ad-6824-4ec9-9bef-7e4df4ae1438.png)
```python
ax = sns.histplot(data=penguins, x="flipper_length_mm", hue="species")
sns.move_legend(ax, 'best', labels=["Adelie", "Chinstrap", "Gentoo"])
```
![issue2](https://user-images.githubusercontent.com/49990506/195336554-560167cb-e718-48f4-a417-10fcc8fb5b3b.png)
My workaround inspired by https://github.com/mwaskom/seaborn/issues/2280 is to also give the handles
```python
ax = sns.histplot(data=penguins, x="flipper_length_mm", hue="species")
sns.move_legend(ax, 'best', handles=ax.legend_.legendHandles, labels=["Adelie", "Chinstrap", "Gentoo"])
```
I don't understand why this works as `sns.move_legend` according to  https://github.com/mwaskom/seaborn/issues/2280 does exactly the same. 
I could reproduce the same also with `sns.scatterplot`, so it must be a general problem. 
I could only test this on version 11.2

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

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/mwaskom/seaborn/pull/3454/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://app.codecov.io/gh/mwaskom/seaborn/pull/3454?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    #3454   +/-   ##
=======================================
  Coverage   98.32%   98.32%           
=======================================
  Files          75       75           
  Lines       24388    24408   +20     
=======================================
+ Hits        23980    24000   +20     
  Misses        408      408           
```


| [Files Changed](https://app.codecov.io/gh/mwaskom/seaborn/pull/3454?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) | Coverage Δ | |
|---|---|---|
| [seaborn/utils.py](https://app.codecov.io/gh/mwaskom/seaborn/pull/3454?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-c2VhYm9ybi91dGlscy5weQ==) | `94.13% <100.00%> (+0.07%)` | :arrow_up: |
| [tests/test\_utils.py](https://app.codecov.io/gh/mwaskom/seaborn/pull/3454?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-dGVzdHMvdGVzdF91dGlscy5weQ==) | `98.95% <100.00%> (+0.04%)` | :arrow_up: |

</details>

Patch:
diff --git a/seaborn/utils.py b/seaborn/utils.py
index c5acc5e2..7cc804a0 100644
--- a/seaborn/utils.py
+++ b/seaborn/utils.py
@@ -460,6 +460,13 @@ def move_legend(obj, loc, **kwargs):
     handles = get_legend_handles(old_legend)
     labels = [t.get_text() for t in old_legend.get_texts()]
 
+    # Handle the case where the user is trying to override the labels
+    if (new_labels := kwargs.pop("labels", None)) is not None:
+        if len(new_labels) != len(labels):
+            err = "Length of new labels does not match existing legend."
+            raise ValueError(err)
+        labels = new_labels
+
     # Extract legend properties that can be passed to the recreation method
     # (Vexingly, these don't all round-trip)
     legend_kws = inspect.signature(mpl.legend.Legend).parameters


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a892418d..98c6f48d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -20,7 +20,7 @@ from pandas.testing import (
     assert_frame_equal,
 )
 
-from seaborn import utils, rcmod
+from seaborn import utils, rcmod, scatterplot
 from seaborn.utils import (
     get_dataset_names,
     get_color_cycle,
@@ -430,6 +430,28 @@ def test_move_legend_input_checks():
         utils.move_legend(ax.figure, "best")
 
 
+def test_move_legend_with_labels(long_df):
+
+    order = long_df["a"].unique()
+    labels = [s.capitalize() for s in order]
+    ax = scatterplot(long_df, x="x", y="y", hue="a", hue_order=order)
+
+    handles_before = get_legend_handles(ax.get_legend())
+    colors_before = [tuple(h.get_facecolor().squeeze()) for h in handles_before]
+    utils.move_legend(ax, "best", labels=labels)
+    _draw_figure(ax.figure)
+
+    texts = [t.get_text() for t in ax.get_legend().get_texts()]
+    assert texts == labels
+
+    handles_after = get_legend_handles(ax.get_legend())
+    colors_after = [tuple(h.get_facecolor().squeeze()) for h in handles_after]
+    assert colors_before == colors_after
+
+    with pytest.raises(ValueError, match="Length of new labels"):
+        utils.move_legend(ax, "best", labels=labels[:-1])
+
+
 def check_load_dataset(name):
     ds = load_dataset(name, cache=False)
     assert isinstance(ds, pd.DataFrame)