Python Tkinter Interactive Charts

How to Make Interactive Charts with Hover Effects Using Python Tkinter

Python Tkinter Interactive Charts


In this Python Tutorial we will see How to Create an interactive charts with animated bar/line/scatter charts, smooth entrance animations, hover tooltips, and gradient styling using Python And Tkinter library.

What We Are Gonna Use In This Project:

- Python Programming Language.
- Tkinter (GUI).
- VS Editor.





Project Source Code:



import tkinter as tk
from tkinter import ttk
import math

class InteractiveChart(tk.Tk):
"""
Main application class that creates an interactive chart window.
This class inherits from tk.Tk, which means it IS a tkinter window.
"""
def __init__(self):
"""
Constructor method - runs automatically when we create a new instance.
Sets up the entire application window and all its components.
"""
# Call the parent class (tk.Tk) constructor to create the window
super().__init__()
# Configure the main window properties
self.title("Interactive Chart")
self.protocol("WM_DELETE_WINDOW", self.on_close)
self.geometry("800x600")
self.configure(bg="#0D1117")
# Define a color scheme dictionary for consistent styling throughout the app
# This makes it easy to change colors later and keeps the design consistent
self.colors = {
"bg_primary": "#0D1117", # Main background (very dark blue-gray)
"bg_secondary": "#161B22", # Secondary background (slightly lighter)
"bg_tertiary": "#21262D", # Third background level (even lighter)
"accent_primary": "#58A6FF", # Main accent color (bright blue)
"accent_secondary": "#238636", # Secondary accent (green)
"accent_danger": "#F85149", # Error/danger color (red)
"accent_warning": "#D29922", # Warning color (yellow)
"text_primary": "#F0F6FC", # Main text color (almost white)
"text_secondary": "#8B949E", # Secondary text color (gray)
"border": "#30363D", # Border color (medium gray)
"hover": "#262C36" # Hover state color (slightly lighter gray)
}
# Create sample data for our chart
# Each DataPoint has a label (like "Q1"), value (like 65),
# and category (like "Revenue")
self.data = [
DataPoint("Q1", 65, "Revenue"), # Q1 had 65 units of revenue
DataPoint("Q2", 85, "Revenue"), # Q2 had 85 units of revenue
DataPoint("Q3", 45, "Revenue"), # Q3 had 45 units of revenue
DataPoint("Q4", 22, "Revenue"), # Q4 had 22 units of revenue
DataPoint("Q5", 78, "Revenue") # Q5 had 78 units of revenue
]
# Create all the visual components of our application
self.create_main_container() # Create the main container that holds everything
self.create_chart() # Create the chart area
self.create_controls() # Create the control buttons
# Initialize tooltip (the popup that shows when you hover over data points)
self.tooltip = None

def create_main_container(self):
"""
Creates the main container frame that holds all other components.
"""
# Create a frame (container) that fills the entire window
self.main_frame = tk.Frame(self, bg=self.colors["bg_primary"])
# Pack it to fill the entire window with some padding around the edges
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=30, pady=30)

def create_chart(self):
"""
Creates the chart area where the actual graph will be displayed.
This includes the frame that contains the chart and the chart itself.
"""
# Create a frame specifically for the chart with styling
self.chart_frame = tk.Frame(
self.main_frame, # Put it inside the main frame
bg=self.colors["bg_secondary"], # Set background color
highlightbackground=self.colors["border"], # Border color
highlightthickness=2 # Border thickness (2 pixels)
)
# Pack the chart frame to fill most of the space,
# leaving room for controls below
self.chart_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
# Create the actual chart panel
self.chart_panel = ChartPanel(self.chart_frame, self.data, self.colors)
# Pack the chart panel to fill the entire chart frame
self.chart_panel.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)

