# -*- coding: utf-8 -*-
import numpy
import numpy.ma
import math
import sys
import traceback
import os.path
import matplotlib

DEG = math.pi/180;
ARCMIN = math.pi/(180*60);


def init_pyplot (output_type="X11"):
  """Initializes the pyplot singleton. Inits the matplotlib back-end
  corresponding to the specified plot type""";
  if 'pyplot' not in globals():
    if output_type.upper() != "X11":
      matplotlib.use('agg');
    else:
      matplotlib.use('qt4agg');
    global pyplot;
    import matplotlib.pyplot as pyplot

def _int_or_0 (x):
  try:
    return int(x);
  except:
    return 0;
_version = map(_int_or_0,matplotlib.__version__.split('.'));
_version_major = (_version and _version[0]) or 0;
_version_minor = (len(_version)>1 and _version[1]) or 0;

class PlotCollection (object):
  """PlotCollection plots a collection of data tracks in one plot""";
  def __init__ (self,options):
    self.data = {};
    self.mean = {};
    self.stddev = {};
    self.label = {};
    self.track_counts = {};
    # inter-plot offset -- may be set from outside
    self.offset = 0;

  def num_tracks (self):
    return len(self.data);

  def add_track (self,key,data,mean=None,stddev=None,label=None,count=1):
    self.data[key] = data;
    self.mean[key] = mean if mean is not None else data.mean();
    self.stddev[key] = stddev if stddev is not None else data.std();
    self.label[key] = label if label is not None else str(key);
    self.track_counts[key] = count;

  def get_track_data (self,key):
    return self.data.get(key,None);

  def get_track_count (self,key):
    return self.track_counts.get(key,0);

  # function to make single-page plot
  def make_figure (self,keylist=None,suptitle=None,save=None,
                   dual=True,offset_std=10,xgrid=False,ygrid=False,
                   figsize=(210,290),dpi=100,papertype='a4',landscape=False):
    import matplotlib.pyplot as pyplot
    # setup sizes
    keylist = keylist or sorted(self.data.keys());
    # compute inter-plot offset, based on the median stddev
    offset = self.offset;
    if not offset:
      stddevs = sorted(self.stddev.itervalues());
      if stddevs:
        offset = self.offset = offset_std*stddevs[len(stddevs)/2];
    # if still not set, force to 1
    if not offset:
      offset = self.offset = 1;
    # partition keys into two sets, if asked to
    if dual:
      # split ifrs into two sets (if number is odd, make sure first subset has the extra 1)
      n = len(keylist);
      n2 = len(keylist)/2;
      if n%2:
        n2 += 1;
      keysets = [ keylist[0:n2],keylist[n2:] ];
    else:
      keysets = [ keylist ];
    # create Figure object of given size and resolution
    figsize_in = (figsize[0]/25.4,figsize[1]/25.4);
    fig = pyplot.figure(figsize=figsize_in,dpi=100);
    # margin sizes. We want to keep them fixed in absolute terms,
    # regardless of plot size. The relative numbers here are good for a 210x290 plot, so
    # we rescale them accordingly.
    mleft   = 0.05 * 210./figsize[0];
    mbottom = 0.03 * 290./figsize[1];
    mright  = 0.01 * 210./figsize[0];
    mtop    = 0.03 * 290./figsize[1];
    ytitle  = 1 - 0.01 * 290./figsize[1];
    width   = ( 1 - mleft - mright );
    height  = ( 1 - mtop - mbottom );
    # now plot all tracks
    for iplot,keys in enumerate(keysets):
      if dual:
        plt = fig.add_subplot(1,2,iplot+1);
      else:
        plt = fig.add_axes([mleft,mbottom,width,height]);
      nx = max([len(self.data.get(key,[])) for key in keys]);
      # first and last key
      key0 = keys[0];
      key1 = keys[-1];
      mindata,maxdata = 1e+99,-1e+99;
      # make plots for all ifrs
      firstkey = None;
      for key in keys:
        data = self.data.get(key);
        if data is None or data.mask.all():
          continue;
        else:
          # print data.min(),data.max();
          # first plot goes at its natural coordinates.
          if firstkey is None:
            firstkey = key;
            y0 = 0;
            y0text = self.mean[key] - offset/2;
            mindata = data.min() - offset/2;
#            mindata = y0text = self.mean[key] - offset/2;
            dd = data;
          # subsequent plots offset accordingly
          else:
            y0 += offset;
            dd = data + y0;
