Skip to content

Commit ae1c575

Browse files
committed
ScoreTable: Add indicator to select scores
1 parent 6d5f0ac commit ae1c575

File tree

3 files changed

+115
-57
lines changed

3 files changed

+115
-57
lines changed

Orange/widgets/evaluate/tests/test_owtestandscore.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ def __call__(self, data):
325325
header = view.horizontalHeader()
326326
p = header.rect().center()
327327
# second visible header section (after 'Model')
328-
_, idx, *_ = (i for i in range(header.count())
328+
_, _, idx, *_ = (i for i in range(header.count())
329329
if not header.isSectionHidden(i))
330330
p.setX(header.sectionPosition(idx) + 5)
331331
QTest.mouseClick(header.viewport(), Qt.LeftButton, pos=p)

Orange/widgets/evaluate/tests/test_utils.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ class NewScore(Score):
5656
Specificity=False, NewScore=True))
5757
self.score_table = ScoreTable(None)
5858
self.score_table.update_header([F1, CA, AUC, Specificity, NewScore])
59-
self.score_table._update_shown_columns()
6059

6160
def tearDown(self):
6261
ScoreTable.show_score_hints = self.orig_hints
@@ -75,20 +74,19 @@ def addAction(menu, a):
7574
def execmenu(*_):
7675
# pylint: disable=unsubscriptable-object,unsupported-assignment-operation
7776
scorers = [F1, CA, AUC, Specificity, self.NewScore]
78-
self.assertEqual(list(actions)[2:], ['F1',
77+
self.assertEqual(list(actions)[3:], ['F1',
7978
'Classification accuracy (CA)',
8079
'Area under ROC curve (AUC)',
8180
'Specificity (Spec)',
8281
'new score'])
8382
header = self.score_table.view.horizontalHeader()
84-
for i, action, scorer in zip(count(), actions.values(), scorers):
85-
if i >= 2:
86-
self.assertEqual(action.isChecked(),
87-
hints[scorer.__name__],
88-
msg=f"error in section {scorer.name}")
89-
self.assertEqual(header.isSectionHidden(i),
90-
hints[scorer.__name__],
91-
msg=f"error in section {scorer.name}")
83+
for i, action, scorer in zip(count(), list(actions.values())[3:], scorers):
84+
self.assertEqual(action.isChecked(),
85+
hints[scorer.__name__],
86+
msg=f"error in section {scorer.name}")
87+
self.assertEqual(header.isSectionHidden(3 + i),
88+
not hints[scorer.__name__],
89+
msg=f"error in section {scorer.name}")
9290
actions["Classification accuracy (CA)"].triggered.emit(True)
9391
hints["CA"] = True
9492
for k, v in hints.items():
@@ -107,7 +105,7 @@ def execmenu(*_):
107105
# `menuexec` finishes.
108106
with patch("AnyQt.QtWidgets.QMenu.addAction", addAction), \
109107
patch("AnyQt.QtWidgets.QMenu.exec", execmenu):
110-
self.score_table.show_column_chooser(QPoint(0, 0))
108+
self.score_table.view.horizontalHeader().show_column_chooser(QPoint(0, 0))
111109

112110
def test_sorting(self):
113111
def order(n=5):

Orange/widgets/evaluate/utils.py

+105-45
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
from typing import Union, Dict, List
44

55
import numpy as np
6+
from sklearn.exceptions import UndefinedMetricWarning
67

78
from AnyQt.QtWidgets import QHeaderView, QStyledItemDelegate, QMenu, \
8-
QApplication
9-
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QClipboard
9+
QApplication, QToolButton
10+
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QClipboard, QColor
1011
from AnyQt.QtCore import Qt, QSize, QObject, pyqtSignal as Signal, \
1112
QSortFilterProxyModel
12-
from sklearn.exceptions import UndefinedMetricWarning
13+
14+
from orangewidget.gui import OrangeUserRole
1315

1416
from Orange.data import Domain, Variable
1517
from Orange.evaluation import scoring
@@ -128,7 +130,84 @@ def is_bad(x):
128130
return left < right
129131

130132

131-
DEFAULT_HINTS = {"Model_": True, "Train": False, "Test": False}
133+
DEFAULT_HINTS = {"Model_": True, "Train_": False, "Test_": False}
134+
135+
136+
class PersistentMenu(QMenu):
137+
def mouseReleaseEvent(self, e):
138+
action = self.activeAction()
139+
if action:
140+
action.setEnabled(False)
141+
super().mouseReleaseEvent(e)
142+
action.setEnabled(True)
143+
action.trigger()
144+
else:
145+
super().mouseReleaseEvent(e)
146+
147+
148+
class SelectableColumnsHeader(QHeaderView):
149+
SelectMenuRole = next(OrangeUserRole)
150+
ShownHintRole = next(OrangeUserRole)
151+
sectionVisibleChanged = Signal(int, bool)
152+
153+
def __init__(self, shown_columns_hints, *args, **kwargs):
154+
super().__init__(Qt.Horizontal, *args, **kwargs)
155+
self.show_column_hints = shown_columns_hints
156+
self.button = QToolButton(self)
157+
self.button.setArrowType(Qt.DownArrow)
158+
self.button.setFixedSize(24, 12)
159+
col = self.button.palette().color(self.button.backgroundRole())
160+
self.button.setStyleSheet(
161+
f"border: none; background-color: {col.name(QColor.NameFormat.HexRgb)}")
162+
self.setContextMenuPolicy(Qt.CustomContextMenu)
163+
self.customContextMenuRequested.connect(self.show_column_chooser)
164+
self.button.clicked.connect(self._on_button_clicked)
165+
166+
def showEvent(self, e):
167+
self._set_pos()
168+
self.button.show()
169+
super().showEvent(e)
170+
171+
def resizeEvent(self, e):
172+
self._set_pos()
173+
super().resizeEvent(e)
174+
175+
def _set_pos(self):
176+
w, h = self.button.width(), self.button.height()
177+
vw, vh = self.viewport().width(), self.viewport().height()
178+
self.button.setGeometry(vw - w, (vh - h) // 2, w, h)
179+
180+
def __data(self, section, role):
181+
return self.model().headerData(section, Qt.Horizontal, role)
182+
183+
def show_column_chooser(self, pos):
184+
# pylint: disable=unsubscriptable-object, unsupported-assignment-operation
185+
menu = PersistentMenu()
186+
for section in range(self.count()):
187+
name, enabled = self.__data(section, self.SelectMenuRole)
188+
hint_id = self.__data(section, self.ShownHintRole)
189+
action = menu.addAction(name)
190+
action.setDisabled(not enabled)
191+
action.setCheckable(True)
192+
action.setChecked(self.show_column_hints[hint_id])
193+
194+
@action.triggered.connect # pylint: disable=cell-var-from-loop
195+
def update(checked, q=hint_id, section=section):
196+
self.show_column_hints[q] = checked
197+
self.setSectionHidden(section, not checked)
198+
self.sectionVisibleChanged.emit(section, checked)
199+
self.resizeSections(self.ResizeToContents)
200+
201+
pos.setY(self.viewport().height())
202+
menu.exec(self.mapToGlobal(pos))
203+
204+
def _on_button_clicked(self):
205+
self.show_column_chooser(self.button.pos())
206+
207+
def update_shown_columns(self):
208+
for section in range(self.count()):
209+
hint_id = self.__data(section, self.ShownHintRole)
210+
self.setSectionHidden(section, not self.show_column_hints[hint_id])
132211

133212

134213
class ScoreTable(OWComponent, QObject):
@@ -138,6 +217,7 @@ class ScoreTable(OWComponent, QObject):
138217
# backwards compatibility
139218
@property
140219
def shown_scores(self):
220+
# pylint: disable=unsubscriptable-object
141221
column_names = {
142222
self.model.horizontalHeaderItem(col).data(Qt.DisplayRole)
143223
for col in range(1, self.model.columnCount())}
@@ -166,65 +246,45 @@ def __init__(self, master):
166246
header.setSectionResizeMode(QHeaderView.ResizeToContents)
167247
header.setDefaultAlignment(Qt.AlignCenter)
168248
header.setStretchLastSection(False)
169-
header.setContextMenuPolicy(Qt.CustomContextMenu)
170-
header.customContextMenuRequested.connect(self.show_column_chooser)
171249

172250
for score in Score.registry.values():
173251
self.show_score_hints.setdefault(score.__name__, score.default_visible)
174252

175253
self.model = QStandardItemModel(master)
176-
self.model.setHorizontalHeaderLabels(["Method"])
254+
header = SelectableColumnsHeader(self.show_score_hints)
255+
header.setSectionsClickable(True)
256+
self.view.setHorizontalHeader(header)
177257
self.sorted_model = ScoreModel()
178258
self.sorted_model.setSourceModel(self.model)
179259
self.view.setModel(self.sorted_model)
180260
self.view.setItemDelegate(self.ItemDelegate())
181-
182-
def show_column_chooser(self, pos):
183-
menu = QMenu()
184-
header = self.view.horizontalHeader()
185-
for col in range(1, self.model.columnCount()):
186-
item = self.model.horizontalHeaderItem(col)
187-
qualname = item.data(Qt.UserRole)
188-
if col < 3:
189-
option = item.data(Qt.DisplayRole)
190-
else:
191-
score = Score.registry[qualname]
192-
option = score.long_name
193-
if score.name != score.long_name:
194-
option += f" ({score.name})"
195-
action = menu.addAction(option)
196-
action.setCheckable(True)
197-
action.setChecked(self.show_score_hints[qualname])
198-
199-
@action.triggered.connect
200-
def update(checked, q=qualname):
201-
self.show_score_hints[q] = checked
202-
self._update_shown_columns()
203-
204-
menu.exec(header.mapToGlobal(pos))
205-
206-
def _update_shown_columns(self):
207-
self.view.resizeColumnsToContents()
208-
header = self.view.horizontalHeader()
209-
for section in range(1, header.count()):
210-
qualname = self.model.horizontalHeaderItem(section).data(Qt.UserRole)
211-
header.setSectionHidden(section, not self.show_score_hints[qualname])
212-
self.shownScoresChanged.emit()
261+
header.sectionVisibleChanged.connect(self.shownScoresChanged.emit)
262+
self.sorted_model.dataChanged.connect(self.view.resizeColumnsToContents)
213263

214264
def update_header(self, scorers: List[Score]):
215265
self.model.setColumnCount(3 + len(scorers))
216-
for i, name, id_ in ((0, "Model", "Model_"),
217-
(1, "Train time [s]", "Train"),
218-
(2, "Test time [s]", "Test")):
266+
SelectMenuRole = SelectableColumnsHeader.SelectMenuRole
267+
ShownHintRole = SelectableColumnsHeader.ShownHintRole
268+
for i, name, long_name, id_, in ((0, "Model", "Model", "Model_"),
269+
(1, "Train", "Train time [s]", "Train_"),
270+
(2, "Test", "Test time [s]", "Test_")):
219271
item = QStandardItem(name)
220-
item.setData(id_, Qt.UserRole)
272+
item.setData((long_name, i != 0), SelectMenuRole)
273+
item.setData(id_, ShownHintRole)
274+
item.setToolTip(long_name)
221275
self.model.setHorizontalHeaderItem(i, item)
222276
for col, score in enumerate(scorers, start=3):
223277
item = QStandardItem(score.name)
224-
item.setData(score.__name__, Qt.UserRole)
278+
name = score.long_name
279+
if name != score.name:
280+
name += f" ({score.name})"
281+
item.setData((name, True), SelectMenuRole)
282+
item.setData(score.__name__, ShownHintRole)
225283
item.setToolTip(score.long_name)
226284
self.model.setHorizontalHeaderItem(col, item)
227-
self._update_shown_columns()
285+
286+
self.view.horizontalHeader().update_shown_columns()
287+
self.view.resizeColumnsToContents()
228288

229289
def copy_selection_to_clipboard(self):
230290
mime = table_selection_to_mime_data(self.view)

0 commit comments

Comments
 (0)