# GP_PLOT.PY
# Dominic Ford
# 27/04/2006

# Implementation of plot command

import gp_eval
import gp_datafile
import gp_settings
from gp_autocomplete import *

import os
import sys
import signal
import re
import pyx
from pyx import *

# Stores the number of lines on our graph. PyX gets unhappy when this is zero.
plot_counter = 0     # Used for X11 terminal, to give each plot output an individual name
ghostview_pid = None # pid of any running gv process launched under X11_singlewindow

# Store list of previously plotted items. Used to replot.
plotlist = []
axes_this = { 'x':{},'y':{},'z':{} }

# The canvas used by multiplot
multiplot_plotdesc = []
multiplot_axes     = []   # List of multiplot axes, used for linking axes

# Passes line/point styles between plotting functions and "with" parser
stylestr  = ""
pointsize = 0
pointtype = 0
linewidth = 0
pointlinewidth = 0
linestyle = 0
plotcolour= 0
linecount = 0 # Counts how many lines have been plotted; used to cycle line styles.
ptcount   = 0 # As above; used to cycle point styles
colourcnt = 0 # As above; used to cycle colours
withstate = 0 # State parameter used when processing input after the word "with"

# Used to count how many lines have gone onto graph. If zero, be careful.... PyX crashes when producing an empty key
successful_plot_operations = 0

# COORD_TRANSFORM(): Transform from "first", "second" coord systems, etc, into canvas coordinates

def coord_transform(g, axes, systx, systy, x0, y0):
  # Transform x coordinate
  if   (systx == "first") :
   x,dummy = g.pos(x=x0,y=1,xaxis=axes['x'][1]['AXIS'],yaxis=axes['y'][1]['AXIS'])
  elif (systx == "second"):
   if (2 in axes['x']):
    x,dummy = g.pos(x=x0,y=1,xaxis=axes['x'][2]['AXIS'],yaxis=axes['y'][1]['AXIS'])
   else:
    print "Warning -- attempt to use second x axis when it doesn't exist... reverting to first x axis"
    x,dummy = g.pos(x=x0,y=1,xaxis=axes['x'][1]['AXIS'],yaxis=axes['y'][1]['AXIS'])
  else:
   x = x0 # screen or graph coordinates being used

  # Transform y coordinate
  if   (systy == "first") :
   dummy,y = g.pos(x=1,y=y0,xaxis=axes['x'][1]['AXIS'],yaxis=axes['y'][1]['AXIS'])
  elif (systy == "second"):
   if (2 in axes['y']):
    dummy,y = g.pos(x=1,y=y0,xaxis=axes['x'][1]['AXIS'],yaxis=axes['y'][2]['AXIS'])
   else:
    print "Warning -- attempt to use second y axis when it doesn't exist... reverting to first x axis"
    dummy,y = g.pos(x=1,y=y0,xaxis=axes['x'][1]['AXIS'],yaxis=axes['y'][1]['AXIS'])
  else:
   y = y0 # screen or graph coordinates being used

  # return
  return [x,y]

# DIRECTIVE_PLOT(): Handles the 'plot' command

