Repository: scipp/plopp

Issue:
Better error message when plotting binned data
A key UX difference to Scipp's plotting is that automatic histogramming was removed. This is however totally unclear from the error message. Given that we recommend migration and will soon make Plopp the default we should provide a better error message, including an example of how to fix it (use `hist`).

Example:

```python
import scipp as sc
import plopp

plopp.plot(sc.data.binned_x(100, 10))
```
gives
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [24], line 3
      1 import plopp
----> 3 plopp.plot(sc.data.binned_x(100, 10))

File ~/code/scipp/plopp/src/plopp/functions/plot.py:109, in plot(obj, aspect, cbar, crop, errorbars, grid, ignore_size, mask_color, norm, scale, title, vmin, vmax, figsize, **kwargs)
    107 ndim = ndims.pop()
    108 if ndim == 1:
--> 109     return figure1d(*[input_node(da) for da in data_arrays],
    110                     errorbars=errorbars,
    111                     mask_color=mask_color,
    112                     **common_args,
    113                     **kwargs)
    114 elif ndim == 2:
    115     return figure2d(*[input_node(da) for da in data_arrays],
    116                     aspect=aspect,
    117                     cbar=cbar,
   (...)
    120                         **kwargs
    121                     })

File ~/code/scipp/plopp/src/plopp/functions/figure.py:17, in figure1d(*args, **kwargs)
     15 else:
     16     from ..graphics import StaticFig1d
---> 17     return StaticFig1d(*args, **kwargs)

File ~/code/scipp/plopp/src/plopp/graphics/fig1d.py:96, in Figure1d.__init__(self, norm, vmin, vmax, scale, errorbars, mask_color, aspect, grid, crop, title, figsize, ax, format, *nodes, **kwargs)
     86 self.canvas = Canvas(cbar=False,
     87                      aspect=aspect,
     88                      grid=grid,
   (...)
     92                      vmin=vmin,
     93                      vmax=vmax)
     94 self.canvas.yscale = norm
---> 96 self.render()
     97 self.canvas.autoscale()
     98 if crop is not None:

File ~/code/scipp/plopp/src/plopp/graphics/basefig.py:55, in BaseFig.render(self)
     53 for node in self.graph_nodes.values():
     54     new_values = node.request_data()
---> 55     self.update(new_values=new_values, key=node.id, draw=False)

File ~/code/scipp/plopp/src/plopp/graphics/fig1d.py:122, in Figure1d.update(self, new_values, key, draw)
    118     raise ValueError("Figure1d can only be used to plot 1-D data.")
    120 if key not in self.artists:
--> 122     line = Line(canvas=self.canvas,
    123                 data=new_values,
    124                 number=len(self.artists),
    125                 errorbars=self._errorbars,
    126                 mask_color=self._mask_color,
    127                 **self._kwargs)
    128     self.artists[key] = line
    129     if line.label:

File ~/code/scipp/plopp/src/plopp/graphics/line.py:65, in Line.__init__(self, canvas, data, number, **kwargs)
     62     if key in args:
     63         args[alias] = args.pop(key)
---> 65 self._make_line(data=self._make_data(), number=number, **args)

File ~/code/scipp/plopp/src/plopp/graphics/line.py:185, in Line._make_data(self)
    181     one_mask = merge_masks(self._data.masks).values
    182     data["mask"] = {
    183         "y": np.where(one_mask, data["values"]["y"], None).astype(np.float32)
    184     }
--> 185 return self._preprocess_hist(data)

File ~/code/scipp/plopp/src/plopp/graphics/line.py:164, in Line._preprocess_hist(self, data)
    162 hist = len(x) != len(y)
    163 if hist:
--> 164     data["values"]["y"] = np.concatenate((y[0:1], y))
    165     if data["mask"] is not None:
    166         data["mask"]["y"] = np.concatenate(
    167             (data["mask"]["y"][0:1], data["mask"]["y"]))

TypeError: __getitem__(): incompatible function arguments. The following argument types are supported:
    1. (self: scipp._scipp.core.ElementArrayView_bin_DataArray, arg0: int) -> scipp._scipp.core.DataArray

