TextPlot class and a demo file

The TextPlot class can be kept in its own file, and imported into other files for use as needed. This webpage includes both the TextPlot class and a demonstration program that uses it.

TextPlot class

This file must be saved as TextPlot.py.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Apr 16 11:07:15 2021

@author: rmontant
"""

def interpolate(xmid, xa, xb, ya, yb):
    frac = (xmid - xa) / (xb - xa)
    return ya + frac * (yb - ya)
#--------

class TextPlot:
    z = ['whatever...'] # this has nothing to do with plotting....

    def __init__(self, nrows, ncols):
        # copy the local values into the object itself:
        self.nrows = nrows - 10 # space for y-axis, prompts, other outputs
        self.ncols = ncols - 19 # space for x-, y- labels
    #----

    def set_data(self, xlist, ylist):
        '''Do NOT assume that xlist is sorted, but
        DO assume that xlist and ylist are matched to each other'''
        self.pairs = []
        for n in range( len(xlist) ):
            self.pairs.append( (xlist[n], ylist[n]) )
        self.pairs.sort()    # put them in smallest-x to biggest-x order

        self.xmin, self.xmax = self.pairs[0][0], self.pairs[-1][0]
        self.ymin, self.ymax = min(ylist), max(ylist)
    #----

    def set_function(self, xmin, xmax, y_ftn, nsamples = 1000):
        '''Generate x- and y- values from a function and an x-range'''
        self.pairs = []
        for n in range( nsamples ):
            x = interpolate(n, 0, nsamples, xmin, xmax)
            y = y_ftn(x)
            self.pairs.append( (x, y) )

        pmin = min(self.pairs, key=lambda p: p[1])[1]
        pmax = max(self.pairs, key=lambda p: p[1])[1]
        print(pmin, pmax)

        self.xmin, self.xmax = xmin, xmax
        self.ymin, self.ymax = pmin, pmax
    #----

    def vert_plot(self):
        '''Simple vertical plot, one x-value per row regardless of
        the screen dimensions.'''

        #y_cols = []
        #for point in self.pairs:
        #    y_col = interpolate(point[1], self.ymin, self.ymax, 0, self.ncols)
        #    y_cols.append(y_col)
        y_cols = [ interpolate(point[1], self.ymin, self.ymax, 0, self.ncols) \
            for point in self.pairs ]

        col_0 = int(interpolate(0, self.ymin, self.ymax, 0, self.ncols))

        for n, point in enumerate(self.pairs):
            rowstr = '{:7.2f}| '.format(point[0])
            for col in range(1, self.ncols):
                if col == col_0:
                    rowstr += '|'
                elif col-1 < y_cols[n] <= col \
                    or   col-1 > y_cols[n] >= col:
                    rowstr += 'o'
                elif col_0 < col < y_cols[n]  or  col_0 > col > y_cols[n]:
                    rowstr += '-'
                else:
                    rowstr += ' '
            rowstr += ' {:.2e}'.format(point[1])
            print(rowstr)

        print('=' * self.ncols)
    #----

    def vertical_plot(self):
        '''Vertical plot, with x-range scaled into number of visible rows.'''

        # Interpolate an x-value for each row:
        x_rows = [ interpolate(row, 0, self.nrows, self.xmin, self.xmax) \
            for row in range(self.nrows) ]

        # Interpolate the appropriate y-value for each x in x_rows:
        y_cols = []
        for x_row in x_rows:
            # find the 1st point >= x_row
            for i, point in enumerate(self.pairs[1:]):
                if point[0] >= x_row:
                    x_1 = self.pairs[i - 1][0]
                    y_1 = self.pairs[i - 1][1]
                    y_row = interpolate(x_row, x_1, point[0], y_1, point[1])
                    # Interpolate the column that y_row falls in:
                    y_col = interpolate(y_row, self.ymin, self.ymax, 0, self.ncols)
                    y_cols.append(y_col)
                    break   # quit looking at points for this row

        col_0 = int(interpolate(0, self.ymin, self.ymax, 0, self.ncols))

        for n, x_row in enumerate(x_rows):
            rowstr = '{:7.2f}| '.format(x_row)
            for col in range(1, self.ncols):
                if col == col_0:
                    if col-1 < y_cols[n] <= col \
                            or   col-1 > y_cols[n] >= col:
                        rowstr += '+'
                    else:
                        rowstr += '|'
                elif col-1 < y_cols[n] <= col \
                        or   col-1 > y_cols[n] >= col:
                    rowstr += 'o'
                elif col_0 < col < y_cols[n]  or  col_0 > col > y_cols[n]:
                    rowstr += ':'   # fill the space under the curve
                else:
                    rowstr += ' '
            rowstr += ' {:.2e}'.format(point[1])
            print(rowstr)

        print('=' * self.ncols)
    #----

    def horizontal_plot(self):
        x_cols = [interpolate(col, 0, self.ncols, self.xmin, self.xmax) \
                    for col in range(self.ncols)]
        y_cols = []
        for i in range(1, len(self.pairs) ):
            xa = self.pairs[i-1][0]
            ya = self.pairs[i-1][1]
            xb = self.pairs[i][0]
            yb = self.pairs[i][1]
            for x in x_cols:
                if xa <= x <= xb:
                    y_cols.append( interpolate(x, xa, xb, ya, yb) )
                    break

        y_rows = [interpolate(row, self.nrows, 0, self.ymin, self.ymax) \
                    for row in range(self.nrows)]
        row_0 = round( interpolate(0, self.ymin, self.ymax, self.nrows, 0) )

        for row in range(1, self.nrows):
            rowstr = '{:12.2f}| '.format(y_rows[row])

            for col in range(self.ncols):
                # draw y==0 axis
                if row == row_0:
                    if  y_rows[row-1] <= y_cols[col] <= y_rows[row] \
                        or  y_rows[row-1] >= y_cols[col] >= y_rows[row] :
                        rowstr += '+'
                    else:
                        rowstr += '-'
                # this-y on this-row?
                elif  y_rows[row-1] <= y_cols[col] <= y_rows[row] \
                    or  y_rows[row-1] >= y_cols[col] >= y_rows[row] :
                    rowstr += 'o'
                # this-row between 0 and this-y?
                elif  0 < y_rows[row] <= y_cols[col] \
                    or  0 > y_rows[row] >= y_cols[col] :
                    rowstr += ':'   # fill the space under the curve
                else:
                    rowstr += ' '

            print(rowstr)

        print('{:12s}+{:s}'.format('', '=' * self.ncols))
    #----

    def report_shape(self):
        print('shape is {:d} rows of {:d} columns'. \
              format(self.nrows, self.ncols))
        print(self.z)   # no good reason for this
    #----
#--------

Demonstration program

Save this file with any desired name (that ends in ".py").

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Apr 16 11:07:15 2021

@author: rmontant
"""
import math
import textplot

