Repository: pyqtgraph/pyqtgraph

Issue:
HDF5Exporter: throws error when curves aren't the same length
When trying to save data from a graph as an hdf5 file, the HDF5Exporter throws and error when you have multiple curves with differing lengths. This looks to be because the numpy.array(data).astype('double') can't handle lists with different lengths. Below is a traceback from the error. This occurs when trying to save data from the "Multiple curves" graph in the "Basic Plotting" example.

````
Traceback (most recent call last):
  File "/home/jchrist/PycharmProjects/lib/python3.5/site-packages/pyqtgraph/exporters/Exporter.py", line 77, in fileSaveFinished
    self.export(fileName=fileName, **self.fileDialog.opts)
  File "/home/jchrist/PycharmProjects/lib/python3.5/site-packages/pyqtgraph/exporters/HDF5Exporter.py", line 55, in export
    fdata = numpy.array(data).astype('double')
ValueError: setting an array element with a sequence.
````


Discussion:

Resolving patch:
diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py
--- a/pyqtgraph/exporters/HDF5Exporter.py
+++ b/pyqtgraph/exporters/HDF5Exporter.py
@@ -44,20 +44,27 @@ def export(self, fileName=None):
         data = []
 
         appendAllX = self.params['columnMode'] == '(x,y) per plot'
-        #print dir(self.item.curves[0])
-        tlen = 0
-        for i, c in enumerate(self.item.curves):
-            d = c.getData()
-            if i > 0 and len(d[0]) != tlen:
-                raise ValueError ("HDF5 Export requires all curves in plot to have same length")
-            if appendAllX or i == 0:
-                data.append(d[0])
-                tlen = len(d[0])
-            data.append(d[1])
+        # Check if the arrays are ragged
+        len_first = len(self.item.curves[0].getData()[0]) if self.item.curves[0] else None
+        ragged = any(len(i.getData()[0]) != len_first for i in self.item.curves)
 
+        if ragged:
+            dgroup = fd.create_group(dsname)
+            for i, c in enumerate(self.item.curves):
+                d = c.getData()
+                fdata = numpy.array([d[0], d[1]]).astype('double')
+                cname = c.name() if c.name() is not None else str(i)
+                dset = dgroup.create_dataset(cname, data=fdata)
+        else:
+            for i, c in enumerate(self.item.curves):
+                d = c.getData()
+                if appendAllX or i == 0:
+                    data.append(d[0])
+                data.append(d[1])
+
+            fdata = numpy.array(data).astype('double')
+            dset = fd.create_dataset(dsname, data=fdata)
 
-        fdata = numpy.array(data).astype('double')
-        dset = fd.create_dataset(dsname, data=fdata)
         fd.close()
 
 if HAVE_HDF5:


Test patch:
diff --git a/pyqtgraph/exporters/tests/test_hdf5.py b/pyqtgraph/exporters/tests/test_hdf5.py
new file mode 100644
--- /dev/null
+++ b/pyqtgraph/exporters/tests/test_hdf5.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+import pytest
+import pyqtgraph as pg
+from pyqtgraph.exporters import HDF5Exporter
+import numpy as np
+from numpy.testing import assert_equal
+import h5py
+import os
+
+
+@pytest.fixture
+def tmp_h5(tmp_path):
+    yield tmp_path / "data.h5"
+
+
+@pytest.mark.parametrize("combine", [False, True])
+def test_HDF5Exporter(tmp_h5, combine):
+    # Basic test of functionality: multiple curves with shared x array. Tests
+    # both options for stacking the data (columnMode).
+    x = np.linspace(0, 1, 100)
+    y1 = np.sin(x)
+    y2 = np.cos(x)
+
+    plt = pg.plot()
+    plt.plot(x=x, y=y1)
+    plt.plot(x=x, y=y2)
+
+    ex = HDF5Exporter(plt.plotItem)
+
+    if combine:
+        ex.parameters()['columnMode'] = '(x,y,y,y) for all plots'
+
+    ex.export(fileName=tmp_h5)
+
+    with h5py.File(tmp_h5, 'r') as f:
+        # should be a single dataset with the name of the exporter
+        dset = f[ex.parameters()['Name']]
+        assert isinstance(dset, h5py.Dataset)
+
+        if combine:
+            assert_equal(np.array([x, y1, y2]), dset)
+        else:
+            assert_equal(np.array([x, y1, x, y2]), dset)
+
+
+def test_HDF5Exporter_unequal_lengths(tmp_h5):
+    # Test export with multiple curves of different size. The exporter should
+    # detect this and create multiple hdf5 datasets under a group.
+    x1 = np.linspace(0, 1, 10)
+    y1 = np.sin(x1)
+    x2 = np.linspace(0, 1, 100)
+    y2 = np.cos(x2)
+
+    plt = pg.plot()
+    plt.plot(x=x1, y=y1, name='plot0')
+    plt.plot(x=x2, y=y2)
+
+    ex = HDF5Exporter(plt.plotItem)
+    ex.export(fileName=tmp_h5)
+
+    with h5py.File(tmp_h5, 'r') as f:
+        # should be a group with the name of the exporter
+        group = f[ex.parameters()['Name']]
+        assert isinstance(group, h5py.Group)
+
+        # should be a dataset under the group with the name of the PlotItem
+        assert_equal(np.array([x1, y1]), group['plot0'])
+
+        # should be a dataset under the group with a default name that's the
+        # index of the curve in the PlotItem
+        assert_equal(np.array([x2, y2]), group['1'])