Skip to content

Commit

Permalink
Merge pull request #4596 from ales-erjavec/gradient-selection-widget
Browse files Browse the repository at this point in the history
[ENH] Gradient selection/parameters widget
  • Loading branch information
janezd authored May 8, 2020
2 parents 20f72be + 505995f commit 4a0066c
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 77 deletions.
52 changes: 23 additions & 29 deletions Orange/widgets/unsupervised/owdistancemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import numpy

from AnyQt.QtWidgets import (
QFormLayout, QGraphicsRectItem, QGraphicsGridLayout, QApplication,
QSizePolicy
QGraphicsRectItem, QGraphicsGridLayout, QApplication, QSizePolicy
)
from AnyQt.QtGui import QFontMetrics, QPen, QTransform, QFont
from AnyQt.QtCore import Qt, QRect, QRectF, QPointF
Expand All @@ -30,6 +29,7 @@
from Orange.widgets.visualize.utils.heatmap import (
GradientColorMap, GradientLegendWidget,
)
from Orange.widgets.utils.colorgradientselection import ColorGradientSelection


def _remove_item(item):
Expand Down Expand Up @@ -301,28 +301,23 @@ def __init__(self):
callback=self._invalidate_ordering)

box = gui.vBox(self.controlArea, "Colors")
self.color_box = gui.palette_combo_box(self.palette_name)
self.color_box.currentIndexChanged.connect(self._update_color)
box.layout().addWidget(self.color_box)

