################################################################################ #This software was developed by the University of Tennessee as part of the #Distributed Data Analysis of Neutron Scattering Experiments (DANSE) #project funded by the US National Science Foundation. # #See the license text in license.txt # #copyright 2008, University of Tennessee ################################################################################ import sys import math import logging import wx import numpy as np from matplotlib.font_manager import FontProperties from sas.sasgui.plottools.PlotPanel import PlotPanel from sas.sasgui.plottools.plottables import Graph from sas.sasgui.plottools.TextDialog import TextDialog from sas.sasgui.guiframe.events import StatusEvent from sas.sasgui.guiframe.events import NewPlotEvent from sas.sasgui.guiframe.events import PanelOnFocusEvent from sas.sasgui.guiframe.events import SlicerEvent from sas.sasgui.guiframe.utils import PanelMenu from sas.sasgui.guiframe.local_perspectives.plotting.binder import BindArtist from sas.sasgui.plottools.toolbar import NavigationToolBar from .Plotter1D import ModelPanel1D from .graphAppearance import graphAppearance (InternalEvent, EVT_INTERNAL) = wx.lib.newevent.NewEvent() logger = logging.getLogger(__name__) DEFAULT_QMAX = 0.05 DEFAULT_QSTEP = 0.001 DEFAULT_BEAM = 0.005 BIN_WIDTH = 1.0 def find_key(dic, val): """return the key of dictionary dic given the value""" return [k for k, v in dic.items() if v == val][0] class NavigationToolBar2D(NavigationToolBar): """ """ def __init__(self, canvas, parent=None): NavigationToolBar.__init__(self, canvas=canvas, parent=parent) def delete_option(self): """ remove default toolbar item """ #delete reset button self.DeleteToolByPos(0) #delete dragging self.DeleteToolByPos(2) #delete unwanted button that configures subplot parameters self.DeleteToolByPos(4) def add_option(self): """ add item to the toolbar """ #add button id_context = wx.NewId() context_tip = 'Graph Menu' context = wx.ArtProvider.GetBitmap(wx.ART_LIST_VIEW, wx.ART_TOOLBAR) self.InsertSimpleTool(0, id_context, context, context_tip, context_tip) wx.EVT_TOOL(self, id_context, self.parent.onToolContextMenu) self.InsertSeparator(1) #add print button id_print = wx.NewId() print_bmp = wx.ArtProvider.GetBitmap(wx.ART_PRINT, wx.ART_TOOLBAR) self.AddSimpleTool(id_print, print_bmp, 'Print', 'Activate printing') wx.EVT_TOOL(self, id_print, self.on_print) class ModelPanel2D(ModelPanel1D): """ Plot panel for use with the GUI manager """ ## Internal name for the AUI manager window_name = "plotpanel" ## Title to appear on top of the window window_caption = "Plot Panel" ## Flag to tell the GUI manager that this panel is not # tied to any perspective ALWAYS_ON = True ## Group ID group_id = None def __init__(self, parent, id=-1, data2d=None, color=None, dpi=None, style=wx.NO_FULL_REPAINT_ON_RESIZE, **kwargs): """ Initialize the panel """ ModelPanel1D.__init__(self, parent, id=id, style=style, **kwargs) self.parent = parent ## Reference to the parent window if hasattr(parent, "parent"): self.parent = self.parent.parent ## Dictionary containing Plottables self.plots = {} ## Save reference of the current plotted self.data2D = data2d ## Unique ID (from gui_manager) self.uid = None ## Action IDs for internal call-backs self.action_ids = {} ## Create Artist and bind it self.connect = BindArtist(self.subplot.figure) ## Beam stop self.beamstop_radius = DEFAULT_BEAM ## to set the order of lines drawn first. self.slicer_z = 5 ## Reference to the current slicer self.slicer = None ## event to send slicer info self.Bind(EVT_INTERNAL, self._onEVT_INTERNAL) self.axes_frozen = False ## panel that contains result from slicer motion (ex: Boxsum info) self.panel_slicer = None self.title_label = None self.title_font = None self.title_color = 'black' ## Graph self.graph = Graph() self.graph.xaxis("\\rm{Q}", 'A^{-1}') self.graph.yaxis("\\rm{Intensity} ", "cm^{-1}") self.graph.render(self) ## store default value of zmin and zmax self.default_zmin_ctl = self.zmin_2D self.default_zmax_ctl = self.zmax_2D def on_plot_qrange(self, event=None): """ On Qmin Qmax vertical line event """ # Not implemented if event is None: return event.Skip() def onLeftDown(self, event): """ left button down and ready to drag """ # Check that the LEFT button was pressed PlotPanel.onLeftDown(self, event) ax = event.inaxes if ax is not None: # data coordinate position pos_x = "%8.3g" % event.xdata pos_y = "%8.3g" % event.ydata position = "x: %s y: %s" % (pos_x, pos_y) wx.PostEvent(self.parent, StatusEvent(status=position)) self.plottable_selected(self.data2D.id) self._manager.set_panel_on_focus(self) wx.PostEvent(self.parent, PanelOnFocusEvent(panel=self)) def add_toolbar(self): """ add toolbar """ self.enable_toolbar = True self.toolbar = NavigationToolBar2D(parent=self, canvas=self.canvas) self.toolbar.Realize() # On Windows platform, default window size is incorrect, so set # toolbar width to figure width. _, th = self.toolbar.GetSizeTuple() fw, _ = self.canvas.GetSizeTuple() self.toolbar.SetSize(wx.Size(fw, th)) self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND) # update the axes menu on the toolbar self.toolbar.update() def plot_data(self, data): """ Data is ready to be displayed :TODO: this name should be changed to something more appropriate Don't forget that changing this name will mean changing code in plotting.py :param event: data event """ xlo = None xhi = None ylo = None yhi = None if data.__class__.__name__ == 'Data1D': return ## Update self.data2d with the current plot self.data2D = data if data.id in self.plots: #replace xlo, xhi = self.subplot.get_xlim() ylo, yhi = self.subplot.get_ylim() self.graph.replace(data) self.plots[data.id] = data else: self.plots[data.id] = data self.graph.add(self.plots[data.id]) # update qmax with the new xmax of data plotted self.qmax = data.xmax self.slicer = None # Check axis labels #TODO: Should re-factor this ## render the graph with its new content #data2D: put 'Pixel (Number)' for axis title and unit in case of having no detector info and none in _units if len(data.detector) < 1: if len(data._xunit) < 1 and len(data._yunit) < 1: data._xaxis = '\\rm{x}' data._yaxis = '\\rm{y}' data._xunit = 'pixel' data._yunit = 'pixel' # graph properties self.graph.xaxis(data._xaxis, data._xunit) self.graph.yaxis(data._yaxis, data._yunit) if self._is_changed_legend_label: data.label = self.title_label if data.label is None: data.label = data.name if not self.title_font: self.graph.title(data.label) self.graph.render(self) # Set the axis labels on subplot self._set_axis_labels() self.draw_plot() else: self.graph.render(self) self.draw_plot() self.subplot.set_title(label=data.label, fontproperties=self.title_font, color=self.title_color) self.subplot.figure.canvas.draw_idle() ## store default value of zmin and zmax self.default_zmin_ctl = self.zmin_2D self.default_zmax_ctl = self.zmax_2D if not self.is_zoomed: return # Recover the x,y limits if xlo and xhi and ylo and yhi: if xlo > data.xmin and xhi < data.xmax and \ ylo > data.ymin and yhi < data.ymax: self.subplot.set_xlim((xlo, xhi)) self.subplot.set_ylim((ylo, yhi)) else: self.toolbar.update() self.is_zoomed = False def _set_axis_labels(self): """ Set axis labels """ data = self.data2D # control axis labels from the panel itself yname, yunits = data.get_yaxis() if self.yaxis_label is not None: yname = self.yaxis_label yunits = self.yaxis_unit else: self.yaxis_label = yname self.yaxis_unit = yunits xname, xunits = data.get_xaxis() if self.xaxis_label is not None: xname = self.xaxis_label xunits = self.xaxis_unit else: self.xaxis_label = xname self.xaxis_unit = xunits self.xaxis(xname, xunits, self.xaxis_font, self.xaxis_color, self.xaxis_tick) self.yaxis(yname, yunits, self.yaxis_font, self.yaxis_color, self.yaxis_tick) def onContextMenu(self, event): """ 2D plot context menu :param event: wx context event """ ids = iter(self._menu_ids) slicerpop = PanelMenu() slicerpop.set_plots(self.plots) slicerpop.set_graph(self.graph) wx_id = ids.next() slicerpop.Append(wx_id, '&Save Image', 'Save image as png') wx.EVT_MENU(self, wx_id, self.onSaveImage) wx_id = ids.next() slicerpop.Append(wx_id, '&Print Image', 'Print image') wx.EVT_MENU(self, wx_id, self.onPrint) wx_id = ids.next() slicerpop.Append(wx_id, '&Copy to Clipboard', 'Copy to the clipboard') wx.EVT_MENU(self, wx_id, self.OnCopyFigureMenu) slicerpop.AppendSeparator() # saving data plot = self.data2D wx_id = ids.next() slicerpop.Append(wx_id, "&Data Info") wx.EVT_MENU(self, wx_id, self._onDataShow) wx_id = ids.next() slicerpop.Append(wx_id, "&Save as a File (DAT)") self.action_ids[str(wx_id)] = plot wx.EVT_MENU(self, wx_id, self._onSave) slicerpop.AppendSeparator() if len(self.data2D.detector) <= 1: item_list = self.parent.get_current_context_menu(self) if ((item_list is not None) and len(item_list) and self.data2D.name.split(" ")[0] != 'Residuals'): for item, wx_id in zip(item_list, [ids.next() for i in range(len(item_list))]): try: slicerpop.Append(wx_id, item[0], item[1]) wx.EVT_MENU(self, wx_id, item[2]) except Exception as exc: msg = "ModelPanel1D.onContextMenu: " msg += "bad menu item %s" % exc wx.PostEvent(self.parent, StatusEvent(status=msg)) slicerpop.AppendSeparator() wx_id = ids.next() slicerpop.Append(wx_id, '&Perform Circular Average') wx.EVT_MENU(self, wx_id, self.onCircular) \ # For Masked Data if not plot.mask.all(): wx_id = ids.next() slicerpop.Append(wx_id, '&Masked Circular Average') wx.EVT_MENU(self, wx_id, self.onMaskedCircular) wx_id = ids.next() slicerpop.Append(wx_id, '&Sector [Q View]') wx.EVT_MENU(self, wx_id, self.onSectorQ) wx_id = ids.next() slicerpop.Append(wx_id, '&Annulus [Phi View ]') wx.EVT_MENU(self, wx_id, self.onSectorPhi) wx_id = ids.next() slicerpop.Append(wx_id, '&Box Sum') wx.EVT_MENU(self, wx_id, self.onBoxSum) wx_id = ids.next() slicerpop.Append(wx_id, '&Box Averaging in Qx') wx.EVT_MENU(self, wx_id, self.onBoxavgX) wx_id = ids.next() slicerpop.Append(wx_id, '&Box Averaging in Qy') wx.EVT_MENU(self, wx_id, self.onBoxavgY) if self.slicer is not None: wx_id = ids.next() slicerpop.Append(wx_id, '&Clear Slicer') wx.EVT_MENU(self, wx_id, self.onClearSlicer) if self.slicer.__class__.__name__ != "BoxSum": wx_id = ids.next() name = '&Edit Slicer Parameters and Batch Slicing' slicerpop.Append(wx_id, name) wx.EVT_MENU(self, wx_id, self._onEditSlicer) slicerpop.AppendSeparator() wx_id = ids.next() slicerpop.Append(wx_id, '&Edit Graph Label', 'Edit Graph Label') wx.EVT_MENU(self, wx_id, self.onEditLabels) slicerpop.AppendSeparator() # ILL mod here wx_id = ids.next() slicerpop.Append(wx_id, '&Modify graph appearance', 'Modify graph appearance') wx.EVT_MENU(self, wx_id, self.modifyGraphAppearance) slicerpop.AppendSeparator() wx_id = ids.next() slicerpop.Append(wx_id, '&2D Color Map') wx.EVT_MENU(self, wx_id, self._onEditDetector) slicerpop.AppendSeparator() wx_id = ids.next() slicerpop.Append(wx_id, '&Toggle Linear/Log Scale') wx.EVT_MENU(self, wx_id, self._onToggleScale) slicerpop.AppendSeparator() wx_id = ids.next() slicerpop.Append(wx_id, '&Window Title') wx.EVT_MENU(self, wx_id, self.onChangeCaption) try: pos_evt = event.GetPosition() pos = self.ScreenToClient(pos_evt) except: pos_x, pos_y = self.toolbar.GetPositionTuple() pos = (pos_x, pos_y + 5) self.PopupMenu(slicerpop, pos) def onEditLabels(self, event): """ Edit legend label """ try: selected_plot = self.plots[self.graph.selected_plottable] except: selected_plot = self.plots[self.data2D.id] label = selected_plot.label dial = TextDialog(None, -1, 'Change Label', label) if dial.ShowModal() == wx.ID_OK: try: FONT = FontProperties() newlabel = dial.getText() font = FONT.copy() font.set_size(dial.getSize()) font.set_family(dial.getFamily()) font.set_style(dial.getStyle()) font.set_weight(dial.getWeight()) colour = dial.getColor() if len(newlabel) > 0: # update Label selected_plot.label = newlabel self.graph.title(newlabel) self.title_label = selected_plot.label self.title_font = font self.title_color = colour ## render the graph self.subplot.set_title(label=self.title_label, fontproperties=self.title_font, color=self.title_color) self._is_changed_legend_label = True self.subplot.figure.canvas.draw_idle() except: msg = "Add Text: Error. Check your property values..." logger.error(msg) if self.parent is not None: wx.PostEvent(self.parent, StatusEvent(status=msg)) dial.Destroy() def _onEditDetector(self, event): """ Allow to view and edits detector parameters :param event: wx.menu event """ from . import detector_dialog dialog = detector_dialog.DetectorDialog(self, -1, base=self.parent, reset_zmin_ctl=self.default_zmin_ctl, reset_zmax_ctl=self.default_zmax_ctl, cmap=self.cmap) ## info of current detector and data2D xnpts = len(self.data2D.x_bins) ynpts = len(self.data2D.y_bins) xmax = max(self.data2D.xmin, self.data2D.xmax) ymax = max(self.data2D.ymin, self.data2D.ymax) qmax = math.sqrt(math.pow(xmax, 2) + math.pow(ymax, 2)) ## set dialog window content dialog.setContent(xnpts=xnpts, ynpts=ynpts, qmax=qmax, beam=self.data2D.xmin, zmin=self.zmin_2D, zmax=self.zmax_2D) if dialog.ShowModal() == wx.ID_OK: evt = dialog.getContent() self.zmin_2D = evt.zmin self.zmax_2D = evt.zmax self.cmap = evt.cmap dialog.Destroy() ## Redraw the current image self.image(data=self.data2D.data, qx_data=self.data2D.qx_data, qy_data=self.data2D.qy_data, xmin=self.data2D.xmin, xmax=self.data2D.xmax, ymin=self.data2D.ymin, ymax=self.data2D.ymax, zmin=self.zmin_2D, zmax=self.zmax_2D, cmap=self.cmap, color=0, symbol=0, label=self.data2D.name) self.subplot.figure.canvas.draw_idle() def freeze_axes(self): """ """ self.axes_frozen = True def thaw_axes(self): """ """ self.axes_frozen = False def onMouseMotion(self, event): """ """ pass def onWheel(self, event): """ """ pass def update(self, draw=True): """ Respond to changes in the model by recalculating the profiles and resetting the widgets. """ self.draw_plot() def _getEmptySlicerEvent(self): """ create an empty slicervent """ return SlicerEvent(type=None, params=None, obj_class=None) def _onEVT_INTERNAL(self, event): """ Draw the slicer :param event: wx.lib.newevent (SlicerEvent) containing slicer parameter """ self._setSlicer(event.slicer) def _setSlicer(self, slicer): """ Clear the previous slicer and create a new one.Post an internal event. :param slicer: slicer class to create """ # Clear current slicer if self.slicer is not None: self.slicer.clear() # Create a new slicer self.slicer_z += 1 self.slicer = slicer(self, self.subplot, zorder=self.slicer_z) self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax) self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax) # Draw slicer self.update() self.slicer.update() msg = "Plotter2D._setSlicer %s" % self.slicer.__class__.__name__ wx.PostEvent(self.parent, StatusEvent(status=msg)) # Post slicer event event = self._getEmptySlicerEvent() event.type = self.slicer.__class__.__name__ event.obj_class = self.slicer.__class__ event.params = self.slicer.get_params() wx.PostEvent(self, event) def onMaskedCircular(self, event): """ perform circular averaging on Data2D with mask if it exists :param event: wx.menu event """ self.onCircular(event, True) def onCircular(self, event, ismask=False): """ perform circular averaging on Data2D :param event: wx.menu event """ # Find the best number of bins npt = math.sqrt(len(self.data2D.data[np.isfinite(self.data2D.data)])) npt = math.floor(npt) from sas.sascalc.dataloader.manipulations import CircularAverage # compute the maximum radius of data2D self.qmax = max(math.fabs(self.data2D.xmax), math.fabs(self.data2D.xmin)) self.ymax = max(math.fabs(self.data2D.ymax), math.fabs(self.data2D.ymin)) self.radius = math.sqrt(math.pow(self.qmax, 2) + math.pow(self.ymax, 2)) # Compute beam width bin_width = (self.qmax + self.qmax) / npt # Create data1D circular average of data2D Circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width) circ = Circle(self.data2D, ismask=ismask) from sas.sasgui.guiframe.dataFitting import Data1D if hasattr(circ, "dxl"): dxl = circ.dxl else: dxl = None if hasattr(circ, "dxw"): dxw = circ.dxw else: dxw = None new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx) new_plot.dxl = dxl new_plot.dxw = dxw new_plot.name = "Circ avg " + self.data2D.name new_plot.source = self.data2D.source # new_plot.info = self.data2D.info new_plot.interactive = True new_plot.detector = self.data2D.detector # If the data file does not tell us what the axes are, just assume... new_plot.xaxis("\\rm{Q}", "A^{-1}") if hasattr(self.data2D, "scale") and \ self.data2D.scale == 'linear': new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "normalized") else: new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") new_plot.group_id = "2daverage" + self.data2D.name new_plot.id = "Circ avg " + self.data2D.name new_plot.is_data = True self.parent.update_theory(data_id=self.data2D.id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=new_plot.name)) def _onEditSlicer(self, event): """ Is available only when a slicer is drawn.Create a dialog window where the user can enter value to reset slicer parameters. :param event: wx.menu event """ if self.slicer is not None: from .parameters_panel_slicer import SlicerParameterPanel dialog = SlicerParameterPanel(self, -1, "Slicer Parameters") dialog.set_slicer(self.slicer.__class__.__name__, self.slicer.get_params()) if dialog.ShowModal() == wx.ID_OK: dialog.Destroy() def onSectorQ(self, event): """ Perform sector averaging on Q and draw sector slicer """ from .SectorSlicer import SectorInteractor self.onClearSlicer(event) wx.PostEvent(self, InternalEvent(slicer=SectorInteractor)) def onSectorPhi(self, event): """ Perform sector averaging on Phi and draw annulus slicer """ from .AnnulusSlicer import AnnulusInteractor self.onClearSlicer(event) wx.PostEvent(self, InternalEvent(slicer=AnnulusInteractor)) def onBoxSum(self, event): """ """ from sas.sasgui.guiframe.gui_manager import MDIFrame from .boxSum import BoxSum self.onClearSlicer(event) self.slicer_z += 1 self.slicer = BoxSum(self, self.subplot, zorder=self.slicer_z) self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax) self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax) self.update() self.slicer.update() ## Value used to initially set the slicer panel params = self.slicer.get_params() ## Create a new panel to display results of summation of Data2D from .parameters_panel_boxsum import SlicerPanel win = MDIFrame(self.parent, None, 'None', (100, 200)) new_panel = SlicerPanel(parent=win, id=-1, base=self, type=self.slicer.__class__.__name__, params=params, style=wx.RAISED_BORDER) new_panel.window_caption = self.slicer.__class__.__name__ + " " + \ str(self.data2D.name) new_panel.window_name = self.slicer.__class__.__name__ + " " + \ str(self.data2D.name) ## Store a reference of the new created panel ## save the window_caption of the new panel in the current slicer self.slicer.set_panel_name(name=new_panel.window_caption) ## post slicer panel to guiframe to display it from sas.sasgui.guiframe.events import SlicerPanelEvent win.set_panel(new_panel) new_panel.frame = win wx.PostEvent(self.parent, SlicerPanelEvent(panel=new_panel, main_panel=self)) wx.CallAfter(new_panel.frame.Show) self.panel_slicer = new_panel def onBoxavgX(self, event): """ Perform 2D data averaging on Qx Create a new slicer . :param event: wx.menu event """ from .boxSlicer import BoxInteractorX self.onClearSlicer(event) wx.PostEvent(self, InternalEvent(slicer=BoxInteractorX)) def onBoxavgY(self, event): """ Perform 2D data averaging on Qy Create a new slicer . :param event: wx.menu event """ from .boxSlicer import BoxInteractorY self.onClearSlicer(event) wx.PostEvent(self, InternalEvent(slicer=BoxInteractorY)) def onClearSlicer(self, event): """ Clear the slicer on the plot """ if self.slicer is not None: self.slicer.clear() self.subplot.figure.canvas.draw() self.slicer = None # Post slicer None event event = self._getEmptySlicerEvent() wx.PostEvent(self, event) def _onSave(self, evt): """ Save a data set to a dat(text) file :param evt: Menu event """ event_id = str(evt.GetId()) if self.parent is not None: self._default_save_location = self.parent._default_save_location default_name = self.plots[self.graph.selected_plottable].label if default_name.count('.') > 0: default_name = default_name.split('.')[0] default_name += "_out" if event_id in self.action_ids: self.parent.save_data2d(self.data2D, default_name) def _onDataShow(self, evt): """ Show the data set in text :param evt: Menu event """ menu = evt.GetEventObject() event_id = evt.GetId() self.set_selected_from_menu(menu, event_id) data = self.plots[self.graph.selected_plottable] default_name = data.label if default_name.count('.') > 0: default_name = default_name.split('.')[0] if self.parent is not None: self.parent.show_data2d(data, default_name) def modifyGraphAppearance(self, e): self.graphApp = graphAppearance(self, 'Modify graph appearance', legend=False) self.graphApp.setDefaults(self.grid_on, self.legend_on, self.xaxis_label, self.yaxis_label, self.xaxis_unit, self.yaxis_unit, self.xaxis_font, self.yaxis_font, find_key(self.get_loc_label(), self.legendLoc), self.xcolor, self.ycolor, self.is_xtick, self.is_ytick) self.graphApp.Bind(wx.EVT_CLOSE, self.on_graphApp_close) def on_graphApp_close(self, e): """ Gets values from graph appearance dialog and sends them off to modify the plot """ self.onGridOnOff(self.graphApp.get_togglegrid()) self.xaxis_label = self.graphApp.get_xlab() self.yaxis_label = self.graphApp.get_ylab() self.xaxis_unit = self.graphApp.get_xunit() self.yaxis_unit = self.graphApp.get_yunit() self.xaxis_font = self.graphApp.get_xfont() self.yaxis_font = self.graphApp.get_yfont() self.is_xtick = self.graphApp.get_xtick_check() self.is_ytick = self.graphApp.get_ytick_check() if self.is_xtick: self.xaxis_tick = self.xaxis_font if self.is_ytick: self.yaxis_tick = self.yaxis_font self.xaxis(self.xaxis_label, self.xaxis_unit, self.graphApp.get_xfont(), self.graphApp.get_xcolor(), self.xaxis_tick) self.yaxis(self.yaxis_label, self.yaxis_unit, self.graphApp.get_yfont(), self.graphApp.get_ycolor(), self.yaxis_tick) self.graphApp.Destroy()