How to Make Interactive Charts with Hover Effects Using Python Tkinter
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.
- 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:
Download Projects Source Code