Home » Building a Modern Dashboard with Python and Tkinter

Building a Modern Dashboard with Python and Tkinter

, before there was Streamlit, before there was Taipy, there was Tkinter. Tkinter was and is the original Python GUI builder, and, until a few years ago, it was one of the few ways you could produce any type of dashboard or GUI using Python.

As newer, web-based frameworks like the ones mentioned above have taken the limelight for the desktop presentation of data-centric and machine learning applications, we ask the question, “Is there still mileage left in using the Tkinter library?”.

My answer to this question is a resounding Yes! I hope to demonstrate in this article that Tkinter remains a powerful, lightweight, and highly relevant tool for creating native desktop GUI and data dashboard applications.

For developers who need to create internal tools, simple utilities, or educational software, Tkinter can be the ideal choice. It doesn’t require complex web servers, JavaScript knowledge, or heavy dependencies. It’s Python, pure and simple. And as I show later, you can produce some pretty complex, modern-looking dashboards with it.

In the rest of this article, we will journey from the fundamental principles of Tkinter to the practical construction of a dynamic, data-driven dashboard, proving that this “OG” GUI library still has plenty of modern tricks up its sleeve.

What is Tkinter and Why Should You Still Care?

Tkinter is the standard, built-in Graphical User Interface (GUI) toolkit for Python. The name is a play on words of “Tk Interface.” It’s a wrapper around Tcl/Tk, a robust and cross-platform GUI toolkit that has been around since the early 1990s.

Its single most significant advantage is its inclusion in the Python standard library. This means if you have Python installed, you have Tkinter. There are no pip install commands to run, no virtual environment dependency conflicts to resolve. It works out of the box on Windows, macOS, and Linux.

So, why choose Tkinter in an age of flashy web frameworks?

  • Simplicity and Speed: For small to medium-sized applications, Tkinter is fast to develop with. You can have a functional window with interactive elements in just a few lines of code.
  • Lightweight: Tkinter applications have a tiny footprint. They don’t require a browser or a web server, making them ideal for simple utilities that need to run efficiently on any machine.
  • Native Look and Feel (to an extent): While classic Tkinter has a famously dated look, the ttk themed widget set provides access to more modern, native-looking controls that better match the host operating system.
  • Excellent for Learning: Tkinter teaches the fundamental concepts of event-driven programming — the core of all GUI development. Understanding how to manage widgets, layouts, and user events in Tkinter provides a solid foundation for learning any other GUI framework.

Of course, it has its drawbacks. Complex, aesthetically demanding applications can be challenging to build, and their design philosophy can feel more verbose compared to the declarative style of Streamlit or Gradio. However, for its intended purpose — creating functional, standalone desktop applications — it excels. 

Over time, though, additional libraries have been written that make Tkinter GUIs more modern-looking. One of these, which we’ll use, is called ttkbootstrap. This is built on top of Tkinter, adds extra widgets and can give your GUIs a Bootstrap-inspired look.

The Core Concepts of a Tkinter Application

Every Tkinter application is built upon a few key pillars. Grasping these concepts is essential before you can create anything meaningful.

1/ The Root Window
The root window is the main container for your entire application. It’s the top-level window that has a title bar, minimise, maximise, and close buttons. You create it with a single line of code like this.

import tkinter as tk

root = tk.Tk()
root.title("My First Tkinter App")

root.mainloop()

That code produces this. Not the most exciting thing to look at, but it is a start.

Image by Author

Everything else in your application — buttons, labels, input fields , and so on — will live inside this root window.

2/ Widgets
Widgets are the building blocks of your GUI. They are the elements the user sees and interacts with. Some of the most common widgets include:

  • Label: Displays static text or images.
  • Button: A clickable button that can trigger a function.
  • Entry: A single-line text input field.
  • Text: A multi-line text input and display area.
  • Frame: An invisible rectangular container used to group other widgets. This is crucial for organising complex layouts.
  • Canvas: A versatile widget for drawing shapes, creating graphs, or displaying images.
  • Checkbutton and Radiobutton: For boolean or multiple-choice selections.

3/ Geometry Managers
Once you’ve created your widgets, you need to tell Tkinter where to put them inside the window. This is the job of geometry managers. Note that you can’t mix and match different managers within the same parent container (like a root or a Frame).

  • pack(): The simplest manager. It “packs” widgets into the window, either vertically or horizontally. It’s quick for straightforward layouts but offers little precise control.
  • place(): The most precise manager. It allows you to specify the exact pixel coordinates (x, y) and size (width, height) of a widget. This is generally to be avoided because it makes your application rigid and not responsive to window resizing.
  • grid(): The most powerful and flexible manager, and the one we will use for our dashboard. It organises widgets in a table-like structure of rows and columns, making it perfect for creating aligned, structured layouts.

