Repository: rasterio/rasterio

Issue:
Plot: extents from array plus transform 
Currently, when passing an array into the `rasterio.plot.show`, there is no way to set the extent axis labels. (`with_bounds` is only available with an open dataset).

A common use case is plotting an intermediate result for which you have an array and affine but no open dataset. Would be nice to have the following produce axes in spatial coordinates

```
show(array, transform=aff)  # would imply with_bounds=True
```



Discussion:

Resolving patch:
diff --git a/rasterio/plot.py b/rasterio/plot.py
--- a/rasterio/plot.py
+++ b/rasterio/plot.py
@@ -14,7 +14,7 @@
 
 import rasterio
 from rasterio._io import RasterReader
-
+from rasterio.transform import guard_transform
 from rasterio.compat import zip_longest
 
 logger = logging.getLogger(__name__)
@@ -34,7 +34,7 @@ def get_plt():
 
 
 def show(source, with_bounds=True, contour=False, contour_label_kws=None,
-         ax=None, title=None, **kwargs):
+         ax=None, title=None, transform=None, **kwargs):
     """Display a raster or raster band using matplotlib.
 
     Parameters
@@ -58,6 +58,8 @@ def show(source, with_bounds=True, contour=False, contour_label_kws=None,
         Axis to plot on, otherwise uses current axis.
     title : str, optional
         Title for the figure.
+    transform : Affine, optional
+        Defines the affine transform if source is an array
     **kwargs : key, value pairings optional
         These will be passed to the matplotlib imshow or contour method
         depending on contour argument.
@@ -100,6 +102,8 @@ def show(source, with_bounds=True, contour=False, contour_label_kws=None,
             arr = reshape_as_image(source)
         else:
             arr = source
+        if transform and with_bounds:
+            kwargs['extent'] = plotting_extent(arr, transform)
 
     show = False
     if not ax:
@@ -130,17 +134,29 @@ def show(source, with_bounds=True, contour=False, contour_label_kws=None,
     return ax
 
 
-def plotting_extent(source):
+def plotting_extent(source, transform=None):
     """Returns an extent in the format needed
      for matplotlib's imshow (left, right, bottom, top)
      instead of rasterio's bounds (left, bottom, top, right)
 
     Parameters
     ----------
-    source : raster dataset
+    source : raster dataset or array in image order (see reshape_as_image)
+    transform: Affine, required if source is array
     """
-    extent = (source.bounds.left, source.bounds.right,
-              source.bounds.bottom, source.bounds.top)
+    if hasattr(source, 'bounds'):
+        extent = (source.bounds.left, source.bounds.right,
+                  source.bounds.bottom, source.bounds.top)
+    elif not transform:
+        raise ValueError(
+            "transform is required if source is an array")
+    else:
+        transform = guard_transform(transform)
+        rows, cols = source.shape[0:2]
+        left, top = transform * (0, 0)
+        right, bottom = transform * (cols, rows)
+        extent = (left, right, bottom, top)
+
     return extent
 
 
@@ -216,7 +232,7 @@ def show_hist(source, bins=10, masked=True, title='Histogram', ax=None, **kwargs
         arr = arr.reshape(arr.shape[0], -1).T
         colors = ['red', 'green', 'blue', 'violet', 'gold', 'saddlebrown']
 
-    #The goal is to provide a curated set of colors for working with
+    # The goal is to provide a curated set of colors for working with
     # smaller datasets and let matplotlib define additional colors when
     # working with larger datasets.
     if arr.shape[-1] > len(colors):
@@ -241,11 +257,11 @@ def show_hist(source, bins=10, masked=True, title='Histogram', ax=None, **kwargs
     fig = ax.get_figure()
 
     ax.hist(arr,
-             bins=bins,
-             color=colors,
-             label=labels,
-             range=rng,
-             **kwargs)
+            bins=bins,
+            color=colors,
+            label=labels,
+            range=rng,
+            **kwargs)
 
     ax.legend(loc="upper right")
     ax.set_title(title, fontweight='bold')


Test patch:
diff --git a/tests/test_plot.py b/tests/test_plot.py
--- a/tests/test_plot.py
+++ b/tests/test_plot.py
@@ -10,7 +10,7 @@
     plt = None
 
 import rasterio
-from rasterio.plot import show, show_hist, get_plt
+from rasterio.plot import show, show_hist, get_plt, plotting_extent
 from rasterio.enums import ColorInterp
 
 @pytest.mark.skipif(plt is None,
@@ -66,8 +66,7 @@ def test_show_cmyk_interp(tmpdir):
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_raster_no_bounds():
     """
     This test only verifies that code up to the point of plotting with
@@ -81,8 +80,7 @@ def test_show_raster_no_bounds():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_raster_title():
     """
     This test only verifies that code up to the point of plotting with
@@ -96,8 +94,7 @@ def test_show_raster_title():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_hist_large():
     """
     This test only verifies that code up to the point of plotting with
@@ -111,8 +108,7 @@ def test_show_hist_large():
     except ImportError:
         pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_raster_cmap():
     """
     This test only verifies that code up to the point of plotting with
@@ -126,8 +122,7 @@ def test_show_raster_cmap():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_raster_ax():
     """
     This test only verifies that code up to the point of plotting with
@@ -142,8 +137,7 @@ def test_show_raster_ax():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_array():
     """
     This test only verifies that code up to the point of plotting with
@@ -157,8 +151,7 @@ def test_show_array():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_array3D():
     """
     This test only verifies that code up to the point of plotting with
@@ -172,8 +165,7 @@ def test_show_array3D():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_hist():
     """
     This test only verifies that code up to the point of plotting with
@@ -202,8 +194,7 @@ def test_show_hist():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_hist_mplargs():
     """
     This test only verifies that code up to the point of plotting with
@@ -218,8 +209,7 @@ def test_show_hist_mplargs():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_contour():
     """
     This test only verifies that code up to the point of plotting with
@@ -233,8 +223,7 @@ def test_show_contour():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_show_contour_mplargs():
     """
     This test only verifies that code up to the point of plotting with
@@ -250,15 +239,33 @@ def test_show_contour_mplargs():
         except ImportError:
             pass
 
-@pytest.mark.skipif(plt is None,
-                    reason="requires matplotlib")
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
 def test_get_plt():
     """
     This test only verifies that code up to the point of plotting with
     matplotlib works correctly.  Tests do not exercise matplotlib.
     """
-    with rasterio.open('tests/data/RGB.byte.tif') as src:
+    with rasterio.open('tests/data/RGB.byte.tif'):
         try:
             assert plt == get_plt()
         except ImportError:
-            pass
\ No newline at end of file
+            pass
+
+@pytest.mark.skipif(plt is None, reason="requires matplotlib")
+def test_plt_transform():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        show(src.read(), transform=src.transform)
+        show(src.read(1), transform=src.transform)
+
+def test_plotting_extent():
+    from rasterio.plot import reshape_as_image
+    expected = (101985.0, 339315.0, 2611485.0, 2826915.0)
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert plotting_extent(src) == expected
+        assert plotting_extent(
+            reshape_as_image(src.read()), transform=src.affine) == expected
+        assert plotting_extent(
+            src.read(1), transform=src.transform) == expected
+        # array requires a transform
+        with pytest.raises(ValueError):
+            plotting_extent(src.read(1))