Skip to content

Commit 28a2430

Browse files
committed
owwditdomain: Add support for ordered categorical values
1 parent e6c793a commit 28a2430

File tree

1 file changed

+55
-11
lines changed

1 file changed

+55
-11
lines changed

Orange/widgets/data/oweditdomain.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,
2121
QToolButton, QLineEdit, QAction, QActionGroup, QStackedWidget, QGroupBox,
2222
QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy, QToolTip,
23-
QDialogButtonBox, QPushButton
23+
QDialogButtonBox, QPushButton, QCheckBox
2424
)
2525
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QKeySequence, QIcon
2626
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
@@ -51,6 +51,10 @@ class Categorical(
5151
])): pass
5252

5353

54+
class Ordered(Categorical):
55+
pass
56+
57+
5458
class Real(
5559
NamedTuple("Real", [
5660
("name", str),
@@ -130,8 +134,14 @@ def __call__(self, var):
130134
return var._replace(annotations=self.annotations)
131135

132136

133-
Transform = Union[Rename, CategoriesMapping, Annotate]
134-
TransformTypes = (Rename, CategoriesMapping, Annotate)
137+
class ChangeOrdered(NamedTuple("ChangeOrdered", [("ordered", bool)])):
138+
"""
139+
Change Categorical <-> Ordered
140+
"""
141+
142+
143+
Transform = Union[Rename, CategoriesMapping, Annotate, ChangeOrdered]
144+
TransformTypes = (Rename, CategoriesMapping, Annotate, ChangeOrdered)
135145

136146

137147
def deconstruct(obj):
@@ -465,7 +475,10 @@ def __init__(self, *args, **kwargs):
465475
super().__init__(*args, **kwargs)
466476
form = self.layout().itemAt(0)
467477
assert isinstance(form, QFormLayout)
468-
478+
self.ordered_cb = QCheckBox(
479+
"Ordered", self, toolTip="Is this an ordered categorical."
480+
)
481+
self.ordered_cb.toggled.connect(self._set_ordered)
469482
#: A list model of discrete variable's values.
470483
self.values_model = itemmodels.PyListModel(
471484
flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
@@ -553,9 +566,12 @@ def __init__(self, *args, **kwargs):
553566
hlayout.addStretch(10)
554567
vlayout.addLayout(hlayout)
555568

556-
form.insertRow(1, "Values:", vlayout)
569+
form.insertRow(1, "", self.ordered_cb)
570+
form.insertRow(2, "Values:", vlayout)
571+
572+
QWidget.setTabOrder(self.name_edit, self.ordered_cb)
573+
QWidget.setTabOrder(self.ordered_cb, self.values_edit)
557574

558-
QWidget.setTabOrder(self.name_edit, self.values_edit)
559575
QWidget.setTabOrder(self.values_edit, button1)
560576
QWidget.setTabOrder(button1, button2)
561577
QWidget.setTabOrder(button2, button3)
@@ -568,9 +584,12 @@ def set_data(self, var, transform=()):
568584
"""
569585
super().set_data(var, transform)
570586
tr = None # type: Optional[CategoriesMapping]
587+
ordered = None # type: Optional[ChangeOrdered]
571588
for tr_ in transform:
572589
if isinstance(tr_, CategoriesMapping):
573590
tr = tr_
591+
if isinstance(tr_, ChangeOrdered):
592+
ordered = tr_
574593

575594
items = []
576595
if tr is not None:
@@ -622,6 +641,10 @@ def set_data(self, var, transform=()):
622641
self.values_model.index(i, 0),
623642
item
624643
)
644+
if ordered is not None:
645+
self.ordered_cb.setChecked(ordered.ordered)
646+
elif var is not None:
647+
self.ordered_cb.setChecked(isinstance(var, Ordered))
625648
self.add_new_item.actionGroup().setEnabled(var is not None)
626649

627650
def __categories_mapping(self):
@@ -657,6 +680,9 @@ def get_data(self):
657680
if any(_1 != _2 or _2 != _3
658681
for (_1, _2), _3 in zip_longest(mapping, var.categories)):
659682
tr.append(CategoriesMapping(mapping))
683+
ordered = self.ordered_cb.isChecked()
684+
if ordered != isinstance(var, Ordered):
685+
tr.append(ChangeOrdered(ordered))
660686
return var, tr
661687

662688
def clear(self):
@@ -753,6 +779,10 @@ def _add_category(self):
753779
view.edit(index)
754780
self.on_values_changed()
755781

782+
def _set_ordered(self, ordered):
783+
self.ordered_cb.setChecked(ordered)
784+
self.variable_changed.emit()
785+
756786

757787
class ContinuousVariableEditor(VariableEditor):
758788
# TODO: enable editing of display format...
@@ -766,7 +796,7 @@ class TimeVariableEditor(VariableEditor):
766796

767797
def variable_icon(var):
768798
# type: (Variable) -> QIcon
769-
if isinstance(var, Categorical):
799+
if isinstance(var, (Categorical, Ordered)):
770800
return gui.attributeIconDict[1]
771801
elif isinstance(var, Real):
772802
return gui.attributeIconDict[2]
@@ -1037,7 +1067,7 @@ def open_editor(self, index):
10371067
tr = []
10381068

10391069
editors = {
1040-
Categorical: 0,
1070+
Categorical: 0, Ordered: 0,
10411071
Real: 1,
10421072
Time: 2,
10431073
String: 3
@@ -1248,7 +1278,7 @@ def i(text):
12481278
def text(text):
12491279
return "<span>{}</span>".format(escape(text))
12501280
assert trs
1251-
rename = annotate = catmap = None
1281+
rename = annotate = catmap = ordered = None
12521282

12531283
for tr in trs:
12541284
if isinstance(tr, Rename):
@@ -1257,10 +1287,17 @@ def text(text):
12571287
annotate = tr
12581288
elif isinstance(tr, CategoriesMapping):
12591289
catmap = tr
1290+
elif isinstance(tr, ChangeOrdered):
1291+
ordered = tr
1292+
12601293
if rename is not None:
12611294
header = "{} → {}".format(var.name, rename.name)
12621295
else:
12631296
header = var.name
1297+
if ordered is not None and ordered.ordered != isinstance(var, Ordered):
1298+
assert isinstance(var, (Categorical, Ordered))
1299+
header += " (changed to {})".format(
1300+
"ordered" if ordered.ordered else "unordered")
12641301
values_section = None
12651302
if catmap is not None:
12661303
values_section = ("Values", [])
@@ -1328,7 +1365,10 @@ def abstract(var):
13281365
if isinstance(var, Orange.data.DiscreteVariable):
13291366
values, base = var.values, var.base_value
13301367
base = values[base] if base >= 0 else None
1331-
return Categorical(var.name, tuple(values), base, annotations)
1368+
if var.ordered:
1369+
return Ordered(var.name, tuple(values), base, annotations)
1370+
else:
1371+
return Categorical(var.name, tuple(values), base, annotations)
13321372
elif isinstance(var, Orange.data.TimeVariable):
13331373
return Time(var.name, annotations)
13341374
elif isinstance(var, Orange.data.ContinuousVariable):
@@ -1363,13 +1403,16 @@ def apply_transform_discete(var, trs):
13631403
name, annotations = var.name, var.attributes
13641404
base_value = var.base_value
13651405
mapping = None
1406+
ordered = var.ordered
13661407
for tr in trs:
13671408
if isinstance(tr, Rename):
13681409
name = tr.name
13691410
elif isinstance(tr, CategoriesMapping):
13701411
mapping = tr.mapping
13711412
elif isinstance(tr, Annotate):
13721413
annotations = _parse_attributes(tr.annotations)
1414+
elif isinstance(tr, ChangeOrdered):
1415+
ordered = tr.ordered
13731416

13741417
source_values = var.values
13751418
if mapping is not None:
@@ -1399,7 +1442,8 @@ def positions(values):
13991442
else:
14001443
lookup = Identity(var)
14011444
variable = Orange.data.DiscreteVariable(
1402-
name, values=dest_values, base_value=base_value, compute_value=lookup
1445+
name, values=dest_values, base_value=base_value, compute_value=lookup,
1446+
ordered=ordered,
14031447
)
14041448
variable.attributes.update(annotations)
14051449
return variable

0 commit comments

Comments
 (0)