Skip to content

Commit

Permalink
Merge pull request #3276 from janezd/vizrank-selection
Browse files Browse the repository at this point in the history
Selection of attributes in the widget is reflected in VizRank
  • Loading branch information
lanzagar authored Dec 14, 2018
2 parents 3ae100d + c154c72 commit 19f9074
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 16 deletions.
24 changes: 21 additions & 3 deletions Orange/widgets/visualize/owmosaic.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def __init__(self, master):
self.marginal = {}
self.last_run_max_attr = None

self.master.attrs_changed_manually.connect(self.on_manual_change)

def sizeHint(self):
return QSize(400, 512)

Expand Down Expand Up @@ -237,8 +239,18 @@ def bar_length(self, score):
return 1 if score == 0 else -log(score, 10) / 50

def on_selection_changed(self, selected, deselected):
attrs = selected.indexes()[0].data(self._AttrRole)
self.selectionChanged.emit(attrs + (None, ) * (4 - len(attrs)))
if not selected.isEmpty():
attrs = selected.indexes()[0].data(self._AttrRole)
self.selectionChanged.emit(attrs + (None, ) * (4 - len(attrs)))

def on_manual_change(self, attrs):
model = self.rank_model
self.rank_table.selectionModel().clear()
for row in range(model.rowCount()):
row_attrs = model.data(model.index(row, 0), self._AttrRole)
if row_attrs == tuple(attrs):
self.rank_table.selectRow(row)
return