4/ The Main Loop
The line root.mainloop() is the final and most critical part of any Tkinter application. This method starts the event loop. The application enters a waiting state, listening for user actions like mouse clicks, key presses, or window resizing. When an event occurs, Tkinter processes it (e.g., calling a function tied to a button click) and then returns to the loop. The application will only close when this loop is terminated, usually by closing the window.

Setting up a dev environment

Before we start to code, let’s set up a development environment. I’m slowly switching to the UV command line tool for my environment setup, replacing conda, and that’s what we’ll use here.

# initialise our project
uv init tktest
cd tktest
# create a new venv
uv venv tktest
# switch to it
tktestScriptsactivate
# Install required external libraries
(tktest) uv pip install matplotlib ttkbootstrap pandas

Example 1: A Simple “Hello, Tkinter!” app

Let’s put these concepts into practice. We’ll create a window with a label and a button. When the button is clicked, the label’s text will change.

import tkinter as tk

# 1. Create the root window
root = tk.Tk()
root.title("Simple Interactive App")
root.geometry("300x150") # Set window size: width x height

# This function will be called when the button is clicked
def on_button_click():
    # Update the text of the label widget
    label.config(text="Hello, Tkinter!")

# 2. Create the widgets
label = tk.Label(root, text="Click the button below.")
button = tk.Button(root, text="Click Me!", command=on_button_click)

# 3. Use a geometry manager to place the widgets
# We use pack() for this simple layout
label.pack(pady=20) # pady adds some vertical padding
button.pack()

# 4. Start the main event loop
root.mainloop()

It should look like this, with the image on the right what you get when you click the button.

Image by Author

So far, so straightforward; however, you can create modern, visually appealing GUIs and dashboards with Tkinter. To illustrate this, we’ll create a more comprehensive and complex app that showcases what Tkinter can do.

Example 2 — A modern data dashboard

For this example, we’ll create a data dashboard using a dataset from Kaggle called CarsForSale. This comes with a CC0:Public Domain licence, meaning it can be freely used for most purposes. 

It is a US-centric data set containing sales and performance details for approximately 9300 different car models from about 40 different manufacturers spanning the period 2001–2022. You can get it using the link below:

https://www.kaggle.com/datasets/chancev/carsforsale/data

Download the data set and save it to a CSV file on your local system.

NB: This data set is provided under the CC0: Public Domain licence, therefore it’s fine to use in this context.

Image from the Kaggle website

This example will be much more complex than the first, but I wanted to give you a good idea of exactly what was possible with Tkinter, so here goes. I’ll present the code and describe its general functionality before we examine the GUI it produces.

###############################################################################
#  USED-CAR MARKETPLACE DASHBOARD 
#
#
###############################################################################
import tkinter as tk
import ttkbootstrap as tb
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import pandas as pd, numpy as np, re, sys
from pathlib import Path
from textwrap import shorten

# ─────────────────────────  CONFIG  ──────────────────────────
CSV_PATH = r"C:Usersthomatempcarsforsale.csv"       
COLUMN_ALIASES = {
    "brand": "make", "manufacturer": "make", "carname": "model",
    "rating": "consumerrating", "safety": "reliabilityrating",
}
REQUIRED = {"make", "price"}                            
# ──────────────────────────────────────────────────────────────

