Personal tools
You are here: Home / CarPy / Burns' Python Scripts / Modules / pyflot.py

pyflot.py

A python class that generates javascript-based flot plot output.

Python Source icon pyflot.py — Python Source, 21 KB (21750 bytes)

File contents

'''pyflot:  a python wrapper around the JS Flot package.

Author:  Chris Burns <cburns@obs.carnegiescience.edu>

Note:  This module produces JavaScript (JS) that will make pretty plots.  The JS
       itself will be ugly.  Very ugly.  The point is to do the work in python
       and produce JS that *works*.
'''

import numpy as np
import string,os,sys
import copy

base = os.path.dirname(__file__)
flotdir = os.path.join(base, 'flot')

generic_header = '''
<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
      <title>Flot Examples</title>
      <link href="layout.css" rel="stylesheet" type="text/css">
      <!--[if lte IE 8]><script language="javascript" type="text/javascript" src="%s/excanvas.min.js"></script><![endif]-->
''' % (flotdir)

for file in ['jquery.js','jquery.flot.js','jquery.flot.symbol.js',\
      'jquery.flot.errorbars.js']:
   generic_header += '      <script language="javascript" type="text/javascript" src="%s/%s"></script>\n' % (flotdir,file)


symbols = {'o':'circle',
           's':'square',
           'd':'diamond',
           '^':'triangle',
           'x':'cross'}
rotate_style = "-webkit-transform: rotate(-90deg);-moz-transform: rotate(-90deg); -ms-transform: rotate(-90deg); -o-transform: rotate(-90deg); transform: rotate(-90deg); zoom: 1;"

def indent(s):
   s = string.split(s, '\n')
   s = ['   ' + string.strip(line) for line in s]
   s = string.join(s, '\n')
   return s

class SpecialValue:
   '''A base class for special objects that mirror python built-ins or standard
   objects that don't have __repr__'s that conform to JS, so need to be translated.'''

   def __init__(self, val):
      self.val = val

   def __repr__(self):
      return self.val.__repr__()


class TrueFalse(SpecialValue):
   '''A boolean-like object that replaces the usual True/False built-ins.
   Needed because JS un-capitalizes True and False.'''

   def __init__(self, val):
      if type(val) is not type(True):
         raise ValueError, 'val must be Boolean'
      self.val = val

   def __repr__(self):
      if self.val:
         return "true"
      else:
         return "false"

class Javascript(SpecialValue):
   
   def __init__(self, val):
      if type(val) is not type(""):
         raise ValueError, "value must be a string"
      self.val = val

   def __repr__(self):
      return self.val

JS = Javascript   # short-hand.

OverrideTypes = {
      type(True):TrueFalse
}
      


class Option:
   '''A dictionary-like object that can be nested and understands
   hiarichical dot-notation.'''
   def __init__(self, options=[]):
      self.dict = {}
      for key in options:
         self.dict[key] = None

   def keys(self):
      return self.dict.keys()

   def __getitem__(self, key):
      if key in self.dict:
         #if isinstance(self.dict[key], SpecialValue):
         #   return self.dict[key].val
         #else:
         return self.dict[key]
      else:
         return None

   def __setitem__(self, key, value):
      if type(value) in OverrideTypes:
         self.dict[key] = OverrideTypes[type(value)](value)
      else:
         self.dict[key] = value

   def __delitem__(self, key):
      self.dict.__deltiem__(key)

   def __contains__(self, key):
      return self.dict.__contains__(key)

   def __iter__(self):
      return self.dict.__iter__()

   def __repr__(self):
      st = '{'
      items = self.keys()
      for i,item in enumerate(items):
         val = self.__getitem__(item)
         if val is not None and val.__repr__() not in ["", []]:
            #if val is True:
            #   st += item+": true,"
            #elif val is False:
            #   st += item+": false,"
            #else:
            st += item+": "+ val.__repr__() + ","
      if st == '{':  return ""
      st = st[:-1] + ' }'
      return st

   def __str__(self):
      return self.__repr__()

   def __getattr__(self, key):
      if key in self.__dict__['dict']:
         return self.__getitem__(key)
      else:
         raise AttributeError, "No such attribute %s" % key

   def __setattr__(self,key,value):
      if key != 'dict' and key in self.__dict__['dict']:
         self.__setitem__(key, value)
         #self.__dict__['dict'][key] = value
      else:
         self.__dict__[key] = value

