2

I have been stuck on this for a while and cannot work out the issue, so was hoping somebody on here could assist.

I am attempting to create a program to graph some data stored in a .csv file. My program starts with an empty graph and requires the user to first select a file, and then prompts the user to specify which columns from the csv file should be plotted. For the purposes of getting assistance, I have created a smaller program to outline the issue that I am experiencing.

As part of the graphing process, I want to have 5 gridlines displayed on the x-axis at all times, with the interval between these lines determined by the current viewable x-axis limits. For this reason, I have created a 'CustomFormatter' class which inherits from matplotlib.ticker's Formatter class, and overridden the 'call' method to try and get the desired behaviour. I believe that I need to do this in order to keep track of the current viewable xlimits of the graph, which then enable me to calculate the required intervals.

When I run my code, the following GUI is generated, which seems to be working as intended: enter image description here

However, once I click the 'Load Data' button, my specified data is loaded into the graph, but the graph's x-axis labels and gridlines disappear, as shown in the below image. enter image description here

It is not until I use the built-in pan tool to pan the graph that these gridlines and labels reappear, as shown in the below image. enter image description here

The same thing happens when I zoom in on the graph. After zooming, it is not until I pan again that the x-axis labels and gridlines re-calibrate themselves to the desired positions.

Any help that anyone is willing to provide to help me solve this issue would be greatly appreciated. Hopefully it is just something basic or silly that I am overlooking.

Cheers.

The code I used to generate my matplotlib graph embedded in a tkinter frame is shown below:

import tkinter as tk

import matplotlib
import matplotlib.dates as mdates
from matplotlib.ticker import Formatter
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure

class CustomFormatter(Formatter):
    def __init__(self, ax, graph):
        super().__init__()
        self.set_axis(ax)
        self.ax = ax
        self.graph = graph

    def __call__(self, x, pos=None):
        xlims = self.ax.get_xlim()
        ylims = self.ax.get_ylim()
        
        xmin, xmax = xlims[0], xlims[1]
        xmin = mdates.num2date(xmin)
        xmax = mdates.num2date(xmax)

        x = mdates.num2date(x)
        
        interval = (xmax - xmin) / 6
        self.ax.set_xticks([xmin, xmin + 1 * interval, xmin + 2 * interval, xmin + 3 * interval, xmin + 4 * interval, xmin + 5 * interval, xmax])
        self.ax.grid(b=True, which='major', axis='both')

        return f"{x.hour}:{x.minute}:{x.second}\n{x.day}/{x.month}/{x.year}"

class App(object):
    def __init__(self, master):
        initialFrame = tk.Frame(master)
        initialFrame.pack(fill=tk.BOTH, expand=1)
        master.title('Tkinter Graph Example')
        self.master = master
        
        self._load_data_button = tk.Button(initialFrame, takefocus=0, text='Load data')
        self._load_data_button.bind('<Button-1>', self.load_data)
        self._load_data_button.pack(side=tk.TOP, fill=tk.BOTH, padx=2, pady=2, ipady=5)

        self.graphFrame = tk.Frame(initialFrame, borderwidth=5, relief=tk.SUNKEN)
        self.graphFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.f=Figure(figsize=(5,5), dpi=100)
        self.a=self.f.add_subplot(111)

        self.graph = FigureCanvasTkAgg(self.f, self.graphFrame)
        self.graph.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)

        self.toolbar = NavigationToolbar2Tk(self.graph, self.graphFrame)
        self.toolbar.update()
        self.graph._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.a.set_xlabel('Time')
        self.a.set_ylabel('Y-Axis')
        self.a.set_title('Test')
        self.a.grid(b=True)
        self.a.xaxis.set_major_formatter(CustomFormatter(self.a, self.graph))#, self.df, self.toPlot, self.index_dict, self.name_dict))
        
        self.graph.draw()

    def load_data(self, event):
        data = [1, 4, 2, 7, 4, 6]
        time = [18820.410517624932, 18820.414807940015, 18820.41623804504, 18820.41766815007, 18820.41862155342, 18820.420528360122]

        self.a.plot(time, data)
        self.a.set_xlim(xmin=time[0], xmax=time[-1])
        self.graph.draw()

