File: slidergraph.py

package info (click to toggle)
orange3 3.40.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,908 kB
  • sloc: python: 162,745; ansic: 622; makefile: 322; sh: 93; cpp: 77
file content (269 lines) | stat: -rw-r--r-- 9,082 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import numpy as np
from pyqtgraph import mkPen, InfiniteLine, PlotCurveItem, \
    TextItem, Point
from AnyQt.QtGui import QColor
from AnyQt.QtCore import Qt

from Orange.widgets.visualize.utils.plotutils import PlotWidget


class InteractiveInfiniteLine(InfiniteLine):  # pylint: disable=abstract-method
    """
    A subclass of InfiniteLine that provides custom hover behavior.
    """

    def __init__(self, angle=90, pos=None, movable=False, bounds=None,
                 normal_pen=None, highlight_pen=None, **kwargs):
        super().__init__(angle=angle, pos=pos, movable=movable, bounds=bounds, **kwargs)
        self._normal_pen = normal_pen
        self._highlight_pen = highlight_pen
        self.setPen(normal_pen)

    def hoverEvent(self, ev):
        """
        Override hoverEvent to provide custom hover behavior.

        Parameters
        ----------
        ev : HoverEvent
            The hover event from pyqtgraph
        """

        if ev.isEnter() and self._highlight_pen is not None:
            self.setPen(self._highlight_pen)
        elif ev.isExit() and self._normal_pen is not None:
            self.setPen(self._normal_pen)