def create_controls(self):
"""
Creates the control panel with buttons to switch between different chart types.
This appears at the bottom of the window.
"""
# Create a frame for the control buttons
self.control_frame = tk.Frame(
self.main_frame, # Put it inside the main frame
bg=self.colors["bg_secondary"], # Background color
highlightbackground=self.colors["border"], # Border color
highlightthickness=2, # Border thickness
height=80 # Fixed height of 80 pixels
)
# Pack the control frame at the bottom
self.control_frame.pack(fill=tk.X) # Fill horizontally but not vertically
self.control_frame.pack_propagate(False) # Don't let children change the size
# Create another frame inside the control frame to center the buttons
button_frame = tk.Frame(self.control_frame, bg=self.colors["bg_secondary"])
button_frame.pack(expand=True) # This centers the button frame
# Dictionary to store our buttons so we can reference them later
self.buttons = {}
# Define the configuration for each button
# Each tuple contains: (button text, chart type identifier, button color)
button_configs = [
("Bar Chart", "bar", self.colors["accent_primary"]), # Blue bar chart button
# Green line chart button
("Line Chart", "line", self.colors["accent_secondary"]),
# Yellow scatter plot button
("Scatter Plot", "scatter", self.colors["accent_warning"])
]
# Create each button based on the configuration
for text, chart_type, color in button_configs:
# Create a modern-styled button
btn = self.create_modern_button(button_frame, text, chart_type, color)
# Pack the button horizontally with some spacing
btn.pack(side=tk.LEFT, padx=15, pady=20)
# Store the button in our dictionary for later reference
self.buttons[chart_type] = btn
# Set the bar chart button as active by default
self.set_active_button("bar")

def create_modern_button(self, parent, text, chart_type, accent_color):
"""
Creates a modern-styled button with hover effects.
Args:
parent: The widget that will contain this button
text: The text to display on the button
chart_type: The type of chart this button represents
("bar", "line", "scatter")
accent_color: The color to use when the button is active
Returns:
The created button widget
"""
# Create a button with styling
button = tk.Button(
parent, # Put the button in the specified parent widget
text=text, # Button text
# What happens when clicked
command=lambda: self.change_chart_type(chart_type),
font=("Segoe UI", 11, "bold"), # Font family, size, and style
fg=self.colors["text_secondary"], # Text color (gray)
bg=self.colors["bg_tertiary"], # Background color (dark)
activeforeground=self.colors["text_primary"], # Text color when clicked
activebackground=accent_color, # Background color when clicked
relief=tk.FLAT, # No 3D border effect
bd=0, # No border
padx=20, # Horizontal padding inside button
pady=12, # Vertical padding inside button
cursor="hand2" # Change cursor to hand when hovering
)
# Store the accent color as a custom attribute for later use
button.accent_color = accent_color
# Bind hover events to create interactive effects
button.bind("<Enter>", lambda e: self.on_button_hover(button))
button.bind("<Leave>", lambda e: self.on_button_leave(button))
return button

def on_button_hover(self, button):
"""
Called when the mouse cursor enters a button.
Changes the button appearance to show it's being hovered over.
"""
# Only change appearance if the button is not currently active
if not hasattr(button, 'is_active') or not button.is_active:
button.config(bg=self.colors["hover"],
fg=self.colors["text_primary"])


def on_button_leave(self, button):
"""
Called when the mouse cursor leaves a button.
Returns the button to its normal appearance.
"""
# Only change appearance if the button is not currently active
if not hasattr(button, 'is_active') or not button.is_active:
button.config(bg=self.colors["bg_tertiary"], fg=self.colors["text_secondary"])


def change_chart_type(self, chart_type):
"""
Changes the type of chart being displayed.
Called when a user clicks one of the chart type buttons.
Args:
chart_type: The new chart type to display |"bar"|"line"|"scatter"
"""
# Tell the chart panel to change its chart type
self.chart_panel.set_chart_type(chart_type)
# Update the button appearance to show which one is active
self.set_active_button(chart_type)


def set_active_button(self, chart_type):
"""
Sets one button as "active" (highlighted) and others as inactive.
This shows the user which chart type is currently selected.
Args:
chart_type: The type of chart button to make active ("bar", "line", "scatter")
"""
# Loop through all buttons
for btn_type, button in self.buttons.items():
if btn_type == chart_type:
# This is the active button - make it bright and colorful
button.config(bg=button.accent_color,
fg=self.colors["text_primary"])
button.is_active = True # Mark it as active
else:
# This is not the active button - make it dim
button.config(bg=self.colors["bg_tertiary"],
fg=self.colors["text_secondary"])
button.is_active = False # Mark it as inactive


def on_close(self):
"""
Called when the user closes the window.
Cleans up any running animations before closing.
"""
# Stop any running animations to prevent errors
if hasattr(self.chart_panel, 'animation_running'):
self.chart_panel.animation_running = False
# Close the window
self.destroy()