form = QFormLayout(
formAlignment=Qt.AlignLeft,
labelAlignment=Qt.AlignLeft,
fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
self.color_map_widget = cmw = ColorGradientSelection(
thresholds=(self.color_low, self.color_high),
)
form.addRow(
"Low:",
gui.hSlider(box, self, "color_low", minValue=0.0, maxValue=1.0,
step=0.05, ticks=True, intOnly=False,
createLabel=False, callback=self._update_color)
)
form.addRow(
"High:",
gui.hSlider(box, self, "color_high", minValue=0.0, maxValue=1.0,
step=0.05, ticks=True, intOnly=False,
createLabel=False, callback=self._update_color)
)
box.layout().addLayout(form)
model = itemmodels.ContinuousPalettesModel(parent=self)
cmw.setModel(model)
idx = cmw.findData(self.palette_name, model.KeyRole)
if idx != -1:
cmw.setCurrentIndex(idx)

cmw.activated.connect(self._update_color)

def _set_thresholds(low, high):
self.color_low, self.color_high = low, high
self._update_color()

cmw.thresholdsChanged.connect(_set_thresholds)
box.layout().addWidget(self.color_map_widget)

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

self.gradient_legend = GradientLegendWidget(
0, 1, self._color_map()
)
self.gradient_legend = GradientLegendWidget(0, 1, self._color_map())
self.gradient_legend.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.gradient_legend.setMaximumWidth(250)
self.grid.addItem(self.gradient_legend, 0, 1)
Expand Down Expand Up @@ -613,17 +606,18 @@ def _set_labels(self, labels):
self.bottom_labels.setMaximumHeight(constraint)

def _color_map(self) -> GradientColorMap:
palette = self.color_box.currentData()
palette = self.color_map_widget.currentData()
return GradientColorMap(
palette.lookup_table(),
thresholds=(self.color_low, max(self.color_high, self.color_low)),
span=(0., self._matrix_range))

def _update_color(self):
palette = self.color_box.currentData()
palette = self.color_map_widget.currentData()
self.palette_name = palette.name
if self.matrix_item:
colors = palette.lookup_table(self.color_low, self.color_high)
cmap = self._color_map().replace(span=(0., 1.))
colors = cmap.apply(numpy.arange(256) / 255.)
self.matrix_item.setLookupTable(colors)
self.gradient_legend.show()
self.gradient_legend.setRange(0, self._matrix_range)
Expand Down
151 changes: 151 additions & 0 deletions Orange/widgets/utils/colorgradientselection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from typing import Any, Tuple

from AnyQt.QtCore import Qt, QSize, QAbstractItemModel, Property
from AnyQt.QtWidgets import (
QWidget, QSlider, QFormLayout, QComboBox, QStyle
)
from AnyQt.QtCore import Signal

from Orange.widgets.utils import itemmodels


class ColorGradientSelection(QWidget):
activated = Signal(int)

currentIndexChanged = Signal(int)
thresholdsChanged = Signal(float, float)

def __init__(self, *args, thresholds=(0.0, 1.0), **kwargs):
super().__init__(*args, **kwargs)

low = round(clip(thresholds[0], 0., 1.), 2)
high = round(clip(thresholds[1], 0., 1.), 2)
high = max(low, high)
self.__threshold_low, self.__threshold_high = low, high
form = QFormLayout(
formAlignment=Qt.AlignLeft,
labelAlignment=Qt.AlignLeft,
fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
)
form.setContentsMargins(0, 0, 0, 0)
self.gradient_cb = QComboBox(
None, objectName="gradient-combo-box",
)
self.gradient_cb.setAttribute(Qt.WA_LayoutUsesWidgetRect)
icsize = self.style().pixelMetric(
QStyle.PM_SmallIconSize, None, self.gradient_cb
)
self.gradient_cb.setIconSize(QSize(64, icsize))
model = itemmodels.ContinuousPalettesModel()
model.setParent(self)

self.gradient_cb.setModel(model)
self.gradient_cb.activated[int].connect(self.activated)
self.gradient_cb.currentIndexChanged.connect(self.currentIndexChanged)

slider_low = QSlider(
objectName="threshold-low-slider", minimum=0, maximum=100,
value=int(low * 100), orientation=Qt.Horizontal,
tickPosition=QSlider.TicksBelow, pageStep=10,
toolTip=self.tr("Low gradient threshold"),
whatsThis=self.tr("Applying a low threshold will squeeze the "
"gradient from the lower end")
)
slider_high = QSlider(
objectName="threshold-low-slider", minimum=0, maximum=100,
value=int(high * 100), orientation=Qt.Horizontal,
tickPosition=QSlider.TicksAbove, pageStep=10,
toolTip=self.tr("High gradient threshold"),
whatsThis=self.tr("Applying a high threshold will squeeze the "
"gradient from the higher end")
)
form.setWidget(0, QFormLayout.SpanningRole, self.gradient_cb)
form.addRow(self.tr("Low:"), slider_low)
form.addRow(self.tr("High:"), slider_high)
self.slider_low = slider_low
self.slider_high = slider_high
self.slider_low.valueChanged.connect(self.__on_slider_low_moved)
self.slider_high.valueChanged.connect(self.__on_slider_high_moved)
self.setLayout(form)

def setModel(self, model: QAbstractItemModel) -> None:
self.gradient_cb.setModel(model)

def model(self) -> QAbstractItemModel:
return self.gradient_cb.model()

def findData(self, data: Any, role: Qt.ItemDataRole) -> int:
return self.gradient_cb.findData(data, role)

def setCurrentIndex(self, index: int) -> None:
self.gradient_cb.setCurrentIndex(index)

def currentIndex(self) -> int:
return self.gradient_cb.currentIndex()

currentIndex_ = Property(
int, currentIndex, setCurrentIndex, notify=currentIndexChanged)

def currentData(self, role=Qt.UserRole) -> Any:
return self.gradient_cb.currentData(role)

def thresholds(self) -> Tuple[float, float]:
return self.__threshold_low, self.__threshold_high

thresholds_ = Property(object, thresholds, notify=thresholdsChanged)

def thresholdLow(self) -> float:
return self.__threshold_low

def setThresholdLow(self, low: float) -> None:
self.setThresholds(low, max(self.__threshold_high, low))

thresholdLow_ = Property(
float, thresholdLow, setThresholdLow, notify=thresholdsChanged)

def thresholdHigh(self) -> float:
return self.__threshold_high

def setThresholdHigh(self, high: float) -> None:
self.setThresholds(min(self.__threshold_low, high), high)

thresholdHigh_ = Property(
float, thresholdLow, setThresholdLow, notify=thresholdsChanged)

def __on_slider_low_moved(self, value: int) -> None:
high = self.slider_high
old = self.__threshold_low, self.__threshold_high
self.__threshold_low = value / 100.
if value >= high.value():
self.__threshold_high = value / 100.
high.setSliderPosition(value)
new = self.__threshold_low, self.__threshold_high
if new != old:
self.thresholdsChanged.emit(*new)

def __on_slider_high_moved(self, value: int) -> None:
low = self.slider_low
old = self.__threshold_low, self.__threshold_high
self.__threshold_high = value / 100.
if low.value() >= value:
self.__threshold_low = value / 100
low.setSliderPosition(value)
new = self.__threshold_low, self.__threshold_high
if new != old:
self.thresholdsChanged.emit(*new)

def setThresholds(self, low: float, high: float) -> None:
low = round(clip(low, 0., 1.), 2)
high = round(clip(high, 0., 1.), 2)
if low > high:
high = low
if self.__threshold_low != low or self.__threshold_high != high:
self.__threshold_high = high
self.__threshold_low = low
self.slider_low.setSliderPosition(low * 100)
self.slider_high.setSliderPosition(high * 100)
self.thresholdsChanged.emit(high, low)


def clip(a, amin, amax):
return min(max(a, amin), amax)
3 changes: 3 additions & 0 deletions Orange/widgets/utils/itemmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ class ContinuousPalettesModel(QAbstractListModel):
"""
Model for combo boxes
"""
KeyRole = Qt.UserRole + 1
def __init__(self, parent=None, categories=None, icon_width=64):
super().__init__(parent)
self.icon_width = icon_width
Expand Down Expand Up @@ -641,6 +642,8 @@ def data(self, index, role):
return item.color_strip(self.icon_width, 16)
if role == Qt.UserRole:
return item
if role == self.KeyRole:
return item.name
return None

def flags(self, index):
Expand Down
70 changes: 70 additions & 0 deletions Orange/widgets/utils/tests/test_colorgradientselection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import numpy as np

from AnyQt.QtTest import QSignalSpy
from AnyQt.QtCore import Qt, QStringListModel

from Orange.widgets.utils.colorgradientselection import ColorGradientSelection
from Orange.widgets.tests.base import GuiTest

class TestColorGradientSelection(GuiTest):
def test_constructor(self):
w = ColorGradientSelection(thresholds=(0.1, 0.9))
self.assertEqual(w.thresholds(), (0.1, 0.9))

w = ColorGradientSelection(thresholds=(-0.1, 1.1))
self.assertEqual(w.thresholds(), (0.0, 1.0))

w = ColorGradientSelection(thresholds=(1.0, 0.0))
self.assertEqual(w.thresholds(), (1.0, 1.0))

def test_setModel(self):
w = ColorGradientSelection()
model = QStringListModel(["A", "B"])
w.setModel(model)
self.assertIs(w.model(), model)
self.assertEqual(w.findData("B", Qt.DisplayRole), 1)
current = QSignalSpy(w.currentIndexChanged)
w.setCurrentIndex(1)
self.assertEqual(w.currentIndex(), 1)
self.assertSequenceEqual(list(current), [[1]])

def test_thresholds(self):
w = ColorGradientSelection()
w.setThresholds(0.2, 0.8)
self.assertEqual(w.thresholds(), (0.2, 0.8))
w.setThresholds(0.5, 0.5)
self.assertEqual(w.thresholds(), (0.5, 0.5))
w.setThresholds(0.5, np.nextafter(0.5, 0))
self.assertEqual(w.thresholds(), (0.5, 0.5))
w.setThresholds(-1, 2)
self.assertEqual(w.thresholds(), (0., 1.))
w.setThresholds(0.1, 0.0)
self.assertEqual(w.thresholds(), (0.1, 0.1))
w.setThresholdLow(0.2)
self.assertEqual(w.thresholds(), (0.2, 0.2))
self.assertEqual(w.thresholdLow(), 0.2)
w.setThresholdHigh(0.1)
self.assertEqual(w.thresholdHigh(), 0.1)
self.assertEqual(w.thresholds(), (0.1, 0.1))

def test_slider_move(self):
w = ColorGradientSelection()
w.adjustSize()
w.setThresholds(0.5, 0.5)
changed = QSignalSpy(w.thresholdsChanged)
sl, sh = w.slider_low, w.slider_high
sl.triggerAction(sl.SliderToMinimum)
self.assertEqual(len(changed), 1)
low, high = changed[-1]
self.assertLessEqual(low, high)
self.assertEqual(low, 0.0)
sl.triggerAction(sl.SliderToMaximum)
self.assertEqual(len(changed), 2)
low, high = changed[-1]
self.assertLessEqual(low, high)
self.assertEqual(low, 1.0)
sh.triggerAction(sl.SliderToMinimum)
self.assertEqual(len(changed), 3)
low, high = changed[-1]
self.assertLessEqual(low, high)
self.assertEqual(high, 0.0)
Loading

0 comments on commit 4a0066c

Please sign in to comment.