Invoked with: [<scipp.DataArray>
Dimensions: Sizes[row:12, ]
Coordinates:
  x                         float64              [m]  (row)  [0.0601387, 0.0601373, ..., 0.0338069, 0.0670407]
  y                         float64              [m]  (row)  [0.728827, 0.556402, ..., 0.00843672, 0.266437]
  z                         float64              [m]  (row)  [0.0469184, 4.61286e-05, ..., 0.367852, 0.26365]
Data:
                            float64              [K]  (row)  [1.00044, 1.09189, ..., 1.0173, 1.05255]

, <scipp.DataArray>
Dimensions: Sizes[row:7, ]
Coordinates:
  x                         float64              [m]  (row)  [0.118091, 0.172066, ..., 0.13441, 0.136524]
  y                         float64              [m]  (row)  [0.76057, 0.127554, ..., 0.981099, 0.72708]
  z                         float64              [m]  (row)  [0.0989363, 0.98037, ..., 0.462533, 0.049891]
Data:
                            float64              [K]  (row)  [1.07883, 1.01681, ..., 1.06678, 1.01892]

, ..., <scipp.DataArray>
Dimensions: Sizes[row:15, ]
Coordinates:
  x                         float64              [m]  (row)  [0.863621, 0.863758, ..., 0.873824, 0.823521]
  y                         float64              [m]  (row)  [0.279595, 0.948835, ..., 0.216893, 0.741137]
  z                         float64              [m]  (row)  [0.235477, 0.0280613, ..., 0.432808, 0.454869]
Data:
                            float64              [K]  (row)  [1.03548, 1.02456, ..., 1.08424, 1.01219]

, <scipp.DataArray>
Dimensions: Sizes[row:12, ]
Coordinates:
  x                         float64              [m]  (row)  [0.9767, 0.923246, ..., 0.956514, 0.947199]
  y                         float64              [m]  (row)  [0.29784, 0.301757, ..., 0.0903527, 0.941554]
  z                         float64              [m]  (row)  [0.747273, 0.809356, ..., 0.443394, 0.313959]
Data:
                            float64              [K]  (row)  [1.05724, 1.07197, ..., 1.09963, 1.03879]

], slice(0, 1, None)
```

Discussion:

Patch:
diff --git a/src/plopp/functions/common.py b/src/plopp/functions/common.py
index d5072d1..71f7b0a 100644
--- a/src/plopp/functions/common.py
+++ b/src/plopp/functions/common.py
@@ -66,6 +66,16 @@ def _check_size(da: DataArray):
                          "default. To bypass this check, use `ignore_size=True`.")
 
 
+def check_not_binned(obj):
+    if obj.bins is not None:
+        params = ', '.join([f'{dim}=100' for dim in obj.dims])
+        raise ValueError(
+            "Cannot plot binned data, it must be histogrammed first, "
+            f"e.g., using ``obj.hist()`` or obj.hist({params})``."
+            "See https://scipp.github.io/generated/functions/scipp.hist.html for "
+            "more details.")
+
+
 def preprocess(obj: Union[ndarray, Variable, DataArray],
                crop: Dict[str, Dict[str, Variable]] = None,
                name: str = '',
@@ -90,6 +100,7 @@ def preprocess(obj: Union[ndarray, Variable, DataArray],
         Do not perform a size check on the object before plotting it.
     """
     out = _to_data_array(obj)
+    check_not_binned(out)
     if not out.name:
         out.name = name
     crop = {} if crop is None else crop
diff --git a/src/plopp/functions/scatter3d.py b/src/plopp/functions/scatter3d.py
index 5e65dde..81366d8 100644
--- a/src/plopp/functions/scatter3d.py
+++ b/src/plopp/functions/scatter3d.py
@@ -2,6 +2,7 @@
 # Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
 
 from ..core import input_node
+from .common import check_not_binned
 
 import scipp as sc
 from typing import Literal, Tuple, Union
@@ -65,6 +66,8 @@ def scatter3d(da: sc.DataArray,
     from ..functions import figure3d
     from ..widgets import Box, ToggleTool, TriCutTool
 
+    check_not_binned(da)
+
     if pos is not None:
         if any((x, y, z)):
             raise ValueError(f'If pos ({pos}) is defined, all of '


Test patch:
diff --git a/tests/functions/inspector_test.py b/tests/functions/inspector_test.py
new file mode 100644
index 0000000..f277afb
--- /dev/null
+++ b/tests/functions/inspector_test.py
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
+import pytest
+import scipp as sc
+import plopp as pp
+
+
+@pytest.mark.skip(reason="Requires widget backend")
+def test_raises_ValueError_when_given_binned_data():
+    da = sc.data.table_xyz(100).bin(x=10, y=20, z=30)
+    with pytest.raises(ValueError, match='Cannot plot binned data'):
+        pp.inspector(da, orientation='vertical')
diff --git a/tests/functions/plot_test.py b/tests/functions/plot_test.py
index 3bb3221..b3e26f4 100644
--- a/tests/functions/plot_test.py
+++ b/tests/functions/plot_test.py
@@ -216,3 +216,9 @@ def test_kwarg_as_dict():
     p = pp.plot({'a': da, 'b': 0.2 * da}, color={'a': 'red', 'b': 'black'})
     assert _get_line(p, 0).get_color() == 'red'
     assert _get_line(p, 1).get_color() == 'black'
+
+
+def test_raises_ValueError_when_given_binned_data():
+    da = sc.data.table_xyz(100).bin(x=10, y=20)
+    with pytest.raises(ValueError, match='Cannot plot binned data'):
+        pp.plot(da)
diff --git a/tests/functions/scatter3d_test.py b/tests/functions/scatter3d_test.py
index fd4bdca..3dafc5d 100644
--- a/tests/functions/scatter3d_test.py
+++ b/tests/functions/scatter3d_test.py
@@ -40,3 +40,12 @@ def test_scatter3d_dimensions_are_flattened():
                       coords={'position': sc.geometry.position(x, y, z)})
     p = pp.scatter3d(da, pos="position")
     assert list(p.children[0].artists.values())[0].data.ndim == 1
+
+
+def test_raises_ValueError_when_given_binned_data():
+    nx = 10
+    da = sc.data.table_xyz(100).bin(x=nx)
+    x = sc.linspace(dim='x', start=-10.0, stop=10.0, num=nx, unit='m')
+    da.coords['position'] = sc.geometry.position(x, x, x)
+    with pytest.raises(ValueError, match='Cannot plot binned data'):
+        pp.scatter3d(da, pos='position')
diff --git a/tests/functions/slicer_test.py b/tests/functions/slicer_test.py
index cfe9811..6fefb62 100644
--- a/tests/functions/slicer_test.py
+++ b/tests/functions/slicer_test.py
@@ -72,3 +72,9 @@ def test_with_mismatching_data_arrays_raises():
     b.coords['xx'] *= 1.1
     with pytest.raises(sc.DatasetError):
         _ = Slicer({'a': a, 'b': b}, keep=['xx'])
+
+
+def test_raises_ValueError_when_given_binned_data():
+    da = sc.data.table_xyz(100).bin(x=10, y=20)
+    with pytest.raises(ValueError, match='Cannot plot binned data'):
+        Slicer(da, keep=['x'])
diff --git a/tests/functions/superplot_test.py b/tests/functions/superplot_test.py
index 7053171..30093ed 100644
--- a/tests/functions/superplot_test.py
+++ b/tests/functions/superplot_test.py
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 # Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
+import pytest
 
 from plopp.data.testing import data_array
 from plopp.functions.superplot import Superplot
@@ -65,3 +66,9 @@ def test_change_line_color():
     line_id = list(tool._lines.keys())[-1]
     tool.change_line_color(change={'new': '#000000'}, line_id=line_id)
     assert tool._lines[line_id]['line'].color == '#000000'
+
+
+def test_raises_ValueError_when_given_binned_data():
+    da = sc.data.table_xyz(100).bin(x=10, y=20)
+    with pytest.raises(ValueError, match='Cannot plot binned data'):
+        Superplot(da, keep='x')