Skip to content

Commit 915fe6e

Browse files
committed
Create class: Add widget
1 parent bd783f3 commit 915fe6e

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import numpy as np
2+
3+
from AnyQt.QtWidgets import QGridLayout, QLabel, QLineEdit, QSizePolicy
4+
from AnyQt.QtCore import QSize, Qt
5+
6+
from Orange.data import StringVariable, DiscreteVariable, Domain
7+
from Orange.data.table import Table
8+
from Orange.preprocess.transformation import Transformation
9+
from Orange.widgets import gui, widget
10+
from Orange.widgets.settings import DomainContextHandler, ContextSetting
11+
from Orange.widgets.utils.itemmodels import DomainModel
12+
from Orange.widgets.widget import Msg
13+
14+
15+
class ValueFromSubstring(Transformation):
16+
def __init__(self, variable, values):
17+
super().__init__(variable)
18+
self.values = values
19+
20+
def transform(self, c):
21+
c = c.flatten()
22+
res = np.empty(len(c))
23+
for i, val in enumerate(c):
24+
for res[i], pattern in enumerate(self.values):
25+
if pattern in val:
26+
break
27+
else:
28+
res[i] = np.nan
29+
return res
30+
31+
32+
class OWCreateClass(widget.OWWidget):
33+
name = "Create class"
34+
description = "Create class attribute from a string attribute"
35+
icon = "icons/CreateClass.svg"
36+
category = "Data"
37+
keywords = ["data"]
38+
39+
inputs = [("Data", Table, "set_data")]
40+
outputs = [("Data", Table)]
41+
42+
want_main_area = False
43+
44+
settingsHandler = DomainContextHandler()
45+
attribute = ContextSetting(None)
46+
47+
class Warning(widget.OWWidget.Warning):
48+
no_string_variables = Msg("No string variables.")
49+
50+
def __init__(self):
51+
super().__init__()
52+
self.data = None
53+
self.rules = [["C1", ""], ["C2", ""]]
54+
self.line_edits = []
55+
self.remove_buttons = []
56+
self.counts = []
57+
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
58+
59+
box = gui.hBox(self.controlArea)
60+
gui.widgetLabel(box, "Class from column: ", addSpace=12)
61+
gui.comboBox(
62+
box, self, "attribute", callback=self.apply,
63+
model=DomainModel(valid_types=StringVariable),
64+
sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))
65+
66+
self.rules_box = rules_box = QGridLayout()
67+
self.controlArea.layout().addLayout(self.rules_box)
68+
self.add_button = gui.button(None, self, "+", flat=True,
69+
callback=self.add_row,
70+
minimumSize=QSize(12, 20))
71+
self.rules_box.setColumnMinimumWidth(1, 80)
72+
self.rules_box.setColumnMinimumWidth(0, 10)
73+
self.rules_box.setColumnStretch(0, 1)
74+
self.rules_box.setColumnStretch(1, 1)
75+
self.rules_box.setColumnStretch(2, 100)
76+
rules_box.addWidget(QLabel("Name"), 0, 1)
77+
rules_box.addWidget(QLabel("Pattern"), 0, 2)
78+
rules_box.addWidget(QLabel("#Instances"), 0, 3, 1, 2)
79+
self.update_rules()
80+
81+
box = gui.hBox(self.controlArea)
82+
gui.rubber(box)
83+
gui.button(box, self, "Apply", autoDefault=False, callback=self.apply)
84+
85+
def retrieveSpecificSettings(self):
86+
self.rules = list(self.current_context.rules)
87+
self.update_rules()
88+
89+
def storeSpecificSettings(self):
90+
self.current_context.rules = tuple(self.rules)
91+
92+
def rules_to_edits(self):
93+
for editr, textr in zip(self.line_edits, self.rules):
94+
for edit, text in zip(editr, textr):
95+
edit.setText(text)
96+
97+
def edits_to_rule(self):
98+
self.rules = [[edit.text() for edit in row] for row in self.line_edits]
99+
100+
def set_data(self, data):
101+
# We don't reset the rules before opening new context, thus keeping the
102+
# existing rules. This makes sense within a lifetime of a widget, and
103+
# it doesn't extend past it.
104+
self.closeContext()
105+
self.data = data
106+
model = self.controls.attribute.model()
107+
model.set_domain(data and data.domain)
108+
self.Warning.no_string_variables(shown=data is not None and not model)
109+
if not model:
110+
self.attribute = None
111+
self.send("Data", None)
112+
return
113+
self.attribute = model[0]
114+
self.openContext(data)
115+
self.update_rules()
116+
117+
def update_rules(self):
118+
self.adjust_n_rule_rows()
119+
self.rules_to_edits()
120+
self.update_counts()
121+
122+
def adjust_n_rule_rows(self):
123+
def _add_line():
124+
self.line_edits.append([])
125+
n_lines = len(self.line_edits)
126+
for coli in range(1, 3):
127+
edit = QLineEdit()
128+
self.line_edits[-1].append(edit)
129+
self.rules_box.addWidget(edit, n_lines, coli)
130+
edit.textChanged.connect(self.sync_edit)
131+
button = gui.button(
132+
None, self, label='×', flat=True, height=20,
133+
styleSheet='* {font-size: 16pt; color: silver}'
134+
'*:hover {color: black}',
135+
callback=self.remove_row)
136+
button.setMinimumSize(QSize(12, 20))
137+
self.remove_buttons.append(button)
138+
self.rules_box.addWidget(button, n_lines, 0)
139+
self.counts.append([])
140+
for coli in range(3, 5):
141+
label = QLabel(alignment=Qt.AlignRight if coli == 3
142+
else Qt.AlignLeft)
143+
self.counts[-1].append(label)
144+
self.rules_box.addWidget(label, n_lines, coli)
145+
146+
def _remove_line():
147+
for edit in self.line_edits.pop():
148+
edit.deleteLater()
149+
self.remove_buttons.pop().deleteLater()
150+
for label in self.counts.pop():
151+
label.deleteLater()
152+
153+
def _fix_tab_order():
154+
prev = None
155+
for row, rule in zip(self.line_edits, self.rules):
156+
for col_idx, edit in enumerate(row):
157+
edit.row, edit.col_idx = rule, col_idx
158+
if prev is not None:
159+
self.setTabOrder(prev, edit)
160+
prev = edit
161+
162+
n = len(self.rules)
163+
while n > len(self.line_edits):
164+
_add_line()
165+
while len(self.line_edits) > n:
166+
_remove_line()
167+
self.rules_box.addWidget(self.add_button, len(self.rules) + 1, 0)
168+
_fix_tab_order()
169+
170+
def add_row(self):
171+
self.rules.append(["", ""])
172+
self.adjust_n_rule_rows()
173+
174+
def remove_row(self):
175+
remove_idx = self.remove_buttons.index(self.sender())
176+
del self.rules[remove_idx]
177+
self.update_rules()
178+
179+
def sync_edit(self, text):
180+
edit = self.sender()
181+
edit.row[edit.col_idx] = text
182+
self.update_counts()
183+
184+
def update_counts(self):
185+
if not self.attribute:
186+
for label_row in self.counts:
187+
for label in label_row:
188+
label.setText("")
189+
return
190+
data = [str(x) for x in self.data[:, self.attribute]]
191+
remaining = data.copy()
192+
for (lab_match, lab_all), (_, pattern) in zip(self.counts, self.rules):
193+
new_remaining = [x for x in remaining if pattern not in x]
194+
n_matched = len(remaining) - len(new_remaining)
195+
n_match_in_all = sum(pattern in x for x in data)
196+
lab_match.setText("{}".format(n_matched) if remaining else "")
197+
lab_all.setText("({})".format(n_match_in_all)
198+
if remaining and n_match_in_all != n_matched
199+
else "")
200+
remaining = new_remaining
201+
202+
def apply(self):
203+
if not self.data:
204+
return
205+
domain = self.data.domain
206+
names, patterns = \
207+
zip(*((name.strip(), pattern)
208+
for name, pattern in self.rules if name.strip()))
209+
new_class = DiscreteVariable(
210+
"class", names,
211+
compute_value=ValueFromSubstring(self.attribute, patterns))
212+
new_domain = Domain(domain.attributes, new_class,
213+
domain.metas + domain.class_vars)
214+
new_data = Table(new_domain, self.data)
215+
self.send("Data", new_data)
216+
217+
218+
def main():
219+
import sys
220+
from AnyQt.QtWidgets import QApplication
221+
222+
a = QApplication(sys.argv)
223+
table = Table("zoo")
224+
ow = OWCreateClass()
225+
ow.show()
226+
ow.set_data(table)
227+
a.exec()
228+
ow.saveSettings()
229+
230+
if __name__ == "__main__":
231+
main()

0 commit comments

Comments
 (0)