Repository: scverse/scanpy

Issue:
Plotting triggering copy of view
Example using scanpy 9dd2e94846aa and anndata `762fdb924e757cdd758231`

```python
import scanpy as sc

pbmc = sc.datasets.pbmc3k_processed()
sc.pl.umap(pbmc, color="louvain")  # To make sure that "louvain_colors" has been made
bcells = pbmc[pbmc.obs["louvain"] == "B cells"]
# This line triggers a copy being made:
sc.pl.umap(bcells)
# /Users/isaac/github/anndata/anndata/_core/anndata.py:1120: ImplicitModificationWarning: # Initializing view as actual.
#   "Initializing view as actual.", ImplicitModificationWarning,
assert not bcells.is_view
```

Pretty sure that shouldn't be making a copy, since nothing should be modified in the view. To make sure:

```python
from anndata.tests.helpers import assert_equal

bcells_view =  pbmc[pbmc.obs["louvain"] == "B cells"]
assert_equal(bcells, bcells_view, exact=True)
```

This also seems to be happening with some of the other plotting functions, like `sc.pl.rank_genes_groups_dotplot`.

Elaborating a bit:

To me this is an issue since it will use quite a lot of memory for cases where it isn't needed. Why copy a large number of arrays when you don't need to? 


Discussion:

Resolving patch:
diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py
--- a/scanpy/plotting/_utils.py
+++ b/scanpy/plotting/_utils.py
@@ -320,7 +320,8 @@ def _validate_palette(adata, key):
                 _palette = None
                 break
         _palette.append(color)
-    if _palette is not None:
+    # Don't modify if nothing changed
+    if (_palette is not None and list(_palette) != list(adata.uns[color_key])):
         adata.uns[color_key] = _palette
 
 


Test patch:
diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py
--- a/scanpy/tests/test_plotting.py
+++ b/scanpy/tests/test_plotting.py
@@ -770,3 +770,35 @@ def test_paga(image_comparer):
         pbmc, basis='X_pca', legend_fontweight='normal', threshold=0.5, show=False,
     )
     save_and_compare_images('master_paga_compare_pca')
+
+
+def test_no_copy():
+    # https://github.com/theislab/scanpy/issues/1000
+    # Tests that plotting functions don't make a copy from a view unless they
+    # actually have to
+    actual = sc.datasets.pbmc68k_reduced()
+    sc.pl.umap(actual, color=["bulk_labels", "louvain"], show=False)  # Set colors
+
+    view = actual[np.random.choice(actual.obs_names, size=actual.shape[0] // 5), :]
+
+    sc.pl.umap(view, color=["bulk_labels", "louvain"], show=False)
+    assert view.is_view
+
+    rank_genes_groups_plotting_funcs = [
+        sc.pl.rank_genes_groups,
+        sc.pl.rank_genes_groups_dotplot,
+        sc.pl.rank_genes_groups_heatmap,
+        sc.pl.rank_genes_groups_matrixplot,
+        sc.pl.rank_genes_groups_stacked_violin,
+        # TODO: raises ValueError about empty distance matrix – investigate
+        # sc.pl.rank_genes_groups_tracksplot,
+        sc.pl.rank_genes_groups_violin,
+    ]
+
+    # Only plotting one group at a time to avoid generating dendrogram
+    # TODO: Generating a dendrogram modifies the object, this should be
+    # optional and also maybe not modify the object.
+    for plotfunc in rank_genes_groups_plotting_funcs:
+        view = actual[actual.obs["bulk_labels"] == "Dendritic"]
+        plotfunc(view, ["Dendritic"], show=False)
+        assert view.is_view