Repository: holoviz/hvplot

Issue:
move scalar coords to title in hvplot.xarray
#### Is your feature request related to a problem? Please describe.
Upstreaming xarray .plot() methods to hvplot.xarray

#### Describe the solution you'd like
an xarray.Dataset with one data variable:
`ds["data_var"].plot()`
to match
`ds,hvplot()`

#### Describe alternatives you've considered


#### Additional context
```
import xarray as xr
import hvplot.xarray

ds = xr.tutorial.open_dataset("air_temperature").isel(lat=0, lon=0)
ds["air"].plot()
```

<img width="425" alt="Screen Shot 2021-09-06 at 10 40 38 PM" src="https://user-images.githubusercontent.com/17162724/132275954-827286c0-81c9-4cd1-943a-cfe6f393905e.png">

```
ds.hvplot()
```
<img width="721" alt="Screen Shot 2021-09-06 at 10 41 41 PM" src="https://user-images.githubusercontent.com/17162724/132276030-07e2cd2a-8019-4079-bfab-78a1ead377de.png">






Discussion:
Hi @raybellwaves!

Thanks for raising this, hvplot would indeed benefit from being more coherent here with the output obtained from xarray's plot API. I'll address it soon.

Resolving patch:
diff --git a/hvplot/converter.py b/hvplot/converter.py
--- a/hvplot/converter.py
+++ b/hvplot/converter.py
@@ -498,6 +498,7 @@ def __init__(self, data, x, y, kind=None, by=None, use_index=True,
             plot_opts['global_extent'] = global_extent
         if projection:
             plot_opts['projection'] = process_crs(projection)
+        title = title if title is not None else getattr(self, '_title', None)
         if title is not None:
             plot_opts['title'] = title
         if (self.kind in self._colorbar_types or self.rasterize or self.datashade or self._color_dim):
@@ -654,6 +655,9 @@ def _process_data(self, kind, data, x, y, by, groupby, row, col,
         elif is_xarray(data):
             import xarray as xr
             z = kwds.get('z')
+            if isinstance(data, xr.Dataset):
+                if len(data.data_vars) == 0:
+                    raise ValueError("Cannot plot an empty xarray.Dataset object.")
             if z is None:
                 if isinstance(data, xr.Dataset):
                     z = list(data.data_vars)[0]
@@ -703,6 +707,15 @@ def _process_data(self, kind, data, x, y, by, groupby, row, col,
 
             if groupby:
                 groupby = [g for g in groupby if g not in grid]
+
+            # Add a title to hvplot.xarray plots that displays scalar coords values,
+            # as done by xarray.plot()  
+            if not groupby and not grid:
+                if isinstance(da, xr.DataArray):
+                    self._title = da._title_for_slice()
+                elif isinstance(da, xr.Dataset):
+                    self._title = partial(xr.DataArray._title_for_slice, da)()
+                        
             self.data = data
         else:
             raise ValueError('Supplied data type %s not understood' % type(data).__name__)


Test patch:
diff --git a/hvplot/tests/testoptions.py b/hvplot/tests/testoptions.py
--- a/hvplot/tests/testoptions.py
+++ b/hvplot/tests/testoptions.py
@@ -1,5 +1,7 @@
 from unittest import SkipTest, expectedFailure
 
+import numpy as np
+
 from parameterized import parameterized
 
 from holoviews import Store
@@ -298,3 +300,87 @@ def test_kde_opts(self):
         self.assertEqual(opts.kwargs['bandwidth'], 0.2)
         self.assertEqual(opts.kwargs['cut'], 1)
         self.assertEqual(opts.kwargs['filled'], True)
+
+class TestXarrayTitle(ComparisonTestCase):
+
+    def setUp(self):
+        try:
+            import xarray as xr
+        except:
+            raise SkipTest('Xarray not available')
+        self.backend = 'bokeh'
+        hv.extension(self.backend)
+        Store.current_backend = self.backend
+        self.store_copy = OptionTree(sorted(Store.options().items()),
+                                     groups=Options._option_groups)
+        import hvplot.xarray   # noqa
+        self.da = xr.DataArray(
+            data=np.arange(16).reshape((2, 2, 2, 2)),
+            coords={'time': [0, 1], 'y': [0, 1], 'x': [0, 1], 'band': [0, 1]},
+            dims=['time', 'y', 'x', 'band'],
+            name='test',
+        )
+        da2 = xr.DataArray(
+            data=np.arange(27).reshape((3, 3, 3)),
+            coords={'y': [0, 1, 2], 'x': [0, 1, 2]},
+            dims=['y', 'x', 'other'],
+            name='test2'
+        )
+        self.ds1 = xr.Dataset(dict(foo=self.da))
+        self.ds2 = xr.Dataset(dict(foo=self.da, bar=da2))
+
+    def tearDown(self):
+        Store.options(val=self.store_copy)
+        Store._custom_options = {k:{} for k in Store._custom_options.keys()}
+        super(TestXarrayTitle, self).tearDown()
+
+    def test_dataarray_2d_with_title(self):
+        da_sel = self.da.sel(time=0, band=0)
+        plot = da_sel.hvplot()  # Image plot
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'time = 0, band = 0')
+
+    def test_dataarray_1d_with_title(self):
+        da_sel = self.da.sel(time=0, band=0, x=0)
+        plot = da_sel.hvplot()  # Line plot
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'time = 0, x = 0, band = 0')
+
+    def test_dataarray_1d_and_by_with_title(self):
+        da_sel = self.da.sel(time=0, band=0, x=[0, 1])
+        plot = da_sel.hvplot(by='x')  # Line plot with hue/by
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'time = 0, band = 0')
+
+    def test_override_title(self):
+        da_sel = self.da.sel(time=0, band=0)
+        plot = da_sel.hvplot(title='title')  # Imege plot
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'title')
+
+    def test_dataarray_4d_line_no_title(self):
+        plot = self.da.hvplot.line(dynamic=False)  # Line plot with widgets
+        opts = Store.lookup_options('bokeh', plot.last, 'plot')
+        self.assertNotIn('title', opts.kwargs)
+
+    def test_dataarray_3d_histogram_with_title(self):
+        da_sel = self.da.sel(time=0)
+        plot = da_sel.hvplot()  # Histogram and no widgets
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'time = 0')
+
+    def test_dataset_empty_raises(self):
+        with self.assertRaisesRegex(ValueError, 'empty xarray.Dataset'):
+            self.ds1.drop('foo').hvplot()
+
+    def test_dataset_one_var_behaves_like_dataarray(self):
+        ds_sel = self.ds1.sel(time=0, band=0)
+        plot = ds_sel.hvplot()  # Image plot
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'time = 0, band = 0')
+
+    def test_dataset_scatter_with_title(self):
+        ds_sel = self.ds2.sel(time=0, band=0, x=0, y=0)
+        plot = ds_sel.hvplot.scatter(x='foo', y='bar')  # Image plot
+        opts = Store.lookup_options('bokeh', plot, 'plot')
+        self.assertEqual(opts.kwargs['title'], 'y = 0, x = 0, time = 0, band = 0')