def directive_plot(line,linestyles,vars,funcs,settings,axes,labels,arrows,replot_stat):
  global plotlist, axes_this
  global multiplot_plotdesc, multiplot_axes

  if (settings['MULTIPLOT'] != 'ON'):
   multiplot_plotdesc = []
   multiplot_axes     = []

  if (replot_stat == 0):
   plotlist = [] # If not replotting, wipe plot list
  else:
   if (settings['MULTIPLOT'] == 'ON'): # If replotting a multiplot, wipe last graph
    if (len(multiplot_plotlist) > 0):
     multiplot_plotlist = multiplot_plotlist[0:len(multiplot_plotlist)-1]
     multiplot_axes     = multiplot_axes    [0:len(multiplot_plotlist)-1]

  test = re.match(r"^\s*r?e?p\w*\s*(.*)$",line) # Strip initial "plot" command
  if (test == None):
    print "Syntax Error -- This Should Not Happen!"; return # Error - This Should Not Happen!
  line = test.group(1)

  # Prepare PyX

  if (text.defaulttexrunner.texruns != 0): # attempt to clean up the mess if text runner broke last time it was used
    text.defaulttexrunner.texruns = 0
    text.defaulttexrunner.dvifile = None
    text.defaulttexrunner.preamblemode = 1
    text.defaulttexrunner.preambles = []
    text.defaulttexrunner.needdvitextboxes = []
    text.defaulttexrunner.textboxesincluded = 0
    text.reset()
  text.set(mode="latex")
  text.texrunner.waitfortex=20 # seconds

  # Now make a local copy of axes
  if (replot_stat == 0):
   axes_this = { 'x':{},'y':{},'z':{} } # Make a local copy of the list 'axes', NB: replot on same axes second time around
   for [direction,axis_list_to] in axes_this.iteritems():
    for [number,axis] in axes[direction].iteritems():
     axis_list_to[number] = {'SETTINGS':axis.copy(), 'MIN_USED':None, 'MAX_USED':None, 'AXIS':None}
     # Nones are min/max range of data plotted on each axis and PyX axis

  # Now read range specifications, modifying local copy of axes as required
  state     = 0
  while ((len(line) > 0) and (line[0] == '[')):
    test = re.match("""^\s*\[([^:\]]*)((:)|( *to *))?([^:\]]*)\]\s*(.*)$""",line)
    if (test == None):
      print "Could not read range. Use format: [min:max]."
      return # Error
    else:
      try: 
        if (state == 0): # set x-axis range
          if (len(test.group(1).strip()) != 0):
            axes_this['x'][1]['SETTINGS']['MIN']       = gp_eval.gp_eval(test.group(1),vars,funcs) # Setting ranges in the plot command in GnuPlot is *stupid*
          if (len(test.group(5).strip()) != 0):                                                    # Why does it set range for x1y1 axes,
            axes_this['x'][1]['SETTINGS']['MAX']       = gp_eval.gp_eval(test.group(5),vars,funcs) # even when you're plotting on x1y2?
          axes_this['x'][1]['SETTINGS']['AUTOSCALE'] = 'OFF'
          state = 1 ; line = test.group(6) ; continue
        elif (state == 1): # set y-axis range
          if (len(test.group(1).strip()) != 0):
            axes_this['y'][1]['SETTINGS']['MIN']       = gp_eval.gp_eval(test.group(1),vars,funcs) # But we go ahead and copy this brokeness anyway....
          if (len(test.group(5).strip()) != 0):                                                    # because brokeness is good: it's character
            axes_this['y'][1]['SETTINGS']['MAX']       = gp_eval.gp_eval(test.group(5),vars,funcs) # building for the user.
          axes_this['y'][1]['SETTINGS']['AUTOSCALE'] = 'OFF'
          state = 2 ; line = test.group(6) ; continue 
        else: 
          print "Too many ranges. Specify a maximum of two ranges, one for x, second for y."
          raise ValueError # User has specified too many ranges
      except:
        return # Error

  # Set up key
  if (settings['KEY'] == "ON"):
    if   (settings['KEYPOS'] == "TOP RIGHT"):     [hpos,vpos] = [1.0, 1.0]
    elif (settings['KEYPOS'] == "TOP MIDDLE"):    [hpos,vpos] = [0.5, 1.0]
    elif (settings['KEYPOS'] == "TOP LEFT"):      [hpos,vpos] = [0.0, 1.0]
    elif (settings['KEYPOS'] == "MIDDLE RIGHT"):  [hpos,vpos] = [1.0, 0.5]
    elif (settings['KEYPOS'] == "MIDDLE MIDDLE"): [hpos,vpos] = [0.5, 0.5]
    elif (settings['KEYPOS'] == "MIDDLE LEFT"):   [hpos,vpos] = [0.0, 0.5]
    elif (settings['KEYPOS'] == "BOTTOM RIGHT"):  [hpos,vpos] = [1.0, 0.0]
    elif (settings['KEYPOS'] == "BOTTOM MIDDLE"): [hpos,vpos] = [0.5, 0.0]
    elif (settings['KEYPOS'] == "BOTTOM LEFT"):   [hpos,vpos] = [0.0, 0.0]
    elif (settings['KEYPOS'] == "BELOW"      ):   [hpos,vpos] = [0.5,-0.5]
    elif (settings['KEYPOS'] == "OUTSIDE"    ):   [hpos,vpos] = [1.5, 1.0]
    else:
      print "Internal Error: Cannot work out where key is.... defaulting to top-right"
      [hpos,vpos] = [1.0, 1.0]
    key = graph.key.key(pos=None,hpos=hpos+settings['KEY_XOFF'],vpos=vpos+settings['KEY_YOFF'])
  else:
    key = None

  # Split line into comma-separated things to plot
  for item in gp_eval.gp_split(line,","): plotlist.append(item)

  # Add plot to multiplot list (we do this even when not in multiplot mode, in which case the list has just been wiped, and will only have one member)
  if (settings['MULTIPLOT'] == 'ON'): print "Plot added to multiplot with reference %d"%len(multiplot_plotdesc)
  multiplot_plotdesc.append([plotlist,key,settings.copy(),labels.copy(),arrows.copy(),'OFF'])
  multiplot_axes.append(axes_this)

  # Go ahead and make a canvas and plot everything!
  multiplot_plot(linestyles,vars,funcs,settings)

# MULTIPLOT_PLOT(): The main plotting engine. Plots whatever is in the current list "multiplot_plotdesc"

