1
|
|
|
''' |
2
|
|
|
|
3
|
|
|
################# |
4
|
|
|
Plot (``owplot``) |
5
|
|
|
################# |
6
|
|
|
|
7
|
|
|
.. autoclass:: OrangeWidgets.plot.OWPlot |
8
|
|
|
|
9
|
|
|
''' |
10
|
|
|
|
11
|
|
|
from PyQt4 import QtCore, QtGui |
|
|
|
|
12
|
|
|
from Orange.widgets.gui import OWComponent |
13
|
|
|
from Orange.widgets.settings import Setting |
14
|
|
|
|
15
|
|
|
LeftLegend = 0 |
16
|
|
|
RightLegend = 1 |
17
|
|
|
BottomLegend = 2 |
18
|
|
|
TopLegend = 3 |
19
|
|
|
ExternalLegend = 4 |
20
|
|
|
|
21
|
|
|
UNUSED_ATTRIBUTES_STR = 'unused attributes' |
22
|
|
|
|
23
|
|
|
from .owaxis import * |
|
|
|
|
24
|
|
|
from .owcurve import * |
|
|
|
|
25
|
|
|
from .owlegend import * |
|
|
|
|
26
|
|
|
from .owplotgui import OWPlotGUI |
27
|
|
|
from .owtools import * |
|
|
|
|
28
|
|
|
|
29
|
|
|
from ..colorpalette import ColorPaletteGenerator |
30
|
|
|
|
31
|
|
|
from PyQt4.QtGui import ( |
|
|
|
|
32
|
|
|
QPen, QBrush, QColor, |
33
|
|
|
QGraphicsView, QGraphicsScene, QPainter, QTransform, QPolygonF, |
34
|
|
|
QGraphicsRectItem) |
35
|
|
|
|
36
|
|
|
from PyQt4.QtCore import QPointF, QPropertyAnimation, pyqtProperty, SIGNAL, Qt, QEvent |
|
|
|
|
37
|
|
|
|
38
|
|
|
## Color values copied from orngView.SchemaView for consistency |
39
|
|
|
SelectionPen = QPen(QBrush(QColor(51, 153, 255, 192)), |
40
|
|
|
1, Qt.SolidLine, Qt.RoundCap) |
41
|
|
|
SelectionBrush = QBrush(QColor(168, 202, 236, 192)) |
42
|
|
|
|
43
|
|
|
#from OWDlgs import OWChooseImageSizeDlg |
44
|
|
|
#from OWColorPalette import * # color palletes, ... |
45
|
|
|
#from Orange.utils import deprecated_members, deprecated_attribute |
46
|
|
|
|
47
|
|
|
import orangeqt |
|
|
|
|
48
|
|
|
|
49
|
|
|
def n_min(*args): |
50
|
|
|
lst = args[0] if len(args) == 1 else args |
51
|
|
|
a = [i for i in lst if i is not None] |
52
|
|
|
return min(a) if a else None |
53
|
|
|
|
54
|
|
|
def n_max(*args): |
55
|
|
|
lst = args[0] if len(args) == 1 else args |
56
|
|
|
a = [i for i in lst if i is not None] |
57
|
|
|
return max(a) if a else None |
58
|
|
|
|
59
|
|
|
name_map = { |
60
|
|
|
"saveToFileDirect": "save_to_file_direct", |
61
|
|
|
"saveToFile" : "save_to_file", |
62
|
|
|
"addCurve" : "add_curve", |
63
|
|
|
"addMarker" : "add_marker", |
64
|
|
|
"updateLayout" : "update_layout", |
65
|
|
|
"activateZooming" : "activate_zooming", |
66
|
|
|
"activateSelection" : "activate_selection", |
67
|
|
|
"activateRectangleSelection" : "activate_rectangle_selection", |
68
|
|
|
"activatePolygonSelection" : "activate_polygon_selection", |
69
|
|
|
"activatePanning" : "activate_panning", |
70
|
|
|
"getSelectedPoints" : "get_selected_points", |
71
|
|
|
"setAxisScale" : "set_axis_scale", |
72
|
|
|
"setAxisLabels" : "set_axis_labels", |
73
|
|
|
"setAxisAutoScale" : "set_axis_autoscale", |
74
|
|
|
"setTickLength" : "set_axis_tick_length", |
75
|
|
|
"updateCurves" : "update_curves", |
76
|
|
|
"itemList" : "plot_items", |
77
|
|
|
"setShowMainTitle" : "set_show_main_title", |
78
|
|
|
"setMainTitle" : "set_main_title", |
79
|
|
|
"invTransform" : "inv_transform", |
80
|
|
|
"setAxisTitle" : "set_axis_title", |
81
|
|
|
"setShowAxisTitle" : "set_show_axis_title" |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
#@deprecated_members(name_map, wrap_methods=list(name_map.keys())) |
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.