Home > Software design >  Class to 'draw' a graph on the terminal doesn't output the correct graph
Class to 'draw' a graph on the terminal doesn't output the correct graph

Time:06-09

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()

CodePudding user response:

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()
  • Related