Search code examples
pythonclassgraph

Class to 'draw' a graph on the terminal doesn't output the correct graph


I'm trying to get it to plot a graph, but right now it doesn't really seem to output the correct values for a function. I'm pretty sure the problem lies in either the scale_to_idx or scale_to_range function but I'm stumped with regards to fixing it.

The code is supposed to take a simple expression like x**2 for example and 'draw' a graph for the specified x and y values.

class Plotter():
    def __init__(self, size, xrange, yrange):
        self.size = size
        self.xrange = xrange
        self.yrange = yrange
        self.refresh()

    def refresh(self):
        self.pixels = [["  " for i in range(self.size)] for i in range(self.size)]

        #Draw Left Boundary
        for y in range(self.size):
            self.pixels[y][0] = "*"

        #Draw Bottom Boundary
        for x in range(self.size):
            self.pixels[0][x] = '**'

    def display(self):

        for row in self.pixels:
            print("".join(row))

    def scale_to_idx(self, value, value_range): ##smth wrong with scale
        rel = (int(value) - int(value_range[0]))/(int(value_range[0]) - int(value_range[-1]))
        print("(int(value_range[0]) - int(value_range[-1]))", (int(value_range[0]) - int(value_range[-1])))
        print("int(value) - int(value_range[0]))", (int(value) - int(value_range[0])))
        idx = rel * self.size
        return idx

    def scale_to_range(self, idx, value_range): ##smth wrong with scale
        rel = idx / self.size
        value = int(value_range[0]) + rel * (int(value_range[-1]) - int(value_range[0]))
        return value


    def plot(self, f, plot_char): 
        for x_idx in range(self.size):
            x = self.scale_to_range(x_idx, self.xrange)
            y = f(x_idx)
            if y > int(self.yrange[0]) or y < int(self.yrange[1]):
                y_idx = int(self.scale_to_idx(x, self.yrange)) 
                self.pixels[y_idx][x_idx] = plot_char
            
def validate_range(value_range):
    if len(value_range) != 2:
        print("Enter two values separated by a space! Try again...")
        return False
    if value_range[0] >= value_range[1]:
        print("Lower end of range must be lower than higher end! Try again...")
        return False
    return True

if __name__ == "__main__":
    size = int(input("Enter plot size (Press [ENTER] for default) ") or 50)
    while True:
        xrange = input("Enter X-axis range seperated by space (default is '-10 10'): ") or "-10 10"
        yrange = input("Enter Y-axis range seperated by space (default is '-10 10'): ") or "-10 10"
        xrange = xrange.split()
        yrange = yrange.split()
        if validate_range(xrange) and validate_range(yrange):
            break
    plotter = Plotter(size, xrange, yrange)
                
    while True:
        answer = input("Do you wish to add (another) function? [y/n]: ")
        if answer == "y":
            func_string = input("Input function code with x as variable (e.g. 'x**2'): ")
            if not func_string:
                break
            plot_char = input("Set Plotting Character: ") or "--"            
            try:
                exec(f"def f(x): return {func_string}")
                plotter.plot(f, plot_char)          
            except SyntaxError:
                print("Invalid Function Expression! Try again...")
        elif answer == "n":
            break


    plotter.display()