def multiplot_plot(linestyles,vars,funcs,settings):
  global plot_counter, ghostview_pid
  global multiplot_plotdesc, multiplot_axes

  # We make a canvas on which to put our graph.
  # This serves two purposes: first, we can then put our arrows etc on top of gridlines (by default PyX puts them under)
  # secondly, we use it to overlay multiplot plots
  try:
   multiplot_canvas = canvas.canvas()
  except:
    print "Failed while creating initial drawing canvas:"
    print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
    return # Error

  # Multiplot all of our multiplot items

  for [[Mplotlist, Mkey, Msettings, Mlabels, Marrows, Mdeleted], Maxes_this] in zip(multiplot_plotdesc,multiplot_axes):

   g = None # We have no graph.... yet

   if (Mdeleted != 'ON'): # Don't plot items which have been deleted
     plot_dataset_toplevel(g,Mplotlist,Msettings,Maxes_this,linestyles,vars,funcs,0==1) # Do a dry-run before plotting to establish ranges of data for axes.

   g = plot_dataset_makeaxes(Msettings, Mkey, Maxes_this)                               # Now assign axes. We don't use PyX autoscaling, because it's unreliable.
                                                                                        # Do this even for deleted items, as they may be link-axis-ed.

   if (Mdeleted != 'ON'): # Don't plot items which have been deleted
     if (g == None): continue                                                           # Ooops... *That* didn't really work....
     plot_dataset_toplevel(g,Mplotlist,Msettings,Maxes_this,linestyles,vars,funcs,0==0) # Now do plotting proper.

     # We now transfer graph onto our multiplot canvas, and draw arrows/labels on top of it as required
     try:
      multiplot_canvas.insert(g)
     except:
      print "Failed while adding graph to canvas:"
      print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
      return # Error

     # Print title of plot, if we have one
     if (len(Msettings['TITLE']) > 0):
       # Count number of x-axes along top of plot, and shift title to be above them all
       number_top_axes = 0
       for [number,xaxis] in Maxes_this['x'].iteritems():
        if ((number % 2) == 0): number_top_axes = number_top_axes + 1
       vertical_pos = g.height + 0.3 + Msettings['TIT_YOFF'] + Msettings['ORIGINY']
       if (number_top_axes > 0): vertical_pos = vertical_pos + 1.1
       if (number_top_axes > 1): vertical_pos = vertical_pos + 1.85 * (number_top_axes - 1)
       horizontal_pos = g.width/2 + Msettings['TIT_XOFF'] + Msettings['ORIGINX']
       multiplot_canvas.text(horizontal_pos, vertical_pos, Msettings['TITLE'], [text.halign.center, text.valign.bottom])

     # Print text labels
     try:
       for dummy,(txt,systx,x,systy,y) in Mlabels.iteritems():
         [x,y] = coord_transform(g, Maxes_this, systx, systy, x, y)
         multiplot_canvas.text(x, y, txt)
     except:
       print "Error printing labels:"
       print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"

     # Print arrows
     try:
       for dummy,(systx0,x0,systy0,y0,systx1,x1,systy1,y1,arrow_style) in Marrows.iteritems():
        [x0,y0] = coord_transform(g, Maxes_this, systx0, systy0, x0, y0)
        [x1,y1] = coord_transform(g, Maxes_this, systx1, systy1, x1, y1)
        if (arrow_style == 'head'): arrow_style_list = [deco.earrow.normal]
        if (arrow_style == 'nohead'): arrow_style_list = []
        if (arrow_style == 'twoway'): arrow_style_list = [deco.barrow.normal, deco.earrow.normal]
        multiplot_canvas.stroke(path.line(x0,y0,x1,y1),arrow_style_list)
     except:
       print "Error printing arrows:"
       print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"

  # End of the multiplot loop!

  # Output plot
  if (settings['TERMTYPE'][0:3] == "X11"):
    try:
      plot_counter = plot_counter + 1
      fname = "/tmp/gp+_" + str(os.getpid()) + "_" + str(plot_counter) + ".eps"
      multiplot_canvas.writeEPSfile(fname)
    except:
      print "Failed while producing eps output:"
      print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
      return # Error
    if (settings['TERMTYPE'] == "X11_multiwindow"):      # X11_multiwindow
     os.system("gv "+fname+" &")
    else:                                                # X11_singlewindow
     if (ghostview_pid != None):
      os.kill(ghostview_pid, signal.SIGKILL) # Kill previous gv session
     fork = os.fork()
     if (fork != 0):
      ghostview_pid = fork
     else:
      os.execlp('gv', 'gv', fname)
      os._exit(0)
  else:                                                  # EPS output
    try:
      tempname = "/tmp/gp+_" + str(os.getpid()) + ".eps" # PyX adds .eps ending to filename which we don't want
      multiplot_canvas.writeEPSfile(tempname) # so we make a temporary file, and move it to user's chosen location.
      os.system("mv %s %s"%(tempname,os.path.join(gp_settings.cwd, settings['OUTPUT'])))
    except:
      print "PyX failed while producing eps output:"
      print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
      return # Error

# PLOT_DATASET_MAKEAXES(): Makes axes for a plot, using the ranges which have been found from 