#          maxdata = self.mean[key] + y0 + offset/2;
          mindata = min(mindata,data.min() + y0 - offset/10);
          maxdata = max(maxdata,self.mean[key] + y0 + offset);
          # plot data
          try:
            if len(dd) == 1:
              dd = numpy.array([dd[0],dd[0]]);
            plt.plot(dd);
            if ygrid:
              plt.axhline(y0,color='0.8',zorder=-10);
            #print dd;
          except:
            traceback.print_exc();
            print "Error plotting data for",key;
            continue;
          if self.label[key]:
            # figure out where to place text label
            ytext = dd[:nx/10].mean();
            if numpy.isnan(ytext):
              ytext = dd.mean();
            # do not put too close to previous label
            y0text = max(y0text+offset/4,ytext);
            # annotate() seems to have a bug in matplotlib 0.98, so use text() instead
            if _version_major > 0 or _version_minor > 98:
              plt.annotate(self.label[key],xy=(nx/100,ytext),xytext=(nx/100,y0text),
                    size=5,horizontalalignment='left',verticalalignment='center',
                    bbox=dict(facecolor='white',edgecolor='none',alpha=0.6),
                    arrowprops=dict(fc='none',width=0,headwidth=0,ec="0.8",alpha=0.6));
            else:
              plt.text(nx/100,ytext,self.label[key],
                    size=5,horizontalalignment='left',verticalalignment='center',
                    bbox=dict(facecolor='white',edgecolor='none',alpha=0.6));

      # set plot limits. First panel is determined by data. Scale of second panel is fixed to first.
      plt.set_xbound(0,nx);
      if iplot == 0:
        ylim = mindata,maxdata;
        plt.set_ybound(*ylim);
        for lab in plt.get_yticklabels():
          lab.set_fontsize(5);
      else:
        for lab in plt.get_yticklabels():
          lab.set_fontsize(0);
        # plt.set_yticklabels([]);
        plt.set_ybound(*ylim);
      for lab in plt.get_xticklabels():
        lab.set_fontsize(5);
      # set x grid
      if _version_major > 0 or _version_minor > 98:
        plt.minorticks_on();
      if xgrid and xgrid != [0]:
        if isinstance(xgrid,(list,tuple)):
          if len(xgrid)>0:
            plt.set_xticks(range(0,nx,xgrid[0]));
          if len(xgrid)>1:
            plt.set_xticks(range(0,nx,xgrid[1]),True);
        for x in plt.get_xticks():
          plt.axvline(x,color='0.7',ls='-',lw=1,zorder=-10);
        for x in plt.get_xticks(True):
          plt.axvline(x,color='0.9',ls='-',lw=1,zorder=-10);
        plt.set_xlim(0,nx);
    fig.subplots_adjust(left=mleft,right=1-mright,top=1-mtop,bottom=mbottom,wspace=0.01);
    # plot title if asked to
    if suptitle:
      fig.suptitle(suptitle,y=ytitle,size=8);
    if save:
      fig.savefig(save,papertype=papertype,dpi=dpi,
                  orientation='portrait' if not landscape else 'landscape');
      print "===> Wrote",save;
    return fig;

class PlotCollectionSep (PlotCollection):
  """PlotCollectionSep is a PlotCollection that separates data tracks into individual plots,
  and optionally groups them by key""";
  def __init__ (self,options):
    PlotCollection.__init__(self,options);
    self.groups = {};

  def num_tracks (self):
    return len(self.data);

  def add_track (self,key,data,mean=None,stddev=None,label=None,count=1,group=None):
    self.groups.setdefault(group,set()).add(key);
    PlotCollection.add_track(self,key,data,mean=mean,stddev=stddev,label=label,count=count);

  # function to make single-page plot
  def make_figure (self,grouplist=None,suptitle=None,save=None,
                   dual=True,xgrid=False,ygrid=False,
                   figsize=(210,290),dpi=100,papertype='a4',landscape=False):
    import matplotlib.pyplot as pyplot
    # setup sizes
    grouplist = grouplist or sorted(self.groups.keys());
    # partition keys into two sets, if asked to
    if dual:
      # split ifrs into two sets (if number is odd, make sure first subset has the extra 1)
      n = len(grouplist);
      nrow = len(grouplist)/2;
      if n%2:
        nrow += 1;
      # swap things around, so plots start at the bottom
      plotnums = numpy.arange(1,nrow*2+1).reshape(nrow,2)[-1::-1].transpose().ravel()
    else:
      nrow = len(grouplist);
      plotnums = numpy.arange(nrow,0,-1);
    # create Figure object of given size and resolution
    figsize_in = (figsize[0]/25.4,figsize[1]/25.4);
    fig = pyplot.figure(figsize=figsize_in,dpi=100);
    # margin sizes. We want to keep them fixed in absolute terms,
    # regardless of plot size. The relative numbers here are good for a 210x290 plot, so
    # we rescale them accordingly.
    mleft   = 0.05 * 210./figsize[0];
    mbottom = 0.03 * 290./figsize[1];
    mright  = 0.01 * 210./figsize[0];
    mtop    = 0.03 * 290./figsize[1];
    ytitle  = 1 - 0.01 * 290./figsize[1];
    width   = ( 1 - mleft - mright );
    height  = ( 1 - mtop - mbottom );
    colors = ( "blue","green","red","cyan","purple","magenta","black","orange","grey" );
    # now plot all tracks
    for iplot,groupname in enumerate(grouplist):
      plt = fig.add_subplot(nrow,2 if dual else 1,plotnums[iplot]);
      keys = list(self.groups[groupname]);
      nx = max([len(self.data.get(key,[])) for key in keys]);
      # first and last key
      key0 = keys[0];
      key1 = keys[-1];
      mindata,maxdata = 1e+99,-1e+99;
      # make plots for all ifrs
      for ikey,key in enumerate(keys):
        dd = self.data.get(key);
        if dd is None or dd.mask.all():
          continue;
        else:
          try:
            if len(dd) == 1:
              dd = numpy.array([dd[0],dd[0]]);
            plt.plot(dd,',',color=colors[ikey%len(colors)],label=self.label[key]);
          except:
            traceback.print_exc();
            print "Error plotting data for",key;
            continue;
      # add text labels
      # legend is not quite functional in matplotlib 0.98, so use text() instead
      y0,y1 = plt.get_ylim();
      if _version_major > 0 or _version_minor > 98:
        legend = plt.legend(ncol=len(keys),numpoints=1,prop=dict(size=5),handletextpad=0,columnspacing=0);
  #      legend.set_frame_on(False);
        for i,txt in enumerate(legend.get_texts()):
          txt.set_color(colors[i%len(colors)]);
        legend.get_frame().set_edgecolor('None');
        legend.get_frame().set_facecolor('white');
        legend.get_frame().set_alpha(.6);
      else:
        x0 = 0;
        for ikey,key in enumerate(keys):
          txt = plt.text(x0,y1,self.label[key],color=colors[ikey%len(colors)],
                          size=5,horizontalalignment='left',verticalalignment='top',
                          bbox=dict(facecolor='white',edgecolor='none',alpha=0.6));
          x0 += nx/20;
      plt.set_ylabel(groupname,size=8);
      # set x grid
      if _version_major > 0 or _version_minor > 98:
        plt.minorticks_on();
      for lab in plt.get_xmajorticklabels():
        lab.set_fontsize(5);
      for lab in plt.get_xminorticklabels():
        lab.set_fontsize(0);
      for lab in plt.get_ymajorticklabels():
        lab.set_fontsize(5);
      for lab in plt.get_yminorticklabels():
        lab.set_fontsize(0);
      if xgrid and xgrid != [0]:
        if isinstance(xgrid,(list,tuple)):
          if len(xgrid)>0:
            plt.set_xticks(range(0,nx,xgrid[0]));
          if len(xgrid)>1:
            plt.set_xticks(range(0,nx,xgrid[1]),True);
        for x in plt.get_xticks():
          plt.axvline(x,color='0.7',ls='-',lw=1,zorder=-10);
        for x in plt.get_xticks(True):
          plt.axvline(x,color='0.9',ls='-',lw=1,zorder=-10);
        plt.set_xlim(0,nx);
      if ygrid:
        for y in plt.get_yticks():
          plt.axhline(y,color='0.7',ls='-',lw=1,zorder=-10);
        for y in plt.get_yticks(True):
          plt.axhline(y,color='0.9',ls='-',lw=1,zorder=-10);
        plt.set_ylim(y0,y1);
        
    fig.subplots_adjust(left=mleft,right=1-mright,top=1-mtop,bottom=mbottom,wspace=0.12);
    # plot title if asked to
    if suptitle:
      fig.suptitle(suptitle,y=ytitle,size=8);
    if save:
      fig.savefig(save,papertype=papertype,dpi=dpi,
                  orientation='portrait' if not landscape else 'landscape');
      print "===> Wrote",save;
    return fig;