Solution

  • Cool idea. I have seen a few other ideas floating out there about this.

    Your code seems to work but I didn't work through exactly what was broken with your implementation since it was just a little hard to see. However, when doing the conversions correctly, everything seems to work for me. See the revised code below. The changes are in converting your range inputs to floats, plotting the rows in reversed order so that the plot looks right, and lastly, changing the logic for calculating the x,y pixel indices. I commented out your code that is no longer needed

    here is a result:

    enter image description here

    class Plotter():
        def __init__(self, size, xrange, yrange):
            self.size = size
            self.xrange = xrange
            self.yrange = yrange
            self.refresh()
    
        def refresh(self):
            self.pixels = [["  " for i in range(self.size)] for i in range(self.size)]
    
            #Draw Left Boundary
            for y in range(self.size):
                self.pixels[y][0] = "*"
    
            #Draw Bottom Boundary
            for x in range(self.size):
                self.pixels[0][x] = '**'
    
        def display(self):
            
            print(self.size)
            print(self.xrange)
            print(self.yrange)
    
            # if you want the plot to look right, you need to draw in reverse order
            for row in reversed(self.pixels):
                print("".join(row))
    
        # def scale_to_idx(self, value, value_range): ##smth wrong with scale
        #     rel = (int(value) - int(value_range[0]))/(int(value_range[0]) - int(value_range[-1]))
        #     # print("(int(value_range[0]) - int(value_range[-1]))", (int(value_range[0]) - int(value_range[-1])))
        #     # print("int(value) - int(value_range[0]))", (int(value) - int(value_range[0])))
        #     idx = rel * self.size
        #     return idx
    
        # def scale_to_range(self, idx, value_range): ##smth wrong with scale
        #     rel = idx / self.size
        #     value = int(value_range[0]) + rel * (int(value_range[-1]) - int(value_range[0]))
        #     return value
    
        def plot(self, f, plot_char): 
            xmin, xmax = self.xrange
            ymin, ymax = self.yrange
            size = self.size
    
            # we know what the xidxs are and the xvals will just be a linspace type operation
            xidx = list(range(size))
            xstep = (xmax - xmin) / (size - 1)
            xvals = [xmin + xstep * i for i in xidx]
            
            # the yvals we can create just with the function call
            yvals = [f(x) for x in xvals]
    
            # basically the relationship between the yscale and the ypixel index is a line
            m = (size - 1) / (ymax - ymin)
            b = -m * ymin
            # convert to the nearest integer value
            yidx = [int(round(m * x + b)) for x in yvals]
            
            for x, y, x_idx, y_idx in zip(xvals, yvals, xidx, yidx):
                if y_idx >= 0 and y_idx < size:
                    try:
                        self.pixels[y_idx][x_idx] = plot_char
                    except Exception as e:
                        print(x, y, x_idx, y_idx)
                        raise e
            
            # for x_idx in range(self.size):
            #     x = self.scale_to_range(x_idx, self.xrange)
            #     # y = f(x_idx)
            #     y = f(x)
            #     if y > int(self.yrange[0]) or y < int(self.yrange[1]):
            #         y_idx = int(self.scale_to_idx(x, self.yrange)) 
            #         self.pixels[y_idx][x_idx] = plot_char
                
    def validate_range(value_range):
        if len(value_range) != 2:
            print("Enter two values separated by a space! Try again...")
            return False
        if value_range[0] >= value_range[1]:
            print("Lower end of range must be lower than higher end! Try again...")
            return False
        return True
    
    if __name__ == "__main__":
        size = int(input("Enter plot size (Press [ENTER] for default) ") or 50)
        while True:
            xrange = input("Enter X-axis range seperated by space (default is '-10 10'): ") or "-10 10"
            yrange = input("Enter Y-axis range seperated by space (default is '-10 10'): ") or "-10 10"
            xrange = list(map(float, xrange.split()))
            yrange = list(map(float, yrange.split()))
            if validate_range(xrange) and validate_range(yrange):
                break
        plotter = Plotter(size, xrange, yrange)
                    
        while True:
            answer = input("Do you wish to add (another) function? [y/n]: ")
            if answer == "y":
                func_string = input("Input function code with x as variable (e.g. 'x**2'): ")
                if not func_string:
                    break
                plot_char = input("Set Plotting Character: ") or "--"            
                try:
                    exec(f"def f(x): return {func_string}")
                    plotter.plot(f, plot_char)          
                except SyntaxError:
                    print("Invalid Function Expression! Try again...")
            elif answer == "n":
                break
    
        plotter.display()