class DataPoint:
"""
A simple class to represent a single data point in our chart.
Each data point has a label, value, and category.
"""
def __init__(self, label, value, category):
"""
Creates a new data point.
Args:
label: A short name for this data point (like "Q1")
value: The numeric value (like 65)
category: What this data represents (like "Revenue")
"""
self.label = label
self.value = value
self.category = category



class ModernTooltip(tk.Toplevel):
"""
A modern-looking tooltip that appears when you hover over data points.
This class creates a small popup window with information about the data point.
"""
def __init__(self, parent, label, category, value, colors):
"""
Creates a new tooltip window.
Args:
parent: The widget that owns this tooltip
label: The data point label to display
category: The data point category to display
value: The data point value to display
colors: The color scheme dictionary
"""
# Create a new top-level window (a popup)
super().__init__(parent)
# Configure the tooltip window
self.overrideredirect(True) # Remove window decorations (title bar, etc.)
self.wm_attributes("-topmost", True) # Keep tooltip on top of other windows
self.configure(bg=colors["bg_tertiary"]) # Set background color
# Create a main frame for the tooltip content with a border
main_frame = tk.Frame(
self,
bg=colors["bg_tertiary"],
highlightbackground=colors["accent_primary"], # Blue border
highlightthickness=2 # 2-pixel border
)
main_frame.pack(fill=tk.BOTH, expand=True)
# Create a label showing the data point label and category
category_label = tk.Label(
main_frame,
text=f"{label}{category}", # Format: "Q1 • Revenue"
fg=colors["text_secondary"], # Gray text
bg=colors["bg_tertiary"], # Dark background
font=("Segoe UI", 10), # Font styling
padx=15, pady=10 # Padding around text
)
category_label.pack(anchor=tk.W) # Align to the left (west)
# Create a label showing the data point value (large and prominent)
value_label = tk.Label(
main_frame,
text=str(value), # Convert number to string
fg=colors["accent_primary"], # Bright blue text
bg=colors["bg_tertiary"], # Dark background
font=("Segoe UI", 16, "bold"), # Large, bold font
padx=15, pady=10 # Padding around text
)
value_label.pack(anchor=tk.W) # Align to the left (west)
# Update the tooltip to calculate its size
self.update_idletasks()


class ChartPanel(tk.Canvas):
"""
The main chart drawing area.
This is where all the actual chart visualization happens.
This class inherits from tk.Canvas, which is like a drawing surface.
"""
def __init__(self, parent, data, colors):
"""
Creates a new chart panel.
Args:
parent: The widget that will contain this chart
data: List of DataPoint objects to display
colors: Color scheme dictionary
"""
# Create a canvas (drawing surface) with no border
super().__init__(parent, bg=colors["bg_secondary"], highlightthickness=0)
# Store the parameters for later use
self.parent = parent
self.data = data
self.colors = colors
self.chart_type = "bar" # Start with bar chart
# Animation properties
self.animation_progress = 0.0 # How far along the animation is (0.0 to 1.0)
self.animation_running = False # Whether an animation is currently running
self.animation_increment = 0.08 # How much to advance animation each frame
# Chart layout margins (space around the chart for labels and axes)
self.margins = {
"left": 80, # Space for Y-axis labels
"right": 50, # Space on the right
"top": 60, # Space for title
"bottom": 80 # Space for X-axis labels
}
# Mouse interaction properties
self.hover_index = -1 # Which data point is being hovered over (-1 = none)
self.tooltip = None # The currently displayed tooltip
# Bind mouse events to enable interactivity
self.bind("<Motion>", self.on_mouse_move) # Mouse moves over the chart
self.bind("<Leave>", self.on_mouse_leave) # Mouse leaves the chart area
self.bind("<Configure>", lambda e: self.redraw()) # Window is resized
# Start the initial animation
self.start_animation()
def set_chart_type(self, chart_type):
"""
Changes the chart type and restarts the animation.
Args:
chart_type: The new chart type ("bar", "line", or "scatter")
"""
self.chart_type = chart_type
self.animation_progress = 0.0 # Reset animation to the beginning
self.start_animation() # Start animating the new chart type
def start_animation(self):
"""
Starts a new animation sequence.
Animations make the chart appear gradually,
which looks much nicer than instant appearance.
"""
self.animation_progress = 0.0 # Start from the beginning
self.animation_running = True # Mark animation as running
self.animate() # Start the animation loop
def animate(self):
"""
Handles one frame of animation.
This method calls itself repeatedly to create smooth animation.
"""
# If animation was stopped, don't continue
if not self.animation_running:
return
# Advance the animation progress
self.animation_progress += self.animation_increment
# Check if animation is complete
if self.animation_progress >= 1.0:
self.animation_progress = 1.0 # Cap at 100%
self.animation_running = False # Stop the animation
# Redraw the chart with the new animation progress
self.redraw()
# If animation is still running, schedule the next frame
if self.animation_running:
self.after(25, self.animate) # Call this method again in 25 milliseconds