class ComplexCirclePlot (PlotCollection):
  """ComplexCirclePlot plots a complex circle plot""";
  # plot colors: xx xy yx yy
  colors = ("blue","red","purple","green");

  # function to make single-page plot
  def make_figure (self,keylist=None,suptitle=None,save=None,xgrid=False,ygrid=False,
                   dual=True,offset_std=10,
                   figsize=(210,290),dpi=100,papertype='a4',landscape=False):
    import matplotlib.pyplot as pyplot
    figsize_in = (figsize[0]/25.4,figsize[1]/25.4);
    fig = pyplot.figure(figsize=figsize_in,dpi=100);
    if keylist:
      datalist = [ (key,self.data.get(key)) for key in keylist ];
    else:
      datalist = data.iteritems();
    # margin sizes. We want to keep them fixed in absolute terms,
    # regardless of plot size. The relative numbers here are good for a 210x210 plot, so
    # we rescale them accordingly.
    mleft   = 0.06 * 210./figsize[0];
    mbottom = 0.06 * 210./figsize[1];
    mright  = 0.01 * 210./figsize[0];
    mtop    = 0.04 * 210./figsize[1];
    ytitle  = 1 - 0.01 * 210./figsize[1];
    width   = ( 1 - mleft - mright );
    height  = ( 1 - mtop - mbottom );
    plt = fig.add_axes([mleft,mbottom,width,height]);
    # find length of common prefix of labels
    labels = [lab for lab in self.label.itervalues() if lab];
    if labels:
      prefix = 1;
      while len(labels[0])>prefix and all([lab.startswith(labels[0][:prefix]) for lab in labels]):
        prefix += 1;
    # plot data over text labels
    ncorr = 0;
    rad = 0;
    for key,data in datalist:
      label = self.label.get(key);
      ncorr = max(ncorr,data.shape[-1]);
      for icorr in range(data.shape[-1]):
        color = self.colors[icorr];
        d = data[...,icorr];
        rad = max(rad,abs(d).max());
        if d.size > 1 or not label:
          plt.plot(d.real,d.imag,".-",mec=color,mfc=color,zorder=0);
        if label:
          d0 = d[-1] if d.ndim else d;
          plt.text(d0.real,d0.imag,label[prefix-1:],horizontalalignment='center',
                  verticalalignment='center',zorder=1,fontsize=8,color=color);
    # plot arrow to means
    for icorr in range(ncorr):
      color = self.colors[icorr];
      datameans = [d[...,icorr].mean() for d in self.data.itervalues() if d.shape[-1]>icorr ];
      meanval = sum(datameans)/len(datameans) if datameans else 0;
      plt.arrow(0,0,meanval.real,meanval.imag,color=color);
      # plot circle
      phi = numpy.arange(0,1.001,0.001)*2*math.pi;
      plt.plot(numpy.cos(phi)*abs(meanval),numpy.sin(phi)*abs(meanval),"-",color=color);
    for lab in plt.get_xticklabels():
      lab.set_fontsize(10);
    for lab in plt.get_yticklabels():
      lab.set_fontsize(10);
      lab.set_rotation('vertical');
    if xgrid or ygrid:
      plt.grid(color="0.4",linestyle='-');
    # plot title if asked to
    plt.set_xlim(-rad,rad);
    plt.set_ylim(-rad,rad);
    if suptitle:
      fig.suptitle(suptitle,y=ytitle,size=10);
    if save:
      fig.savefig(save,papertype=papertype,dpi=dpi,
                  orientation='portrait' if not landscape else 'landscape');
      print "===> Wrote",save;
    return fig;




