pyflot.py
A python class that generates javascript-based flot plot output.
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()