Home » General Python Tutorials » How to Develop a Data Visualization Interface in Python with Dash and Tkinter

How to Develop a Data Visualization Interface in Python with Dash and Tkinter

In a world where data reigns supreme, being able to visualize it interactively and clearly is essential. Whether you’re exploring trends, tracking key metrics, or simplifying complex information, a good interface can make all the difference. That’s where Python’s dynamic duo, Dash and Tkinter, come into play.

Today, we’re going to roll up our sleeves and build a cool data visualization interface using Dash and Tkinter. I’ll walk you through each step of creating an interactive dashboard that blends the strengths of both tools. By the end, you’ll have a dynamic and user-friendly visualization that’s easy to set up and even easier to use. Let’s dive in and have some fun with it!

Table of Contents

Necessary Libraries

Before you start, make sure you have these libraries installed. Just open your terminal or command prompt and set them up.

$ pip install dash
$ pip install plotly
$ pip install tk

Imports

To build an app that truly impresses users, we need the right tools. First, we’ll use Dash to create our web app and visualize data. We’ll import dash components and layout functions like this:

import dash
from dash import dcc, html
from dash.dependencies import Input, Output

For making our plots interactive and dynamic, we’ll use plotly.express:

import plotly.express as px

Since we want Dash and tkinter to work together concurrently, we’ll use threading to avoid freezing the main window. For more details on how to use Dash, check out the Dash documentation.

import threading

Tkinter will help us create a user-friendly graphical interface with ttk for stylish widgets:

import tkinter as tk
from tkinter import ttk

And to ensure that Dash and Tkinter can share information safely, we’ll use a queue:

from queue import Queue

Dataset Loading & Dash Initialization

First, we need to load our dataset to get our data visualization dashboard up and running. We’re using the “Gapminder” dataset, which offers valuable insights about countries, including life expectancy. You can explore more about this dataset and Gapminder’s work on their official website:

# Load a real-world dataset
df = px.data.gapminder()

Next, we ensure smooth communication between Tkinter and Dash by using a Queue(). This keeps both parts of our app in sync and allows them to share information effectively:

# Shared state using queues
state_queue = Queue()

Lastly, we’ll go ahead and set up our Dash application. This step is where we lay the groundwork for our interactive dashboard, making sure everything is ready to bring our data visualizations to life:

# Initialize the Dash app
app = dash.Dash(__name__)

Creating the Layout for Dash

Now that we’ve got our web app up and running, it’s time to make it look and work just the way we want. Let’s walk through the layout setup step by step:

  • First up, we need a container to hold all the components of our app. We use html.Div for this, which will serve as the main wrapper for everything:
app.layout = html.Div([
  • Next, to let users select a continent, we add a dropdown menu. This is done with dcc.Dropdown, which provides a list of options for users to choose from:
dcc.Dropdown(
    id='continent-dropdown',
    options=[{'label': continent, 'value': continent} for continent in df['continent'].unique()],
    value='Asia',
    style={'width': '50%'}
),
  • For selecting the year, we use a checklist. This allows users to pick multiple years from the dcc.Checklist:
dcc.Checklist(
    id='year-checklist',
    options=[{'label': str(year), 'value': year} for year in df['year'].unique()],
    value=[1952],
    style={'margin': '20px 0'}
),
  • To choose the minimum population value, we’ll use a slider. The dcc.Slider makes it easy to adjust the population range:
dcc.Slider(
    id='pop-slider',
    min=df['pop'].min(),
    max=df['pop'].max(),
    value=df['pop'].min(),
    marks={int(pop): f'{int(pop / 1e6)}M' for pop in df['pop'].unique()},
    step=1e7,
    tooltip={"placement": "bottom", "always_visible": True}
),
  • For giving users a choice between different chart types, we use radio buttons. dcc.RadioItems allows us to let users select either a scatter plot or a bar graph:
dcc.RadioItems(
    id='chart-type',
    options=[
        {'label': 'Scatter Plot', 'value': 'scatter'},
        {'label': 'Bar Graph', 'value': 'bar'}
    ],
    value='scatter',
    labelStyle={'display': 'inline-block', 'margin': '10px'}
),
  • Finally, we need a spot to display our charts. We set this up with another html.Div to act as a container for the charts:
html.Div(id='charts-container')
])

