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