Skip to content

Commit 2c54035

Browse files
authored
Merge pull request #2445 from jerneju/new-linearprojection
[ENH] Linear Projection (LDA, PCA)
2 parents 92c91f3 + 6b932f9 commit 2c54035

File tree

7 files changed

+1188
-1403
lines changed

7 files changed

+1188
-1403
lines changed

Orange/widgets/utils/plot/owplotgui.py

Lines changed: 196 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,210 @@
2727

2828
import os
2929

30-
from Orange.data import ContinuousVariable, DiscreteVariable
30+
import unicodedata
31+
from functools import reduce
32+
from operator import itemgetter
33+
34+
from Orange.data import ContinuousVariable, DiscreteVariable, Variable
3135
from Orange.widgets import gui
36+
from Orange.widgets.utils import itemmodels
37+
from Orange.widgets.utils.listfilter import variables_filter
3238
from Orange.widgets.utils.itemmodels import DomainModel
3339

34-
from AnyQt.QtWidgets import QWidget, QToolButton, QVBoxLayout, QHBoxLayout, QMenu, QAction
35-
from AnyQt.QtGui import QIcon
36-
from AnyQt.QtCore import Qt, pyqtSignal
40+
from AnyQt.QtWidgets import QWidget, QToolButton, QVBoxLayout, QHBoxLayout, QMenu, QAction,\
41+
QDialog, QSizePolicy, QPushButton, QListView
42+
from AnyQt.QtGui import QIcon, QKeySequence
43+
from AnyQt.QtCore import Qt, pyqtSignal, QPoint, QSize
3744

3845
from .owconstants import NOTHING, ZOOMING, SELECT, SELECT_POLYGON, PANNING, SELECTION_ADD,\
3946
SELECTION_REMOVE, SELECTION_TOGGLE, SELECTION_REPLACE
4047

4148