class ScatterPlot (object):
  """ScatterPlot plots a complex scatterplot""";
  def __init__ (self,options):
    self.data = {};
    self.label = {};
    self.track_counts = {};
    # inter-plot offset -- may be set from outside
    self.offset = 0;

  def add_point (self,key,x,y,label=None):
    if isinstance(x,float):
      x = [x];
    if isinstance(y,float):
      y = [y];
    self.data[key] = x,y;
    self.label[key] = label if label is not None else str(key);

  def get_point (self,key):
    return self.data.get(key,None);

  # function to make single-page plot
  def make_figure (self,suptitle=None,save=None,xlabel=None,ylabel=None,
                   offset_std=10,xgrid=False,ygrid=False,
                   figsize=(210,210),dpi=100,papertype='a4',landscape=False):
    import matplotlib.pyplot as pyplot
    # create Figure object of given size and resolution
    figsize_in = (figsize[0]/25.4,figsize[1]/25.4);
    fig = pyplot.figure(figsize=figsize_in,dpi=100);
    # margin sizes. We want to keep them fixed in absolute terms,
    # regardless of plot size. The relative numbers here are good for a 210x210 plot, so
    # we rescale them accordingly.
    mleft   = 0.06 * 210./figsize[0];
    mbottom = 0.06 * 210./figsize[1];
    mright  = 0.01 * 210./figsize[0];
    mtop    = 0.04 * 210./figsize[1];
    ytitle  = 1 - 0.01 * 210./figsize[1];
    width   = ( 1 - mleft - mright );
    height  = ( 1 - mtop - mbottom );
    plt = fig.add_axes([mleft,mbottom,width,height]);
    # remove common prefix from labels
    if self.label:
      lab0 = self.label.values()[0];
      prefix = 1;
      while len(lab0) >= prefix and all([lab.startswith(lab0[:prefix]) for lab in self.label.itervalues()]):
        prefix += 1;
      labels = dict([(key,label[prefix-1:]) for key,label in self.label.iteritems()]);
    else:
      labels = None;
    # plot data over text labels
    for key,(x,y) in self.data.iteritems():
      # plot data
      try:
        plt.plot(x,y,"-x",zorder=0);
      except:
        traceback.print_exc();
        print "Error plotting data for",key;
        continue;
      # plot text labels
      if labels.get(key):
        plt.text(x[0],y[0]," "+labels[key],horizontalalignment='left',verticalalignment='top',zorder=1,fontsize=8);
#    plt.set_xbound(min([min(x) for x,y in self.data.itervalues()])[0],
#                   max([max(x) for x,y in self.data.itervalues()])[0]);
#    plt.set_ybound(min([min(y) for x,y in self.data.itervalues()])[0],
#                   max([max(y) for x,y in self.data.itervalues()])[0]);
    for lab in plt.get_xticklabels():
      lab.set_fontsize(10);
    for lab in plt.get_yticklabels():
      lab.set_fontsize(10);
      lab.set_rotation('vertical');
    # plot title if asked to
    if xlabel:
      plt.set_xlabel(xlabel,size=10);
    if ylabel:
      plt.set_ylabel(ylabel,size=10);
    if suptitle:
      fig.suptitle(suptitle,y=ytitle,size=10);
    if save:
      fig.savefig(save,papertype=papertype,dpi=dpi,
                  orientation='portrait' if not landscape else 'landscape');
      print "===> Wrote",save;
    return fig;


PLOT_SINGLE = 'single';
PLOT_MULTI  = 'multi';
PLOT_ERRORBARS = 'errorbars';
PLOT_BARPLOT = 'barplot';

class AbstractBasePlot (object):
  """Abstract base class for the various plotting objects""";

  def __init__ (self,options,figsize=(290,210)):
    init_pyplot(options.output_type);
    self.options = options;
    if options.width and options.height:
      self._default_figsize = self._user_figsize = (options.width,options.height);
    else:
      self._user_figsize = None;
      self._default_figsize = figsize;

  @classmethod
  def init_options (self,plotgroup,outputgroup):
    """Initializes options relevant to this plotting class""";
    self._plotgroup = plotgroup;
    self._outputgroup = outputgroup;

  @classmethod
  def add_plot_option (self,opt,*args,**kw):
    """Helper method: adds a plot option, if it's not already defined""";
    if not self._plotgroup.has_option(opt):
      self._plotgroup.add_option(opt,*args,**kw);

  @classmethod
  def add_output_option (self,opt,*args,**kw):
    """Helper method: adds an output option, if it's not already defined""";
    if not self._outputgroup.has_option(opt):
      self._outputgroup.add_option(opt,*args,**kw);