def plot_dataset_makeaxes(Msettings, Mkey, Maxes_this):
  try:
   # Set up axes
   axisassign = "" # List of axes to pass to graph.graphxy method
   if (Msettings['GRID'] == 'ON'): gridalloc = ",painter=graph.axis.painter.regular(gridattrs=[attr.changelist([color.gray(0.6),color.gray(0.9)])])"
   else:                           gridalloc = ""
   for [direction, axis_list] in Maxes_this.iteritems():
    if (direction != 'z'): # 2D plots don't have z axes
     for [number,axis] in axis_list.iteritems():
      if (number == 1): axisname = direction             # x1 axis is called x in PyX
      else            : axisname = direction+str(number) # but x2 axis is called x2 in PyX
      axis_initialise = ""

      test = re.match(r"""linkaxis\s\s*(\d\d*)(\s*,?\s*)(\d*)""", axis['SETTINGS']['LABEL']) # Do multiplot linked axes
      if (test != None):
       if (settings['MULTIPLOT'] != 'ON'):
        print "Warning: apparent attempt to create a linked axis when not in multiplot mode... doomed to fail!"
       else:
        try:
         linkaxis_plot = int(test.group(1))
         if (len(test.group(2)) == 0): linkaxis_no = 1
         else                        : linkaxis_no = int(test.group(2))
         if (linkaxis_plot >= len(multiplot_axes)):
          print "Warning: attempt to create a linked axis to a non-existant plot: %s"%axis['SETTINGS']['LABEL']
         elif (not linkaxis_no in multiplot_axes[linkaxis_plot][direction]):
          print "Warning: attempt to create a linked axis to %s-axis number %d of plot %d, but this plot has no such axis:\n%s"%(direction,linkaxis_no,linkaxis_plot,axis['SETTINGS']['LABEL'])
         else:
          axis_initialise = 'axis["AXIS"] = multiplot_axes[linkaxis_plot][direction][linkaxis_no]["AXIS"].createlinkaxis()'
        except:
         print "Error whilst reading linkaxis command: %s"%axis['SETTINGS']['LABEL']

      if (axis_initialise == ""): # We didn't make a link axis....
       if (axis['SETTINGS']['LOG'] != "ON"): axistype = "graph.axis.linear"
       else                                : axistype = "graph.axis.log"
       if (axis['SETTINGS']['AUTOSCALE'] == "ON"): # PyX axis autoscaling crashes if no data is plotted on axis.... in this case we default to -10 -> 10
        if (axis['MIN_USED'] == None):
         axis['SETTINGS']['AUTOSCALE'] = "OFF"
         axis['SETTINGS']['MIN'] = -10.0
         axis['SETTINGS']['MAX'] =  10.0
        elif (axis['MAX_USED'] == axis['MIN_USED']): # PyX also crashes if there's no spread of data on axis which is set to autoscale... in this case stick in a factor 2 ish
         axis['SETTINGS']['AUTOSCALE'] = "OFF"
         axis['SETTINGS']['MIN'] = axis['MIN_USED'] - 1.0
         axis['SETTINGS']['MAX'] = axis['MIN_USED'] + 1.0
       axis_initialise = 'axis["AXIS"] ='+axistype+'(title=r"""'+axis['SETTINGS']['LABEL']+'"""'

       if (axis['SETTINGS']['LOG'] == "ON"): # Log axes going below zero is *bad* !!
        if (axis['SETTINGS']['AUTOSCALE'] != "ON"): # If we're not autoscaling, make sure user limits are sensible
         if (axis['SETTINGS']['MIN'] <= 0.0):
          print "Warning: Log axes set with range minimum < 0 -- this is impossible. Reverting to 1e-6 instead."
          axis['SETTINGS']['MIN'] = 1e-6
         if (axis['SETTINGS']['MAX'] <= 0.0):
          print "Warning: Log axes set with range maximum < 0 -- this is impossible. Reverting to 1.0 instead."
          axis['SETTINGS']['MAX'] = 1.0
        else: # Unfortunately, PyX autoscaling of log axes is also very very unsafe -- tries to go to negative ordinates.
         axis['SETTINGS']['MIN'] = axis['MIN_USED']
         axis['SETTINGS']['MAX'] = axis['MAX_USED']

       if ((axis['SETTINGS']['AUTOSCALE'] != "ON") or (axis['SETTINGS']['LOG'] == "ON")): # Set min/max manually for non-autoscale axes
        axis_initialise = axis_initialise + ', min='+str(axis['SETTINGS']['MIN'])+', max='+str(axis['SETTINGS']['MAX'])

       if (number == 1): axis_initialise = axis_initialise + gridalloc # Make gridlines only from axes x1y1
       axis_initialise = axis_initialise + ')'

      # Now make axis
      exec axis_initialise
      axisassign = axisassign+axisname+"=Maxes_this['"+direction+"']["+str(number)+"]['AXIS'], "

   # Set up plot
   if (Msettings['AUTOASPECT'] == 'ON'):
    ratioassign=""
   else:
    ratioassign="ratio=%f,"%(1.0/Msettings['ASPECT'])
   if (successful_plot_operations < 1): Mkey = None # Don't put a key on a plot with no datasets... PyX crashes!
   exec "g = graph.graphxy(width=Msettings['WIDTH'],"+ratioassign+axisassign+"key=Mkey,xpos=%f,ypos=%f)"%(Msettings['ORIGINX'],Msettings['ORIGINY'])
  except:
   print "Could not make axis objects. Perhaps a numerical issue with the range specified?"
   print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
   return None
  if (g == None): print "Internal error: Failed to produce graph object in PyX."
  return g

# PLOT_DATASET_TOPLEVEL():

def plot_dataset_toplevel(g,Mplotlist,Msettings,Maxes_this,linestyles,vars,funcs,plotting):
  global linecount, ptcount, colourcnt, successful_plot_operations

  # Counts number of lines/pointsets plotted, so that we can cycle styles
  colourcnt = 0
  linecount = 0
  ptcount   = 0
  successful_plot_operations = 0 # Count how many datasets successfully get plotted

  # Plot datafiles first, to get an idea of the range of the x-axis
  for plotitem in Mplotlist:
    plotwords = plotitem.strip().split()
    if ((len(plotwords) > 0) and (plotwords[0][0] == "'")):
      plot_datafile(g,Maxes_this,Msettings,linestyles,plotwords,vars,funcs,plotting)

  # If we have no datafiles or autoscale to give us an x-range, we need to make one up...
  for [number,xaxis] in Maxes_this['x'].iteritems():
   if (xaxis['SETTINGS']['AUTOSCALE'] != 'ON'):
    xaxis['MIN_USED'] = xaxis['SETTINGS']['MIN']
    xaxis['MAX_USED'] = xaxis['SETTINGS']['MAX']
   else:
    if (xaxis['MIN_USED'] == None): # No data plotted on x-axis, so need to make up a range for it for evaluating functions
     xaxis['MIN_USED'] = -10.0
     xaxis['MAX_USED'] =  10.0
     xaxis['SETTINGS']['AUTOSCALE'] = "OFF"

  # Plot functions second, sampling them over the range that we have now determined
  for plotitem in Mplotlist:
    plotwords = plotitem.strip().split()
    if ((len(plotwords) > 0) and (plotwords[0][0] != "'")):
      plot_function(g,Maxes_this,Msettings,linestyles,plotwords,vars,funcs,plotting)