def on_mouse_move(self, event):
"""
Called when the mouse moves over the chart.
Checks if the mouse is hovering over a data point.
Args:
event: Mouse event containing the mouse position
"""
self.check_hover(event.x, event.y)

def on_mouse_leave(self, event):
"""
Called when the mouse leaves the chart area.
Hides any tooltips and removes hover effects.
"""
self.hover_index = -1 # No data point is being hovered
self.destroy_tooltip() # Remove any visible tooltip
self.redraw() # Redraw to remove hover effects
def check_hover(self, mouse_x, mouse_y):
"""
Checks if the mouse is hovering over any data points.
If so, highlights that data point and shows a tooltip.
Args:
mouse_x: The X position of the mouse cursor
mouse_y: The Y position of the mouse cursor
"""
# Get the current size of the canvas
width = self.winfo_width()
height = self.winfo_height()
# If the canvas isn't ready yet, don't do anything
if width <= 1 or height <= 1:
return
# Calculate the chart area (excluding margins)
chart_width = width - self.margins["left"] - self.margins["right"]
chart_height = height - self.margins["top"] - self.margins["bottom"]
bar_width = chart_width / len(self.data) # Width of each bar/column
# Remember what was previously being hovered
old_hover = self.hover_index
self.hover_index = -1 # Assume nothing is being hovered initially
# Check each data point to see if the mouse is over it
for i, point in enumerate(self.data):
# Calculate the position of this data point
x = self.margins["left"] + i * bar_width + bar_width / 2
y = self.margins["top"] + chart_height-(chart_height * point.value / 100.0)
if self.chart_type == "bar":
# For bar charts, check if mouse is inside the bar rectangle
bar_left = self.margins["left"] + i * bar_width + 10
bar_right = bar_left + bar_width - 20
bar_top = y
bar_bottom = height - self.margins["bottom"]
# If mouse is inside the bar area, this bar is being hovered
if (bar_left <= mouse_x <= bar_right and bar_top<=mouse_y<=bar_bottom):
self.hover_index = i
break
else:
# For line and scatter charts, check if mouse is near the data point
distance = math.sqrt((mouse_x - x) ** 2 + (mouse_y - y) ** 2)
if distance < 20: # Within 20 pixels
self.hover_index = i
break
# If the hover state changed, update the display
if self.hover_index != old_hover:
self.destroy_tooltip() # Remove old tooltip
if self.hover_index != -1:
# Show tooltip for the newly hovered data point
point = self.data[self.hover_index]
self.show_tooltip(mouse_x, mouse_y, point)
self.redraw() # Redraw to show hover effects






def show_tooltip(self, x, y, point):
"""
Shows a tooltip near the mouse cursor with information about a data point.
Args:
x: X position where the tooltip should appear
y: Y position where the tooltip should appear
point: The DataPoint to show information about
"""
# Remove any existing tooltip first
self.destroy_tooltip()
# Create a new tooltip
self.tooltip = ModernTooltip(self.master, point.label, point.category,
point.value, self.colors)
# Calculate where to position the tooltip
self.tooltip.update_idletasks() # Let tkinter calculate the tooltip size
tooltip_width = self.tooltip.winfo_width()
tooltip_height = self.tooltip.winfo_height()
# Convert canvas coordinates to screen coordinates
canvas_x = self.winfo_rootx() + x
canvas_y = self.winfo_rooty() + y
# Position the tooltip centered horizontally and above the mouse cursor
x = canvas_x - tooltip_width // 2
y = canvas_y - tooltip_height - 15
self.tooltip.geometry(f"+{x}+{y}")


def destroy_tooltip(self):
"""
Removes the currently displayed tooltip if there is one.
"""
if self.tooltip:
self.tooltip.destroy()
self.tooltip = None