class MultigridPlot (AbstractBasePlot):
  """MultigridPlot makes NxM plots on a single page""";

  def __init__ (self,options,figsize=(290,210)):
    AbstractBasePlot.__init__(self,options,figsize);
    self._borders = [ 0.05,0.99,0.05,0.95 ];
    if options.borders and isinstance(options.borders,str):
      try:
        self._borders = [ float(x) for x in options.borders.split(",") ];
      except:
        pass;
    if options.y_lock:
      try:
        y0,y1 = map(float,options.y_lock.split(","));
      except:
        print "Error parsing --y-lock option, --y-lock MIN,MAX expected."
        sys.exit(1);
      self.ylock = y0,y1;
    else:
      self.ylock = None;

  @classmethod
  def init_options (self,plotgroup,outputgroup):
    AbstractBasePlot.init_options(plotgroup,outputgroup);
    self.add_plot_option("--title-fontsize",metavar="POINTS",type="int",default=12,
                    help="Set plot title font size, 0 for no title. Default is %default.");
    self.add_plot_option("--subtitle-fontsize",metavar="POINTS",type="int",default=10,
                    help="Set plot subtitle font size, 0 for none. Default is %default.");
    self.add_plot_option("--label-fontsize",metavar="POINTS",type="int",default=8,
                    help="Set plot label font size, 0 for no labels. Default is %default.");
    self.add_plot_option("--text-fontsize",metavar="POINTS",type="int",default=6,
                    help="Set plot text font size, 0 for no labels. Default is %default.");
    self.add_plot_option("--axis-fontsize",metavar="POINTS",type="int",default=5,
                    help="Set axis label font size, 0 for no axis labels. Default is %default.");
    self.add_plot_option("--text-spacing",metavar="VALUE",type="float",default=0.15,
                    help="Set spacing between lines of text in text comments. Default is %default.");
    self.add_plot_option("--y-ticks",metavar="N",type="int",default=4,
                    help="Maximum number of major ticks along the Y axis. Default is %default.");
    self.add_plot_option("--y-minor-ticks",metavar="INTERVAL",type="float",default=0,
                    help="Add minor ticks along the Y axis at the given intervals.");
    self.add_plot_option("--y-lock",metavar="MIN,MAX",type="str",default="",
                    help="Locks the Y scale of the plot to the given min/max values.");
    self.add_plot_option("--borders",metavar="L,R,B,T",type="str",
                    help="Set left/right/bottom/top plot borders, in normalized page coordinates. Default is %default.");
    self.add_plot_option("--subplot-wspace",metavar="W",type="float",default=0,
                    help="Set spacing between subplots, as fraction of subplot width. Default is automatic.");
    self.add_plot_option("-S","--subtitle",type="str",default="",
                      help="Subtitle for plot, added (in parentheses) after plot title");

    self.add_output_option("-o","--output-type",metavar="TYPE",type="string",default="png",
                      help="File format, see matplotlib documentation "
                      "for supported formats. At least 'png', 'pdf', 'ps', 'eps' and 'svg' are supported, or use 'x11' to display "
                      "plots interactively. Default is '%default.'");
    self.add_output_option("-r","--refresh",action="store_true",
                      help="Refresh plots even if they already exist (default is to keep existing plots.)");
    self.add_output_option("--output-prefix",metavar="PREFIX",type="string",default="",
                      help="Prefix output filenames with PREFIX_");
    self.add_output_option("--papertype",dest="papertype",type="string",default="a4",
                      help="set paper type (for .ps output only.) Default is '%default', but can also use e.g. 'letter', 'a3', etc.");
    self.add_output_option("-W","--width",type="int",default=0,
                      help="set explicit plot width, in mm. (Useful for .eps output)");
    self.add_output_option("-H","--height",type="int",default=0,
                      help="set explicit plot height, in mm. (Useful for .eps output)");
    self.add_output_option("--dpi",type="int",default=300,
                      help="figure resolution. Default is %default.");
    self.add_output_option("--scale",type="float",default=1,
                      help="scale plot sizes up by the given factor.");
    self.add_output_option("--portrait",action="store_true",
                    help="Force portrait orientation. Default is to select orientation based on plot size");
    self.add_output_option("--landscape",action="store_true",
                    help="Force landscape orientation.");

  @staticmethod
  def get_plot_data (data,xaxis=None):
    """Helper function, interprets the plot data returned by a datafunc.
    'data' is input data as returned by the datafunc argument to make_figure() below.
    'xaxis' is default X axis, Note to use ordinal numbering.
    Return value is tuple of X,Y,Yerr. Yerr is None if no error bars are provided.
    """;
    # a 2- or 3-tuple is interpreted as x,y[,yerr]
    if isinstance(data,tuple):
      if len(data) == 2:
        x,y = data;
        yerr = None;
      elif len(data) == 3:
        x,y,yerr = data;
      else:
        raise TypeError,"incorrect datum returned: 2- or 3-tuple expected, got %d-tuple"""%len(data);
    # else interpret data as Y
    else:
      y = data;
      x = yerr = None;
    # set X axis
    if x is None:
      x = range(len(y)) if xaxis is None else xaxis;
    # check lengths
    if len(x) != len(y):
      raise TypeError,"misshaped datum returned: %d X elements, %d Y elements"""%(len(x),len(y));
    if yerr is not None and len(yerr) != len(y):
      raise TypeError,"misshaped datum returned: %d Y elements, %d Yerr elements"""%(len(y),len(yerr));
    return x,y,yerr;

  def make_figure (self,rows,cols,  # (irow,row) and (icol,col) list
                  datafunc,   # datafunc(irow,icol) returns plot data for plot i,j
                  mode=PLOT_SINGLE,xaxis=None,
                  hline=None, # plot horizontal line at Y position, None for none
                  ylock="row", # lock Y scale. "row" locks across rows, "col" locks across columns, True locks across whole plot, (ymin,ymax) sets an explicit scale
                  mean_format="%.2f",
                  suptitle=None,      # title of plot
                  save=None,          # filename to save to
                  format=None,        # format: use options.output_type by default
                  figsize=None        # figure width,height in mm
                  ):
    from matplotlib import ticker
    if save and (format or self.options.output_type.upper()) != 'X11':
      save = "%s.%s"%(save,format or self.options.output_type);
      if self.options.output_prefix:
        save = "%s_%s"%(self.options.output_prefix,save);
      # exit if figure already exists, and we're not refreshing
      if os.path.exists(save) and not self.options.refresh: # and os.path.getmtime(save) >= os.path.getmtime(__file__):
        print save,"exists, not redoing";
        return save;
    else:
      save = None;

    figsize = numpy.array(self._user_figsize or figsize or self._default_figsize)/25.4;
    figsize *= self.options.scale;
    fig = pyplot.figure(figsize=figsize,dpi=10);
    iplot = 0;
    rows = list(rows);
    cols = list(cols);
    if ylock and not self.ylock:
      dummy = numpy.array([0.]);
      # form up ymin, ymax: NROWxNCOL arrays of min/max values per each plot
      if mode is PLOT_SINGLE or mode is PLOT_BARPLOT:
        datafilter = lambda x:x if isinstance(x,numpy.ndarray) else dummy;
        ymin = numpy.array([[datafilter(datafunc(row[0],col[0])).min() for col in cols ] for row in rows]);
        ymax = numpy.array([[datafilter(datafunc(row[0],col[0])).max() for col in cols ] for row in rows]);
      elif mode is PLOT_MULTI:
        ymin = numpy.array([[ min([self.get_plot_data(dd,xaxis)[1].min() for dd in datafunc(row[0],col[0])]) for col in cols ] for row in rows ]);
        ymax = numpy.array([[ max([self.get_plot_data(dd,xaxis)[1].max() for dd in datafunc(row[0],col[0])]) for col in cols ] for row in rows ]);
      elif mode is PLOT_ERRORBARS:
        dummy = numpy.array([0.]);
        datafilter1 = lambda x:x[0]-x[1] if isinstance(x[0],numpy.ndarray) else dummy;
        datafilter2 = lambda x:x[0]+x[1] if isinstance(x[0],numpy.ndarray) else dummy;
        ymin = numpy.array([[ datafilter1(datafunc(row[0],col[0])).min() for col in cols ] for row in rows]);
        ymax = numpy.array([[ datafilter2(datafunc(row[0],col[0])).max() for col in cols ] for row in rows]);
      # now collapse them according to ylock mode
      if isinstance(ylock,(list,tuple)) and len(ylock) == 2:
        ymin[...],ymax[...] = ylock;
      elif ylock == "row":
        ymin[...] = ymin.min(1)[:,numpy.newaxis];
        ymax[...] = ymax.max(1)[:,numpy.newaxis];
      elif ylock == "col":
        ymin[...] = ymin.min(0)[numpy.newaxis,:];
        ymax[...] = ymax.max(0)[numpy.newaxis,:];
      else:
        ymin[...] = ymin.min();
        ymax[...] = ymax.max();
    # make legend across the top
    for icol,col in cols:
      iplot += 1;
      plt = fig.add_subplot(len(rows)+1,len(cols)+1,iplot);
      plt.axis("off");
      plt.text(.5,0,col,fontsize=self.options.subtitle_fontsize*self.options.scale,
        horizontalalignment='center',verticalalignment='bottom');
    iplot += 1;
    # helper function to put lines of text instead of real plot
    def plot_text (plt,lines):
      y0 = 0.95;
      for i,text in enumerate(lines):
        plt.text(0,y0-self.options.text_spacing*i,text,
           fontsize=self.options.text_fontsize*self.options.scale,horizontalalignment='left',verticalalignment='top');
      plt.axis("off");
      plt.set_ylim(0,1);
    # now make plots
    for irow,row in rows:
      for icol,col in cols:
        iplot += 1;
        plt = fig.add_subplot(len(rows)+1,len(cols)+1,iplot);
        # plt.axis("off");
        plt.yaxis.set_major_locator(ticker.MaxNLocator(self.options.y_ticks));
        if self.options.y_minor_ticks:
          plt.yaxis.set_minor_locator(ticker.MultipleLocator(self.options.y_minor_ticks));
        if ylock and ylock != "col" and icol:
          plt.set_yticklabels([]);
        plt.set_xticks([]);
        data = datafunc(irow,icol);
        realplot = True;
        # if data is a sequence of strings, plot them
        if isinstance(data,(list,tuple)) and isinstance(data[0],str):
          plot_text(plt,data);
          realplot = False;
        elif mode is PLOT_SINGLE:
          plt.plot(range(len(data)) if xaxis is None else xaxis,data);
          if xaxis is None:
            plt.set_xlim(-1,len(data));
        elif mode is PLOT_BARPLOT:
          plt.bar(range(len(data)) if xaxis is None else xaxis,data,linewidth=0);
          if xaxis is None:
            plt.set_xlim(-1,len(data));
        elif mode is PLOT_MULTI:
          for dd in data:
            x,y,yerr = self.get_plot_data(dd,xaxis);
            if yerr is None:
              plt.plot(x,y);
            else:
              plt.errorbar(x,y,yerr,fmt=None,capsize=1);
            if x == range(len(y)):
              plt.set_xlim(-1,len(y));
        elif mode is PLOT_ERRORBARS:
          y,yerr = data;
          if len(y) == 1:
            plot_text(plt,(mean_format%y[0],"+/-"+(mean_format%yerr[0])));
            realplot = False;
          else:
            plt.errorbar(range(len(y)) if xaxis is None else xaxis,y,yerr,fmt=None,capsize=1);
            if xaxis is None:
              plt.set_xlim(-1,len(y));
        if realplot:
          if self.ylock:
            plt.set_ylim(*self.ylock);
            if icol:
              plt.set_yticklabels([]);
          elif ylock:
            plt.set_ylim(ymin[irow,icol],ymax[irow,icol]);
            if ylock is not "col" and icol:
              plt.set_yticklabels([]);
          if hline is not None:
            plt.axhline(y=hline,color='black');
        for lab in plt.get_yticklabels():
          lab.set_fontsize(self.options.axis_fontsize*self.options.scale);
      # add row label
      iplot += 1;
      plt = fig.add_subplot(len(rows)+1,len(cols)+1,iplot);
      plt.text(0,.5,row,fontsize=self.options.label_fontsize*self.options.scale,horizontalalignment='left',verticalalignment='center');
      plt.axis("off");
    # adjust layout
    fig.subplots_adjust(left=self._borders[0],right=self._borders[1],
      top=self._borders[3],bottom=self._borders[2],
      wspace=self.options.subplot_wspace or None);
    # plot title if asked to
    if suptitle and self.options.title_fontsize:
      if self.options.subtitle:
        suptitle = "%s (%s)"%(suptitle,self.options.subtitle);
      fig.suptitle(suptitle,fontsize=self.options.title_fontsize*self.options.scale);
    if save:
      if self.options.portrait:
        orientation = 'portrait';
      elif self.options.landscape:
        orientation = 'landscape';
      else:
        orientation='portrait' if figsize[0]<figsize[1] else 'landscape';
      fig.savefig(save,papertype=self.options.papertype,dpi=self.options.dpi,
                  orientation=orientation);
      print "Wrote",save,"in",orientation,"orientation";
      fig = None;
      pyplot.close("all");
    return save;