# PLOT_DATAFILE(): Plot datapoints listed in datafile
def plot_datafile(g,axes,settings,linestyles,plotwords,vars,funcs,plotting):
  global stylestr, pointsize, pointtype, linewidth, pointlinewidth, linestyle, plotcolour

  datafile = ''
  axis_x   = 1
  axis_y   = 1
  using    = ''
  every    = 1
  index    = -1 # GnuPlot API doesn't have an index value for "plot all indices", so I define it to be -1.
  title    = ''
  quotetype= ''
  state    = 0
  stylestr = settings['DATASTYLE']
  pointsize= settings['POINTSIZE']
  pointtype  = -1
  linestyle  = -1
  plotcolour = -1
  linewidth  = settings['LINEWIDTH']
  pointlinewidth = settings['POINTLINEWIDTH']
  withstate  = 0

  for i in range(len(plotwords)):
    item = plotwords[i].strip()

    if (state < 2):
     if autocomplete(item, "using"  ,1): state = 2 ; continue
     if autocomplete(item, "every"  ,1): state = 3 ; continue
     if autocomplete(item, "index"  ,1): state = 4 ; continue
     if autocomplete(item, "notitle",3): title = ''; continue
     if autocomplete(item, "title"  ,1): state = 5 ; continue # also uses state 6
     if autocomplete(item, "with"   ,1): state = 7 ; continue
     if autocomplete(item, "axes"   ,1): state = 8 ; continue

    if (state == 4): # set 'index' setting
      try:
        index = int(item)
      except: 
        print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
        print "'index' keyword should be followed by an integer"
        return # Error
      state = 1
      continue

    if (state == 3): # set 'every' setting
      try:
        every = int(item)
      except: 
        print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
        print "'every' keyword should be followed by an integer"
        return # Error
      state = 1
      continue

    if (state == 2): # read 'using' string
      using = item
      state = 1
      continue

    if (state == 5): # read 'title' string
      test = re.match(r"""^('|")(.*)$""",item)
      if (test == None):
        print "Error: title must be in quotes"
      item = test.group(2)
      title = ""
      quote_type = test.group(1)
      state = 6
    if (state == 6): # Multiple word titles supported
      test = re.match("""^(.*)%s$"""%quote_type,item)
      if (test == None):
        title = title + item + " "
      else:
        title = title + test.group(1)
        state=1 # We have hit a closing quote
      continue

    if (state == 7): # read 'with' clauses
      process_with_word(settings, linestyles, item)
      continue

    if (state == 8): # read axes string
      try:
        test = re.match(r"""^x(\d\d*)y(\d\d*)$""",item)
        axis_x = int(test.group(1))
        axis_y = int(test.group(2))
        if ((axis_x < 1) or (axis_y < 1)): raise ValueError # Don't allow user to use axis zero!
        if (not axis_x in axes['x']):
         axes['x'][axis_x] = {'SETTINGS':gp_settings.default_axis.copy(), 'MIN_USED':None, 'MAX_USED':None, 'AXIS':None} # Create axes if they don't already exist
        if (not axis_y in axes['y']):
         axes['y'][axis_y] = {'SETTINGS':gp_settings.default_axis.copy(), 'MIN_USED':None, 'MAX_USED':None, 'AXIS':None}
      except:
        print "Error: axes declaration must take the form x<n>y<m> where {n,m}>0."
        return # Error
      state=1
      continue

    if ((state == 0) and (item[0] == "'")):
      test = re.match(r"""^'(.*)'$""",item)
      if (test == None):
        print """Badly formed datafile name "%s"."""%item
        return # Error
      datafile = test.group(1)
      title    = test.group(1)
      title    = re.sub('_', r'\_', title) # LaTeX does not like underscores....
      state = 1
      continue

    print "Syntax Error: Unrecognised word '%s'"%item
    return # Error

    # We have now read our filename, and operators on the filename

  if (using == ''):
    using = '1:2'
    if ((stylestr == 'xerrorbars') or (stylestr == 'yerrorbars')): using = '1:2:3'
    if  (stylestr == 'xyerrorbars')                              : using = '1:2:3:4'
  columns_req = 2
  if ((stylestr == 'xerrorbars') or (stylestr == 'yerrorbars')): columns_req = 3
  if  (stylestr == 'xyerrorbars')                              : columns_req = 4

  try:
    datafile_totalgrid = gp_datafile.gp_dataread(datafile, index, using, every, vars, funcs)
  except:
    print "Error reading input datafile %s"%datafile
    print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
    return # Error

  if (len(datafile_totalgrid) < 2):
    print "Warning: No datapoints found in file '%s'."%datafile
    return

  if (datafile_totalgrid[0][1] < columns_req):
    print "Need at least %d columns to plot data from file '%s'."%(columns_req,datafile)
    return

  for data_section in range(1,len(datafile_totalgrid)): # Loop over data sections within index, plotting each as a separate line

    [rows, columns, datagrid] = datafile_totalgrid[data_section]

    # Recalculate data bounding box
    if (not plotting):
     for i in range(rows):
      if ((axes['y'][axis_y]['SETTINGS']['AUTOSCALE'] == 'ON') or  # If data is off the y-axis' range, don't consider it for calculating range of x-axis
          ((datagrid[i][1] >= axes['y'][axis_y]['MIN_USED']) and (datagrid[i][1] <= axes['y'][axis_y]['MAX_USED'])) ):
       if ((axes['x'][axis_x]['SETTINGS']['LOG'] != 'ON') or (datagrid[i][0] > 0)): # Don't count negative points on log axes
        if ((axes['x'][axis_x]['MIN_USED'] == None) or (axes['x'][axis_x]['MIN_USED'] > datagrid[i][0])): axes['x'][axis_x]['MIN_USED'] = datagrid[i][0]
        if ((axes['x'][axis_x]['MAX_USED'] == None) or (axes['x'][axis_x]['MAX_USED'] < datagrid[i][0])): axes['x'][axis_x]['MAX_USED'] = datagrid[i][0]
      if ((axes['x'][axis_x]['SETTINGS']['AUTOSCALE'] == 'ON') or  # If data is off the x-axis' range, don't consider it for calculating range of y-axis
          ((datagrid[i][0] >= axes['x'][axis_x]['MIN_USED']) and (datagrid[i][0] <= axes['x'][axis_x]['MAX_USED'])) ):
       if ((axes['y'][axis_y]['SETTINGS']['LOG'] != 'ON') or (datagrid[i][1] > 0)): # Don't count negative points on log axes
        if ((axes['y'][axis_y]['MIN_USED'] == None) or (axes['y'][axis_y]['MIN_USED'] > datagrid[i][1])): axes['y'][axis_y]['MIN_USED'] = datagrid[i][1]
        if ((axes['y'][axis_y]['MAX_USED'] == None) or (axes['y'][axis_y]['MAX_USED'] < datagrid[i][1])): axes['y'][axis_y]['MAX_USED'] = datagrid[i][1]

    # Plot dataset
    if (data_section == 1): repeat = 0 # Are we to use same style as previous lump of data we plotted?
    else                  : repeat = 1
    plot_dataset(g,axes,axis_x,axis_y,settings,title,datagrid,rows,columns,"datafile '%s'"%datafile,repeat,plotting)

