source: orange/orange/OrangeWidgets/plot/owplot.py @ 8225:4ce052fafa6b

Revision 8225:4ce052fafa6b, 63.1 KB checked in by Noughmad <Noughmad@…>, 22 months ago (diff)

Prevent animation flickering on older PyQt versions

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