Repository: biolab/orange3

Issue:
Line Plot unnecessarily strict about missing values
<!-- 
Thanks for taking the time to submit a feature request!
For the best chance at our team considering your request, please answer the following questions to the best of your ability.
-->

**What's your use case?**
<!-- In other words, what's your pain point? -->
<!-- Is your request related to a problem, or perhaps a frustration? -->
<!-- Tell us the story that led you to write this request. -->

If a row of data has even one missing data item, Line Plot refuses to plot it.  I think this is unnecessarily strict—it should be possible to plot the line up to and including the item before the missing item, and then restart after the missing item(s).  Clearly I can manually remove undesirable columns, as shown in the attached workflow, so why shouldn’t Line Plot be able to do so automatically?  (The advantage would also be that in an automatic solution there would be visible gaps in the diagram, in my example the missing data are just treated is if there was no year 1964 to begin with.)

[LinePlot.zip](https://github.com/biolab/orange3/files/7728446/LinePlot.zip)

**What's your proposed solution?**
<!-- Be specific, clear, and concise. -->

Line Plot should be able to plot data with missing values.

**Are there any alternative solutions?**








Discussion:
We discussed this at the meeting and liked the idea. Somebody exposed the problem that in case of multiple broken lines it is not clear which line continues where. @pavlin-policar then had a nice idea to connect the disconnected ends with dashed lines.

Resolving patch:
diff --git a/Orange/widgets/visualize/owlineplot.py b/Orange/widgets/visualize/owlineplot.py
--- a/Orange/widgets/visualize/owlineplot.py
+++ b/Orange/widgets/visualize/owlineplot.py
@@ -17,7 +17,7 @@
 
 from Orange.data import Table, DiscreteVariable
 from Orange.data.sql.table import SqlTable
-from Orange.statistics.util import countnans, nanmean, nanmin, nanmax, nanstd
+from Orange.statistics.util import nanmean, nanmin, nanmax, nanstd
 from Orange.widgets import gui, report
 from Orange.widgets.settings import (
     Setting, ContextSetting, DomainContextHandler
@@ -195,7 +195,9 @@ def reset(self):
 class ParameterSetter(CommonParameterSetter):
     MEAN_LABEL = "Mean"
     LINE_LABEL = "Lines"
+    MISSING_LINE_LABEL = "Lines (missing value)"
     SEL_LINE_LABEL = "Selected lines"
+    SEL_MISSING_LINE_LABEL = "Selected lines (missing value)"
     RANGE_LABEL = "Range"
     SEL_RANGE_LABEL = "Selected range"
 
@@ -214,12 +216,24 @@ def update_setters(self):
             Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,
             Updater.ANTIALIAS_LABEL: True,
         }
+        self.missing_line_settings = {
+            Updater.WIDTH_LABEL: LinePlotStyle.UNSELECTED_LINE_WIDTH,
+            Updater.ALPHA_LABEL: LinePlotStyle.UNSELECTED_LINE_ALPHA,
+            Updater.STYLE_LABEL: "Dash line",
+            Updater.ANTIALIAS_LABEL: True,
+        }
         self.sel_line_settings = {
             Updater.WIDTH_LABEL: LinePlotStyle.SELECTED_LINE_WIDTH,
             Updater.ALPHA_LABEL: LinePlotStyle.SELECTED_LINE_ALPHA,
             Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,
             Updater.ANTIALIAS_LABEL: False,
         }
+        self.sel_missing_line_settings = {
+            Updater.WIDTH_LABEL: LinePlotStyle.SELECTED_LINE_WIDTH,
+            Updater.ALPHA_LABEL: LinePlotStyle.SELECTED_LINE_ALPHA,
+            Updater.STYLE_LABEL: "Dash line",
+            Updater.ANTIALIAS_LABEL: False,
+        }
         self.range_settings = {
             Updater.ALPHA_LABEL: LinePlotStyle.RANGE_ALPHA,
         }
@@ -255,6 +269,15 @@ def update_setters(self):
                                           LinePlotStyle.UNSELECTED_LINE_ALPHA),
                     Updater.ANTIALIAS_LABEL: (None, True),
                 },
