Skip to content

Commit 20a860c

Browse files
committed
Refactoring of Sieve, Mosaic and VizRank.
Extract OWWidget's methods for progress bars to mix-in class `Orange.widgets.utils.progressbar.ProgressBarMixin`. This simplifies `OWWidget` and also allows other dialogs (e.g. `VizRankDialog`) to use progress bars without being derived from `OWWidget`. Move `HorizontalGridDelegate` from `OWColor` to `gui` - it was also used in the `OWFile` 's domain editor and now in VizRank. Move `CanvasText`, `CanvasRectangle` and `ViewWithPress` from `OWMosaic` to `orange.widgets.visualize.utils` since they were used by Sieve and Mosaic (and may also be used elsewhere). Move the common functionality of Scatter Plot's and Sieve's VizRank to more `VizRankDialog` and `VizRankDialogAttrPair` (module `orange.widgets.visualiza.utils`. The classes are also general enough to also support ranking of other visualizations in the future. Derive `VizRankDialog` from `QDialog` (with the progress bar mix-in) instead of `OWWidget` with all its balast. Visually improve VizRankDialog. Remove the uninformative score. Remove the redundant dictionary `ScaleData.attribute_name_index` since it duplicates the functionality of `ScaleData.data_domain.index`. Fix the type of argument `buttonType` and the return type in docstring of `gui.button`. Rename `Orange.widget.utils.getHtmlCompatibleString` to `to_html`.
1 parent e60b140 commit 20a860c

File tree

13 files changed

+737
-636
lines changed

13 files changed

+737
-636
lines changed

Orange/widgets/data/owcolor.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
1-
"""
2-
Widget for assigning colors to variables
3-
"""
4-
5-
from PyQt4.QtCore import Qt, QAbstractTableModel, QSize
6-
from PyQt4.QtGui import QStyledItemDelegate, QColor, QHeaderView, QFont, \
7-
QColorDialog, QTableView, qRgb, QImage, QBrush, QApplication
81
import numpy as np
2+
from PyQt4.QtCore import Qt, QAbstractTableModel, QSize
3+
from PyQt4.QtGui import (
4+
QColor, QHeaderView, QFont, QColorDialog, QTableView, qRgb, QImage,
5+
QBrush)
96

107
import Orange
118
from Orange.widgets import widget, settings, gui
9+
from Orange.widgets.gui import HorizontalGridDelegate
1210
from Orange.widgets.utils.colorpalette import \
1311
ContinuousPaletteGenerator, ColorPaletteDlg
1412

1513
ColorRole = next(gui.OrangeUserRole)
1614

1715

18-
class HorizontalGridDelegate(QStyledItemDelegate):
19-
"""Delegate that draws a horizontal grid."""
20-
def paint(self, painter, option, index):
21-
# pylint: disable=missing-docstring
22-
painter.save()
23-
painter.setPen(QColor(212, 212, 212))
24-
painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight())
25-
painter.restore()
26-
QStyledItemDelegate.paint(self, painter, option, index)
27-
28-
2916
# noinspection PyMethodOverriding
3017
class ColorTableModel(QAbstractTableModel):
3118
"""Base color model for discrete and continuous attributes. The model

Orange/widgets/gui.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from PyQt4 import QtGui, QtCore
1515
from PyQt4.QtCore import Qt, pyqtSignal as Signal
1616
from PyQt4.QtGui import QCursor, QApplication, QTableView, QHeaderView, \
17-
QStyledItemDelegate, QSizePolicy
17+
QStyledItemDelegate, QSizePolicy, QColor
1818

1919
# Some Orange widgets might expect this here
2020
from Orange.widgets.webview import WebView as WebviewWidget # pylint: disable=unused-import
@@ -1074,8 +1074,8 @@ def button(widget, master, label, callback=None, width=None, height=None,
10741074
activated on pressing Return.
10751075
:type autoDefault: bool
10761076
:param buttonType: the button type (default: `QPushButton`)
1077-
:type buttonType: PyQt4.QtGui.QAbstractButton
1078-
:rtype: PyQt4.QtGui.QAbstractButton
1077+
:type buttonType: PyQt4.QtGui.QPushButton
1078+
:rtype: PyQt4.QtGui.QPushButton
10791079
"""
10801080
button = buttonType(widget)
10811081
if label:
@@ -3096,6 +3096,15 @@ def get_bar_brush(self, _, index):
30963096
return QtGui.QBrush(bar_brush)
30973097

30983098

3099+
class HorizontalGridDelegate(QStyledItemDelegate):
3100+
def paint(self, painter, option, index):
3101+
painter.save()
3102+
painter.setPen(QColor(212, 212, 212))
3103+
painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight())
3104+
painter.restore()
3105+
QStyledItemDelegate.paint(self, painter, option, index)
3106+
3107+
30993108
class VerticalLabel(QtGui.QLabel):
31003109
def __init__(self, text, parent=None):
31013110
super().__init__(text, parent)

