Repository: scipp/plopp

Issue:
Add ylabel on 1d plots if only 1 line is plotted?
Currently only the unit is shown on the ylabel of a 1d plot. If the data has a name, we could display it as long as only one curve is on the graph.

This is already the case for the label on the color bar for 2D plots.

Discussion:

Patch:
diff --git a/src/plopp/backends/matplotlib/canvas.py b/src/plopp/backends/matplotlib/canvas.py
index 04ae1b3..675198e 100644
--- a/src/plopp/backends/matplotlib/canvas.py
+++ b/src/plopp/backends/matplotlib/canvas.py
@@ -11,7 +11,7 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 from ...core.utils import maybe_variable_to_number, scalar_to_string
 from ...graphics.bbox import BoundingBox
-from .utils import fig_to_bytes, is_sphinx_build, make_figure
+from .utils import fig_to_bytes, is_sphinx_build, make_figure, make_legend
 
 
 def _cursor_value_to_variable(x: float, dtype: sc.DType, unit: str) -> sc.Variable:
@@ -150,6 +150,17 @@ class Canvas:
         """
         self.fig.canvas.draw_idle()
 
+    def update_legend(self):
+        """
+        Update the legend on the canvas.
+        """
+        if self._legend:
+            handles, labels = self.ax.get_legend_handles_labels()
+            if len(handles) > 1:
+                self.ax.legend(handles, labels, **make_legend(self._legend))
+            elif (leg := self.ax.get_legend()) is not None:
+                leg.remove()
+
     def save(self, filename: str, **kwargs):
         """
         Save the figure to file.
@@ -192,9 +203,10 @@ class Canvas:
             self._cursor_x_prefix = self.dims['x'] + '='
             self._cursor_y_prefix = self.dims['y'] + '='
         self.ax.format_coord = self.format_coord
+        key = 'y' if 'y' in self.units else 'data'
         self.bbox = BoundingBox(
-            ymin=maybe_variable_to_number(self._user_vmin, unit=self.units.get('y')),
-            ymax=maybe_variable_to_number(self._user_vmax, unit=self.units.get('y')),
+            ymin=maybe_variable_to_number(self._user_vmin, unit=self.units[key]),
+            ymax=maybe_variable_to_number(self._user_vmax, unit=self.units[key]),
         )
 
     def register_format_coord(self, formatter):
@@ -216,7 +228,8 @@ class Canvas:
             The y coordinate of the mouse pointer.
         """
         xstr = _cursor_formatter(x, self.dtypes['x'], self.units['x'])
-        ystr = _cursor_formatter(y, self.dtypes['y'], self.units['y'])
+        key = 'y' if 'y' in self.dtypes else 'data'
+        ystr = _cursor_formatter(y, self.dtypes[key], self.units[key])
         out = f"({self._cursor_x_prefix}{xstr}, {self._cursor_y_prefix}{ystr})"
         if not self._coord_formatters:
             return out
diff --git a/src/plopp/backends/matplotlib/line.py b/src/plopp/backends/matplotlib/line.py
index 4553082..33286d0 100644
--- a/src/plopp/backends/matplotlib/line.py
+++ b/src/plopp/backends/matplotlib/line.py
@@ -14,7 +14,7 @@ from ...core.utils import merge_masks
 from ...graphics.bbox import BoundingBox, axis_bounds
 from ..common import check_ndim, make_line_data
 from .canvas import Canvas
-from .utils import make_legend, parse_dicts_in_kwargs
+from .utils import parse_dicts_in_kwargs
 
 
 def _to_float(x):
@@ -137,11 +137,6 @@ class Line:
                 zorder=10,
                 fmt="none",
             )
-        self.update_legend()
-
-    def update_legend(self) -> None:
-        if self.label and self._canvas._legend:
-            self._ax.legend(**make_legend(self._canvas._legend))
 
     def update(self, new_values: sc.DataArray):
         """
@@ -187,7 +182,6 @@ class Line:
         self._mask.remove()
         if self._error is not None:
             self._error.remove()
-        self.update_legend()
         self._canvas.draw()
 
     @property
diff --git a/src/plopp/backends/matplotlib/scatter.py b/src/plopp/backends/matplotlib/scatter.py
index ed843aa..258164c 100644
--- a/src/plopp/backends/matplotlib/scatter.py
+++ b/src/plopp/backends/matplotlib/scatter.py
@@ -12,7 +12,7 @@ from ...core.utils import merge_masks
 from ...graphics.bbox import BoundingBox, axis_bounds
 from ..common import check_ndim
 from .canvas import Canvas