class Series:

   def __init__(self, x, y, **kwds):
      '''Create a series object.  A series is the basic plot element in 
      flot. Only required arguments are data in x and y.  The options are:
      '''
      self.x = [];  self.y = []
      self.xerr = [];  self.yerr = []
      for xi in x:
         if xi is not None:
            self.x.append(float(xi))
         else:
            self.x.append(None)
      for yi in y:
         if yi is not None:
            self.y.append(float(yi))
         else:
            self.y.append(None)
      if 'xerr' in kwds and kwds['xerr'] is not None:
         for xi in kwds['xerr']:
            if xi is not None:
               if len(xi) == 1:
                  self.xerr.append([float(xi)])
               elif len(xi) == 2:
                  self.xerr.append([float(xi[0]),float(xi[1])])
               else:
                  raise ValueError, "xerr must be 1XN or 2XN"
            else:
               self.xerr.append(None)
      if 'yerr' in kwds and kwds['yerr'] is not None:
         for yi in kwds['yerr']:
            if yi is not None:
               if len(np.shape(yi)) == 0:
                  self.yerr.append([float(yi)])
               elif len(np.shape(yi)) == 1 and len(yi) == 2:
                  self.yerr.append([float(yi[0]),float(yi[1])])
               else:
                  raise ValueError, "yerr must be 1XN or 2XN"
            else:
               self.yerr.append(None)

      if len(self.x) != len(self.y):
         raise ValueError, "Error, x and y must have the same shape"
      if len(self.xerr) > 0 and len(self.xerr) != len(self.x):
         raise ValueError, "Error, x and xerr must have the same shape"
      if len(self.yerr) > 0 and len(self.yerr) != len(self.y):
         raise ValueError, "Error, y and yerr must have the same shape"

      self.options = Option(options=[
            'color',      # color of the series
            'label',       # label in the legend
            'lines',       # line options
            'bars',        # bar options
            'points',      # point options
            'xaxis',       # which x-axis to use
            'yaxis',       # which y-axis to use
            'clickable',   # can we click the object?
            'hoverable',   # does hovering do anything?
            'shadowSize'])  # 

      self.options.lines = Option(options=[
         'show',       # show the line?
         'lineWidth',  # width of the line
         'fill',       # fill the line?
         'fillColor',  # fill color
         'steps'])      # use steps instead of segments?
      self.options.points = Option(options=[
         'show',       #show the point
         'fill',       # fill the symbols?
         'fillColor',  # color to fill with
         'radius',     # radius of symbol
         'symbol',     # symbol to use
         'errorbars',  # kind of errorbars to plot
         'xerr',       # x errorbar options
         'yerr'])      # y errorbar options

      self.options.points.xerr = Option(options=[
         'show',       # Show the errorbar?
         'asymmetric', # whether or not the errors are symmetric
         'upperCap',   # null or '-' or function
         'lowerCap',   # null or '-' or function
         'color',      # null or color
         'radius'])    # amount to blank-out so we don't over-plot the symbols
      self.options.points.yerr = Option(options=[
         'show',       # Show the errorbar?
         'asymmetric', # whether or not the errors are symmetric
         'upperCap',   # null or '-' or function
         'lowerCap',   # null or '-' or function
         'color',      # null or color
         'radius'])    # amount to blank-out so we don't over-plot the symbols

      self.options.bars = Option(options=[
         'show',       # show the bar?
         'fill',       # fill the bars?
         'fillColor',  # color to fill with
         'barWidth',   # width of the bar
         'align',      # left-aligned?
         'horizontal']) # plot horizontal bars?

   def __getattr__(self, key):
      if 'options' in self.__dict__ and key in self.__dict__['options']:
         return self.__dict__['options'][key]
      #if key in self.__dict__:
      return self.__dict__[key]
      #raise AttributeError, "no such attribute %s" % key

   def __setattr__(self, key, value):
      if key == 'options':
         self.__dict__[key] = value
      elif 'options' in self.__dict__ and key in self.__dict__['options']:
         self.options[key] = value
      else:
         self.__dict__[key] = value

   def __repr__(self):
      '''print out the data in the proper format for flot.'''
      st = '{ '
      st += "data: ["
      for i in range(len(self.x)):
         if self.x[i] is not None and self.y[i] is not None:
            dat = ["%f" % self.x[i], "%f" % self.y[i]]
            if self.xerr:
               dat += ["%f" % val for val in self.xerr[i]]
            if self.yerr:
               dat += ["%f" % val for val in self.yerr[i]]
            st += "[" + ",".join(dat) + "],"
         else:
            st += "null,"
      st = st[:-1]+"] ,"
      items = self.options.keys()
      for i,item in enumerate(items):
         val = self.options.__getitem__(item)
         if val is not None and val.__repr__() != "":
            if val is True:
               st += item+": true,"
            elif val is False:
               st += item+": false,"
            else:
               st += item+": "+ val.__repr__() + ","
      st = st[:-1] + ' }'
      return st