This layout sets the stage for an interactive and user-friendly dashboard where users can explore the data dynamically.

Updating the Charts

# Function to update the charts
def update_charts(selected_continent, selected_years, min_pop, chart_type):
   filtered_df = df[
       (df['continent'] == selected_continent) & (df['year'].isin(selected_years)) & (df['pop'] >= min_pop)]


   if chart_type == 'scatter':
       fig = px.scatter(filtered_df, x='gdpPercap', y='lifeExp', size='pop', color='country', hover_name='country',
                        title=f'Life Expectancy vs GDP per Capita in {selected_continent}', log_x=True)
   elif chart_type == 'bar':
       fig = px.bar(filtered_df, x='country', y='lifeExp', color='country',
                    title=f'Life Expectancy in {selected_continent}')


   return [dcc.Graph(figure=fig)]

With our canvas set up, we now need the brush to paint our charts, and that brush is the update_charts() function. This function uses filtered_df to pull out just the data selected by users from our dataset.

Here’s what it does:

  • It first checks which type of chart the user has picked.
  • If the choice is a scatter plot, it displays the relationship between GDP per capita and life expectancy, with the size of the dots representing the population.
  • If a bar graph is selected, it shows life expectancy for the chosen countries within the selected continent.

Finally, update_charts() returns the chart so it can be displayed on our dashboard.

Callback to Update Charts

# Callback to update the charts based on selections
@app.callback(
   Output('charts-container', 'children'),
   [Input('continent-dropdown', 'value'),
    Input('year-checklist', 'value'),
    Input('pop-slider', 'value'),
    Input('chart-type', 'value')]
)
def display_charts(selected_continent, selected_years, min_pop, chart_type):
   return update_charts(selected_continent, selected_years, min_pop, chart_type)

For this step, we’ll connect the user’s choices to our web app using @app.callback. This links the user’s inputs to the display_charts() function, ensuring that whenever the user makes changes, the function updates the charts with the new values.

Starting the Dash Server & Main Window

With everything set up, it’s time to start the Dash server using the run_dash() function. This function will launch our web app in a browser, show any errors that occur, and prevent the server from restarting every time we make code changes.

# Function to run the Dash server
def run_dash():
   app.run_server(debug=True, use_reloader=False)

Now, let’s set up our command center:

We begin by creating the main window for our application. This window will be our control center, where users can make selections that will update the Dash app. We give it a title and define its size to make everything fit nicely on the screen.

root = tk.Tk()
root.title("Dash Controller - The Pycodes")
root.geometry("350x550")

Next, we create a dropdown menu where users can select a continent. This menu is linked to a StringVar, which stores the selected continent automatically. We also add a label to guide the user in making their choice.

continent_var = tk.StringVar(value="Asia")
continent_label = tk.Label(root, text="Select Continent:")
continent_label.pack(pady=5)
continent_dropdown = ttk.Combobox(root, textvariable=continent_var, values=df['continent'].unique(), state='readonly')
continent_dropdown.pack(pady=5)

For the year selection, we add a listbox that allows users to choose multiple years. The listbox is filled with the available years from the dataset. A label is also added to prompt the user to select the years.

year_label = tk.Label(root, text="Select Years:")
year_label.pack(pady=5)
year_listbox = tk.Listbox(root, selectmode=tk.MULTIPLE)
for year in df['year'].unique():
    year_listbox.insert(tk.END, year)
year_listbox.pack(pady=5)

Now, we add a slider that lets users pick the minimum population value in millions. This helps them filter the data based on population size. Again, we include a label to explain what the slider does.

pop_label = tk.Label(root, text="Minimum Population (Millions):")
pop_label.pack(pady=5)
pop_scale = tk.Scale(root, from_=df['pop'].min() / 1e6, to=df['pop'].max() / 1e6, orient=tk.HORIZONTAL)
pop_scale.pack(pady=5)

We also include a dropdown menu for the user to select the type of chart they want to see, either a scatter plot or a bar graph. Like before, we link this to a StringVar and add a label to guide the selection.

