source: orange/Orange/OrangeWidgets/plot/owplot.py @ 10580:c4cbae8dcf8b

Revision 10580:c4cbae8dcf8b, 69.8 KB checked in by markotoplak, 2 years ago (diff)

Moved deprecation functions, progress bar support and environ into Orange.utils. Orange imports cleanly, although it is not tested yet.

Line 
1'''
2
3#################
4Plot (``owplot``)
5#################
6
7.. autoclass:: OrangeWidgets.plot.OWPlot
8   
9'''
10
11LeftLegend = 0
12RightLegend = 1
13BottomLegend = 2
14TopLegend = 3
15ExternalLegend = 4
16
17UNUSED_ATTRIBUTES_STR = 'unused attributes'
18
19from owaxis import *
20from owcurve import *
21from owlegend import *
22from owpalette import *
23from owplotgui import OWPlotGUI
24from owtools import *
25
26## Color values copied from orngView.SchemaView for consistency
27SelectionPen = QPen(QBrush(QColor(51, 153, 255, 192)), 1, Qt.SolidLine, Qt.RoundCap)
28SelectionBrush = QBrush(QColor(168, 202, 236, 192))
29
30from PyQt4.QtGui import QGraphicsView,  QGraphicsScene, QPainter, QTransform, QPolygonF, QGraphicsItem, QGraphicsPolygonItem, QGraphicsRectItem, QRegion
31from PyQt4.QtCore import QPointF, QPropertyAnimation, pyqtProperty, SIGNAL, Qt, QEvent
32
33from OWDlgs import OWChooseImageSizeDlg
34from OWBaseWidget import unisetattr
35from OWColorPalette import *      # color palletes, ...
36from Orange.utils import deprecated_members, deprecated_attribute
37
38import orangeqt
39
40def n_min(*args):
41    lst = args[0] if len(args) == 1 else args
42    a = [i for i in lst if i is not None]
43    return min(a) if a else None
44   
45def n_max(*args):
46    lst = args[0] if len(args) == 1 else args
47    a = [i for i in lst if i is not None]
48    return max(a) if a else None
49   
50name_map = {
51    "saveToFileDirect": "save_to_file_direct", 
52    "saveToFile" : "save_to_file", 
53    "addCurve" : "add_curve", 
54    "addMarker" : "add_marker", 
55    "updateLayout" : "update_layout", 
56    "activateZooming" : "activate_zooming", 
57    "activateSelection" : "activate_selection", 
58    "activateRectangleSelection" : "activate_rectangle_selection", 
59    "activatePolygonSelection" : "activate_polygon_selection", 
60    "activatePanning" : "activate_panning",
61    "getSelectedPoints" : "get_selected_points",
62    "setAxisScale" : "set_axis_scale",
63    "setAxisLabels" : "set_axis_labels", 
64    "setAxisAutoScale" : "set_axis_autoscale",
65    "setTickLength" : "set_axis_tick_length",
66    "updateCurves" : "update_curves",
67    "itemList" : "plot_items",
68    "setShowMainTitle" : "set_show_main_title",
69    "setMainTitle" : "set_main_title",
70    "invTransform" : "inv_transform",
71    "setAxisTitle" : "set_axis_title",
72    "setShowAxisTitle" : "set_show_axis_title"
73}
74
75@deprecated_members(name_map, wrap_methods=name_map.keys())
76class OWPlot(orangeqt.Plot): 
77    """
78    The base class for all plots in Orange. It uses the Qt Graphics View Framework
79    to draw elements on a graph.
80   
81    **Plot layout**
82   
83        .. attribute:: show_legend
84   
85            A boolean controlling whether the legend is displayed or not
86       
87        .. attribute:: show_main_title
88   
89            Controls whether or not the main plot title is displayed
90           
91        .. attribute:: main_title
92       
93            The plot title, usually show on top of the plot
94           
95        .. automethod:: set_main_title
96       
97        .. automethod:: set_show_main_title
98       
99        .. attribute:: axis_margin
100           
101            How much space (in pixels) should be left on each side for the axis, its label and its title.
102       
103        .. attribute:: title_margin
104       
105            How much space (in pixels) should be left at the top of the plot for the title, if the title is shown.
106           
107            .. seealso:: attribute :attr:`show_main_title`
108           
109        .. attribute:: plot_margin
110       
111            How much space (in pixels) should be left at each side of the plot as whitespace.
112           
113   
114    **Coordinate transformation**
115   
116        There are several coordinate systems used by OWPlot:
117       
118        * `widget` coordinates.
119         
120          This is the coordinate system of the position returned by :meth:`.QEvent.pos()`.
121          No calculations or positions is done with this coordinates, they must first be converted
122          to scene coordinates with :meth:`mapToScene`.
123         
124        * `data` coordinates.
125       
126          The value used internally in Orange to specify the values of attributes.
127          For example, this can be age in years, the number of legs, or any other numeric value.
128         
129        * `plot` coordinates.
130       
131          These coordinates specify where the plot items are placed on the graph, but doesn't account for zoom.
132          They can be retrieved for a particular plot item with :meth:`.PlotItem.pos()`.
133         
134        * `scene` or `zoom` coordinates.
135       
136          Like plot coordinates, except that they take the :attr:`zoom_transform` into account. They represent the
137          actual position of an item on the scene.
138         
139          These are the coordinates returned by :meth:`.PlotItem.scenePos()` and :meth:`mapToScene`.
140         
141          For example, they can be used to determine what is under the cursor.
142         
143        In most cases, you will use data coordinates for interacting with the actual data, and scene coordinates for
144        interacting with the plot items. The other two sets are mostly used for converting.
145       
146        .. automethod:: map_to_graph
147       
148        .. automethod:: map_from_graph
149       
150        .. automethod:: transform
151       
152        .. automethod:: inv_transform
153       
154        .. method:: nearest_point(pos)
155       
156            Returns the point nearest to ``pos``, or ``None`` if no point is close enough.
157           
158            :param pos: The position in scene coordinates
159            :type pos: QPointF
160           
161            :rtype: :obj:`.OWPoint`
162           
163        .. method:: point_at(pos)
164       
165            If there is a point with data coordinates equal to ``pos``, if is returned.
166            Otherwise, this function returns None.
167       
168            :param pos: The position in data coordinates
169            :type pos: tuple of float float
170           
171            :rtype: :obj:`.OWPoint`
172       
173       
174    **Data curves**
175        The preferred method for showing a series of data points is :meth:`set_main_curve_data`.
176        It allows you to specify point positions, colors, labels, sizes and shapes.
177       
178        .. automethod:: set_main_curve_data
179       
180        .. automethod:: add_curve
181       
182        .. automethod:: add_custom_curve
183       
184        .. automethod:: add_marker
185       
186        .. method:: add_item(item)
187       
188            Adds any PlotItem ``item`` to this plot.
189            Calling this function directly is useful for adding a :obj:`.Marker` or another object that does not have to appear in the legend.
190            For data curves, consider using :meth:`add_custom_curve` instead.
191           
192        .. method:: plot_items()
193       
194            Returns the list of all plot items added to this graph with :meth:`add_item` or :meth:`.PlotItem.attach`.
195           
196    **Axes**
197   
198        .. automethod:: add_axis
199       
200        .. automethod:: add_custom_axis
201       
202        .. automethod:: set_axis_enabled
203       
204        .. automethod:: set_axis_labels
205       
206        .. automethod:: set_axis_scale
207       
208    **Settings**
209   
210    .. attribute:: gui
211   
212            An :obj:`.OWPlotGUI` object associated with this graph
213           
214    **Point Selection and Marking**
215   
216        There are four possible selection behaviors used for selecting or marking points in OWPlot.
217        They are used in :meth:`select_points` and :meth:`mark_points` and are the same for both operations.
218   
219        .. data:: AddSelection
220           
221            The points are added to the selection, without affected the currently selected points
222           
223        .. data:: RemoveSelection
224   
225            The points are removed from the selection, without affected the currently selected points
226           
227        .. data:: ToggleSelection
228       
229            The points' selection state is toggled
230           
231        .. data:: ReplaceSelection
232       
233            The current selection is replaced with the new one
234           
235        .. note:: There are exacly the same functions for point selection and marking.
236                For simplicity, they are only documented once. 
237
238        .. method:: select_points(area, behavior)
239        .. method:: mark_points(area, behavior)
240       
241            Selects or marks all points inside the ``area``
242       
243            :param area: The newly selected/marked area
244            :type area: QRectF or QPolygonF
245           
246            :param behavior: :data:`AddSelection`, :data:`RemoveSelection`, :data:`ToggleSelection` or :data:`ReplaceSelection`
247            :type behavior: int
248           
249        .. method:: unselect_all_points()
250        .. method:: unmark_all_points()
251       
252            Unselects or unmarks all the points in the plot
253           
254        .. method:: selected_points()
255        .. method:: marked_points()
256       
257            Returns a list of all selected or marked points
258           
259            :rtype: list of OWPoint
260           
261        .. method:: selected_points(xData, yData)
262       
263            For each of the point specified by ``xData`` and ``yData``, the point's selection state is returned.
264           
265            :param xData: The list of x coordinates
266            :type xData: list of float
267           
268            :param yData: The list of y coordinates
269            :type yData: list of float
270           
271            :rtype: list of int
272           
273    **Color schemes**
274   
275        By default, OWPlot uses the application's system palette for drawing everything
276        except data curves and points. This way, it maintains consistency with other application
277        with regards to the user interface.
278       
279        If data is plotted with no color specified, it will use a system color as well,
280        so that a good contrast with the background in guaranteed.
281       
282        OWPlot uses the :meth:`.OWidget.palette` to determine its color scheme, so it can be
283        changed using :meth:`.QWidget.setPalette`. There are also two predefined color schemes:
284        ``OWPalette.Dark`` and ``OWPalette.Light``, which provides a dark and a light scheme
285        respectively.
286       
287        .. attribute:: theme_name
288       
289            A string attribute with three possible values:
290            ==============  ===========================
291            Value           Meaning
292            --------------  ---------------------------
293            "default"       The system palette is used
294            "dark"          The dark theme is used
295            "light"         The light theme is used
296            ==============  ===========================
297           
298            To apply the settings, first set this attribute's value, and then call :meth:`update_theme`
299           
300        .. automethod:: update_theme
301           
302        On the other hand, curves with a specified color will use colors from Orange's palette,
303        which can be configured within Orange. Each plot contains two separate palettes:
304        one for continuous attributes, and one for discrete ones. Both are created by
305        :obj:`.OWColorPalette.ColorPaletteGenerator`
306       
307        .. attribute:: continuous_palette
308       
309            The palette used when point color represents a continuous attribute
310       
311        .. attribute:: discrete_palette
312       
313            The palette used when point color represents a discrete attribute
314
315    """
316   
317    point_settings = ["point_width", "alpha_value"]
318    plot_settings = ["show_legend", "show_grid"]
319    appearance_settings = ["antialias_plot", "animate_plot", "animate_points", "disable_animations_threshold", "auto_adjust_performance"]
320   
321    def settings_list(self, graph_name, settings):
322        return [graph_name + '.' + setting for setting in settings]
323   
324    def __init__(self, parent = None,  name = "None",  show_legend = 1, axes = [xBottom, yLeft], widget = None ):
325        """
326            Creates a new graph
327           
328            If your visualization uses axes other than ``xBottom`` and ``yLeft``, specify them in the
329            ``axes`` parameter. To use non-cartesian axes, set ``axes`` to an empty list
330            and add custom axes with :meth:`add_axis` or :meth:`add_custom_axis`
331        """
332        orangeqt.Plot.__init__(self, parent)
333        self.widget = widget
334        self.parent_name = name
335        self.show_legend = show_legend
336        self.title_item = None
337       
338        self.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
339       
340        self._legend = OWLegend(self, self.scene())
341        self._legend.setZValue(LegendZValue)
342        self._legend_margin = QRectF(0, 0, 100, 0)
343        self._legend_moved = False
344        self.axes = dict()
345
346        self.axis_margin = 50
347        self.y_axis_extra_margin = 30
348        self.title_margin = 40
349        self.graph_margin = 10
350       
351        self.mainTitle = None
352        self.showMainTitle = False
353        self.XaxisTitle = None
354        self.YLaxisTitle = None
355        self.YRaxisTitle = None
356       
357        # Method aliases, because there are some methods with different names but same functions
358        self.setCanvasBackground = self.setCanvasColor
359        self.map_from_widget = self.mapToScene
360       
361        # OWScatterPlot needs these:
362        self.point_width = 5
363        self.show_filled_symbols = True
364        self.alpha_value = 255
365        self.show_grid = True
366       
367        self.curveSymbols = range(13)
368        self.tips = TooltipManager(self)
369        self.setMouseTracking(True)
370        self.grabGesture(Qt.PinchGesture)
371        self.grabGesture(Qt.PanGesture)
372       
373        self.state = NOTHING
374        self._pressed_mouse_button = Qt.NoButton
375        self._pressed_point = None
376        self.selection_items = []
377        self._current_rs_item = None
378        self._current_ps_item = None
379        self.polygon_close_treshold = 10
380        self.sendSelectionOnUpdate = False
381        self.auto_send_selection_callback = None
382       
383        self.data_range = {}
384        self.map_transform = QTransform()
385        self.graph_area = QRectF()
386       
387        ## Performance optimization
388        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
389        self.scene().setItemIndexMethod(QGraphicsScene.NoIndex)
390       
391        self.animate_plot = True
392        self.animate_points = True
393        self.antialias_plot = True
394        self.antialias_points = True
395        self.antialias_lines = True
396       
397        self.auto_adjust_performance = True
398        self.disable_animations_threshold = 5000
399     #   self.setInteractive(False)
400
401        self.warn_unused_attributes = False
402       
403        self._bounds_cache = {}
404        self._transform_cache = {}
405        self.block_update = False
406       
407        self.use_animations = True
408        self._animations = []
409       
410        ## Mouse event handlers
411        self.mousePressEventHandler = None
412        self.mouseMoveEventHandler = None
413        self.mouseReleaseEventHandler = None
414        self.mouseStaticClickHandler = self.mouseStaticClick
415        self.static_click = False
416       
417        self._marker_items = []
418        self.grid_curve = PlotGrid(self)
419       
420        self._zoom_rect = None
421        self._zoom_transform = QTransform()
422        self.zoom_stack = []
423        self.old_legend_margin = None
424        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
425        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
426       
427        ## Add specified axes:
428       
429        for key in axes:
430            if key in [yLeft, xTop]:
431                self.add_axis(key, title_above=1)
432            else:
433                self.add_axis(key)
434               
435        self.continuous_palette = ColorPaletteGenerator(numberOfColors = -1)
436        self.discrete_palette = ColorPaletteGenerator()
437       
438        self.gui = OWPlotGUI(self)
439    """
440            An :obj:`.OWPlotGUI` object associated with this plot
441    """
442        self.activate_zooming()
443        self.selection_behavior = self.AddSelection
444       
445        self.main_curve = None
446       
447        self.replot()
448       
449    selectionCurveList = deprecated_attribute("selectionCurveList", "selection_items")
450    autoSendSelectionCallback = deprecated_attribute("autoSendSelectionCallback", "auto_send_selection_callback")
451    showLegend = deprecated_attribute("showLegend", "show_legend")
452    pointWidth = deprecated_attribute("pointWidth", "point_width")
453    alphaValue = deprecated_attribute("alphaValue", "alpha_value")
454    useAntialiasing = deprecated_attribute("useAntialiasing", "use_antialiasing")
455    showFilledSymbols = deprecated_attribute("showFilledSymbols", "show_filled_symbols")
456    mainTitle = deprecated_attribute("mainTitle", "main_title")
457    showMainTitle = deprecated_attribute("showMainTitle", "show_main_title")
458    gridCurve = deprecated_attribute("gridCurve", "grid_curve")
459    contPalette = deprecated_attribute("contPalette", "continuous_palette")
460    discPalette = deprecated_attribute("discPalette", "discrete_palette")
461   
462    def scrollContentsBy(self, dx, dy):
463        # This is overriden here to prevent scrolling with mouse and keyboard
464        # Instead of moving the contents, we simply do nothing
465        pass
466   
467    def graph_area_rect(self):
468        return self.graph_area
469       
470    def map_to_graph(self, point, axes = None, zoom = False):
471        '''
472            Maps ``point``, which can be ether a tuple of (x,y), a QPoint or a QPointF, from data coordinates
473            to plot coordinates.
474           
475            :param point: The point in data coordinates
476            :type point: tuple or QPointF
477           
478            :param axes: The pair of axes along which to transform the point.
479                         If none are specified, (xBottom, yLeft) will be used.
480            :type axes: tuple of float float
481           
482            :param zoom: if ``True``, the current :attr:`zoom_transform` will be considered in the transformation, and the result will be in scene coordinates instead.
483            :type zoom: int
484           
485            :return: The transformed point in scene coordinates
486            :type: tuple of float float
487        '''
488        if type(point) == tuple:
489            (x, y) = point
490            point = QPointF(x, y)
491        if axes:
492            x_id, y_id = axes
493            point = point * self.transform_for_axes(x_id, y_id)
494        else:
495            point = point * self.map_transform
496        if zoom:
497            point = point * self._zoom_transform
498        return (point.x(), point.y())
499       
500    def map_from_graph(self, point, axes = None, zoom = False):
501        '''
502            Maps ``point``, which can be ether a tuple of (x,y), a QPoint or a QPointF, from plot coordinates
503            to data coordinates.
504           
505            :param point: The point in data coordinates
506            :type point: tuple or QPointF
507           
508            :param axes: The pair of axes along which to transform the point. If none are specified, (xBottom, yLeft) will be used.
509            :type axes: tuple of float float
510           
511            :param zoom: if ``True``, the current :attr:`zoom_transform` will be considered in the transformation, and the ``point`` should be in scene coordinates instead.
512            :type zoom: int
513           
514            :returns: The transformed point in data coordinates
515            :rtype: tuple of float float
516        '''
517        if type(point) == tuple:
518            (x, y) = point
519            point = QPointF(x,y)
520        if zoom:
521            t, ok = self._zoom_transform.inverted()
522            point = point * t
523        if axes:
524            x_id, y_id = axes
525            t, ok = self.transform_for_axes(x_id, y_id).inverted()
526        else:
527            t, ok = self.map_transform.inverted()
528        ret = point * t
529        return (ret.x(), ret.y())
530       
531    def save_to_file(self, extraButtons = []):
532        sizeDlg = OWChooseImageSizeDlg(self, extraButtons, parent=self)
533        sizeDlg.exec_()
534       
535    def save_to_file_direct(self, fileName, size = None):
536        sizeDlg = OWChooseImageSizeDlg(self)
537        sizeDlg.saveImage(fileName, size)
538       
539    def activate_zooming(self):
540        '''
541            Activates the zooming mode, where the user can zoom in and out with a single mouse click
542            or by dragging the mouse to form a rectangular area
543        '''
544        self.state = ZOOMING
545       
546    def activate_rectangle_selection(self):
547        '''
548            Activates the rectangle selection mode, where the user can select points in a rectangular area
549            by dragging the mouse over them
550        '''
551        self.state = SELECT_RECTANGLE
552       
553    def activate_selection(self):
554        '''
555            Activates the point selection mode, where the user can select points by clicking on them
556        '''
557        self.state = SELECT
558       
559    def activate_polygon_selection(self):
560        '''
561            Activates the polygon selection mode, where the user can select points by drawing a polygon around them
562        '''
563        self.state = SELECT_POLYGON
564       
565    def activate_panning(self):
566        '''
567            Activates the panning mode, where the user can move the zoom projection by dragging the mouse
568        '''
569        self.state = PANNING
570       
571    def set_show_main_title(self, b):
572        '''
573            Shows the main title if ``b`` is ``True``, and hides it otherwise.
574        '''
575        self.showMainTitle = b
576        self.replot()
577
578    def set_main_title(self, t):
579        '''
580            Sets the main title to ``t``
581        '''
582        self.mainTitle = t
583        self.replot()
584
585    def setShowXaxisTitle(self, b = -1):
586        if b == -1 and hasattr(self, 'showXaxisTitle'):
587            b = self.showXaxisTitle
588        self.setShowAxisTitle(xBottom, b)
589       
590    def setXaxisTitle(self, title):
591        self.setAxisTitle(xBottom, title)
592
593    def setShowYLaxisTitle(self, b = -1):
594        if b == -1 and hasattr(self, 'showYLaxisTitle'):
595            b = self.showYLaxisTitle
596        self.setShowAxisTitle(yLeft, b)
597
598    def setYLaxisTitle(self, title):
599        self.setAxisTitle(yLeft, title)
600
601    def setShowYRaxisTitle(self, b = -1):
602        if b == -1 and hasattr(self, 'showYRaxisTitle'):
603            b = self.showYRaxisTitle
604        self.setShowAxisTitle(yRight, b)
605
606    def setYRaxisTitle(self, title):
607        self.setAxisTitle(yRight, title)
608
609    def enableGridXB(self, b):
610        self.grid_curve.set_x_enabled(b)
611        self.replot()
612
613    def enableGridYL(self, b):
614        self.grid_curve.set_y_enabled(b)
615        self.replot()
616
617    def setGridColor(self, c):
618        self.grid_curve.set_pen(QPen(c))
619        self.replot()
620
621    def setCanvasColor(self, c):
622        p = self.palette()
623        p.setColor(OWPalette.Canvas, c)
624        self.set_palette(p)
625       
626    def setData(self, data):
627        self.clear()
628        self.replot()
629       
630    def setXlabels(self, labels):
631        if xBottom in self.axes:
632            self.set_axis_labels(xBottom, labels)
633        elif xTop in self.axes:
634            self.set_axis_labels(xTop, labels)
635           
636    def set_axis_autoscale(self, axis_id):
637        if axis_id in self.axes:
638            self.axes[axis_id].auto_scale = True
639        elif axis_id in self.data_range:
640            del self.data_range[axis_id]
641       
642    def set_axis_labels(self, axis_id, labels):
643        '''
644            Sets the labels of axis ``axis_id`` to ``labels``. This is used for axes displaying a discrete data type.
645           
646            :param labels: The ID of the axis to change
647            :type labels: int
648           
649            :param labels: The list of labels to be displayed along the axis
650            :type labels: A list of strings
651           
652            .. note:: This changes the axis scale and removes any previous scale set with :meth:`set_axis_scale`.
653        '''
654        if axis_id in self._bounds_cache:
655            del self._bounds_cache[axis_id]
656        self._transform_cache = {}
657        self.axes[axis_id].set_labels(labels)
658   
659    def set_axis_scale(self, axis_id, min, max, step_size=0):
660        '''
661            Sets the scale of axis ``axis_id`` to show an interval between ``min`` and ``max``.
662            If ``step`` is specified and non-zero, it determines the steps between label on the axis.
663            Otherwise, they are calculated automatically.
664           
665            .. note:: This changes the axis scale and removes any previous labels set with :meth:`set_axis_labels`.
666        '''
667        if axis_id in self._bounds_cache:
668            del self._bounds_cache[axis_id]
669        self._transform_cache = {}
670        if axis_id in self.axes:
671            self.axes[axis_id].set_scale(min, max, step_size)
672        else:
673            self.data_range[axis_id] = (min, max)
674           
675    def set_axis_title(self, axis_id, title):
676        if axis_id in self.axes:
677            self.axes[axis_id].set_title(title)
678           
679    def set_show_axis_title(self, axis_id, b):
680        if axis_id in self.axes:
681            if b == -1:
682                b = not self.axes[axis_id].show_title
683            self.axes[axis_id].set_show_title(b)
684            self.replot()
685       
686    def set_axis_tick_length(self, axis_id, minor, medium, major):
687        if axis_id in self.axes:
688            self.axes[axis_id].set_tick_legth(minor, medium, major)
689
690    def setYLlabels(self, labels):
691        self.set_axis_labels(yLeft, labels)
692
693    def setYRlabels(self, labels):
694        self.set_axis_labels(yRight, labels)
695       
696    def add_custom_curve(self, curve, enableLegend = False):
697        '''
698            Adds a custom PlotItem ``curve`` to the plot.
699            If ``enableLegend`` is ``True``, a curve symbol defined by
700            :meth:`.OWCurve.point_item` and the ``curve``'s name
701            :obj:`.OWCurve.name` is added to the legend.
702           
703            This function recalculates axis bounds and replots the plot if needed.
704           
705            :param curve: The curve to add
706            :type curve: :obj:`.OWCurve`
707        '''
708        self.add_item(curve)
709        if enableLegend:
710            self.legend().add_curve(curve)
711        for key in [curve.axes()]:
712            if key in self._bounds_cache:
713                del self._bounds_cache[key]
714        self._transform_cache = {}
715        if hasattr(curve, 'tooltip'):
716            curve.setToolTip(curve.tooltip)
717        x,y = curve.axes()
718        if curve.is_auto_scale() and (self.is_axis_auto_scale(x) or self.is_axis_auto_scale(y)):
719            self.set_dirty()
720            self.replot()
721        else:
722            curve.set_graph_transform(self.transform_for_axes(x,y))
723            curve.update_properties()
724        return curve
725       
726    def add_curve(self, name, brushColor = None, penColor = None, size = 5, style = Qt.NoPen, 
727                 symbol = OWPoint.Ellipse, enableLegend = False, xData = [], yData = [], showFilledSymbols = None,
728                 lineWidth = 1, pen = None, autoScale = 0, antiAlias = None, penAlpha = 255, brushAlpha = 255, 
729                 x_axis_key = xBottom, y_axis_key = yLeft):
730        '''
731            Creates a new :obj:`.OWCurve` with the specified parameters and adds it to the graph.
732            If ``enableLegend`` is ``True``, a curve symbol is added to the legend.
733        '''
734        c = OWCurve(xData, yData, x_axis_key, y_axis_key, tooltip=name)
735        c.set_zoom_transform(self._zoom_transform)
736        c.name = name
737        c.set_style(style)
738       
739        if not brushColor:
740            brushColor = self.color(OWPalette.Data)
741        if not penColor:
742            penColor = self.color(OWPalette.Data)
743       
744        c.set_color(penColor)
745       
746        if pen:
747            p = pen
748        else:
749            p = QPen()
750            p.setColor(penColor)
751            p.setWidth(lineWidth)
752        c.set_pen(p)
753       
754        c.set_brush(brushColor)
755       
756        c.set_symbol(symbol)
757        c.set_point_size(size)
758        c.set_data(xData,  yData)
759       
760        c.set_auto_scale(autoScale)
761       
762        return self.add_custom_curve(c, enableLegend)
763               
764    def set_main_curve_data(self, x_data, y_data, color_data, label_data, size_data, shape_data, marked_data = [], valid_data = [], x_axis_key=xBottom, y_axis_key=yLeft):
765        """
766            Creates a single curve that can have points of different colors, shapes and sizes.
767            This is the preferred method for visualization that show a series of different points.
768           
769            :param x_data: The list of X coordinates of the points
770            :type x_data: list of float
771           
772            :param y_data: The list of Y coordinates of the points
773            :type y_data: list of float
774           
775            :param color_data: The list of point colors
776            :type color_data: list of QColor
777           
778            :param label_data: The list of point labels
779            :type label_data: list of str
780           
781            :param size_data: The list of point sizes
782            :type size_data: list of int
783           
784            :param shape_data: The list of point symbols
785            :type shape_data: list of int
786           
787            The number of points in the curve will be equal to min(len(x_data), len(y_data)).
788            The other four list can be empty, in which case a default value will be used.
789            If they contain only one element, its value will be used for all points.
790           
791            .. note:: This function does not add items to the legend automatically.
792                      You will have to add them yourself with :meth:`.OWLegend.add_item`.
793                     
794            .. seealso:: :obj:`.OWMultiCurve`, :obj:`.OWPoint`
795        """
796        if not self.main_curve:
797            self.main_curve = OWMultiCurve([], [])
798            self.add_item(self.main_curve)
799           
800        self.update_performance(len(x_data))
801       
802        if len(valid_data):
803            import numpy
804            x_data = numpy.compress(valid_data, x_data)
805            y_data = numpy.compress(valid_data, y_data)
806            if len(color_data) > 1:
807                color_data = numpy.compress(valid_data, color_data)
808            if len(size_data) > 1:
809                size_data = numpy.compress(valid_data, size_data)
810            if len(shape_data) > 1:
811                shape_data = numpy.compress(valid_data, shape_data)
812            if len(label_data) > 1:
813                label_data = numpy.compress(valid_data, label_data)
814            if len(marked_data) > 1:
815                marked_data = numpy.compress(valid_data, marked_data).tolist()
816       
817        c = self.main_curve
818        c.set_data(x_data, y_data)
819        c.set_axes(x_axis_key, y_axis_key)
820        c.set_point_colors(color_data)
821        c.set_point_labels(label_data)
822        c.set_point_sizes(size_data)
823        c.set_point_symbols(shape_data)
824        if len(marked_data):
825            c.set_points_marked(marked_data)
826            self.emit(SIGNAL('marked_points_changed()'))
827        c.name = 'Main Curve'
828   
829        self.replot()
830       
831    def remove_curve(self, item):
832        '''
833            Removes ``item`` from the plot
834        '''
835        self.remove_item(item)
836        self.legend().remove_curve(item)
837       
838    def plot_data(self, xData, yData, colors, labels, shapes, sizes):
839        pass
840       
841    def add_axis(self, axis_id, title = '', title_above = False, title_location = AxisMiddle, line = None, arrows = AxisEnd, zoomable = False):
842        '''
843            Creates an :obj:`OrangeWidgets.plot.OWAxis` with the specified ``axis_id`` and ``title``.
844        '''
845        a = OWAxis(axis_id, title, title_above, title_location, line, arrows, self)
846        self.scene().addItem(a)
847        a.zoomable = zoomable
848        a.update_callback = self.replot
849        if axis_id in self._bounds_cache:
850            del self._bounds_cache[axis_id]
851        self._transform_cache = {}
852        self.axes[axis_id] = a
853        if not axis_id in CartesianAxes:
854            self.setShowAxisTitle(axis_id, True)
855        return a
856       
857    def remove_all_axes(self, user_only = True):
858        '''
859            Removes all axes from the plot
860        '''
861        ids = []
862        for id,item in self.axes.iteritems():
863            if not user_only or id >= UserAxis:
864                ids.append(id)
865                self.scene().removeItem(item)
866        for id in ids:
867            del self.axes[id]
868       
869    def add_custom_axis(self, axis_id, axis):
870        '''
871            Adds a custom ``axis`` with id ``axis_id`` to the plot
872        '''
873        self.axes[axis_id] = axis
874        self.replot()
875       
876    def add_marker(self, name, x, y, alignment = -1, bold = 0, color = None, brushColor = None, size=None, antiAlias = None, 
877                    x_axis_key = xBottom, y_axis_key = yLeft):
878        m = Marker(name, x, y, alignment, bold, color, brushColor)
879        self._marker_items.append((m, x, y, x_axis_key, y_axis_key))
880        self.add_custom_curve(m)
881       
882        return m
883       
884    def removeAllSelections(self):
885        ## TODO
886        pass
887       
888    def clear(self):
889        '''
890            Clears the plot, removing all curves, markers and tooltips.
891            Axes and the grid are not removed
892        '''
893        for i in self.plot_items():
894            if i is not self.grid_curve:
895                self.remove_item(i)
896        self.main_curve = None
897        self._bounds_cache = {}
898        self._transform_cache = {}
899        self.clear_markers()
900        self.tips.removeAll()
901        self.legend().clear()
902        self.old_legend_margin = None
903        self.update_grid()
904       
905    def clear_markers(self):
906        '''
907            Removes all markers added with :meth:`add_marker` from the plot
908        '''
909        for item,x,y,x_axis,y_axis in self._marker_items:
910            item.detach()
911        self._marker_items = []
912       
913    def update_layout(self):
914        '''
915            Updates the plot layout.
916           
917            This function recalculates the position of titles, axes, the legend and the main plot area.
918            It does not update the curve or the other plot items.
919        '''
920        if not self.isVisible():
921            # No point in updating the graph if it's still hidden
922            return
923        graph_rect = QRectF(self.contentsRect())
924        self.centerOn(graph_rect.center())
925        m = self.graph_margin
926        graph_rect.adjust(m, m, -m, -m)
927       
928        if self.showMainTitle and self.mainTitle:
929            if self.title_item:
930                self.scene().remove_item(self.title_item)
931                del self.title_item
932            self.title_item = QGraphicsTextItem(self.mainTitle, scene=self.scene())
933            title_size = self.title_item.boundingRect().size()
934            ## TODO: Check if the title is too big
935            self.title_item.setPos( graph_rect.width()/2 - title_size.width()/2, self.title_margin/2 - title_size.height()/2 )
936            graph_rect.setTop(graph_rect.top() + self.title_margin)
937       
938        if self.show_legend:
939            self._legend_outside_area = QRectF(graph_rect)
940            self._legend.max_size = self._legend_outside_area.size()
941            r = self._legend_margin
942            graph_rect.adjust(r.left(), r.top(), -r.right(), -r.bottom())
943           
944        self._legend.update_items()
945           
946        axis_rects = dict()
947        base_margin = min(self.axis_margin,  graph_rect.height()/4, graph_rect.height()/4)
948        if xBottom in self.axes and self.axes[xBottom].isVisible():
949            margin = base_margin
950            if self.axes[xBottom].should_be_expanded():
951                margin += min(20, graph_rect.height()/8, graph_rect.width() / 8)
952            bottom_rect = QRectF(graph_rect)
953            bottom_rect.setTop( bottom_rect.bottom() - margin)
954            axis_rects[xBottom] = bottom_rect
955            graph_rect.setBottom( graph_rect.bottom() - margin)
956        if xTop in self.axes and self.axes[xTop].isVisible():
957            margin = base_margin
958            if self.axes[xTop].should_be_expanded():
959                margin += min(20, graph_rect.height()/8, graph_rect.width() / 8)
960            top_rect = QRectF(graph_rect)
961            top_rect.setBottom(top_rect.top() + margin)
962            axis_rects[xTop] = top_rect
963            graph_rect.setTop(graph_rect.top() + margin)
964        if yLeft in self.axes and self.axes[yLeft].isVisible():
965            margin = base_margin
966            if self.axes[yLeft].should_be_expanded():
967                margin += min(20, graph_rect.height()/8, graph_rect.width() / 8)
968            left_rect = QRectF(graph_rect)
969            left = graph_rect.left() + margin + self.y_axis_extra_margin
970            left_rect.setRight(left)
971            graph_rect.setLeft(left)
972            axis_rects[yLeft] = left_rect
973            if xBottom in axis_rects:
974                axis_rects[xBottom].setLeft(left)
975            if xTop in axis_rects:
976                axis_rects[xTop].setLeft(left)
977        if yRight in self.axes and self.axes[yRight].isVisible():
978            margin = base_margin
979            if self.axes[yRight].should_be_expanded():
980                margin += min(20, graph_rect.height()/8, graph_rect.width() / 8)
981            right_rect = QRectF(graph_rect)
982            right = graph_rect.right() - margin - self.y_axis_extra_margin
983            right_rect.setLeft(right)
984            graph_rect.setRight(right)
985            axis_rects[yRight] = right_rect
986            if xBottom in axis_rects:
987                axis_rects[xBottom].setRight(right)
988            if xTop in axis_rects:
989                axis_rects[xTop].setRight(right)
990               
991        if self.graph_area != graph_rect:
992            self.graph_area = QRectF(graph_rect)
993            self.set_graph_rect(self.graph_area)
994            self._transform_cache = {}
995            if self._zoom_rect:
996                data_zoom_rect = self.map_transform.inverted()[0].mapRect(self._zoom_rect)
997                self.map_transform = self.transform_for_axes()
998                self.set_zoom_rect(self.map_transform.mapRect(data_zoom_rect))
999            else:
1000                self.map_transform = self.transform_for_axes()
1001       
1002        for c in self.plot_items():
1003            x,y = c.axes()
1004            c.set_graph_transform(self.transform_for_axes(x,y))
1005            c.update_properties()
1006           
1007    def update_zoom(self):
1008        '''
1009            Updates the zoom transformation of the plot items.
1010        '''
1011        zt = self.zoom_transform()
1012        self._zoom_transform = zt
1013        self.set_zoom_transform(zt)
1014       
1015        self.update_axes(zoom_only=True)
1016        self.viewport().update()
1017       
1018    def update_axes(self, zoom_only=False):
1019        """
1020            Updates the axes.
1021           
1022            If ``zoom_only`` is ``True``, only the positions of the axes and their labels are recalculated.
1023            Otherwise, all their labels are updated.
1024        """
1025        if self.warn_unused_attributes and not zoom_only:
1026            self._legend.remove_category(UNUSED_ATTRIBUTES_STR)
1027           
1028        for id, item in self.axes.iteritems():
1029            if item.scale is None and item.labels is None:
1030                item.auto_range = self.bounds_for_axis(id)
1031           
1032            if id in XAxes:
1033                (x,y) = (id, yLeft)
1034            elif id in YAxes:
1035                (x,y) = (xBottom, id)
1036            else:
1037                (x,y) = (xBottom, yLeft)
1038               
1039            if id in CartesianAxes:
1040                ## This class only sets the lines for these four axes, widgets are responsible for the rest
1041                if x in self.axes and y in self.axes:
1042                    item.data_line = self.axis_line(self.data_rect_for_axes(x,y), id)
1043            if id in CartesianAxes:
1044                item.graph_line = self.axis_line(self.graph_area, id, invert_y = True)
1045            elif item.data_line:
1046                t = self.transform_for_axes(x, y)
1047                item.graph_line = t.map(item.data_line)
1048           
1049            if item.graph_line and item.zoomable:
1050                item.graph_line = self._zoom_transform.map(item.graph_line)
1051               
1052            if not zoom_only:
1053                if item.graph_line:
1054                    item.show()
1055                else:
1056                    item.hide()
1057                    if self.warn_unused_attributes:
1058                        self._legend.add_item(UNUSED_ATTRIBUTES_STR, item.title, None)
1059            item.zoom_transform = self._zoom_transform
1060            item.update(zoom_only)
1061       
1062    def replot(self):
1063        '''
1064            Replot the entire graph.
1065           
1066            This functions redraws everything on the graph, so it can be very slow
1067        '''
1068        self.setBackgroundBrush(self.color(OWPalette.Canvas))
1069        self._bounds_cache = {}
1070        self._transform_cache = {}
1071        self.set_clean()
1072        self.update_antialiasing()
1073        self.update_legend()
1074        self.update_layout()
1075        self.update_zoom()
1076        self.update_axes()
1077        self.update_grid()
1078        self.update_filled_symbols()
1079        self.setSceneRect(QRectF(self.contentsRect()))
1080        self.viewport().update()
1081       
1082    def update_legend(self):
1083        if self.show_legend and not self._legend_moved:
1084            ## If the legend hasn't been moved it, we set it outside, in the top right corner
1085            m = self.graph_margin
1086            r = QRectF(self.contentsRect())
1087            r.adjust(m, m, -m, -m)
1088            self._legend.max_size = r.size()
1089            self._legend.update_items()
1090            w = self._legend.boundingRect().width()
1091            self._legend_margin = QRectF(0, 0, w, 0)
1092            self._legend.set_floating(False)
1093            self._legend.set_orientation(Qt.Vertical)
1094            self._legend.setPos(QRectF(self.contentsRect()).topRight() + QPointF(-w, 0))
1095           
1096       
1097        if (self._legend.isVisible() == self.show_legend):
1098            return
1099           
1100        self._legend.setVisible(self.show_legend)
1101        if self.show_legend:
1102            if self.old_legend_margin is not None:
1103                self.animate(self, 'legend_margin', self.old_legend_margin, duration = 100)
1104            else:
1105                r = self.legend_rect()
1106                self.ensure_inside(r, self.contentsRect())
1107                self._legend.setPos(r.topLeft())
1108                self.notify_legend_moved(r.topLeft())
1109        else:
1110            self.old_legend_margin = self.legend_margin
1111            self.animate(self, 'legend_margin', QRectF(), duration=100)
1112       
1113    def update_filled_symbols(self):
1114        ## TODO: Implement this in Curve.cpp
1115        pass
1116   
1117    def update_grid(self):
1118        self.grid_curve.set_x_enabled(self.show_grid)
1119        self.grid_curve.set_y_enabled(self.show_grid)
1120        self.grid_curve.update_properties()
1121       
1122    def legend(self):
1123        '''
1124            Returns the plot's legend, which is a :obj:`OrangeWidgets.plot.OWLegend`
1125        '''
1126        return self._legend
1127       
1128    def legend_rect(self):
1129        if self.show_legend:
1130            return self._legend.mapRectToScene(self._legend.boundingRect())
1131        else:
1132            return QRectF()
1133       
1134    def isLegendEvent(self, event, function):
1135        if self.show_legend and self.legend_rect().contains(self.mapToScene(event.pos())):
1136            function(self, event)
1137            return True
1138        else:
1139            return False
1140   
1141    def mouse_action(self, event):
1142        b = event.buttons() | event.button()
1143        m = event.modifiers()
1144        if b == Qt.LeftButton | Qt.RightButton:
1145            b = Qt.MidButton
1146        if m & Qt.AltModifier and b == Qt.LeftButton:
1147            m = m & ~Qt.AltModifier
1148            b = Qt.MidButton
1149       
1150        if b == Qt.LeftButton and not m:
1151            return self.state
1152       
1153        if b == Qt.RightButton and not m and self.state == SELECT:
1154            return SELECT_RIGHTCLICK
1155           
1156        if b == Qt.MidButton:
1157            return PANNING
1158           
1159        if b in [Qt.LeftButton, Qt.RightButton] and (self.state == ZOOMING or m == Qt.ControlModifier):
1160            return ZOOMING
1161           
1162        if b == Qt.LeftButton and m == Qt.ShiftModifier:
1163            return SELECT
1164   
1165    ## Event handling
1166   
1167    def event(self, event):
1168        if event.type() == QEvent.Gesture:
1169            return self.gestureEvent(event)
1170        else:
1171            return orangeqt.Plot.event(self, event)
1172           
1173    def gestureEvent(self, event):
1174        for gesture in event.gestures():
1175            if gesture.state() == Qt.GestureStarted:
1176                self.current_gesture_scale = 1.
1177                event.accept(gesture)
1178                continue
1179            elif gesture.gestureType() == Qt.PinchGesture:
1180                old_animate_plot = self.animate_plot
1181                self.animate_plot = False
1182                self.zoom(gesture.centerPoint(), gesture.scaleFactor()/self.current_gesture_scale )
1183                self.current_gesture_scale = gesture.scaleFactor()
1184                self.animate_plot = old_animate_plot
1185            elif gesture.gestureType() == Qt.PanGesture:
1186                self.pan(gesture.delta())
1187        return True
1188   
1189    def resizeEvent(self, event):
1190        self.replot()
1191        s = event.size() - event.oldSize()
1192        if self.legend_margin.right() > 0:
1193            self._legend.setPos(self._legend.pos() + QPointF(s.width(), 0))
1194        if self.legend_margin.bottom() > 0:
1195            self._legend.setPos(self._legend.pos() + QPointF(0, s.height()))
1196       
1197    def showEvent(self, event):
1198        self.replot()
1199
1200    def mousePressEvent(self, event):
1201        self.static_click = True
1202        self._pressed_mouse_button = event.button()
1203        self._pressed_mouse_pos = event.pos()
1204
1205        if self.mousePressEventHandler and self.mousePressEventHandler(event):
1206            event.accept()
1207            return
1208           
1209        if self.isLegendEvent(event, QGraphicsView.mousePressEvent):
1210            return
1211       
1212        point = self.mapToScene(event.pos())
1213        a = self.mouse_action(event)
1214
1215        if a == SELECT and hasattr(self, 'move_selected_points'):
1216            self._pressed_point = self.nearest_point(point)
1217            self._pressed_point_coor = None 
1218            if self._pressed_point is not None:
1219                self._pressed_point_coor = self._pressed_point.coordinates()
1220           
1221        if a == PANNING:
1222            self._last_pan_pos = point
1223            event.accept()
1224        else:
1225            orangeqt.Plot.mousePressEvent(self, event)
1226           
1227    def mouseMoveEvent(self, event):
1228        if event.buttons() and (self._pressed_mouse_pos - event.pos()).manhattanLength() > qApp.startDragDistance():
1229            self.static_click = False
1230       
1231        if self.mouseMoveEventHandler and self.mouseMoveEventHandler(event):
1232            event.accept()
1233            return
1234           
1235        if self.isLegendEvent(event, QGraphicsView.mouseMoveEvent):
1236            return
1237       
1238        point = self.mapToScene(event.pos())
1239        if not self._pressed_mouse_button:
1240            if self.receivers(SIGNAL('point_hovered(Point*)')) > 0:
1241                self.emit(SIGNAL('point_hovered(Point*)'), self.nearest_point(point))
1242       
1243        ## We implement a workaround here, because sometimes mouseMoveEvents are not fast enough
1244        ## so the moving legend gets left behind while dragging, and it's left in a pressed state
1245        if self._legend.mouse_down:
1246            QGraphicsView.mouseMoveEvent(self, event)
1247            return
1248           
1249        a = self.mouse_action(event)
1250       
1251        if a == SELECT and self._pressed_point is not None and self._pressed_point.is_selected() and hasattr(self, 'move_selected_points'):
1252            animate_points = self.animate_points
1253            self.animate_points = False
1254            x1, y1 = self._pressed_point_coor
1255            x2, y2 = self.map_from_graph(point)
1256            self.move_selected_points((x2 - x1, y2 - y1))
1257            self.replot()
1258            if self._pressed_point is not None:
1259                self._pressed_point_coor = self._pressed_point.coordinates()
1260               
1261            self.animate_points = animate_points
1262           
1263        elif a in [SELECT, ZOOMING] and self.graph_area.contains(point):
1264            if not self._current_rs_item:
1265                self._selection_start_point = self.mapToScene(self._pressed_mouse_pos)
1266                self._current_rs_item = QGraphicsRectItem(scene=self.scene())
1267                self._current_rs_item.setPen(SelectionPen)
1268                self._current_rs_item.setBrush(SelectionBrush)
1269                self._current_rs_item.setZValue(SelectionZValue)
1270            self._current_rs_item.setRect(QRectF(self._selection_start_point, point).normalized())
1271        elif a == PANNING:
1272            if not self._last_pan_pos:
1273                self._last_pan_pos = self.mapToScene(self._pressed_mouse_pos)
1274            self.pan(point - self._last_pan_pos)
1275            self._last_pan_pos = point
1276        else:
1277            x, y = self.map_from_graph(point, zoom=True)
1278            text, x, y = self.tips.maybeTip(x, y)
1279            if type(text) == int: 
1280                text = self.buildTooltip(text)
1281            if text and x is not None and y is not None:
1282                tp = self.mapFromScene(QPointF(x,y) * self.map_transform * self._zoom_transform)
1283                self.showTip(tp.x(), tp.y(), text)
1284            else:
1285                orangeqt.Plot.mouseMoveEvent(self, event)
1286       
1287    def mouseReleaseEvent(self, event):
1288        self._pressed_mouse_button = Qt.NoButton
1289
1290        if self.mouseReleaseEventHandler and self.mouseReleaseEventHandler(event):
1291            event.accept()
1292            return
1293        if self.static_click and self.mouseStaticClickHandler and self.mouseStaticClickHandler(event):
1294            event.accept()
1295            return
1296       
1297        if self.isLegendEvent(event, QGraphicsView.mouseReleaseEvent):
1298            return
1299       
1300        a = self.mouse_action(event)
1301        if a == SELECT and self._pressed_point is not None:
1302            self._pressed_point = None
1303        if a in [ZOOMING, SELECT] and self._current_rs_item:
1304            rect = self._current_rs_item.rect()
1305            if a == ZOOMING:
1306                self.zoom_to_rect(self._zoom_transform.inverted()[0].mapRect(rect))
1307            else:
1308                self.add_selection(rect)
1309            self.scene().removeItem(self._current_rs_item)
1310            self._current_rs_item = None
1311            return
1312        orangeqt.Plot.mouseReleaseEvent(self, event)
1313   
1314    def mouseStaticClick(self, event):
1315        point = self.mapToScene(event.pos())
1316        if point not in self.graph_area:
1317            return False
1318           
1319        a = self.mouse_action(event)
1320        b = event.buttons() | event.button()
1321       
1322        if a == ZOOMING:
1323            if event.button() == Qt.LeftButton:
1324                self.zoom_in(point)
1325            elif event.button() == Qt.RightButton:
1326                self.zoom_back()
1327            else:
1328                return False
1329            return True
1330        elif a == SELECT and b == Qt.LeftButton:
1331            point_item = self.nearest_point(point)
1332            b = self.selection_behavior
1333           
1334            if b == self.ReplaceSelection:
1335                self.unselect_all_points()
1336                b = self.AddSelection
1337           
1338            if point_item:
1339                point_item.set_selected(b == self.AddSelection or (b == self.ToggleSelection and not point_item.is_selected()))
1340            self.emit(SIGNAL('selection_changed()'))
1341        elif a == SELECT and b == Qt.RightButton:
1342            point_item = self.nearest_point(point)
1343            if point_item:
1344                self.emit(SIGNAL('point_rightclicked(Point*)'), self.nearest_point(point))
1345            else:
1346                self.unselect_all_points()
1347        else:
1348            return False
1349           
1350    def wheelEvent(self, event):
1351        point = self.mapToScene(event.pos())
1352        d = event.delta() / 120.0
1353        self.zoom(point, pow(2,d))
1354           
1355    @staticmethod
1356    def transform_from_rects(r1, r2):
1357        """
1358            Returns a QTransform that maps from rectangle ``r1`` to ``r2``.
1359        """
1360        if r1 is None or r2 is None:
1361            return QTransform()
1362        if r1.width() == 0 or r1.height() == 0 or r2.width() == 0 or r2.height() == 0:
1363            return QTransform()
1364        tr1 = QTransform().translate(-r1.left(), -r1.top())
1365        ts = QTransform().scale(r2.width()/r1.width(), r2.height()/r1.height())
1366        tr2 = QTransform().translate(r2.left(), r2.top())
1367        return tr1 * ts * tr2
1368       
1369    def transform_for_zoom(self, factor, point, rect):
1370        if factor == 1:
1371            return QTransform()
1372           
1373        dp = point
1374       
1375        t = QTransform()
1376        t.translate(dp.x(), dp.y())
1377        t.scale(factor, factor)
1378        t.translate(-dp.x(), -dp.y())
1379        return t
1380       
1381    def rect_for_zoom(self, point, old_rect, scale = 2):
1382        r = QRectF()
1383        r.setWidth(old_rect.width() / scale)
1384        r.setHeight(old_rect.height() / scale)
1385        r.moveCenter(point)
1386       
1387        self.ensure_inside(r, self.graph_area)
1388       
1389        return r
1390       
1391    def set_state(self, state):
1392        self.state = state
1393        if state != SELECT_RECTANGLE:
1394            self._current_rs_item = None
1395        if state != SELECT_POLYGON:
1396            self._current_ps_item = None
1397       
1398    def get_selected_points(self, xData, yData, validData):
1399        if self.main_curve:
1400            selected = []
1401            points = self.main_curve.points()
1402            i = 0
1403            for d in validData:
1404                if d:
1405                    selected.append(points[i].is_selected())
1406                    i += 1
1407                else:
1408                    selected.append(False)
1409        else:
1410            selected = self.selected_points(xData, yData)
1411        unselected = [not i for i in selected]
1412        return selected, unselected
1413       
1414    def add_selection(self, reg):
1415        """
1416            Selects all points in the region ``reg`` using the current :attr: `selection_behavior`.
1417        """
1418        self.select_points(reg, self.selection_behavior)
1419        self.viewport().update()
1420        if self.auto_send_selection_callback:
1421            self.auto_send_selection_callback()
1422       
1423    def points_equal(self, p1, p2):
1424        if type(p1) == tuple:
1425            (x, y) = p1
1426            p1 = QPointF(x, y)
1427        if type(p2) == tuple:
1428            (x, y) = p2
1429            p2 = QPointF(x, y)
1430        return (QPointF(p1)-QPointF(p2)).manhattanLength() < self.polygon_close_treshold
1431       
1432    def data_rect_for_axes(self, x_axis = xBottom, y_axis = yLeft):
1433        """
1434            Calculates the bounding rectangle in data coordinates for the axes ``x_axis`` and ``y_axis``.
1435        """
1436        if x_axis in self.axes and y_axis in self.axes:
1437            x_min, x_max = self.bounds_for_axis(x_axis, try_auto_scale=True)
1438            y_min, y_max = self.bounds_for_axis(y_axis, try_auto_scale=True)
1439            if (x_min or x_max) and (y_min or y_max):
1440                r = QRectF(x_min, y_min, x_max-x_min, y_max-y_min)
1441                return r
1442        r = orangeqt.Plot.data_rect_for_axes(self, x_axis, y_axis)
1443        for id, axis in self.axes.iteritems():
1444            if id not in CartesianAxes and axis.data_line:
1445                r |= QRectF(axis.data_line.p1(), axis.data_line.p2())
1446        ## We leave a 5% margin on each side so the graph doesn't look overcrowded
1447        ## TODO: Perhaps change this from a fixed percentage to always round to a round number
1448        dx = r.width()/20.0
1449        dy = r.height()/20.0
1450        r.adjust(-dx, -dy, dx, dy)
1451        return r
1452       
1453    def transform_for_axes(self, x_axis = xBottom, y_axis = yLeft):
1454        """
1455            Returns the graph transform that maps from data to scene coordinates using axes ``x_axis`` and ``y_axis``.
1456        """
1457        if not (x_axis, y_axis) in self._transform_cache:
1458            # We must flip the graph area, becase Qt coordinates start from top left, while graph coordinates start from bottom left
1459            a = QRectF(self.graph_area)
1460            t = a.top()
1461            a.setTop(a.bottom())
1462            a.setBottom(t)
1463            self._transform_cache[(x_axis, y_axis)] = self.transform_from_rects(self.data_rect_for_axes(x_axis, y_axis), a)
1464        return self._transform_cache[(x_axis, y_axis)]
1465       
1466    def transform(self, axis_id, value):
1467        """
1468            Transforms the ``value`` from data to plot coordinates along the axis ``axis_id``.
1469           
1470            This function always ignores zoom. If you need to account for zooming, use :meth:`map_to_graph`.
1471        """
1472        if axis_id in XAxes:
1473            size = self.graph_area.width()
1474            margin = self.graph_area.left()
1475        else:
1476            size = self.graph_area.height()
1477            margin = self.graph_area.top()
1478        m, M = self.bounds_for_axis(axis_id)
1479        if m is None or M is None or M == m:
1480            return 0
1481        else:
1482            return margin + (value-m)/(M-m) * size
1483       
1484    def inv_transform(self, axis_id, value):
1485        """
1486            Transforms the ``value`` from plot to data coordinates along the axis ``axis_id``.
1487           
1488            This function always ignores zoom. If you need to account for zooming, use :meth:`map_from_graph`.
1489        """
1490        if axis_id in XAxes:
1491            size = self.graph_area.width()
1492            margin = self.graph_area.left()
1493        else:
1494            size = self.graph_area.height()
1495            margin = self.graph_area.top()
1496        m, M = self.bounds_for_axis(axis_id)
1497        if m is not None and M is not None:
1498            return m + (value-margin)/size * (M-m)
1499        else:
1500            return 0
1501       
1502    def bounds_for_axis(self, axis_id, try_auto_scale=True):
1503        if axis_id in self.axes and not self.axes[axis_id].auto_scale:
1504            return self.axes[axis_id].bounds()
1505        if try_auto_scale:
1506            lower, upper = orangeqt.Plot.bounds_for_axis(self, axis_id)
1507            if lower != upper:
1508          lower = lower - (upper-lower)/20.0
1509          upper = upper + (upper-lower)/20.0
1510        return lower, upper
1511        else:
1512            return None, None
1513           
1514    def enableYRaxis(self, enable=1):
1515        self.set_axis_enabled(yRight, enable)
1516       
1517    def enableLRaxis(self, enable=1):
1518        self.set_axis_enabled(yLeft, enable)
1519       
1520    def enableXaxis(self, enable=1):
1521        self.set_axis_enabled(xBottom, enable)
1522       
1523    def set_axis_enabled(self, axis, enable):
1524        if axis not in self.axes:
1525            self.add_axis(axis)
1526        self.axes[axis].setVisible(enable)
1527        self.replot()
1528
1529    @staticmethod
1530    def axis_coordinate(point, axis_id):
1531        if axis_id in XAxes:
1532            return point.x()
1533        elif axis_id in YAxes:
1534            return point.y()
1535        else:
1536            return None
1537           
1538    # ####################################################################
1539    # return string with attribute names and their values for example example
1540    def getExampleTooltipText(self, example, indices = None, maxIndices = 20):
1541        if indices and type(indices[0]) == str:
1542            indices = [self.attributeNameIndex[i] for i in indices]
1543        if not indices: 
1544            indices = range(len(self.dataDomain.attributes))
1545       
1546        # don't show the class value twice
1547        if example.domain.classVar:
1548            classIndex = self.attributeNameIndex[example.domain.classVar.name]
1549            while classIndex in indices:
1550                indices.remove(classIndex)     
1551     
1552        text = "<b>Attributes:</b><br>"
1553        for index in indices[:maxIndices]:
1554            attr = self.attributeNames[index]
1555            if attr not in example.domain:  text += "&nbsp;"*4 + "%s = ?<br>" % (Qt.escape(attr))
1556            elif example[attr].isSpecial(): text += "&nbsp;"*4 + "%s = ?<br>" % (Qt.escape(attr))
1557            else:                           text += "&nbsp;"*4 + "%s = %s<br>" % (Qt.escape(attr), Qt.escape(str(example[attr])))
1558        if len(indices) > maxIndices:
1559            text += "&nbsp;"*4 + " ... <br>"
1560
1561        if example.domain.classVar:
1562            text = text[:-4]
1563            text += "<hr><b>Class:</b><br>"
1564            if example.getclass().isSpecial(): text += "&nbsp;"*4 + "%s = ?<br>" % (Qt.escape(example.domain.classVar.name))
1565            else:                              text += "&nbsp;"*4 + "%s = %s<br>" % (Qt.escape(example.domain.classVar.name), Qt.escape(str(example.getclass())))
1566
1567        if len(example.domain.getmetas()) != 0:
1568            text = text[:-4]
1569            text += "<hr><b>Meta attributes:</b><br>"
1570            # show values of meta attributes
1571            for key in example.domain.getmetas():
1572                try: text += "&nbsp;"*4 + "%s = %s<br>" % (Qt.escape(example.domain[key].name), Qt.escape(str(example[key])))
1573                except: pass
1574        return text[:-4]        # remove the last <br>
1575
1576    # show a tooltip at x,y with text. if the mouse will move for more than 2 pixels it will be removed
1577    def showTip(self, x, y, text):
1578        QToolTip.showText(self.mapToGlobal(QPoint(x, y)), text, self, QRect(x-3,y-3,6,6))
1579       
1580    def notify_legend_moved(self, pos):
1581        self._legend_moved = True
1582        l = self.legend_rect()
1583        g = getattr(self, '_legend_outside_area', QRectF())
1584        p = QPointF()
1585        rect = QRectF()
1586        offset = 20
1587        if pos.x() > g.right() - offset:
1588            self._legend.set_orientation(Qt.Vertical)
1589            rect.setRight(self._legend.boundingRect().width())
1590            p = g.topRight() - self._legend.boundingRect().topRight()
1591        elif pos.x() < g.left() + offset:
1592            self._legend.set_orientation(Qt.Vertical)
1593            rect.setLeft(self._legend.boundingRect().width())
1594            p = g.topLeft()
1595        elif pos.y() < g.top() + offset:
1596            self._legend.set_orientation(Qt.Horizontal)
1597            rect.setTop(self._legend.boundingRect().height())
1598            p = g.topLeft()
1599        elif pos.y() > g.bottom() - offset:
1600            self._legend.set_orientation(Qt.Horizontal)
1601            rect.setBottom(self._legend.boundingRect().height())
1602            p = g.bottomLeft() - self._legend.boundingRect().bottomLeft()
1603           
1604        if p.isNull():
1605            self._legend.set_floating(True, pos)
1606        else:
1607            self._legend.set_floating(False, p)
1608           
1609        if rect != self._legend_margin:
1610            orientation = Qt.Horizontal if rect.top() or rect.bottom() else Qt.Vertical
1611            self._legend.set_orientation(orientation)
1612            self.animate(self, 'legend_margin', rect, duration=100)
1613
1614    def get_legend_margin(self):
1615        return self._legend_margin
1616       
1617    def set_legend_margin(self, value):
1618        self._legend_margin = value
1619        self.update_layout()
1620        self.update_axes()
1621
1622    legend_margin = pyqtProperty(QRectF, get_legend_margin, set_legend_margin)
1623       
1624    def update_curves(self):
1625        if self.main_curve:
1626            self.main_curve.set_alpha_value(self.alpha_value)
1627        else:
1628            for c in self.plot_items():
1629                if isinstance(c, orangeqt.Curve) and not getattr(c, 'ignore_alpha', False):
1630                    au = c.auto_update()
1631                    c.set_auto_update(False)
1632                    c.set_point_size(self.point_width)
1633                    color = c.color()
1634                    color.setAlpha(self.alpha_value)
1635                    c.set_color(color)
1636                    c.set_auto_update(au)
1637                    c.update_properties()
1638        self.viewport().update()
1639   
1640    update_point_size = update_curves
1641    update_alpha_value = update_curves
1642           
1643    def update_antialiasing(self, use_antialiasing=None):
1644        if use_antialiasing is not None:
1645            self.antialias_plot = use_antialiasing
1646           
1647        self.setRenderHint(QPainter.Antialiasing, self.antialias_plot)
1648       
1649    def update_animations(self, use_animations=None):
1650        if use_animations is not None:
1651            self.animate_plot = use_animations
1652            self.animate_points = use_animations
1653           
1654    def update_performance(self, num_points = None):
1655        if self.auto_adjust_performance:
1656            if not num_points:
1657                if self.main_curve:
1658                    num_points = len(self.main_curve.points())
1659                else:
1660                    num_points = sum( len(c.points()) for c in self.curves )
1661            if num_points > self.disable_animations_threshold:
1662                self.disabled_animate_points = self.animate_points
1663                self.animate_points = False
1664               
1665                self.disabled_animate_plot = self.animate_plot
1666                self.animate_plot = False
1667               
1668                self.disabled_antialias_lines = self.animate_points
1669                self.antialias_lines = False
1670           
1671            elif hasattr(self, 'disabled_animate_points'):
1672                self.animate_points = self.disabled_animate_points
1673                del self.disabled_animate_points
1674               
1675                self.animate_plot = self.disabled_animate_plot
1676                del self.disabled_animate_plot
1677               
1678                self.antialias_lines = self.disabled_antialias_lines
1679                del self.disabled_antialias_lines
1680       
1681    def animate(self, target, prop_name, end_val, duration = None, start_val = None):
1682        for a in self._animations:
1683            if a.state() == QPropertyAnimation.Stopped:
1684                self._animations.remove(a)
1685        if self.animate_plot:
1686            a = QPropertyAnimation(target, prop_name)
1687            a.setEndValue(end_val)
1688            if start_val is not None:
1689                a.setStartValue(start_val)
1690            if duration:
1691                a.setDuration(duration)
1692            self._animations.append(a)
1693            a.start(QPropertyAnimation.KeepWhenStopped)
1694        else:
1695            target.setProperty(prop_name, end_val)
1696           
1697    def clear_selection(self):
1698        self.unselect_all_points()
1699   
1700    def send_selection(self):
1701        if self.auto_send_selection_callback:
1702            self.auto_send_selection_callback()
1703           
1704    def pan(self, delta):
1705        if type(delta) == tuple:
1706            x, y = delta
1707        else:
1708            x, y = delta.x(), delta.y()
1709        t = self.zoom_transform()
1710        x = x / t.m11()
1711        y = y / t.m22()
1712        r = QRectF(self.zoom_rect)
1713        r.translate(-QPointF(x,y))
1714        self.ensure_inside(r, self.graph_area)
1715        self.zoom_rect = r
1716
1717    def zoom_to_rect(self, rect):
1718        self.ensure_inside(rect, self.graph_area)
1719        self.zoom_stack.append(self.zoom_rect)
1720        self.animate(self, 'zoom_rect', rect, start_val = self.get_zoom_rect())
1721       
1722    def zoom_back(self):
1723        if self.zoom_stack:
1724            rect = self.zoom_stack.pop()
1725            self.animate(self, 'zoom_rect', rect, start_val = self.get_zoom_rect())
1726
1727    def reset_zoom(self):
1728        self._zoom_rect = None
1729        self.update_zoom()
1730       
1731    def zoom_transform(self):
1732        return self.transform_from_rects(self.zoom_rect, self.graph_area)
1733       
1734    def zoom_in(self, point):
1735        self.zoom(point, scale = 2)
1736       
1737    def zoom_out(self, point):
1738        self.zoom(point, scale = 0.5)
1739       
1740    def zoom(self, point, scale):
1741        t, ok = self._zoom_transform.inverted()
1742        point = point * t
1743        r = QRectF(self.zoom_rect)
1744        i = 1.0/scale
1745        r.setTopLeft(point*(1-i) + r.topLeft()*i)
1746        r.setBottomRight(point*(1-i) + r.bottomRight()*i)
1747       
1748        self.ensure_inside(r, self.graph_area)
1749        self.zoom_to_rect(r)
1750       
1751    def get_zoom_rect(self):
1752        if self._zoom_rect:
1753            return self._zoom_rect
1754        else:
1755            return self.graph_area
1756       
1757    def set_zoom_rect(self, rect):
1758        self._zoom_rect = rect
1759        self._zoom_transform = self.transform_from_rects(rect, self.graph_area)
1760        self.update_zoom()
1761
1762    zoom_rect = pyqtProperty(QRectF, get_zoom_rect, set_zoom_rect)
1763       
1764    @staticmethod
1765    def ensure_inside(small_rect, big_rect):
1766        if small_rect.width() > big_rect.width():
1767            small_rect.setWidth(big_rect.width())
1768        if small_rect.height() > big_rect.height():
1769            small_rect.setHeight(big_rect.height())
1770       
1771        if small_rect.right() > big_rect.right():
1772            small_rect.moveRight(big_rect.right())
1773        elif small_rect.left() < big_rect.left():
1774            small_rect.moveLeft(big_rect.left())
1775           
1776        if small_rect.bottom() > big_rect.bottom():
1777            small_rect.moveBottom(big_rect.bottom())
1778        elif small_rect.top() < big_rect.top():
1779            small_rect.moveTop(big_rect.top())
1780           
1781    def shuffle_points(self):
1782        if self.main_curve:
1783            self.main_curve.shuffle_points()
1784           
1785    def set_progress(self, done, total):
1786        if not self.widget:
1787            return
1788           
1789        if done == total:
1790            self.widget.progressBarFinished()
1791        else:
1792            self.widget.progressBarSet(100.0 * done / total)
1793       
1794    def start_progress(self):
1795        if self.widget:
1796            self.widget.progressBarInit()
1797           
1798    def end_progress(self):
1799        if self.widget:
1800            self.widget.progressBarFinished()
1801           
1802    def is_axis_auto_scale(self, axis_id):
1803        if axis_id not in self.axes:
1804            return axis_id not in self.data_range
1805        return self.axes[axis_id].auto_scale
1806
1807    def axis_line(self, rect, id, invert_y = False):
1808        if invert_y:
1809            r = QRectF(rect)
1810            r.setTop(rect.bottom())
1811            r.setBottom(rect.top())
1812            rect = r
1813        if id == xBottom:
1814            line = QLineF(rect.topLeft(), rect.topRight())
1815        elif id == xTop:
1816            line = QLineF(rect.bottomLeft(), rect.bottomRight())
1817        elif id == yLeft:
1818            line = QLineF(rect.topLeft(), rect.bottomLeft())
1819        elif id == yRight:
1820            line = QLineF(rect.topRight(), rect.bottomRight())
1821        else:
1822            line = None
1823        return line
1824       
1825    def color(self, role, group = None):
1826        if group:
1827            return self.palette().color(group, role)
1828        else:
1829            return self.palette().color(role)
1830           
1831    def set_palette(self, p):
1832        '''
1833            Sets the plot palette to ``p``.
1834           
1835            :param p: The new color palette
1836            :type p: :obj:`.QPalette`
1837        '''
1838        self.setPalette(p)
1839        self.replot()
1840       
1841    def update_theme(self):
1842        '''
1843            Updates the current color theme, depending on the value of :attr:`theme_name`.
1844        '''
1845        if self.theme_name.lower() == 'default':
1846            self.set_palette(OWPalette.System)
1847        elif self.theme_name.lower() == 'light':
1848            self.set_palette(OWPalette.Light)
1849        elif self.theme_name.lower() == 'dark':
1850            self.set_palette(OWPalette.Dark)
Note: See TracBrowser for help on using the repository browser.