-from .utils import make_legend, parse_dicts_in_kwargs
+from .utils import parse_dicts_in_kwargs
 
 
 class Scatter:
@@ -88,14 +88,6 @@ class Scatter:
             visible=visible_mask,
         )
 
-        if self._canvas._legend:
-            leg_args = make_legend(self._canvas._legend)
-            if np.shape(s) == np.shape(self._data.coords[self._x].values):
-                handles, labels = self._scatter.legend_elements(prop="sizes")
-                self._ax.legend(handles, labels, title="Sizes", **leg_args)
-            if self.label:
-                self._ax.legend(**leg_args)
-
     def update(self, new_values: sc.DataArray):
         """
         Update the x and y positions of the data points from new data.
diff --git a/src/plopp/backends/plotly/canvas.py b/src/plopp/backends/plotly/canvas.py
index ff37b6e..6a388d9 100644
--- a/src/plopp/backends/plotly/canvas.py
+++ b/src/plopp/backends/plotly/canvas.py
@@ -116,9 +116,10 @@ class Canvas:
         self.dims = dims
         self.units = units
         self.dtypes = dtypes
+        key = 'y' if 'y' in self.units else 'data'
         self.bbox = BoundingBox(
-            ymin=maybe_variable_to_number(self._user_vmin, unit=self.units.get('y')),
-            ymax=maybe_variable_to_number(self._user_vmax, unit=self.units.get('y')),
+            ymin=maybe_variable_to_number(self._user_vmin, unit=self.units[key]),
+            ymax=maybe_variable_to_number(self._user_vmax, unit=self.units[key]),
         )
 
     @property
@@ -303,3 +304,6 @@ class Canvas:
 
     def draw(self):
         pass
+
+    def update_legend(self):
+        pass
diff --git a/src/plopp/backends/pythreejs/canvas.py b/src/plopp/backends/pythreejs/canvas.py
index 14ebe97..fecca1b 100644
--- a/src/plopp/backends/pythreejs/canvas.py
+++ b/src/plopp/backends/pythreejs/canvas.py
@@ -395,3 +395,6 @@ class Canvas:
     @zrange.setter
     def zrange(self, value: tuple[float, float]):
         self.zmin, self.zmax = value
+
+    def update_legend(self):
+        pass
diff --git a/src/plopp/graphics/colormapper.py b/src/plopp/graphics/colormapper.py
index 00739c4..63df59a 100644
--- a/src/plopp/graphics/colormapper.py
+++ b/src/plopp/graphics/colormapper.py
@@ -112,9 +112,8 @@ class ColorMapper:
         # raised when making the colorbar before any call to update is made.
         self.normalizer = _get_normalizer(self.norm)
         self.colorbar = None
-        self.unit = None
-
-        self.name = None
+        self._unit = None
+        self.empty = True
         self.changed = False
         self.artists = {}
         self.widget = None
@@ -221,35 +220,42 @@ class ColorMapper:
         for k in self.artists.keys():
             self.artists[k].set_colors(self.rgba(self.artists[k].data))
 
-    def update(self, *args, **kwargs):
+    def update(self):
         """