chart_type_var = tk.StringVar(value="scatter")
chart_type_label = tk.Label(root, text="Select Chart Type:")
chart_type_label.pack(pady=5)
chart_type_dropdown = ttk.Combobox(root, textvariable=chart_type_var, values=["scatter", "bar"], state='readonly')
chart_type_dropdown.pack(pady=5)

The next part is crucial: we create the update_and_send_state() function. This function grabs the user’s inputs—like the selected continent, years, minimum population, and chart type—and puts them into a queue. This queue sends the inputs to the Dash app, ensuring that the user’s selections are reflected in the app.

def update_and_send_state():
    selected_continent = continent_var.get()
    selected_years = [int(year_listbox.get(i)) for i in year_listbox.curselection()]
    min_pop = pop_scale.get() * 1e6
    chart_type = chart_type_var.get()

    # Send state to Dash app via queue
    state_queue.put((selected_continent, selected_years, min_pop, chart_type))

    # Dynamically update Dash layout (thread-safe)
    app.layout.children[0].value = selected_continent
    app.layout.children[1].value = selected_years
    app.layout.children[2].value = min_pop
    app.layout.children[3].value = chart_type

To make everything work, we add two buttons. The “Update Dash App” button triggers the update_and_send_state() function, sending the user’s selections to the Dash app. The “Start Dash App” button starts the Dash app in a new thread, so it runs independently of the Tkinter interface.

update_button = tk.Button(root, text="Update Dash App", command=update_and_send_state)
update_button.pack(pady=10)

start_button = tk.Button(root, text="Start Dash App", command=lambda: threading.Thread(target=run_dash).start())
start_button.pack(pady=10)

Finally, we keep the Tkinter window running and responsive to user interactions by starting the main event loop.

root.mainloop()

This setup makes our Tkinter interface a powerful control panel that lets users easily interact with and update the Dash app.

Synchronizing Between Tkinter and Dash

To keep Tkinter and Dash in sync, we’ve set up a sync_states() function. This function runs in the background while the app is active, constantly checking for any new data in the state_queue. Whenever it finds something, it grabs the data and updates the Dash app to reflect those changes.

# Background thread to sync states between Tkinter and Dash
def sync_states():
   while True:
       if not state_queue.empty():
           state = state_queue.get()
           continent, years, min_pop, chart_type = state
           display_charts(continent, years, min_pop, chart_type)

And finally, we kick off our Tkinter interface in a new thread. This way, it can run on its own while still staying in sync with the Dash app, thanks to the connection through the queue.

# Start the Tkinter app and synchronization thread
if __name__ == "__main__":
   threading.Thread(target=start_tkinter).start()
   threading.Thread(target=sync_states, daemon=True).start()

Example

Full Code

import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.express as px
import threading
import tkinter as tk
from tkinter import ttk
from queue import Queue


# Load a real-world dataset
df = px.data.gapminder()


# Shared state using queues
state_queue = Queue()


# Initialize the Dash app
app = dash.Dash(__name__)


# Layout for the Dash app
app.layout = html.Div([
   dcc.Dropdown(
       id='continent-dropdown',
       options=[{'label': continent, 'value': continent} for continent in df['continent'].unique()],
       value='Asia',
       style={'width': '50%'}
   ),
   dcc.Checklist(
       id='year-checklist',
       options=[{'label': str(year), 'value': year} for year in df['year'].unique()],
       value=[1952],
       style={'margin': '20px 0'}
   ),
   dcc.Slider(
       id='pop-slider',
       min=df['pop'].min(),
       max=df['pop'].max(),
       value=df['pop'].min(),
       marks={int(pop): f'{int(pop / 1e6)}M' for pop in df['pop'].unique()},
       step=1e7,
       tooltip={"placement": "bottom", "always_visible": True}
   ),
   dcc.RadioItems(
       id='chart-type',
       options=[
           {'label': 'Scatter Plot', 'value': 'scatter'},
           {'label': 'Bar Graph', 'value': 'bar'}
       ],
       value='scatter',
       labelStyle={'display': 'inline-block', 'margin': '10px'}
   ),
   html.Div(id='charts-container')
])


