2

I just put together this basic retirement savings calculator using python. While this works, I had a couple of questions:

  1. Is there a way to embed the plot directly, i.e. without saving it as a PNG and then loading it again?
  2. Line 30 reads img.image = render. While I understand that this updates the image attribute for the label defined on line 29, I am confused why this line is required, since we already call out image = render on line 29 itself. Why twice?
from tkinter import *
import pandas as pd
import matplotlib.pyplot as plt
from PIL import ImageTk, Image

def generate_plot():
    rate = float(entry_1.get())
    years_saving = int(entry_2.get())
    initial_savings = float(entry_3.get())
    yearly_contribution = float(entry_4.get())

    model = pd.DataFrame({'time': range(years_saving)})
    model['simple_exp'] = [initial_savings*rate**year for year in model['time']]
    model['yearly_invest'] = model['simple_exp'] + [yearly_contribution*(rate**year - 1)/(rate-1) for year in model['time']]

    final = model['yearly_invest'].sort_values(ascending= False).iloc[0]

    label_5 = Label(frame, text=f"You would have saved INR {final} by retirement")
    label_5.grid(row=0, column=0)

    plt.plot(model['time'], model['yearly_invest'])
    plt.title('Retirement Savings')
    plt.xlabel('Time in Years)')
    plt.ylabel('INR (Lacs)')
    plt.savefig('plot.png')

    load = Image.open('plot.png')
    render = ImageTk.PhotoImage(load)
    img = Label(frame, image = render)
    img.image = render
    img.grid(row=1, column=0)   
    # my_label = Label(frame, image = my_img)
    # my_label.grid(row=1, column=0)

    # img = ImageTk.PhotoImage(Image.open('plot.png'))
    # img_label = Label(frame, image = img)
    # img_label.grid(row=1, column=0)

root = Tk()

label_1 = Label(root, text = 'INTEREST RATE(%)')
label_2 = Label(root, text = 'NUMBER OF YEARS IN SAVINGS')
label_3 = Label(root, text = 'INITIAL CORPUS (INR LACS)')
label_4 = Label(root, text = 'YEARLY CONTRIBUTION (INR LACS')
frame = Frame(root, width=300, height=300)
button = Button(root, text="GENERATE PLOT", command = generate_plot, padx = 5, pady=5)

entry_1 = Entry(root)
entry_2 = Entry(root)
entry_3 = Entry(root)
entry_4 = Entry(root)

label_1.grid(row=0, column=0, pady=5, padx=5)
entry_1.grid(row=0, column=1, pady=5, padx=5)

label_2.grid(row=1, column=0, pady=5, padx=5)
entry_2.grid(row=1, column=1, pady=5, padx=5)

label_3.grid(row=2, column=0, pady=5, padx=5)
entry_3.grid(row=2, column=1, pady=5, padx=5)

label_4.grid(row=3, column=0, pady=5, padx=5)
entry_4.grid(row=3, column=1, pady=5, padx=5)

button.grid(row=4,column=0, columnspan=2, pady=20, padx=5)

frame.grid(row=5, column=0, columnspan = 2, padx = 5, pady = 5)

root.mainloop()

martineau
  • 119,623
  • 25
  • 170
  • 301
Rishi
  • 87
  • 6
  • This line: `img.image = render` is useless if you aren't going to keep a reference to `img`. Make `img` global or add it to a global list – TheLizzard May 22 '21 at 10:28

1 Answers1

2

You can try saving to a stream using BytesIO:

from tkinter import *
from io import BytesIO
import pandas as pd
import matplotlib.pyplot as plt
from PIL import ImageTk, Image