-        Update the colorscale bounds taking into account new values,
-        by either supplying a dictionary of new data or by keyword arguments.
-        We also update the colorbar widget if it exists.
+        Update the colors of all artists, if the `self.set_colors_on_update` attribute
+        is set to `True`.
         """
-        new = dict(*args, **kwargs)
-        for data in new.values():
-            if self.name is None:
-                self.name = data.name
-                # If name is None, this is the first time update is called
-                if self.user_vmin is not None:
-                    self.user_vmin = maybe_variable_to_number(
-                        self.user_vmin, unit=self.unit
-                    )
-                if self.user_vmax is not None:
-                    self.user_vmax = maybe_variable_to_number(
-                        self.user_vmax, unit=self.unit
-                    )
-            elif data.name != self.name:
-                self.name = ''
-            if self.cax is not None:
-                text = self.name
-                if self.unit is not None:
-                    text += f'{" " if self.name else ""}[{self.unit}]'
-                self.cax.set_ylabel(text)
         if self.set_colors_on_update:
             self._set_artists_colors()
 
+    @property
+    def unit(self) -> str | None:
+        """
+        Get or set the unit of the colorbar.
+        """
+        return self._unit
+
+    @unit.setter
+    def unit(self, unit: str | None):
+        self._unit = unit
+        if self.user_vmin is not None:
+            self.user_vmin = maybe_variable_to_number(self.user_vmin, unit=self._unit)
+        if self.user_vmax is not None:
+            self.user_vmax = maybe_variable_to_number(self.user_vmax, unit=self._unit)
+
+    @property
+    def ylabel(self) -> str | None:
+        """
+        Get or set the label of the colorbar axis.
+        """
+        if self.cax is not None:
+            return self.cax.get_ylabel()
+
+    @ylabel.setter
+    def ylabel(self, lab: str):
+        if self.cax is not None:
+            self.cax.set_ylabel(lab)
+
     def toggle_norm(self):
         """
         Toggle the norm flag, between `linear` and `log`.
diff --git a/src/plopp/graphics/graphicalview.py b/src/plopp/graphics/graphicalview.py
index 401199a..0bbea7f 100644
--- a/src/plopp/graphics/graphicalview.py
+++ b/src/plopp/graphics/graphicalview.py
@@ -65,6 +65,8 @@ class GraphicalView(View):
         self._repr_format = format
         self.bbox = BoundingBox()
         self.draw_on_update = True
