source: sasview/src/sas/sasgui/plottools/PlotPanel.py @ 45cab3c

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 45cab3c was 54a1cf8, checked in by Piotr Rozyczko <rozyczko@…>, 8 years ago

Fix units on Kratky plot

  • Property mode set to 100644
File size: 73.3 KB
Line 
1"""
2    Plot panel.
3"""
4import logging
5import traceback
6import wx
7# Try a normal import first
8# If it fails, try specifying a version
9import matplotlib
10matplotlib.interactive(False)
11#Use the WxAgg back end. The Wx one takes too long to render
12matplotlib.use('WXAgg')
13from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
14from matplotlib.figure import Figure
15import os
16import transform
17from plottables import Data1D
18#TODO: make the plottables interactive
19from binder import BindArtist
20from matplotlib.font_manager import FontProperties
21DEBUG = False
22
23from plottables import Graph
24from TextDialog import TextDialog
25from LabelDialog import LabelDialog
26import operator
27
28import math
29import pylab
30DEFAULT_CMAP = pylab.cm.jet
31import copy
32import numpy
33
34from sas.sasgui.guiframe.events import StatusEvent
35from .toolbar import NavigationToolBar, PlotPrintout, bind
36
37def show_tree(obj, d=0):
38    """Handy function for displaying a tree of graph objects"""
39    print "%s%s" % ("-"*d, obj.__class__.__name__)
40    if 'get_children' in dir(obj):
41        for a in obj.get_children(): show_tree(a, d + 1)
42
43from convert_units import convert_unit
44
45
46def _rescale(lo, hi, step, pt=None, bal=None, scale='linear'):
47    """
48        Rescale (lo,hi) by step, returning the new (lo,hi)
49        The scaling is centered on pt, with positive values of step
50        driving lo/hi away from pt and negative values pulling them in.
51        If bal is given instead of point, it is already in [0,1] coordinates.
52
53        This is a helper function for step-based zooming.
54
55    """
56    # Convert values into the correct scale for a linear transformation
57    # TODO: use proper scale transformers
58    loprev = lo
59    hiprev = hi
60    if scale == 'log':
61        assert lo > 0
62        if lo > 0:
63            lo = math.log10(lo)
64        if hi > 0:
65            hi = math.log10(hi)
66        if pt is not None:
67            pt = math.log10(pt)
68
69    # Compute delta from axis range * %, or 1-% if persent is negative
70    if step > 0:
71        delta = float(hi - lo) * step / 100
72    else:
73        delta = float(hi - lo) * step / (100 - step)
74
75    # Add scale factor proportionally to the lo and hi values,
76    # preserving the
77    # point under the mouse
78    if bal is None:
79        bal = float(pt - lo) / (hi - lo)
80    lo = lo - (bal * delta)
81    hi = hi + (1 - bal) * delta
82
83    # Convert transformed values back to the original scale
84    if scale == 'log':
85        if (lo <= -250) or (hi >= 250):
86            lo = loprev
87            hi = hiprev
88        else:
89            lo, hi = math.pow(10., lo), math.pow(10., hi)
90    return (lo, hi)
91
92
93class PlotPanel(wx.Panel):
94    """
95    The PlotPanel has a Figure and a Canvas. OnSize events simply set a
96    flag, and the actually redrawing of the
97    figure is triggered by an Idle event.
98    """
99    def __init__(self, parent, id=-1, xtransform=None,
100                 ytransform=None, scale='log_{10}',
101                 color=None, dpi=None, **kwargs):
102        """
103        """
104        wx.Panel.__init__(self, parent, id=id, **kwargs)
105        self.parent = parent
106        if hasattr(parent, "parent"):
107            self.parent = self.parent.parent
108        self.dimension = 1
109        self.gotLegend = 0  # to begin, legend is not picked.
110        self.legend_pos_loc = None
111        self.legend = None
112        self.line_collections_list = []
113        self.figure = Figure(None, dpi, linewidth=2.0)
114        self.color = '#b3b3b3'
115        from canvas import FigureCanvas
116        self.canvas = FigureCanvas(self, -1, self.figure)
117        self.SetColor(color)
118        self._resizeflag = True
119        self._SetSize()
120        self.subplot = self.figure.add_subplot(111)
121        self.figure.subplots_adjust(left=0.2, bottom=.2)
122        self.yscale = 'linear'
123        self.xscale = 'linear'
124        self.sizer = wx.BoxSizer(wx.VERTICAL)
125        self.sizer.Add(self.canvas, 1, wx.EXPAND)
126        #add toolbar
127        self.enable_toolbar = True
128        self.toolbar = None
129        self.add_toolbar()
130        self.SetSizer(self.sizer)
131
132        # Graph object to manage the plottables
133        self.graph = Graph()
134
135        #Boolean value to keep track of whether current legend is
136        #visible or not
137        self.legend_on = True
138        self.grid_on = False
139        #Location of legend, default is 0 or 'best'
140        self.legendLoc = 0
141        self.position = None
142        self._loc_labels = self.get_loc_label()
143
144        self.Bind(wx.EVT_CONTEXT_MENU, self.onContextMenu)
145
146        # Define some constants
147        self.colorlist = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
148        self.symbollist = ['o', 'x', '^', 'v', '<', '>', '+',
149                           's', 'd', 'D', 'h', 'H', 'p', '-']
150
151        #List of texts currently on the plot
152        self.textList = []
153        #User scale
154        if xtransform != None:
155            self.xLabel = xtransform
156        else:
157            self.xLabel = "log10(x)"
158        if ytransform != None:
159            self.yLabel = ytransform
160        else:
161            self.yLabel = "log10(y)"
162        self.viewModel = "--"
163        # keep track if the previous transformation of x
164        # and y in Property dialog
165        self.prevXtrans = "log10(x)"
166        self.prevYtrans = "log10(y)"
167        self.scroll_id = self.canvas.mpl_connect('scroll_event', self.onWheel)
168        #taking care of dragging
169        self.motion_id = self.canvas.mpl_connect('motion_notify_event',
170                                                 self.onMouseMotion)
171        self.press_id = self.canvas.mpl_connect('button_press_event',
172                                                self.onLeftDown)
173        self.pick_id = self.canvas.mpl_connect('pick_event', self.onPick)
174        self.release_id = self.canvas.mpl_connect('button_release_event',
175                                                  self.onLeftUp)
176
177        wx.EVT_RIGHT_DOWN(self, self.onLeftDown)
178        # to turn axis off whenn resizing the panel
179        self.resizing = False
180
181        self.leftdown = False
182        self.leftup = False
183        self.mousemotion = False
184        self.axes = [self.subplot]
185        ## Fit dialog
186        self._fit_dialog = None
187        # Interactor
188        self.connect = BindArtist(self.subplot.figure)
189        #self.selected_plottable = None
190
191        # new data for the fit
192        self.fit_result = Data1D(x=[], y=[], dy=None)
193        self.fit_result.symbol = 13
194        #self.fit_result = Data1D(x=[], y=[],dx=None, dy=None)
195        self.fit_result.name = "Fit"
196        # For fit Dialog initial display
197        self.xmin = 0.0
198        self.xmax = 0.0
199        self.xminView = 0.0
200        self.xmaxView = 0.0
201        self._scale_xlo = None
202        self._scale_xhi = None
203        self._scale_ylo = None
204        self._scale_yhi = None
205        self.Avalue = None
206        self.Bvalue = None
207        self.ErrAvalue = None
208        self.ErrBvalue = None
209        self.Chivalue = None
210
211        # for 2D scale
212        if scale != 'linear':
213            scale = 'log_{10}'
214        self.scale = scale
215        self.data = None
216        self.qx_data = None
217        self.qy_data = None
218        self.xmin_2D = None
219        self.xmax_2D = None
220        self.ymin_2D = None
221        self.ymax_2D = None
222        ## store reference to the current plotted vmin and vmax of plotted image
223        ##z range in linear scale
224        self.zmin_2D = None
225        self.zmax_2D = None
226
227        #index array
228        self.index_x = None
229        self.index_y = None
230
231        #number of bins
232        self.x_bins = None
233        self.y_bins = None
234
235        ## default color map
236        self.cmap = DEFAULT_CMAP
237
238        # Dragging info
239        self.begDrag = False
240        self.xInit = None
241        self.yInit = None
242        self.xFinal = None
243        self.yFinal = None
244
245        #axes properties
246        self.xaxis_font = None
247        self.xaxis_label = None
248        self.xaxis_unit = None
249        self.xaxis_color = 'black'
250        self.xaxis_tick = None
251        self.yaxis_font = None
252        self.yaxis_label = None
253        self.yaxis_unit = None
254        self.yaxis_color = 'black'
255        self.yaxis_tick = None
256
257        # check if zoomed.
258        self.is_zoomed = False
259        # Plottables
260        self.plots = {}
261
262        # Default locations
263        self._default_save_location = os.getcwd()
264        # let canvas know about axes
265        self.canvas.set_panel(self)
266        self.ly = None
267        self.q_ctrl = None
268        #Bind focus to change the border color
269        self.canvas.Bind(wx.EVT_SET_FOCUS, self.on_set_focus)
270        self.canvas.Bind(wx.EVT_KILL_FOCUS, self.on_kill_focus)
271
272    def _SetInitialSize(self,):
273        """
274        """
275        pixels = self.parent.GetClientSize()
276        self.canvas.SetSize(pixels)
277        self.figure.set_size_inches(pixels[0] / self.figure.get_dpi(),
278                                    pixels[1] / self.figure.get_dpi(), forward=True)
279
280    def On_Paint(self, event):
281        """
282        """
283        self.canvas.SetBackgroundColour(self.color)
284
285    def on_set_focus(self, event):
286        """
287        Send to the parenet the current panel on focus
288        """
289        # light blue
290        self.color = '#0099f7'
291        self.figure.set_edgecolor(self.color)
292        if self.parent and self.window_caption:
293            self.parent.send_focus_to_datapanel(self.window_caption)
294        self.draw()
295
296    def on_kill_focus(self, event):
297        """
298        Reset the panel color
299        """
300        # light grey
301        self.color = '#b3b3b3'
302        self.figure.set_edgecolor(self.color)
303        self.draw()
304
305    def set_resizing(self, resizing=False):
306        """
307        Set the resizing (True/False)
308        """
309        pass  # Not implemented
310
311    def schedule_full_draw(self, func='append'):
312        """
313        Put self in schedule to full redraw list
314        """
315        pass  # Not implemented
316
317    def add_toolbar(self):
318        """
319        add toolbar
320        """
321        self.enable_toolbar = True
322        self.toolbar = NavigationToolBar(parent=self, canvas=self.canvas)
323        bind(self.toolbar, wx.EVT_TOOL, self.onResetGraph, id=self.toolbar._NTB2_RESET)
324        bind(self.toolbar, wx.EVT_TOOL, self.onContextMenu, id=self.toolbar._NTB2_HOME)
325        self.toolbar.Realize()
326        ## The 'SetToolBar()' is not working on MAC: JHC
327        #if IS_MAC:
328        # Mac platform (OSX 10.3, MacPython) does not seem to cope with
329        # having a toolbar in a sizer. This work-around gets the buttons
330        # back, but at the expense of having the toolbar at the top
331        #self.SetToolBar(self.toolbar)
332        #else:
333        # On Windows platform, default window size is incorrect, so set
334        # toolbar width to figure width.
335        tw, th = self.toolbar.GetSizeTuple()
336        fw, fh = self.canvas.GetSizeTuple()
337        # By adding toolbar in sizer, we are able to put it at the bottom
338        # of the frame - so appearance is closer to GTK version.
339        # As noted above, doesn't work for Mac.
340        self.toolbar.SetSize(wx.Size(fw, th))
341        self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
342
343        # update the axes menu on the toolbar
344        self.toolbar.update()
345
346    def onLeftDown(self, event):
347        """
348        left button down and ready to drag
349        """
350        # Check that the LEFT button was pressed
351        if event.button == 1:
352            self.leftdown = True
353            ax = event.inaxes
354            if ax != None:
355                self.xInit, self.yInit = event.xdata, event.ydata
356                try:
357                    pos_x = float(event.xdata)  # / size_x
358                    pos_y = float(event.ydata)  # / size_y
359                    pos_x = "%8.3g" % pos_x
360                    pos_y = "%8.3g" % pos_y
361                    self.position = str(pos_x), str(pos_y)
362                    wx.PostEvent(self.parent, StatusEvent(status=self.position))
363                except:
364                    self.position = None
365
366    def onLeftUp(self, event):
367        """
368        Dragging is done
369        """
370        # Check that the LEFT button was released
371        if event.button == 1:
372            self.leftdown = False
373            self.mousemotion = False
374            self.leftup = True
375
376        #release the legend
377        if self.gotLegend == 1:
378            self.gotLegend = 0
379            self.set_legend_alpha(1)
380
381    def set_legend_alpha(self, alpha=1):
382        """
383        Set legend alpha
384        """
385        if self.legend != None:
386            self.legend.legendPatch.set_alpha(alpha)
387
388    def onPick(self, event):
389        """
390        On pick legend
391        """
392        legend = self.legend
393        if event.artist == legend:
394            #gets the box of the legend.
395            bbox = self.legend.get_window_extent()
396            #get mouse coordinates at time of pick.
397            self.mouse_x = event.mouseevent.x
398            self.mouse_y = event.mouseevent.y
399            #get legend coordinates at time of pick.
400            self.legend_x = bbox.xmin
401            self.legend_y = bbox.ymin
402            #indicates we picked up the legend.
403            self.gotLegend = 1
404            self.set_legend_alpha(0.5)
405
406    def _on_legend_motion(self, event):
407        """
408        On legend in motion
409        """
410        ax = event.inaxes
411        if ax == None:
412            return
413        # Event occurred inside a plotting area
414        lo_x, hi_x = ax.get_xlim()
415        lo_y, hi_y = ax.get_ylim()
416        # How much the mouse moved.
417        x = mouse_diff_x = self.mouse_x - event.x
418        y = mouse_diff_y = self.mouse_y - event.y
419        # Put back inside
420        if x < lo_x:
421            x = lo_x
422        if x > hi_x:
423            x = hi_x
424        if y < lo_y:
425            y = lo_y
426        if y > hi_y:
427            y = hi_y
428        # Move the legend from its previous location by that same amount
429        loc_in_canvas = self.legend_x - mouse_diff_x, \
430                        self.legend_y - mouse_diff_y
431        # Transform into legend coordinate system
432        trans_axes = self.legend.parent.transAxes.inverted()
433        loc_in_norm_axes = trans_axes.transform_point(loc_in_canvas)
434        self.legend_pos_loc = tuple(loc_in_norm_axes)
435        self.legend._loc = self.legend_pos_loc
436        self.resizing = True
437        self.canvas.set_resizing(self.resizing)
438        self.canvas.draw()
439
440    def onMouseMotion(self, event):
441        """
442        check if the left button is press and the mouse in moving.
443        computer delta for x and y coordinates and then calls draghelper
444        to perform the drag
445        """
446        self.cusor_line(event)
447        if self.gotLegend == 1:
448            self._on_legend_motion(event)
449            return
450        if self.enable_toolbar:
451            #Disable dragging without the toolbar to allow zooming with toolbar
452            return
453        self.mousemotion = True
454        if self.leftdown == True and self.mousemotion == True:
455            ax = event.inaxes
456            if ax != None:  # the dragging is perform inside the figure
457                self.xFinal, self.yFinal = event.xdata, event.ydata
458                # Check whether this is the first point
459                if self.xInit == None:
460                    self.xInit = self.xFinal
461                    self.yInit = self.yFinal
462
463                xdelta = self.xFinal - self.xInit
464                ydelta = self.yFinal - self.yInit
465
466                if self.xscale == 'log':
467                    xdelta = math.log10(self.xFinal) - math.log10(self.xInit)
468                if self.yscale == 'log':
469                    ydelta = math.log10(self.yFinal) - math.log10(self.yInit)
470                self._dragHelper(xdelta, ydelta)
471            else:  # no dragging is perform elsewhere
472                self._dragHelper(0, 0)
473
474    def cusor_line(self, event):
475        """
476        """
477        pass
478
479    def _offset_graph(self):
480        """
481        Zoom and offset the graph to the last known settings
482        """
483        for ax in self.axes:
484            if self._scale_xhi is not None and self._scale_xlo is not None:
485                ax.set_xlim(self._scale_xlo, self._scale_xhi)
486            if self._scale_yhi is not None and self._scale_ylo is not None:
487                ax.set_ylim(self._scale_ylo, self._scale_yhi)
488
489    def _dragHelper(self, xdelta, ydelta):
490        """
491        dragging occurs here
492        """
493        # Event occurred inside a plotting area
494        for ax in self.axes:
495            lo, hi = ax.get_xlim()
496            newlo, newhi = lo - xdelta, hi - xdelta
497            if self.xscale == 'log':
498                if lo > 0:
499                    newlo = math.log10(lo) - xdelta
500                if hi > 0:
501                    newhi = math.log10(hi) - xdelta
502            if self.xscale == 'log':
503                self._scale_xlo = math.pow(10, newlo)
504                self._scale_xhi = math.pow(10, newhi)
505                ax.set_xlim(math.pow(10, newlo), math.pow(10, newhi))
506            else:
507                self._scale_xlo = newlo
508                self._scale_xhi = newhi
509                ax.set_xlim(newlo, newhi)
510
511            lo, hi = ax.get_ylim()
512            newlo, newhi = lo - ydelta, hi - ydelta
513            if self.yscale == 'log':
514                if lo > 0:
515                    newlo = math.log10(lo) - ydelta
516                if hi > 0:
517                    newhi = math.log10(hi) - ydelta
518            if  self.yscale == 'log':
519                self._scale_ylo = math.pow(10, newlo)
520                self._scale_yhi = math.pow(10, newhi)
521                ax.set_ylim(math.pow(10, newlo), math.pow(10, newhi))
522            else:
523                self._scale_ylo = newlo
524                self._scale_yhi = newhi
525                ax.set_ylim(newlo, newhi)
526        self.canvas.draw_idle()
527
528    def resetFitView(self):
529        """
530        For fit Dialog initial display
531        """
532        self.xmin = 0.0
533        self.xmax = 0.0
534        self.xminView = 0.0
535        self.xmaxView = 0.0
536        self._scale_xlo = None
537        self._scale_xhi = None
538        self._scale_ylo = None
539        self._scale_yhi = None
540        self.Avalue = None
541        self.Bvalue = None
542        self.ErrAvalue = None
543        self.ErrBvalue = None
544        self.Chivalue = None
545
546    def onWheel(self, event):
547        """
548        Process mouse wheel as zoom events
549
550        :param event: Wheel event
551
552        """
553        ax = event.inaxes
554        step = event.step
555
556        if ax != None:
557            # Event occurred inside a plotting area
558            lo, hi = ax.get_xlim()
559            lo, hi = _rescale(lo, hi, step,
560                              pt=event.xdata, scale=ax.get_xscale())
561            if not self.xscale == 'log' or lo > 0:
562                self._scale_xlo = lo
563                self._scale_xhi = hi
564                ax.set_xlim((lo, hi))
565
566            lo, hi = ax.get_ylim()
567            lo, hi = _rescale(lo, hi, step, pt=event.ydata,
568                              scale=ax.get_yscale())
569            if not self.yscale == 'log' or lo > 0:
570                self._scale_ylo = lo
571                self._scale_yhi = hi
572                ax.set_ylim((lo, hi))
573        else:
574            # Check if zoom happens in the axes
575            xdata, ydata = None, None
576            x, y = event.x, event.y
577
578            for ax in self.axes:
579                insidex, _ = ax.xaxis.contains(event)
580                if insidex:
581                    xdata, _ = ax.transAxes.inverted().transform_point((x, y))
582                insidey, _ = ax.yaxis.contains(event)
583                if insidey:
584                    _, ydata = ax.transAxes.inverted().transform_point((x, y))
585            if xdata is not None:
586                lo, hi = ax.get_xlim()
587                lo, hi = _rescale(lo, hi, step,
588                                  bal=xdata, scale=ax.get_xscale())
589                if not self.xscale == 'log' or lo > 0:
590                    self._scale_xlo = lo
591                    self._scale_xhi = hi
592                    ax.set_xlim((lo, hi))
593            if ydata is not None:
594                lo, hi = ax.get_ylim()
595                lo, hi = _rescale(lo, hi, step, bal=ydata,
596                                  scale=ax.get_yscale())
597                if not self.yscale == 'log' or lo > 0:
598                    self._scale_ylo = lo
599                    self._scale_yhi = hi
600                    ax.set_ylim((lo, hi))
601        self.canvas.draw_idle()
602
603    def returnTrans(self):
604        """
605        Return values and labels used by Fit Dialog
606        """
607        return self.xLabel, self.yLabel, self.Avalue, self.Bvalue, \
608                self.ErrAvalue, self.ErrBvalue, self.Chivalue
609
610    def setTrans(self, xtrans, ytrans):
611        """
612
613        :param xtrans: set x transformation on Property dialog
614        :param ytrans: set y transformation on Property dialog
615
616        """
617        self.prevXtrans = xtrans
618        self.prevYtrans = ytrans
619
620    def onFitting(self, event):
621        """
622        when clicking on linear Fit on context menu , display Fitting Dialog
623        """
624        plot_dict = {}
625        menu = event.GetEventObject()
626        event_id = event.GetId()
627        self.set_selected_from_menu(menu, event_id)
628        plotlist = self.graph.returnPlottable()
629        if self.graph.selected_plottable is not None:
630            for item in plotlist:
631                if item.id == self.graph.selected_plottable:
632                    plot_dict[item] = plotlist[item]
633        else:
634            plot_dict = plotlist
635        from fitDialog import LinearFit
636
637        if len(plot_dict.keys()) > 0:
638            first_item = plot_dict.keys()[0]
639            dlg = LinearFit(parent=None, plottable=first_item,
640                            push_data=self.onFitDisplay,
641                            transform=self.returnTrans,
642                            title='Linear Fit')
643
644            if (self.xmin != 0.0)and (self.xmax != 0.0)\
645                and(self.xminView != 0.0)and (self.xmaxView != 0.0):
646                dlg.setFitRange(self.xminView, self.xmaxView,
647                                self.xmin, self.xmax)
648            # It would be nice for this to NOT be modal (i.e. Show).
649            # Not sure about other ramifications - for example
650            # if a second linear fit is started before the first is closed.
651            # consider for future - being able to work on the plot while
652            # seing the fit values would be very nice  -- PDB 7/10/16
653            dlg.ShowModal()
654
655    def set_selected_from_menu(self, menu, id):
656        """
657        Set selected_plottable from context menu selection
658
659        :param menu: context menu item
660        :param id: menu item id
661        """
662        if len(self.plots) < 1:
663            return
664        name = menu.GetHelpString(id)
665        for plot in self.plots.values():
666            if plot.name == name:
667                self.graph.selected_plottable = plot.id
668                break
669
670    def linear_plottable_fit(self, plot):
671        """
672            when clicking on linear Fit on context menu, display Fitting Dialog
673
674            :param plot: PlotPanel owning the graph
675
676        """
677        from fitDialog import LinearFit
678        if self._fit_dialog is not None:
679            return
680        self._fit_dialog = LinearFit(None, plot, self.onFitDisplay,
681                                     self.returnTrans, -1, 'Linear Fit')
682        # Set the zoom area
683        if self._scale_xhi is not None and self._scale_xlo is not None:
684            self._fit_dialog.set_fit_region(self._scale_xlo, self._scale_xhi)
685        # Register the close event
686        self._fit_dialog.register_close(self._linear_fit_close)
687        # Show a non-model dialog
688        self._fit_dialog.Show()
689
690    def _linear_fit_close(self):
691        """
692        A fit dialog was closed
693        """
694        self._fit_dialog = None
695
696    def _onProperties(self, event):
697        """
698        when clicking on Properties on context menu ,
699        The Property dialog is displayed
700        The user selects a transformation for x or y value and
701        a new plot is displayed
702        """
703        if self._fit_dialog is not None:
704            self._fit_dialog.Destroy()
705            self._fit_dialog = None
706        plot_list = self.graph.returnPlottable()
707        if len(plot_list.keys()) > 0:
708            first_item = plot_list.keys()[0]
709            if first_item.x != []:
710                from PropertyDialog import Properties
711                dial = Properties(self, -1, 'Properties')
712                dial.setValues(self.prevXtrans, self.prevYtrans, self.viewModel)
713                if dial.ShowModal() == wx.ID_OK:
714                    self.xLabel, self.yLabel, self.viewModel = dial.getValues()
715                    self._onEVT_FUNC_PROPERTY()
716                dial.Destroy()
717
718    def set_yscale(self, scale='linear'):
719        """
720        Set the scale on Y-axis
721
722        :param scale: the scale of y-axis
723
724        """
725        self.subplot.set_yscale(scale, nonposy='clip')
726        self.yscale = scale
727
728    def get_yscale(self):
729        """
730
731        :return: Y-axis scale
732
733        """
734        return self.yscale
735
736    def set_xscale(self, scale='linear'):
737        """
738        Set the scale on x-axis
739
740        :param scale: the scale of x-axis
741
742        """
743        self.subplot.set_xscale(scale)
744        self.xscale = scale
745
746    def get_xscale(self):
747        """
748
749        :return: x-axis scale
750
751        """
752        return self.xscale
753
754    def SetColor(self, rgbtuple):
755        """
756        Set figure and canvas colours to be the same
757
758        """
759        if not rgbtuple:
760            rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
761        col = [c / 255.0 for c in rgbtuple]
762        self.figure.set_facecolor(col)
763        self.figure.set_edgecolor(self.color)
764        self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple))
765
766    def _onSize(self, event):
767        """
768        """
769        self._resizeflag = True
770
771    def _onIdle(self, evt):
772        """
773        """
774        if self._resizeflag:
775            self._resizeflag = False
776            self._SetSize()
777            self.draw()
778
779    def _SetSize(self, pixels=None):
780        """
781        This method can be called to force the Plot to be a desired size,
782         which defaults to the ClientSize of the panel
783
784        """
785        if not pixels:
786            pixels = tuple(self.GetClientSize())
787        self.canvas.SetSize(pixels)
788        self.figure.set_size_inches(float(pixels[0]) / self.figure.get_dpi(),
789                                    float(pixels[1]) / self.figure.get_dpi())
790
791    def draw(self):
792        """
793        Where the actual drawing happens
794
795        """
796        self.figure.canvas.draw_idle()
797
798    def legend_picker(self, legend, event):
799        """
800            Pick up the legend patch
801        """
802        return self.legend.legendPatch.contains(event)
803
804    def get_loc_label(self):
805        """
806        Associates label to a specific legend location
807        """
808        _labels = {}
809        i = 0
810        _labels['best'] = i
811        i += 1
812        _labels['upper right'] = i
813        i += 1
814        _labels['upper left'] = i
815        i += 1
816        _labels['lower left'] = i
817        i += 1
818        _labels['lower right'] = i
819        i += 1
820        _labels['right'] = i
821        i += 1
822        _labels['center left'] = i
823        i += 1
824        _labels['center right'] = i
825        i += 1
826        _labels['lower center'] = i
827        i += 1
828        _labels['upper center'] = i
829        i += 1
830        _labels['center'] = i
831        return _labels
832
833    def onSaveImage(self, evt):
834        """
835        Implement save image
836        """
837        self.toolbar.save_figure(evt)
838
839    def onContextMenu(self, event):
840        """
841        Default context menu for a plot panel
842
843        """
844        # Slicer plot popup menu
845        wx_id = wx.NewId()
846        slicerpop = wx.Menu()
847        slicerpop.Append(wx_id, '&Save image', 'Save image as PNG')
848        wx.EVT_MENU(self, wx_id, self.onSaveImage)
849
850        wx_id = wx.NewId()
851        slicerpop.Append(wx_id, '&Printer setup', 'Set image size')
852        wx.EVT_MENU(self, wx_id, self.onPrinterSetup)
853
854        wx_id = wx.NewId()
855        slicerpop.Append(wx_id, '&Print image', 'Print image ')
856        wx.EVT_MENU(self, wx_id, self.onPrint)
857
858        wx_id = wx.NewId()
859        slicerpop.Append(wx_id, '&Copy', 'Copy to the clipboard')
860        wx.EVT_MENU(self, wx_id, self.OnCopyFigureMenu)
861
862        wx_id = wx.NewId()
863        slicerpop.AppendSeparator()
864        slicerpop.Append(wx_id, '&Properties')
865        wx.EVT_MENU(self, wx_id, self._onProperties)
866
867        wx_id = wx.NewId()
868        slicerpop.AppendSeparator()
869        slicerpop.Append(wx_id, '&Linear Fit')
870        wx.EVT_MENU(self, wx_id, self.onFitting)
871
872        wx_id = wx.NewId()
873        slicerpop.AppendSeparator()
874        slicerpop.Append(wx_id, '&Toggle Legend On/Off', 'Toggle Legend On/Off')
875        wx.EVT_MENU(self, wx_id, self.onLegend)
876
877        loc_menu = wx.Menu()
878        for label in self._loc_labels:
879            wx_id = wx.NewId()
880            loc_menu.Append(wx_id, str(label), str(label))
881            wx.EVT_MENU(self, wx_id, self.onChangeLegendLoc)
882        wx_id = wx.NewId()
883        slicerpop.AppendMenu(wx_id, '&Modify Legend Location', loc_menu)
884
885        wx_id = wx.NewId()
886        slicerpop.Append(wx_id, '&Modify Y Axis Label')
887        wx.EVT_MENU(self, wx_id, self._on_yaxis_label)
888        wx_id = wx.NewId()
889        slicerpop.Append(wx_id, '&Modify X Axis Label')
890        wx.EVT_MENU(self, wx_id, self._on_xaxis_label)
891
892        try:
893            # mouse event
894            pos_evt = event.GetPosition()
895            pos = self.ScreenToClient(pos_evt)
896        except:
897            # toolbar event
898            pos_x, pos_y = self.toolbar.GetPositionTuple()
899            pos = (pos_x, pos_y + 5)
900
901        self.PopupMenu(slicerpop, pos)
902
903    def onToolContextMenu(self, event):
904        """
905        ContextMenu from toolbar
906
907        :param event: toolbar event
908        """
909        # reset postion
910        self.position = None
911        if self.graph.selected_plottable != None:
912            self.graph.selected_plottable = None
913
914        self.onContextMenu(event)
915
916# modified kieranrcampbell ILL june2012
917    def onLegend(self, legOnOff):
918        """
919        Toggles whether legend is visible/not visible
920        """
921        self.legend_on = legOnOff
922        if not self.legend_on:
923            for ax in self.axes:
924                self.remove_legend(ax)
925        else:
926            # sort them by labels
927            handles, labels = self.subplot.get_legend_handles_labels()
928            hl = sorted(zip(handles, labels),
929                        key=operator.itemgetter(1))
930            handles2, labels2 = zip(*hl)
931            self.line_collections_list = handles2
932            self.legend = self.subplot.legend(handles2, labels2,
933                                              prop=FontProperties(size=10),
934                                              loc=self.legendLoc)
935            if self.legend != None:
936                self.legend.set_picker(self.legend_picker)
937                self.legend.set_axes(self.subplot)
938                self.legend.set_zorder(20)
939
940        self.subplot.figure.canvas.draw_idle()
941
942    def onChangeLegendLoc(self, event):
943        """
944        Changes legend loc based on user input
945        """
946        menu = event.GetEventObject()
947        label = menu.GetLabel(event.GetId())
948        self.ChangeLegendLoc(label)
949
950    def ChangeLegendLoc(self, label):
951        """
952        Changes legend loc based on user input
953        """
954        self.legendLoc = label
955        self.legend_pos_loc = None
956        # sort them by labels
957        handles, labels = self.subplot.get_legend_handles_labels()
958        hl = sorted(zip(handles, labels),
959                    key=operator.itemgetter(1))
960        handles2, labels2 = zip(*hl)
961        self.line_collections_list = handles2
962        self.legend = self.subplot.legend(handles2, labels2,
963                                          prop=FontProperties(size=10),
964                                          loc=self.legendLoc)
965        if self.legend != None:
966            self.legend.set_picker(self.legend_picker)
967            self.legend.set_axes(self.subplot)
968            self.legend.set_zorder(20)
969        self.subplot.figure.canvas.draw_idle()
970
971    def remove_legend(self, ax=None):
972        """
973        Remove legend for ax or the current axes.
974        """
975        from pylab import gca
976        if ax is None:
977            ax = gca()
978        ax.legend_ = None
979
980    def _on_addtext(self, event):
981        """
982        Allows you to add text to the plot
983        """
984        pos_x = 0
985        pos_y = 0
986        if self.position != None:
987            pos_x, pos_y = self.position
988        else:
989            pos_x, pos_y = 0.01, 1.00
990
991        textdial = TextDialog(None, -1, 'Add Custom Text')
992        if textdial.ShowModal() == wx.ID_OK:
993            try:
994                FONT = FontProperties()
995                label = textdial.getText()
996                xpos = pos_x
997                ypos = pos_y
998                font = FONT.copy()
999                font.set_size(textdial.getSize())
1000                font.set_family(textdial.getFamily())
1001                font.set_style(textdial.getStyle())
1002                font.set_weight(textdial.getWeight())
1003                colour = textdial.getColor()
1004                if len(label) > 0 and xpos > 0 and ypos > 0:
1005                    new_text = self.subplot.text(str(xpos), str(ypos), label,
1006                                                 fontproperties=font,
1007                                                 color=colour)
1008                    self.textList.append(new_text)
1009                    self.subplot.figure.canvas.draw_idle()
1010            except:
1011                if self.parent != None:
1012                    msg = "Add Text: Error. Check your property values..."
1013                    wx.PostEvent(self.parent, StatusEvent(status=msg))
1014                else:
1015                    raise
1016        textdial.Destroy()
1017        #Have a pop up box come up for user to type in the
1018        #text that they want to add...then create text Plottable
1019        #based on this and plot it at user designated coordinates
1020
1021    def onGridOnOff(self, gridon_off):
1022        """
1023        Allows ON/OFF Grid
1024        """
1025        self.grid_on = gridon_off
1026
1027        self.subplot.figure.canvas.draw_idle()
1028
1029    def _on_xaxis_label(self, event):
1030        """
1031        Allows you to add text to the plot
1032        """
1033        xaxis_label, xaxis_unit, xaxis_font, xaxis_color, \
1034                     is_ok, is_tick = self._on_axis_label(axis='x')
1035        if not is_ok:
1036            return
1037
1038        self.xaxis_label = xaxis_label
1039        self.xaxis_unit = xaxis_unit
1040        self.xaxis_font = xaxis_font
1041        self.xaxis_color = xaxis_color
1042        if is_tick:
1043            self.xaxis_tick = xaxis_font
1044
1045        if self.data != None:
1046            # 2D
1047            self.xaxis(self.xaxis_label, self.xaxis_unit, \
1048                        self.xaxis_font, self.xaxis_color, self.xaxis_tick)
1049            self.subplot.figure.canvas.draw_idle()
1050        else:
1051            # 1D
1052            self._check_zoom_plot()
1053
1054    def _check_zoom_plot(self):
1055        """
1056        Check the zoom range and plot (1D only)
1057        """
1058        xlo, xhi = self.subplot.get_xlim()
1059        ylo, yhi = self.subplot.get_ylim()
1060        ## Set the view scale for all plots
1061        self._onEVT_FUNC_PROPERTY(False)
1062        if self.is_zoomed:
1063            # Recover the x,y limits
1064            self.subplot.set_xlim((xlo, xhi))
1065            self.subplot.set_ylim((ylo, yhi))
1066
1067    @property
1068    def is_zoomed(self):
1069        toolbar_zoomed = self.toolbar.GetToolEnabled(self.toolbar.wx_ids['Back'])
1070        return self._is_zoomed or toolbar_zoomed
1071
1072    @is_zoomed.setter
1073    def is_zoomed(self, value):
1074        self._is_zoomed = value
1075
1076    def _on_yaxis_label(self, event):
1077        """
1078        Allows you to add text to the plot
1079        """
1080        yaxis_label, yaxis_unit, yaxis_font, yaxis_color, \
1081                        is_ok, is_tick = self._on_axis_label(axis='y')
1082        if not is_ok:
1083            return
1084
1085        self.yaxis_label = yaxis_label
1086        self.yaxis_unit = yaxis_unit
1087        self.yaxis_font = yaxis_font
1088        self.yaxis_color = yaxis_color
1089        if is_tick:
1090            self.yaxis_tick = yaxis_font
1091
1092        if self.data != None:
1093            # 2D
1094            self.yaxis(self.yaxis_label, self.yaxis_unit, \
1095                        self.yaxis_font, self.yaxis_color, self.yaxis_tick)
1096            self.subplot.figure.canvas.draw_idle()
1097        else:
1098            # 1D
1099            self._check_zoom_plot()
1100
1101    def _on_axis_label(self, axis='x'):
1102        """
1103        Modify axes labels
1104
1105        :param axis: x or y axis [string]
1106        """
1107        is_ok = True
1108        title = 'Modify %s axis label' % axis
1109        font = 'serif'
1110        colour = 'black'
1111        if axis == 'x':
1112            label = self.xaxis_label
1113            unit = self.xaxis_unit
1114        else:
1115            label = self.yaxis_label
1116            unit = self.yaxis_unit
1117        textdial = TextDialog(None, -1, title, label, unit)
1118        if textdial.ShowModal() == wx.ID_OK:
1119            try:
1120                FONT = FontProperties()
1121                font = FONT.copy()
1122                font.set_size(textdial.getSize())
1123                font.set_family(textdial.getFamily())
1124                font.set_style(textdial.getStyle())
1125                font.set_weight(textdial.getWeight())
1126                unit = textdial.getUnit()
1127                colour = textdial.getColor()
1128                is_tick = textdial.getTickLabel()
1129                label_temp = textdial.getText()
1130                if label_temp.count("\%s" % "\\") > 0:
1131                    if self.parent != None:
1132                        msg = "Add Label: Error. Can not use double '\\' "
1133                        msg += "characters..."
1134                        wx.PostEvent(self.parent, StatusEvent(status=msg))
1135                else:
1136                    label = label_temp
1137            except:
1138                if self.parent != None:
1139                    msg = "Add Label: Error. Check your property values..."
1140                    wx.PostEvent(self.parent, StatusEvent(status=msg))
1141                else:
1142                    pass
1143        else:
1144            is_ok = False
1145            is_tick = True
1146        textdial.Destroy()
1147        return label, unit, font, colour, is_ok, is_tick
1148
1149    def _on_removetext(self, event):
1150        """
1151        Removes all text from the plot.
1152        Eventually, add option to remove specific text boxes
1153        """
1154        num_text = len(self.textList)
1155        if num_text < 1:
1156            if self.parent != None:
1157                msg = "Remove Text: Nothing to remove.  "
1158                wx.PostEvent(self.parent, StatusEvent(status=msg))
1159            else:
1160                raise
1161            return
1162        txt = self.textList[num_text - 1]
1163        try:
1164            text_remove = txt.get_text()
1165            txt.remove()
1166            if self.parent != None:
1167                msg = "Removed Text: '%s'. " % text_remove
1168                wx.PostEvent(self.parent, StatusEvent(status=msg))
1169        except:
1170            if self.parent != None:
1171                msg = "Remove Text: Error occurred. "
1172                wx.PostEvent(self.parent, StatusEvent(status=msg))
1173            else:
1174                raise
1175        self.textList.remove(txt)
1176
1177        self.subplot.figure.canvas.draw_idle()
1178
1179    def properties(self, prop):
1180        """
1181        Set some properties of the graph.
1182        The set of properties is not yet determined.
1183
1184        """
1185        # The particulars of how they are stored and manipulated (e.g., do
1186        # we want an inventory internally) is not settled.  I've used a
1187        # property dictionary for now.
1188        #
1189        # How these properties interact with a user defined style file is
1190        # even less clear.
1191
1192        # Properties defined by plot
1193
1194        # Ricardo:
1195        # A empty label "$$" will prevent the panel from displaying!
1196        if prop["xlabel"]:
1197            self.subplot.set_xlabel(r"$%s$"%prop["xlabel"])
1198        if prop["ylabel"]:
1199            self.subplot.set_ylabel(r"$%s$"%prop["ylabel"])
1200        self.subplot.set_title(prop["title"])
1201
1202
1203    def clear(self):
1204        """Reset the plot"""
1205        # TODO: Redraw is brutal.  Render to a backing store and swap in
1206        # TODO: rather than redrawing on the fly.
1207        self.subplot.clear()
1208        self.subplot.hold(True)
1209
1210    def render(self):
1211        """Commit the plot after all objects are drawn"""
1212        # TODO: this is when the backing store should be swapped in.
1213        if self.legend_on:
1214            ax = self.subplot
1215            ax.texts = self.textList
1216            try:
1217                handles, labels = ax.get_legend_handles_labels()
1218                # sort them by labels
1219                hl = sorted(zip(handles, labels),
1220                            key=operator.itemgetter(1))
1221                handles2, labels2 = zip(*hl)
1222                self.line_collections_list = handles2
1223                self.legend = ax.legend(handles2, labels2,
1224                                        prop=FontProperties(size=10),
1225                                        loc=self.legendLoc)
1226                if self.legend != None:
1227                    self.legend.set_picker(self.legend_picker)
1228                    self.legend.set_axes(self.subplot)
1229                    self.legend.set_zorder(20)
1230
1231            except:
1232                self.legend = ax.legend(prop=FontProperties(size=10),
1233                                        loc=self.legendLoc)
1234
1235    def xaxis(self, label, units, font=None, color='black', t_font=None):
1236        """xaxis label and units.
1237
1238        Axis labels know about units.
1239
1240        We need to do this so that we can detect when axes are not
1241        commesurate.  Currently this is ignored other than for formatting
1242        purposes.
1243
1244        """
1245
1246        self.xcolor = color
1247        if units.count("{") > 0 and units.count("$") < 2:
1248            units = '$' + units + '$'
1249        if label.count("{") > 0 and label.count("$") < 2:
1250            label = '$' + label + '$'
1251        if units != "":
1252            label = label + " (" + units + ")"
1253        if font:
1254            self.subplot.set_xlabel(label, fontproperties=font, color=color)
1255            if t_font != None:
1256                for tick in self.subplot.xaxis.get_major_ticks():
1257                    tick.label.set_fontproperties(t_font)
1258                for line in self.subplot.xaxis.get_ticklines():
1259                    size = t_font.get_size()
1260                    line.set_markersize(size / 3)
1261        else:
1262            self.subplot.set_xlabel(label, color=color)
1263        pass
1264
1265    def yaxis(self, label, units, font=None, color='black', t_font=None):
1266        """yaxis label and units."""
1267        self.ycolor = color
1268        if units.count("{") > 0 and units.count("$") < 2:
1269            units = '$' + units + '$'
1270        if label.count("{") > 0 and label.count("$") < 2:
1271            label = '$' + label + '$'
1272        if units != "":
1273            label = label + " (" + units + ")"
1274        if font:
1275            self.subplot.set_ylabel(label, fontproperties=font, color=color)
1276            if t_font != None:
1277                for tick_label in self.subplot.get_yticklabels():
1278                    tick_label.set_fontproperties(t_font)
1279                for line in self.subplot.yaxis.get_ticklines():
1280                    size = t_font.get_size()
1281                    line.set_markersize(size / 3)
1282        else:
1283            self.subplot.set_ylabel(label, color=color)
1284        pass
1285
1286    def _connect_to_xlim(self, callback):
1287        """Bind the xlim change notification to the callback"""
1288        def process_xlim(axes):
1289            lo, hi = subplot.get_xlim()
1290            callback(lo, hi)
1291        self.subplot.callbacks.connect('xlim_changed', process_xlim)
1292
1293    def interactive_points(self, x, y, dx=None, dy=None, name='', color=0,
1294                           symbol=0, markersize=5, zorder=1, id=None,
1295                           label=None, hide_error=False):
1296        """Draw markers with error bars"""
1297        self.subplot.set_yscale('linear')
1298        self.subplot.set_xscale('linear')
1299        if id is None:
1300            id = name
1301        from plottable_interactor import PointInteractor
1302        p = PointInteractor(self, self.subplot, zorder=zorder, id=id)
1303        if p.markersize != None:
1304            markersize = p.markersize
1305        p.points(x, y, dx=dx, dy=dy, color=color, symbol=symbol, zorder=zorder,
1306                 markersize=markersize, label=label, hide_error=hide_error)
1307
1308        self.subplot.set_yscale(self.yscale, nonposy='clip')
1309        self.subplot.set_xscale(self.xscale)
1310
1311    def interactive_curve(self, x, y, dy=None, name='', color=0,
1312                          symbol=0, zorder=1, id=None, label=None):
1313        """Draw markers with error bars"""
1314        self.subplot.set_yscale('linear')
1315        self.subplot.set_xscale('linear')
1316        if id is None:
1317            id = name
1318        from plottable_interactor import PointInteractor
1319        p = PointInteractor(self, self.subplot, zorder=zorder, id=id)
1320        p.curve(x, y, dy=dy, color=color, symbol=symbol, zorder=zorder,
1321                label=label)
1322
1323        self.subplot.set_yscale(self.yscale, nonposy='clip')
1324        self.subplot.set_xscale(self.xscale)
1325
1326    def plottable_selected(self, id):
1327        """
1328        Called to register a plottable as selected
1329        """
1330        #TODO: check that it really is in the list of plottables
1331        self.graph.selected_plottable = id
1332
1333    def points(self, x, y, dx=None, dy=None,
1334               color=0, symbol=0, marker_size=5, label=None,
1335               id=None, hide_error=False):
1336        """Draw markers with error bars"""
1337
1338        # Convert tuple (lo,hi) to array [(x-lo),(hi-x)]
1339        if dx != None and type(dx) == type(()):
1340            dx = nx.vstack((x - dx[0], dx[1] - x)).transpose()
1341        if dy != None and type(dy) == type(()):
1342            dy = nx.vstack((y - dy[0], dy[1] - y)).transpose()
1343        if dx == None and dy == None:
1344            self.subplot.plot(x, y, color=self._color(color),
1345                              marker=self._symbol(symbol),
1346                              markersize=marker_size,
1347                              linestyle='',
1348                              label=label)
1349        else:
1350            col = self._color(color)
1351            if hide_error:
1352                self.subplot.plot(x, y, color=col,
1353                                  marker=self._symbol(symbol),
1354                                  markersize=marker_size,
1355                                  linestyle='',
1356                                  label=label)
1357            else:
1358                self.subplot.errorbar(x, y, yerr=dy, xerr=None,
1359                                      ecolor=col, capsize=2, linestyle='',
1360                                      barsabove=False,
1361                                      mec=col, mfc=col,
1362                                      marker=self._symbol(symbol),
1363                                      markersize=marker_size,
1364                                      lolims=False, uplims=False,
1365                                      xlolims=False, xuplims=False, label=label)
1366
1367        self.subplot.set_yscale(self.yscale, nonposy='clip')
1368        self.subplot.set_xscale(self.xscale)
1369
1370    def _onToggleScale(self, event):
1371        """
1372        toggle axis and replot image
1373
1374        """
1375        zmin_2D_temp = self.zmin_2D
1376        zmax_2D_temp = self.zmax_2D
1377        if self.scale == 'log_{10}':
1378            self.scale = 'linear'
1379            if not self.zmin_2D is None:
1380                zmin_2D_temp = math.pow(10, self.zmin_2D)
1381            if not self.zmax_2D is None:
1382                zmax_2D_temp = math.pow(10, self.zmax_2D)
1383        else:
1384            self.scale = 'log_{10}'
1385            if not self.zmin_2D is None:
1386                # min log value: no log(negative)
1387                if self.zmin_2D <= 0:
1388                    zmin_2D_temp = -32
1389                else:
1390                    zmin_2D_temp = math.log10(self.zmin_2D)
1391            if not self.zmax_2D is None:
1392                zmax_2D_temp = math.log10(self.zmax_2D)
1393
1394        self.image(data=self.data, qx_data=self.qx_data,
1395                   qy_data=self.qy_data, xmin=self.xmin_2D,
1396                   xmax=self.xmax_2D,
1397                   ymin=self.ymin_2D, ymax=self.ymax_2D,
1398                   cmap=self.cmap, zmin=zmin_2D_temp,
1399                   zmax=zmax_2D_temp)
1400
1401    def image(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
1402              zmin, zmax, color=0, symbol=0, markersize=0,
1403              label='data2D', cmap=DEFAULT_CMAP):
1404        """
1405        Render the current data
1406
1407        """
1408        self.data = data
1409        self.qx_data = qx_data
1410        self.qy_data = qy_data
1411        self.xmin_2D = xmin
1412        self.xmax_2D = xmax
1413        self.ymin_2D = ymin
1414        self.ymax_2D = ymax
1415        self.zmin_2D = zmin
1416        self.zmax_2D = zmax
1417        c = self._color(color)
1418        # If we don't have any data, skip.
1419        if self.data == None:
1420            return
1421        if self.data.ndim == 1:
1422            output = self._build_matrix()
1423        else:
1424            output = copy.deepcopy(self.data)
1425        # check scale
1426        if self.scale == 'log_{10}':
1427            try:
1428                if  self.zmin_2D <= 0  and len(output[output > 0]) > 0:
1429                    zmin_temp = self.zmin_2D
1430                    output[output > 0] = numpy.log10(output[output > 0])
1431                    #In log scale Negative values are not correct in general
1432                    #output[output<=0] = math.log(numpy.min(output[output>0]))
1433                elif self.zmin_2D <= 0:
1434                    zmin_temp = self.zmin_2D
1435                    output[output > 0] = numpy.zeros(len(output))
1436                    output[output <= 0] = -32
1437                else:
1438                    zmin_temp = self.zmin_2D
1439                    output[output > 0] = numpy.log10(output[output > 0])
1440                    #In log scale Negative values are not correct in general
1441                    #output[output<=0] = math.log(numpy.min(output[output>0]))
1442            except:
1443                #Too many problems in 2D plot with scale
1444                pass
1445
1446        else:
1447            zmin_temp = self.zmin_2D
1448        self.cmap = cmap
1449        if self.dimension != 3:
1450            #Re-adjust colorbar
1451            self.subplot.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
1452
1453            im = self.subplot.imshow(output, interpolation='nearest',
1454                                     origin='lower',
1455                                     vmin=zmin_temp, vmax=self.zmax_2D,
1456                                     cmap=self.cmap,
1457                                     extent=(self.xmin_2D, self.xmax_2D,
1458                                             self.ymin_2D, self.ymax_2D))
1459
1460            cbax = self.subplot.figure.add_axes([0.84, 0.2, 0.02, 0.7])
1461        else:
1462            # clear the previous 2D from memory
1463            # mpl is not clf, so we do
1464            self.subplot.figure.clear()
1465
1466            self.subplot.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
1467
1468            X = self.x_bins[0:-1]
1469            Y = self.y_bins[0:-1]
1470            X, Y = numpy.meshgrid(X, Y)
1471
1472            try:
1473                # mpl >= 1.0.0
1474                ax = self.subplot.figure.gca(projection='3d')
1475                #ax.disable_mouse_rotation()
1476                cbax = self.subplot.figure.add_axes([0.84, 0.1, 0.02, 0.8])
1477                if len(X) > 60:
1478                    ax.disable_mouse_rotation()
1479            except:
1480                # mpl < 1.0.0
1481                try:
1482                    from mpl_toolkits.mplot3d import Axes3D
1483                except:
1484                    logging.error("PlotPanel could not import Axes3D")
1485                self.subplot.figure.clear()
1486                ax = Axes3D(self.subplot.figure)
1487                if len(X) > 60:
1488                    ax.cla()
1489                cbax = None
1490            self.subplot.figure.canvas.resizing = False
1491            im = ax.plot_surface(X, Y, output, rstride=1, cstride=1, cmap=cmap,
1492                                 linewidth=0, antialiased=False)
1493            self.subplot.set_axis_off()
1494
1495        if cbax == None:
1496            ax.set_frame_on(False)
1497            cb = self.subplot.figure.colorbar(im, shrink=0.8, aspect=20)
1498        else:
1499            cb = self.subplot.figure.colorbar(im, cax=cbax)
1500        cb.update_bruteforce(im)
1501        cb.set_label('$' + self.scale + '$')
1502        if self.dimension != 3:
1503            self.figure.canvas.draw_idle()
1504        else:
1505            self.figure.canvas.draw()
1506
1507    def _build_matrix(self):
1508        """
1509        Build a matrix for 2d plot from a vector
1510        Returns a matrix (image) with ~ square binning
1511        Requirement: need 1d array formats of
1512        self.data, self.qx_data, and self.qy_data
1513        where each one corresponds to z, x, or y axis values
1514
1515        """
1516        # No qx or qy given in a vector format
1517        if self.qx_data == None or self.qy_data == None \
1518                or self.qx_data.ndim != 1 or self.qy_data.ndim != 1:
1519            # do we need deepcopy here?
1520            return copy.deepcopy(self.data)
1521
1522        # maximum # of loops to fillup_pixels
1523        # otherwise, loop could never stop depending on data
1524        max_loop = 1
1525        # get the x and y_bin arrays.
1526        self._get_bins()
1527        # set zero to None
1528
1529        #Note: Can not use scipy.interpolate.Rbf:
1530        # 'cause too many data points (>10000)<=JHC.
1531        # 1d array to use for weighting the data point averaging
1532        #when they fall into a same bin.
1533        weights_data = numpy.ones([self.data.size])
1534        # get histogram of ones w/len(data); this will provide
1535        #the weights of data on each bins
1536        weights, xedges, yedges = numpy.histogram2d(x=self.qy_data,
1537                                                    y=self.qx_data,
1538                                                    bins=[self.y_bins, self.x_bins],
1539                                                    weights=weights_data)
1540        # get histogram of data, all points into a bin in a way of summing
1541        image, xedges, yedges = numpy.histogram2d(x=self.qy_data,
1542                                                  y=self.qx_data,
1543                                                  bins=[self.y_bins, self.x_bins],
1544                                                  weights=self.data)
1545        # Now, normalize the image by weights only for weights>1:
1546        # If weight == 1, there is only one data point in the bin so
1547        # that no normalization is required.
1548        image[weights > 1] = image[weights > 1] / weights[weights > 1]
1549        # Set image bins w/o a data point (weight==0) as None (was set to zero
1550        # by histogram2d.)
1551        image[weights == 0] = None
1552
1553        # Fill empty bins with 8 nearest neighbors only when at least
1554        #one None point exists
1555        loop = 0
1556
1557        # do while loop until all vacant bins are filled up up
1558        #to loop = max_loop
1559        while not(numpy.isfinite(image[weights == 0])).all():
1560            if loop >= max_loop:  # this protects never-ending loop
1561                break
1562            image = self._fillup_pixels(image=image, weights=weights)
1563            loop += 1
1564
1565        return image
1566
1567    def _get_bins(self):
1568        """
1569        get bins
1570        set x_bins and y_bins into self, 1d arrays of the index with
1571        ~ square binning
1572        Requirement: need 1d array formats of
1573        self.qx_data, and self.qy_data
1574        where each one corresponds to  x, or y axis values
1575        """
1576        # No qx or qy given in a vector format
1577        if self.qx_data == None or self.qy_data == None \
1578                or self.qx_data.ndim != 1 or self.qy_data.ndim != 1:
1579            # do we need deepcopy here?
1580            return copy.deepcopy(self.data)
1581
1582        # find max and min values of qx and qy
1583        xmax = self.qx_data.max()
1584        xmin = self.qx_data.min()
1585        ymax = self.qy_data.max()
1586        ymin = self.qy_data.min()
1587
1588        # calculate the range of qx and qy: this way, it is a little
1589        # more independent
1590        x_size = xmax - xmin
1591        y_size = ymax - ymin
1592
1593        # estimate the # of pixels on each axes
1594        npix_y = int(math.floor(math.sqrt(len(self.qy_data))))
1595        npix_x = int(math.floor(len(self.qy_data) / npix_y))
1596
1597        # bin size: x- & y-directions
1598        xstep = x_size / (npix_x - 1)
1599        ystep = y_size / (npix_y - 1)
1600
1601        # max and min taking account of the bin sizes
1602        xmax = xmax + xstep / 2.0
1603        xmin = xmin - xstep / 2.0
1604        ymax = ymax + ystep / 2.0
1605        ymin = ymin - ystep / 2.0
1606
1607        # store x and y bin centers in q space
1608        x_bins = numpy.linspace(xmin, xmax, npix_x)
1609        y_bins = numpy.linspace(ymin, ymax, npix_y)
1610
1611        #set x_bins and y_bins
1612        self.x_bins = x_bins
1613        self.y_bins = y_bins
1614
1615    def _fillup_pixels(self, image=None, weights=None):
1616        """
1617        Fill z values of the empty cells of 2d image matrix
1618        with the average over up-to next nearest neighbor points
1619
1620        :param image: (2d matrix with some zi = None)
1621
1622        :return: image (2d array )
1623
1624        :TODO: Find better way to do for-loop below
1625
1626        """
1627        # No image matrix given
1628        if image == None or numpy.ndim(image) != 2 \
1629                or numpy.isfinite(image).all() \
1630                or weights == None:
1631            return image
1632        # Get bin size in y and x directions
1633        len_y = len(image)
1634        len_x = len(image[1])
1635        temp_image = numpy.zeros([len_y, len_x])
1636        weit = numpy.zeros([len_y, len_x])
1637        # do for-loop for all pixels
1638        for n_y in range(len(image)):
1639            for n_x in range(len(image[1])):
1640                # find only null pixels
1641                if weights[n_y][n_x] > 0 or numpy.isfinite(image[n_y][n_x]):
1642                    continue
1643                else:
1644                    # find 4 nearest neighbors
1645                    # check where or not it is at the corner
1646                    if n_y != 0 and numpy.isfinite(image[n_y - 1][n_x]):
1647                        temp_image[n_y][n_x] += image[n_y - 1][n_x]
1648                        weit[n_y][n_x] += 1
1649                    if n_x != 0 and numpy.isfinite(image[n_y][n_x - 1]):
1650                        temp_image[n_y][n_x] += image[n_y][n_x - 1]
1651                        weit[n_y][n_x] += 1
1652                    if n_y != len_y - 1 and numpy.isfinite(image[n_y + 1][n_x]):
1653                        temp_image[n_y][n_x] += image[n_y + 1][n_x]
1654                        weit[n_y][n_x] += 1
1655                    if n_x != len_x - 1 and numpy.isfinite(image[n_y][n_x + 1]):
1656                        temp_image[n_y][n_x] += image[n_y][n_x + 1]
1657                        weit[n_y][n_x] += 1
1658                    # go 4 next nearest neighbors when no non-zero
1659                    # neighbor exists
1660                    if n_y != 0 and n_x != 0 and\
1661                         numpy.isfinite(image[n_y - 1][n_x - 1]):
1662                        temp_image[n_y][n_x] += image[n_y - 1][n_x - 1]
1663                        weit[n_y][n_x] += 1
1664                    if n_y != len_y - 1 and n_x != 0 and \
1665                        numpy.isfinite(image[n_y + 1][n_x - 1]):
1666                        temp_image[n_y][n_x] += image[n_y + 1][n_x - 1]
1667                        weit[n_y][n_x] += 1
1668                    if n_y != len_y and n_x != len_x - 1 and \
1669                        numpy.isfinite(image[n_y - 1][n_x + 1]):
1670                        temp_image[n_y][n_x] += image[n_y - 1][n_x + 1]
1671                        weit[n_y][n_x] += 1
1672                    if n_y != len_y - 1 and n_x != len_x - 1 and \
1673                        numpy.isfinite(image[n_y + 1][n_x + 1]):
1674                        temp_image[n_y][n_x] += image[n_y + 1][n_x + 1]
1675                        weit[n_y][n_x] += 1
1676
1677        # get it normalized
1678        ind = (weit > 0)
1679        image[ind] = temp_image[ind] / weit[ind]
1680
1681        return image
1682
1683    def curve(self, x, y, dy=None, color=0, symbol=0, label=None):
1684        """Draw a line on a graph, possibly with confidence intervals."""
1685        c = self._color(color)
1686        self.subplot.set_yscale('linear')
1687        self.subplot.set_xscale('linear')
1688
1689        self.subplot.plot(x, y, color=c, marker='',
1690                          linestyle='-', label=label)
1691        self.subplot.set_yscale(self.yscale)
1692        self.subplot.set_xscale(self.xscale)
1693
1694    def _color(self, c):
1695        """Return a particular colour"""
1696        return self.colorlist[c % len(self.colorlist)]
1697
1698    def _symbol(self, s):
1699        """Return a particular symbol"""
1700        return self.symbollist[s % len(self.symbollist)]
1701
1702    def _replot(self, remove_fit=False):
1703        """
1704        Rescale the plottables according to the latest
1705        user selection and update the plot
1706
1707        :param remove_fit: Fit line will be removed if True
1708
1709        """
1710        self.graph.reset_scale()
1711        self._onEVT_FUNC_PROPERTY(remove_fit=remove_fit)
1712        #TODO: Why do we have to have the following line?
1713        self.fit_result.reset_view()
1714        self.graph.render(self)
1715        self.subplot.figure.canvas.draw_idle()
1716
1717    def _onEVT_FUNC_PROPERTY(self, remove_fit=True, show=True):
1718        """
1719        Receive the x and y transformation from myDialog,
1720        Transforms x and y in View
1721        and set the scale
1722        """
1723        # The logic should be in the right order
1724        # Delete first, and then get the whole list...
1725        if remove_fit:
1726            self.graph.delete(self.fit_result)
1727        self.ly = None
1728        self.q_ctrl = None
1729        list = self.graph.returnPlottable()
1730        # Changing the scale might be incompatible with
1731        # currently displayed data (for instance, going
1732        # from ln to log when all plotted values have
1733        # negative natural logs).
1734        # Go linear and only change the scale at the end.
1735        self.set_xscale("linear")
1736        self.set_yscale("linear")
1737        _xscale = 'linear'
1738        _yscale = 'linear'
1739        for item in list:
1740            item.setLabel(self.xLabel, self.yLabel)
1741            # control axis labels from the panel itself
1742            yname, yunits = item.get_yaxis()
1743            if self.yaxis_label != None:
1744                yname = self.yaxis_label
1745                yunits = self.yaxis_unit
1746            else:
1747                self.yaxis_label = yname
1748                self.yaxis_unit = yunits
1749            xname, xunits = item.get_xaxis()
1750            if self.xaxis_label != None:
1751                xname = self.xaxis_label
1752                xunits = self.xaxis_unit
1753            else:
1754                self.xaxis_label = xname
1755                self.xaxis_unit = xunits
1756            # Goes through all possible scales
1757            if self.xLabel == "x":
1758                item.transformX(transform.toX, transform.errToX)
1759                self.graph._xaxis_transformed("%s" % xname, "%s" % xunits)
1760            if self.xLabel == "x^(2)":
1761                item.transformX(transform.toX2, transform.errToX2)
1762                xunits = convert_unit(2, xunits)
1763                self.graph._xaxis_transformed("%s^{2}" % xname, "%s" % xunits)
1764            if self.xLabel == "x^(4)":
1765                item.transformX(transform.toX4, transform.errToX4)
1766                xunits = convert_unit(4, xunits)
1767                self.graph._xaxis_transformed("%s^{4}" % xname, "%s" % xunits)
1768            if self.xLabel == "ln(x)":
1769                item.transformX(transform.toLogX, transform.errToLogX)
1770                self.graph._xaxis_transformed("\ln{(%s)}" % xname, "%s" % xunits)
1771            if self.xLabel == "log10(x)":
1772                item.transformX(transform.toX_pos, transform.errToX_pos)
1773                _xscale = 'log'
1774                self.graph._xaxis_transformed("%s" % xname, "%s" % xunits)
1775            if self.xLabel == "log10(x^(4))":
1776                item.transformX(transform.toX4, transform.errToX4)
1777                xunits = convert_unit(4, xunits)
1778                self.graph._xaxis_transformed("%s^{4}" % xname, "%s" % xunits)
1779                _xscale = 'log'
1780            if self.yLabel == "ln(y)":
1781                item.transformY(transform.toLogX, transform.errToLogX)
1782                self.graph._yaxis_transformed("\ln{(%s)}" % yname, "%s" % yunits)
1783            if self.yLabel == "y":
1784                item.transformY(transform.toX, transform.errToX)
1785                self.graph._yaxis_transformed("%s" % yname, "%s" % yunits)
1786            if self.yLabel == "log10(y)":
1787                item.transformY(transform.toX_pos, transform.errToX_pos)
1788                _yscale = 'log'
1789                self.graph._yaxis_transformed("%s" % yname, "%s" % yunits)
1790            if self.yLabel == "y^(2)":
1791                item.transformY(transform.toX2, transform.errToX2)
1792                yunits = convert_unit(2, yunits)
1793                self.graph._yaxis_transformed("%s^{2}" % yname, "%s" % yunits)
1794            if self.yLabel == "1/y":
1795                item.transformY(transform.toOneOverX, transform.errOneOverX)
1796                yunits = convert_unit(-1, yunits)
1797                self.graph._yaxis_transformed("1/%s" % yname, "%s" % yunits)
1798            if self.yLabel == "y*x^(2)":
1799                item.transformY(transform.toYX2, transform.errToYX2)
1800                xunits = convert_unit(2, self.xaxis_unit)
1801                self.graph._yaxis_transformed("%s \ \ %s^{2}" % (yname, xname),
1802                                              "%s%s" % (yunits, xunits))
1803            if self.yLabel == "y*x^(4)":
1804                item.transformY(transform.toYX4, transform.errToYX4)
1805                xunits = convert_unit(4, self.xaxis_unit)
1806                self.graph._yaxis_transformed("%s \ \ %s^{4}" % (yname, xname),
1807                                              "%s%s" % (yunits, xunits))
1808            if self.yLabel == "1/sqrt(y)":
1809                item.transformY(transform.toOneOverSqrtX,
1810                                transform.errOneOverSqrtX)
1811                yunits = convert_unit(-0.5, yunits)
1812                self.graph._yaxis_transformed("1/\sqrt{%s}" % yname,
1813                                              "%s" % yunits)
1814            if self.yLabel == "ln(y*x)":
1815                item.transformY(transform.toLogXY, transform.errToLogXY)
1816                self.graph._yaxis_transformed("\ln{(%s \ \ %s)}" % (yname, xname),
1817                                              "%s%s" % (yunits, self.xaxis_unit))
1818            if self.yLabel == "ln(y*x^(2))":
1819                item.transformY(transform.toLogYX2, transform.errToLogYX2)
1820                xunits = convert_unit(2, self.xaxis_unit)
1821                self.graph._yaxis_transformed("\ln (%s \ \ %s^{2})" % (yname, xname),
1822                                              "%s%s" % (yunits, xunits))
1823            if self.yLabel == "ln(y*x^(4))":
1824                item.transformY(transform.toLogYX4, transform.errToLogYX4)
1825                xunits = convert_unit(4, self.xaxis_unit)
1826                self.graph._yaxis_transformed("\ln (%s \ \ %s^{4})" % (yname, xname),
1827                                              "%s%s" % (yunits, xunits))
1828            if self.yLabel == "log10(y*x^(4))":
1829                item.transformY(transform.toYX4, transform.errToYX4)
1830                xunits = convert_unit(4, self.xaxis_unit)
1831                _yscale = 'log'
1832                self.graph._yaxis_transformed("%s \ \ %s^{4}" % (yname, xname),
1833                                              "%s%s" % (yunits, xunits))
1834            item.transformView()
1835
1836        # set new label and units
1837        yname = self.graph.prop["ylabel"]
1838        yunits = ''
1839        xname = self.graph.prop["xlabel"]
1840        xunits = ''
1841
1842        self.resetFitView()
1843        self.prevXtrans = self.xLabel
1844        self.prevYtrans = self.yLabel
1845        self.graph.render(self)
1846        self.set_xscale(_xscale)
1847        self.set_yscale(_yscale)
1848
1849        self.xaxis(xname, xunits, self.xaxis_font,
1850                   self.xaxis_color, self.xaxis_tick)
1851        self.yaxis(yname, yunits, self.yaxis_font,
1852                   self.yaxis_color, self.yaxis_tick)
1853        self.subplot.texts = self.textList
1854        if show:
1855            self.subplot.figure.canvas.draw_idle()
1856
1857    def onFitDisplay(self, tempx, tempy, xminView,
1858                     xmaxView, xmin, xmax, func):
1859        """
1860        Add a new plottable into the graph .In this case this plottable
1861        will be used to fit some data
1862
1863        :param tempx: The x data of fit line
1864        :param tempy: The y data of fit line
1865        :param xminView: the lower bound of fitting range
1866        :param xminView: the upper bound of  fitting range
1867        :param xmin: the lowest value of data to fit to the line
1868        :param xmax: the highest value of data to fit to the line
1869
1870        """
1871        # Saving value to redisplay in Fit Dialog when it is opened again
1872        self.Avalue, self.Bvalue, self.ErrAvalue, \
1873                      self.ErrBvalue, self.Chivalue = func
1874        self.xminView = xminView
1875        self.xmaxView = xmaxView
1876        self.xmin = xmin
1877        self.xmax = xmax
1878        #In case need to change the range of data plotted
1879        for item in self.graph.returnPlottable():
1880            item.onFitRange(None, None)
1881        # Create new data plottable with result
1882        self.fit_result.x = []
1883        self.fit_result.y = []
1884        self.fit_result.x = tempx
1885        self.fit_result.y = tempy
1886        self.fit_result.dx = None
1887        self.fit_result.dy = None
1888        #Load the view with the new values
1889        self.fit_result.reset_view()
1890        # Add the new plottable to the graph
1891        self.graph.add(self.fit_result)
1892        self.graph.render(self)
1893        self._offset_graph()
1894        self.subplot.figure.canvas.draw_idle()
1895
1896    def onChangeCaption(self, event):
1897        """
1898        """
1899        if self.parent == None:
1900            return
1901        # get current caption
1902        old_caption = self.window_caption
1903        # Get new caption dialog
1904        dial = LabelDialog(None, -1, 'Modify Window Title', old_caption)
1905        if dial.ShowModal() == wx.ID_OK:
1906            new_caption = dial.getText()
1907
1908            # send to guiframe to change the panel caption
1909            caption = self.parent.on_change_caption(self.window_name,
1910                                                    old_caption, new_caption)
1911
1912            # also set new caption in plot_panels list
1913            self.parent.plot_panels[self.uid].window_caption = caption
1914            # set new caption
1915            self.window_caption = caption
1916
1917        dial.Destroy()
1918
1919    def onResetGraph(self, event):
1920        """
1921        Reset the graph by plotting the full range of data
1922        """
1923        for item in self.graph.returnPlottable():
1924            item.onReset()
1925        self.graph.render(self)
1926        self._onEVT_FUNC_PROPERTY(False)
1927        self.is_zoomed = False
1928        self.toolbar.update()
1929
1930    def onPrint(self, event=None):
1931        self.toolbar.print_figure(event)
1932
1933    def onPrinterSetup(self, event=None):
1934        """
1935        """
1936        self.canvas.Printer_Setup(event=event)
1937        self.Update()
1938
1939    def onPrinterPreview(self, event=None):
1940        """
1941        Matplotlib camvas can no longer print itself.  Thus need to do
1942        everything ourselves: need to create a printpreview frame to to
1943        see the preview but needs a parent frame object.  Also needs a
1944        printout object (just as any printing task).
1945        """
1946        try:
1947            #check if parent is a frame.  If not keep getting the higher
1948            #parent till we find a frame
1949            _plot = self
1950            while not isinstance(_plot, wx.Frame):
1951                _plot = _plot.GetParent()
1952                assert _plot is not None
1953
1954            #now create the printpeview object
1955            _preview = wx.PrintPreview(PlotPrintout(self.canvas),
1956                                       PlotPrintout(self.canvas))
1957            #and tie it to a printpreview frame then show it
1958            _frame = wx.PreviewFrame(_preview, _plot, "Print Preview", wx.Point(100, 100), wx.Size(600, 650))
1959            _frame.Centre(wx.BOTH)
1960            _frame.Initialize()
1961            _frame.Show(True)
1962        except:
1963            traceback.print_exc()
1964            pass
1965
1966    def OnCopyFigureMenu(self, evt):
1967        """
1968        Copy the current figure to clipboard
1969        """
1970        try:
1971            self.toolbar.copy_figure(self.canvas)
1972        except:
1973            print "Error in copy Image"
1974
1975
1976#---------------------------------------------------------------
1977class NoRepaintCanvas(FigureCanvasWxAgg):
1978    """
1979    We subclass FigureCanvasWxAgg, overriding the _onPaint method, so that
1980    the draw method is only called for the first two paint events. After that,
1981    the canvas will only be redrawn when it is resized.
1982
1983    """
1984    def __init__(self, *args, **kwargs):
1985        """
1986        """
1987        FigureCanvasWxAgg.__init__(self, *args, **kwargs)
1988        self._drawn = 0
1989
1990    def _onPaint(self, evt):
1991        """
1992        Called when wxPaintEvt is generated
1993
1994        """
1995        if not self._isRealized:
1996            self.realize()
1997        if self._drawn < 2:
1998            self.draw(repaint=False)
1999            self._drawn += 1
2000        self.gui_repaint(drawDC=wx.PaintDC(self))
Note: See TracBrowser for help on using the repository browser.