Repository: scipp/plopp

Issue:
Wrong 1-D plots from mapping to items with distinct dimensions
Example 1:
```python
import scipp as sc

x = sc.arange('x', 0, 5, 0.5)
y = sc.arange('y',0, 12,0.7)
dg = sc.Dataset({'a':x,'b':y})
dg.coords['x'] = x
dg.coords['y'] = y
dg.plot()
```
Creates a plot with "y" labeling the axis.

Example2:
```python
import scipp as sc

x = sc.arange('x', 0, 5, 0.5)
y = sc.arange('y',0, 12,0.7)
dg = sc.Dataset({'a':x,'b':y})
dg.plot()
```
Similar problem for items without coords.

Discussion:

Patch:
diff --git a/src/plopp/core/utils.py b/src/plopp/core/utils.py
index 834a70f..36e3377 100644
--- a/src/plopp/core/utils.py
+++ b/src/plopp/core/utils.py
@@ -3,7 +3,7 @@
 
 from functools import reduce
 import scipp as sc
-from typing import Dict, Union
+from typing import Dict, Union, Optional
 import uuid
 
 
@@ -171,3 +171,34 @@ def coord_element_to_string(x: sc.Variable) -> str:
     if x.unit is not None:
         out += f" [{x.unit}]"
     return out
+
+
+def make_compatible(x: sc.Variable,
+                    *,
+                    unit: Union[str, None],
+                    dim: Optional[str] = None):
+    """
+    Raise exception if the dimensions of the supplied variable do not contain the
+    requested dimension.
+    Then, if the variable unit differs from the requested unit, attempt a conversion.
+
+    Parameters
+    ----------
+    x:
+        The input variable.
+    unit:
+        Attempt unit conversion to this unit if the input variable has a different unit.
+    dim:
+        If supplied, check that the dim is found in the input variable's dimensions.
+    """
+    if (dim is not None) and (dim not in x.dims):
+        raise sc.DimensionError(
+            f'The supplied variable has dimension {x.dims} which is '
+            f'incompatible with the requested dimension {dim}.')
+    if x.unit != unit:
+        if unit is not None:
+            return x.to(unit=unit, dtype=float)
+        else:
+            raise sc.UnitError(f'The supplied variable has unit {x.unit} which is '
+                               'incompatible with the requested unit None/<no unit>.')
+    return x
diff --git a/src/plopp/graphics/canvas3d.py b/src/plopp/graphics/canvas3d.py
index 4b5cac2..6b4280b 100644
--- a/src/plopp/graphics/canvas3d.py
+++ b/src/plopp/graphics/canvas3d.py
@@ -29,6 +29,9 @@ class Canvas3d:
 
         import pythreejs as p3
 
+        self.xunit = None
+        self.yunit = None
+        self.zunit = None
         self.outline = None
         self.axticks = None
         self.figsize = figsize