+        self._data_name = None
+        self._data_axis = None
 
         self.canvas = canvas_maker(
             cbar=cbar,
@@ -72,8 +74,6 @@ class GraphicalView(View):
             grid=grid,
             figsize=figsize,
             title=title,
-            vmin=vmin,
-            vmax=vmax,
             legend=legend,
             camera=camera,
             ax=ax,
@@ -129,6 +129,7 @@ class GraphicalView(View):
         new data or by keyword arguments.
         """
         new = dict(*args, **kwargs)
+        need_legend_update = False
         for key, new_values in new.items():
             coords = {}
             for i, direction in enumerate(self._dims):
@@ -143,42 +144,53 @@ class GraphicalView(View):
                     ) from e
 
             if self.canvas.empty:
+                self._data_name = new_values.name
                 axes_units = {k: coord.unit for k, coord in coords.items()}
                 axes_dtypes = {k: coord.dtype for k, coord in coords.items()}
-                if 'y' in self._dims:
-                    self.canvas.ylabel = name_with_unit(
-                        var=coords['y'], name=self._dims['y']
-                    )
-                    if self._dims['y'] in self._scale:
-                        self.canvas.yscale = self._scale[self._dims['y']]
+
+                if set(self._dims) == {'x'}:
+                    axes_units['data'] = new_values.unit
+                    axes_dtypes['data'] = new_values.dtype
+                if self.colormapper is not None:
+                    self.colormapper.unit = new_values.unit
+                    axes_units['data'] = new_values.unit
+                    axes_dtypes['data'] = new_values.dtype
+                    self._data_axis = self.colormapper
                 else:
-                    self.canvas.ylabel = name_with_unit(var=new_values.data, name="")
-                    axes_units['y'] = new_values.unit
-                    axes_dtypes['y'] = new_values.dtype
+                    self._data_axis = self.canvas
 
                 self.canvas.set_axes(
                     dims=self._dims, units=axes_units, dtypes=axes_dtypes
                 )
-                self.canvas.xlabel = name_with_unit(
-                    var=coords['x'], name=self._dims['x']
-                )
-                if self.colormapper is not None:
-                    self.colormapper.unit = new_values.unit
-                if self._dims['x'] in self._scale:
-                    self.canvas.xscale = self._scale[self._dims['x']]
-            else:
-                if self.colormapper is not None:
-                    new_values.data = make_compatible(
-                        new_values.data, unit=self.colormapper.unit
+
+                for xyz, dim in self._dims.items():
+                    setattr(
+                        self.canvas,
+                        f'{xyz}label',
+                        name_with_unit(var=coords[xyz], name=dim),
+                    )
+                    if dim in self._scale:
+                        setattr(self.canvas, f'{xyz}scale', self._scale[dim])
+
+                if self._data_axis is not None:
+                    self._data_axis.ylabel = name_with_unit(
+                        var=new_values.data, name=self._data_name
                     )
+
+            else:
                 for xy, dim in self._dims.items():
                     new_values.coords[dim] = make_compatible(
                         coords[xy], unit=self.canvas.units[xy]
                     )
-                if 'y' not in self._dims:
+                if 'data' in self.canvas.units:
                     new_values.data = make_compatible(
-                        new_values.data, unit=self.canvas.units['y']
+                        new_values.data, unit=self.canvas.units['data']
                     )
+                    if self._data_name and (new_values.name != self._data_name):
+                        self._data_name = None
+                        self._data_axis.ylabel = name_with_unit(
+                            var=sc.scalar(0.0, unit=self.canvas.units['data']), name=''
+                        )
 
             if key not in self.artists:
                 self.artists[key] = self._artist_maker(
@@ -191,10 +203,15 @@ class GraphicalView(View):
                 if self.colormapper is not None:
                     self.colormapper[key] = self.artists[key]
 
+                need_legend_update = getattr(self.artists[key], "label", False)
+
             self.artists[key].update(new_values=new_values)
 
         if self.colormapper is not None:
-            self.colormapper.update(**new)
+            self.colormapper.update()
+
+        if need_legend_update:
+            self.canvas.update_legend()
 
         if self.draw_on_update:
             self.canvas.draw()
@@ -233,4 +250,5 @@ class GraphicalView(View):
         """
         self.artists[key].remove()
         del self.artists[key]
+        self.canvas.update_legend()
         self.autoscale()
diff --git a/src/plopp/plotting/scatter.py b/src/plopp/plotting/scatter.py
index b507ef2..7e80cc6 100644
--- a/src/plopp/plotting/scatter.py
+++ b/src/plopp/plotting/scatter.py
@@ -46,6 +46,7 @@ def scatter(
     vmax: sc.Variable | float = None,
     cbar: bool = False,
     cmap: str = 'viridis',
+    legend: bool | tuple[float, float] = True,
     **kwargs,
 ) -> FigureLike:
     """
@@ -81,6 +82,9 @@ def scatter(
         using the data values in the supplied data array.
     cmap:
         The colormap to be used for the colorscale.
+    legend:
+        Show legend if ``True``. If ``legend`` is a tuple, it should contain the
+        ``(x, y)`` coordinates of the legend's anchor point in axes coordinates.
     **kwargs:
         All other kwargs are forwarded the underlying plotting library.
     """
@@ -102,5 +106,6 @@ def scatter(
         vmax=vmax,
         cmap=cmap,
         cbar=cbar,
+        legend=legend,
         **kwargs,
     )


Test patch:
diff --git a/tests/backends/matplotlib/mpl_plot_test.py b/tests/backends/matplotlib/mpl_plot_test.py
index 1f7f699..ddce55a 100644
--- a/tests/backends/matplotlib/mpl_plot_test.py
+++ b/tests/backends/matplotlib/mpl_plot_test.py
@@ -63,6 +63,36 @@ def test_legend_location():
     assert leg3[0] == leg1[0]
 
 
+def test_no_legend_for_single_artist():
+    da = data_array(ndim=1)
+    da.name = "Velocity"
+    p = pp.plot(da)
+    leg = p.ax.get_legend()
+    assert leg is None
+
+
+def test_legend_is_removed_when_only_one_artist_is_left():
+    a = data_array(ndim=1)
+    b = 2.3 * a
+    f = pp.plot({'a': a, 'b': b})
+    ka, _ = f.view.artists.keys()
+    f.view.remove(ka)
+    assert f.ax.get_legend() is None
+
+
+def test_legend_entry_is_removed_when_artist_is_removed():
+    a = data_array(ndim=1)
+    b = 2.3 * a
+    c = 0.8 * a
+    f = pp.plot({'a': a, 'b': b, 'c': c})
+    ka, _, _ = f.view.artists.keys()
+    f.view.remove(ka)
+    texts = f.ax.get_legend().get_texts()
+    assert len(texts) == 2
+    assert texts[0].get_text() == 'b'
+    assert texts[1].get_text() == 'c'
+
+
 def test_with_string_coord_1d():
     strings = ['a', 'b', 'c', 'd', 'e']
     da = sc.DataArray(
diff --git a/tests/backends/matplotlib/mpl_scatter_test.py b/tests/backends/matplotlib/mpl_scatter_test.py
index c03d021..50adfe9 100644
--- a/tests/backends/matplotlib/mpl_scatter_test.py
+++ b/tests/backends/matplotlib/mpl_scatter_test.py
@@ -4,6 +4,7 @@
 import numpy as np
 import scipp as sc
 
+import plopp as pp
 from plopp.backends.matplotlib.canvas import Canvas
 from plopp.backends.matplotlib.scatter import Scatter
 from plopp.data.testing import scatter as scatter_data
@@ -44,3 +45,33 @@ def test_scatter_update():
     x, y = scat._scatter.get_offsets().T
     assert np.allclose(x, new.coords['x'].values)
     assert np.allclose(y, new.coords['y'].values)
+
+
+def test_scatter_no_legend_for_a_single_artist():
+    da = scatter_data()
+    da.name = "SomeScatterData"
+    fig = pp.scatter(da)
+    leg = fig.ax.get_legend()
+    assert leg is None
+
+
+def test_scatter_has_legend_for_multiple_artists():
+    a = scatter_data(seed=1)
+    b = scatter_data(seed=2)
+    fig = pp.scatter({'a': a, 'b': b})
+    leg = fig.ax.get_legend()
+    assert leg is not None
+    texts = leg.get_texts()
+    assert len(texts) == 2
+    assert texts[0].get_text() == 'a'
+    assert texts[1].get_text() == 'b'
+
+    c = scatter_data(seed=3)
+    fig = pp.scatter({'a': a, 'b': b, 'c': c})
+    leg = fig.ax.get_legend()
+    assert leg is not None
+    texts = leg.get_texts()
+    assert len(texts) == 3
+    assert texts[0].get_text() == 'a'
+    assert texts[1].get_text() == 'b'
+    assert texts[2].get_text() == 'c'
diff --git a/tests/graphics/colormapper_test.py b/tests/graphics/colormapper_test.py
index a23590d..417e43a 100644
--- a/tests/graphics/colormapper_test.py
+++ b/tests/graphics/colormapper_test.py
@@ -41,14 +41,10 @@ def test_creation():
         cmap='magma',
         mask_cmap='jet',
         norm='linear',
-        vmin=sc.scalar(1, unit='K'),
-        vmax=sc.scalar(10, unit='K'),
     )
     assert mapper.cmap.name == 'magma'
     assert mapper.mask_cmap.name == 'jet'
     assert mapper.norm == 'linear'
-    assert sc.identical(mapper.user_vmin, sc.scalar(1, unit='K'))
-    assert sc.identical(mapper.user_vmax, sc.scalar(10, unit='K'))
 
 
 def test_norm():
@@ -62,7 +58,6 @@ def test_autoscale():
     artist = DummyChild(da)
     mapper['data'] = artist
 
-    mapper.update(data=da)
     mapper.autoscale()
     assert mapper.vmin == da.min().value
     assert mapper.vmax == da.max().value
@@ -70,7 +65,6 @@ def test_autoscale():
     # Limits grow
     const = 2.3
     artist.update(da * const)
-    mapper.update(data=da * const)
     mapper.autoscale()
     assert mapper.vmin == (da.min() * const).value
     assert mapper.vmax == (da.max() * const).value
@@ -78,7 +72,6 @@ def test_autoscale():
     # Limits shrink
     const = 0.5
     artist.update(da * const)
-    mapper.update(data=da * const)
     mapper.autoscale()
     assert mapper.vmin == da.min().value * const
     assert mapper.vmax == da.max().value * const
@@ -90,7 +83,6 @@ def test_update_without_autoscale_does_not_change_limits():
     artist = DummyChild(da)
     mapper['data'] = artist
 
-    mapper.update(data=da)
     mapper.autoscale()
     assert mapper.vmin == da.min().value
     assert mapper.vmax == da.max().value
@@ -100,7 +92,7 @@ def test_update_without_autoscale_does_not_change_limits():
     # Limits grow
     const = 2.3
     artist.update(da * const)
-    mapper.update(data=da * const)
+    mapper.update()
     assert mapper.vmin != (da.min() * const).value
     assert mapper.vmin == backup[0]
     assert mapper.vmax != (da.max() * const).value
@@ -109,7 +101,7 @@ def test_update_without_autoscale_does_not_change_limits():
     # Limits shrink
     const = 0.5
     artist.update(da * const)
-    mapper.update(data=da * const)
+    mapper.update()
     assert mapper.vmin != da.min().value * const
     assert mapper.vmin == backup[0]
     assert mapper.vmax != da.max().value * const
@@ -121,7 +113,6 @@ def test_correct_normalizer_limits():
     mapper = ColorMapper()
     artist = DummyChild(da)
     mapper['data'] = artist
-    mapper.update(data=da)
     mapper.autoscale()
     assert mapper.vmin == da.min().value
     assert mapper.vmax == da.max().value
@@ -140,7 +131,7 @@ def test_vmin_vmax():
     mapper = ColorMapper(vmin=vmin, vmax=vmax)
     artist = DummyChild(da)
     mapper['data'] = artist
-    mapper.update(data=da)
+    mapper.unit = 'K'
     mapper.autoscale()
     assert mapper.user_vmin == vmin.value
     assert mapper.user_vmax == vmax.value
@@ -155,7 +146,7 @@ def test_vmin_vmax_no_variable():
     mapper = ColorMapper(vmin=vmin, vmax=vmax)
     artist = DummyChild(da)
     mapper['data'] = artist
-    mapper.update(data=da)
+    mapper.unit = 'K'
     mapper.autoscale()
     assert mapper.user_vmin == vmin
     assert mapper.user_vmax == vmax
@@ -167,7 +158,6 @@ def test_toggle_norm():
     mapper = ColorMapper()
     da = data_array(ndim=2, unit='K')
     mapper['child1'] = DummyChild(da)
-    mapper.update(child1=da)
     mapper.autoscale()
     assert mapper.norm == 'linear'
     assert isinstance(mapper.normalizer, Normalize)
@@ -187,14 +177,12 @@ def test_update_changes_limits():
     artist = DummyChild(da)
     mapper['data'] = artist
 
-    mapper.update(data=da)
     mapper.autoscale()
     assert mapper.normalizer.vmin == da.min().value
     assert mapper.normalizer.vmax == da.max().value
 
     const = 2.3
     artist.update(da * const)
-    mapper.update(data=da * const)
     mapper.autoscale()
     assert mapper.normalizer.vmin == (da.min() * const).value
     assert mapper.normalizer.vmax == (da.max() * const).value
@@ -221,7 +209,6 @@ def test_colorbar_updated_on_rescale():
     key = 'data'
     mapper[key] = artist
 
-    mapper.update(data=da)
     mapper.autoscale()
     _ = mapper.to_widget()
     old_image = mapper.widget.value
@@ -229,21 +216,16 @@ def test_colorbar_updated_on_rescale():
 
     # Update with the same values should not make a new colorbar image
     artist.update(da)
-    mapper.update(data=da)
     mapper.autoscale()
     assert string_similarity(old_image_array, mapper.widget.value) > 0.9
 
     # Update with a smaller range should make a new colorbar image
-    const = 0.8
-    artist.update(da * const)
-    mapper.update(data=da * const)
+    artist.update(da * 0.6)
     mapper.autoscale()
     assert string_similarity(old_image_array, mapper.widget.value) < 0.9
 
     # Update with larger range should make a new colorbar image
-    const = 2.3
-    artist.update(da * const)
-    mapper.update(data=da * const)
+    artist.update(da * 3.1)
     mapper.autoscale()
     assert string_similarity(old_image_array, mapper.widget.value) < 0.9
 
@@ -255,7 +237,6 @@ def test_colorbar_does_not_update_if_no_autoscale():
     key = 'data'
     mapper[key] = artist
 
-    mapper.update(data=da)
     mapper.autoscale()
     _ = mapper.to_widget()
     old_image = mapper.widget.value
@@ -263,19 +244,17 @@ def test_colorbar_does_not_update_if_no_autoscale():
 
     # Update with the same values
     artist.update(da)
-    mapper.update(data=da)
+    mapper.update()
     assert old_image is mapper.widget.value
 
     # Update with a smaller range
-    const = 0.8
-    artist.update(da * const)
-    mapper.update(data=da * const)
+    artist.update(da * 0.8)
+    mapper.update()
     assert old_image is mapper.widget.value
 
     # Update with larger range
-    const = 2.3
-    artist.update(da * const)
-    mapper.update(data=da * const)
+    artist.update(da * 2.3)
+    mapper.update()
     assert old_image_array is mapper.widget.value
 
 
@@ -300,12 +279,11 @@ def test_autoscale_vmin_set():
     artist = DummyChild(da)
     key = 'data'
     mapper[key] = artist
-    mapper.update(data=da)
     mapper.autoscale()
     assert mapper.vmin == -0.5
     assert mapper.vmax == da.max().value
-    # Make sure it handles when da.max() is greater than vmin
-    mapper.update(data=da - sc.scalar(5.0, unit='K'))
+    # Make sure it handles when da.max() is less than vmin
+    artist.update(da - sc.scalar(5.0, unit='K'))
     mapper.autoscale()
     assert mapper.vmin == -0.5
     assert mapper.vmin < mapper.vmax
@@ -317,12 +295,11 @@ def test_autoscale_vmax_set():
     artist = DummyChild(da)
     key = 'data'
     mapper[key] = artist
-    mapper.update(data=da)
     mapper.autoscale()
     assert mapper.vmax == 0.5
     assert mapper.vmin == da.min().value
     # Make sure it handles when da.min() is greater than vmax
-    mapper.update(data=da + sc.scalar(5.0, unit='K'))
+    artist.update(da + sc.scalar(5.0, unit='K'))
     mapper.autoscale()
     assert mapper.vmax == 0.5
     assert mapper.vmin < mapper.vmax
@@ -362,6 +339,14 @@ def test_toolbar_log_norm_button_toggles_colormapper_norm(case):
     assert fig.view.colormapper.norm == 'log'
 
 
+@pytest.mark.parametrize('case', PLOTCASES)
+def test_colorbar_label_has_name_with_one_artist(case):
+    a = case.data(unit='K')
+    a.name = 'A data'
+    fig = case.figure(Node(a))
+    assert fig.view.colormapper.ylabel == 'A data [K]'
+
+
 @pytest.mark.parametrize('case', PLOTCASES)
 def test_colorbar_label_has_no_name_with_multiple_artists(case):
     a = case.data(unit='K')
@@ -369,4 +354,4 @@ def test_colorbar_label_has_no_name_with_multiple_artists(case):
     a.name = 'A data'
     b.name = 'B data'
     fig = case.figure(Node(a), Node(b))
-    assert fig.view.colormapper.cax.get_ylabel() == '[K]'
+    assert fig.view.colormapper.ylabel == '[K]'
diff --git a/tests/plotting/plot_1d_test.py b/tests/plotting/plot_1d_test.py
index cdc546e..6b100cf 100644
--- a/tests/plotting/plot_1d_test.py
+++ b/tests/plotting/plot_1d_test.py
@@ -211,7 +211,7 @@ def test_plot_xarray_data_array_1d():
     p = pp.plot(da)
     assert p.canvas.dims['x'] == 'time'
     assert p.canvas.units['x'] == 'dimensionless'
-    assert p.canvas.units['y'] == 'dimensionless'
+    assert p.canvas.units['data'] == 'dimensionless'
 
 
 def test_plot_xarray_dataset():
@@ -230,7 +230,7 @@ def test_plot_xarray_dataset():
     p = pp.plot(ds)
     assert p.canvas.dims['x'] == 'time'
     assert p.canvas.units['x'] == 'dimensionless'
-    assert p.canvas.units['y'] == 'dimensionless'
+    assert p.canvas.units['data'] == 'dimensionless'
     assert len(p.view.artists) == 2
 
 
@@ -390,6 +390,18 @@ def test_plot_1d_datetime_coord_log_with_mask():
     pp.plot(da, scale={'time': 'log'})
 
 
+def test_plot_1d_datetime_data():
+    t = np.arange(
+        np.datetime64('2017-03-16T20:58:17'), np.datetime64('2017-03-16T21:15:17'), 20
+    )
+    time = sc.array(dims=['time'], values=t)
+    da = sc.DataArray(
+        data=time,
+        coords={'x': sc.arange('time', len(time), unit='m')},
+    )
+    pp.plot(da)
+
+
 def test_plot_1d_data_with_errorbars():
     da = data_array(ndim=1, variances=True)
     p = da.plot()
@@ -490,3 +502,43 @@ def test_1d_plot_does_not_accept_higher_dimension_data_on_update():
         sc.DimensionError, match='Line only accepts data with 1 dimension'
     ):
         fig.update(new=data_array(ndim=3))
