Source code for scitools.aplotter

"""
aplotter is a Python module for drawing graphs in pure ASCII format.
This allows graphs to be included in doc strings (Python programs)
or as illustrations in programs in any computer language.

The module offers a function

plot(x, y, draw_axis=True, plot_slope=True, plot_labels=False, dot='*',
     min_x=None, max_x=None, min_y=None, max_y=None,
     output=sys.stdout)

where x and y are sequences of x and y data for a curve.
Axes are automatically calculated from the x and y data if not
min_x, max_x, min_y or max_y are given.

Multiple curves in the plot is not supported.

Here are examples on various plot commands:

>>> from scitools.aplotter import plot
>>> from numpy import linspace, exp, cos, pi
>>> x = linspace(-2, 2, 81)
>>> y = exp(-0.5*x**2)*cos(pi*x)
>>> plot(x, y)
                                        |                                       
                                       -+1                                      
                                     // |\\                                     
                                    /   |  \                                    
                                   /    |   \                                   
                                  /     |   \                                   
                                  /     |    \                                  
                                 /      |     \                                 
                                /       |      \                                
                               /        |      \                                
   -------\                    /        |       \                    -------*   
---+-------\\-----------------/---------+--------\-----------------//-------+---
   -2        \                /         |        \                /       +2    
              \\              /         |         \             //              
                \            /          |         \            /                
                 \\         /           |          \         //                 
                   \       /            |           \       /                   
                    \    //             |            \-    //                   
                     ----               -0.63          ---/                     
                                        |                                       
>>> 
>>> plot(x, y, draw_axes=False)
                                                                                
                                       -+1                                      
                                     //  \\                                     
                                    /      \                                    
                                   /        \                                   
                                  /         \                                   
                                  /          \                                  
                                 /            \                                 
                                /              \                                
                               /               \                                
   -------\                    /                \                    -------*   
   +       \\                 /                  \                 //       +   
   -2        \                /                  \                /       +2    
              \\              /                   \             //              
                \            /                    \            /                
                 \\         /                      \         //                 
                   \       /                        \       /                   
                    \    //                          \-    //                   
                     ----               -0.63          ---/                     
                                                                                
>>> 
>>> # plot symbols (the dot argument) at data points:
>>> plot(x, y, plot_slope=False)
                                        |                                       
                                       *+1                                      
                                     ** |**                                     
                                    *   |  *                                    
                                   *    |   *                                   
                                        |                                       
                                  *     |    *                                  
                                 *      |     *                                 
                                *       |      *                                
                                        |                                       
   ********                    *        |       *                    ********   
---+-------**-----------------*---------+--------*-----------------**-------+---
   -2        *                          |                         *       +2    
              **              *         |         *             **              
                *            *          |         *            *                
                 **         *           |          *         **                 
                   *       *            |           *       *                   
                    *    **             |            **    **                   
                     ****               -0.63          ****                     
                                        |                                       
>>>  
>>> # drop axis labels:
>>> plot(x, y, plot_labels=False)
                                        |                                       
                                       -\                                       
                                     // |\\                                     
                                    /   |  \                                    
                                   /    |   \                                   
                                  /     |   \                                   
                                  /     |    \                                  
                                 /      |     \                                 
                                /       |      \                                
                               /        |      \                                
   -------\                    /        |       \                    -------*   
-----------\\-----------------/---------+--------\-----------------//-----------
             \                /         |        \                /             
              \\              /         |         \             //              
                \            /          |         \            /                
                 \\         /           |          \         //                 
                   \       /            |           \       /                   
                    \    //             |            \-    //                   
                     ----               |              ---/                     
                                        |                                       
>>> 
>>> plot(x, y, dot='o', plot_slope=False)
                                        |                                       
                                       o+1                                      
                                     oo |oo                                     
                                    o   |  o                                    
                                   o    |   o                                   
                                        |                                       
                                  o     |    o                                  
                                 o      |     o                                 
                                o       |      o                                
                                        |                                       
   oooooooo                    o        |       o                    oooooooo   
---+-------oo-----------------o---------+--------o-----------------oo-------+---
   -2        o                          |                         o       +2    
              oo              o         |         o             oo              
                o            o          |         o            o                
                 oo         o           |          o         oo                 
                   o       o            |           o       o                   
                    o    oo             |            oo    oo                   
                     oooo               -0.63          oooo                     
                                        |                                       
>>> 
>>> # store plot in a string:
>>> p = plot(x, y, output=str)
>>> print p
                                        |                                       
                                       -+1                                      
                                     // |\\                                     
                                    /   |  \                                    
                                   /    |   \                                   
                                  /     |   \                                   
                                  /     |    \                                  
                                 /      |     \                                 
                                /       |      \                                
                               /        |      \                                
   -------\                    /        |       \                    -------*   
---+-------\\-----------------/---------+--------\-----------------//-------+---
   -2        \                /         |        \                /       +2    
              \\              /         |         \             //              
                \            /          |         \            /                
                 \\         /           |          \         //                 
                   \       /            |           \       /                   
                    \    //             |            \-    //                   
                     ----               -0.63          ---/                     
                                        |                                       
"""
#-----------------------------------------------
#aplotter.py - ascii art function plotter
#Copyright (c) 2006, Imri Goldberg
#All rights reserved.
#Additional documentation and examples by Hans Petter Langtangen.
#
#Redistribution and use in source and binary forms,
#with or without modification, are permitted provided
#that the following conditions are met:
#
#    * Redistributions of source code must retain the
#		above copyright notice, this list of conditions
#		and the following disclaimer.
#    * Redistributions in binary form must reproduce the
#		above copyright notice, this list of conditions
#		and the following disclaimer in the documentation
#		and/or other materials provided with the distribution.
#    * Neither the name of the <ORGANIZATION> nor the names of
#		its contributors may be used to endorse or promote products
#		derived from this software without specific prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#-----------------------------------------------