def redraw(self):
"""
Redraws the entire chart.
This is called whenever something changes (animation, hover, resize, etc.)
"""
# Get the current canvas size
width = self.winfo_width()
height = self.winfo_height()
# If the canvas isn't ready yet, try again later
if width <= 1 or height <= 1:
self.after(50, self.redraw)
return
# Clear everything on the canvas to start fresh
self.delete("all")
# Calculate the area available for the actual chart
chart_width = width - self.margins["left"] - self.margins["right"]
chart_height = height - self.margins["top"] - self.margins["bottom"]
# Draw all the components of the chart
self.draw_background_grid(width, height, chart_width, chart_height)
self.draw_title(width)
self.draw_axes(width, height, chart_width, chart_height)
# Draw the actual data based on the current chart type
if self.chart_type == "bar":
self.draw_bar_chart(width, height, chart_width, chart_height)
elif self.chart_type == "line":
self.draw_line_chart(width, height, chart_width, chart_height)
elif self.chart_type == "scatter":
self.draw_scatter_chart(width, height, chart_width, chart_height)


def draw_background_grid(self, width, height, chart_width, chart_height):
"""
Draws horizontal grid lines to make it easier to read values.
Args:
width: Total canvas width
height: Total canvas height
chart_width: Width of the chart area
chart_height: Height of the chart area
"""
# Draw 6 horizontal lines (including top and bottom)
for i in range(6):
# Calculate Y position for this grid line
y = self.margins["top"] + (chart_height * i) / 5
# Draw a dashed line across the chart
self.create_line(
self.margins["left"], y, # Start point
width - self.margins["right"], y, # End point
fill=self.colors["border"], # Line color (gray)
width=1, # Line thickness
dash=(2, 4) # Dashed pattern (2 pixels on, 4 pixels off)
)



def draw_title(self, width):
"""
Draws the chart title at the top.
Args:
width: Total canvas width (used for centering)
"""
# Draw the main title text
self.create_text(
width / 2, 25, # Position (centered horizontally, 25 pixels from top)
text="Performance Analytics", # Title text
fill=self.colors["text_primary"], # Text color (white)
font=("Segoe UI", 18, "bold") # Font styling
)
# Draw a decorative line under the title
self.create_rectangle(
width / 2 - 60, 40, # Top-left corner
width / 2 + 60, 43, # Bottom-right corner (makes a thin rectangle)
fill=self.colors["accent_primary"], # Blue color
outline="" # No border
)


def draw_axes(self, width, height, chart_width, chart_height):
"""
Draws the X and Y axes with labels.
Args:
width: Total canvas width
height: Total canvas height
chart_width: Width of the chart area
chart_height: Height of the chart area
"""
# Draw the Y-axis (vertical line on the left)
self.create_line(
self.margins["left"], self.margins["top"], # Top of Y-axis
self.margins["left"], height - self.margins["bottom"], # Bottom of Y-axis
fill=self.colors["text_secondary"], # Gray color
width=2 # 2 pixels thick
)
# Draw the X-axis (horizontal line at the bottom)
self.create_line(
self.margins["left"], height - self.margins["bottom"], # Left end of X-axis
width - self.margins["right"], height - self.margins["bottom"], # The Right
fill=self.colors["text_secondary"], # Gray color
width=2 # 2 pixels thick
)

# Draw Y-axis labels (numbers from 0 to 100)
for i in range(6):
# Calculate position and value for this label
y = self.margins["top"] + (chart_height * i) / 5
value = 100 - i * 20 # Values: 100, 80, 60, 40, 20, 0
# Draw the number label
self.create_text(
self.margins["left"] - 15, y, # Position (15 pixels left of Y-axis)
text=str(value), # The number
fill=self.colors["text_secondary"], # Gray color
font=("Segoe UI", 10), # Font styling
anchor=tk.E # Align to the right (east)
)
# Draw X-axis labels (the data point labels like "Q1", "Q2", etc.)
bar_width = chart_width / len(self.data)
for i, point in enumerate(self.data):
# Calculate X position for this label (center of each data column)
x = self.margins["left"] + i * bar_width + bar_width / 2
# Draw the label
self.create_text(
# Position (25 pixels below X-axis)
x, height - self.margins["bottom"] + 25,
# The label text ("Q1", "Q2", etc.)
text=point.label,
fill=self.colors["text_secondary"], # Gray color
font=("Segoe UI", 12, "bold") # Font styling
)