49+
SIZE_POLICY_ADAPTING = (QSizePolicy.Expanding, QSizePolicy.Ignored)
50+
SIZE_POLICY_FIXED = (QSizePolicy.Minimum, QSizePolicy.Maximum)
51+
52+
53+
class AddVariablesDialog(QDialog):
54+
def __init__(self, master, model):
55+
QDialog.__init__(self)
56+
57+
self.master = master
58+
59+
self.setWindowFlags(Qt.Tool)
60+
self.setLayout(QVBoxLayout())
61+
self.setWindowTitle("Hidden Axes")
62+
63+
btns_area = gui.widgetBox(
64+
self, addSpace=0, spacing=9, orientation=Qt.Horizontal,
65+
sizePolicy=QSizePolicy(*SIZE_POLICY_FIXED)
66+
)
67+
self.btn_add = QPushButton(
68+
"Add", autoDefault=False, sizePolicy=QSizePolicy(*SIZE_POLICY_FIXED)
69+
)
70+
self.btn_add.clicked.connect(self._add)
71+
self.btn_cancel = QPushButton(
72+
"Cancel", autoDefault=False, sizePolicy=QSizePolicy(*SIZE_POLICY_FIXED)
73+
)
74+
self.btn_cancel.clicked.connect(self._cancel)
75+
76+
btns_area.layout().addWidget(self.btn_add)
77+
btns_area.layout().addWidget(self.btn_cancel)
78+
79+
filter_edit, view = variables_filter(model=model)
80+
self.view_other = view
81+
view.setMinimumSize(QSize(30, 60))
82+
view.setSizePolicy(*SIZE_POLICY_ADAPTING)
83+
view.viewport().setAcceptDrops(True)
84+
85+
self.layout().addWidget(filter_edit)
86+
self.layout().addWidget(view)
87+
self.layout().addWidget(btns_area)
88+
89+
master = self.master
90+
box = master.box
91+
master.master.setEnabled(False)
92+
self.move(box.mapToGlobal(QPoint(0, box.pos().y() + box.height())))
93+
self.setFixedWidth(master.master.controlArea.width())
94+
self.setMinimumHeight(300)
95+
self.show()
96+
self.raise_()
97+
self.activateWindow()
98+
99+
def _cancel(self):
100+
self.closeEvent(None)
101+
102+
def _add(self):
103+
self.add_variables()
104+
self.closeEvent(None)
105+
106+
def closeEvent(self, QCloseEvent):
107+
self.master.master.setEnabled(True)
108+
super().closeEvent(QCloseEvent)
109+
110+
def keyPressEvent(self, e):
111+
if e.key() == Qt.Key_Escape:
112+
self.closeEvent(None)
113+
elif e.key() in [Qt.Key_Return, Qt.Key_Enter]:
114+
self._add()
115+
else:
116+
super().keyPressEvent(e)
117+
118+
def selected_rows(self, view):
119+
""" Return the selected rows in the view.
120+
"""
121+
rows = view.selectionModel().selectedRows()
122+
model = view.model()
123+
return [model.mapToSource(r) for r in rows]
124+
125+
def add_variables(self):
126+
view = self.view_other
127+
model = self.master.model_other
128+
129+
indices = self.selected_rows(view)
130+
variables = [model.data(ind, Qt.EditRole) for ind in indices]
131+
132+
for i in sorted((ind.row() for ind in indices), reverse=True):
133+
del model[i]
134+
135+
self.master.model_selected.extend(variables)
136+
137+
138+
class VariablesSelection:
139+
def __call__(self, master, model_selected, model_other):
140+
self.master = master
141+
142+
params_view = {"sizePolicy": QSizePolicy(*SIZE_POLICY_ADAPTING),
143+
"selectionMode": QListView.ExtendedSelection,
144+
"dragEnabled": True,
145+
"defaultDropAction": Qt.MoveAction,
146+
"dragDropOverwriteMode": False,
147+
"dragDropMode": QListView.DragDrop}
148+
149+
self.view_selected = view = gui.listView(widget=master.controlArea, master=master,
150+
box="Displayed Axes", **params_view)
151+
view.box.setMinimumHeight(120)
152+
view.viewport().setAcceptDrops(True)
153+
154+
delete = QAction(
155+
"Delete", view,
156+
shortcut=QKeySequence(Qt.Key_Delete),
157+
triggered=self.__deactivate_selection
158+
)
159+
view.addAction(delete)
160+
161+
self.model_selected = model = model_selected
162+
163+
model.rowsInserted.connect(master.invalidate_plot)
164+
model.rowsRemoved.connect(master.invalidate_plot)
165+
model.rowsMoved.connect(master.invalidate_plot)
166+
167+
view.setModel(model)
168+
169+
addClassLabel = QAction("+", master,
170+
toolTip="Add new class label",
171+
triggered=self._action_add)
172+
removeClassLabel = QAction(unicodedata.lookup("MINUS SIGN"), master,
173+
toolTip="Remove selected class label",
174+
triggered=self.__deactivate_selection)
175+
176+
add_remove = itemmodels.ModelActionsWidget([addClassLabel, removeClassLabel], master)
177+
add_remove.layout().addStretch(10)
178+
add_remove.layout().setSpacing(1)
179+
add_remove.setSizePolicy(*SIZE_POLICY_FIXED)
180+
view.box.layout().addWidget(add_remove)
181+
182+
self.add_remove = add_remove
183+
self.box = add_remove.buttons[1]
184+
185+
self.model_other = model_other
186+
187+
def set_enabled(self, is_enabled):
188+
self.view_selected.setEnabled(is_enabled)
189+
for btn in self.add_remove.buttons:
190+
btn.setEnabled(is_enabled)
191+
192+
def display_all(self):
193+
self.model_selected[:] += self.model_other[:]
194+
self.model_other[:] = []
195+
196+
def display_none(self):
197+
self.model_other[:] += self.model_selected[:]
198+
self.model_selected[:] = []
199+
200+
def __deactivate_selection(self):
201+
view = self.view_selected
202+
model = self.model_selected
203+
indices = view.selectionModel().selectedRows()
204+
205+
variables = [model.data(ind, Qt.EditRole) for ind in indices]
206+
207+
for i in sorted((ind.row() for ind in indices), reverse=True):
208+
del model[i]
209+
210+
self.model_other.extend(variables)
211+
212+
def _action_add(self):
213+
self.add_variables_dialog = AddVariablesDialog(self, self.model_other)
214+
215+
@staticmethod
216+
def encode_var_state(lists):
217+
return {(type(var), var.name): (source_ind, pos)
218+
for source_ind, var_list in enumerate(lists)
219+
for pos, var in enumerate(var_list)
220+
if isinstance(var, Variable)}
221+
222+
@staticmethod
223+
def decode_var_state(state, lists):
224+
all_vars = reduce(list.__iadd__, lists, [])
225+
226+
newlists = [[] for _ in lists]
227+
for var in all_vars:
228+
source, pos = state[(type(var), var.name)]
229+
newlists[source].append((pos, var))
230+
return [[var for _, var in sorted(newlist, key=itemgetter(0))]
231+
for newlist in newlists]
232+
233+
42234
class OrientedWidget(QWidget):
43235
'''
44236
A simple QWidget with a box layout that matches its ``orientation``.

Orange/widgets/visualize/owdistributions.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
from Orange.widgets import widget, gui, settings
2424
from Orange.widgets.utils import itemmodels
2525
from Orange.widgets.widget import Input
26-
from Orange.widgets.visualize.owlinearprojection import LegendItem, ScatterPlotItem
2726

28-
from Orange.widgets.visualize.owscatterplotgraph import HelpEventDelegate
27+
from Orange.widgets.visualize.owscatterplotgraph import LegendItem as SPGLegendItem,\
28+
HelpEventDelegate
2929

3030

3131
def selected_index(view):
@@ -43,6 +43,62 @@ def selected_index(view):
4343
return -1
4444

4545

46+
class ScatterPlotItem(pg.ScatterPlotItem):
47+
Symbols = pg.graphicsItems.ScatterPlotItem.Symbols
48+
49+
def paint(self, painter, option, widget=None):
50+
if self.opts["pxMode"]:
51+
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
52+
53+
if self.opts["antialias"]:
54+
painter.setRenderHint(QPainter.Antialiasing, True)
55+
56+
super().paint(painter, option, widget)
57+
58+
59+
class LegendItem(SPGLegendItem):
60+
def __init__(self):
61+
super().__init__()
62+
self.items = []
63+
64+
def clear(self):
65+
"""
66+
Clear all legend items.
67+
"""
68+
items = list(self.items)
69+
self.items = []
70+
for sample, label in items:
71+
# yes, the LegendItem shadows QGraphicsWidget.layout() with
72+
# an instance attribute.
73+
self.layout.removeItem(sample)
74+
self.layout.removeItem(label)
75+
sample.hide()
76+
label.hide()
77+
78+
self.updateSize()
79+
80+
def mousePressEvent(self, event):
81+
if event.button() == Qt.LeftButton:
82+
event.accept()
83+
else:
84+
event.ignore()
85+
86+
def mouseMoveEvent(self, event):
87+
if event.buttons() & Qt.LeftButton:
88+
event.accept()
89+
if self.parentItem() is not None:
90+
self.autoAnchor(
91+
self.pos() + (event.pos() - event.lastPos()) / 2)
92+
else:
93+
event.ignore()
94+
95+
def mouseReleaseEvent(self, event):
96+
if event.button() == Qt.LeftButton:
97+
event.accept()
98+
else:
99+
event.ignore()
100+
101+
46102
class DistributionBarItem(pg.GraphicsObject):
47103
def __init__(self, geometry, dist, colors):
48104
super().__init__()

0 commit comments

Comments
 (0)