# PLOT_FUNCTION(): Plot a function
def plot_function(g,axes,settings,linestyles,plotwords,vars,funcs,plotting):
  global stylestr, pointsize, pointtype, linewidth, pointlinewidth, linestyle, plotcolour

  function = ''
  axis_x   = 1
  axis_y   = 1
  xname    = ''
  title    = ''
  titledefault = 1
  quotetype= ''
  state    = 0
  stylestr = settings['FUNCSTYLE']
  pointsize= settings['POINTSIZE']
  pointtype  = -1
  linestyle  = -1
  plotcolour = -1
  linewidth  = settings['LINEWIDTH']
  pointlinewidth = settings['POINTLINEWIDTH']
  withstate  = 0

  for i in range(len(plotwords)):
    item = plotwords[i].strip()

    if (state < 2):
     if autocomplete(item, "notitle", 3): title = ''; continue
     if autocomplete(item, "title"  , 1): state = 5 ; continue # also uses state 6
     if autocomplete(item, "with"   , 1): state = 7 ; continue
     if autocomplete(item, "axes"   , 1): state = 8 ; continue

    if (state == 5): # read 'title' string
      titledefault = 0
      test = re.match(r"""^('|")(.*)$""",item)
      if (test == None):
        print "Error: title must be in quotes"
      item = test.group(2)
      title = ""
      quote_type = test.group(1)
      state = 6
    if (state == 6): # Multiple word titles supported
      test = re.match("""^(.*)%s$"""%quote_type,item)
      if (test == None):
        title = title + item + " "
      else:
        title = title + test.group(1)
        state=1 # We have hit a closing quote
      continue

    if (state == 7): # read 'with' clauses
      process_with_word(settings, linestyles, item)
      continue

    if (state == 8): # read axes string
      try:
        test = re.match(r"""^x(\d\d*)y(\d\d*)$""",item)
        axis_x = int(test.group(1))
        axis_y = int(test.group(2))
        if ((axis_x < 1) or (axis_y < 1)): raise ValueError # Don't allow user to use axis zero!
        if (not axis_x in axes['x']):
         axes['x'][axis_x] = {'SETTINGS':gp_settings.default_axis.copy(), 'MIN_USED':-10.0, 'MAX_USED':10.0, 'AXIS':None} # Create axes if they don't already exist; linear axis from -10.0 to 10.0
        if (not axis_y in axes['y']):
         axes['y'][axis_y] = {'SETTINGS':gp_settings.default_axis.copy(), 'MIN_USED':None, 'MAX_USED':None, 'AXIS':None}
      except:
        print "Error: axes declaration must take the form x<n>y<m> where {n,m}>0."
        return # Error
      state=1
      continue

    if (state < 2): # Receive function
      function = function + item
      if (titledefault != 0): title = title + item
      xname    = 'x' # We always vary variable x along x axis...
      state = 1
      continue

    print "Syntax Error: Unrecognised word '%s'"%item
    return # Error

  # We have now read our function expression, and operators on it

  # Make raster to evaluate function along, between limits of x-axis.
  if (axes['x'][axis_x]['SETTINGS']['LOG'] == 'ON'): xrast = lograst(axes['x'][axis_x]['MIN_USED'], axes['x'][axis_x]['MAX_USED'], settings['SAMPLES'])
  else:                                              xrast = linrast(axes['x'][axis_x]['MIN_USED'], axes['x'][axis_x]['MAX_USED'], settings['SAMPLES'])

  datagrid = []
  local_vars = vars.copy()
  for x in xrast:
   try:
    local_vars[xname] = x
    val = gp_eval.gp_eval(function,local_vars,funcs)
    datagrid.append([x,val])
    if (not plotting): # Update bounding-boxes of y-axis
     if ((axes['y'][axis_y]['SETTINGS']['LOG'] != 'ON') or (val > 0)): # Don't count negative points on log axes
      if ((axes['y'][axis_y]['MIN_USED'] == None) or (axes['y'][axis_y]['MIN_USED'] > val)): axes['y'][axis_y]['MIN_USED'] = val
      if ((axes['y'][axis_y]['MAX_USED'] == None) or (axes['y'][axis_y]['MAX_USED'] < val)): axes['y'][axis_y]['MAX_USED'] = val
       
   except:
    print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
    return # Error

  # Plot dataset
  plot_dataset(g,axes,axis_x,axis_y,settings,title,datagrid,2,settings['SAMPLES'],"function '%s'"%function,0,plotting)

# PROCESS_WITH_WORD(): Process a word after the "with" directive

