source: sasview/src/sas/qtgui/Plotting/Plotter.py @ 6c7ebb88

Last change on this file since 6c7ebb88 was 6c7ebb88, checked in by GitHub <noreply@…>, 5 years ago

Merge d32a594acc82ad29555832f498167b51564c75f3 into f994f188e28dca36e7823b2deb3bf2bfc351c35c

  • Property mode set to 100644
File size: 27.2 KB
RevLine 
[4992ff2]1from PyQt5 import QtCore
2from PyQt5 import QtGui
3from PyQt5 import QtWidgets
4
[aadf0af1]5import functools
6import copy
[0231f93]7import matplotlib as mpl
[facf4ca]8import numpy as np
[9290b1a]9from matplotlib.font_manager import FontProperties
[863ebca]10
[dc5ef15]11from sas.qtgui.Plotting.PlotterData import Data1D
[83eb5208]12from sas.qtgui.Plotting.PlotterBase import PlotterBase
13from sas.qtgui.Plotting.AddText import AddText
14from sas.qtgui.Plotting.SetGraphRange import SetGraphRange
15from sas.qtgui.Plotting.LinearFit import LinearFit
16from sas.qtgui.Plotting.PlotProperties import PlotProperties
[bb57068]17from sas.qtgui.Plotting.ScaleProperties import ScaleProperties
[dc5ef15]18
19import sas.qtgui.Utilities.GuiUtils as GuiUtils
[83eb5208]20import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
[8cb6cd6]21
[b016f17]22def _legendResize(width):
23    """
24    resize factor for the legend, based on total canvas width
25    """
26    # The factor 4.0 was chosen to look similar in size/ratio to what we had in 4.x
27    return (width/100.0)+4.0
28
[416fa8f]29class PlotterWidget(PlotterBase):
[c4e5400]30    """
31    1D Plot widget for use with a QDialog
[fecfe28]32    """
[416fa8f]33    def __init__(self, parent=None, manager=None, quickplot=False):
34        super(PlotterWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
[570a58f9]35
[27313b7]36        self.parent = parent
[8cb6cd6]37
[aadf0af1]38        # Dictionary of {plot_id:Data1d}
39        self.plot_dict = {}
[c2f3ca2]40        # Dictionaty of {plot_id:line}
[aadf0af1]41
[c2f3ca2]42        self.plot_lines = {}
[fed94a2]43        # Window for text add
44        self.addText = AddText(self)
[aadf0af1]45
[fed94a2]46        # Log-ness of the axes
[570a58f9]47        self.xLogLabel = "log10(x)"
48        self.yLogLabel = "log10(y)"
49
50        # Data container for the linear fit
[fed94a2]51        self.fit_result = Data1D(x=[], y=[], dy=None)
52        self.fit_result.symbol = 13
53        self.fit_result.name = "Fit"
[570a58f9]54
[31c5b58]55    @property
56    def data(self):
57        return self._data
58
59    @data.setter
60    def data(self, value):
[8cb6cd6]61        """ data setter """
[31c5b58]62        self._data = value
[cb4d219]63        if value._xunit:
64            self.xLabel = "%s(%s)"%(value._xaxis, value._xunit)
65        else:
66            self.xLabel = "%s"%(value._xaxis)
67        if value._yunit:
68            self.yLabel = "%s(%s)"%(value._yaxis, value._yunit)
69        else:
70            self.yLabel = "%s"%(value._yaxis)
71
72        if value.scale == 'linear' or value.isSesans:
[749b715]73            self.xscale = 'linear'
74            self.yscale = 'linear'
[aadf0af1]75        self.title(title=value.name)
[8cb6cd6]76
[04ac604]77    def plot(self, data=None, color=None, marker=None, hide_error=False, transform=True):
[8cb6cd6]78        """
[aadf0af1]79        Add a new plot of self._data to the chart.
[8cb6cd6]80        """
[9290b1a]81        # Data1D
82        if isinstance(data, Data1D):
83            self.data = data
[34f13a83]84
85        if not self._data:
[1f34e00]86            return
[9290b1a]87
[87cc73a]88        is_fit = (self.data.id=="fit")
[570a58f9]89
[685e0e3]90        if not is_fit:
91            # make sure we have some function to operate on
92            if self.data.xtransform is None:
[0239237]93                if self.data.isSesans:
94                    self.data.xtransform='x'
95                else:
96                    self.data.xtransform = 'log10(x)'
[685e0e3]97            if self.data.ytransform is None:
[0239237]98                if self.data.isSesans:
99                    self.data.ytransform='y'
100                else:
101                    self.data.ytransform = 'log10(y)'
[d0952de]102            #Added condition to Dmax explorer from P(r) perspective
103            if self.data._xaxis == 'D_{max}':
104                self.xscale = 'linear'
[685e0e3]105            # Transform data if required.
[04ac604]106            if transform and (self.data.xtransform is not None or self.data.ytransform is not None):
[685e0e3]107                _, _, xscale, yscale = GuiUtils.xyTransform(self.data, self.data.xtransform, self.data.ytransform)
108                if xscale != 'log':
109                    self.xscale = xscale
110                if yscale != 'log':
111                    self.yscale = yscale
112
113                # Redefine the Scale properties dialog
114                self.properties = ScaleProperties(self,
115                                        init_scale_x=self.data.xtransform,
116                                        init_scale_y=self.data.ytransform)
[f182f93]117
[87cc73a]118        # Shortcuts
[ef01be4]119        ax = self.ax
[87cc73a]120        x = self._data.view.x
121        y = self._data.view.y
122
123        # Marker symbol. Passed marker is one of matplotlib.markers characters
124        # Alternatively, picked up from Data1D as an int index of PlotUtilities.SHAPES dict
125        if marker is None:
126            marker = self.data.symbol
[6fd4e36]127            # Try name first
128            try:
[cb4d219]129                marker = dict(PlotUtilities.SHAPES)[marker]
[6fd4e36]130            except KeyError:
[8f83719f]131                marker = list(PlotUtilities.SHAPES.values())[marker]
[87cc73a]132
[6fd4e36]133        assert marker is not None
[87cc73a]134        # Plot name
[b789967]135        if self.data.title:
136            self.title(title=self.data.title)
137        else:
138            self.title(title=self.data.name)
[87cc73a]139
140        # Error marker toggle
141        if hide_error is None:
142            hide_error = self.data.hide_error
143
144        # Plot color
145        if color is None:
146            color = self.data.custom_color
147
148        color = PlotUtilities.getValidColor(color)
[0231f93]149        self.data.custom_color = color
[87cc73a]150
151        markersize = self._data.markersize
152
[facf4ca]153        # Include scaling (log vs. linear)
[8274471e]154        ax.set_xscale(self.xscale, nonposx='clip')
[facf4ca]155        ax.set_yscale(self.yscale, nonposy='clip')
156
157        # define the ranges
158        self.setRange = SetGraphRange(parent=self,
159            x_range=self.ax.get_xlim(), y_range=self.ax.get_ylim())
160
[239214f]161        # Draw non-standard markers
162        l_width = markersize * 0.4
163        if marker == '-' or marker == '--':
164            line = self.ax.plot(x, y, color=color, lw=l_width, marker='',
165                             linestyle=marker, label=self._title, zorder=10)[0]
166
167        elif marker == 'vline':
168            y_min = min(y)*9.0/10.0 if min(y) < 0 else 0.0
169            line = self.ax.vlines(x=x, ymin=y_min, ymax=y, color=color,
170                            linestyle='-', label=self._title, lw=l_width, zorder=1)
171
172        elif marker == 'step':
173            line = self.ax.step(x, y, color=color, marker='', linestyle='-',
174                                label=self._title, lw=l_width, zorder=1)[0]
[8cb6cd6]175
[c4e5400]176        else:
[87cc73a]177            # plot data with/without errorbars
178            if hide_error:
179                line = ax.plot(x, y, marker=marker, color=color, markersize=markersize,
180                        linestyle='', label=self._title, picker=True)
181            else:
[facf4ca]182                dy = self._data.view.dy
183                # Convert tuple (lo,hi) to array [(x-lo),(hi-x)]
184                if dy is not None and type(dy) == type(()):
185                    dy = np.vstack((y - dy[0], dy[1] - y)).transpose()
186
[87cc73a]187                line = ax.errorbar(x, y,
[facf4ca]188                            yerr=dy,
189                            xerr=None,
[87cc73a]190                            capsize=2, linestyle='',
191                            barsabove=False,
192                            color=color,
193                            marker=marker,
194                            markersize=markersize,
195                            lolims=False, uplims=False,
196                            xlolims=False, xuplims=False,
197                            label=self._title,
[facf4ca]198                            zorder=1,
[87cc73a]199                            picker=True)
[8cb6cd6]200
[aadf0af1]201        # Update the list of data sets (plots) in chart
[0cd98a1]202        self.plot_dict[self._data.name] = self.data
[aadf0af1]203
[0cd98a1]204        self.plot_lines[self._data.name] = line
[c2f3ca2]205
[8cb6cd6]206        # Now add the legend with some customizations.
[f0bb711]207
[42787fb]208        if self.showLegend:
[b016f17]209            width=_legendResize(self.canvas.size().width())
210
211            self.legend = ax.legend(loc='upper right', shadow=True, prop={'size':width})
[42787fb]212            if self.legend:
213                self.legend.set_picker(True)
[8cb6cd6]214
[6d05e1d]215        # Current labels for axes
[570a58f9]216        if self.y_label and not is_fit:
217            ax.set_ylabel(self.y_label)
218        if self.x_label and not is_fit:
219            ax.set_xlabel(self.x_label)
[6d05e1d]220
[8cb6cd6]221        # refresh canvas
[d9e7792]222        self.canvas.draw_idle()
[8cb6cd6]223
[b016f17]224    def onResize(self, event):
225        """
226        Resize the legend window/font on canvas resize
227        """
[6edd344]228        if not self.showLegend:
229            return
[b016f17]230        width = _legendResize(event.width)
231        # resize the legend to follow the canvas width.
232        self.legend.prop.set_size(width)
233
[aadf0af1]234    def createContextMenu(self):
[c4e5400]235        """
236        Define common context menu and associated actions for the MPL widget
237        """
238        self.defaultContextMenu()
239
[aadf0af1]240        # Separate plots
241        self.addPlotsToContextMenu()
242
[27313b7]243        # Additional menu items
244        self.contextMenu.addSeparator()
245        self.actionAddText = self.contextMenu.addAction("Add Text")
246        self.actionRemoveText = self.contextMenu.addAction("Remove Text")
247        self.contextMenu.addSeparator()
248        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
249        self.contextMenu.addSeparator()
250        self.actionSetGraphRange = self.contextMenu.addAction("Set Graph Range")
251        self.actionResetGraphRange =\
252            self.contextMenu.addAction("Reset Graph Range")
253        # Add the title change for dialogs
[aadf0af1]254        self.contextMenu.addSeparator()
255        self.actionWindowTitle = self.contextMenu.addAction("Window Title")
[27313b7]256
257        # Define the callbacks
258        self.actionAddText.triggered.connect(self.onAddText)
259        self.actionRemoveText.triggered.connect(self.onRemoveText)
260        self.actionChangeScale.triggered.connect(self.onScaleChange)
261        self.actionSetGraphRange.triggered.connect(self.onSetGraphRange)
262        self.actionResetGraphRange.triggered.connect(self.onResetGraphRange)
263        self.actionWindowTitle.triggered.connect(self.onWindowsTitle)
[c4e5400]264
[aadf0af1]265    def addPlotsToContextMenu(self):
266        """
267        Adds operations on all plotted sets of data to the context menu
268        """
[b3e8629]269        for id in list(self.plot_dict.keys()):
[aadf0af1]270            plot = self.plot_dict[id]
[b789967]271
272            name = plot.name if plot.name else plot.title
[aadf0af1]273            plot_menu = self.contextMenu.addMenu('&%s' % name)
274
275            self.actionDataInfo = plot_menu.addAction("&DataInfo")
276            self.actionDataInfo.triggered.connect(
277                                functools.partial(self.onDataInfo, plot))
278
279            self.actionSavePointsAsFile = plot_menu.addAction("&Save Points as a File")
280            self.actionSavePointsAsFile.triggered.connect(
281                                functools.partial(self.onSavePoints, plot))
282            plot_menu.addSeparator()
283
284            if plot.id != 'fit':
285                self.actionLinearFit = plot_menu.addAction('&Linear Fit')
[570a58f9]286                self.actionLinearFit.triggered.connect(
287                                functools.partial(self.onLinearFit, id))
[aadf0af1]288                plot_menu.addSeparator()
289
290            self.actionRemovePlot = plot_menu.addAction("Remove")
291            self.actionRemovePlot.triggered.connect(
292                                functools.partial(self.onRemovePlot, id))
293
294            if not plot.is_data:
295                self.actionFreeze = plot_menu.addAction('&Freeze')
296                self.actionFreeze.triggered.connect(
297                                functools.partial(self.onFreeze, id))
298            plot_menu.addSeparator()
299
300            if plot.is_data:
301                self.actionHideError = plot_menu.addAction("Hide Error Bar")
302                if plot.dy is not None and plot.dy != []:
303                    if plot.hide_error:
304                        self.actionHideError.setText('Show Error Bar')
305                else:
306                    self.actionHideError.setEnabled(False)
307                self.actionHideError.triggered.connect(
308                                functools.partial(self.onToggleHideError, id))
309                plot_menu.addSeparator()
310
311            self.actionModifyPlot = plot_menu.addAction('&Modify Plot Property')
[87cc73a]312            self.actionModifyPlot.triggered.connect(
313                                functools.partial(self.onModifyPlot, id))
[aadf0af1]314
315    def createContextMenuQuick(self):
[6d05e1d]316        """
317        Define context menu and associated actions for the quickplot MPL widget
318        """
[c4e5400]319        # Default actions
320        self.defaultContextMenu()
321
322        # Additional actions
[6d05e1d]323        self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
324        self.contextMenu.addSeparator()
325        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
326
327        # Define the callbacks
328        self.actionToggleGrid.triggered.connect(self.onGridToggle)
329        self.actionChangeScale.triggered.connect(self.onScaleChange)
330
331    def onScaleChange(self):
332        """
333        Show a dialog allowing axes rescaling
334        """
[4992ff2]335        if self.properties.exec_() == QtWidgets.QDialog.Accepted:
[570a58f9]336            self.xLogLabel, self.yLogLabel = self.properties.getValues()
[bb57068]337            self.data.xtransform = self.xLogLabel
338            self.data.ytransform = self.yLogLabel
[570a58f9]339            self.xyTransform(self.xLogLabel, self.yLogLabel)
[6d05e1d]340
[27313b7]341    def onAddText(self):
342        """
343        Show a dialog allowing adding custom text to the chart
344        """
[eb1a386]345        if self.addText.exec_() != QtWidgets.QDialog.Accepted:
346            return
347
348        # Retrieve the new text, its font and color
349        extra_text = self.addText.text()
350        extra_font = self.addText.font()
351        extra_color = self.addText.color()
352
353        # Place the text on the screen at the click location
354        pos_x = self.x_click
355        pos_y = self.y_click
356
357        # Map QFont onto MPL font
358        mpl_font = FontProperties()
359        mpl_font.set_size(int(extra_font.pointSize()))
360        mpl_font.set_family(str(extra_font.family()))
361        mpl_font.set_weight(int(extra_font.weight()))
362        # MPL style names
363        styles = ['normal', 'italic', 'oblique']
364        # QFont::Style maps directly onto the above
365        try:
366            mpl_font.set_style(styles[extra_font.style()])
367        except:
368            pass
369
370        if len(extra_text) > 0:
371            new_text = self.ax.text(pos_x,
372                                    pos_y,
373                                    extra_text,
374                                    color=extra_color,
375                                    fontproperties=mpl_font)
376
377            # Update the list of annotations
378            self.textList.append(new_text)
[d9e7792]379            self.canvas.draw_idle()
[27313b7]380
381    def onRemoveText(self):
382        """
[9290b1a]383        Remove the most recently added text
[27313b7]384        """
[d3ca363]385        num_text = len(self.textList)
386        if num_text < 1:
387            return
388        txt = self.textList[num_text - 1]
389        text_remove = txt.get_text()
[eb1a386]390        try:
391            txt.remove()
392        except ValueError:
393            # Text got already deleted somehow
394            pass
[d3ca363]395        self.textList.remove(txt)
396
397        self.canvas.draw_idle()
[27313b7]398
399    def onSetGraphRange(self):
400        """
401        Show a dialog allowing setting the chart ranges
402        """
[d3ca363]403        # min and max of data
[4992ff2]404        if self.setRange.exec_() == QtWidgets.QDialog.Accepted:
[257bd57]405            x_range = self.setRange.xrange()
406            y_range = self.setRange.yrange()
407            if x_range is not None and y_range is not None:
408                self.ax.set_xlim(x_range)
409                self.ax.set_ylim(y_range)
410                self.canvas.draw_idle()
[27313b7]411
412    def onResetGraphRange(self):
413        """
[d3ca363]414        Resets the chart X and Y ranges to their original values
[27313b7]415        """
[d3ca363]416        x_range = (self.data.x.min(), self.data.x.max())
417        y_range = (self.data.y.min(), self.data.y.max())
[257bd57]418        if x_range is not None and y_range is not None:
419            self.ax.set_xlim(x_range)
420            self.ax.set_ylim(y_range)
421            self.canvas.draw_idle()
[27313b7]422
[570a58f9]423    def onLinearFit(self, id):
[aadf0af1]424        """
425        Creates and displays a simple linear fit for the selected plot
426        """
[570a58f9]427        selected_plot = self.plot_dict[id]
428
429        maxrange = (min(selected_plot.x), max(selected_plot.x))
430        fitrange = self.ax.get_xlim()
431
432        fit_dialog = LinearFit(parent=self,
433                    data=selected_plot,
434                    max_range=maxrange,
435                    fit_range=fitrange,
436                    xlabel=self.xLogLabel,
437                    ylabel=self.yLogLabel)
[7969b9c]438        fit_dialog.updatePlot.connect(self.onFitDisplay)
[4992ff2]439        if fit_dialog.exec_() == QtWidgets.QDialog.Accepted:
[570a58f9]440            return
[aadf0af1]441
[87cc73a]442    def replacePlot(self, id, new_plot):
443        """
444        Remove plot 'id' and add 'new_plot' to the chart.
445        This effectlvely refreshes the chart with changes to one of its plots
446        """
[4c11b2a]447
448        # Pull the current transform settings from the old plot
449        selected_plot = self.plot_dict[id]
450        new_plot.xtransform = selected_plot.xtransform
451        new_plot.ytransform = selected_plot.ytransform
452
[87cc73a]453        self.removePlot(id)
454        self.plot(data=new_plot)
455
[aadf0af1]456    def onRemovePlot(self, id):
457        """
[570a58f9]458        Responds to the plot delete action
459        """
460        self.removePlot(id)
461
462        if len(self.plot_dict) == 0:
463            # last plot: graph is empty must be the panel must be destroyed
464                self.parent.close()
465
466    def removePlot(self, id):
467        """
[aadf0af1]468        Deletes the selected plot from the chart
469        """
[b3e8629]470        if id not in list(self.plot_dict.keys()):
[570a58f9]471            return
472
[aadf0af1]473        selected_plot = self.plot_dict[id]
474
475        plot_dict = copy.deepcopy(self.plot_dict)
476
[b46f285]477        # Labels might have been changed
478        xl = self.ax.xaxis.label.get_text()
479        yl = self.ax.yaxis.label.get_text()
480
[aadf0af1]481        self.plot_dict = {}
482
[0231f93]483        mpl.pyplot.cla()
[aadf0af1]484        self.ax.cla()
485
486        for ids in plot_dict:
487            if ids != id:
[b46f285]488                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)
489
490        # Reset the labels
491        self.ax.set_xlabel(xl)
492        self.ax.set_ylabel(yl)
[d9e7792]493        self.canvas.draw_idle()
[aadf0af1]494
495    def onFreeze(self, id):
496        """
497        Freezes the selected plot to a separate chart
498        """
499        plot = self.plot_dict[id]
[3b95b3b]500        plot = copy.deepcopy(plot)
[e2e5f3d]501        self.manager.add_data(data_list={id:plot})
502        self.manager.freezeDataToItem(plot)
[aadf0af1]503
[87cc73a]504    def onModifyPlot(self, id):
[aadf0af1]505        """
506        Allows for MPL modifications to the selected plot
507        """
[87cc73a]508        selected_plot = self.plot_dict[id]
[c2f3ca2]509        selected_line = self.plot_lines[id]
[87cc73a]510        # Old style color - single integer for enum color
511        # New style color - #hhhhhh
512        color = selected_plot.custom_color
513        # marker symbol and size
514        marker = selected_plot.symbol
515        marker_size = selected_plot.markersize
516        # plot name
517        legend = selected_plot.title
518
519        plotPropertiesWidget = PlotProperties(self,
520                                color=color,
521                                marker=marker,
522                                marker_size=marker_size,
523                                legend=legend)
[4992ff2]524        if plotPropertiesWidget.exec_() == QtWidgets.QDialog.Accepted:
[87cc73a]525            # Update Data1d
[0f3c22d]526            selected_plot.markersize = plotPropertiesWidget.markersize()
527            selected_plot.custom_color = plotPropertiesWidget.color()
528            selected_plot.symbol = plotPropertiesWidget.marker()
529            selected_plot.title = plotPropertiesWidget.legend()
[87cc73a]530
531            # Redraw the plot
532            self.replacePlot(id, selected_plot)
[aadf0af1]533
534    def onToggleHideError(self, id):
535        """
536        Toggles hide error/show error menu item
537        """
538        selected_plot = self.plot_dict[id]
539        current = selected_plot.hide_error
540
541        # Flip the flag
542        selected_plot.hide_error = not current
543
544        plot_dict = copy.deepcopy(self.plot_dict)
545        self.plot_dict = {}
546
547        # Clean the canvas
[0231f93]548        mpl.pyplot.cla()
[aadf0af1]549        self.ax.cla()
550
551        # Recreate the plots but reverse the error flag for the current
552        for ids in plot_dict:
553            if ids == id:
554                self.plot(data=plot_dict[ids], hide_error=(not current))
555            else:
556                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)               
557
[6d05e1d]558    def xyTransform(self, xLabel="", yLabel=""):
559        """
560        Transforms x and y in View and set the scale
561        """
[570a58f9]562        # Transform all the plots on the chart
[b3e8629]563        for id in list(self.plot_dict.keys()):
[570a58f9]564            current_plot = self.plot_dict[id]
565            if current_plot.id == "fit":
566                self.removePlot(id)
567                continue
[fed94a2]568
[570a58f9]569            new_xlabel, new_ylabel, xscale, yscale =\
570                GuiUtils.xyTransform(current_plot, xLabel, yLabel)
571            self.xscale = xscale
572            self.yscale = yscale
[b46f285]573
[570a58f9]574            # Plot the updated chart
575            self.removePlot(id)
[b46f285]576
577            # This assignment will wrap the label in Latex "$"
578            self.xLabel = new_xlabel
579            self.yLabel = new_ylabel
[04ac604]580
581            self.plot(data=current_plot, transform=False)
[570a58f9]582
583        pass # debug hook
584
[fed94a2]585    def onFitDisplay(self, fit_data):
586        """
587        Add a linear fitting line to the chart
588        """
589        # Create new data structure with fitting result
590        tempx = fit_data[0]
591        tempy = fit_data[1]
592        self.fit_result.x = []
593        self.fit_result.y = []
594        self.fit_result.x = tempx
595        self.fit_result.y = tempy
596        self.fit_result.dx = None
597        self.fit_result.dy = None
598
599        #Remove another Fit, if exists
[5b144c6]600        self.removePlot("Fit")
[fed94a2]601
602        self.fit_result.reset_view()
603        #self.offset_graph()
604
605        # Set plot properties
606        self.fit_result.id = 'fit'
607        self.fit_result.title = 'Fit'
608        self.fit_result.name = 'Fit'
609
610        # Plot the line
[87cc73a]611        self.plot(data=self.fit_result, marker='-', hide_error=True)
[fed94a2]612
[3bdbfcc]613    def onMplMouseDown(self, event):
614        """
615        Left button down and ready to drag
616        """
617        # Check that the LEFT button was pressed
[0268aed]618        if event.button != 1:
619            return
620
621        self.leftdown = True
622        for text in self.textList:
623            if text.contains(event)[0]: # If user has clicked on text
624                self.selectedText = text
625                return
626        if event.inaxes is None:
627            return
628        try:
629            self.x_click = float(event.xdata)  # / size_x
630            self.y_click = float(event.ydata)  # / size_y
631        except:
632            self.position = None
[3bdbfcc]633
[c30822c]634        x_str = GuiUtils.formatNumber(self.x_click)
635        y_str = GuiUtils.formatNumber(self.y_click)
636        coord_str = "x: {}, y: {}".format(x_str, y_str)
637        self.manager.communicator.statusBarUpdateSignal.emit(coord_str)
638
[3bdbfcc]639    def onMplMouseUp(self, event):
640        """
641        Set the data coordinates of the click
642        """
643        self.x_click = event.xdata
644        self.y_click = event.ydata
645
646        # Check that the LEFT button was released
647        if event.button == 1:
648            self.leftdown = False
649            self.selectedText = None
650
651        #release the legend
652        if self.gotLegend == 1:
653            self.gotLegend = 0
654
655    def onMplMouseMotion(self, event):
656        """
657        Check if the left button is press and the mouse in moving.
658        Compute delta for x and y coordinates and then perform the drag
659        """
660        if self.gotLegend == 1 and self.leftdown:
661            self.onLegendMotion(event)
662            return
663
[0268aed]664        #if self.leftdown and self.selectedText is not None:
[9f25bce]665        if not self.leftdown or self.selectedText is None:
[3bdbfcc]666            return
[0268aed]667        # User has clicked on text and is dragging
[9f25bce]668        if event.inaxes is None:
[0268aed]669            # User has dragged outside of axes
670            self.selectedText = None
671        else:
672            # Only move text if mouse is within axes
673            self.selectedText.set_position((event.xdata, event.ydata))
674            self.canvas.draw_idle()
675        return
[3bdbfcc]676
677    def onMplPick(self, event):
678        """
679        On pick legend
680        """
681        legend = self.legend
[0268aed]682        if event.artist != legend:
683            return
684        # Get the box of the legend.
685        bbox = self.legend.get_window_extent()
686        # Get mouse coordinates at time of pick.
687        self.mouse_x = event.mouseevent.x
688        self.mouse_y = event.mouseevent.y
689        # Get legend coordinates at time of pick.
690        self.legend_x = bbox.xmin
691        self.legend_y = bbox.ymin
692        # Indicate we picked up the legend.
693        self.gotLegend = 1
694
695        #self.legend.legendPatch.set_alpha(0.5)
[3bdbfcc]696
697    def onLegendMotion(self, event):
698        """
699        On legend in motion
700        """
701        ax = event.inaxes
[cee5c78]702        if ax is None:
[3bdbfcc]703            return
704        # Event occurred inside a plotting area
705        lo_x, hi_x = ax.get_xlim()
706        lo_y, hi_y = ax.get_ylim()
707        # How much the mouse moved.
708        x = mouse_diff_x = self.mouse_x - event.x
709        y = mouse_diff_y = self.mouse_y - event.y
710        # Put back inside
711        if x < lo_x:
712            x = lo_x
713        if x > hi_x:
714            x = hi_x
715        if y < lo_y:
716            y = lo_y
717        if y > hi_y:
718            y = hi_y
719        # Move the legend from its previous location by that same amount
720        loc_in_canvas = self.legend_x - mouse_diff_x, \
721                        self.legend_y - mouse_diff_y
722        # Transform into legend coordinate system
723        trans_axes = self.legend.parent.transAxes.inverted()
724        loc_in_norm_axes = trans_axes.transform_point(loc_in_canvas)
725        self.legend_pos_loc = tuple(loc_in_norm_axes)
726        self.legend._loc = self.legend_pos_loc
727        self.canvas.draw_idle()
728
729    def onMplWheel(self, event):
730        """
731        Process mouse wheel as zoom events
732        """
733        ax = event.inaxes
734        step = event.step
735
[cee5c78]736        if ax is not None:
[3bdbfcc]737            # Event occurred inside a plotting area
738            lo, hi = ax.get_xlim()
739            lo, hi = PlotUtilities.rescale(lo, hi, step,
740                              pt=event.xdata, scale=ax.get_xscale())
741            if not self.xscale == 'log' or lo > 0:
742                self._scale_xlo = lo
743                self._scale_xhi = hi
744                ax.set_xlim((lo, hi))
745
746            lo, hi = ax.get_ylim()
747            lo, hi = PlotUtilities.rescale(lo, hi, step, pt=event.ydata,
748                              scale=ax.get_yscale())
749            if not self.yscale == 'log' or lo > 0:
750                self._scale_ylo = lo
751                self._scale_yhi = hi
752                ax.set_ylim((lo, hi))
753        else:
754            # Check if zoom happens in the axes
755            xdata, ydata = None, None
756            x, y = event.x, event.y
757
758            for ax in self.axes:
759                insidex, _ = ax.xaxis.contains(event)
760                if insidex:
761                    xdata, _ = ax.transAxes.inverted().transform_point((x, y))
762                insidey, _ = ax.yaxis.contains(event)
763                if insidey:
764                    _, ydata = ax.transAxes.inverted().transform_point((x, y))
765            if xdata is not None:
766                lo, hi = ax.get_xlim()
767                lo, hi = PlotUtilities.rescale(lo, hi, step,
768                                  bal=xdata, scale=ax.get_xscale())
769                if not self.xscale == 'log' or lo > 0:
770                    self._scale_xlo = lo
771                    self._scale_xhi = hi
772                    ax.set_xlim((lo, hi))
773            if ydata is not None:
774                lo, hi = ax.get_ylim()
775                lo, hi = PlotUtilities.rescale(lo, hi, step, bal=ydata,
776                                  scale=ax.get_yscale())
777                if not self.yscale == 'log' or lo > 0:
778                    self._scale_ylo = lo
779                    self._scale_yhi = hi
780                    ax.set_ylim((lo, hi))
781        self.canvas.draw_idle()
782
[c4e5400]783
[4992ff2]784class Plotter(QtWidgets.QDialog, PlotterWidget):
[416fa8f]785    def __init__(self, parent=None, quickplot=False):
786
[4992ff2]787        QtWidgets.QDialog.__init__(self)
[aadf0af1]788        PlotterWidget.__init__(self, parent=self, manager=parent, quickplot=quickplot)
[c4e5400]789        icon = QtGui.QIcon()
790        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
791        self.setWindowIcon(icon)
792
[416fa8f]793
Note: See TracBrowser for help on using the repository browser.