class SkyPlot (AbstractBasePlot):
  """SkyPlot plots things in an l,m plot""";
  def __init__ (self,options,figsize=(290,210)):
    AbstractBasePlot.__init__(self,options,figsize);
    self._borders = [ .05,.9,.05,.9 ];
    if options.circle_borders and isinstance(options.circle_borders,str):
      try:
        self._borders = [ float(x) for x in options.circle_borders.split(",") ];
      except:
        pass;

  @classmethod
  def init_options (self,plotgroup,outputgroup):
    AbstractBasePlot.init_options(plotgroup,outputgroup);
    self.add_plot_option("--radius",type="int",default=0,
                      help="radius of circle plots, in arcmin. Default is auto-sizing.");
    self.add_plot_option("-G","--grid-circle",metavar="ARCMIN",type="int",action="append",
                      help="sets radius of grid circle(s) in circle plots. May be given multiple times. Default is 30 and 60.");
    self.add_plot_option("--circle-title-fontsize",metavar="POINTS",type="int",default=12,
                    help="Set plot title font size in circle plots, 0 for no title. Default is %default.");
    self.add_plot_option("--circle-label-fontsize",metavar="POINTS",type="int",default=8,
                    help="Set plot label font size in circle plots, 0 for no labels. Default is %default.");
    self.add_plot_option("--circle-axis-fontsize",metavar="POINTS",type="int",default=5,
                    help="Set axis label font size in circle_plots, 0 for no axis labels. Default is %default.");
    self.add_plot_option("--names-only",action="store_true",
                    help="Use only source names for labels in circle plots. Default is name, mean and std values.");
    self.add_plot_option("--circle-label-offset",metavar="FRAC",type="float",default=0.02,
                    help="Vertical offset of circle labels. Use 0 to center labels on source position. Default is %default.");
    self.add_plot_option("--circle-borders",metavar="L,R,B,T",type="str",
                    help="Set left/right/bottom/top circle-plot borders, in normalized page coordinates. Default is %default.");
    self.add_plot_option("-S","--subtitle",type="str",default="",
                      help="Subtitle for plot, added (in parentheses) after plot title");

    self.add_output_option("-o","--output-type",metavar="TYPE",type="string",default="png",
                    help="File format, see matplotlib documentation "
                    "for supported formats. At least 'png', 'pdf', 'ps', 'eps' and 'svg' are supported, or use 'x11' to display "
                    "plots interactively. Default is '%default.'");
    self.add_output_option("-r","--refresh",action="store_true",
                      help="Refresh plots even if they already exist (default is to keep existing plots.)");
    self.add_output_option("--output-prefix",metavar="PREFIX",type="string",default="",
                      help="Prefix output filenames with PREFIX_");
    self.add_output_option("--papertype",dest="papertype",type="string",default="a4",
                      help="set paper type (for .ps output only.) Default is '%default', but can also use e.g. 'letter', 'a3', etc.");
    self.add_output_option("-W","--width",type="int",
                      help="set explicit plot width, in mm. (Useful for .eps output)");
    self.add_output_option("-H","--height",type="int",
                      help="set explicit plot height, in mm. (Useful for .eps output)");
    self.add_output_option("--dpi",type="int",default=300,
                      help="figure resolution. Default is %default.");
    self.add_output_option("--portrait",action="store_true",
                    help="Force portrait orientation. Default is to select orientation based on plot size");
    self.add_output_option("--landscape",action="store_true",
                    help="Force landscape orientation.");

  """Plotting function""";
  def make_figure (self,ll,mm,
    markers,  # marker is a list of strings or dicts or tuples.
              # For each marker, if str: axes.plot(x,y,str) is called
              # if dict: axes.plot(x,y,**dict)
              # else tuple is func,arg,kwargs
              # and axes.func(*args,**kwargs) is called
    labels=None,
    radius=None,        # plot radius. If None, then it's set automatically
    zero_lines=True,    # plot l=0 and m=0 lines
    suptitle=None,      # title of plot
    save=None,          # filename to save to
    format=None,        # format: use self.options.output_type by default
    figsize=(210,210),       # figure width,height in mm
    ):
    if save and (format or self.options.output_type.upper()) != 'X11':
      save = "%s.%s"%(save,format or self.options.output_type);
      if self.options.output_prefix:
        save = "%s_%s"%(self.options.output_prefix,save);
      # exit if figure already exists, and we're not refreshing
      if os.path.exists(save) and not self.options.refresh: # and os.path.getmtime(save) >= os.path.getmtime(__file__):
        print save,"exists, not redoing";
        return save;
    else:
      save = None;

    figsize = numpy.array(figsize or self._default_figsize)/25.4;
    fig = pyplot.figure(figsize=figsize,dpi=600);
    plt = fig.add_axes([self._borders[0],self._borders[2],self._borders[1]-self._borders[0],
                        self._borders[3]-self._borders[2]]);
    if zero_lines:
      plt.axhline(y=0,color='black',linestyle=':');
      plt.axvline(x=0,color='black',linestyle=':');

    # if ll is None, use length of markers for everything
    if ll is None:
      ll = mm = numpy.zeros(len(markers),float);

    if not isinstance(markers,(list,tuple)):
      markers = [markers]*len(ll);

    if not labels:
      labels = [None]*len(ll);

    # set limits
    if self.options.radius:
      lim = self.options.radius;
    elif radius:
      lim = radius;
    else:
      lim = max(max(abs(ll)),max(abs(mm)))*1.05/ARCMIN;

    # plot markers
    for l,m,mark,label in zip(ll/ARCMIN,mm/ARCMIN,markers,labels):
      if isinstance(mark,str):
        plt.plot(l,m,mark);
      elif isinstance(mark,dict):
        plt.plot(l,m,**mark);
      elif isinstance(mark,tuple):
        func,args,kwargs = mark;
        getattr(plt,func)(*args,**kwargs);
      if label and self.options.circle_label_fontsize:
        plt.text(l,m-lim*self.options.circle_label_offset,
          label,fontsize=self.options.circle_label_fontsize,
          horizontalalignment='center',
          verticalalignment='top' if self.options.circle_label_offset else 'center');

    x = numpy.cos(numpy.arange(0,361)*math.pi/180);
    y = numpy.sin(numpy.arange(0,361)*math.pi/180);

    gc = self.options.grid_circle or (30,60);
    for R in gc:
      plt.plot(x*R,y*R,linestyle=':',color='black');

    plt.set_xlim(lim,-lim);
    plt.set_ylim(-lim,lim);
    for lab in list(plt.get_xticklabels())+list(plt.get_yticklabels()):
      lab.set_fontsize(self.options.circle_axis_fontsize);

    if suptitle and self.options.title_fontsize:
      if self.options.subtitle:
        suptitle = "%s (%s)"%(suptitle,self.options.subtitle);
      fig.suptitle(suptitle,fontsize=self.options.circle_title_fontsize);
    if save:
      if self.options.portrait:
        orientation = 'portrait';
      elif self.options.landscape:
        orientation = 'landscape';
      else:
        orientation='portrait' if figsize[0]<figsize[1] else 'landscape';
      fig.savefig(save,papertype=self.options.papertype,dpi=self.options.dpi,
                  orientation=orientation);
      print "Wrote",save,"in",orientation,"orientation";
      fig = None;
      pyplot.close("all");
    return save;