def process_with_word(settings, linestyles, word, iteration=0):
  global stylestr, pointsize, pointtype, linewidth, linestyle, plotcolour, pointlinewidth
  global withstate

  if (len(word) == 0): return

  if (withstate == 0):
   if (autocomplete(word,'linetype',5)       or  (word == 'lt')  ): withstate = 1 ; return
   if (autocomplete(word,'linewidth',5)      or  (word == 'lw')  ): withstate = 2 ; return
   if (autocomplete(word,'pointsize',6)      or  (word == 'ps')  ): withstate = 3 ; return
   if (autocomplete(word,'pointtype',6)      or  (word == 'pt')  ): withstate = 4 ; return
   if (autocomplete(word,'linestyle',5)      or  (word == 'ls')  ): withstate = 5 ; return
   if (autocomplete(word,'pointlinewidth',6) or  (word == 'plw') ): withstate = 6 ; return
   
   if (autocomplete(word,"colour",1) or autocomplete(word,"color",1) ): withstate = 7 ; return

   if autocomplete(word,"lines",1)      : stylestr = 'lines'      ; return
   if autocomplete(word,"points",1)     : stylestr = 'points'     ; return
   if autocomplete(word,"lp",2)         : stylestr = 'linespoints'; return
   if autocomplete(word,"linespoints",5): stylestr = 'linespoints'; return
   if autocomplete(word,"dots",1)       : stylestr = 'dots'       ; return
   if autocomplete(word,"xerrorbars",2) : stylestr = 'xerrorbars' ; return
   if autocomplete(word,"yerrorbars",2) : stylestr = 'yerrorbars' ; return
   if autocomplete(word,"xyerrorbars",2): stylestr = 'xyerrorbars'; return

  if (withstate == 1):
   try:
    linestyle = int(word) ; withstate = 0
   except:
    print "Error: linetype should be followed by an integer."
   return

  if (withstate == 2):
   try:
    linewidth = float(word) ; withstate = 0
   except:
    print "Error: linewidth should be followed by a floating point value."
   return

  if (withstate == 3):
   try:
    pointsize = float(word) ; withstate = 0
   except:
    print "Error: pointsize should be followed by a floating point value."
   return

  if (withstate == 4):
   try:
    pointtype = int(word) ; withstate = 0
   except:
    print "Error: pointtype should be followed by an integer."
   return

  if (withstate == 5):
    try:
     number = int(word) ; withstate = 0
    except:
     print "Error: linestyle should be followed by the number of the desired linestyle."

    if (not linestyles.has_key(number)):
     print "Error: linestyle %d is not defined."%number
    else:
     if (iteration > 4):
      print "Iteration ceiling hit whilst processing linestyle %d."%number
     else:
      for word in linestyles[number].split():
        process_with_word(settings,linestyles,word,iteration+1)
    return

  if (withstate == 6):
   try:
    pointlinewidth = float(word) ; withstate = 0
   except:
    print "Error: pointlinewidth should be followed by a floating point value."
   return

  if (withstate == 7):
   try:
    plotcolour = int(word) ; withstate = 0
   except:
    print "Error: colour should be followed by an integer."
   return

  print "Warning: Unrecognised word '%s' in with section"%word
  return

# PLOT_LINEWIDTH(): Turn a numerical linewidth into a PyX style

def plot_linewidth(width):
  output = style.linewidth.THIN
  if (width >= 0.2): output = style.linewidth.THIn
  if (width >= 0.4): output = style.linewidth.THin
  if (width >= 0.6): output = style.linewidth.Thin
  if (width >= 0.8): output = style.linewidth.thin
  if (width >= 1.0): output = style.linewidth.normal
  if (width >= 2.0): output = style.linewidth.thick
  if (width >= 3.0): output = style.linewidth.Thick
  if (width >= 4.0): output = style.linewidth.THick
  if (width >= 5.0): output = style.linewidth.THIck
  if (width >= 6.0): output = style.linewidth.THICk
  if (width >= 7.0): output = style.linewidth.THICK
  return output

# PLOT_DATASET(): Plot a datagrid