class SliderGraph(PlotWidget):
    """
    An widget graph element that shows a line plot with more sequences. It
    also plot a vertical line that can be moved left and right by a user. When
    the line is moved a callback function is called with selected value (on
    x axis).

    Parameters
    ----------
    x_axis_label : str
        A text label for x axis
    y_axis_label : str
        A text label for y axis
    callback : callable
        A function which is called when selection is changed.
    """

    def __init__(self, x_axis_label, y_axis_label, callback, **kwargs):
        super().__init__(**kwargs)

        axis = self.getAxis("bottom")
        axis.setLabel(x_axis_label)
        axis = self.getAxis("left")
        axis.setLabel(y_axis_label)

        self.getViewBox().setMenuEnabled(False)
        self.getViewBox().setMouseEnabled(False, False)
        self.showGrid(True, True, alpha=0.5)
        self.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0))
        self.hideButtons()

        # tuples to store horisontal lines and labels
        self.plot_horlabel = []
        self.plot_horline = []
        self._line = None
        self.callback = callback

        # variables to store sequences
        self.sequences = None
        self.x = None
        self.selection_limit = None
        self.data_increasing = None  # true if data mainly increasing

    def update(self, x, y, colors, cutpoint_x=None, selection_limit=None,
               names=None):
        """
        Function replots a graph.

        Parameters
        ----------
        x : np.ndarray
            One-dimensional array with X coordinates of the points
        y : array-like
            List of np.ndarrays that contains an array of Y values for each
            sequence.
        colors : array-like
            List of Qt colors (eg. Qt.red) for each sequence.
        cutpoint_x : int, optional
            A starting cutpoint - the location of the vertical line.
        selection_limit : tuple
            The tuple of two values that limit the range for selection.
        names : array-like
            The name of each sequence that shows in the legend, if None
            legend is not shown.
        legend_anchor : array-like
            The anchor of the legend in the graph
        """
        self.clear_plot()
        if names is None:
            names = [None] * len(y)

        self.sequences = y
        self.x = x
        self.selection_limit = selection_limit

        self.data_increasing = [np.sum(d[1:] - d[:-1]) > 0 for d in y]
        foreground = self.palette().text().color()
        foreground.setAlpha(128)
        # plot sequence
        for s, c, n, inc in zip(y, colors, names, self.data_increasing):
            c = QColor(c)
            self.plot(x, s, pen=mkPen(c, width=2), antialias=True)

            if n is not None:
                label = TextItem(
                    text=n, anchor=(0, 1), color=foreground)
                label.setPos(x[-1], s[-1])
                self._set_anchor(label, len(x) - 1, inc)
                self.addItem(label)

        self._plot_cutpoint(cutpoint_x)
        self.autoRange()

    def clear_plot(self):
        """
        This function clears the plot and removes data.
        """
        self.clear()
        self.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0))
        self.plot_horlabel = []
        self.plot_horline = []
        self._line = None
        self.sequences = None

    def set_cut_point(self, x):
        """
        This function sets the cutpoint (selection line) at the specific
        location.

        Parameters
        ----------
        x : int
            Cutpoint location at the x axis.
        """
        self._plot_cutpoint(x)

    def _plot_cutpoint(self, x):
        """
        Function plots the cutpoint.

        Parameters
        ----------
        x : int
            Cutpoint location.
        """
        if x is None:
            self._line = None
            return
        if self._line is None:
            normal_pen = mkPen(
                self.palette().text().color(), width=4,
                style=Qt.SolidLine, capStyle=Qt.RoundCap
            )
            highlight_pen = mkPen(
                self.palette().link().color(), width=4,
                style=Qt.SolidLine, capStyle=Qt.RoundCap
            )

            self._line = InteractiveInfiniteLine(
                angle=90, pos=x, movable=True,
                bounds=self.selection_limit if self.selection_limit is not None
                    else (self.x.min(), self.x.max()),
                normal_pen=normal_pen,
                highlight_pen=highlight_pen
            )
            self._line.setCursor(Qt.SizeHorCursor)
            self._line.sigPositionChanged.connect(self._on_cut_changed)
            self.addItem(self._line)
        else:
            self._line.setValue(x)

        self._update_horizontal_lines()

    def _plot_horizontal_lines(self):
        """
        Function plots the vertical dashed lines that points to the selected
        sequence values at the y axis.
        """
        highlight = self.palette().highlight()
        text = self.palette().text()
        for _ in range(len(self.sequences)):
            self.plot_horline.append(PlotCurveItem(
                pen=mkPen(highlight.color(), style=Qt.DashLine)))
            self.plot_horlabel.append(TextItem(
                color=text.color(), anchor=(0, 1)))
        for item in self.plot_horlabel + self.plot_horline:
            self.addItem(item)

    def _set_anchor(self, label, cutidx, inc):
        """
        This function set the location of the text label around the selected
        point at the curve. It place the text such that it is not plotted
        at the line.

        Parameters
        ----------
        label : TextItem
            Text item that needs to have location set.
        cutidx : int
            The index of the selected element in the list. If index in first
            part of the list we put label on the right side else on the left,
            such that it does not disappear at the graph edge.
        inc : bool
            This parameter tels whether the curve value is increasing or
            decreasing.
        """
        if inc:
            label.anchor = Point(0, 0) if cutidx < len(self.x) / 2 \
                else Point(1, 1)
        else:
            label.anchor = Point(0, 1) if cutidx < len(self.x) / 2 \
                else Point(1, 0)

    def _update_horizontal_lines(self):
        """
        This function update the horisontal lines when selection changes.
        If lines are present jet it calls the function to init them.
        """
        if not self.plot_horline:  # init horizontal lines
            self._plot_horizontal_lines()

        # in every case set their position
        location = int(round(self._line.value()))
        cutidx = np.searchsorted(self.x, location)
        minx = np.min(self.x)
        for s, curve, label, inc in zip(
                self.sequences, self.plot_horline, self.plot_horlabel,
                self.data_increasing):
            y = s[cutidx]
            curve.setData([minx, location], [y, y])
            self._set_anchor(label, cutidx, inc)
            label.setPos(location, y)
            label.setPlainText("{:.3f}".format(y))

    def _on_cut_changed(self, line):
        """
        This function is called when selection changes. It extract the selected
        value and calls the callback function.

        Parameters
        ----------
        line : InfiniteLine
            The cutpoint - selection line.
        """
        # cut changed by means of a cut line over the scree plot.
        value = int(round(line.value()))

        # vertical line can take only int positions
        self._line.setValue(value)

        self._update_horizontal_lines()
        self.callback(value)