import math, sys


EPSILON = 0.000001

def transposed(mat):
	result = []
	for i in xrange(len(mat[0])):
		result.append([x[i] for x in mat])
	return result

def y_reversed(mat):    
	result = []
	for i in range(len(mat)):
		result.append(list(reversed(mat[i])))
	return result

def sign(x):
	if 0<x:
		return 1
	if 0 == x:
		return 0
	return -1

[docs]class Plotter(object):
[docs] class PlotData(object):
[docs] def __init__(self, x_size, y_size, min_x, max_x, min_y, max_y, x_mod, y_mod): self.x_size = x_size self.y_size = y_size self.min_x = min_x self.max_x = max_x self.min_y = min_y self.max_y = max_y self.x_mod = x_mod self.y_mod = y_mod self.x_step = float(max_x - min_x)/float(self.x_size) self.y_step = float(max_y - min_y)/float(self.y_size) self.inv_x_step = 1/self.x_step self.inv_y_step = 1/self.y_step self.ratio = self.y_step / self.x_step
[docs] def __repr__(self): s = "size: %s, bl: %s, tr: %s, step: %s" % ((self.x_size, self.y_size), (self.min_x, self.min_y), (self.max_x, self.max_y), (self.x_step, self.y_step)) return s
[docs] def __init__(self, **kwargs): self.x_size = kwargs.get("x_size", 80) self.y_size = kwargs.get("y_size", 20) self.will_draw_axes = kwargs.get("draw_axes", True) self.new_line = kwargs.get("newline", "\n") self.dot = kwargs.get("dot", "*") self.plot_slope = kwargs.get("plot_slope", True) self.x_margin = kwargs.get("x_margin", 0.05) self.y_margin = kwargs.get("y_margin", 0.1) self.will_plot_labels = kwargs.get("plot_labels", True)
@staticmethod
[docs] def get_symbol_by_slope(slope, default_symbol): draw_symbol = default_symbol if slope > math.tan(3*math.pi/8): draw_symbol = "|" elif slope > math.tan(math.pi/8) and slope < math.tan(3*math.pi/8): draw_symbol = "/" elif abs(slope) < math.tan(math.pi/8): draw_symbol = "-" elif slope < math.tan(-math.pi/8) and slope > math.tan(-3*math.pi/8): draw_symbol = "\\" elif slope < math.tan(-3*math.pi/8): draw_symbol = "|" return draw_symbol
[docs] def plot_labels(self, output_buffer, plot_data): if plot_data.y_size < 2: return margin_factor = 1 do_plot_x_label = True do_plot_y_label = True x_str = "%+g" if plot_data.x_size < 16: do_plot_x_label = False elif plot_data.x_size < 23: x_str = "%+.2g" y_str = "%+g" if plot_data.x_size < 8: do_plot_y_label = False elif plot_data.x_size < 11: y_str = "%+.2g" act_min_x = (plot_data.min_x + plot_data.x_mod*margin_factor) act_max_x = (plot_data.max_x - plot_data.x_mod*margin_factor) act_min_y = (plot_data.min_y + plot_data.y_mod*margin_factor) act_max_y = (plot_data.max_y - plot_data.y_mod*margin_factor) if abs(act_min_x) < 1: min_x_str = "%+.2g" % act_min_x else: min_x_str = x_str % act_min_x if abs(act_max_x) < 1: max_x_str = "%+.2g" % act_max_x else: max_x_str = x_str % act_max_x if abs(act_min_y) < 1: min_y_str = "%+.2g" % act_min_y else: min_y_str = y_str % act_min_y if abs(act_max_y) < 1: max_y_str = "%+.2g" % act_max_y else: max_y_str = y_str % act_max_y min_x_coord = self.get_coord(act_min_x,plot_data.min_x,plot_data.x_step) max_x_coord = self.get_coord(act_max_x,plot_data.min_x,plot_data.x_step) min_y_coord = self.get_coord(act_min_y,plot_data.min_y,plot_data.y_step) max_y_coord = self.get_coord(act_max_y,plot_data.min_y,plot_data.y_step) #print plot_data y_zero_coord = self.get_coord(0, plot_data.min_y, plot_data.y_step) #if plot_data.min_x < 0 and plot_data.max_x > 0: x_zero_coord = self.get_coord(0, plot_data.min_x, plot_data.x_step) #else: #pass output_buffer[x_zero_coord][min_y_coord] = "+" output_buffer[x_zero_coord][max_y_coord] = "+" output_buffer[min_x_coord][y_zero_coord] = "+" output_buffer[max_x_coord][y_zero_coord] = "+" if do_plot_x_label: for i,c in enumerate(min_x_str): output_buffer[min_x_coord+i][y_zero_coord-1] = c for i,c in enumerate(max_x_str): output_buffer[max_x_coord+i-len(max_x_str)][y_zero_coord-1] = c if do_plot_y_label: for i,c in enumerate(max_y_str): output_buffer[x_zero_coord+i][max_y_coord] = c for i,c in enumerate(min_y_str): output_buffer[x_zero_coord+i][min_y_coord] = c
[docs] def plot_data(self, xy_seq, output_buffer, plot_data): if self.plot_slope: xy_seq = list(xy_seq) #sort according to the x coord xy_seq.sort(key = lambda c: c[0]) prev_p = xy_seq[0] e_xy_seq = enumerate(xy_seq) e_xy_seq.next() for i,(x,y) in e_xy_seq: draw_symbol = self.dot line_drawn = self.plot_line(prev_p, (x,y), output_buffer, plot_data) prev_p = (x,y) if not line_drawn: if i > 0 and i < len(xy_seq)-1: px,py = xy_seq[i-1] nx,ny = xy_seq[i+1] if abs(nx-px) > EPSILON: slope = (1.0/plot_data.ratio)*(ny-py)/(nx-px) draw_symbol = self.get_symbol_by_slope(slope, draw_symbol) if x < plot_data.min_x or x >= plot_data.max_x or y < plot_data.min_y or y >= plot_data.max_y: continue x_coord = self.get_coord(x, plot_data.min_x, plot_data.x_step) y_coord = self.get_coord(y, plot_data.min_y, plot_data.y_step) if x_coord >= 0 and x_coord < len(output_buffer) and y_coord >= 0 and y_coord < len(output_buffer[0]): if self.draw_axes: if y_coord == self.get_coord(0, plot_data.min_y, plot_data.y_step) and draw_symbol == "-": draw_symbol = "=" output_buffer[x_coord][y_coord] = draw_symbol else: for x,y in xy_seq: if x < plot_data.min_x or x >= plot_data.max_x or y < plot_data.min_y or y >= plot_data.max_y: continue x_coord = self.get_coord(x, plot_data.min_x, plot_data.x_step) y_coord = self.get_coord(y, plot_data.min_y, plot_data.y_step) if x_coord >= 0 and x_coord < len(output_buffer) and y_coord > 0 and y_coord < len(output_buffer[0]): output_buffer[x_coord][y_coord] = self.dot
[docs] def plot_line(self, start, end, output_buffer, plot_data): start_coord = self.get_coord(start[0], plot_data.min_x, plot_data.x_step), self.get_coord(start[1], plot_data.min_y, plot_data.y_step) end_coord = self.get_coord(end[0], plot_data.min_x, plot_data.x_step), self.get_coord(end[1], plot_data.min_y, plot_data.y_step) x0,y0 = start_coord x1,y1 = end_coord if (x0,y0) == (x1,y1): return True clipped_line = clip_line(start, end, (plot_data.min_x, plot_data.min_y), (plot_data.max_x, plot_data.max_y)) if clipped_line != None: start,end = clipped_line else: return False start_coord = self.get_coord(start[0], plot_data.min_x, plot_data.x_step), self.get_coord(start[1], plot_data.min_y, plot_data.y_step) end_coord = self.get_coord(end[0], plot_data.min_x, plot_data.x_step), self.get_coord(end[1], plot_data.min_y, plot_data.y_step) x0,y0 = start_coord x1,y1 = end_coord if (x0,y0) == (x1,y1): return True x_zero_coord = self.get_coord(0, plot_data.min_x, plot_data.x_step) y_zero_coord = self.get_coord(0, plot_data.min_y, plot_data.y_step) if start[0]-end[0] == 0: draw_symbol = "|" else: slope = (1.0/plot_data.ratio)*(end[1]-start[1])/(end[0]-start[0]) draw_symbol = self.get_symbol_by_slope(slope, self.dot) try: delta = x1-x0, y1-y0 if abs(delta[0])>abs(delta[1]): s = sign(delta[0]) slope = float(delta[1])/delta[0] for i in range(0,abs(int(delta[0]))): cur_draw_symbol = draw_symbol x = i*s cur_y = int(y0+slope*x) if self.draw_axes and cur_y == y_zero_coord and draw_symbol == "-": cur_draw_symbol = "=" output_buffer[x0+x][cur_y] = cur_draw_symbol else: s = sign(delta[1]) slope = float(delta[0])/delta[1] for i in range(0,abs(int(delta[1]))): y = i*s cur_draw_symbol = draw_symbol cur_y = y0+y if self.draw_axes and cur_y == y_zero_coord and draw_symbol == "-": cur_draw_symbol = "=" output_buffer[int(x0+slope*y)][cur_y] = cur_draw_symbol except Exception, e: print start, end print start_coord, end_coord print plot_data raise e return False
[docs] def plot_single(self, seq, min_x = None, max_x = None, min_y = None, max_y = None): return self.plot_double(range(len(seq)),seq, min_x, max_x, min_y, max_y)
[docs] def plot_double(self, x_seq, y_seq, min_x = None, max_x = None, min_y = None, max_y = None): if min_x == None: min_x = min(x_seq) if max_x == None: max_x = max(x_seq) if min_y == None: min_y = min(y_seq) if max_y == None: max_y = max(y_seq) if max_y == min_y: max_y += 1 x_mod = (max_x-min_x)*self.x_margin y_mod = (max_y-min_y)*self.y_margin min_x-=x_mod max_x+=x_mod min_y-=y_mod max_y+=y_mod plot_data = self.PlotData(self.x_size, self.y_size, min_x, max_x, min_y, max_y, x_mod, y_mod) output_buffer = [[" "]*self.y_size for i in range(self.x_size)] if self.will_draw_axes: self.draw_axes(output_buffer, plot_data) self.plot_data(zip(x_seq, y_seq), output_buffer, plot_data) if self.will_plot_labels: self.plot_labels(output_buffer, plot_data) trans_result = transposed(y_reversed(output_buffer)) result = self.new_line.join(["".join(row) for row in trans_result]) return result
[docs] def draw_axes(self, output_buffer, plot_data): draw_x = False draw_y = False if plot_data.min_x <= 0 and plot_data.max_x > 0: draw_y = True zero_x = self.get_coord(0, plot_data.min_x, plot_data.x_step) for y in xrange(plot_data.y_size): output_buffer[zero_x][y] = "|" if plot_data.min_y <= 0 and plot_data.max_y > 0: draw_x = True zero_y = self.get_coord(0, plot_data.min_y, plot_data.y_step) for x in xrange(plot_data.x_size): output_buffer[x][zero_y] = "-" if draw_x and draw_y: output_buffer[zero_x][zero_y] = "+"
@staticmethod
[docs] def get_coord(val, min, step): result = int((val - min)/step) return result
def clip_line(line_pt_1, line_pt_2, rect_bottom_left, rect_top_right): ts = [0.0,1.0] if line_pt_1[0] == line_pt_2[0]: return ((line_pt_1[0], max(min(line_pt_1[1], line_pt_2[1]), rect_bottom_left[1])), (line_pt_1[0], min(max(line_pt_1[1], line_pt_2[1]), rect_top_right[1]))) if line_pt_1[1] == line_pt_2[1]: return ((max(min(line_pt_1[0], line_pt_2[0]), rect_bottom_left[0]), line_pt_1[1]), (min(max(line_pt_1[0], line_pt_2[0]), rect_top_right[0]), line_pt_1[1])) if ((rect_bottom_left[0] <= line_pt_1[0] and line_pt_1[0] < rect_top_right[0]) and (rect_bottom_left[1] <= line_pt_1[1] and line_pt_1[1] < rect_top_right[1]) and (rect_bottom_left[0] <= line_pt_2[0] and line_pt_2[0] < rect_top_right[0]) and (rect_bottom_left[1] <= line_pt_2[1] and line_pt_2[1] < rect_top_right[1])): return line_pt_1, line_pt_2 ts.append( float(rect_bottom_left[0]-line_pt_1[0])/(line_pt_2[0]-line_pt_1[0]) ) ts.append( float(rect_top_right[0]-line_pt_1[0])/(line_pt_2[0]-line_pt_1[0]) ) ts.append( float(rect_bottom_left[1]-line_pt_1[1])/(line_pt_2[1]-line_pt_1[1]) ) ts.append( float(rect_top_right[1]-line_pt_1[1])/(line_pt_2[1]-line_pt_1[1]) ) ts.sort() if ts[2] < 0 or ts[2] >= 1 or ts[3] < 0 or ts[2]>= 1: return None result = [(pt_1 + t*(pt_2-pt_1)) for t in (ts[2],ts[3]) for (pt_1, pt_2) in zip(line_pt_1, line_pt_2)] return (result[0],result[1]), (result[2], result[3])
[docs]def plot(*args,**flags): limit_flags_names = set(["min_x","min_y","max_x","max_y"]) limit_flags = dict([(n,flags[n]) for n in limit_flags_names & set(flags)]) settting_flags = dict([(n,flags[n]) for n in set(flags) - limit_flags_names]) output = flags.get('output', sys.stdout) if len(args) == 1: p = Plotter(**settting_flags) r = p.plot_single(args[0],**limit_flags) elif len(args) == 2: p = Plotter(**settting_flags) r = p.plot_double(args[0],args[1],**limit_flags) else: raise NotImplementedError("can't draw multiple graphs yet") if output == sys.stdout: print r else: return r
__all__ = ["Plotter","plot"] def _demo(): from numpy import linspace, exp, sin, pi x = linspace(-2, 2, 31) y = exp(-0.5*x**2)*sin(2*pi*x) data = """ from numpy import linspace, exp, sin, pi x = linspace(-2, 2, 31) y = exp(-0.5*x**2)*sin(2*pi*x) """ cmd = ["plot(x, y)", "plot(x, y, draw_axes=False)", "plot(x, y, plot_slope=False)", "plot(x, y, plot_labels=False)", "plot(x, y, dot='o', plot_slope=False)", "p = plot(x, y, output=str)", "print p" ] print data for c in cmd: print '\n\n', c exec(c) if __name__ == '__main__': _demo()