Repository: pydata/xarray

Issue:
Should imshow() recognise 0-255 images?
#### Code Sample, a copy-pastable example if possible

```python
import os
import urllib.request
import xarray as xr
import matplotlib.pyplot as plt

# Download the file from rasterio's repository
url = 'https://github.com/mapbox/rasterio/raw/master/tests/data/RGB.byte.tif'
urllib.request.urlretrieve(url, 'RGB.byte.tif')

# Read the data
da = xr.open_rasterio('RGB.byte.tif')

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4))
da.plot.imshow(ax=ax1)
(da / 255).plot.imshow(ax=ax2)
plt.tight_layout()
plt.show()

# Delete the file
os.remove('RGB.byte.tif')
```

![figure_1](https://user-images.githubusercontent.com/10050469/35730809-c157c7ba-0813-11e8-8909-24b6d61e685c.png)

#### Problem description

In https://github.com/pydata/xarray/pull/1796, @Zac-HD added support for RGBA images. If an alpha channel is not found, it is added ([code](https://github.com/pydata/xarray/blob/master/xarray/plot/plot.py#L708))

The problem is that adding this alpha channel requires the images to be normalized to 0-1, while plotting an image in 0-255 range without alpha channel works fine in matplotlib. Removing https://github.com/pydata/xarray/blob/master/xarray/plot/plot.py#L708-L715 would solve the problem, but I guess it was added for a reason. 

@Zac-HD , thoughts?




Discussion:
Zac-HD: Ping @fmaussion 
fmaussion: Thanks @Zac-HD ! Again (and sorry for the hassle ;), this needs a regression test. Also, could you change the rasterio example in the docs and see if your change indeed makes the normalisation obsolete? (https://github.com/pydata/xarray/blob/master/doc/gallery/plot_rasterio_rgb.py#L27)
fmaussion: Appart from the comment in the gallery example and the flake8 error, this looks good to me! Thanks @Zac-HD !

Patch:
diff --git a/doc/gallery/plot_rasterio_rgb.py b/doc/gallery/plot_rasterio_rgb.py
index 35c1d044..ec2bbe63 100644
--- a/doc/gallery/plot_rasterio_rgb.py
+++ b/doc/gallery/plot_rasterio_rgb.py
@@ -26,9 +26,6 @@ urllib.request.urlretrieve(url, 'RGB.byte.tif')
 # Read the data
 da = xr.open_rasterio('RGB.byte.tif')
 
-# Normalize the image
-da = da / 255
-
 # The data is in UTM projection. We have to set it manually until
 # https://github.com/SciTools/cartopy/issues/813 is implemented
 crs = ccrs.UTM('18N')
diff --git a/doc/time-series.rst b/doc/time-series.rst
index bdf8b1e7..4d9a9950 100644
--- a/doc/time-series.rst
+++ b/doc/time-series.rst
@@ -129,6 +129,14 @@ the first letters of the corresponding months.
 
 You can use these shortcuts with both Datasets and DataArray coordinates.
 
+In addition, xarray supports rounding operations ``floor``, ``ceil``, and ``round``. These operations require that you supply a `rounding frequency as a string argument.`__
+
+__ http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
+
+.. ipython:: python
+
+    ds['time'].dt.floor('D')
+
 .. _resampling:
 
 Resampling and grouped operations
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index f17bb8f0..115cd9bb 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -79,6 +79,8 @@ Enhancements
   (:pull:`1840`), and keeping float16 and float32 as float32 (:issue:`1842`).
   Correspondingly, encoded variables may also be saved with a smaller dtype.
   By `Zac Hatfield-Dodds <https://github.com/Zac-HD>`_.
+- `.dt` accessor can now ceil, floor and round timestamps to specified frequency.
+  By `Deepak Cherian <https://github.com/dcherian>`_.
 
 .. _Zarr: http://zarr.readthedocs.io/
 
@@ -94,7 +96,7 @@ Bug fixes
 ~~~~~~~~~
 - Added warning in api.py of a netCDF4 bug that occurs when
   the filepath has 88 characters (:issue:`1745`).
-  By `Liam Brannigan <https://github.com/braaannigan>` _.
+  By `Liam Brannigan <https://github.com/braaannigan>`_.
 - Fixed encoding of multi-dimensional coordinates in
   :py:meth:`~Dataset.to_netcdf` (:issue:`1763`).
   By `Mike Neish <https://github.com/neishm>`_.
diff --git a/xarray/core/accessors.py b/xarray/core/accessors.py
index 5052b555..b3e7e1ff 100644
--- a/xarray/core/accessors.py
+++ b/xarray/core/accessors.py
@@ -58,6 +58,43 @@ def _get_date_field(values, name, dtype):
         return _access_through_series(values, name)
 
 
+def _round_series(values, name, freq):
+    """Coerce an array of datetime-like values to a pandas Series and
+    apply requested rounding
+    """
+    values_as_series = pd.Series(values.ravel())
+    method = getattr(values_as_series.dt, name)
+    field_values = method(freq=freq).values
+
+    return field_values.reshape(values.shape)
+
+
+def _round_field(values, name, freq):
+    """Indirectly access pandas rounding functions by wrapping data
+    as a Series and calling through `.dt` attribute.
+
+    Parameters
+    ----------
+    values : np.ndarray or dask.array-like
+        Array-like container of datetime-like values
+    name : str (ceil, floor, round)
+        Name of rounding function
+    freq : a freq string indicating the rounding resolution
+
+    Returns
+    -------
+    rounded timestamps : same type as values
+        Array-like of datetime fields accessed for each element in values
+
+    """
+    if isinstance(values, dask_array_type):
+        from dask.array import map_blocks
+        return map_blocks(_round_series,
+                          values, name, freq=freq, dtype=np.datetime64)
+    else:
+        return _round_series(values, name, freq)
+
+
 class DatetimeAccessor(object):
     """Access datetime fields for DataArrays with datetime-like dtypes.
 
@@ -147,3 +184,58 @@ class DatetimeAccessor(object):
     time = _tslib_field_accessor(
         "time", "Timestamps corresponding to datetimes", object
     )
+
+    def _tslib_round_accessor(self, name, freq):
+        obj_type = type(self._obj)
+        result = _round_field(self._obj.data, name, freq)
+        return obj_type(result, name=name,
+                        coords=self._obj.coords, dims=self._obj.dims)
+
+    def floor(self, freq):
+        '''
+        Round timestamps downward to specified frequency resolution.
+
+        Parameters
+        ----------
+        freq : a freq string indicating the rounding resolution
+            e.g. 'D' for daily resolution
+
+        Returns
+        -------
+        floor-ed timestamps : same type as values
+            Array-like of datetime fields accessed for each element in values
+        '''
+
+        return self._tslib_round_accessor("floor", freq)
+
+    def ceil(self, freq):
+        '''
+        Round timestamps upward to specified frequency resolution.
+
+        Parameters
+        ----------
+        freq : a freq string indicating the rounding resolution
+            e.g. 'D' for daily resolution
+
+        Returns
+        -------
+        ceil-ed timestamps : same type as values
+            Array-like of datetime fields accessed for each element in values
+        '''
+        return self._tslib_round_accessor("ceil", freq)
+
+    def round(self, freq):
+        '''
+        Round timestamps to specified frequency resolution.
+
+        Parameters
+        ----------
+        freq : a freq string indicating the rounding resolution
+            e.g. 'D' for daily resolution
+
+        Returns
+        -------
+        rounded timestamps : same type as values
+            Array-like of datetime fields accessed for each element in values
+        '''
+        return self._tslib_round_accessor("round", freq)
diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py
index d17ceb84..97fee5f0 100644
--- a/xarray/plot/plot.py
+++ b/xarray/plot/plot.py
@@ -710,8 +710,12 @@ def imshow(x, y, z, ax, **kwargs):
         # missing data transparent.  We therefore add an alpha channel if
         # there isn't one, and set it to transparent where data is masked.
         if z.shape[-1] == 3:
-            z = np.ma.concatenate((z, np.ma.ones(z.shape[:2] + (1,))), 2)
-        z = z.copy()
+            alpha = np.ma.ones(z.shape[:2] + (1,), dtype=z.dtype)
+            if np.issubdtype(z.dtype, np.integer):
+                alpha *= 255
+            z = np.ma.concatenate((z, alpha), axis=2)
+        else:
+            z = z.copy()
         z[np.any(z.mask, axis=-1), -1] = 0
 
     primitive = ax.imshow(z, **defaults)


Test patch:
diff --git a/xarray/tests/test_accessors.py b/xarray/tests/test_accessors.py
index 30ea1a88..1fcde8f5 100644
--- a/xarray/tests/test_accessors.py
+++ b/xarray/tests/test_accessors.py
@@ -57,6 +57,9 @@ class TestDatetimeAccessor(TestCase):
         months = self.times_data.dt.month
         hours = self.times_data.dt.hour
         days = self.times_data.dt.day
+        floor = self.times_data.dt.floor('D')
+        ceil = self.times_data.dt.ceil('D')
+        round = self.times_data.dt.round('D')
 
         dask_times_arr = da.from_array(self.times_arr, chunks=(5, 5, 50))
         dask_times_2d = xr.DataArray(dask_times_arr,
@@ -67,6 +70,9 @@ class TestDatetimeAccessor(TestCase):
         dask_month = dask_times_2d.dt.month
         dask_day = dask_times_2d.dt.day
         dask_hour = dask_times_2d.dt.hour
+        dask_floor = dask_times_2d.dt.floor('D')
+        dask_ceil = dask_times_2d.dt.ceil('D')
+        dask_round = dask_times_2d.dt.round('D')
 
         # Test that the data isn't eagerly evaluated
         assert isinstance(dask_year.data, da.Array)
@@ -86,6 +92,9 @@ class TestDatetimeAccessor(TestCase):
         assert_equal(months, dask_month.compute())
         assert_equal(days, dask_day.compute())
         assert_equal(hours, dask_hour.compute())
+        assert_equal(floor, dask_floor.compute())
+        assert_equal(ceil, dask_ceil.compute())
+        assert_equal(round, dask_round.compute())
 
     def test_seasons(self):
         dates = pd.date_range(start="2000/01/01", freq="M", periods=12)
@@ -95,3 +104,14 @@ class TestDatetimeAccessor(TestCase):
         seasons = xr.DataArray(seasons)
 
         assert_array_equal(seasons.values, dates.dt.season.values)
+
+    def test_rounders(self):
+        dates = pd.date_range("2014-01-01", "2014-05-01", freq='H')
+        xdates = xr.DataArray(np.arange(len(dates)),
+                              dims=['time'], coords=[dates])
+        assert_array_equal(dates.floor('D').values,
+                           xdates.time.dt.floor('D').values)
+        assert_array_equal(dates.ceil('D').values,
+                           xdates.time.dt.ceil('D').values)
+        assert_array_equal(dates.round('D').values,
+                           xdates.time.dt.round('D').values)
diff --git a/xarray/tests/test_distributed.py b/xarray/tests/test_distributed.py
index 1d450ff5..47bb6cdc 100644
--- a/xarray/tests/test_distributed.py
+++ b/xarray/tests/test_distributed.py
@@ -81,4 +81,4 @@ def test_async(c, s, a, b):
     assert not dask.is_dask_collection(w)
     assert_allclose(x + 10, w)
 
-    assert s.task_state
+    assert s.tasks
diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py
index 1573577a..479b1dce 100644
--- a/xarray/tests/test_plot.py
+++ b/xarray/tests/test_plot.py
@@ -1146,6 +1146,13 @@ class TestImshow(Common2dMixin, PlotTestCase):
         for kwds in [dict(vmax=-1, vmin=-1.2), dict(vmin=2, vmax=2.1)]:
             da.plot.imshow(**kwds)
 
+    def test_imshow_rgb_values_in_valid_range(self):
+        da = DataArray(np.arange(75, dtype='uint8').reshape((5, 5, 3)))
+        _, ax = plt.subplots()
+        out = da.plot.imshow(ax=ax).get_array()
+        assert out.dtype == np.uint8
+        assert (out[..., :3] == da.values).all()  # Compare without added alpha
+
 
 class TestFacetGrid(PlotTestCase):
     def setUp(self):