class Markings:
   '''A class similar to Series, but used with the grid markings option.'''
   def __init__(self, xaxis=(None,None), yaxis=(None,None), **kwds):
      '''Create a markings object.  A markings is the basic plot element that
      draws elements on the grid.
      '''

      self.options = Option(options=[
            'color',       # color of this grid element
            'lineWidth'])  # line width for this element

      for key in kwds:
         if key in self.options:
            self.options[key] = kwds[key]

      self.xaxis = xaxis
      self.yaxis = yaxis


   def __getattr__(self, key):
      if 'options' in self.__dict__ and key in self.__dict__['options']:
         return self.__dict__['options'][key]
      #if key in self.__dict__:
      return self.__dict__[key]
      #raise AttributeError, "no such attribute %s" % key

   def __setattr__(self, key, value):
      if key == 'options':
         self.__dict__[key] = value
      elif 'options' in self.__dict__ and key in self.__dict__['options']:
         self.options[key] = value
      else:
         self.__dict__[key] = value

   def __repr__(self):
      '''print out the data in the proper format for flot.'''
      st = '{ '
      if self.xaxis[0] is not None or self.xaxis[1] is not None:
         st += 'xaxis: { '
         if self.xaxis[0] is not None:
            st += 'from: %f,' % (float(self.xaxis[0]))
         if self.xaxis[1] is not None:
            st += 'to: %f,' % (float(self.xaxis[1]))
         st = st[:-1] + '},'
      if self.yaxis[0] is not None or self.yaxis[1] is not None:
         st += 'yaxis: { '
         if self.yaxis[0] is not None:
            st += 'from: %f,' % (float(self.yaxis[0]))
         if self.yaxis[1] is not None:
            st += 'to: %f,' % (float(self.yaxis[1]))
         st = st[:-1] + '},'
      items = self.options.keys()
      for i,item in enumerate(items):
         val = self.options.__getitem__(item)
         if val is not None and val.__repr__() != "":
            if val is True:
               st += item+": true,"
            elif val is False:
               st += item+": false,"
            else:
               st += item+": "+ val.__repr__() + ","
      st = st[:-1] + ' }'
      return st

class Label:
   '''A class that holds a label.  It will generate JS code to place the label
   within the appropriate placeholder in the document.'''

   def __init__(self, x, y, string, **kwds):
      '''Place a label [string] at ([x],[y]).'''

      self.x = float(x)
      self.y = float(y)
      self.string = str(string)
      self.kwds = kwds
      
   def render(self, id):
      st = "o = plot.pointOffset({x: %f, y:%f});\n" % (self.x, self.y)
      st += '$("#%s").append(\'<div style="position:absolute;left:\'+(o.left + 4)' %\
            (id)
      st += ' + \'px;top:\' + o.top + \'px;'
      for key in self.kwds:
         val = str(self.kwds[key]).replace("_","-")
         st += '%s: %s;' % (key, val)
      st += '">%s</div>\');\n' % (self.string)
      return st


