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