+                self.MISSING_LINE_LABEL: {
+                    Updater.WIDTH_LABEL: (range(1, 15),
+                                          LinePlotStyle.UNSELECTED_LINE_WIDTH),
+                    Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),
+                                          "Dash line"),
+                    Updater.ALPHA_LABEL: (range(0, 255, 5),
+                                          LinePlotStyle.UNSELECTED_LINE_ALPHA),
+                    Updater.ANTIALIAS_LABEL: (None, True),
+                },
                 self.SEL_LINE_LABEL: {
                     Updater.WIDTH_LABEL: (range(1, 15),
                                           LinePlotStyle.SELECTED_LINE_WIDTH),
@@ -264,6 +287,15 @@ def update_setters(self):
                                           LinePlotStyle.SELECTED_LINE_ALPHA),
                     Updater.ANTIALIAS_LABEL: (None, False),
                 },
+                self.SEL_MISSING_LINE_LABEL: {
+                    Updater.WIDTH_LABEL: (range(1, 15),
+                                          LinePlotStyle.SELECTED_LINE_WIDTH),
+                    Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),
+                                          "Dash line"),
+                    Updater.ALPHA_LABEL: (range(0, 255, 5),
+                                          LinePlotStyle.SELECTED_LINE_ALPHA),
+                    Updater.ANTIALIAS_LABEL: (None, True),
+                },
                 self.RANGE_LABEL: {
                     Updater.ALPHA_LABEL: (range(0, 255, 5),
                                           LinePlotStyle.RANGE_ALPHA),
@@ -283,10 +315,20 @@ def update_lines(**settings):
             self.line_settings.update(**settings)
             Updater.update_lines(self.lines_items, **self.line_settings)
 
+        def update_missing_lines(**settings):
+            self.missing_line_settings.update(**settings)
+            Updater.update_lines(self.missing_lines_items,
+                                 **self.missing_line_settings)
+
         def update_sel_lines(**settings):
             self.sel_line_settings.update(**settings)
             Updater.update_lines(self.sel_lines_items, **self.sel_line_settings)
 
+        def update_sel_missing_lines(**settings):
+            self.sel_missing_line_settings.update(**settings)
+            Updater.update_lines(self.sel_missing_lines_items,
+                                 **self.sel_missing_line_settings)
+
         def _update_brush(items, **settings):
             for item in items:
                 brush = item.brush()
@@ -306,7 +348,9 @@ def update_sel_range(**settings):
         self._setters[self.PLOT_BOX] = {
             self.MEAN_LABEL: update_mean,
             self.LINE_LABEL: update_lines,
+            self.MISSING_LINE_LABEL: update_missing_lines,
             self.SEL_LINE_LABEL: update_sel_lines,
+            self.SEL_MISSING_LINE_LABEL: update_sel_missing_lines,
             self.RANGE_LABEL: update_range,
             self.SEL_RANGE_LABEL: update_sel_range,
         }
@@ -331,11 +375,20 @@ def mean_lines_items(self):
     def lines_items(self):
         return [group.profiles for group in self.master.groups]
 
+    @property
+    def missing_lines_items(self):
+        return [group.missing_profiles for group in self.master.groups]
+
     @property
     def sel_lines_items(self):
         return [group.sel_profiles for group in self.master.groups] + \
                [group.sub_profiles for group in self.master.groups]
 
+    @property
+    def sel_missing_lines_items(self):
+        return [group.sel_missing_profiles for group in self.master.groups] + \
+               [group.sub_missing_profiles for group in self.master.groups]
+
     @property
     def range_items(self):
         return [group.range for group in self.master.groups]
@@ -348,6 +401,7 @@ def sel_range_items(self):
     def getAxis(self):
         return self.master.getAxis
 
+
 # Customizable plot widget
 class LinePlotGraph(PlotWidget):
     def __init__(self, parent):
@@ -433,11 +487,11 @@ def __init__(self, data, indices, color, graph):
         self.color = color
         self.graph = graph
 
-        self.profiles_added = False
-        self.sub_profiles_added = False
-        self.range_added = False
-        self.mean_added = False
-        self.error_bar_added = False
+        self._profiles_added = False
+        self._sub_profiles_added = False
+        self._range_added = False
+        self._mean_added = False
+        self._error_bar_added = False
 
         self.graph_items = []
         self.__mean = nanmean(self.y_data, axis=0)
@@ -445,15 +499,20 @@ def __init__(self, data, indices, color, graph):
 
     def __create_curves(self):
         self.profiles = self._get_profiles_curve()
+        self.missing_profiles = self._get_missing_profiles_curve()
         self.sub_profiles = self._get_sel_profiles_curve()
+        self.sub_missing_profiles = self._get_sel_missing_profiles_curve()
         self.sel_profiles = self._get_sel_profiles_curve()
+        self.sel_missing_profiles = self._get_sel_missing_profiles_curve()
         self.range = self._get_range_curve()
         self.sel_range = self._get_sel_range_curve()
         self.mean = self._get_mean_curve()
         self.error_bar = self._get_error_bar()
         self.graph_items = [
             self.mean, self.range, self.sel_range, self.profiles,
-            self.sub_profiles, self.sel_profiles, self.error_bar
+            self.sub_profiles, self.sel_profiles, self.error_bar,
+            self.missing_profiles, self.sel_missing_profiles,
+            self.sub_missing_profiles,
         ]
 
     def _get_profiles_curve(self):
@@ -463,11 +522,25 @@ def _get_profiles_curve(self):
         Updater.update_lines([curve], **self.graph.parameter_setter.line_settings)
         return curve
 
+    def _get_missing_profiles_curve(self):
+        x, y, con = self.__get_disconnected_curve_missing_data(self.y_data)
+        pen = self.make_pen(self.color)
+        curve = pg.PlotCurveItem(x=x, y=y, connect=con, pen=pen)
+        settings = self.graph.parameter_setter.missing_line_settings
+        Updater.update_lines([curve], **settings)
+        return curve
+
     def _get_sel_profiles_curve(self):
         curve = pg.PlotCurveItem(x=None, y=None, pen=self.make_pen(self.color))
         Updater.update_lines([curve], **self.graph.parameter_setter.sel_line_settings)
         return curve
 
+    def _get_sel_missing_profiles_curve(self):
+        curve = pg.PlotCurveItem(x=None, y=None, pen=self.make_pen(self.color))
+        settings = self.graph.parameter_setter.sel_missing_line_settings
+        Updater.update_lines([curve], **settings)
+        return curve
+
     def _get_range_curve(self):
         color = QColor(self.color)
         color.setAlpha(self.graph.parameter_setter.range_settings[Updater.ALPHA_LABEL])
@@ -500,38 +573,44 @@ def remove_items(self):
         self.graph_items = []
 
     def set_visible_profiles(self, show_profiles=True, show_range=True, **_):
-        if not self.profiles_added and show_profiles:
-            self.profiles_added = True
+        if not self._profiles_added and show_profiles:
+            self._profiles_added = True
             self.graph.addItem(self.profiles)
+            self.graph.addItem(self.missing_profiles)
             self.graph.addItem(self.sel_profiles)
-        if not self.sub_profiles_added and (show_profiles or show_range):
-            self.sub_profiles_added = True
+            self.graph.addItem(self.sel_missing_profiles)
+        if not self._sub_profiles_added and (show_profiles or show_range):
+            self._sub_profiles_added = True
             self.graph.addItem(self.sub_profiles)
+            self.graph.addItem(self.sub_missing_profiles)
         self.profiles.setVisible(show_profiles)
+        self.missing_profiles.setVisible(show_profiles)
         self.sel_profiles.setVisible(show_profiles)
+        self.sel_missing_profiles.setVisible(show_profiles)
         self.sub_profiles.setVisible(show_profiles or show_range)
+        self.sub_missing_profiles.setVisible(show_profiles or show_range)
 
     def set_visible_range(self, show_profiles=True, show_range=True, **_):
-        if not self.range_added and show_range:
-            self.range_added = True
+        if not self._range_added and show_range:
+            self._range_added = True
             self.graph.addItem(self.range)
             self.graph.addItem(self.sel_range)
-        if not self.sub_profiles_added and (show_profiles or show_range):
-            self.sub_profiles_added = True
+        if not self._sub_profiles_added and (show_profiles or show_range):
+            self._sub_profiles_added = True
             self.graph.addItem(self.sub_profiles)
         self.range.setVisible(show_range)
         self.sel_range.setVisible(show_range)
         self.sub_profiles.setVisible(show_profiles or show_range)
 
     def set_visible_mean(self, show_mean=True, **_):
-        if not self.mean_added and show_mean:
-            self.mean_added = True
+        if not self._mean_added and show_mean:
+            self._mean_added = True
             self.graph.addItem(self.mean)
         self.mean.setVisible(show_mean)
 
     def set_visible_error(self, show_error=True, **_):
-        if not self.error_bar_added and show_error:
-            self.error_bar_added = True
+        if not self._error_bar_added and show_error:
+            self._error_bar_added = True
             self.graph.addItem(self.error_bar)
         self.error_bar.setVisible(show_error)
 
@@ -544,11 +623,24 @@ def update_profiles_color(self, selection):
         pen.setColor(color)
         self.profiles.setPen(pen)
 
+        color = QColor(self.color)
+        alpha = self.graph.parameter_setter.missing_line_settings[
+            Updater.ALPHA_LABEL] if not selection else \
+            LinePlotStyle.UNSELECTED_LINE_ALPHA_SEL
+        color.setAlpha(alpha)
+        pen = self.missing_profiles.opts["pen"]
+        pen.setColor(color)
+        self.missing_profiles.setPen(pen)
+
     def update_sel_profiles(self, y_data):
         x, y, connect = self.__get_disconnected_curve_data(y_data) \
             if y_data is not None else (None, None, None)
         self.sel_profiles.setData(x=x, y=y, connect=connect)
 
+        x, y, connect = self.__get_disconnected_curve_missing_data(y_data) \
+            if y_data is not None else (None, None, None)
+        self.sel_missing_profiles.setData(x=x, y=y, connect=connect)
+
     def update_sel_profiles_color(self, subset):
         color = QColor(Qt.black) if subset else QColor(self.color)
         color.setAlpha(self.graph.parameter_setter.sel_line_settings[Updater.ALPHA_LABEL])
@@ -556,11 +648,23 @@ def update_sel_profiles_color(self, subset):
         pen.setColor(color)
         self.sel_profiles.setPen(pen)
 
+        color = QColor(Qt.black) if subset else QColor(self.color)
+        alpha = self.graph.parameter_setter.sel_missing_line_settings[
+            Updater.ALPHA_LABEL]
+        color.setAlpha(alpha)
+        pen = self.sel_missing_profiles.opts["pen"]
+        pen.setColor(color)
+        self.sel_missing_profiles.setPen(pen)
+
     def update_sub_profiles(self, y_data):
         x, y, connect = self.__get_disconnected_curve_data(y_data) \
             if y_data is not None else (None, None, None)
         self.sub_profiles.setData(x=x, y=y, connect=connect)
 
+        x, y, connect = self.__get_disconnected_curve_missing_data(y_data) \
+            if y_data is not None else (None, None, None)
+        self.sub_missing_profiles.setData(x=x, y=y, connect=connect)
+
     def update_sel_range(self, y_data):
         if y_data is None:
             curve1 = curve2 = pg.PlotDataItem(x=self.x_data, y=self.__mean)
@@ -574,8 +678,23 @@ def __get_disconnected_curve_data(y_data):
         m, n = y_data.shape
         x = np.arange(m * n) % n + 1
         y = y_data.A.flatten() if sp.issparse(y_data) else y_data.flatten()
-        connect = np.ones_like(y, bool)
-        connect[n - 1:: n] = False
+        connect = ~np.isnan(y_data.A if sp.issparse(y_data) else y_data)
+        connect[:, -1] = False
+        connect = connect.flatten()
+        return x, y, connect
+
+    @staticmethod
+    def __get_disconnected_curve_missing_data(y_data):
+        m, n = y_data.shape
+        x = np.arange(m * n) % n + 1
+        y = y_data.A.flatten() if sp.issparse(y_data) else y_data.flatten()
+        connect = np.isnan(y_data.A if sp.issparse(y_data) else y_data)
+        # disconnect until the first non nan
+        first_non_nan = np.argmin(connect, axis=1)
+        for row in np.flatnonzero(first_non_nan):
+            connect[row, :first_non_nan[row]] = False
+        connect[:, -1] = False
+        connect = connect.flatten()
         return x, y, connect
 
     @staticmethod
@@ -620,13 +739,11 @@ class Outputs:
 
     class Error(OWWidget.Error):
         not_enough_attrs = Msg("Need at least one numeric feature.")
-        no_valid_data = Msg("No plot due to no valid data.")
 
     class Warning(OWWidget.Warning):
         no_display_option = Msg("No display option is selected.")
 
     class Information(OWWidget.Information):
-        hidden_instances = Msg("Instances with unknown values are not shown.")
         too_many_features = Msg("Data has too many features. Only first {}"
                                 " are shown.".format(MAX_FEATURES))
 
@@ -634,7 +751,6 @@ def __init__(self, parent=None):
         super().__init__(parent)
         self.__groups = []
         self.data = None
-        self.valid_data = None
         self.subset_data = None
         self.subset_indices = None
         self.__pending_selection = self.selection
@@ -738,14 +854,9 @@ def error(err):
         if self.data is not None:
             self.graph_variables = [var for var in self.data.domain.attributes
                                     if var.is_continuous]
-            self.valid_data = ~countnans(self.data.X, axis=1).astype(bool)
             if len(self.graph_variables) < 1:
                 error(self.Error.not_enough_attrs)
-            elif not np.sum(self.valid_data):
-                error(self.Error.no_valid_data)
             else:
-                if not np.all(self.valid_data):
-                    self.Information.hidden_instances()
                 if len(self.graph_variables) > MAX_FEATURES:
                     self.Information.too_many_features()
                     self.graph_variables = self.graph_variables[:MAX_FEATURES]
@@ -756,7 +867,7 @@ def check_display_options(self):
             if not (self.show_profiles or self.show_range or self.show_mean):
                 self.Warning.no_display_option()
             enable = (self.show_profiles or self.show_range) and \
-                len(self.data[self.valid_data]) < SEL_MAX_INSTANCES
+                len(self.data) < SEL_MAX_INSTANCES
             self.enable_selection.emit(enable)
 
     @Inputs.data_subset
@@ -776,8 +887,7 @@ def set_subset_ids(self):
             if self.subset_data is not None else {}
         self.subset_indices = None
         if self.data is not None and sub_ids:
-            self.subset_indices = [x.id for x in self.data[self.valid_data]
-                                   if x.id in sub_ids]
+            self.subset_indices = [x.id for x in self.data if x.id in sub_ids]
 
     def setup_plot(self):
         if self.data is None:
@@ -792,15 +902,14 @@ def setup_plot(self):
 
     def plot_groups(self):
         self._remove_groups()
-        data = self.data[self.valid_data][:, self.graph_variables]
+        data = self.data[:, self.graph_variables]
         if self.group_var is None:
-            self._plot_group(data, np.where(self.valid_data)[0])
+            self._plot_group(data, np.arange(len(data)))
         else:
             class_col_data, _ = self.data.get_column_view(self.group_var)
             for index in range(len(self.group_var.values)):
-                mask = np.logical_and(class_col_data == index, self.valid_data)
-                indices = np.flatnonzero(mask)
-                if not len(indices):
+                indices = np.flatnonzero(class_col_data == index)
+                if len(indices) == 0:
                     continue
                 group_data = self.data[indices, self.graph_variables]
                 self._plot_group(group_data, indices, index)
@@ -876,7 +985,7 @@ def _update_sub_profiles(self):
             group.update_sub_profiles(table)
 
     def _update_visibility(self, obj_name):
-        if not len(self.__groups):
+        if len(self.__groups) == 0:
             return
         self._update_profiles_color()
         self._update_sel_profiles_and_range()
@@ -891,15 +1000,13 @@ def apply_selection(self):
             sel = [i for i in self.__pending_selection if i < len(self.data)]
             mask = np.zeros(len(self.data), dtype=bool)
             mask[sel] = True
-            mask = mask[self.valid_data]
             self.selection_changed(mask)
             self.__pending_selection = None
 
     def selection_changed(self, mask):
         if self.data is None:
             return
-        # need indices for self.data: mask refers to self.data[self.valid_data]
-        indices = np.arange(len(self.data))[self.valid_data][mask]
+        indices = np.arange(len(self.data))[mask]
         self.graph.select(indices)
         old = self.selection
         self.selection = None if self.data and isinstance(self.data, SqlTable)\
@@ -931,7 +1038,6 @@ def sizeHint(self):
         return QSize(1132, 708)
 
     def clear(self):
-        self.valid_data = None
         self.selection = None
         self.__groups = []
         self.graph_variables = []


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owlineplot.py b/Orange/widgets/visualize/tests/test_owlineplot.py
--- a/Orange/widgets/visualize/tests/test_owlineplot.py
+++ b/Orange/widgets/visualize/tests/test_owlineplot.py
@@ -10,7 +10,6 @@
 from AnyQt.QtCore import Qt, QPointF
 from AnyQt.QtGui import QFont
 
-import pyqtgraph
 from pyqtgraph import PlotCurveItem
 from pyqtgraph.Point import Point
 
@@ -21,10 +20,50 @@
     WidgetTest, WidgetOutputsTestMixin, datasets
 )
 from Orange.widgets.visualize.owlineplot import (
-    OWLinePlot, ccw, intersects, line_intersects_profiles
+    OWLinePlot, ccw, intersects, line_intersects_profiles, ProfileGroup
 )
 
 
