Skip to content

Commit 55bb10f

Browse files
authored
Merge pull request #4741 from janezd/featurestatistics-var-settings
[FIX] Feature Statistics: Fix wrong or even crashing selections
2 parents 4de95cb + 05e8a65 commit 55bb10f

File tree

2 files changed

+68
-18
lines changed

2 files changed

+68
-18
lines changed

Orange/widgets/data/owfeaturestatistics.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import datetime
1010
from enum import IntEnum
11+
from itertools import chain
1112
from typing import Any, Optional, Tuple, List
1213

1314
import numpy as np
@@ -24,7 +25,8 @@
2425
ContinuousVariable, TimeVariable, Domain, Variable
2526
from Orange.widgets import widget, gui
2627
from Orange.widgets.data.utils.histogram import Histogram
27-
from Orange.widgets.settings import ContextSetting, DomainContextHandler
28+
from Orange.widgets.settings import Setting, ContextSetting, \
29+
DomainContextHandler
2830
from Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel
2931
from Orange.widgets.utils.signals import Input, Output
3032
from Orange.widgets.utils.widgetpreview import WidgetPreview
@@ -685,13 +687,14 @@ class Outputs:
685687
buttons_area_orientation = Qt.Vertical
686688

687689
settingsHandler = DomainContextHandler()
690+
settings_version = 2
688691

689-
auto_commit = ContextSetting(True)
692+
auto_commit = Setting(True)
690693
color_var = ContextSetting(None) # type: Optional[Variable]
691694
# filter_string = ContextSetting('')
692695

693-
sorting = ContextSetting((0, Qt.DescendingOrder))
694-
selected_rows = ContextSetting([])
696+
sorting = Setting((0, Qt.DescendingOrder))
697+
selected_vars = ContextSetting([], schema_only=True)
695698