+
+
+def test_figure_has_data_name_on_vertical_axis_for_one_curve():
+    da = data_array(ndim=1)
+    da.name = "Velocity"
+    fig = da.plot()
+    ylabel = fig.canvas.ylabel
+    assert da.name in ylabel
+    assert str(da.unit) in ylabel
+
+
+def test_figure_has_data_name_on_vertical_axis_for_dict_with_one_entry():
+    da = data_array(ndim=1)
+    fig = pp.plot({"Velocity": da})
+    ylabel = fig.canvas.ylabel
+    assert da.name in ylabel
+    assert str(da.unit) in ylabel
+
+
+def test_figure_has_only_unit_on_vertical_axis_for_multiple_curves():
+    a = data_array(ndim=1)
+    a.name = "Velocity"
+    b = a * 1.67
+    b.name = "Speed"
+
+    fig = pp.plot({'a': a, 'b': b})
+    ylabel = fig.canvas.ylabel
+    assert str(a.unit) in ylabel
+    assert str(b.unit) in ylabel
+    assert a.name not in ylabel
+    assert b.name not in ylabel
+
+    c = a * 2.5
+    c.name = "Rate"
+    fig = pp.plot({'a': a, 'b': b, 'c': c})
+    ylabel = fig.canvas.ylabel
+    assert str(a.unit) in ylabel
+    assert a.name not in ylabel
+    assert b.name not in ylabel
+    assert c.name not in ylabel
diff --git a/tests/plotting/plot_2d_test.py b/tests/plotting/plot_2d_test.py
index 1edcb2d..214e1e0 100644
--- a/tests/plotting/plot_2d_test.py
+++ b/tests/plotting/plot_2d_test.py
@@ -270,3 +270,28 @@ def test_2d_plot_does_not_accept_data_with_other_dimensionality_on_update():
         sc.DimensionError, match='Image only accepts data with 2 dimension'
     ):
         fig.update(new=data_array(ndim=3))