Orange/widgets/utils/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,9 @@ def getdeepattr(obj, attr, *arg, **kwarg):
3131
return kwarg["default"]
3232
raise
3333

34-
def getHtmlCompatibleString(strVal):
35-
return strVal.replace("<=", "&#8804;").replace(">=","&#8805;").replace("<", "&#60;").replace(">","&#62;").replace("=\\=", "&#8800;")
34+
35+
def to_html(str):
36+
return str.replace("<=", "&#8804;").replace(">=", "&#8805;").\
37+
replace("<", "&#60;").replace(">", "&#62;").replace("=\\=", "&#8800;")
38+
39+
getHtmlCompatibleString = to_html

Orange/widgets/utils/domaineditor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from Orange.data import DiscreteVariable, ContinuousVariable, StringVariable, \
55
TimeVariable
66
from Orange.widgets import gui
7-
from Orange.widgets.data.owcolor import HorizontalGridDelegate
7+
from Orange.widgets.gui import HorizontalGridDelegate
88
from Orange.widgets.utils.itemmodels import TableModel
99

1010

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import contextlib
2+
import time
3+
import warnings
4+
5+
from PyQt4.QtCore import pyqtSignal as Signal, pyqtProperty, QEventLoop
6+
from PyQt4.QtGui import qApp
7+
8+
from Orange.widgets import gui
9+
10+
class ProgressBarMixin:
11+
# Set these here so we avoid having to call `__init__` fromm classes
12+
# that use this mix-in
13+
__progressBarValue = -1
14+
__progressState = 0
15+
startTime = time.time() # used in progressbar
16+
17+
def progressBarInit(self, processEvents=QEventLoop.AllEvents):
18+
"""
19+
Initialize the widget's progress (i.e show and set progress to 0%).
20+
21+
.. note::
22+
This method will by default call `QApplication.processEvents`
23+
with `processEvents`. To suppress this behavior pass
24+
``processEvents=None``.
25+
26+
:param processEvents: Process events flag
27+
:type processEvents: `QEventLoop.ProcessEventsFlags` or `None`
28+
"""
29+
self.startTime = time.time()
30+
self.setWindowTitle(self.captionTitle + " (0% complete)")
31+
32+
if self.__progressState != 1:
33+
self.__progressState = 1
34+
self.processingStateChanged.emit(1)
35+
36+
self.progressBarSet(0, processEvents)
37+
38+
def progressBarSet(self, value, processEvents=QEventLoop.AllEvents):
39+
"""
40+
Set the current progress bar to `value`.
41+
42+
.. note::
43+
This method will by default call `QApplication.processEvents`
44+
with `processEvents`. To suppress this behavior pass
45+
``processEvents=None``.
46+
47+
:param float value: Progress value
48+
:param processEvents: Process events flag
49+
:type processEvents: `QEventLoop.ProcessEventsFlags` or `None`
50+
"""
51+
old = self.__progressBarValue
52+
self.__progressBarValue = value
53+
54+
if value > 0:
55+
if self.__progressState != 1:
56+
warnings.warn("progressBarSet() called without a "
57+
"preceding progressBarInit()",
58+
stacklevel=2)
59+
self.__progressState = 1
60+
self.processingStateChanged.emit(1)
61+
62+
usedTime = max(1, time.time() - self.startTime)
63+
totalTime = 100.0 * usedTime / value
64+
remainingTime = max(0, int(totalTime - usedTime))
65+
hrs = remainingTime // 3600
66+
mins = (remainingTime % 3600) // 60
67+
secs = remainingTime % 60
68+
if hrs > 0:
69+
text = "{}:{:02}:{:02}".format(hrs, mins, secs)
70+
else:
71+
text = "{}:{}:{:02}".format(hrs, mins, secs)
72+
self.setWindowTitle("{} ({:d}%, ETA: {})"
73+
.format(self.captionTitle, value, text))
74+
else:
75+
self.setWindowTitle(self.captionTitle + " (0% complete)")
76+
77+
if old != value:
78+
self.progressBarValueChanged.emit(value)
79+
80+
if processEvents is not None and processEvents is not False:
81+
qApp.processEvents(processEvents)
82+
83+
def progressBarValue(self):
84+
"""Return the state of the progress bar
85+
"""
86+
return self.__progressBarValue
87+
88+
progressBarValue = pyqtProperty(
89+
float, fset=progressBarSet, fget=progressBarValue)
90+
processingState = pyqtProperty(int, fget=lambda self: self.__progressState)
91+
92+
def progressBarAdvance(self, value, processEvents=QEventLoop.AllEvents):
93+
"""
94+
Advance the progress bar.
95+
96+
.. note::
97+
This method will by default call `QApplication.processEvents`
98+
with `processEvents`. To suppress this behavior pass
99+
``processEvents=None``.
100+
101+
Args:
102+
value (int): progress value
103+
processEvents (`QEventLoop.ProcessEventsFlags` or `None`):
104+
process events flag
105+
"""
106+
self.progressBarSet(self.progressBarValue + value, processEvents)
107+
108+
def progressBarFinished(self, processEvents=QEventLoop.AllEvents):
109+
"""
110+
Stop the widget's progress (i.e hide the progress bar).
111+
112+
.. note::
113+
This method will by default call `QApplication.processEvents`
114+
with `processEvents`. To suppress this behavior pass
115+
``processEvents=None``.
116+
117+
:param processEvents: Process events flag
118+
:type processEvents: `QEventLoop.ProcessEventsFlags` or `None`
119+
"""
120+
self.setWindowTitle(self.captionTitle)
121+
if self.__progressState != 0:
122+
self.__progressState = 0
123+
self.processingStateChanged.emit(0)
124+
125+
if processEvents is not None and processEvents is not False:
126+
qApp.processEvents(processEvents)
127+
128+
@contextlib.contextmanager
129+
def progressBar(self, iterations=0):
130+
"""
131+
Context manager for progress bar.
132+
133+
Using it ensures that the progress bar is removed at the end without
134+
needing the `finally` blocks.
135+
136+
Usage:
137+
138+
with self.progressBar(20) as progress:
139+
...
140+
progress.advance()
141+
142+
or
143+
144+
with self.progressBar() as progress:
145+
...
146+
progress.advance(0.15)
147+
148+
or
149+
150+
with self.progressBar():
151+
...
152+
self.progressBarSet(50)
153+
154+
:param iterations: the number of iterations (optional)
155+
:type iterations: int
156+
"""
157+
progress_bar = gui.ProgressBar(self, iterations)
158+
yield progress_bar
159+
progress_bar.finish() # Let us not rely on garbage collector