696699
def __init__(self):
697700
super().__init__()
@@ -753,7 +756,7 @@ def _filter_table_variables(self):
753756
def set_data(self, data):
754757
# Clear outputs and reset widget state
755758
self.closeContext()
756-
self.selected_rows = []
759+
self.selected_vars = []
757760
self.model.resetSorting()
758761
self.Outputs.reduced_data.send(None)
759762
self.Outputs.statistics.send(None)
@@ -786,10 +789,10 @@ def __restore_selection(self):
786789
"""Restore the selection on the table view from saved settings."""
787790
selection_model = self.table_view.selectionModel()
788791
selection = QItemSelection()
789-
# self.selected_rows can be list or numpy.array, thus
790-
# pylint: disable=len-as-condition
791-
if len(self.selected_rows):
792-
for row in self.model.mapFromSourceRows(self.selected_rows):
792+
if self.selected_vars:
793+
var_indices = {var: i for i, var in enumerate(self.model.variables)}
794+
selected_indices = [var_indices[var] for var in self.selected_vars]
795+
for row in self.model.mapFromSourceRows(selected_indices):
793796
selection.append(QItemSelectionRange(
794797
self.model.index(row, 0),
795798
self.model.index(row, self.model.columnCount() - 1)
@@ -816,22 +819,21 @@ def __color_var_changed(self, *_):
816819
self.model.set_target_var(self.color_var)
817820

818821
def on_select(self):
819-
self.selected_rows = list(self.model.mapToSourceRows([
822+
selection_indices = list(self.model.mapToSourceRows([
820823
i.row() for i in self.table_view.selectionModel().selectedRows()
821824
]))
825+
self.selected_vars = list(self.model.variables[selection_indices])
822826
self.commit()
823827

824828
def commit(self):
825-
# self.selected_rows can be list or numpy.array, thus
826-
# pylint: disable=len-as-condition
827-
if not len(self.selected_rows):
829+
if not self.selected_vars:
828830
self.info.set_output_summary(self.info.NoOutput)
829831
self.Outputs.reduced_data.send(None)
830832
self.Outputs.statistics.send(None)
831833
return
832834

833835
# Send a table with only selected columns to output
834-
variables = self.model.variables[self.selected_rows]
836+
variables = self.selected_vars
835837
self.info.set_output_summary(len(self.data[:, variables]),
836838
format_summary_details(self.data[:, variables]))
837839
self.Outputs.reduced_data.send(self.data[:, variables])
@@ -850,6 +852,26 @@ def commit(self):
850852
def send_report(self):
851853
pass
852854

855+
@classmethod
856+
def migrate_context(cls, context, version):
857+
if not version or version < 2:
858+
selected_rows = context.values.pop("selected_rows", None)
859+
if not selected_rows:
860+
selected_vars = []
861+
else:
862+
# This assumes that dict was saved by Python >= 3.6 so dict is
863+
# ordered; if not, context hasn't had worked anyway.
864+
all_vars = [
865+
(var, tpe)
866+
for (var, tpe) in chain(context.attributes.items(),
867+
context.metas.items())
868+
# it would be nicer to use cls.HIDDEN_VAR_TYPES, but there
869+
# is no suitable conversion function, and StringVariable (3)
870+
# was the only hidden var when settings_version < 2, so:
871+
if tpe != 3]
872+
selected_vars = [all_vars[i] for i in selected_rows]
873+
context.values["selected_vars"] = selected_vars, -3
874+
853875

854876
if __name__ == '__main__': # pragma: no cover
855877
WidgetPreview(OWFeatureStatistics).run(Table("iris"))

Orange/widgets/data/tests/test_owfeaturestatistics.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from Orange.widgets.data.owfeaturestatistics import \
1919
OWFeatureStatistics
2020
from Orange.widgets.utils.state_summary import format_summary_details
21+
from orangewidget.settings import Context
2122

2223
VarDataPair = namedtuple('VarDataPair', ['variable', 'data'])
2324

@@ -454,17 +455,44 @@ def setUp(self):
454455
def test_restores_previous_selection(self):
455456
"""Widget should remember selection with domain context handler."""
456457
# Send data and select rows
458+
domain1 = self.data1.domain
457459
self.send_signal(self.widget.Inputs.data, self.data1)
458460
self.select_rows([0, 2])
459-
self.assertEqual(len(self.widget.selected_rows), 2)
461+
self.assertEqual(set(self.widget.selected_vars),
462+
{domain1[0], domain1[2]})
460463

461464
# Sending new data clears selection
462465
self.send_signal(self.widget.Inputs.data, self.data2)
463-
self.assertEqual(len(self.widget.selected_rows), 0)
466+
self.assertEqual(len(self.widget.selected_vars), 0)
464467

465468
# Sending back the old data restores the selection
466-
self.send_signal(self.widget.Inputs.data, self.data1)
467-
self.assertEqual(len(self.widget.selected_rows), 2)
469+
iris3 = self.data1.transform(
470+
Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))
471+
self.send_signal(self.widget.Inputs.data, iris3)
472+
self.assertEqual(set(self.widget.selected_vars),
473+
{domain1[0], domain1[2]})
474+
475+
def test_settings_migration_to_ver21(self):
476+
settings = {
477+
'controlAreaVisible': True, 'savedWidgetGeometry': '',
478+
'__version__': 1,
479+
'context_settings': [
480+
Context(
481+
values={'auto_commit': (True, -2),
482+
'color_var': ('iris', 101),
483+
'selected_rows': [1, 4],
484+
'sorting': ((1, 0), -2), '__version__': 1},
485+
attributes={'petal length': 2, 'petal width': 2,
486+
'sepal length': 2, 'sepal width': 2},
487+
metas={'iris': 1})]
488+
}
489+
widget = self.create_widget(OWFeatureStatistics,
490+
stored_settings=settings)
491+
self.send_signal(widget.Inputs.data, self.data1)
492+
domain = self.data1.domain
493+
self.assertEqual(widget.selected_vars, [domain["petal width"],
494+
domain["iris"]])
495+
468496

469497
class TestSummary(WidgetTest):
470498
def setUp(self):

0 commit comments

Comments
 (0)