def draw_bar_chart(self, width, height, chart_width, chart_height):
"""
Draws a bar chart with animated bars and gradient effects.
Each bar represents one data point from self.data.
"""
# Calculate how wide each bar should be
# by dividing available space by number of data points
bar_width = chart_width / len(self.data)
# Loop through each data point to draw its corresponding bar
for i, point in enumerate(self.data):
# Calculate the height of this bar based on the data value (0-100 scale)
# Multiply by animation_progress to create smooth animation effect
# (0.0 to 1.0)
bar_height = chart_height * point.value / 100.0 * self.animation_progress
# Calculate the x position (left edge) of this bar
# Add 10 pixels padding from the left edge of each bar's allocated space
x = self.margins["left"] + i * bar_width + 10
# Calculate the y position (top edge) of this bar
# Start from bottom and subtract the bar height to position from top
y = height - self.margins["bottom"] - bar_height
# Make bars slightly narrower than their allocated space
# (20 pixels total padding)
bar_display_width = bar_width - 20
# Change bar colors based on whether mouse is hovering over this bar
if i == self.hover_index:
# Bright gradient colors when mouse hovers over the bar
gradient_colors = [self.colors["accent_primary"], "#7C3AED"]
else:
# Normal gradient colors when not hovering
gradient_colors = [self.colors["accent_primary"], "#1E40AF"]
# Create gradient effect by drawing multiple horizontal segments
# Each segment has a slightly different color to simulate a smooth gradient
segments = 8 # Number of color segments to create gradient effect
for seg in range(segments):
# Calculate height of each segment
# (divide total bar height by number of segments)
seg_height = bar_height / segments
# Calculate y position of this segment (start from top of bar)
seg_y = y + seg * seg_height
# Calculate color interpolation ratio (0.0 at top to 1.0 at bottom)
ratio = seg / (segments - 1) if segments > 1 else 0
# Get the interpolated color for this segment
color = self.interpolate_color(gradient_colors[0],
gradient_colors[1], ratio)
# Draw this segment as a rectangle with the calculated color
self.create_rectangle(x, seg_y, x + bar_display_width,
seg_y + seg_height, fill=color, outline="")

# Only show value labels when animation is nearly complete (90% or more)
if self.animation_progress >= 0.9:
# Draw the numerical value above each bar
self.create_text(x + bar_display_width / 2, y - 15,
text=str(point.value),
fill=self.colors["text_primary"],
font=("Segoe UI", 11, "bold"))

def draw_line_chart(self, width, height, chart_width, chart_height):
"""
Draws a line chart connecting data points with circles at each point.
Animation reveals points one by one from left to right.
"""
# Calculate spacing between data points
bar_width = chart_width / len(self.data)
# Create a list to store the (x, y) coordinates of each data point
points = []
# Calculate the screen coordinates for each data point
for i, point in enumerate(self.data):
# X coordinate: center of each data point's allocated space
x = self.margins["left"] + i * bar_width + bar_width / 2
# Apply animation progress to the value (makes points animate upward)
value_progress = point.value * self.animation_progress
# Y coordinate: convert data value to screen position
# Higher values appear higher on screen (smaller y values)
y = height - self.margins["bottom"] - (chart_height*value_progress / 100.0)
# Add this point's coordinates to our list
points.append((x, y))
# Draw lines connecting consecutive points (if we have at least 2 points)
if len(points) >= 2:
# Connect each point to the next point with a line
for i in range(len(points) - 1):
self.create_line(points[i][0], points[i][1],
points[i+1][0], points[i+1][1],
fill=self.colors["accent_secondary"],
width=4, smooth=True)
# Determine how many points to show based on animation progress
# This creates the effect of points appearing one by one
visible_points = int(len(self.data) * self.animation_progress)
# Draw circles at each data point location
for i in range(min(visible_points + 1, len(points))):
x, y = points[i] # Get coordinates of this point
# Make the hovered point larger and different color
if i == self.hover_index:
fill_color = self.colors["accent_warning"] # Orange/yellow for hover
size = 10 # Larger circle when hovering
else:
fill_color = self.colors["accent_secondary"] # Green for normal
size = 8 # Normal circle size
# Draw the circle (oval with equal width and height)
# Circle extends 'size' pixels in each direction from center point
self.create_oval(x - size, y - size, x + size, y + size,
fill=fill_color, outline=self.colors["text_primary"], width=2)
# Show value labels when animation is nearly complete
if self.animation_progress >= 0.9:
# Display the numerical value above each point
self.create_text(x, y - 25, text=str(self.data[i].value),
fill=self.colors["text_primary"],
font=("Segoe UI", 10, "bold"))


