Repository: biolab/orange3

Issue:
Feature Statistics and other widgets do not report min and max correctly - rounding error
When a variable has low values (e.g. FDR on the below image) some of the widgets (Heatmap annotation legend, Feature Statistics) do not report min and max correctly due to the rounding. This can result in min and max being the same, despite the variable in fact having different values. In such cases scientific notation could be used instead of decimal notation, rounding to N significant instead of N decimal places. 
![Screenshot from 2020-03-11 13-43-45](https://user-images.githubusercontent.com/47607471/76418455-f2571f80-639e-11ea-804b-24ee85d186cf.png)



Discussion:

Resolving patch:
diff --git a/Orange/widgets/data/owfeaturestatistics.py b/Orange/widgets/data/owfeaturestatistics.py
--- a/Orange/widgets/data/owfeaturestatistics.py
+++ b/Orange/widgets/data/owfeaturestatistics.py
@@ -7,14 +7,13 @@
 """
 
 import datetime
-import locale
 from enum import IntEnum
 from typing import Any, Optional, Tuple, List
 
 import numpy as np
 import scipy.stats as ss
 import scipy.sparse as sp
-from AnyQt.QtCore import Qt, QSize, QRectF, QVariant, QModelIndex, pyqtSlot, \
+from AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \
     QRegExp, QItemSelection, QItemSelectionRange, QItemSelectionModel
 from AnyQt.QtGui import QPainter, QColor
 from AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \
@@ -465,44 +464,35 @@ def headerData(self, section, orientation, role):
 
     def data(self, index, role):
         # type: (QModelIndex, Qt.ItemDataRole) -> Any
-        # Text formatting for various data simply requires a lot of branches.
-        # This is much better than overengineering various formatters...
-        # pylint: disable=too-many-branches
-
-        if not index.isValid():
-            return None
-
-        row, column = self.mapToSourceRows(index.row()), index.column()
-        # Make sure we're not out of range
-        if not 0 <= row <= self.n_attributes:
-            return QVariant()
-
-        attribute = self.variables[row]
-
-        if role == Qt.BackgroundRole:
+        def background():
             if attribute in self.domain.attributes:
                 return self.COLOR_FOR_ROLE[self.ATTRIBUTE]
-            elif attribute in self.domain.metas:
+            if attribute in self.domain.metas:
                 return self.COLOR_FOR_ROLE[self.META]
-            elif attribute in self.domain.class_vars:
+            if attribute in self.domain.class_vars:
                 return self.COLOR_FOR_ROLE[self.CLASS_VAR]
+            return None
 
-        elif role == Qt.TextAlignmentRole:
+        def text_alignment():
             if column == self.Columns.NAME:
                 return Qt.AlignLeft | Qt.AlignVCenter
             return Qt.AlignRight | Qt.AlignVCenter
 
-        output = None
-
-        if column == self.Columns.ICON:
-            if role == Qt.DecorationRole:
+        def decoration():
+            if column == self.Columns.ICON:
                 return gui.attributeIconDict[attribute]
-        elif column == self.Columns.NAME:
-            if role == Qt.DisplayRole:
-                output = attribute.name
-        elif column == self.Columns.DISTRIBUTION:
-            if role == Qt.DisplayRole:
-                if isinstance(attribute, (DiscreteVariable, ContinuousVariable)):
+            return None
+
+        def display():
+            # pylint: disable=too-many-branches
+            def render_value(value):
+                return "" if np.isnan(value) else attribute.str_val(value)
+
+            if column == self.Columns.NAME:
+                return attribute.name
+            elif column == self.Columns.DISTRIBUTION:
+                if isinstance(attribute,
+                              (DiscreteVariable, ContinuousVariable)):
                     if row not in self.__distributions_cache:
                         scene = QGraphicsScene(parent=self)
                         histogram = Histogram(
@@ -515,60 +505,45 @@ def data(self, index, role):
                         scene.addItem(histogram)
                         self.__distributions_cache[row] = scene
                     return self.__distributions_cache[row]
-        elif column == self.Columns.CENTER:
-            if role == Qt.DisplayRole:
-                if isinstance(attribute, DiscreteVariable):
-                    output = self._center[row]
-                    if not np.isnan(output):
-                        output = attribute.str_val(self._center[row])
-                elif isinstance(attribute, TimeVariable):
-                    output = attribute.str_val(self._center[row])
-                else:
-                    output = self._center[row]
-        elif column == self.Columns.DISPERSION:
-            if role == Qt.DisplayRole:
+            elif column == self.Columns.CENTER:
+                return render_value(self._center[row])
+            elif column == self.Columns.DISPERSION:
                 if isinstance(attribute, TimeVariable):
-                    output = format_time_diff(self._min[row], self._max[row])
-                else:
-                    output = self._dispersion[row]
-        elif column == self.Columns.MIN:
-            if role == Qt.DisplayRole:
-                if isinstance(attribute, DiscreteVariable):
-                    if attribute.ordered:
-                        output = attribute.str_val(self._min[row])
-                elif isinstance(attribute, TimeVariable):
-                    output = attribute.str_val(self._min[row])
+                    return format_time_diff(self._min[row], self._max[row])
+                elif isinstance(attribute, DiscreteVariable):
+                    return "%.3g" % self._dispersion[row]
                 else:
-                    output = self._min[row]
-        elif column == self.Columns.MAX:
-            if role == Qt.DisplayRole:
-                if isinstance(attribute, DiscreteVariable):
-                    if attribute.ordered:
-                        output = attribute.str_val(self._max[row])
-                elif isinstance(attribute, TimeVariable):
-                    output = attribute.str_val(self._max[row])
-                else:
-                    output = self._max[row]
-        elif column == self.Columns.MISSING:
-            if role == Qt.DisplayRole:
-                output = '%d (%d%%)' % (
+                    return render_value(self._dispersion[row])
+            elif column == self.Columns.MIN:
+                if not isinstance(attribute, DiscreteVariable) \
+                        or attribute.ordered:
+                    return render_value(self._min[row])
+            elif column == self.Columns.MAX:
+                if not isinstance(attribute, DiscreteVariable) \
+                        or attribute.ordered:
+                    return render_value(self._max[row])
+            elif column == self.Columns.MISSING:
+                return '%d (%d%%)' % (
                     self._missing[row],
                     100 * self._missing[row] / self.n_instances
                 )
+            return None
+
+        roles = {Qt.BackgroundRole: background,
+                 Qt.TextAlignmentRole: text_alignment,
+                 Qt.DecorationRole: decoration,
+                 Qt.DisplayRole: display}
+
+        if not index.isValid() or role not in roles:
+            return None
+
+        row, column = self.mapToSourceRows(index.row()), index.column()
+        # Make sure we're not out of range
+        if not 0 <= row <= self.n_attributes:
+            return None
 
-        # Consistently format the text inside the table cells
-        # The easiest way to check for NaN is to compare with itself
-        if output != output:  # pylint: disable=comparison-with-itself
-            output = ''
-        # Format ∞ properly
-        elif output in (np.inf, -np.inf):
-            output = '%s∞' % ['', '-'][output < 0]
-        elif isinstance(output, int):
-            output = locale.format_string('%d', output, grouping=True)
-        elif isinstance(output, float):
-            output = locale.format_string('%.2f', output, grouping=True)
-
-        return output
+        attribute = self.variables[row]
+        return roles[role]()
 
     def rowCount(self, parent=QModelIndex()):
         return 0 if parent.isValid() else self.n_attributes


Test patch: