source: orange/Orange/OrangeWidgets/plot/owtools.py @ 10542:7dde0640e266

Revision 10542:7dde0640e266, 18.0 KB checked in by anzeh <anze.staric@…>, 2 years ago (diff)

Moved preprocess from Orange to Orange.data.

Line 
1'''
2##############################
3Plot tools (``owtools``)
4##############################
5
6.. autofunction:: resize_plot_item_list
7
8.. autofunction:: move_item
9
10.. autofunction:: move_item_xy
11
12.. autoclass:: TooltipManager
13    :members:
14   
15.. autoclass:: PolygonCurve
16    :members:
17    :show-inheritance:
18   
19.. autoclass:: RectangleCurve
20    :members:
21    :show-inheritance:
22   
23.. autoclass:: CircleCurve
24    :members:
25    :show-inheritance:
26   
27.. autoclass:: UnconnectedLinesCurve
28    :members:
29    :show-inheritance:
30   
31.. autoclass:: Marker
32    :members:
33    :show-inheritance:
34
35'''
36
37from PyQt4.QtGui import QGraphicsItem, QGraphicsRectItem, QPolygonF, QGraphicsPolygonItem, QGraphicsEllipseItem, QPen, QBrush
38from PyQt4.QtCore import Qt, QRectF, QPointF, QPropertyAnimation
39
40from owcurve import *
41from owpalette import OWPalette
42
43from Orange.data.preprocess.scaling import get_variable_values_sorted
44import orangeom
45import ColorPalette
46
47def resize_plot_item_list(lst, size, item_type, parent):
48    """
49        Efficiently resizes a list of QGraphicsItems (PlotItems, Curves, etc.).
50        If the list is to be reduced, i.e. if len(lst) > size, then the extra items are first removed from the scene.
51        If items have to be added to the scene, new items will be of type ``item_type`` and will have ``parent``
52        as their parent item.
53       
54        The list is resized in place, this function returns nothing.
55       
56        :param lst: The list to be resized
57        :type lst: list of QGraphicsItem
58       
59        :param size: The needed size of the list
60        :type size: int
61       
62        :param item_type: The type of items that should be added if the list has to be increased
63        :type item_type: type
64       
65        :param parent: Any new items will have this as their parent item
66        :type parent: QGraphicsItem
67    """
68    n = len(lst)
69    if n > size:
70        for i in lst[size:]:
71            i.setParentItem(None)
72            if i.scene():
73                i.scene().removeItem(i)
74        del lst[size:]
75    elif n < size:
76        lst.extend(item_type(parent) for i in range(size - n))
77       
78def move_item(item, pos, animate = True, duration = None):
79    '''
80        Animates ``item`` to move to position ``pos``.
81        If animations are turned off globally, the item is instead move immediately, without any animation.
82       
83        :param item: The item to move
84        :type item: QGraphicsItem
85       
86        :param pos: The final position of the item
87        :type pos: QPointF
88       
89        :param duration: The duration of the animation. If unspecified, Qt's default value of 250 miliseconds is used.
90        :type duration: int
91    '''
92    if not duration:
93        duration = 250
94    orangeqt.PlotItem.move_item(item, pos, animate, duration)
95   
96def move_item_xy(item, x, y, animate = True, duration = None):
97    '''
98        Same as
99        move_item(item, QPointF(x, y), duration)
100    '''
101    move_item(item, QPointF(x, y), animate, duration)
102       
103class TooltipManager:
104    """
105        A dynamic tool tip manager.
106       
107        :param plot: The plot used for transforming the coordinates
108        :type plot: :obj:`.OWPlot`
109    """
110    def __init__(self, plot):
111        self.graph = plot
112        self.positions=[]
113        self.texts=[]
114
115    def addToolTip(self, x, y, text, customX = 0, customY = 0):
116        """
117            Adds a tool tip. If a tooltip with the same name already exists, it updates it instead of adding a new one.
118           
119            :param x: The x coordinate of the tip, in data coordinates.
120            :type x: float
121           
122            :param y: The y coordinate of the tip, in data coordinates.
123            :type y: float
124           
125            :param text: The text to show in the tip.
126            :type text: str or int
127           
128            :param customX: The maximum horizontal distance in pixels from the point (x,y) at which to show the tooltip.
129            :type customX: float
130           
131            :param customY: The maximum vertical distance in pixels from the point (x,y) at which to show the tooltip.
132            :type customY: float
133           
134            If ``customX`` and ``customY`` are omitted, a default of 6 pixels is used.
135        """
136        self.positions.append((x,y, customX, customY))
137        self.texts.append(text)
138
139    #Decides whether to pop up a tool tip and which text to pop up
140    def maybeTip(self, x, y):
141        """
142            Decides whether to pop up a tool tip and which text to show in it.
143           
144            :param x: the x coordinate of the mouse in data coordinates.
145            :type x: float
146           
147            :param y: the y coordinate of the mouse in data coordinates.
148            :type y: float
149           
150            :returns: A tuple consisting of the ``text``, ``x`` and ``y`` arguments to :meth:`addToolTip` of the
151                      closest point.
152            :rtype: tuple of (int or str), float, float
153        """
154        if len(self.positions) == 0: return ("", -1, -1)
155        dists = [max(abs(x-position[0])- position[2],0) + max(abs(y-position[1])-position[3], 0) for position in self.positions]
156        nearestIndex = dists.index(min(dists))
157       
158        intX = abs(self.graph.transform(xBottom, x) - self.graph.transform(xBottom, self.positions[nearestIndex][0]))
159        intY = abs(self.graph.transform(yLeft, y) - self.graph.transform(yLeft, self.positions[nearestIndex][1]))
160        if self.positions[nearestIndex][2] == 0 and self.positions[nearestIndex][3] == 0:   # if we specified no custom range then assume 6 pixels
161            if intX + intY < 6:  return (self.texts[nearestIndex], self.positions[nearestIndex][0], self.positions[nearestIndex][1])
162            else:                return ("", None, None)
163        else:
164            if abs(self.positions[nearestIndex][0] - x) <= self.positions[nearestIndex][2] and abs(self.positions[nearestIndex][1] - y) <= self.positions[nearestIndex][3]:
165                return (self.texts[nearestIndex], x, y)
166            else:
167                return ("", None, None)
168
169    def removeAll(self):
170        """
171            Removes all tips
172        """
173        self.positions = []
174        self.texts = []
175
176class PolygonCurve(OWCurve):
177    """
178        A plot item that shows a filled or empty polygon.
179       
180        :param pen: The pen used to draw the polygon's outline
181        :type pen: :obj:`.QPen`
182       
183        :param brush: The brush used to paint the polygon's inside
184        :type brush: :obj:`.QBrush`
185       
186        :param xData: The list of x coordinates
187        :type xData: list of float
188
189        :param yData: The list of y coordinates
190        :type yData: list of float
191       
192        :param tooltip: The tool tip shown when hovering over this curve
193        :type tooltip: str
194    """
195    def __init__(self, pen = QPen(Qt.black), brush = QBrush(Qt.white), xData = [], yData = [], tooltip = None):
196        OWCurve.__init__(self, xData, yData, tooltip=tooltip)
197        self._data_polygon = self.polygon_from_data(xData, yData)
198        self._polygon_item = QGraphicsPolygonItem(self)
199        self.set_pen(pen)
200        self.set_brush(brush)
201       
202    def update_properties(self):
203        self._polygon_item.setPolygon(self.graph_transform().map(self._data_polygon))
204        self._polygon_item.setPen(self.pen())
205        self._polygon_item.setBrush(self.brush())
206       
207    @staticmethod
208    def polygon_from_data(xData, yData):
209        """
210            Creates a polygon from a list of x and y coordinates.
211           
212            :returns: A polygon with point corresponding to ``xData`` and ``yData``.
213            :rtype: QPolygonF
214        """
215        if xData and yData:
216            n = min(len(xData), len(yData))
217            p = QPolygonF(n+1)
218            for i in range(n):
219                p[i] = QPointF(xData[i], yData[i])
220            p[n] = QPointF(xData[0], yData[0])
221            return p
222        else:
223            return QPolygonF()
224           
225    def set_data(self, xData, yData):
226        self._data_polygon = self.polygon_from_data(xData, yData)
227        OWCurve.set_data(self, xData, yData)
228           
229class RectangleCurve(OWCurve):
230    """
231        A plot item that shows a rectangle.
232       
233        This class accepts the same options as :obj:`.PolygonCurve`.
234        The rectangle is calculated as the smallest rectangle that contains all points in ``xData`` and ``yData``.
235    """
236    def __init__(self, pen = QPen(Qt.black), brush = QBrush(Qt.white), xData = None, yData = None, tooltip = None):
237        OWCurve.__init__(self, xData, yData, tooltip=tooltip)
238        self.set_pen(pen)
239        self.set_brush(brush)
240        self._item = QGraphicsRectItem(self)
241       
242    def update_properties(self):
243        self._item.setRect(self.graph_transform().mapRect(self.data_rect()))
244        self._item.setPen(self.pen())
245        self._item.setBrush(self.brush())
246       
247class UnconnectedLinesCurve(orangeqt.UnconnectedLinesCurve):
248    """
249        A plot item that shows a series of unconnected straight lines.
250       
251        :param name: The name of this curve. :seealso: :attr:`.OWCurve.name`
252        :type name: str
253       
254        :param pen: The pen used to draw the lines
255        :type pen: QPen
256       
257        :param xData: The list of x coordinates
258        :type xData: list of float
259
260        :param yData: The list of y coordinates
261        :type yData: list of float
262       
263        The data should contain an even number of elements. Lines are drawn between the `n`-th and
264        `(n+1)`-th point for each even `n`.
265    """
266    def __init__(self, name, pen = QPen(Qt.black), xData = None, yData = None):
267        orangeqt.UnconnectedLinesCurve.__init__(self)
268        self.set_data(xData, yData)
269        if pen:
270            self.set_pen(pen)
271        self.name = name
272       
273class CircleCurve(OWCurve):
274    """
275        Displays a circle on the plot
276       
277        :param pen: The pen used to draw the outline of the circle
278        :type pen: QPen
279       
280        :param brush: The brush used to paint the inside of the circle
281        :type brush: QBrush
282       
283        :param xCenter: The x coordinate of the circle's center
284        :type xCenter: float
285       
286        :param yCenter: The y coordinate of the circle's center
287        :type yCenter: float
288       
289        :param radius: The circle's radius
290        :type radius: float
291    """
292    def __init__(self, pen = QPen(Qt.black), brush = QBrush(Qt.NoBrush), xCenter = 0.0, yCenter = 0.0, radius = 1.0):
293        OWCurve.__init__(self)
294        self._item = QGraphicsEllipseItem(self)
295        self.center = xCenter, yCenter
296        self.radius = radius
297        self._rect = QRectF(xCenter-radius, yCenter-radius, 2*radius, 2*radius)
298        self.set_pen(pen)
299        self.set_brush(brush)
300       
301    def update_properties(self):
302        self._item.setRect(self.graph_transform().mapRect(self.data_rect()))
303        self._item.setPen(self.pen())
304        self._item.setBrush(self.brush())
305       
306    def data_rect(self):
307        x, y = self.center
308        r = self.radius
309        return QRectF(x-r, y-r, 2*r, 2*r)
310       
311class Marker(orangeqt.PlotItem):
312    """
313        Displays a text marker on the plot.
314       
315        :param text: The text to display. It can be HTML-formatted
316        :type tex: str
317       
318        :param x: The x coordinate of the marker's position
319        :type x: float
320       
321        :param y: The y coordinate of the marker's position
322        :type y: float
323       
324        :param align: The text alignment
325        :type align:
326       
327        :param bold: If ``True``, the text will be show bold.
328        :type bold: int
329       
330        :param color: The text color
331        :type color: QColor
332       
333        :param brushColor: The color of the brush user to paint the background
334        :type color: QColor
335       
336        :param size: Font size
337        :type size: int
338       
339        Markers have the QGraphicsItem.ItemIgnoresTransformations flag set by default,
340        so text remains the same size when zooming. There is no need to scale the manually.
341    """
342    def __init__(self, text, x, y, align, bold = 0, color = None, brushColor = None, size=None):
343        orangeqt.PlotItem.__init__(self)
344        self.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
345        self._item = QGraphicsTextItem(text, parent=self)
346        self._data_point = QPointF(x,y)
347        f = self._item.font()
348        f.setBold(bold)
349        if size:
350            f.setPointSize(size)
351        self._item.setFont(f)
352        self._item.setPos(x, y)
353           
354    def update_properties(self):
355        self._item.setPos(self.graph_transform().map(self._data_point))
356
357class ProbabilitiesItem(orangeqt.PlotItem):
358    """
359        Displays class probabilities in the background
360       
361        :param classifier: The classifier for which the probabilities are calculated
362        :type classifier: orange.P2NN
363       
364        :param granularity: The size of individual cells
365        :type granularity: int
366       
367        :param scale: The data scale factor
368        :type scale: float
369       
370        :param spacing: The space between cells
371        :param spacing: int
372       
373        :param rect: The rectangle into which to draw the probabilities. If unspecified, the entire plot is used.
374        :type rect: QRectF
375    """
376    def __init__(self, classifier, granularity, scale, spacing, rect=None):
377        orangeqt.PlotItem.__init__(self)
378        self.classifier = classifier
379        self.rect = rect
380        self.granularity = granularity
381        self.scale = scale
382        self.spacing = spacing
383        self.pixmap_item = QGraphicsPixmapItem(self)
384        self.set_in_background(True)
385        self.setZValue(ProbabilitiesZValue)
386       
387    def update_properties(self):
388        ## Mostly copied from OWScatterPlotGraph
389        if not self.plot():
390            return
391           
392        if not self.rect:
393            x,y = self.axes()
394            self.rect = self.plot().data_rect_for_axes(x,y)
395        s = self.graph_transform().mapRect(self.rect).size().toSize()
396        if not s.isValid():
397            return
398        rx = s.width()
399        ry = s.height()
400       
401        rx -= rx % self.granularity
402        ry -= ry % self.granularity
403               
404        p = self.graph_transform().map(QPointF(0, 0)) - self.graph_transform().map(self.rect.topLeft())
405        p = p.toPoint()
406       
407        ox = p.x()
408        oy = -p.y()
409       
410        if self.classifier.classVar.varType == orange.VarTypes.Continuous:
411            imagebmp = orangeom.potentialsBitmap(self.classifier, rx, ry, ox, oy, self.granularity, self.scale)
412            palette = [qRgb(255.*i/255., 255.*i/255., 255-(255.*i/255.)) for i in range(255)] + [qRgb(255, 255, 255)]
413        else:
414            imagebmp, nShades = orangeom.potentialsBitmap(self.classifier, rx, ry, ox, oy, self.granularity, self.scale, self.spacing)
415            palette = []
416            sortedClasses = get_variable_values_sorted(self.classifier.domain.classVar)
417            for cls in self.classifier.classVar.values:
418                color = self.plot().discPalette.getRGB(sortedClasses.index(cls))
419                towhite = [255-c for c in color]
420                for s in range(nShades):
421                    si = 1-float(s)/nShades
422                    palette.append(qRgb(*tuple([color[i]+towhite[i]*si for i in (0, 1, 2)])))
423            palette.extend([qRgb(255, 255, 255) for i in range(256-len(palette))])
424
425        self.potentialsImage = QImage(imagebmp, rx, ry, QImage.Format_Indexed8)
426        self.potentialsImage.setColorTable(ColorPalette.signedPalette(palette) if qVersion() < "4.5" else palette)
427        self.potentialsImage.setNumColors(256)
428        self.pixmap_item.setPixmap(QPixmap.fromImage(self.potentialsImage))
429        self.pixmap_item.setPos(self.graph_transform().map(self.rect.bottomLeft()))
430   
431    def data_rect(self):
432        return self.rect if self.rect else QRectF()
433       
434@deprecated_members({
435        'enableX' : 'set_x_enabled',
436        'enableY' : 'set_y_enabled',
437        'xEnabled' : 'is_x_enabled',
438        'yEnabled' : 'is_y_enabled',
439        'setPen' : 'set_pen'
440    })
441class PlotGrid(orangeqt.PlotItem):
442    """
443        Draws a grid onto the plot
444       
445        :param plot: If specified, the grid will be attached to the ``plot``.
446        :type plot: :obj:`.OWPlot`
447    """
448    def __init__(self, plot = None):
449        orangeqt.PlotItem.__init__(self)
450        self._x_enabled = True
451        self._y_enabled = True
452        self._path_item = QGraphicsPathItem(self)
453        self.set_in_background(True)
454        if plot:
455            self.attach(plot)
456            self._path_item.setPen(plot.color(OWPalette.Grid))
457           
458    def set_x_enabled(self, b):
459        """
460            Enables or disabled vertial grid lines
461        """
462        if b < 0:
463            b = not self._x_enabled
464        self._x_enabled = b
465        self.update_properties()
466       
467    def is_x_enabled(self):
468        """
469            Returns whether vertical grid lines are enabled
470        """
471        return self._x_enabled
472       
473    def set_y_enabled(self, b):
474        """
475            Enables or disabled horizontal grid lines
476        """
477        if b < 0:
478            b = not self._y_enabled
479        self._y_enabled = b
480        self.update_properties()
481       
482    def is_y_enabled(self):
483        """
484            Returns whether horizontal grid lines are enabled
485        """
486        return self._y_enabled
487       
488    def set_pen(self, pen):
489        """
490            Sets the pen used for drawing the grid lines
491        """
492        self._path_item.setPen(pen)
493       
494    def update_properties(self):
495        p = self.plot()
496        if p is None:
497            return
498        x_id, y_id = self.axes()
499        rect = p.data_rect_for_axes(x_id, y_id)
500        path = QPainterPath()
501        if self._x_enabled and x_id in p.axes:
502            for pos, label, size, _w in p.axes[x_id].ticks():
503                path.moveTo(pos, rect.bottom())
504                path.lineTo(pos, rect.top())
505        if self._y_enabled and y_id in p.axes:
506            for pos, label, size, _w in p.axes[y_id].ticks():
507                path.moveTo(rect.left(), pos)
508                path.lineTo(rect.right(), pos)
509        self._path_item.setPath(self.graph_transform().map(path))
510       
Note: See TracBrowser for help on using the repository browser.