def myftn(x):
    return (1 - math.exp( -1 / x**2)) * math.sin(x) * math.cos(10*x)
#--------

def main(argv=[__name__]):
    import shutil
    shape = shutil.get_terminal_size()
    rows = shape[1]
    columns = shape[0]

    # create a TextPlot:
    tp1 = textplot.TextPlot(rows, columns)
    tp1.report_shape()

    # Let user select the x-range
    if len(argv) == 3:
        xmin, xmax = float(argv[1]), float(argv[2])
    else:
        xmin = input('xmin (-20): ')
        if len(xmin) > 0:
            xmin = float(xmin)
        else:
            xmin = -20
        xmax = input('xmax (60): ')
        if len(xmax) > 0:
            xmax = float(xmax)
        else:
            xmax = 60
    print('range {:.2f} .. {:.2f}'.format(xmin, xmax))

    xrange = xmax - xmin
    nsamples = 2 * columns
    xdata = [ (xmin + xrange*x/nsamples)  for x in range(nsamples) ]

    # Mildly interesting function:
    ydata = [ (x**3 - 50*x**2 + 25*x + 5000)  for x in xdata ]
    #ydata = xdata[:]

    tp1.set_function(xmin, xmax, myftn)
    tp1.vertical_plot()
    input('?')
    tp1.horizontal_plot()
#--------

if __name__ == '__main__':
    import sys
    sys.exit( main(sys.argv) )
#--------