diff --git a/src/plopp/graphics/colormapper.py b/src/plopp/graphics/colormapper.py
index 227e8d0..0536922 100644
--- a/src/plopp/graphics/colormapper.py
+++ b/src/plopp/graphics/colormapper.py
@@ -206,8 +206,9 @@ class ColorMapper:
         key:
             The id of the node that provided this data.
         """
-        if self.unit is None:
-            self.unit = data.unit
+        if self.name is None:
+            if self.unit is None:
+                self.unit = data.unit
             self.name = data.name
             if self.cax is not None:
                 self.cax.set_ylabel(f'{self.name} [{self.unit}]')
diff --git a/src/plopp/graphics/fig1d.py b/src/plopp/graphics/fig1d.py
index cee03f2..d0210de 100644
--- a/src/plopp/graphics/fig1d.py
+++ b/src/plopp/graphics/fig1d.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
 
-from ..core.utils import name_with_unit
+from ..core.utils import name_with_unit, make_compatible
 from .basefig import BaseFig
 from .canvas import Canvas
 from .line import Line
@@ -117,6 +117,18 @@ class Figure1d(BaseFig):
         if new_values.ndim != 1:
             raise ValueError("Figure1d can only be used to plot 1-D data.")
 
+        dim = new_values.dim
+        coord = new_values.coords[dim]
+        if not self.dims:
+            self.dims['x'] = dim
+            self.canvas.xunit = coord.unit
+            self.canvas.yunit = new_values.unit
+        else:
+            new_values.data = make_compatible(new_values.data, unit=self.canvas.yunit)
+            new_values.coords[dim] = make_compatible(coord,
+                                                     dim=self.dims['x'],
+                                                     unit=self.canvas.xunit)
+
         if key not in self.artists:
 
             line = Line(canvas=self.canvas,
@@ -128,10 +140,7 @@ class Figure1d(BaseFig):
             self.artists[key] = line
             if line.label:
                 self.canvas.legend()
-            self.dims['x'] = new_values.dim
 
-            self.canvas.xunit = new_values.meta[new_values.dim].unit
-            self.canvas.yunit = new_values.unit
             self.canvas.xlabel = name_with_unit(var=new_values.meta[self.dims['x']])
             self.canvas.ylabel = name_with_unit(var=new_values.data, name="")
 
diff --git a/src/plopp/graphics/fig2d.py b/src/plopp/graphics/fig2d.py
index 6b3507a..dc87e72 100644
--- a/src/plopp/graphics/fig2d.py
+++ b/src/plopp/graphics/fig2d.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
 
-from ..core.utils import name_with_unit
+from ..core.utils import name_with_unit, make_compatible
 from .basefig import BaseFig
 from .canvas import Canvas
 from .colormapper import ColorMapper
@@ -128,6 +128,25 @@ class Figure2d(BaseFig):
         if new_values.ndim != 2:
             raise ValueError("Figure2d can only be used to plot 2-D data.")
 
+        xdim = new_values.dims[1]
+        xcoord = new_values.coords[xdim]
+        ydim = new_values.dims[0]
+        ycoord = new_values.coords[ydim]
+        if not self.dims:
+            self.dims.update({"x": xdim, "y": ydim})
+            self.canvas.xunit = xcoord.unit
+            self.canvas.yunit = ycoord.unit
+            self.colormapper.unit = new_values.unit
+        else:
+            new_values.data = make_compatible(new_values.data,
+                                              unit=self.colormapper.unit)
+            new_values.coords[xdim] = make_compatible(xcoord,
+                                                      dim=self.dims['x'],
+                                                      unit=self.canvas.xunit)
+            new_values.coords[ydim] = make_compatible(ycoord,
+                                                      dim=self.dims['y'],
+                                                      unit=self.canvas.yunit)
+
         self.colormapper.update(data=new_values, key=key)
 
         if key not in self.artists:
@@ -139,6 +158,7 @@ class Figure2d(BaseFig):
 
             self.canvas.xunit = new_values.meta[new_values.dims[1]].unit
             self.canvas.yunit = new_values.meta[new_values.dims[0]].unit
+
             self.canvas.xlabel = name_with_unit(var=new_values.meta[self.dims['x']])
             self.canvas.ylabel = name_with_unit(var=new_values.meta[self.dims['y']])
             if self.dims['x'] in self._scale:
diff --git a/src/plopp/graphics/fig3d.py b/src/plopp/graphics/fig3d.py
index bb4101b..c177dcd 100644
--- a/src/plopp/graphics/fig3d.py
+++ b/src/plopp/graphics/fig3d.py
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
 
+from ..core.utils import make_compatible
 from .basefig import BaseFig
 from .canvas3d import Canvas3d
 from .colormapper import ColorMapper
@@ -95,6 +96,25 @@ class Figure3d(BaseFig):
             This argument is ignored for the 3d figure update.
         """
 