def generate_plot():
    rate = float(entry_1.get())
    years_saving = int(entry_2.get())
    initial_savings = float(entry_3.get())
    yearly_contribution = float(entry_4.get())

    model = pd.DataFrame({'time': range(years_saving)})
    model['simple_exp'] = [initial_savings*rate**year for year in model['time']]
    model['yearly_invest'] = model['simple_exp'] + [yearly_contribution*(rate**year - 1)/(rate-1) for year in model['time']]

    final = model['yearly_invest'].sort_values(ascending= False).iloc[0]

    label_5 = Label(frame, text=f"You would have saved INR {final} by retirement")
    label_5.grid(row=0, column=0)

    plt.plot(model['time'], model['yearly_invest'])
    plt.title('Retirement Savings')
    plt.xlabel('Time in Years)')
    plt.ylabel('INR (Lacs)')

    img_data = BytesIO()
    plt.savefig(img_data)

    load = Image.open(img_data)
    render = ImageTk.PhotoImage(load)
    img = Label(frame, image = render)
    img.image = render # This is needed to keep a reference to the image, see the link below
    img.grid(row=1, column=0)   
    # my_label = Label(frame, image = my_img)
    # my_label.grid(row=1, column=0)

    # img = ImageTk.PhotoImage(Image.open('plot.png'))
    # img_label = Label(frame, image = img)
    # img_label.grid(row=1, column=0)

root = Tk()

label_1 = Label(root, text = 'INTEREST RATE(%)')
label_2 = Label(root, text = 'NUMBER OF YEARS IN SAVINGS')
label_3 = Label(root, text = 'INITIAL CORPUS (INR LACS)')
label_4 = Label(root, text = 'YEARLY CONTRIBUTION (INR LACS')
frame = Frame(root, width=300, height=300)
button = Button(root, text="GENERATE PLOT", command = generate_plot, padx = 5, pady=5)

entry_1 = Entry(root)
entry_2 = Entry(root)
entry_3 = Entry(root)
entry_4 = Entry(root)

label_1.grid(row=0, column=0, pady=5, padx=5)
entry_1.grid(row=0, column=1, pady=5, padx=5)

label_2.grid(row=1, column=0, pady=5, padx=5)
entry_2.grid(row=1, column=1, pady=5, padx=5)

label_3.grid(row=2, column=0, pady=5, padx=5)
entry_3.grid(row=2, column=1, pady=5, padx=5)

label_4.grid(row=3, column=0, pady=5, padx=5)
entry_4.grid(row=3, column=1, pady=5, padx=5)

button.grid(row=4,column=0, columnspan=2, pady=20, padx=5)

frame.grid(row=5, column=0, columnspan = 2, padx = 5, pady = 5)

root.mainloop()

Reference: Why do my Tkinter images not appear?

Isma
  • 14,604
  • 5
  • 37
  • 51
  • This isn't going to work because `img.image` goes out of scope so it is deleted by python's garbage collector. – TheLizzard May 22 '21 at 10:41
  • Seems to work when I try it.... Tinker should have a copy of the image data no? – Isma May 22 '21 at 10:42
  • Look at [this](https://stackoverflow.com/questions/34534655/python-function-that-shows-image-in-tkinter). Basically if the `PhotoImage` goes out of scope the image should be deleted in `tkinter`'s world. `tkinter` doesn't keep a reference to the image – TheLizzard May 22 '21 at 10:46
  • I don't see the issue tbh, it seems to work and the image is already rendered when it goes out of scope, if you can elaborate more I will change the answer. – Isma May 22 '21 at 10:50
  • I have no idea why it works for you but when a `ImageTk.PhotoImage` goes out of scope it is deleted by python's garbage collector. When that happens the image is deleted from tkinter because it doesn't actually keep a reference to that image. So in theory it shouldn't work. To solve this problem you just need to add `img_list = []` somewhere in the global scope and `img_list.append(img)` inside the function. For more info please read [this](https://stackoverflow.com/questions/16424091/why-does-tkinter-image-not-show-up-if-created-in-a-function) – TheLizzard May 22 '21 at 10:59
  • It doesnt work for you when you run the code above? – Isma May 22 '21 at 11:02
  • @Isma while you answer the first question the second is open. Please add somehow [this](https://web.archive.org/web/20201111190625id_/http://effbot.org/pyfaq/why-do-my-tkinter-images-not-appear.htm) to your answer. – Thingamabobs May 22 '21 at 11:07
  • Yeah, I will, Im just trying to figure out why I dont get this behaviour, even after forcing collection of the GC right after rendering it seems to work, maybe they have fixed this recently ;) – Isma May 22 '21 at 11:13
  • @Isma you still do what is requierd by calling `img.image = render`. Its a way around for the issue TheLizzard is refering to. In the provided link is the answer to that question. – Thingamabobs May 22 '21 at 11:18
  • 1
    Yeah so there was no issue at all haha, Ill add a reference to that link :) – Isma May 22 '21 at 11:21