source: orange/Orange/OrangeWidgets/plot/owplot.py @ 10990:4dcdb2869545

Revision 10990:4dcdb2869545, 70.3 KB checked in by markotoplak, 19 months ago (diff)

Hide arrows by default in qt plot objects.

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
38from Orange import 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 = 0, 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
996            if self._zoom_rect:
997                data_zoom_rect = self.map_transform.inverted()[0].mapRect(self._zoom_rect)
998                self.map_transform = self.transform_for_axes()
999                self.set_zoom_rect(self.map_transform.mapRect(data_zoom_rect))
1000
1001        self.map_transform = self.transform_for_axes()
1002       
1003        for c in self.plot_items():
1004            x,y = c.axes()
1005            c.set_graph_transform(self.transform_for_axes(x,y))
1006            c.update_properties()
1007           
1008    def update_zoom(self):
1009        '''
1010            Updates the zoom transformation of the plot items.
1011        '''
1012        zt = self.zoom_transform()
1013        self._zoom_transform = zt
1014        self.set_zoom_transform(zt)
1015       
1016        self.update_axes(zoom_only=True)
1017        self.viewport().update()
1018       
1019    def update_axes(self, zoom_only=False):
1020        """
1021            Updates the axes.
1022           
1023            If ``zoom_only`` is ``True``, only the positions of the axes and their labels are recalculated.
1024            Otherwise, all their labels are updated.
1025        """
1026        if self.warn_unused_attributes and not zoom_only:
1027            self._legend.remove_category(UNUSED_ATTRIBUTES_STR)
1028           
1029        for id, item in self.axes.iteritems():
1030            if item.scale is None and item.labels is None:
1031                item.auto_range = self.bounds_for_axis(id)
1032           
1033            if id in XAxes:
1034                (x,y) = (id, yLeft)
1035            elif id in YAxes:
1036                (x,y) = (xBottom, id)
1037            else:
1038                (x,y) = (xBottom, yLeft)
1039               
1040            if id in CartesianAxes:
1041                ## This class only sets the lines for these four axes, widgets are responsible for the rest
1042                if x in self.axes and y in self.axes:
1043                    item.data_line = self.axis_line(self.data_rect_for_axes(x,y), id)
1044            if id in CartesianAxes:
1045                item.graph_line = self.axis_line(self.graph_area, id, invert_y = True)
1046            elif item.data_line:
1047                t = self.transform_for_axes(x, y)
1048                item.graph_line = t.map(item.data_line)
1049           
1050            if item.graph_line and item.zoomable:
1051                item.graph_line = self._zoom_transform.map(item.graph_line)
1052               
1053            if not zoom_only:
1054                if item.graph_line:
1055                    item.show()
1056                else:
1057                    item.hide()
1058                    if self.warn_unused_attributes:
1059                        self._legend.add_item(UNUSED_ATTRIBUTES_STR, item.title, None)
1060            item.zoom_transform = self._zoom_transform
1061            item.update(zoom_only)
1062       
1063    def replot(self):
1064        '''
1065            Replot the entire graph.
1066           
1067            This functions redraws everything on the graph, so it can be very slow
1068        '''
1069        #self.setBackgroundBrush(self.color(OWPalette.Canvas))
1070        self._bounds_cache = {}
1071        self._transform_cache = {}
1072        self.set_clean()
1073        self.update_antialiasing()
1074        self.update_legend()
1075        self.update_layout()
1076        self.update_zoom()
1077        self.update_axes()
1078        self.update_grid()
1079        self.update_filled_symbols()
1080        self.setSceneRect(QRectF(self.contentsRect()))
1081        self.viewport().update()
1082       
1083    def update_legend(self):
1084        if self.show_legend and not self._legend_moved:
1085            ## If the legend hasn't been moved it, we set it outside, in the top right corner
1086            m = self.graph_margin
1087            r = QRectF(self.contentsRect())
1088            r.adjust(m, m, -m, -m)
1089            self._legend.max_size = r.size()
1090            self._legend.update_items()
1091            w = self._legend.boundingRect().width()
1092            self._legend_margin = QRectF(0, 0, w, 0)
1093            self._legend.set_floating(False)
1094            self._legend.set_orientation(Qt.Vertical)
1095            self._legend.setPos(QRectF(self.contentsRect()).topRight() + QPointF(-w, 0))
1096           
1097       
1098        if (self._legend.isVisible() == self.show_legend):
1099            return
1100           
1101        self._legend.setVisible(self.show_legend)
1102        if self.show_legend:
1103            if self.old_legend_margin is not None:
1104                self.animate(self, 'legend_margin', self.old_legend_margin, duration = 100)
1105            else:
1106                r = self.legend_rect()
1107                self.ensure_inside(r, self.contentsRect())
1108                self._legend.setPos(r.topLeft())
1109                self.notify_legend_moved(r.topLeft())
1110        else:
1111            self.old_legend_margin = self.legend_margin
1112            self.animate(self, 'legend_margin', QRectF(), duration=100)
1113       
1114    def update_filled_symbols(self):
1115        ## TODO: Implement this in Curve.cpp
1116        pass
1117   
1118    def update_grid(self):
1119        self.grid_curve.set_x_enabled(self.show_grid)
1120        self.grid_curve.set_y_enabled(self.show_grid)
1121        self.grid_curve.update_properties()
1122       
1123    def legend(self):
1124        '''
1125            Returns the plot's legend, which is a :obj:`OrangeWidgets.plot.OWLegend`
1126        '''
1127        return self._legend
1128       
1129    def legend_rect(self):
1130        if self.show_legend:
1131            return self._legend.mapRectToScene(self._legend.boundingRect())
1132        else:
1133            return QRectF()
1134       
1135    def isLegendEvent(self, event, function):
1136        if self.show_legend and self.legend_rect().contains(self.mapToScene(event.pos())):
1137            function(self, event)
1138            return True
1139        else:
1140            return False
1141   
1142    def mouse_action(self, event):
1143        b = event.buttons() | event.button()
1144        m = event.modifiers()
1145        if b == Qt.LeftButton | Qt.RightButton:
1146            b = Qt.MidButton
1147        if m & Qt.AltModifier and b == Qt.LeftButton:
1148            m = m & ~Qt.AltModifier
1149            b = Qt.MidButton
1150       
1151        if b == Qt.LeftButton and not m:
1152            return self.state
1153       
1154        if b == Qt.RightButton and not m and self.state == SELECT:
1155            return SELECT_RIGHTCLICK
1156           
1157        if b == Qt.MidButton:
1158            return PANNING
1159           
1160        if b in [Qt.LeftButton, Qt.RightButton] and (self.state == ZOOMING or m == Qt.ControlModifier):
1161            return ZOOMING
1162           
1163        if b == Qt.LeftButton and m == Qt.ShiftModifier:
1164            return SELECT
1165   
1166    ## Event handling
1167   
1168    def event(self, event):
1169        if event.type() == QEvent.Gesture:
1170            return self.gestureEvent(event)
1171        else:
1172            return orangeqt.Plot.event(self, event)
1173           
1174    def gestureEvent(self, event):
1175        for gesture in event.gestures():
1176            if gesture.state() == Qt.GestureStarted:
1177                self.current_gesture_scale = 1.
1178                event.accept(gesture)
1179                continue
1180            elif gesture.gestureType() == Qt.PinchGesture:
1181                old_animate_plot = self.animate_plot
1182                self.animate_plot = False
1183                self.zoom(gesture.centerPoint(), gesture.scaleFactor()/self.current_gesture_scale )
1184                self.current_gesture_scale = gesture.scaleFactor()
1185                self.animate_plot = old_animate_plot
1186            elif gesture.gestureType() == Qt.PanGesture:
1187                self.pan(gesture.delta())
1188        return True
1189   
1190    def resizeEvent(self, event):
1191        self.replot()
1192        s = event.size() - event.oldSize()
1193        if self.legend_margin.right() > 0:
1194            self._legend.setPos(self._legend.pos() + QPointF(s.width(), 0))
1195        if self.legend_margin.bottom() > 0:
1196            self._legend.setPos(self._legend.pos() + QPointF(0, s.height()))
1197       
1198    def showEvent(self, event):
1199        self.replot()
1200
1201    def mousePressEvent(self, event):
1202        self.static_click = True
1203        self._pressed_mouse_button = event.button()
1204        self._pressed_mouse_pos = event.pos()
1205
1206        if self.mousePressEventHandler and self.mousePressEventHandler(event):
1207            event.accept()
1208            return
1209           
1210        if self.isLegendEvent(event, QGraphicsView.mousePressEvent):
1211            return
1212       
1213        point = self.mapToScene(event.pos())
1214        a = self.mouse_action(event)
1215
1216        if a == SELECT and hasattr(self, 'move_selected_points'):
1217            self._pressed_point = self.nearest_point(point)
1218            self._pressed_point_coor = None 
1219            if self._pressed_point is not None:
1220                self._pressed_point_coor = self._pressed_point.coordinates()
1221           
1222        if a == PANNING:
1223            self._last_pan_pos = point
1224            event.accept()
1225        else:
1226            orangeqt.Plot.mousePressEvent(self, event)
1227           
1228    def mouseMoveEvent(self, event):
1229        if event.buttons() and (self._pressed_mouse_pos - event.pos()).manhattanLength() > qApp.startDragDistance():
1230            self.static_click = False
1231       
1232        if self.mouseMoveEventHandler and self.mouseMoveEventHandler(event):
1233            event.accept()
1234            return
1235           
1236        if self.isLegendEvent(event, QGraphicsView.mouseMoveEvent):
1237            return
1238       
1239        point = self.mapToScene(event.pos())
1240        if not self._pressed_mouse_button:
1241            if self.receivers(SIGNAL('point_hovered(Point*)')) > 0:
1242                self.emit(SIGNAL('point_hovered(Point*)'), self.nearest_point(point))
1243       
1244        ## We implement a workaround here, because sometimes mouseMoveEvents are not fast enough
1245        ## so the moving legend gets left behind while dragging, and it's left in a pressed state
1246        if self._legend.mouse_down:
1247            QGraphicsView.mouseMoveEvent(self, event)
1248            return
1249           
1250        a = self.mouse_action(event)
1251       
1252        if a == SELECT and self._pressed_point is not None and self._pressed_point.is_selected() and hasattr(self, 'move_selected_points'):
1253            animate_points = self.animate_points
1254            self.animate_points = False
1255            x1, y1 = self._pressed_point_coor
1256            x2, y2 = self.map_from_graph(point, zoom=True)
1257            self.move_selected_points((x2 - x1, y2 - y1))
1258            self.replot()
1259            if self._pressed_point is not None:
1260                self._pressed_point_coor = self._pressed_point.coordinates()
1261               
1262            self.animate_points = animate_points
1263           
1264        elif a in [SELECT, ZOOMING] and self.graph_area.contains(point):
1265            if not self._current_rs_item:
1266                self._selection_start_point = self.mapToScene(self._pressed_mouse_pos)
1267                self._current_rs_item = QGraphicsRectItem(scene=self.scene())
1268                self._current_rs_item.setPen(SelectionPen)
1269                self._current_rs_item.setBrush(SelectionBrush)
1270                self._current_rs_item.setZValue(SelectionZValue)
1271            self._current_rs_item.setRect(QRectF(self._selection_start_point, point).normalized())
1272        elif a == PANNING:
1273            if not self._last_pan_pos:
1274                self._last_pan_pos = self.mapToScene(self._pressed_mouse_pos)
1275            self.pan(point - self._last_pan_pos)
1276            self._last_pan_pos = point
1277        else:
1278            x, y = self.map_from_graph(point, zoom=True)
1279            text, x, y = self.tips.maybeTip(x, y)
1280            if type(text) == int: 
1281                text = self.buildTooltip(text)
1282            if text and x is not None and y is not None:
1283                tp = self.mapFromScene(QPointF(x,y) * self.map_transform * self._zoom_transform)
1284                self.showTip(tp.x(), tp.y(), text)
1285            else:
1286                orangeqt.Plot.mouseMoveEvent(self, event)
1287       
1288    def mouseReleaseEvent(self, event):
1289        self._pressed_mouse_button = Qt.NoButton
1290
1291        if self.mouseReleaseEventHandler and self.mouseReleaseEventHandler(event):
1292            event.accept()
1293            return
1294        if self.static_click and self.mouseStaticClickHandler and self.mouseStaticClickHandler(event):
1295            event.accept()
1296            return
1297       
1298        if self.isLegendEvent(event, QGraphicsView.mouseReleaseEvent):
1299            return
1300       
1301        a = self.mouse_action(event)
1302        if a == SELECT and self._pressed_point is not None:
1303            self._pressed_point = None
1304        if a in [ZOOMING, SELECT] and self._current_rs_item:
1305            rect = self._current_rs_item.rect()
1306            if a == ZOOMING:
1307                self.zoom_to_rect(self._zoom_transform.inverted()[0].mapRect(rect))
1308            else:
1309                self.add_selection(rect)
1310            self.scene().removeItem(self._current_rs_item)
1311            self._current_rs_item = None
1312            return
1313        orangeqt.Plot.mouseReleaseEvent(self, event)
1314   
1315    def mouseStaticClick(self, event):
1316        point = self.mapToScene(event.pos())
1317        if point not in self.graph_area:
1318            return False
1319           
1320        a = self.mouse_action(event)
1321        b = event.buttons() | event.button()
1322       
1323        if a == ZOOMING:
1324            if event.button() == Qt.LeftButton:
1325                self.zoom_in(point)
1326            elif event.button() == Qt.RightButton:
1327                self.zoom_back()
1328            else:
1329                return False
1330            return True
1331        elif a == SELECT and b == Qt.LeftButton:
1332            point_item = self.nearest_point(point)
1333            b = self.selection_behavior
1334           
1335            if b == self.ReplaceSelection:
1336                self.unselect_all_points()
1337                b = self.AddSelection
1338           
1339            if point_item:
1340                point_item.set_selected(b == self.AddSelection or (b == self.ToggleSelection and not point_item.is_selected()))
1341            self.emit(SIGNAL('selection_changed()'))
1342        elif a == SELECT and b == Qt.RightButton:
1343            point_item = self.nearest_point(point)
1344            if point_item:
1345                self.emit(SIGNAL('point_rightclicked(Point*)'), self.nearest_point(point))
1346            else:
1347                self.unselect_all_points()
1348        else:
1349            return False
1350           
1351    def wheelEvent(self, event):
1352        point = self.mapToScene(event.pos())
1353        d = event.delta() / 120.0
1354        self.zoom(point, pow(2,d))
1355           
1356    @staticmethod
1357    def transform_from_rects(r1, r2):
1358        """
1359            Returns a QTransform that maps from rectangle ``r1`` to ``r2``.
1360        """
1361        if r1 is None or r2 is None:
1362            return QTransform()
1363        if r1.width() == 0 or r1.height() == 0 or r2.width() == 0 or r2.height() == 0:
1364            return QTransform()
1365        tr1 = QTransform().translate(-r1.left(), -r1.top())
1366        ts = QTransform().scale(r2.width()/r1.width(), r2.height()/r1.height())
1367        tr2 = QTransform().translate(r2.left(), r2.top())
1368        return tr1 * ts * tr2
1369       
1370    def transform_for_zoom(self, factor, point, rect):
1371        if factor == 1:
1372            return QTransform()
1373           
1374        dp = point
1375       
1376        t = QTransform()
1377        t.translate(dp.x(), dp.y())
1378        t.scale(factor, factor)
1379        t.translate(-dp.x(), -dp.y())
1380        return t
1381       
1382    def rect_for_zoom(self, point, old_rect, scale = 2):
1383        r = QRectF()
1384        r.setWidth(old_rect.width() / scale)
1385        r.setHeight(old_rect.height() / scale)
1386        r.moveCenter(point)
1387       
1388        self.ensure_inside(r, self.graph_area)
1389       
1390        return r
1391       
1392    def set_state(self, state):
1393        self.state = state
1394        if state != SELECT_RECTANGLE:
1395            self._current_rs_item = None
1396        if state != SELECT_POLYGON:
1397            self._current_ps_item = None
1398       
1399    def get_selected_points(self, xData, yData, validData):
1400        if self.main_curve:
1401            selected = []
1402            points = self.main_curve.points()
1403            i = 0
1404            for d in validData:
1405                if d:
1406                    selected.append(points[i].is_selected())
1407                    i += 1
1408                else:
1409                    selected.append(False)
1410        else:
1411            selected = self.selected_points(xData, yData)
1412        unselected = [not i for i in selected]
1413        return selected, unselected
1414       
1415    def add_selection(self, reg):
1416        """
1417            Selects all points in the region ``reg`` using the current :attr: `selection_behavior`.
1418        """
1419        self.select_points(reg, self.selection_behavior)
1420        self.viewport().update()
1421        if self.auto_send_selection_callback:
1422            self.auto_send_selection_callback()
1423       
1424    def points_equal(self, p1, p2):
1425        if type(p1) == tuple:
1426            (x, y) = p1
1427            p1 = QPointF(x, y)
1428        if type(p2) == tuple:
1429            (x, y) = p2
1430            p2 = QPointF(x, y)
1431        return (QPointF(p1)-QPointF(p2)).manhattanLength() < self.polygon_close_treshold
1432       
1433    def data_rect_for_axes(self, x_axis = xBottom, y_axis = yLeft):
1434        """
1435            Calculates the bounding rectangle in data coordinates for the axes ``x_axis`` and ``y_axis``.
1436        """
1437        if x_axis in self.axes and y_axis in self.axes:
1438            x_min, x_max = self.bounds_for_axis(x_axis, try_auto_scale=True)
1439            y_min, y_max = self.bounds_for_axis(y_axis, try_auto_scale=True)
1440            if (x_min or x_max) and (y_min or y_max):
1441                r = QRectF(x_min, y_min, x_max-x_min, y_max-y_min)
1442                return r
1443        r = orangeqt.Plot.data_rect_for_axes(self, x_axis, y_axis)
1444        for id, axis in self.axes.iteritems():
1445            if id not in CartesianAxes and axis.data_line:
1446                r |= QRectF(axis.data_line.p1(), axis.data_line.p2())
1447        ## We leave a 5% margin on each side so the graph doesn't look overcrowded
1448        ## TODO: Perhaps change this from a fixed percentage to always round to a round number
1449        dx = r.width() / 20.0
1450        dy = r.height() / 20.0
1451        r.adjust(-dx, -dy, dx, dy)
1452        return r
1453       
1454    def transform_for_axes(self, x_axis = xBottom, y_axis = yLeft):
1455        """
1456            Returns the graph transform that maps from data to scene coordinates using axes ``x_axis`` and ``y_axis``.
1457        """
1458        if not (x_axis, y_axis) in self._transform_cache:
1459            # We must flip the graph area, becase Qt coordinates start from top left, while graph coordinates start from bottom left
1460            a = QRectF(self.graph_area)
1461            t = a.top()
1462            a.setTop(a.bottom())
1463            a.setBottom(t)
1464            self._transform_cache[(x_axis, y_axis)] = self.transform_from_rects(self.data_rect_for_axes(x_axis, y_axis), a)
1465        return self._transform_cache[(x_axis, y_axis)]
1466       
1467    def transform(self, axis_id, value):
1468        """
1469            Transforms the ``value`` from data to plot coordinates along the axis ``axis_id``.
1470           
1471            This function always ignores zoom. If you need to account for zooming, use :meth:`map_to_graph`.
1472        """
1473        if axis_id in XAxes:
1474            size = self.graph_area.width()
1475            margin = self.graph_area.left()
1476        else:
1477            size = self.graph_area.height()
1478            margin = self.graph_area.top()
1479        m, M = self.bounds_for_axis(axis_id)
1480        if m is None or M is None or M == m:
1481            return 0
1482        else:
1483            return margin + (value-m)/(M-m) * size
1484       
1485    def inv_transform(self, axis_id, value):
1486        """
1487            Transforms the ``value`` from plot to data coordinates along the axis ``axis_id``.
1488           
1489            This function always ignores zoom. If you need to account for zooming, use :meth:`map_from_graph`.
1490        """
1491        if axis_id in XAxes:
1492            size = self.graph_area.width()
1493            margin = self.graph_area.left()
1494        else:
1495            size = self.graph_area.height()
1496            margin = self.graph_area.top()
1497        m, M = self.bounds_for_axis(axis_id)
1498        if m is not None and M is not None:
1499            return m + (value-margin)/size * (M-m)
1500        else:
1501            return 0
1502       
1503    def bounds_for_axis(self, axis_id, try_auto_scale=True):
1504        if axis_id in self.axes and not self.axes[axis_id].auto_scale:
1505            return self.axes[axis_id].bounds()
1506        if try_auto_scale:
1507            lower, upper = orangeqt.Plot.bounds_for_axis(self, axis_id)
1508            if lower != upper:
1509                lower = lower - (upper-lower)/20.0
1510                upper = upper + (upper-lower)/20.0
1511            return lower, upper
1512        else:
1513            return None, None
1514           
1515    def enableYRaxis(self, enable=1):
1516        self.set_axis_enabled(yRight, enable)
1517       
1518    def enableLRaxis(self, enable=1):
1519        self.set_axis_enabled(yLeft, enable)
1520       
1521    def enableXaxis(self, enable=1):
1522        self.set_axis_enabled(xBottom, enable)
1523       
1524    def set_axis_enabled(self, axis, enable):
1525        if axis not in self.axes:
1526            self.add_axis(axis)
1527        self.axes[axis].setVisible(enable)
1528        self.replot()
1529
1530    @staticmethod
1531    def axis_coordinate(point, axis_id):
1532        if axis_id in XAxes:
1533            return point.x()
1534        elif axis_id in YAxes:
1535            return point.y()
1536        else:
1537            return None
1538           
1539    # ####################################################################
1540    # return string with attribute names and their values for example example
1541    def getExampleTooltipText(self, example, indices = None, maxIndices = 20):
1542        if indices and type(indices[0]) == str:
1543            indices = [self.attributeNameIndex[i] for i in indices]
1544        if not indices: 
1545            indices = range(len(self.dataDomain.attributes))
1546       
1547        # don't show the class value twice
1548        if example.domain.classVar:
1549            classIndex = self.attributeNameIndex[example.domain.classVar.name]
1550            while classIndex in indices:
1551                indices.remove(classIndex)     
1552     
1553        text = "<b>Attributes:</b><br>"
1554        for index in indices[:maxIndices]:
1555            attr = self.attributeNames[index]
1556            if attr not in example.domain:  text += "&nbsp;"*4 + "%s = ?<br>" % (Qt.escape(attr))
1557            elif example[attr].isSpecial(): text += "&nbsp;"*4 + "%s = ?<br>" % (Qt.escape(attr))
1558            else:                           text += "&nbsp;"*4 + "%s = %s<br>" % (Qt.escape(attr), Qt.escape(str(example[attr])))
1559        if len(indices) > maxIndices:
1560            text += "&nbsp;"*4 + " ... <br>"
1561
1562        if example.domain.classVar:
1563            text = text[:-4]
1564            text += "<hr><b>Class:</b><br>"
1565            if example.getclass().isSpecial(): text += "&nbsp;"*4 + "%s = ?<br>" % (Qt.escape(example.domain.classVar.name))
1566            else:                              text += "&nbsp;"*4 + "%s = %s<br>" % (Qt.escape(example.domain.classVar.name), Qt.escape(str(example.getclass())))
1567
1568        if len(example.domain.getmetas()) != 0:
1569            text = text[:-4]
1570            text += "<hr><b>Meta attributes:</b><br>"
1571            # show values of meta attributes
1572            for key in example.domain.getmetas():
1573                try: text += "&nbsp;"*4 + "%s = %s<br>" % (Qt.escape(example.domain[key].name), Qt.escape(str(example[key])))
1574                except: pass
1575        return text[:-4]        # remove the last <br>
1576
1577    # show a tooltip at x,y with text. if the mouse will move for more than 2 pixels it will be removed
1578    def showTip(self, x, y, text):
1579        QToolTip.showText(self.mapToGlobal(QPoint(x, y)), text, self, QRect(x-3,y-3,6,6))
1580       
1581    def notify_legend_moved(self, pos):
1582        self._legend_moved = True
1583        l = self.legend_rect()
1584        g = getattr(self, '_legend_outside_area', QRectF())
1585        p = QPointF()
1586        rect = QRectF()
1587        offset = 20
1588        if pos.x() > g.right() - offset:
1589            self._legend.set_orientation(Qt.Vertical)
1590            rect.setRight(self._legend.boundingRect().width())
1591            p = g.topRight() - self._legend.boundingRect().topRight()
1592        elif pos.x() < g.left() + offset:
1593            self._legend.set_orientation(Qt.Vertical)
1594            rect.setLeft(self._legend.boundingRect().width())
1595            p = g.topLeft()
1596        elif pos.y() < g.top() + offset:
1597            self._legend.set_orientation(Qt.Horizontal)
1598            rect.setTop(self._legend.boundingRect().height())
1599            p = g.topLeft()
1600        elif pos.y() > g.bottom() - offset:
1601            self._legend.set_orientation(Qt.Horizontal)
1602            rect.setBottom(self._legend.boundingRect().height())
1603            p = g.bottomLeft() - self._legend.boundingRect().bottomLeft()
1604           
1605        if p.isNull():
1606            self._legend.set_floating(True, pos)
1607        else:
1608            self._legend.set_floating(False, p)
1609           
1610        if rect != self._legend_margin:
1611            orientation = Qt.Horizontal if rect.top() or rect.bottom() else Qt.Vertical
1612            self._legend.set_orientation(orientation)
1613            self.animate(self, 'legend_margin', rect, duration=100)
1614
1615    def get_legend_margin(self):
1616        return self._legend_margin
1617       
1618    def set_legend_margin(self, value):
1619        self._legend_margin = value
1620        self.update_layout()
1621        self.update_axes()
1622
1623    legend_margin = pyqtProperty(QRectF, get_legend_margin, set_legend_margin)
1624       
1625    def update_curves(self):
1626        if self.main_curve:
1627            self.main_curve.set_alpha_value(self.alpha_value)
1628        else:
1629            for c in self.plot_items():
1630                if isinstance(c, orangeqt.Curve) and not getattr(c, 'ignore_alpha', False):
1631                    au = c.auto_update()
1632                    c.set_auto_update(False)
1633                    c.set_point_size(self.point_width)
1634                    color = c.color()
1635                    color.setAlpha(self.alpha_value)
1636                    c.set_color(color)
1637                    c.set_auto_update(au)
1638                    c.update_properties()
1639        self.viewport().update()
1640   
1641    update_point_size = update_curves
1642    update_alpha_value = update_curves
1643           
1644    def update_antialiasing(self, use_antialiasing=None):
1645        if use_antialiasing is not None:
1646            self.antialias_plot = use_antialiasing
1647           
1648        self.setRenderHint(QPainter.Antialiasing, self.antialias_plot)
1649       
1650    def update_animations(self, use_animations=None):
1651        if use_animations is not None:
1652            self.animate_plot = use_animations
1653            self.animate_points = use_animations
1654           
1655    def update_performance(self, num_points = None):
1656        if self.auto_adjust_performance:
1657            if not num_points:
1658                if self.main_curve:
1659                    num_points = len(self.main_curve.points())
1660                else:
1661                    num_points = sum( len(c.points()) for c in self.curves )
1662            if num_points > self.disable_animations_threshold:
1663                self.disabled_animate_points = self.animate_points
1664                self.animate_points = False
1665               
1666                self.disabled_animate_plot = self.animate_plot
1667                self.animate_plot = False
1668               
1669                self.disabled_antialias_lines = self.animate_points
1670                self.antialias_lines = False
1671           
1672            elif hasattr(self, 'disabled_animate_points'):
1673                self.animate_points = self.disabled_animate_points
1674                del self.disabled_animate_points
1675               
1676                self.animate_plot = self.disabled_animate_plot
1677                del self.disabled_animate_plot
1678               
1679                self.antialias_lines = self.disabled_antialias_lines
1680                del self.disabled_antialias_lines
1681       
1682    def animate(self, target, prop_name, end_val, duration = None, start_val = None):
1683        for a in self._animations:
1684            if a.state() == QPropertyAnimation.Stopped:
1685                self._animations.remove(a)
1686        if self.animate_plot:
1687            a = QPropertyAnimation(target, prop_name)
1688            a.setEndValue(end_val)
1689            if start_val is not None:
1690                a.setStartValue(start_val)
1691            if duration:
1692                a.setDuration(duration)
1693            self._animations.append(a)
1694            a.start(QPropertyAnimation.KeepWhenStopped)
1695        else:
1696            target.setProperty(prop_name, end_val)
1697           
1698    def clear_selection(self):
1699        self.unselect_all_points()
1700   
1701    def send_selection(self):
1702        if self.auto_send_selection_callback:
1703            self.auto_send_selection_callback()
1704           
1705    def pan(self, delta):
1706        if type(delta) == tuple:
1707            x, y = delta
1708        else:
1709            x, y = delta.x(), delta.y()
1710        t = self.zoom_transform()
1711        x = x / t.m11()
1712        y = y / t.m22()
1713        r = QRectF(self.zoom_rect)
1714        r.translate(-QPointF(x,y))
1715        self.ensure_inside(r, self.graph_area)
1716        self.zoom_rect = r
1717
1718    def zoom_to_rect(self, rect):
1719        print len(self.zoom_stack)
1720        self.ensure_inside(rect, self.graph_area)
1721
1722        # add to zoom_stack if zoom_rect is larger
1723        if self.zoom_rect.width() > rect.width() or self.zoom_rect.height() > rect.height():
1724            self.zoom_stack.append(self.zoom_rect)
1725
1726        self.animate(self, 'zoom_rect', rect, start_val = self.get_zoom_rect())
1727       
1728    def zoom_back(self):
1729        print len(self.zoom_stack)
1730        if self.zoom_stack:
1731            rect = self.zoom_stack.pop()
1732            self.animate(self, 'zoom_rect', rect, start_val = self.get_zoom_rect())
1733
1734    def reset_zoom(self):
1735        self._zoom_rect = None
1736        self.update_zoom()
1737       
1738    def zoom_transform(self):
1739        return self.transform_from_rects(self.zoom_rect, self.graph_area)
1740       
1741    def zoom_in(self, point):
1742        self.zoom(point, scale = 2)
1743       
1744    def zoom_out(self, point):
1745        self.zoom(point, scale = 0.5)
1746       
1747    def zoom(self, point, scale):
1748        print len(self.zoom_stack)
1749        t, ok = self._zoom_transform.inverted()
1750        point = point * t
1751        r = QRectF(self.zoom_rect)
1752        i = 1.0/scale
1753        r.setTopLeft(point*(1-i) + r.topLeft()*i)
1754        r.setBottomRight(point*(1-i) + r.bottomRight()*i)
1755       
1756        self.ensure_inside(r, self.graph_area)
1757
1758        # remove smaller zoom rects from stack
1759        while len(self.zoom_stack) > 0 and r.width() >= self.zoom_stack[-1].width() and r.height() >= self.zoom_stack[-1].height():
1760            self.zoom_stack.pop()
1761
1762        self.zoom_to_rect(r)
1763       
1764    def get_zoom_rect(self):
1765        if self._zoom_rect:
1766            return self._zoom_rect
1767        else:
1768            return self.graph_area
1769       
1770    def set_zoom_rect(self, rect):
1771        self._zoom_rect = rect
1772        self._zoom_transform = self.transform_from_rects(rect, self.graph_area)
1773        self.update_zoom()
1774
1775    zoom_rect = pyqtProperty(QRectF, get_zoom_rect, set_zoom_rect)
1776       
1777    @staticmethod
1778    def ensure_inside(small_rect, big_rect):
1779        if small_rect.width() > big_rect.width():
1780            small_rect.setWidth(big_rect.width())
1781        if small_rect.height() > big_rect.height():
1782            small_rect.setHeight(big_rect.height())
1783       
1784        if small_rect.right() > big_rect.right():
1785            small_rect.moveRight(big_rect.right())
1786        elif small_rect.left() < big_rect.left():
1787            small_rect.moveLeft(big_rect.left())
1788           
1789        if small_rect.bottom() > big_rect.bottom():
1790            small_rect.moveBottom(big_rect.bottom())
1791        elif small_rect.top() < big_rect.top():
1792            small_rect.moveTop(big_rect.top())
1793           
1794    def shuffle_points(self):
1795        if self.main_curve:
1796            self.main_curve.shuffle_points()
1797           
1798    def set_progress(self, done, total):
1799        if not self.widget:
1800            return
1801           
1802        if done == total:
1803            self.widget.progressBarFinished()
1804        else:
1805            self.widget.progressBarSet(100.0 * done / total)
1806       
1807    def start_progress(self):
1808        if self.widget:
1809            self.widget.progressBarInit()
1810           
1811    def end_progress(self):
1812        if self.widget:
1813            self.widget.progressBarFinished()
1814           
1815    def is_axis_auto_scale(self, axis_id):
1816        if axis_id not in self.axes:
1817            return axis_id not in self.data_range
1818        return self.axes[axis_id].auto_scale
1819
1820    def axis_line(self, rect, id, invert_y = False):
1821        if invert_y:
1822            r = QRectF(rect)
1823            r.setTop(rect.bottom())
1824            r.setBottom(rect.top())
1825            rect = r
1826        if id == xBottom:
1827            line = QLineF(rect.topLeft(), rect.topRight())
1828        elif id == xTop:
1829            line = QLineF(rect.bottomLeft(), rect.bottomRight())
1830        elif id == yLeft:
1831            line = QLineF(rect.topLeft(), rect.bottomLeft())
1832        elif id == yRight:
1833            line = QLineF(rect.topRight(), rect.bottomRight())
1834        else:
1835            line = None
1836        return line
1837       
1838    def color(self, role, group = None):
1839        if group:
1840            return self.palette().color(group, role)
1841        else:
1842            return self.palette().color(role)
1843           
1844    def set_palette(self, p):
1845        '''
1846            Sets the plot palette to ``p``.
1847           
1848            :param p: The new color palette
1849            :type p: :obj:`.QPalette`
1850        '''
1851        self.setPalette(p)
1852        self.replot()
1853       
1854    def update_theme(self):
1855        '''
1856            Updates the current color theme, depending on the value of :attr:`theme_name`.
1857        '''
1858        if self.theme_name.lower() == 'default':
1859            self.set_palette(OWPalette.System)
1860        elif self.theme_name.lower() == 'light':
1861            self.set_palette(OWPalette.Light)
1862        elif self.theme_name.lower() == 'dark':
1863            self.set_palette(OWPalette.Dark)
Note: See TracBrowser for help on using the repository browser.