Skip to content

Commit 4a0066c

Browse files
authored
Merge pull request #4596 from ales-erjavec/gradient-selection-widget
[ENH] Gradient selection/parameters widget
2 parents 20f72be + 505995f commit 4a0066c

File tree

7 files changed

+279
-77
lines changed

7 files changed

+279
-77
lines changed

Orange/widgets/unsupervised/owdistancemap.py

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import numpy
66

77
from AnyQt.QtWidgets import (
8-
QFormLayout, QGraphicsRectItem, QGraphicsGridLayout, QApplication,
9-
QSizePolicy
8+
QGraphicsRectItem, QGraphicsGridLayout, QApplication, QSizePolicy
109
)
1110
from AnyQt.QtGui import QFontMetrics, QPen, QTransform, QFont
1211
from AnyQt.QtCore import Qt, QRect, QRectF, QPointF
@@ -30,6 +29,7 @@
3029
from Orange.widgets.visualize.utils.heatmap import (
3130
GradientColorMap, GradientLegendWidget,
3231
)
32+
from Orange.widgets.utils.colorgradientselection import ColorGradientSelection
3333

3434

3535
def _remove_item(item):
@@ -301,28 +301,23 @@ def __init__(self):
301301
callback=self._invalidate_ordering)
302302

303303
box = gui.vBox(self.controlArea, "Colors")
304-
self.color_box = gui.palette_combo_box(self.palette_name)
305-
self.color_box.currentIndexChanged.connect(self._update_color)
306-
box.layout().addWidget(self.color_box)
307-
308-
form = QFormLayout(
309-
formAlignment=Qt.AlignLeft,
310-
labelAlignment=Qt.AlignLeft,
311-
fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
304+
self.color_map_widget = cmw = ColorGradientSelection(
305+
thresholds=(self.color_low, self.color_high),
312306
)
313-
form.addRow(
314-
"Low:",
315-
gui.hSlider(box, self, "color_low", minValue=0.0, maxValue=1.0,
316-
step=0.05, ticks=True, intOnly=False,
317-
createLabel=False, callback=self._update_color)
318-
)
319-
form.addRow(
320-
"High:",
321-
gui.hSlider(box, self, "color_high", minValue=0.0, maxValue=1.0,
322-
step=0.05, ticks=True, intOnly=False,
323-
createLabel=False, callback=self._update_color)
324-
)
325-
box.layout().addLayout(form)
307+
model = itemmodels.ContinuousPalettesModel(parent=self)
308+
cmw.setModel(model)
309+
idx = cmw.findData(self.palette_name, model.KeyRole)
310+
if idx != -1:
311+
cmw.setCurrentIndex(idx)
312+
313+
cmw.activated.connect(self._update_color)
314+
315+
def _set_thresholds(low, high):
316+
self.color_low, self.color_high = low, high
317+
self._update_color()
318+
319+
cmw.thresholdsChanged.connect(_set_thresholds)
320+
box.layout().addWidget(self.color_map_widget)
326321

327322
self.annot_combo = gui.comboBox(
328323
self.controlArea, self, "annotation_idx", box="Annotations",
@@ -342,9 +337,7 @@ def __init__(self):
342337
self.grid = QGraphicsGridLayout()
343338
self.grid_widget.setLayout(self.grid)
344339

345-
self.gradient_legend = GradientLegendWidget(
346-
0, 1, self._color_map()
347-
)
340+
self.gradient_legend = GradientLegendWidget(0, 1, self._color_map())
348341
self.gradient_legend.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
349342
self.gradient_legend.setMaximumWidth(250)
350343
self.grid.addItem(self.gradient_legend, 0, 1)
@@ -613,17 +606,18 @@ def _set_labels(self, labels):
613606
self.bottom_labels.setMaximumHeight(constraint)
614607

615608
def _color_map(self) -> GradientColorMap:
616-
palette = self.color_box.currentData()
609+
palette = self.color_map_widget.currentData()
617610
return GradientColorMap(
618611
palette.lookup_table(),
619612
thresholds=(self.color_low, max(self.color_high, self.color_low)),
620613
span=(0., self._matrix_range))
621614

622615
def _update_color(self):
623-
palette = self.color_box.currentData()
616+
palette = self.color_map_widget.currentData()
624617
self.palette_name = palette.name
625618
if self.matrix_item:
626-
colors = palette.lookup_table(self.color_low, self.color_high)
619+
cmap = self._color_map().replace(span=(0., 1.))
620+
colors = cmap.apply(numpy.arange(256) / 255.)
627621
self.matrix_item.setLookupTable(colors)
628622
self.gradient_legend.show()
629623
self.gradient_legend.setRange(0, self._matrix_range)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from typing import Any, Tuple
2+
3+
from AnyQt.QtCore import Qt, QSize, QAbstractItemModel, Property
4+
from AnyQt.QtWidgets import (
5+
QWidget, QSlider, QFormLayout, QComboBox, QStyle
6+
)
7+
from AnyQt.QtCore import Signal
8+
9+
from Orange.widgets.utils import itemmodels
10+
11+
12+
class ColorGradientSelection(QWidget):
13+
activated = Signal(int)
14+
15+
currentIndexChanged = Signal(int)
16+
thresholdsChanged = Signal(float, float)
17+
18+
def __init__(self, *args, thresholds=(0.0, 1.0), **kwargs):
19+
super().__init__(*args, **kwargs)
20+
21+
low = round(clip(thresholds[0], 0., 1.), 2)
22+
high = round(clip(thresholds[1], 0., 1.), 2)
23+
high = max(low, high)
24+
self.__threshold_low, self.__threshold_high = low, high
25+
form = QFormLayout(
26+
formAlignment=Qt.AlignLeft,
27+
labelAlignment=Qt.AlignLeft,
28+
fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
29+
)
30+
form.setContentsMargins(0, 0, 0, 0)
31+
self.gradient_cb = QComboBox(
32+
None, objectName="gradient-combo-box",
33+
)
34+
self.gradient_cb.setAttribute(Qt.WA_LayoutUsesWidgetRect)
35+
icsize = self.style().pixelMetric(
36+
QStyle.PM_SmallIconSize, None, self.gradient_cb
37+
)
38+
self.gradient_cb.setIconSize(QSize(64, icsize))
39+
model = itemmodels.ContinuousPalettesModel()
40+
model.setParent(self)
41+
42+
self.gradient_cb.setModel(model)
43+
self.gradient_cb.activated[int].connect(self.activated)
44+
self.gradient_cb.currentIndexChanged.connect(self.currentIndexChanged)
45+
46+
slider_low = QSlider(
47+
objectName="threshold-low-slider", minimum=0, maximum=100,
48+
value=int(low * 100), orientation=Qt.Horizontal,
49+
tickPosition=QSlider.TicksBelow, pageStep=10,
50+
toolTip=self.tr("Low gradient threshold"),
51+
whatsThis=self.tr("Applying a low threshold will squeeze the "
52+
"gradient from the lower end")
53+
)
54+
slider_high = QSlider(
55+
objectName="threshold-low-slider", minimum=0, maximum=100,
56+
value=int(high * 100), orientation=Qt.Horizontal,
57+
tickPosition=QSlider.TicksAbove, pageStep=10,
58+
toolTip=self.tr("High gradient threshold"),
59+
whatsThis=self.tr("Applying a high threshold will squeeze the "
60+
"gradient from the higher end")
61+
)
62+
form.setWidget(0, QFormLayout.SpanningRole, self.gradient_cb)
63+
form.addRow(self.tr("Low:"), slider_low)
64+
form.addRow(self.tr("High:"), slider_high)
65+
self.slider_low = slider_low
66+
self.slider_high = slider_high
67+
self.slider_low.valueChanged.connect(self.__on_slider_low_moved)
68+
self.slider_high.valueChanged.connect(self.__on_slider_high_moved)
69+
self.setLayout(form)
70+
71+
def setModel(self, model: QAbstractItemModel) -> None:
72+
self.gradient_cb.setModel(model)
73+
74+
def model(self) -> QAbstractItemModel:
75+
return self.gradient_cb.model()
76+
77+
def findData(self, data: Any, role: Qt.ItemDataRole) -> int:
78+
return self.gradient_cb.findData(data, role)
79+
80+
def setCurrentIndex(self, index: int) -> None:
81+
self.gradient_cb.setCurrentIndex(index)
82+
83+
def currentIndex(self) -> int:
84+
return self.gradient_cb.currentIndex()
85+
86+
currentIndex_ = Property(
87+
int, currentIndex, setCurrentIndex, notify=currentIndexChanged)
88+
89+
def currentData(self, role=Qt.UserRole) -> Any:
90+
return self.gradient_cb.currentData(role)
91+
92+
def thresholds(self) -> Tuple[float, float]:
93+
return self.__threshold_low, self.__threshold_high
94+
95+
thresholds_ = Property(object, thresholds, notify=thresholdsChanged)
96+
97+
def thresholdLow(self) -> float:
98+
return self.__threshold_low
99+
100+
def setThresholdLow(self, low: float) -> None:
101+
self.setThresholds(low, max(self.__threshold_high, low))
102+
103+
thresholdLow_ = Property(
104+
float, thresholdLow, setThresholdLow, notify=thresholdsChanged)
105+
106+
def thresholdHigh(self) -> float:
107+
return self.__threshold_high
108+
109+
def setThresholdHigh(self, high: float) -> None:
110+
self.setThresholds(min(self.__threshold_low, high), high)
111+
112+
thresholdHigh_ = Property(
113+
float, thresholdLow, setThresholdLow, notify=thresholdsChanged)
114+
115+
def __on_slider_low_moved(self, value: int) -> None:
116+
high = self.slider_high
117+
old = self.__threshold_low, self.__threshold_high
118+
self.__threshold_low = value / 100.
119+
if value >= high.value():
120+
self.__threshold_high = value / 100.
121+
high.setSliderPosition(value)
122+
new = self.__threshold_low, self.__threshold_high
123+
if new != old:
124+
self.thresholdsChanged.emit(*new)
125+
126+
def __on_slider_high_moved(self, value: int) -> None:
127+
low = self.slider_low
128+
old = self.__threshold_low, self.__threshold_high
129+
self.__threshold_high = value / 100.
130+
if low.value() >= value:
131+
self.__threshold_low = value / 100
132+
low.setSliderPosition(value)
133+
new = self.__threshold_low, self.__threshold_high
134+
if new != old:
135+
self.thresholdsChanged.emit(*new)
136+
137+
def setThresholds(self, low: float, high: float) -> None:
138+
low = round(clip(low, 0., 1.), 2)
139+
high = round(clip(high, 0., 1.), 2)
140+
if low > high:
141+
high = low
142+
if self.__threshold_low != low or self.__threshold_high != high:
143+
self.__threshold_high = high
144+
self.__threshold_low = low
145+
self.slider_low.setSliderPosition(low * 100)
146+
self.slider_high.setSliderPosition(high * 100)
147+
self.thresholdsChanged.emit(high, low)
148+
149+
150+
def clip(a, amin, amax):
151+
return min(max(a, amin), amax)

Orange/widgets/utils/itemmodels.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ class ContinuousPalettesModel(QAbstractListModel):
605605
"""
606606
Model for combo boxes
607607
"""
608+
KeyRole = Qt.UserRole + 1
608609
def __init__(self, parent=None, categories=None, icon_width=64):
609610
super().__init__(parent)
610611
self.icon_width = icon_width
@@ -641,6 +642,8 @@ def data(self, index, role):
641642
return item.color_strip(self.icon_width, 16)
642643
if role == Qt.UserRole:
643644
return item
645+
if role == self.KeyRole:
646+
return item.name
644647
return None
645648

646649
def flags(self, index):
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import numpy as np
2+
3+
from AnyQt.QtTest import QSignalSpy
4+
from AnyQt.QtCore import Qt, QStringListModel
5+
6+
from Orange.widgets.utils.colorgradientselection import ColorGradientSelection
7+
from Orange.widgets.tests.base import GuiTest
8+
9+
class TestColorGradientSelection(GuiTest):
10+
def test_constructor(self):
11+
w = ColorGradientSelection(thresholds=(0.1, 0.9))
12+
self.assertEqual(w.thresholds(), (0.1, 0.9))
13+
14+
w = ColorGradientSelection(thresholds=(-0.1, 1.1))
15+
self.assertEqual(w.thresholds(), (0.0, 1.0))
16+
17+
w = ColorGradientSelection(thresholds=(1.0, 0.0))
18+
self.assertEqual(w.thresholds(), (1.0, 1.0))
19+
20+
def test_setModel(self):
21+
w = ColorGradientSelection()
22+
model = QStringListModel(["A", "B"])
23+
w.setModel(model)
24+
self.assertIs(w.model(), model)
25+
self.assertEqual(w.findData("B", Qt.DisplayRole), 1)
26+
current = QSignalSpy(w.currentIndexChanged)
27+
w.setCurrentIndex(1)
28+
self.assertEqual(w.currentIndex(), 1)
29+
self.assertSequenceEqual(list(current), [[1]])
30+
31+
def test_thresholds(self):
32+
w = ColorGradientSelection()
33+
w.setThresholds(0.2, 0.8)
34+
self.assertEqual(w.thresholds(), (0.2, 0.8))
35+
w.setThresholds(0.5, 0.5)
36+
self.assertEqual(w.thresholds(), (0.5, 0.5))
37+
w.setThresholds(0.5, np.nextafter(0.5, 0))
38+
self.assertEqual(w.thresholds(), (0.5, 0.5))
39+
w.setThresholds(-1, 2)
40+
self.assertEqual(w.thresholds(), (0., 1.))
41+
w.setThresholds(0.1, 0.0)
42+
self.assertEqual(w.thresholds(), (0.1, 0.1))
43+
w.setThresholdLow(0.2)
44+
self.assertEqual(w.thresholds(), (0.2, 0.2))
45+
self.assertEqual(w.thresholdLow(), 0.2)
46+
w.setThresholdHigh(0.1)
47+
self.assertEqual(w.thresholdHigh(), 0.1)
48+
self.assertEqual(w.thresholds(), (0.1, 0.1))
49+
50+
def test_slider_move(self):
51+
w = ColorGradientSelection()
52+
w.adjustSize()
53+
w.setThresholds(0.5, 0.5)
54+
changed = QSignalSpy(w.thresholdsChanged)
55+
sl, sh = w.slider_low, w.slider_high
56+
sl.triggerAction(sl.SliderToMinimum)
57+
self.assertEqual(len(changed), 1)
58+
low, high = changed[-1]
59+
self.assertLessEqual(low, high)
60+
self.assertEqual(low, 0.0)
61+
sl.triggerAction(sl.SliderToMaximum)
62+
self.assertEqual(len(changed), 2)
63+
low, high = changed[-1]
64+
self.assertLessEqual(low, high)
65+
self.assertEqual(low, 1.0)
66+
sh.triggerAction(sl.SliderToMinimum)
67+
self.assertEqual(len(changed), 3)
68+
low, high = changed[-1]
69+
self.assertLessEqual(low, high)
70+
self.assertEqual(high, 0.0)

0 commit comments

Comments
 (0)