Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Gradient selection/parameters widget #4596

Merged
merged 4 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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