+class TestProfileGroup(unittest.TestCase):
+    def test_get_disconnected_curve_missing_data_sizes(self):
+        y = np.array([[1.2, 1.5, 2., 1.4, 2.],
+                      [1.3, np.nan, 4., 1.5, 3.],
+                      [np.nan, np.nan, 5.6, np.nan, 3.4],
+                      [3.4, 5.7, 3.5, 3.3, 3.7]])
+        x, y, connect = \
+            ProfileGroup._ProfileGroup__get_disconnected_curve_missing_data(y)
+        self.assertEqual(x.shape, (20,))
+        self.assertEqual(y.shape, (20,))
+        self.assertEqual(connect.shape, (20,))
+
+    def test_get_disconnected_curve_missing_data_connect(self):
+        y = np.array([[1.2, 1.5, 2., 1.4, 2.],
+                      [1.3, np.nan, 4., 1.5, 3.],
+                      [np.nan, np.nan, 5.6, np.nan, 3.4],
+                      [3.4, 5.7, 3.5, 3.3, 3.7]])
+        _, _, connect = \
+            ProfileGroup._ProfileGroup__get_disconnected_curve_missing_data(y)
+        con = [False] * 6 + [True] + [False] * 6 + [True] + [False] * 6
+        np.testing.assert_array_equal(connect, con)
+
+        y = np.array([[1.2, 1.5, 2., 1.4, 2.],
+                      [np.nan, np.nan, np.nan, np.nan, 3.],
+                      [np.nan, np.nan, np.nan, np.nan, 3.4],
+                      [3.4, 5.7, 3.5, 3.3, 3.7]])
+        _, _, connect = \
+            ProfileGroup._ProfileGroup__get_disconnected_curve_missing_data(y)
+        np.testing.assert_array_equal(connect, [False] * 20)
+
+        y = np.array([[np.nan, np.nan, np.nan, np.nan, np.nan],
+                      [np.nan, np.nan, np.nan, np.nan, 3.4],
+                      [np.nan, np.nan, np.nan, np.nan, np.nan],
+                      [3.4, 5.7, 3.5, 3.3, 3.7]])
+        _, _, connect = \
+            ProfileGroup._ProfileGroup__get_disconnected_curve_missing_data(y)
+        con = [True] * 4 + [False] * 6 + [True] * 4 + [False] * 6
+        np.testing.assert_array_equal(connect, con)
+
+
 class TestOWLinePLot(WidgetTest, WidgetOutputsTestMixin):
     @classmethod
     def setUpClass(cls):