class Dashboard:
    # ═══════════════════════════════════════════════════════════
    def __init__(self, root: tb.Window):
        self.root = root
        self.style = tb.Style("darkly")
        self._make_spinbox_style()
        self.clr = self.style.colors
        
        self.current_analysis_plot_func = None 
        
        self._load_data()
        self._build_gui()
        self._apply_filters()

    # ─────────── spin-box style (white arrows) ────────────────
    def _make_spinbox_style(self):
        try:
            self.style.configure("White.TSpinbox",
                                 arrowcolor="white",
                                 arrowsize=12)
            self.style.map("White.TSpinbox",
                           arrowcolor=[("disabled", "white"),
                                       ("active",   "white"),
                                       ("pressed",  "white")])
        except tk.TclError:
            pass

    # ───────────────────── DATA LOAD ───────────────────────────
    def _load_data(self):
        csv = Path(CSV_PATH)
        if not csv.exists():
            tb.dialogs.Messagebox.show_error("CSV not found", str(csv))
            sys.exit()

        df = pd.read_csv(csv, encoding="utf-8-sig", skipinitialspace=True)
        df.columns = [
            COLUMN_ALIASES.get(
                re.sub(r"[^0-9a-z]", "", c.lower().replace("ufeff", "")),
                c.lower()
            )
            for c in df.columns
        ]
        if "year" not in df.columns:
            for col in df.columns:
                nums = pd.to_numeric(df[col], errors="coerce")
                if nums.dropna().between(1900, 2035).all():
                    df.rename(columns={col: "year"}, inplace=True)
                    break
        for col in ("price", "minmpg", "maxmpg",
                    "year", "mileage", "consumerrating"):
            if col in df.columns:
                df[col] = pd.to_numeric(
                    df[col].astype(str)
                          .str.replace(r"[^d.]", "", regex=True),
                    errors="coerce"
                )
        if any(c not in df.columns for c in REQUIRED):
            tb.dialogs.Messagebox.show_error(
                "Bad CSV", "Missing required columns.")
            sys.exit()
        self.df = df.dropna(subset=["make", "price"])

    # ───────────────────── GUI BUILD ───────────────────────────
    def _build_gui(self):
        header = tb.Frame(self.root, width=600, height=60, bootstyle="dark")
        header.pack_propagate(False)
        header.pack(side="top", anchor="w", padx=8, pady=(4, 2))
        tb.Label(header, text="🚗  USED-CAR DASHBOARD",
                 font=("Segoe UI", 16, "bold"), anchor="w")
          .pack(fill="both", padx=8, pady=4)

        self.nb = tb.Notebook(self.root); self.nb.pack(fill="both", expand=True)
        self._overview_tab()
        self._analysis_tab()
        self._data_tab()

    # ─────────────────  OVERVIEW TAB  ─────────────────────────
    def _overview_tab(self):
        tab = tb.Frame(self.nb); self.nb.add(tab, text="Overview")
        self._filters(tab)
        self._cards(tab)
        self._overview_fig(tab)

    def _spin(self, parent, **kw):
        return tb.Spinbox(parent, style="White.TSpinbox", **kw)

    def _filters(self, parent):
        f = tb.Labelframe(parent, text="Filters", padding=6)
        f.pack(fill="x", padx=8, pady=6)
        tk.Label(f, text="Make").grid(row=0, column=0, sticky="w", padx=4)
        self.make = tk.StringVar(value="All")
        tb.Combobox(f, textvariable=self.make, state="readonly", width=14,
                    values=["All"] + sorted(self.df["make"].unique()),
                    bootstyle="dark")
          .grid(row=0, column=1)
        self.make.trace_add("write", self._apply_filters)
        if "drivetrain" in self.df.columns:
            tk.Label(f, text="Drivetrain").grid(row=0, column=2, padx=(20, 4))
            self.drive = tk.StringVar(value="All")
            tb.Combobox(f, textvariable=self.drive, state="readonly", width=14,
                        values=["All"] + sorted(self.df["drivetrain"].dropna()
                                                .unique()),
                        bootstyle="dark")
              .grid(row=0, column=3)
            self.drive.trace_add("write", self._apply_filters)
        pr_min, pr_max = self.df["price"].min(), self.df["price"].max()
        tk.Label(f, text="Price $").grid(row=0, column=4, padx=(20, 4))
        self.pmin = tk.DoubleVar(value=float(pr_min))
        self.pmax = tk.DoubleVar(value=float(pr_max))
        for col, var in [(5, self.pmin), (6, self.pmax)]:
            self._spin(f, from_=0, to=float(pr_max), textvariable=var,
                       width=10, increment=1000, bootstyle="secondary")
              .grid(row=0, column=col)
        if "year" in self.df.columns:
            yr_min, yr_max = int(self.df["year"].min()), int(self.df["year"].max())
            tk.Label(f, text="Year").grid(row=0, column=7, padx=(20, 4))
            self.ymin = tk.IntVar(value=yr_min)
            self.ymax = tk.IntVar(value=yr_max)
            for col, var in [(8, self.ymin), (9, self.ymax)]:
                self._spin(f, from_=1900, to=2035, textvariable=var,
                           width=6, bootstyle="secondary")
                  .grid(row=0, column=col)
        tb.Button(f, text="Apply Year/Price Filters",
                  bootstyle="primary-outline",
                  command=self._apply_filters)
          .grid(row=0, column=10, padx=(30, 4))

    def _cards(self, parent):
        wrap = tb.Frame(parent); wrap.pack(fill="x", padx=8)
        self.cards = {}
        for lbl in ("Total Cars", "Average Price",
                    "Average Mileage", "Avg Rating"):
            card = tb.Frame(wrap, padding=6, relief="ridge", bootstyle="dark")
            card.pack(side="left", fill="x", expand=True, padx=4, pady=4)
            val = tb.Label(card, text="-", font=("Segoe UI", 16, "bold"),
                           foreground=self.clr.info)
            val.pack()
            tb.Label(card, text=lbl, foreground="white").pack()
            self.cards[lbl] = val

    def _overview_fig(self, parent):
        fr = tb.Frame(parent); fr.pack(fill="both", expand=True, padx=8, pady=6)
        self.ov_fig = plt.Figure(figsize=(18, 10), facecolor="#1e1e1e",
                                 constrained_layout=True)
        self.ov_canvas = FigureCanvasTkAgg(self.ov_fig, master=fr)
        self.ov_canvas.get_tk_widget().pack(fill="both", expand=True)

    # ───────────────── ANALYSIS TAB ──────────────────────────
    def _analysis_tab(self):
        tab = tb.Frame(self.nb); self.nb.add(tab, text="Analysis")
        ctl = tb.Frame(tab); ctl.pack(fill="x", padx=8, pady=6)
        def set_and_run_analysis(plot_function):
            self.current_analysis_plot_func = plot_function
            plot_function()
        for txt, fn in (("Correlation", self._corr),
                        ("Price by Make", self._price_make),
                        ("MPG", self._mpg),
                        ("Ratings", self._ratings)):
            tb.Button(ctl, text=txt, command=lambda f=fn: set_and_run_analysis(f),
                      bootstyle="info-outline").pack(side="left", padx=4)
        self.an_fig = plt.Figure(figsize=(12, 7), facecolor="#1e1e1e",
                                 constrained_layout=True)
        self.an_canvas = FigureCanvasTkAgg(self.an_fig, master=tab)
        w = self.an_canvas.get_tk_widget()
        w.configure(width=1200, height=700)
        w.pack(padx=8, pady=4)

    # ───────────────── DATA TAB ────────────────────────────────
    def _data_tab(self):
        tab = tb.Frame(self.nb); self.nb.add(tab, text="Data")
        top = tb.Frame(tab); top.pack(fill="x", padx=8, pady=6)
        tk.Label(top, text="Search").pack(side="left")
        self.search = tk.StringVar()
        tk.Entry(top, textvariable=self.search, width=25)
          .pack(side="left", padx=4)
        self.search.trace_add("write", self._search_tree)
        cols = list(self.df.columns)
        self.tree = tb.Treeview(tab, columns=cols, show="headings",
                                bootstyle="dark")
        for c in cols:
            self.tree.heading(c, text=c.title())
            self.tree.column(c, width=120, anchor="w")
        ysb = tb.Scrollbar(tab, orient="vertical", command=self.tree.yview)
        xsb = tb.Scrollbar(tab, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
        self.tree.pack(side="left", fill="both", expand=True)
        ysb.pack(side="right", fill="y"); xsb.pack(side="bottom", fill="x")

    # ───────────────── FILTER & STATS ──────────────────────────
    def _apply_filters(self, *_):
        df = self.df.copy()
        if self.make.get() != "All":
            df = df[df["make"] == self.make.get()]
        if hasattr(self, "drive") and self.drive.get() != "All":
            df = df[df["drivetrain"] == self.drive.get()]
        try:
            pmin, pmax = float(self.pmin.get()), float(self.pmax.get())
        except ValueError:
            pmin, pmax = df["price"].min(), df["price"].max()
        df = df[(df["price"] >= pmin) & (df["price"] <= pmax)]
        if "year" in df.columns and hasattr(self, "ymin"):
            try:
                ymin, ymax = int(self.ymin.get()), int(self.ymax.get())
            except ValueError:
                ymin, ymax = df["year"].min(), df["year"].max()
            df = df[(df["year"] >= ymin) & (df["year"] <= ymax)]
        self.filtered = df
        self._update_cards()
        self._draw_overview()
        self._fill_tree()
        if self.current_analysis_plot_func:
            self.current_analysis_plot_func()

    def _update_cards(self):
        d = self.filtered
        self.cards["Total Cars"].configure(text=f"{len(d):,}")
        self.cards["Average Price"].configure(
            text=f"${d['price'].mean():,.0f}" if not d.empty else "$0")
        m = d["mileage"].mean() if "mileage" in d.columns else np.nan
        self.cards["Average Mileage"].configure(
            text=f"{m:,.0f} mi" if not np.isnan(m) else "-")
        r = d["consumerrating"].mean() if "consumerrating" in d.columns else np.nan
        self.cards["Avg Rating"].configure(
            text=f"{r:.2f}" if not np.isnan(r) else "-")

    # ───────────────── OVERVIEW PLOTS (clickable) ──────────────
    def _draw_overview(self):
        if hasattr(self, "_ov_pick_id"):
            self.ov_fig.canvas.mpl_disconnect(self._ov_pick_id)
        
        self.ov_fig.clear()
        self._ov_annot = None 

        df = self.filtered
        if df.empty:
            ax = self.ov_fig.add_subplot(111)
            ax.axis("off")
            ax.text(0.5, 0.5, "No data", ha="center", va="center", color="white", fontsize=16)
            self.ov_canvas.draw(); return

        gs = self.ov_fig.add_gridspec(2, 2)
        
        ax_hist = self.ov_fig.add_subplot(gs[0, 0])
        ax_scatter = self.ov_fig.add_subplot(gs[0, 1])
        ax_pie = self.ov_fig.add_subplot(gs[1, 0])
        ax_bar = self.ov_fig.add_subplot(gs[1, 1])
        
        ax_hist.hist(df["price"], bins=30, color=self.clr.info)
        ax_hist.set_title("Price Distribution", color="w")
        ax_hist.set_xlabel("Price ($)", color="w"); ax_hist.set_ylabel("Cars", color="w")
        ax_hist.tick_params(colors="w")

        df_scatter_data = df.dropna(subset=["mileage", "price"])
        self._ov_scatter_map = {}
        if not df_scatter_data.empty:
            sc = ax_scatter.scatter(df_scatter_data["mileage"], df_scatter_data["price"],
                                    s=45, alpha=0.8, c=df_scatter_data["year"], cmap="viridis")
            sc.set_picker(True); sc.set_pickradius(10)
            self._ov_scatter_map[sc] = df_scatter_data.reset_index(drop=True)
            cb = self.ov_fig.colorbar(sc, ax=ax_scatter)
            cb.ax.yaxis.set_major_locator(MaxNLocator(integer=True))
            cb.ax.tick_params(colors="w"); cb.set_label("Year", color="w")

            def _on_pick(event):
                if len(event.ind) == 0:
                    return
                row = self._ov_scatter_map[event.artist].iloc[event.ind[0]]
                label = shorten(f"{row['make']} {row.get('model','')}", width=40, placeholder="…")
                if self._ov_annot:
                    self._ov_annot.remove()
                self._ov_annot = ax_scatter.annotate(
                    label, (row["mileage"], row["price"]),
                    xytext=(10, 10), textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="white", alpha=0.9), color="black")
                self.ov_canvas.draw_idle()
            self._ov_pick_id = self.ov_fig.canvas.mpl_connect("pick_event", _on_pick)

        ax_scatter.set_title("Mileage vs Price", color="w")
        ax_scatter.set_xlabel("Mileage", color="w"); ax_scatter.set_ylabel("Price ($)", color="w")
        ax_scatter.tick_params(colors="w")

        if "drivetrain" in df.columns:
            cnt = df["drivetrain"].value_counts()
            if not cnt.empty:
                ax_pie.pie(cnt, labels=cnt.index, autopct="%1.0f%%", textprops={'color': 'w'})
            ax_pie.set_title("Cars by Drivetrain", color="w")

        if not df.empty:
            top = df.groupby("make")["price"].mean().nlargest(10).sort_values()
            if not top.empty:
                top.plot(kind="barh", ax=ax_bar, color=self.clr.primary)
        ax_bar.set_title("Top-10 Makes by Avg Price", color="w")
        ax_bar.set_xlabel("Average Price ($)", color="w"); ax_bar.set_ylabel("Make", color="w")
        ax_bar.tick_params(colors="w")

        self.ov_canvas.draw()

    # ───────────────── ANALYSIS PLOTS ──────────────────────────
    def _corr(self):
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        
        num = self.filtered.select_dtypes(include=np.number)
        if num.shape[1] < 2:
            ax.text(0.5, 0.5, "Not Enough Numeric Data", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
        
        im = ax.imshow(num.corr(), cmap="RdYlBu_r", vmin=-1, vmax=1)
        ax.set_xticks(range(num.shape[1])); ax.set_yticks(range(num.shape[1]))
        ax.set_xticklabels(num.columns, rotation=45, ha="right", color="w")
        ax.set_yticklabels(num.columns, color="w")
        cb = self.an_fig.colorbar(im, ax=ax, fraction=0.046)
        cb.ax.tick_params(colors="w"); cb.set_label("Correlation", color="w")
        ax.set_title("Feature Correlation Heat-map", color="w")
        self.an_canvas.draw()

    def _price_make(self):
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        
        df = self.filtered
        if df.empty or {"make","price"}.issubset(df.columns) is False:
            ax.text(0.5, 0.5, "No Data for this Filter", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return

        makes = df["make"].value_counts().nlargest(15).index
        if makes.empty:
            ax.text(0.5, 0.5, "No Makes to Display", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
            
        data  = [df[df["make"] == m]["price"] for m in makes]
        # ### FIX: Use 'labels' instead of 'tick_labels' ###
        ax.boxplot(data, labels=makes, vert=False, patch_artist=True,
                   boxprops=dict(facecolor=self.clr.info),
                   medianprops=dict(color=self.clr.danger))
        ax.set_title("Price Distribution by Make", color="w")
        ax.set_xlabel("Price ($)", color="w"); ax.set_ylabel("Make", color="w")
        ax.tick_params(colors="w")
        self.an_canvas.draw()

    def _ratings(self):
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        
        cols = [c for c in (
            "consumerrating","comfortrating","interiordesignrating",
            "performancerating","valueformoneyrating","reliabilityrating")
            if c in self.filtered.columns]
        
        if not cols:
            ax.text(0.5, 0.5, "No Rating Data in CSV", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
            
        data = self.filtered[cols].dropna()
        if data.empty:
            ax.text(0.5, 0.5, "No Rating Data for this Filter", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return

        ax.boxplot(data.values,
                   labels=[c.replace("rating","") for c in cols],
                   patch_artist=True,
                   boxprops=dict(facecolor=self.clr.warning),
                   medianprops=dict(color=self.clr.danger))
        ax.set_title("Ratings Distribution", color="w")
        ax.set_ylabel("Rating (out of 5)", color="w"); ax.set_xlabel("Rating Type", color="w")
        ax.tick_params(colors="w", rotation=45)
        self.an_canvas.draw()

    def _mpg(self):
        if hasattr(self, "_mpg_pick_id"):
            self.an_fig.canvas.mpl_disconnect(self._mpg_pick_id)
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        self._mpg_annot = None
        
        raw = self.filtered
        if {"minmpg","maxmpg","make"}.issubset(raw.columns) is False:
            ax.text(0.5,0.5,"No MPG Data in CSV",ha="center",va="center",color="w", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
            
        df = raw.dropna(subset=["minmpg","maxmpg"])
        if df.empty:
            ax.text(0.5,0.5,"No MPG Data for this Filter",ha="center",va="center",color="w", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return

        top = df["make"].value_counts().nlargest(6).index
        palette = plt.cm.tab10.colors
        self._scatter_map = {}
        rest = df[~df["make"].isin(top)]
        if not rest.empty:
            sc = ax.scatter(rest["minmpg"], rest["maxmpg"],
                            s=25, c="lightgrey", alpha=.45, label="Other")
            sc.set_picker(True); sc.set_pickradius(10)
            self._scatter_map[sc] = rest.reset_index(drop=True)
        for i, mk in enumerate(top):
            sub = df[df["make"] == mk]
            sc = ax.scatter(sub["minmpg"], sub["maxmpg"],
                            s=35, color=palette[i % 10], label=mk, alpha=.8)
            sc.set_picker(True); sc.set_pickradius(10)
            self._scatter_map[sc] = sub.reset_index(drop=True)
        def _on_pick(event):
            if len(event.ind) == 0:
                return
            row = self._scatter_map[event.artist].iloc[event.ind[0]]
            label = shorten(f"{row['make']} {row.get('model','')}", width=40, placeholder="…")
            if self._mpg_annot: self._mpg_annot.remove()
            self._mpg_annot = ax.annotate(
                label, (row["minmpg"], row["maxmpg"]),
                xytext=(10, 10), textcoords="offset points",
                bbox=dict(boxstyle="round", fc="white", alpha=0.9), color="black")
            self.an_canvas.draw_idle()
        self._mpg_pick_id = self.an_fig.canvas.mpl_connect("pick_event", _on_pick)
        try:
            best_hwy  = df.loc[df["maxmpg"].idxmax()]
            best_city = df.loc[df["minmpg"].idxmax()]
            for r, t in [(best_hwy, "Best Hwy"), (best_city, "Best City")]:
                ax.annotate(
                    f"{t}: {shorten(r['make']+' '+str(r.get('model','')),28, placeholder='…')}",
                    xy=(r["minmpg"], r["maxmpg"]),
                    xytext=(5, 5), textcoords="offset points",
                    fontsize=7, color="w", backgroundcolor="#00000080")
        except (ValueError, KeyError): pass
        ax.set_title("City MPG vs Highway MPG", color="w")
        ax.set_xlabel("City MPG", color="w"); ax.set_ylabel("Highway MPG", color="w")
        ax.tick_params(colors="w")
        if len(top) > 0:
            ax.legend(facecolor="#1e1e1e", framealpha=.3, fontsize=8, labelcolor="w", loc="upper left")
        self.an_canvas.draw()

    # ───────────── TABLE / SEARCH / EXPORT ─────────────────────
    def _fill_tree(self):
        self.tree.delete(*self.tree.get_children())
        for _, row in self.filtered.head(500).iterrows():
            vals = [f"{v:,.2f}" if isinstance(v, float)
                    else f"{int(v):,}" if isinstance(v, (int, np.integer)) else v
                    for v in row]
            self.tree.insert("", "end", values=vals)

    def _search_tree(self, *_):
        term = self.search.get().lower()
        self.tree.delete(*self.tree.get_children())
        if not term: self._fill_tree(); return
        mask = self.filtered.astype(str).apply(
            lambda s: s.str.lower().str.contains(term, na=False)).any(axis=1)
        for _, row in self.filtered[mask].head(500).iterrows():
            vals = [f"{v:,.2f}" if isinstance(v, float)
                    else f"{int(v):,}" if isinstance(v, (int, np.integer)) else v
                    for v in row]
            self.tree.insert("", "end", values=vals)

    def _export(self):
        fn = tb.dialogs.filedialog.asksaveasfilename(
            defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if fn:
            self.filtered.to_csv(fn, index=False)
            tb.dialogs.Messagebox.show_info("Export complete", fn)

# ═══════════════════════════════════════════════════════════════
if __name__ == "__main__":
    root = tb.Window(themename="darkly")
    Dashboard(root)
    root.mainloop()

High-Level Code Description and Technology Stack

This Python script creates a comprehensive and highly interactive graphical dashboard designed for the exploratory analysis of a used car dataset. It is built as a standalone desktop application using a combination of powerful libraries. Tkinter, via the ttkbootstrap wrapper, provides the modern, themed graphical user interface (GUI) components and window management. Data manipulation and aggregation are handled efficiently in the background by the pandas library. All data visualisations are generated by matplotlib and seamlessly embedded into the Tkinter window using its FigureCanvasTkAgg backend, allowing for complex, interactive charts within the application frame. The application is architected within a single Dashboard class, encapsulating all its state and methods for a clean, organised structure.

Data Ingestion and Preprocessing

Upon startup, the application performs a robust data loading and cleaning sequence. It reads a specified CSV file using pandas, immediately performing several preprocessing steps to ensure data quality and consistency.

  1. Header Normalisation: It iterates through all column names, converting them to lowercase and removing special characters. This prevents errors caused by inconsistent naming, such as “Price” vs. “price”.
  2. Column Aliasing: It uses a predefined dictionary to rename common alternative column names (e.g., “brand” or “manufacturer”) to a standard internal name (e.g., “make”). This adds flexibility, allowing the application to work with different CSV formats without code changes.
  3. Intelligent ‘Year’ Detection: If a “year” column isn’t explicitly found, the script intelligently scans other columns to find one containing numbers that fall within a typical automotive year range (1900–2035), automatically designating it as the ‘year’ column.
  4. Type Coercion: It systematically cleans columns expected to be numeric (like price and mileage) by removing non-numeric characters (e.g., ‘$’, ‘,’, ‘ mi’) and converting the results to floating-point numbers, gracefully handling any conversion errors.
  5. Data Pruning: Finally, it removes any rows that are missing essential data points (make and price), ensuring that all data used for plotting and analysis is valid.

User Interface and Interactive Filtering

The user interface is organised into a main notebook with three distinct tabs, providing a straightforward workflow for analysis.

  • A central feature is the dynamic filtering panel. This panel contains widgets like a Combobox for car makes and Spinbox controls for price and year ranges. These widgets are linked directly to the application’s core logic.
  • State Management: When a user changes a filter, a central method, _apply_filters, is triggered. This function creates a new, temporary pandas DataFrame named self.filtered by applying the user’s selections to the master dataset. This self.filtered DataFrame then becomes the single source of truth for all visual components.
  • Automatic UI Refresh: After the data is filtered, the _apply_filters method orchestrates a full refresh of the dashboard by calling all necessary update functions. This includes redrawing every plot on the “Overview” tab, updating the key performance indicator (KPI) cards, repopulating the data table, and crucially, redrawing the currently active chart on the “Analysis” tab. This creates a highly responsive and intuitive user experience.

Visualisation and Analysis Tabs

The core value of the application lies in its visualisation capabilities, spread across two tabs:

1/ Overview Tab: This serves as the main dashboard, featuring:

  • KPI Cards: Four prominent cards at the top display key metrics like “Total Cars” and “Average Price,” which update in real-time with the filters.
  • 2×2 Chart Grid: A large, multi-panel figure displays four charts simultaneously: a histogram for price distribution, a pie chart for drivetrain types, a horizontal bar chart for the top 10 makes by average price, and a clickable scatter plot showing vehicle mileage versus price, colored by year. Clicking a point on this scatter plot brings up an annotation showing the car’s make and model. This interactivity is achieved by connecting a Matplotlib pick_event to a handler function that draws the annotation.

2/ Analysis Tab: This tab is for more focused, single-plot analysis. A row of buttons allows the user to select one of several advanced visualisations:

  • Correlation Heatmap: Shows the correlation between all numeric columns in the dataset.
  • Price by Make Box Plot: Compares the price distributions of the top 15 most common car makes, providing insight into price variance and outliers.
  • Ratings Box Plot: Displays and compares the distributions of various consumer rating categories (e.g., comfort, performance, reliability).
  • MPG Scatter Plot: A fully interactive scatter plot for analysing city vs. highway MPG, with points colored by make and a click-to-annotate feature similar to the one on the overview tab.
    The application cleverly remembers which analysis plot was last viewed and automatically redraws it with new data whenever the global filters are changed.

3/ Data Tab: For users who want to inspect the raw numbers, this tab displays the filtered data in a scrollable Treeview table. It also includes a live search box that instantly filters the table’s contents as the user types.

Running the code

The code is run in the same way as a regular Python program, so save it to a Python file, e.g tktest.py, and make sure you change the file location to be wherever you downloaded the file from Kaggle. Run the code like this:

$ python tktest.py

Your screen should look like this,

Image by Author

You can switch between the Overview, Analytics and data TABS for different views on the data. If you change the Make or Drivetrain from the drop-down options, the displayed data will reflect this immediately. Use the Apply Year/Price Filter button to see changes to the data when you choose different year or price ranges.

The overview screen is the one you first see when the GUI displays. It consists of four main charts and informational displays of statistics just underneath the filter fields.

The Analysis TAB provides four additional views of the data. A correlation heat-map, a Price by make chart, an MPG chart showing how efficient the various make/models are and a rating chart over six different metrics. On both the Price by Make chart and the Mileage v price chart on the overview TAB, you can click on an individual “dot” on the chart to see which car make and model it’s referring to. Here is what the MPG chart looks like showing how efficient various makes are in comparing their City v Highway MPG figures.

Image by Author

Lastly, we have a Data TAB. This is just a rows and columns tabular representation of the underlying data set. Like all the displayed charts, this output changes as you filter the data.

To see it in action, I first clicked on the Overview TAB and changed the input parameters to be,

Make: BMW 
Drivetrain: All-wheel Drive 
Price: 2300.0 to 449996.0 
Year: 2022

I then clicked on the data TAB and got this output.

Summary

This article serves as a comprehensive guide to using Tkinter, Python’s original built-in GUI library, for creating modern, data-driven desktop applications. It is a durable, lightweight, and still-relevant tool, and paired with the ttkbootstrap library, is more than capable of producing modern-looking data displays and dashboards.

I began by covering the fundamental building blocks of any Tkinter application, such as the root window, widgets (buttons, labels), and geometry managers for layout. 

I then moved on to a fully-featured analytics tool with a tabbed interface, dynamic filters that update all visuals in real-time, and clickable charts that provide a responsive and professional user experience, making a strong case for Tkinter’s capabilities beyond simple utilities.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *