0

I wish to make a scrolling timeline for an applet using tkinter. I can currently create charts using matplotlib and update these dynamically, but I want to be able to scroll along this by passing in data about the start and end date, similar to the excel image I have attached. Timeline

I have tried using timeline tools built in to tkinter using ttkwidgets, but this shows a scroll bar and a zoom in button, but does not have an option for both:

import tkinter as tk
from ttkwidgets import TimeLine

window = tk.Tk()
timeline = TimeLine(
    window,
    categories={str(key): {"text": "Category {}".format(key)} for key in range(0, 5)},
    height=100, extend=True
)
menu = tk.Menu(window, tearoff=False)
menu.add_command(label="Some Action", command=lambda: print("Command Executed"))
timeline.draw_timeline()
timeline.grid()
window.after(5000, lambda: print(timeline.time))
window.mainloop()

The resulting cole Is there a way to add a stretchable scrollbar?

1 Answers1

0

Tkinter, matplotlib, and ttkwidgets don't have a built-in timeline widget exactly like you want. However, tkinter has all of the basic building blocks to be able to do it. It's just a matter of putting all of the widgets together in a class.

Here's something I threw together. It's not exactly production-ready, but it shows how you can combine multiple widgets into a new custom widget pretty easily.

screenshot

import tkinter as tk
from tkinter import ttk

from dateutil.relativedelta import relativedelta
from datetime import datetime


class Timeline(tk.Frame):
    """
    Timeline widget

    options:
      command     command to be called when user clicks on a month
      header_text text for the header. Defaults to "Date"
      start_date  datetime representing the start of the timeline
      end_date    datetime representing the end of the timeline
      kwargs      additional arguments passed to the superclass
                  (useful for setting colors, borderwidth)

    subcommands;
      highlight   takes one or more arguments that are a tuple of
                  (month,year). kwargs can be bg to set the background,
                  and fg to set the foreground
    """

    def __init__(self, parent, **kwargs):
        header_text = kwargs.pop("header", "Date")
        start_date = kwargs.pop("start_date", datetime.now() + relativedelta(years=-1))
        end_date = kwargs.pop("end_date", datetime.now() + relativedelta(years=1))
        self.command = kwargs.pop("command", None)

        super().__init__(parent, **kwargs)

        subheader_str = f"{start_date.strftime('%b %Y')} - {end_date.strftime('%b %Y')}"

        bg = self.cget("background")
        self.header = tk.Label(self, text=header_text, background=bg, anchor="w")
        self.sep = ttk.Separator(self, orient="horizontal")
        self.subheader = tk.Label(self, text=subheader_str, background=bg, anchor="w")
        self.canvas = tk.Canvas(self, background=bg)
        self.scrollbar = tk.Scrollbar(
            self, orient="horizontal", command=self.canvas.xview
        )
        self.canvas.configure(xscrollcommand=self.scrollbar.set)

        self.header.pack(side="top", fill="x")
        self.sep.pack(side="top", fill="x")
        self.subheader.pack(side="top", fill="x")
        self.canvas.pack(side="top", fill="x")
        self.scrollbar.pack(side="top", fill="x")

        self.inner_frame = tk.Frame(self.canvas, background=bg)
        self.canvas.create_window(0, 0, anchor="nw", window=self.inner_frame)

        self.buttons = {}
        current = start_date
        displayed_year = ""
        column = 0
        while current <= end_date:
            if displayed_year != current.year:
                year_label = tk.Label(
                    self.inner_frame, text=current.year, background=bg, anchor="w"
                )
                year_label.grid(row=0, column=column, sticky="ew")
                displayed_year = current.year

            index = (current.month, current.year)
            month = current.strftime("%b")
            label = tk.Label(self.inner_frame, text=month, anchor="w", background=bg)
            button = tk.Label(
                self.inner_frame,
                text="    ",
                width=6,
                bd=0,
                relief="flat",
                bg="lightgray",
            )
            label.grid(row=1, column=column, padx=2, sticky="ew")
            button.grid(row=2, column=column, padx=2, sticky="ew")
            button.bind(
                "<1>",
                lambda event, index=index: self._callback(event, index[0], index[1]),
            )

            self.buttons[(current.month, current.year)] = button
            column += 1
            current = current + relativedelta(months=1)

        self.canvas.bind("<Configure>", self._resize)

    def highlight(self, *args, bg="red", fg="black"):
        for index in args:
            self.buttons[index].configure(bg=bg, fg=fg)

    def _callback(self, event, month, year):
        if self.command:
            self.command(month=month, year=year)

    def _resize(self, event):
        self.canvas.configure(height=self.inner_frame.winfo_height())
        bbox = self.canvas.bbox("all")
        self.canvas.configure(scrollregion=bbox)


def callback(month, year):
    print(f"you clicked on month={month} year={year}")


root = tk.Tk()
root.geometry("500x200")
t = Timeline(root, bd=2, relief="groove", background="white", command=callback)
t.highlight((12, 2021), (1, 2022), (2, 2022))
t.pack(side="top", fill="x", padx=4, pady=4)

root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685