+        xcoord = new_values.coords[self._x]
+        ycoord = new_values.coords[self._y]
+        zcoord = new_values.coords[self._z]
+        if not self.dims:
+            self.dims.update({'x': self._x, 'y': self._y, 'z': self._z})
+            self.canvas.xunit = xcoord.unit
+            self.canvas.yunit = ycoord.unit
+            self.canvas.zunit = zcoord.unit
+            self.colormapper.unit = new_values.unit
+        else:
+            new_values.data = make_compatible(new_values.data,
+                                              unit=self.colormapper.unit)
+            new_values.coords[self._x] = new_values.coords[self._x].to(
+                unit=self.canvas.xunit, copy=False)
+            new_values.coords[self._y] = new_values.coords[self._y].to(
+                unit=self.canvas.yunit, copy=False)
+            new_values.coords[self._z] = new_values.coords[self._z].to(
+                unit=self.canvas.zunit, copy=False)
+
         self.colormapper.update(data=new_values, key=key)
 
         if key not in self.artists:


Test patch:
diff --git a/tests/graphics/fig1d_test.py b/tests/graphics/fig1d_test.py
index 00d616d..d103545 100644
--- a/tests/graphics/fig1d_test.py
+++ b/tests/graphics/fig1d_test.py
@@ -194,3 +194,60 @@ def test_save_to_disk(ext):
         fname = os.path.join(path, f'plopp_fig1d.{ext}')
         fig.save(filename=fname)
         assert os.path.isfile(fname)
+
+
+def test_raises_for_new_data_with_incompatible_dimension():
+    x = data_array(ndim=1)
+    y = x.rename(xx='yy')
+    with pytest.raises(sc.DimensionError):
+        Figure1d(input_node(x), input_node(y))
+
+
+def test_raises_for_new_data_with_incompatible_unit():
+    a = data_array(ndim=1)
+    b = a * a
+    with pytest.raises(sc.UnitError):
+        Figure1d(input_node(a), input_node(b))
+
+
+def test_raises_for_new_data_with_incompatible_coord_unit():
+    a = data_array(ndim=1)
+    b = a.copy()
+    b.coords['xx'] = a.coords['xx'] * a.coords['xx']
+    with pytest.raises(sc.UnitError):
+        Figure1d(input_node(a), input_node(b))
+
+
+def test_converts_new_data_units():
+    a = data_array(ndim=1, unit='m')
+    b = data_array(ndim=1, unit='cm')
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure1d(anode, bnode)
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    assert sc.identical(fig.artists[bnode.id]._data, b.to(unit='m'))
+
+
+def test_converts_new_data_coordinate_units():
+    a = data_array(ndim=1)
+    b = data_array(ndim=1)
+    b.coords['xx'].unit = 'cm'
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure1d(anode, bnode)
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    c = b.copy()
+    c.coords['xx'] = c.coords['xx'].to(unit='m')
+    assert sc.identical(fig.artists[bnode.id]._data, c)
+
+
+def test_converts_new_data_units_integers():
+    a = sc.DataArray(data=sc.array(dims=['x'], values=[1, 2, 3, 4, 5], unit='m'),
+                     coords={'x': sc.arange('x', 5., unit='s')})
+    b = sc.DataArray(data=sc.array(dims=['x'], values=[10, 20, 30, 40, 50], unit='cm'),
+                     coords={'x': sc.arange('x', 5., unit='s')})
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure1d(anode, bnode)
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    assert sc.identical(fig.artists[bnode.id]._data, b.to(unit='m', dtype=float))
diff --git a/tests/graphics/fig2d_test.py b/tests/graphics/fig2d_test.py
index 8e04901..4b5e7d9 100644
--- a/tests/graphics/fig2d_test.py
+++ b/tests/graphics/fig2d_test.py
@@ -212,3 +212,48 @@ def test_save_to_disk(ext):
         fname = os.path.join(path, f'plopp_fig2d.{ext}')
         fig.save(filename=fname)
         assert os.path.isfile(fname)