class Plot:

   '''A class that containst the entire plot.  Methods are available to
   output the JS.'''

   def __init__(self, id='placeholder', size=(600,300)):

      self.size = size
      self.id = id
      self.data = []
      self.labels = []
      self.xlabel = None
      self.xlabel_style = None
      self.ylabel = None
      self.ylabel_style = None
      self.options = Option(options=[
            'series',      # global options for series
            'legend',      # options for the legend
            'xaxis',       # which x-axis to use
            'yaxis',       # which y-axis to use
            'grid',        # options for the grid
            'clickable',   # can we click the object?
            'hoverable',   # does hovering do anything?
            'shadowSize'])  # 

      self.options.series = Option(options=['lines','points','bars'])
      self.options.series.lines = Option(options=[
         'show',       # show the line?
         'lineWidth',  # width of the line
         'fill',       # fill the line?
         'fillColor',  # fill color
         'steps'])      # use steps instead of segments?
      self.options.series.points = Option(options=[
         'show',       #show the point
         'fill',       # fill the symbols?
         'fillColor',  # color to fill with
         'radius',     # radius of symbol
         'symbol'])    # symbol to use
      self.options.series.bars = Option(options=[
         'show',       # show the bar?
         'fill',       # fill the bars?
         'fillColor',  # color to fill with
         'barWidth',   # width of the bar
         'align',      # left-aligned?
         'horizontal']) # plot horizontal bars?
      
      self.options.legend = Option(options=[
         'show',       # show the legend?
         'labelFormatter',  # a label formatter
         'labelBoxBorderColor',
         'noColumns',
         'position',
         'margin',
         'backgroundColor',
         'backgroundOpacity',
         'container'])
      self.options.legend.show = False   # like matplotlib

      axis_opts = [
         'show',
         'position',
         'mode',
         'color',
         'tickColor',
         'min',
         'max',
         'autoscaleMargin',
         'transform',
         'inverseTransform',
         'ticks',
         'tickSize',
         'minTickSize',
         'tickFormatter',
         'tickDecimals',
         'labelWidth',
         'labelHeight',
         'reserveSpace',
         'tickLength',
         'alignTicksWithAxis']
      self.options.xaxis = Option(options=axis_opts)
      self.options.yaxis = Option(options=axis_opts)

      self.options.grid = Option(options=[
         'show',
         'aboveData',
         'color',
         'backgroundColor',
         'labelMargin',
         'axisMargin',
         'markings',
         'borderWidth',
         'borderColor',
         'minBorderMargin',
         'clickable',
         'hoverable',
         'autoHighlight',
         'mouseActiveRadius'])
      self.options.grid.markings = []

   def plot(self, x, y=None, fmt='o', **kwds):
      '''Draw a line.  Options for the line can be set in the function
      call.  Try to make it act like matplotlib.'''
      if len(np.shape(x)) != 1:
         raise ValueError, "x must be 1-D list-like object"
      if y is None:
         y = range(len(x))
      else:
         if len(np.shape(x)) != 1:
            raise ValueError, "y must be 1-D list-like object"

      newseries = Series(x, y)
      if '-' in fmt:  
         newseries.lines.show = True
      else:
         newseries.lines.show = False
      for symb in symbols:
         if symb in fmt:
            newseries.points.show = True
            newseries.points.symbol = symbols[symb]
            break
      if 'label' in kwds:
         newseries.label = kwds['label']
         self.options.legend.show = True

      self.data.append(newseries)
      return newseries

   def errorbar(self, x, y, xerr=None, yerr=None, fmt='o', **kwds):
      if len(np.shape(x)) != 1:
         raise ValueError, "x must be 1-D list-like object"
      if y is None:
         y = range(len(x))
      else:
         if len(np.shape(x)) != 1:
            raise ValueError, "y must be 1-D list-like object"

      args = {}
      etype = ""
      nxerr = nyerr = 0
      if xerr is not None:
         nxerr = len(np.shape(xerr))
         if nxerr not in [1,2]:
            raise ValueError, "xerr must be 1XN or 2XN"
         args['xerr'] = xerr
         etype += "x"
      if yerr is not None:
         nyerr = len(np.shape(yerr))
         if nyerr not in [1,2]:
            raise ValueError, "xerr must be 1XN or 2XN"
         args['yerr'] = yerr
         etype += "y"

      newseries = Series(x,y, **args)
      if '-' in fmt:  
         newseries.lines.show = True
      else:
         newseries.lines.show = False
      for symb in symbols:
         if symb in fmt:
            newseries.points.show = True
            newseries.points.symbol = symbols[symb]
            break
      if 'label' in kwds:
         newseries.label = kwds['label']
         self.options.legend.show = True

      newseries.points.errorbars = etype
      if nxerr > 0: newseries.points.xerr.show = True
      if nyerr > 0: newseries.points.yerr.show = True
      if nxerr == 2:
         newseries.points.xerr.asymmetric = True
      if nyerr == 2:
         newseries.points.yerr.asymmetric = True

      self.data.append(newseries)
      return newseries


   #def set_xlabel(self, label, style=None):
   #   self.xlabel = label
   #   if style:
   #      self.xlabel_style = style

   #def set_ylabel(self, label, style=None):
   #   self.ylabel = label
   #   if style:
   #      self.ylabel_style = style

   def marking(self, xaxis=(None,None), yaxis=(None,None), **kwds):
      newmarking = Markings(xaxis,yaxis,**kwds)
      self.options.grid.markings.append(newmarking)

   def text(self, x, y, string, **kwds):
      newlabel = Label(x, y, string, **kwds)
      self.labels.append(newlabel)
      return newlabel

   def div(self):
      '''Output the <div></div> HTML needed to renter the graph.'''
      #if self.xlabel is not None or self.ylabel is not None:
      #   # Need to encase in table
      #   st = '<table border=0>\n   <tr>\n     '
      #   if self.ylabel is not None:
      #      st += "<td style='%s'>" \
      #            % (rotate_style)
      #      if self.ylabel_style is not None:
      #         st += "<span style='%s'>" % self.ylabel_style
      #      st += self.ylabel
      #      if self.ylabel_style is not None:
      #         st += "</span>"
      #      st += '</td>'
      #   st += '</td>\n      <td>'

      st = '<div id="%s" style="width:%dpx;height:%dpx;float:left;"></div>' % \
                  (self.id, self.size[0], self.size[1])
      #if self.xlabel is not None or self.ylabel is not None:
      #   st += '</td>\n   </tr>'
      #   st += '<tr>\n      <td></td><td align="center">'
      #   if self.xlabel is not None:
      #      if self.xlabel_style is not None:
      #         st += "<span style='%s'>" % self.xlabel_style
      #      st += self.xlabel
      #      if self.xlabel_style is not None:
      #         st += "</span>"
      #   st += '</td>\n   </tr>\n</table>'
      return st

   def js(self):
      '''Output the JS that defines the graph.'''
      st = '<script type="text/javascript">\n'
      st += '$(function () {\n'
      st += indent(self.__repr__())
      for label in self.labels:
         st += indent(label.render(self.id))
      st += '});\n'
      st += '</script>'
      return st

   def __repr__(self):
      # start out with the call`
      st = 'var plot = $.plot($("#%s"), [\n' % self.id

      # Now add in the data series
      for s in self.data:
         st += s.__repr__() + ','
      st = st[:-1] + " ]"

      # Now deal with the options
      if self.options.__repr__() != "":
         st = st + ", "+self.options.__repr__()

      # Close it up
      st = st + "\n);\n"
      return st

   def __getattr__(self, key):
      if 'options' in self.__dict__ and key in self.__dict__['options']:
         return self.__dict__['options'][key]
      return self.__dict__[key]

   def __setattr__(self, key, value):
      if key == 'options':
         self.__dict__[key] = value
      elif 'options' in self.__dict__ and key in self.__dict__['options']:
         self.options[key] = value
      else:
         self.__dict__[key] = value

   def htmlout(self, filename):
      '''Output in an HTML file that lets you test out the plot.'''
      f = open(filename, 'w')
      print >>f, generic_header
      print >>f, "   <body bgcolor=white>"
      print >>f, "   <h1>Test Output</h1>"
      print >>f, ""
      print >>f, self.div()
      print >>f, ""
      print >>f, self.js()
      print >>f, "   </body>\n</html>\n"
      f.close()