################################################################################
#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 wx
import sys
import math
import numpy as np
import logging
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 Plotter1D import ModelPanel1D
from sas.sasgui.plottools.toolbar import NavigationToolBar
from matplotlib.font_manager import FontProperties
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
[docs]def find_key(dic, val):
"""return the key of dictionary dic given the value"""
return [k for k, v in dic.iteritems() if v == val][0]
[docs]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
[docs] def on_plot_qrange(self, event=None):
"""
On Qmin Qmax vertical line event
"""
# Not implemented
if event is None:
return
event.Skip()
[docs] 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))
[docs] 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.keys():
#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)
[docs] 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
"""
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()
[docs] def freeze_axes(self):
"""
"""
self.axes_frozen = True
[docs] def thaw_axes(self):
"""
"""
self.axes_frozen = False
[docs] def onMouseMotion(self, event):
"""
"""
pass
[docs] def onWheel(self, event):
"""
"""
pass
[docs] 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)
[docs] def onMaskedCircular(self, event):
"""
perform circular averaging on Data2D with mask if it exists
:param event: wx.menu event
"""
self.onCircular(event, True)
[docs] 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()
[docs] 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))
[docs] 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))
[docs] 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
[docs] 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))
[docs] 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))
[docs] 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)
[docs] 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)
[docs] 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()