+
+
+def test_raises_for_new_data_with_incompatible_dimension():
+    a = data_array(ndim=2)
+    b = a.rename(xx='zz')
+    with pytest.raises(sc.DimensionError):
+        Figure2d(input_node(a), input_node(b))
+
+
+def test_raises_for_new_data_with_incompatible_unit():
+    a = data_array(ndim=2)
+    b = a * a
+    with pytest.raises(sc.UnitError):
+        Figure2d(input_node(a), input_node(b))
+
+
+def test_raises_for_new_data_with_incompatible_coord_unit():
+    a = data_array(ndim=2)
+    b = a.copy()
+    b.coords['xx'] = a.coords['xx'] * a.coords['xx']
+    with pytest.raises(sc.UnitError):
+        Figure2d(input_node(a), input_node(b))
+
+
+def test_converts_new_data_units():
+    a = data_array(ndim=2, unit='m')
+    b = data_array(ndim=2, unit='cm')
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure2d(anode, bnode)
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    assert sc.identical(fig.artists[bnode.id]._data, b.to(unit='m'))
+
+
+def test_converts_new_data_coordinate_units():
+    a = data_array(ndim=2)
+    b = data_array(ndim=2)
+    b.coords['xx'].unit = 'cm'
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure2d(anode, bnode)
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    c = b.copy()
+    c.coords['xx'] = c.coords['xx'].to(unit='m')
+    assert sc.identical(fig.artists[bnode.id]._data, c)
diff --git a/tests/graphics/fig3d_test.py b/tests/graphics/fig3d_test.py
index 5994498..918d864 100644
--- a/tests/graphics/fig3d_test.py
+++ b/tests/graphics/fig3d_test.py
@@ -6,6 +6,7 @@ from plopp.graphics.fig3d import Figure3d
 from plopp.graphics.point_cloud import PointCloud
 from plopp import input_node
 import scipp as sc
+import pytest
 
 
 def test_creation():
@@ -30,3 +31,53 @@ def test_log_norm():
     da = scatter()
     fig = Figure3d(input_node(da), x='x', y='y', z='z', norm='log')
     assert fig.colormapper.norm == 'log'
+
+
+def test_raises_for_new_data_with_incompatible_coordinate():
+    a = scatter()
+    b = scatter()
+    b.coords['t'] = b.coords.pop('x')
+    with pytest.raises(KeyError):
+        Figure3d(input_node(a), input_node(b), x='x', y='y', z='z')
+
+
+def test_raises_for_new_data_with_incompatible_unit():
+    a = scatter()
+    b = a * a
+    with pytest.raises(sc.UnitError):
+        Figure3d(input_node(a), input_node(b), x='x', y='y', z='z')
+
+
+def test_raises_for_new_data_with_incompatible_coord_unit():
+    a = scatter()
+    b = a.copy()
+    b.coords['x'] = a.coords['x'] * a.coords['x']
+    with pytest.raises(sc.UnitError):
+        Figure3d(input_node(a), input_node(b), x='x', y='y', z='z')
+
+
+def test_converts_new_data_units():
+    a = scatter()
+    a.unit = 'm'
+    b = scatter()
+    b.unit = 'cm'
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure3d(anode, bnode, x='x', y='y', z='z')
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    assert sc.identical(fig.artists[bnode.id]._data, b.to(unit='m'))
+
+
+def test_converts_new_data_coordinate_units():
+    a = scatter()
+    b = scatter()
+    xcoord = b.coords['x'].copy()
+    xcoord.unit = 'cm'
+    b.coords['x'] = xcoord
+    anode = input_node(a)
+    bnode = input_node(b)
+    fig = Figure3d(anode, bnode, x='x', y='y', z='z')
+    assert sc.identical(fig.artists[anode.id]._data, a)
+    c = b.copy()
+    c.coords['x'] = c.coords['x'].to(unit='m')
+    assert sc.identical(fig.artists[bnode.id]._data, c)