Repository: biolab/orange3

Issue:
Linear Projection: A warning telling why LDA is disabled
### Discussed in https://github.com/biolab/orange3/discussions/6313

<div type='discussions-op-text'>

<sup>Originally posted by **TonciG** January 24, 2023</sup>
Hi,
Exploring the functionality of this widget and have a couple of questions:
- Is there any documentation or help about Circular Placement? I have an idea what this projection is about but would like to be sure.
- For some reason I cannot select LDA, only PCA and Circular Placement. What could be the reasons for this?
Many thanks,
Tonci</div>

The widget should show a warning when disabling the option.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/visualize/owlinearprojection.py b/Orange/widgets/visualize/owlinearprojection.py
--- a/Orange/widgets/visualize/owlinearprojection.py
+++ b/Orange/widgets/visualize/owlinearprojection.py
@@ -24,6 +24,7 @@
 from Orange.widgets import gui, report
 from Orange.widgets.gui import OWComponent
 from Orange.widgets.settings import Setting, ContextSetting, SettingProvider
+from Orange.widgets.utils.localization import pl
 from Orange.widgets.utils.plot import variables_selection
 from Orange.widgets.utils.plot.owplotgui import VariableSelectionModel
 from Orange.widgets.utils.widgetpreview import WidgetPreview
@@ -285,6 +286,9 @@ class OWLinearProjection(OWAnchorProjectionWidget):
     class Error(OWAnchorProjectionWidget.Error):
         no_cont_features = Msg("Plotting requires numeric features")
 
+    class Information(OWAnchorProjectionWidget.Information):
+        no_lda = Msg("LDA placement is disabled due to unsuitable target.\n{}")
+
     def _add_controls(self):
         box = gui.vBox(self.controlArea, box="Features")
         self._add_controls_variables(box)
@@ -377,12 +381,27 @@ def _check_options(self):
         for btn in buttons:
             btn.setEnabled(True)
 
+        problem = None
         if self.data is not None:
-            has_discrete_class = self.data.domain.has_discrete_class
-            if not has_discrete_class or len(np.unique(self.data.Y)) < 3:
-                buttons[Placement.LDA].setEnabled(False)
-                if self.placement == Placement.LDA:
-                    self.placement = Placement.Circular
+            if (class_var := self.data.domain.class_var) is None:
+                problem = "Current data has no target variable"
+            elif not class_var.is_discrete:
+                problem = f"{class_var.name} is not categorical"
+            elif (nclasses := len(distinct := np.unique(self.data.Y))) == 0:
+                problem = f"Data has no defined values for {class_var.name}"
+            elif nclasses < 3:
+                vals = " and ".join(f"'{class_var.values[int(i)]}'" for i in distinct)
+                problem = \
+                    f"Data contains just {['one', 'two'][nclasses - 1]} distinct " \
+                    f"{pl(nclasses, 'value')} ({vals}) for '{class_var.name}'; " \
+                    "at least three are required."
+        if problem is None:
+            self.Information.no_lda.clear()
+        else:
+            self.Information.no_lda(problem)
+            buttons[Placement.LDA].setEnabled(False)
+            if self.placement == Placement.LDA:
+                self.placement = Placement.Circular
 
         self.controls.graph.hide_radius.setEnabled(
             self.placement != Placement.Circular)


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owlinearprojection.py b/Orange/widgets/visualize/tests/test_owlinearprojection.py
--- a/Orange/widgets/visualize/tests/test_owlinearprojection.py
+++ b/Orange/widgets/visualize/tests/test_owlinearprojection.py
@@ -92,6 +92,19 @@ def test_no_data_for_lda(self):
         self.send_signal(self.widget.Inputs.data, None)
         self.assertTrue(buttons[Placement.LDA].isEnabled())
 
+    def test_lda_not_enough_distinct(self):
+        buttons = self.widget.radio_placement.buttons
+        self.send_signal(self.widget.Inputs.data, self.data)
+        self.assertTrue(buttons[Placement.LDA].isEnabled())
+        self.send_signal(self.widget.Inputs.data, self.data[:10])
+        self.assertFalse(buttons[Placement.LDA].isEnabled())
+        self.send_signal(self.widget.Inputs.data, None)
+        self.assertTrue(buttons[Placement.LDA].isEnabled())
+        self.send_signal(self.widget.Inputs.data, self.data[40:60])
+        self.assertFalse(buttons[Placement.LDA].isEnabled())
+        self.send_signal(self.widget.Inputs.data, self.data[40:110])
+        self.assertTrue(buttons[Placement.LDA].isEnabled())
+
     def test_data_no_cont_features(self):
         data = Table("titanic")
         self.assertFalse(self.widget.Error.no_cont_features.is_shown())