Repository: scverse/scanpy

Issue:
sc.pl.scatter does not find gene names in adata.raw
When I tried to use `sc.pl.scatter` with `use_raw=True` to plot genes whose names are found in adata.raw, but not adata (because they have been filtered out during processing), I got:

`ValueError: 'x', 'y', and potential 'color' inputs must all come from either '.obs' or '.var'`

Plotting genes found in both adata and adata.raw works. Plotting the troublesome genes with `sc.pl.umap` with `use_raw=True` also works. So it seems like a specific issue with the scatter plot function. Am I overlooking anything? Thanks in advance! 




Discussion:
I'm having the same issue. My inkling is that it's looking in "obs" and "var" but not "var_names" where gene labels are. I'll take a look at it and see if I can fix it when I get a chance.

Resolving patch:
diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py
--- a/scanpy/plotting/_anndata.py
+++ b/scanpy/plotting/_anndata.py
@@ -121,14 +121,18 @@ def scatter(
     If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it.
     """
     args = locals()
+    if _check_use_raw(adata, use_raw):
+        var_index = adata.raw.var.index
+    else:
+        var_index = adata.var.index
     if basis is not None:
         return _scatter_obs(**args)
     if x is None or y is None:
         raise ValueError('Either provide a `basis` or `x` and `y`.')
     if (
-        (x in adata.obs.keys() or x in adata.var.index)
-        and (y in adata.obs.keys() or y in adata.var.index)
-        and (color is None or color in adata.obs.keys() or color in adata.var.index)
+        (x in adata.obs.keys() or x in var_index)
+        and (y in adata.obs.keys() or y in var_index)
+        and (color is None or color in adata.obs.keys() or color in var_index)
     ):
         return _scatter_obs(**args)
     if (


Test patch:
diff --git a/scanpy/tests/_images/scatter_AAAGCCTGGCTAAC-1_vs_AAATTCGATGCACA-1.png b/scanpy/tests/_images/scatter_AAAGCCTGGCTAAC-1_vs_AAATTCGATGCACA-1.png
new file mode 100644
Binary files /dev/null and b/scanpy/tests/_images/scatter_AAAGCCTGGCTAAC-1_vs_AAATTCGATGCACA-1.png differ
diff --git a/scanpy/tests/_images/scatter_HES_percent_mito_n_genes.png b/scanpy/tests/_images/scatter_HES_percent_mito_n_genes.png
new file mode 100644
Binary files /dev/null and b/scanpy/tests/_images/scatter_HES_percent_mito_n_genes.png differ
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
@@ -1215,6 +1215,76 @@ def test_scatter_specify_layer_and_raw():
         sc.pl.umap(pbmc, color="HES4", use_raw=True, layer="layer")
 
 
+def test_scatter_no_basis_per_obs(image_comparer):
+    """Test scatterplot of per-obs points with no basis"""
+    save_and_compare_images = image_comparer(ROOT, FIGS, tol=15)
+    pbmc = sc.datasets.pbmc68k_reduced()
+    sc.pl.scatter(pbmc, x="HES4", y="percent_mito", color="n_genes", use_raw=False)
+    save_and_compare_images("scatter_HES_percent_mito_n_genes")
+
+
+def test_scatter_no_basis_per_var(image_comparer):
+    """Test scatterplot of per-var points with no basis"""
+    save_and_compare_images = image_comparer(ROOT, FIGS, tol=15)
+    pbmc = sc.datasets.pbmc68k_reduced()
+    sc.pl.scatter(pbmc, x="AAAGCCTGGCTAAC-1", y="AAATTCGATGCACA-1", use_raw=False)
+    save_and_compare_images("scatter_AAAGCCTGGCTAAC-1_vs_AAATTCGATGCACA-1")
+
+
+@pytest.fixture
+def pbmc_filtered():
+    pbmc = sc.datasets.pbmc68k_reduced()
+    sc.pp.filter_genes(pbmc, min_cells=10)
+    return pbmc
+
+
+def test_scatter_no_basis_raw(check_same_image, pbmc_filtered, tmpdir):
+    """Test scatterplots of raw layer with no basis."""
+    path1 = tmpdir / "scatter_EGFL7_F12_FAM185A_rawNone.png"
+    path2 = tmpdir / "scatter_EGFL7_F12_FAM185A_rawTrue.png"
+    path3 = tmpdir / "scatter_EGFL7_F12_FAM185A_rawToAdata.png"
+
+    sc.pl.scatter(pbmc_filtered, x='EGFL7', y='F12', color='FAM185A', use_raw=None)
+    plt.savefig(path1)
+    plt.close()
+
+    # is equivalent to:
+    sc.pl.scatter(pbmc_filtered, x='EGFL7', y='F12', color='FAM185A', use_raw=True)
+    plt.savefig(path2)
+    plt.close()
+
+    # and also to:
+    sc.pl.scatter(pbmc_filtered.raw.to_adata(), x='EGFL7', y='F12', color='FAM185A')
+    plt.savefig(path3)
+
+    check_same_image(path1, path2, tol=15)
+    check_same_image(path1, path3, tol=15)
+
+
+@pytest.mark.parametrize(
+    "x,y,color,use_raw",
+    [
+        # test that plotting fails with a ValueError if trying to plot
+        # var_names only found in raw and use_raw is False
+        ('EGFL7', 'F12', 'FAM185A', False),
+        # test that plotting fails if one axis is a per-var value and the
+        # other is a per-obs value
+        ('HES4', 'n_cells', None, None),
+        ('percent_mito', 'AAAGCCTGGCTAAC-1', None, None),
+    ],
+)
+def test_scatter_no_basis_value_error(pbmc_filtered, x, y, color, use_raw):
+    """Test that `scatter()` raises `ValueError` where appropriate
+
+    If `sc.pl.scatter()` receives variable labels that either cannot be
+    found or are incompatible with one another, the function should
+    raise a `ValueError`. This test checks that this happens as
+    expected.
+    """
+    with pytest.raises(ValueError):
+        sc.pl.scatter(pbmc_filtered, x=x, y=y, color=color, use_raw=use_raw)
+
+
 def test_rankings(image_comparer):
     save_and_compare_images = image_comparer(ROOT, FIGS, tol=15)