# Function to update the charts
def update_charts(selected_continent, selected_years, min_pop, chart_type):
   filtered_df = df[
       (df['continent'] == selected_continent) & (df['year'].isin(selected_years)) & (df['pop'] >= min_pop)]


   if chart_type == 'scatter':
       fig = px.scatter(filtered_df, x='gdpPercap', y='lifeExp', size='pop', color='country', hover_name='country',
                        title=f'Life Expectancy vs GDP per Capita in {selected_continent}', log_x=True)
   elif chart_type == 'bar':
       fig = px.bar(filtered_df, x='country', y='lifeExp', color='country',
                    title=f'Life Expectancy in {selected_continent}')


   return [dcc.Graph(figure=fig)]


# Callback to update the charts based on selections
@app.callback(
   Output('charts-container', 'children'),
   [Input('continent-dropdown', 'value'),
    Input('year-checklist', 'value'),
    Input('pop-slider', 'value'),
    Input('chart-type', 'value')]
)
def display_charts(selected_continent, selected_years, min_pop, chart_type):
   return update_charts(selected_continent, selected_years, min_pop, chart_type)


# Function to run the Dash server
def run_dash():
   app.run_server(debug=True, use_reloader=False)


# Tkinter Application Setup
def start_tkinter():
   root = tk.Tk()
   root.title("Dash Controller - The Pycodes")
   root.geometry("350x550")


   # Tkinter UI Components
   continent_var = tk.StringVar(value="Asia")
   continent_label = tk.Label(root, text="Select Continent:")
   continent_label.pack(pady=5)
   continent_dropdown = ttk.Combobox(root, textvariable=continent_var, values=df['continent'].unique(),
                                     state='readonly')
   continent_dropdown.pack(pady=5)


   year_label = tk.Label(root, text="Select Years:")
   year_label.pack(pady=5)
   year_listbox = tk.Listbox(root, selectmode=tk.MULTIPLE)
   for year in df['year'].unique():
       year_listbox.insert(tk.END, year)
   year_listbox.pack(pady=5)


   pop_label = tk.Label(root, text="Minimum Population (Millions):")
   pop_label.pack(pady=5)
   pop_scale = tk.Scale(root, from_=df['pop'].min() / 1e6, to=df['pop'].max() / 1e6, orient=tk.HORIZONTAL)
   pop_scale.pack(pady=5)


   chart_type_var = tk.StringVar(value="scatter")
   chart_type_label = tk.Label(root, text="Select Chart Type:")
   chart_type_label.pack(pady=5)
   chart_type_dropdown = ttk.Combobox(root, textvariable=chart_type_var, values=["scatter", "bar"], state='readonly')
   chart_type_dropdown.pack(pady=5)


   # Advanced Buttons and Features
   def update_and_send_state():
       selected_continent = continent_var.get()
       selected_years = [int(year_listbox.get(i)) for i in year_listbox.curselection()]
       min_pop = pop_scale.get() * 1e6
       chart_type = chart_type_var.get()


       # Send state to Dash app via queue
       state_queue.put((selected_continent, selected_years, min_pop, chart_type))


       # Dynamically update Dash layout (thread-safe)
       app.layout.children[0].value = selected_continent
       app.layout.children[1].value = selected_years
       app.layout.children[2].value = min_pop
       app.layout.children[3].value = chart_type


   update_button = tk.Button(root, text="Update Dash App", command=update_and_send_state)
   update_button.pack(pady=10)


   start_button = tk.Button(root, text="Start Dash App", command=lambda: threading.Thread(target=run_dash).start())
   start_button.pack(pady=10)


   root.mainloop()


# Background thread to sync states between Tkinter and Dash
def sync_states():
   while True:
       if not state_queue.empty():
           state = state_queue.get()
           continent, years, min_pop, chart_type = state
           display_charts(continent, years, min_pop, chart_type)


# Start the Tkinter app and synchronization thread
if __name__ == "__main__":
   threading.Thread(target=start_tkinter).start()
   threading.Thread(target=sync_states, daemon=True).start()

Happy Coding!

Leave a Comment

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

Scroll to Top
×