def draw_scatter_chart(self, width, height, chart_width, chart_height):
"""
Draws a scatter plot where each point's size and color depends on its value.
Points appear one by one during animation.
"""
# Calculate horizontal spacing between data points
bar_width = chart_width / len(self.data)
# Determine how many points to show based on animation progress
# This creates staggered appearance of points during animation
visible_points = int(len(self.data) * self.animation_progress)
# Draw each visible data point as a colored circle
for i in range(min(visible_points + 1, len(self.data))):
point = self.data[i] # Get the current data point
# Calculate x position (center of allocated space for this point)
x = self.margins["left"] + i * bar_width + bar_width / 2
# Apply animation progress to value (creates upward animation effect)
value_progress = point.value * self.animation_progress
# Calculate y position based on animated value
y = height - self.margins["bottom"]-(chart_height * value_progress / 100.0)
# Make point size proportional to its value
# Larger values get bigger circles (8 base size + value/15)
point_size = 8 + (point.value / 15)
# Determine point color based on hover state and value
if i == self.hover_index:
# Cycle through different colors when hovering
colors_map = [self.colors["accent_warning"],
self.colors["accent_primary"],
self.colors["accent_secondary"]]
# Use modulo to cycle colors
fill_color = colors_map[i % len(colors_map)]
else:
# Color-code points based on their values when not hovering
if point.value < 40:
# Red for low values
fill_color = self.colors["accent_danger"]
elif point.value < 70:
# Blue for medium values
fill_color = self.colors["accent_primary"]
else:
# Green for high values
fill_color = self.colors["accent_secondary"]
# Draw the scatter point as a circle
# Circle extends 'point_size' pixels in each direction from center
self.create_oval(x - point_size, y - point_size,
x + point_size, y + point_size,
fill=fill_color, outline=self.colors["text_primary"], width=2)
# Show value labels when animation is nearly complete
if self.animation_progress >= 0.9:
# Display numerical value above each point
# Position text above the circle
# (subtract point_size and extra padding)
self.create_text(x, y - point_size - 20, text=str(point.value),
fill=self.colors["text_primary"],
font=("Segoe UI", 10, "bold"))


def interpolate_color(self, color1, color2, ratio):
"""
Creates a smooth transition between two colors.
Args:
color1 (str): Starting color in hex format (e.g., "#FF0000" for red)
color2 (str): Ending color in hex format (e.g., "#00FF00" for green)
ratio (float): Blend ratio from 0.0 to 1.0
0.0 = 100% color1, 1.0 = 100% color2, 0.5 = 50/50 mix
Returns:
str: Blended color in hex format
Example:
interpolate_color("#FF0000", "#00FF00", 0.5) returns a yellow-ish color
"""
# Extract RGB values from first color (remove # and convert hex to decimal)
# color1[1:3] gets characters 1-2 (red component), etc.
r1 = int(color1[1:3], 16) # Red component of color1 (0-255)
g1 = int(color1[3:5], 16) # Green component of color1 (0-255)
b1 = int(color1[5:7], 16) # Blue component of color1 (0-255)
# Extract RGB values from second color
r2 = int(color2[1:3], 16) # Red component of color2 (0-255)
g2 = int(color2[3:5], 16) # Green component of color2 (0-255)
b2 = int(color2[5:7], 16) # Blue component of color2 (0-255)
# Calculate the blended RGB values using linear interpolation
# Formula: start_value + (end_value - start_value) * ratio
r = int(r1 + (r2 - r1) * ratio) # Blended red value
g = int(g1 + (g2 - g1) * ratio) # Blended green value
b = int(b1 + (b2 - b1) * ratio) # Blended blue value
# Convert the blended RGB values back to hex format
# {:02x} formats each number as 2-digit hexadecimal
# (with leading zero if needed)
return f"#{r:02x}{g:02x}{b:02x}"


if __name__ == "__main__":
app = InteractiveChart()
app.mainloop()



The Final Result:

Python Tkinter Interactive bar Chart

Python Tkinter Interactive Line Chart

Python Tkinter Interactive Scatter Chart



if you want the source code click on the download button below