Orange/widgets/utils/scaling.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ class ScaleData:
5555
def __init__(self):
5656
self.raw_data = None # input data
5757
self.attribute_names = [] # list of attribute names from self.raw_data
58-
self.attribute_name_index = {} # dict with indices to attributes
5958
self.attribute_flip_info = {} # dictionary with attrName: 0/1 attribute is flipped or not
6059

6160
self.data_has_class = False
@@ -111,8 +110,6 @@ def set_data(self, data, **args):
111110
len_data = data and len(data) or 0
112111

113112
self.attribute_names = [attr.name for attr in full_data.domain]
114-
self.attribute_name_index = dict([(full_data.domain[i].name, i)
115-
for i in range(len(full_data.domain))])
116113
self.attribute_flip_info = {}
117114

118115
self.data_domain = full_data.domain
@@ -122,7 +119,7 @@ def set_data(self, data, **args):
122119

123120
self.data_class_name = self.data_has_class and full_data.domain.class_var.name
124121
if self.data_has_class:
125-
self.data_class_index = self.attribute_name_index[self.data_class_name]
122+
self.data_class_index = self.data_domain.index(self.data_class_name)
126123
self.have_data = bool(self.raw_data and len(self.raw_data) > 0)
127124

128125
self.domain_data_stat = getCached(full_data,
@@ -244,7 +241,7 @@ def flip_attribute(self, attr_name):
244241
if self.data_domain[attr_name].is_discrete:
245242
return 0
246243

247-
index = self.attribute_name_index[attr_name]
244+
index = self.data_domain.index(attr_name)
248245
self.attribute_flip_info[attr_name] = 1 - self.attribute_flip_info.get(attr_name, 0)
249246
if self.data_domain[attr_name].is_continuous:
250247
self.attr_values[attr_name] = [-self.attr_values[attr_name][1], -self.attr_values[attr_name][0]]
@@ -307,8 +304,8 @@ def get_xy_data_positions(self, xattr, yattr, filter_valid=False,
307304
Create x-y projection of attributes in attrlist.
308305
309306
"""
310-
xattr_index = self.attribute_name_index[xattr]
311-
yattr_index = self.attribute_name_index[yattr]
307+
xattr_index = self.data_domain.index(xattr)
308+
yattr_index = self.data_domain.index(yattr)
312309
if filter_valid is True:
313310
filter_valid = self.get_valid_list([xattr_index, yattr_index])
314311
if isinstance(filter_valid, np.ndarray):
@@ -494,9 +491,9 @@ def get_optimal_clusters(self, attribute_name_order, add_result_funct):
494491
for i in range(len(attribute_name_order)):
495492
for j in range(i):
496493
try:
497-
index = self.attribute_name_index
498-
attr1 = index[attribute_name_order[j]]
499-
attr2 = index[attribute_name_order[i]]
494+
index = self.data_domain.index
495+
attr1 = index(attribute_name_order[j])
496+
attr2 = index(attribute_name_order[i])
500497
test_index += 1
501498
if self.clusterOptimization.isOptimizationCanceled():
502499
secs = time.time() - start_time

0 commit comments

Comments
 (0)