@@ -123,7 +162,7 @@ def test_saved_selection(self):
         with data.unlocked():
             data[0, 0] = np.nan
         self.send_signal(self.widget.Inputs.data, data)
-        mask = np.zeros(len(data) - 1, dtype=bool)
+        mask = np.zeros(len(data), dtype=bool)
         mask[::10] = True
         self.widget.selection_changed(mask)
         settings = self.widget.settingsHandler.pack_data(self.widget)
@@ -189,9 +228,6 @@ def test_data_with_missing_values(self):
         with data.unlocked():
             data[0, 0] = np.nan
         self.send_signal(self.widget.Inputs.data, data)
-        self.assertTrue(self.widget.Information.hidden_instances.is_shown())
-        self.send_signal(self.widget.Inputs.data, None)
-        self.assertFalse(self.widget.Information.hidden_instances.is_shown())
 
     def test_display_options(self):
         self.send_signal(self.widget.Inputs.data, self.data[::10])
@@ -240,7 +276,6 @@ def test_datasets(self):
         for ds in datasets.datasets():
             self.send_signal(self.widget.Inputs.data, ds)
         self.send_signal(self.widget.Inputs.data, None)
-        self.assertFalse(self.widget.Error.no_valid_data.is_shown())
 
     def test_none_data(self):
         self.send_signal(self.widget.Inputs.data, self.data[:0])
@@ -354,6 +389,16 @@ def test_visual_settings(self, timeout=DEFAULT_TIMEOUT):
         self.assertEqual(axis.label.toPlainText().strip(), "Foo3")
         self.assertEqual(axis.labelText, "Foo3")
 
+        key, value = ("Figure", "Lines (missing value)", "Width"), 10
+        self.widget.set_visual_settings(key, value)
+        for line in graph.parameter_setter.missing_lines_items:
+            self.assertEqual(line.opts["pen"].width(), 10)
+
+        key, value = ("Figure", "Selected lines (missing value)", "Width"), 11
+        self.widget.set_visual_settings(key, value)
+        for line in graph.parameter_setter.sel_missing_lines_items:
+            self.assertEqual(line.opts["pen"].width(), 11)
+
     def assertFontEqual(self, font1, font2):
         self.assertEqual(font1.family(), font2.family())
         self.assertEqual(font1.pointSize(), font2.pointSize())