def main():
    root = tk.Tk()
    app = App(root)
    root.geometry('%dx%d+0+0'%(800,600))
    root.mainloop()

if __name__ == '__main__':
    main()

1
  • Let me know if there is any more information that I can provide to assist in answering my question. I am new to stack overflow so apologies if I have not followed the correct etiquette for posts such as this. Commented Jul 12, 2021 at 10:18

1 Answer 1

1

In the end I managed to figure out a solution to my issue. I added the following two lines of code to my App's _init method, along with two new function definitions.

self.a.callbacks.connect('xlim_changed', self.on_xlims_change)
self.a.callbacks.connect('ylim_changed', self.on_ylims_change)

def on_xlims_change(self, event_ax):
    self.graph.draw()

def on_ylims_change(self, event_ax):
    self.graph.draw()

These event handlers enabled me to redraw the graph whenever the x or y limits are changed (as they are during zoom or pan operations). I am now observing my desired behaviour.

Full code is shown below.

import tkinter as tk

import matplotlib
import matplotlib.dates as mdates
from matplotlib.ticker import Formatter
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure

class CustomFormatter(Formatter):
    def __init__(self, ax, graph):
        super().__init__()
        self.set_axis(ax)
        self.ax = ax
        self.graph = graph

    def __call__(self, x, pos=None):
        xlims = self.ax.get_xlim()
        ylims = self.ax.get_ylim()
        
        xmin, xmax = xlims[0], xlims[1]
        xmin = mdates.num2date(xmin)
        xmax = mdates.num2date(xmax)

        x = mdates.num2date(x)
        
        interval = (xmax - xmin) / 6
        self.ax.set_xticks([xmin, xmin + 1 * interval, xmin + 2 * interval, xmin + 3 * interval, xmin + 4 * interval, xmin + 5 * interval, xmax])
        self.ax.grid(b=True, which='major', axis='both')

        return f"{x.hour}:{x.minute}:{x.second}\n{x.day}/{x.month}/{x.year}"

class App(object):
    def __init__(self, master):
        initialFrame = tk.Frame(master)
        initialFrame.pack(fill=tk.BOTH, expand=1)
        master.title('Tkinter Graph Example')
        self.master = master
        
        self._load_data_button = tk.Button(initialFrame, takefocus=0, text='Load data')
        self._load_data_button.bind('<Button-1>', self.load_data)
        self._load_data_button.pack(side=tk.TOP, fill=tk.BOTH, padx=2, pady=2, ipady=5)

        self.graphFrame = tk.Frame(initialFrame, borderwidth=5, relief=tk.SUNKEN)
        self.graphFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.f=Figure(figsize=(5,5), dpi=100)
        self.a=self.f.add_subplot(111)

        self.graph = FigureCanvasTkAgg(self.f, self.graphFrame)
        self.graph.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)

        self.toolbar = NavigationToolbar2Tk(self.graph, self.graphFrame)
        self.toolbar.update()
        self.graph._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.a.set_xlabel('Time')
        self.a.set_ylabel('Y-Axis')
        self.a.set_title('Test')
        self.a.grid(b=True)
        
        self.a.callbacks.connect('xlim_changed', self.on_xlims_change)
        self.a.callbacks.connect('ylim_changed', self.on_ylims_change)
        
        self.a.xaxis.set_major_formatter(CustomFormatter(self.a, self.graph))#, self.df, self.toPlot, self.index_dict, self.name_dict))
        
        self.graph.draw()

    def on_xlims_change(self, event_ax):
        self.graph.draw()

    def on_ylims_change(self, event_ax):
        self.graph.draw()
        
    def load_data(self, event):
        data = [1, 4, 2, 7, 4, 6]
        time = [18820.410517624932, 18820.414807940015, 18820.41623804504, 18820.41766815007, 18820.41862155342, 18820.420528360122]

        self.a.plot(time, data)
        self.a.set_xlim(xmin=time[0], xmax=time[-1])
        self.graph.draw()

def main():
    root = tk.Tk()
    app = App(root)
    root.geometry('%dx%d+0+0'%(800,600))
    root.mainloop()

if __name__ == '__main__':
    main()
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.