+
+
+def test_figure_has_data_name_on_colorbar_for_one_image():
+    da = data_array(ndim=2)
+    da.name = "Velocity"
+    fig = da.plot()
+    ylabel = fig.view.colormapper.ylabel
+    assert da.name in ylabel
+    assert str(da.unit) in ylabel
+
+
+def test_figure_has_only_unit_on_colorbar_for_multiple_images():
+    a = data_array(ndim=2)
+    a.name = "Velocity"
+    b = a * 1.67
+    dim = b.dims[-1]
+    b.coords[dim] += b.coords[dim].max() * 1.1
+    b.name = "Speed"
+
+    fig = pp.imagefigure(pp.Node(a), pp.Node(b), cbar=True)
+    ylabel = fig.view.colormapper.ylabel
+    assert str(a.unit) in ylabel
+    assert str(b.unit) in ylabel
+    assert a.name not in ylabel
+    assert b.name not in ylabel
diff --git a/tests/plotting/scatter3d_test.py b/tests/plotting/scatter3d_test.py
index c1f6ca8..08ee91e 100644
--- a/tests/plotting/scatter3d_test.py
+++ b/tests/plotting/scatter3d_test.py
@@ -139,3 +139,25 @@ def test_scatter3d_does_not_accept_data_with_other_dimensionality_on_update():
         sc.DimensionError, match='Scatter3d only accepts data with 1 dimension'
     ):
         fig.update(new=data_array(ndim=3, dim_list="xyzab"))
+
+
+def test_figure_has_data_name_on_colorbar_for_one_set_of_scatter_points():
+    da = scatter()
+    da.name = "MyScatterData"
+    fig = pp.scatter3d(da, cbar=True)
+    ylabel = fig.view.colormapper.ylabel
+    assert da.name in ylabel
+    assert str(da.unit) in ylabel
+
+
+def test_figure_has_only_unit_on_colorbar_for_multiple_sets_of_scatter_points():
+    a = scatter(seed=1)
+    a.name = "A data"
+    b = scatter(seed=2)
+    b.name = "B data"
+    fig = pp.scatter3d({'a': a, 'b': b}, cbar=True)
+    ylabel = fig.view.colormapper.ylabel
+    assert str(a.unit) in ylabel
+    assert str(b.unit) in ylabel
+    assert a.name not in ylabel
+    assert b.name not in ylabel