def row_for_state(self, score, state):
"""The row consists of attributes sorted by name; class is at the
Expand Down Expand Up @@ -288,6 +300,8 @@ class Outputs:
QColor(255, 100, 100), QColor(255, 0, 0)]
graph_name = "canvas"

attrs_changed_manually = Signal(list)

class Warning(OWWidget.Warning):
incompatible_subset = Msg("Data subset is incompatible with Data")
no_valid_data = Msg("No valid data")
Expand Down Expand Up @@ -324,7 +338,7 @@ def __init__(self):
gui.comboBox(
box, self, value="variable{}".format(i),
orientation=Qt.Horizontal, contentsLength=12,
callback=self.reset_graph,
callback=self.attr_changed,
model=self.model_1 if i == 1 else self.model_234)
for i in range(1, 5)]
self.vizrank, self.vizrank_button = MosaicVizRank.add_vizrank(
Expand Down Expand Up @@ -394,6 +408,10 @@ def set_attr(self, *attrs):
attr and self.data.domain[attr.name] for attr in attrs]
self.reset_graph()

def attr_changed(self):
self.attrs_changed_manually.emit(self.get_disc_attr_list())
self.reset_graph()

def resizeEvent(self, e):
OWWidget.resizeEvent(self, e)
self.update_graph()
Expand Down
16 changes: 11 additions & 5 deletions Orange/widgets/visualize/owradviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def __init__(self, master):
self.data = None
self.valid_data = None

self.rank_table.clicked.connect(self.on_row_clicked)
self.rank_table.verticalHeader().sectionClicked.connect(
self.on_header_clicked)

def initialize(self):
super().initialize()
self.attr_color = self.master.attr_color
Expand Down Expand Up @@ -131,9 +135,11 @@ def check_preconditions(self):
self.n_attrs_spin.setMaximum(20) # all primitive vars except color one
return True

def on_selection_changed(self, selected, deselected):
attrs = selected.indexes()[0].data(self._AttrRole)
self.selectionChanged.emit([attrs])
def on_row_clicked(self, index):
self.selectionChanged.emit(index.data(self._AttrRole))

def on_header_clicked(self, section):
self.on_row_clicked(self.rank_model.index(section, 0))

def iterate_states(self, state):
if state is None: # on the first call, compute order
Expand Down Expand Up @@ -285,7 +291,7 @@ def __init__(self):
self.model_other = VariableListModel(enable_dnd=True)

self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank(
None, self, "Suggest features", self.__vizrank_set_attrs
None, self, "Suggest features", self.vizrank_set_attrs
)
super().__init__()

Expand Down Expand Up @@ -314,7 +320,7 @@ def primitive_variables(self):
def effective_variables(self):
return self.model_selected[:]

def __vizrank_set_attrs(self, attrs):
def vizrank_set_attrs(self, *attrs):
if not attrs:
return
self.model_selected[:] = attrs[:]
Expand Down
14 changes: 11 additions & 3 deletions Orange/widgets/visualize/owscatterplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sklearn.neighbors import NearestNeighbors
from sklearn.metrics import r2_score

from AnyQt.QtCore import Qt, QTimer, QPointF
from AnyQt.QtCore import Qt, QTimer, QPointF, Signal
from AnyQt.QtGui import QColor

import pyqtgraph as pg
Expand Down Expand Up @@ -173,6 +173,8 @@ class Outputs(OWDataProjectionWidget.Outputs):
graph = SettingProvider(OWScatterPlotGraph)
embedding_variables_names = None

xy_changed_manually = Signal(Variable, Variable)

class Warning(OWDataProjectionWidget.Warning):
missing_coords = Msg(
"Plot cannot be displayed because '{}' or '{}' "
Expand Down Expand Up @@ -216,10 +218,12 @@ def _add_controls_axis(self):
dmod = DomainModel
self.xy_model = DomainModel(dmod.MIXED, valid_types=ContinuousVariable)
self.cb_attr_x = gui.comboBox(
box, self, "attr_x", label="Axis x:", callback=self.attr_changed,
box, self, "attr_x", label="Axis x:",
callback=self.set_attr_from_combo,
model=self.xy_model, **common_options)
self.cb_attr_y = gui.comboBox(
box, self, "attr_y", label="Axis y:", callback=self.attr_changed,
box, self, "attr_y", label="Axis y:",
callback=self.set_attr_from_combo,
model=self.xy_model, **common_options)
vizrank_box = gui.hBox(box)
self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(
Expand Down Expand Up @@ -395,6 +399,10 @@ def set_attr(self, attr_x, attr_y):
self.attr_x, self.attr_y = attr_x, attr_y
self.attr_changed()

def set_attr_from_combo(self):
self.attr_changed()
self.xy_changed_manually.emit(self.attr_x, self.attr_y)

def attr_changed(self):
self.setup_plot()
self.commit()
Expand Down
10 changes: 8 additions & 2 deletions Orange/widgets/visualize/owsieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np
from scipy.stats.distributions import chi2

from AnyQt.QtCore import Qt, QSize
from AnyQt.QtCore import Qt, QSize, Signal
from AnyQt.QtGui import QColor, QPen, QBrush
from AnyQt.QtWidgets import QGraphicsScene, QGraphicsLineItem, QSizePolicy

Expand Down Expand Up @@ -94,6 +94,8 @@ class Outputs:
attr_y = ContextSetting(None)
selection = ContextSetting(set())

xy_changed_manually = Signal(Variable, Variable)

def __init__(self):
# pylint: disable=missing-docstring
super().__init__()
Expand All @@ -108,7 +110,7 @@ def __init__(self):
self.domain_model = DomainModel(valid_types=DomainModel.PRIMITIVE)
combo_args = dict(
widget=self.attr_box, master=self, contentsLength=12,
callback=self.update_attr, sendSelectedValue=True, valueType=str,
callback=self.attr_changed, sendSelectedValue=True, valueType=str,
model=self.domain_model)
fixed_size = (QSizePolicy.Fixed, QSizePolicy.Fixed)
gui.comboBox(value="attr_x", **combo_args)
Expand Down Expand Up @@ -196,6 +198,10 @@ def set_attr(self, attr_x, attr_y):
self.attr_x, self.attr_y = attr_x, attr_y
self.update_attr()

def attr_changed(self):
self.update_attr()
self.xy_changed_manually.emit(self.attr_x, self.attr_y)

def update_attr(self):
"""Update the graph and selection."""
self.selection = set()
Expand Down
32 changes: 32 additions & 0 deletions Orange/widgets/visualize/tests/test_owmosaic.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ def test_subset(self):
output = self.get_output(self.widget.Outputs.annotated_data)
np.testing.assert_array_equal(output.X, self.data[:1].X)

@patch('Orange.widgets.visualize.owmosaic.MosaicVizRank.on_manual_change')
def test_vizrank_receives_manual_change(self, on_manual_change):
# Recreate the widget so the patch kicks in
self.widget = self.create_widget(OWMosaicDisplay)
data = Table("iris.tab")
self.send_signal(self.widget.Inputs.data, data)
self.widget.variable1 = data.domain[0]
self.widget.variable2 = data.domain[1]
simulate.combobox_activate_index(self.widget.controls.variable2, 3)
self.assertEqual(self.widget.variable2, data.domain[2])
call_args = on_manual_change.call_args[0][0]
self.assertEqual(len(call_args), 2)
self.assertEqual(call_args[0].name, data.domain[0].name)
self.assertEqual(call_args[1].name, data.domain[2].name)


# Derive from WidgetTest to simplify creation of the Mosaic widget, although
# we are actually testing the MosaicVizRank dialog and not the widget
Expand Down Expand Up @@ -394,6 +409,23 @@ def test_incompatible_subset(self):
self.send_signal(self.widget.Inputs.data_subset, self.iris)
self.assertFalse(self.widget.Warning.incompatible_subset.is_shown())

def test_on_manual_change(self):
data = Table("iris.tab")
self.send_signal(self.widget.Inputs.data, data)
self.vizrank.toggle()
self.process_events(until=lambda: not self.vizrank.keep_running)

model = self.vizrank.rank_model
attrs = model.data(model.index(3, 0), self.vizrank._AttrRole)
self.vizrank.on_manual_change(attrs)
selection = self.vizrank.rank_table.selectedIndexes()
self.assertEqual(len(selection), 1)
self.assertEqual(selection[0].row(), 3)

self.vizrank.on_manual_change(attrs[::-1])
selection = self.vizrank.rank_table.selectedIndexes()
self.assertEqual(len(selection), 0)


if __name__ == "__main__":
unittest.main()
34 changes: 33 additions & 1 deletion Orange/widgets/visualize/tests/test_owscatterplot.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Test methods with long descriptive names can omit docstrings
# pylint: disable=missing-docstring
# pylint: disable=missing-docstring,too-many-public-methods,protected-access
from unittest.mock import MagicMock, patch, Mock
import numpy as np

Expand Down Expand Up @@ -703,6 +703,38 @@ def test_invalidated_diff_features_same_time_features_first(self):
self.widget.setup_plot.assert_called_once()
self.assertListEqual(self.widget.effective_variables, list(features))

@patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'
'on_manual_change')
def test_vizrank_receives_manual_change(self, on_manual_change):
# Recreate the widget so the patch kicks in
self.widget = self.create_widget(OWScatterPlot)
data = Table("iris.tab")
self.send_signal(self.widget.Inputs.data, data)
model = self.widget.controls.attr_x.model()
self.widget.attr_x = model[0]
self.widget.attr_y = model[1]
simulate.combobox_activate_index(self.widget.controls.attr_x, 2)
self.assertIs(self.widget.attr_x, model[2])
on_manual_change.assert_called_with(model[2], model[1])

def test_on_manual_change(self):
data = Table("iris.tab")
self.send_signal(self.widget.Inputs.data, data)
vizrank = self.widget.vizrank
vizrank.toggle()
self.process_events(until=lambda: not vizrank.keep_running)

model = vizrank.rank_model
attrs = model.data(model.index(3, 0), vizrank._AttrRole)
vizrank.on_manual_change(*attrs)
selection = vizrank.rank_table.selectedIndexes()
self.assertEqual(len(selection), 1)
self.assertEqual(selection[0].row(), 3)

vizrank.on_manual_change(*attrs[::-1])
selection = vizrank.rank_table.selectedIndexes()
self.assertEqual(len(selection), 0)


if __name__ == "__main__":
import unittest
Expand Down
21 changes: 21 additions & 0 deletions Orange/widgets/visualize/tests/test_owsieve.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Test methods with long descriptive names can omit docstrings
# pylint: disable=missing-docstring
from math import isnan
import unittest
from unittest.mock import patch

import numpy as np
Expand All @@ -10,6 +11,7 @@

from Orange.data import ContinuousVariable, DiscreteVariable, Domain, Table
from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin
from Orange.widgets.tests.utils import simulate
from Orange.widgets.visualize.owsieve import OWSieveDiagram
from Orange.widgets.visualize.owsieve import ChiSqStats
from Orange.widgets.visualize.owsieve import Discretize
Expand Down Expand Up @@ -147,3 +149,22 @@ def test_sparse_data(self):
self.assertEqual(len(self.widget.discrete_data.domain), 2)
output = self.get_output("Data")
self.assertTrue(output.is_sparse())

@patch('Orange.widgets.visualize.owsieve.SieveRank.on_manual_change')
def test_vizrank_receives_manual_change(self, on_manual_change):
# Recreate the widget so the patch kicks in
self.widget = self.create_widget(OWSieveDiagram)
data = Table("iris.tab")
self.send_signal(self.widget.Inputs.data, data)
model = self.widget.controls.attr_x.model()
self.widget.attr_x = model[2]
self.widget.attr_y = model[3]
simulate.combobox_activate_index(self.widget.controls.attr_x, 4)
call_args = on_manual_change.call_args[0]
self.assertEqual(len(call_args), 2)
self.assertEqual(call_args[0].name, data.domain[2].name)
self.assertEqual(call_args[1].name, data.domain[1].name)


if __name__ == "__main__":
unittest.main()
17 changes: 15 additions & 2 deletions Orange/widgets/visualize/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,9 @@ def check_preconditions(self):
return can_rank

def on_selection_changed(self, selected, deselected):
attr = selected.indexes()[0].data(self._AttrRole)
self.attrSelected.emit(attr)
if not selected.isEmpty():
attr = selected.indexes()[0].data(self._AttrRole)
self.attrSelected.emit(attr)

def state_count(self):
return len(self.attrs)
Expand Down Expand Up @@ -470,6 +471,9 @@ def __init__(self, master):
VizRankDialog.__init__(self, master)
self.resize(320, 512)
self.attrs = []
manual_change_signal = getattr(master, "xy_changed_manually", None)
if manual_change_signal:
manual_change_signal.connect(self.on_manual_change)

def sizeHint(self):
"""Assuming two columns in the table, return `QSize(320, 512)` as
Expand All @@ -491,6 +495,15 @@ def on_selection_changed(self, selected, deselected):
attrs = selected.indexes()[0].data(self._AttrRole)
self.selectionChanged.emit(attrs)

def on_manual_change(self, attr1, attr2):
model = self.rank_model
self.rank_table.selectionModel().clear()
for row in range(model.rowCount()):
a1, a2 = model.data(model.index(row, 0), self._AttrRole)
if a1 is attr1 and a2 is attr2:
self.rank_table.selectRow(row)
return

def state_count(self):
n_attrs = len(self.attrs)
return n_attrs * (n_attrs - 1) / 2
Expand Down

0 comments on commit 19f9074

Please sign in to comment.