def plot_dataset(g,axes,axis_x,axis_y,settings,title,datagrid,rows,columns,description,repeat,plotting):
  global successful_plot_operations
  global stylestr, pointsize, pointtype, linewidth, pointlinewidth, linestyle, plotcolour
  global linecount, ptcount, colourcnt

  symbol_list = [graph.style.symbol.cross, graph.style.symbol.plus, graph.style.symbol.square, graph.style.symbol.triangle, graph.style.symbol.circle, graph.style.symbol.diamond]

  linestyle_list = [style.linestyle.solid, style.linestyle.dashed, style.linestyle.dotted, style.linestyle.dashdotted, style.dash((3,2,1,1,1,2),0), style.dash((3,1,1,3,1,1),0), style.dash((3,1,3,1,3,1),0), style.dash((4,4),0)]

  colour_list = [color.grey.black, color.rgb.red, color.rgb.blue, color.cmyk.Magenta, color.cmyk.Cyan, color.cmyk.Brown, color.cmyk.Salmon, color.cmyk.Gray, color.rgb.green, color.cmyk.NavyBlue, color.cmyk.Periwinkle, color.cmyk.PineGreen, color.cmyk.SeaGreen, color.cmyk.GreenYellow, color.cmyk.Orange, color.cmyk.CarnationPink, color.cmyk.Plum ]

  try:
    stylelist = []
    dx = None
    dy = None

    if (repeat == 1):
     localtitle = None    # Stops key have multiple references to the same line
    else:
     localtitle = title
     if ((localtitle != None) and (len(localtitle) < 1)): # Don't put blank titles into key
      localtitle = None

    # Determine what colour to use to plot this dataset

    if (settings['COLOUR'] == 'ON'): # Match colour
     if (plotcolour < 0):
      if (repeat != 0): colourcnt  = colourcnt -1
      colour = colour_list[colourcnt%len(colour_list)] # If plotcolour not set, automatically increment colour
      colourcnt  = colourcnt + 1
     else:
      colour = colour_list[plotcolour%len(colour_list)] # otherwise used specified colour
    else:
      colour = color.grey.black # If monochrome, then set colour to black

    # Now determine what linestyle to use, and make a list of style items to plot (in the case of linespoints or error bars, there are more than one)

    if (re.search('lines'    ,stylestr) != None): # Match lines and linespoints
      if (linestyle < 0):
        if (settings['COLOUR'] == 'ON'):
          linestyle = 0 # Colour lines are automatically all solid
        else:
          if (repeat != 0): linecount = linecount - 1
          linestyle = linecount # Monochrome lines automatically have different linestyles
          linecount = linecount + 1
      stylelist.append(graph.style.line(lineattrs=[ linestyle_list[linestyle%len(linestyle_list)], plot_linewidth(linewidth), colour ]))
    if (re.search('points'   ,stylestr) != None): # Match points and linespoints
      if (pointtype < 0):
        if (repeat != 0): ptcount   = ptcount - 1
        pointtype = ptcount # Both colour and monochrome point types automatically increment
        ptcount   = ptcount + 1
      stylelist.append(graph.style.symbol(size=0.1*pointsize, symbol=symbol_list[pointtype%len(symbol_list)], symbolattrs=[colour,plot_linewidth(pointlinewidth)]))
    if (re.search('dots'   ,stylestr) != None): # Match dots
      stylelist.append(graph.style.symbol(size=0.005*pointsize, symbol=graph.style.symbol.circle, symbolattrs=[colour,deco.filled([colour])]))
    if (re.search('errorbars',stylestr) != None): # Match {x|y|xy}errorbars
      if (linestyle < 0):
        if (settings['COLOUR'] == 'ON'):
          linestyle = 0
        else:
          if (repeat != 0): linecount = linecount - 1
          linestyle = linecount
          linecount = linecount + 1
      stylelist.append(graph.style.errorbar(errorbarattrs=[linestyle_list[linestyle%len(linestyle_list)], plot_linewidth(linewidth), colour]))
      stylelist.append(graph.style.symbol(size=0.1*pointsize, symbol=graph.style.symbol.plus, symbolattrs=[colour,plot_linewidth(linewidth)]))
      if   ((stylestr == 'xerrorbars' ) and (columns > 2)): dx = 3 ; dy = None
      elif ((stylestr == 'yerrorbars' ) and (columns > 2)): dy = 3 ; dx = None
      elif ((stylestr == 'xyerrorbars') and (columns > 3)): dx = 3 ; dy = 4

    # Clean up datagrid, removing any points < 0 which might go on log axis
    datagrid_cpy_list = []
    datagrid_cpy      = []
    for ptlist in datagrid:
      if (((axes['x'][axis_x]['SETTINGS']['LOG'] == 'ON') and (ptlist[0] <= 0.0)) or
          ((axes['y'][axis_y]['SETTINGS']['LOG'] == 'ON') and (ptlist[1] <= 0.0))):
       if (len(datagrid_cpy) != 0):
        datagrid_cpy_list.append(datagrid_cpy)
        datagrid_cpy = [] # Split dataset, not connecting points where it goes to minus infinity
      else:
       datagrid_cpy.append(ptlist)
    if (len(datagrid_cpy) != 0): datagrid_cpy_list.append(datagrid_cpy)
    if (len(datagrid_cpy_list) == 0): return # No data to plot!

    for datagrid_cpy in datagrid_cpy_list:
     if (localtitle != None): successful_plot_operations = successful_plot_operations + 1 # Only count datasets which will put an entry into the key
     if (not plotting): return # First time around, we quit here and don't actually plot anything

     if (axis_x == 1): x_axisname = ""          # x1 axis is called x in PyX
     else            : x_axisname = str(axis_x) # but x2 axis is called x2 in PyX
     if (axis_y == 1): y_axisname = ""          # likewise for y
     else            : y_axisname = str(axis_y)
     x_set = "x"+x_axisname+"=1,"
     y_set = "y"+y_axisname+"=2,"
     if (dx != None): dx_set = "dx"+x_axisname+"=dx,"
     else           : dx_set = ""
     if (dy != None): dy_set = "dy"+y_axisname+"=dy,"
     else           : dy_set = ""
     exec "g.plot(graph.data.list(datagrid_cpy,"+x_set+y_set+dx_set+dy_set+"title=localtitle),styles=stylelist)"
     localtitle = None # Only put a title on one dataset
  except:
    print "Failed while plotting %s:"%description
    print "Error:" , sys.exc_info()[1], "(" , sys.exc_info()[0] , ")"
    return # Error
  return

# LINRAST()
# LOGRAST(): Linear and log rasters, used for evaluating functions

def linrast( xmin, xmax, steps):
    return [ xmin + i* float(xmax-xmin)/steps for i in range(steps+1) ]
    
def lograst ( xmin , xmax , nsteps):
    if (xmin <= 0.0):
     print "Warning: Minimum end of logarithmic axis range is trying to expand to negative ordinates. Setting new minimum for axis range to 1e-6."
     xmin = 1e-6
    if (xmax <= 0.0):
     print "Warning: Maximum end of logarithmic axis range is trying to expand to negative ordinates. Setting new maximum for axis range to 1.0."
     xmax = 1.0
    return [ xmin * (xmax / xmin ) ** ( float(i)/nsteps ) for i in range(nsteps+1) ]

