Integrasi Matplotlib ke dalam Tkinter
Bagian 1: Persiapan dan Konsep Dasar
Pada bagian ini, kita akan mempersiapkan lingkungan kerja untuk mengintegrasikan visualisasi data Matplotlib ke dalam aplikasi GUI Tkinter. Kita akan memahami konsep dasar embedding plot dan membangun aplikasi visualisasi data yang interaktif.
1.1 Konsep Embedding Matplotlib dalam Tkinter
Embedding adalah proses memasukkan plot Matplotlib langsung ke dalam widget Tkinter, bukan menampilkannya dalam jendela terpisah. Ini memungkinkan kita membuat aplikasi GUI yang terintegrasi dengan visualisasi data.
Ada beberapa komponen kunci dalam embedding:
- FigureCanvasTkAgg: Widget yang menampilkan matplotlib figure dalam Tkinter
- NavigationToolbar2Tk: Toolbar standar untuk interaksi dengan plot (zoom, pan, save)
- Figure dan Axes: Objek matplotlib untuk menggambar plot
Praktikum 1.1: Aplikasi Plot Sederhana
Buat file baru data_visualizer.py dan mulai dengan struktur dasar:
import tkinter as tk
from tkinter import ttk, messagebox
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import numpy as np
# Membuat jendela utama
root = tk.Tk()
root.title("Data Visualizer")
root.geometry("800x600")
root.configure(bg="lightgray")
# Membuat Figure matplotlib
fig = Figure(figsize=(8, 6), dpi=100)
ax = fig.add_subplot(111)
# Plot data sederhana
x = np.linspace(0, 10, 100)
y = np.sin(x)
ax.plot(x, y)
ax.set_title("Grafik Sinus")
ax.set_xlabel("X")
ax.set_ylabel("Y")
# Embed plot ke dalam Tkinter
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
root.mainloop()
Tugas: Jalankan aplikasi dan amati plot sinus yang tertanam dalam jendela Tkinter.
Praktikum 1.2: Menambahkan Toolbar Navigasi
Tambahkan toolbar setelah membuat canvas:
# Membuat toolbar navigasi
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
Tugas: Test toolbar - coba fitur zoom, pan, dan save yang tersedia.
1.2 Struktur Aplikasi Visualisasi Data
Sekarang kita akan membangun aplikasi yang lebih terstruktur dengan interface kontrol untuk mengubah visualisasi secara real-time.
Praktikum 1.3: Membuat Layout dengan Frame
Hapus kode sebelumnya dan ganti dengan struktur yang lebih terorganisir:
import tkinter as tk
from tkinter import ttk, messagebox
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import numpy as np
class DataVisualizer:
def __init__(self, root):
self.root = root
self.root.title("Data Visualizer - Aplikasi Visualisasi Data")
self.root.geometry("1000x700")
self.root.configure(bg="white")
self.setup_ui()
self.setup_plot()
def setup_ui(self):
# Frame utama
main_frame = tk.Frame(self.root, bg="white")
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Frame untuk kontrol (kiri)
self.control_frame = tk.Frame(main_frame, bg="lightblue", width=250)
self.control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
self.control_frame.pack_propagate(False) # Maintain fixed width
# Frame untuk plot (kanan)
self.plot_frame = tk.Frame(main_frame, bg="white")
self.plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# Judul di control frame
title_label = tk.Label(
self.control_frame,
text="KONTROL VISUALISASI",
font=("Arial", 14, "bold"),
bg="lightblue"
)
title_label.pack(pady=20)
def setup_plot(self):
# Membuat Figure matplotlib
self.fig = Figure(figsize=(8, 6), dpi=100)
self.ax = self.fig.add_subplot(111)
# Plot awal
self.update_plot()
# Embed plot ke dalam Tkinter
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# Toolbar navigasi
self.toolbar = NavigationToolbar2Tk(self.canvas, self.plot_frame)
self.toolbar.update()
def update_plot(self):
# Clear plot sebelumnya
self.ax.clear()
# Plot data baru
x = np.linspace(0, 10, 100)
y = np.sin(x)
self.ax.plot(x, y, 'b-', linewidth=2)
self.ax.set_title("Grafik Sinus", fontsize=16)
self.ax.set_xlabel("X", fontsize=12)
self.ax.set_ylabel("Y", fontsize=12)
self.ax.grid(True, alpha=0.3)
# Refresh canvas
self.canvas.draw()
# Membuat dan menjalankan aplikasi
if __name__ == "__main__":
root = tk.Tk()
app = DataVisualizer(root)
root.mainloop()
Tugas: Jalankan aplikasi dan perhatikan layout dengan panel kontrol di kiri dan plot di kanan.
Bagian 2: Kontrol Interaktif dan Real-time Updates
Pada bagian ini, kita akan menambahkan kontrol interaktif yang memungkinkan pengguna mengubah visualisasi secara real-time. Kita akan belajar cara menghubungkan widget Tkinter dengan update plot matplotlib.
2.1 Kontrol Parameter dengan Scale Widget
Scale widget memungkinkan pengguna memilih nilai dalam rentang tertentu menggunakan slider. Ini sangat berguna untuk mengontrol parameter numerik seperti frekuensi, amplitudo, atau phase dalam fungsi matematika.
Praktikum 2.1: Menambahkan Kontrol Frekuensi
Tambahkan method berikut dalam class DataVisualizer setelah setup_plot:
def setup_controls(self):
# Variabel kontrol
self.frequency = tk.DoubleVar(value=1.0)
self.amplitude = tk.DoubleVar(value=1.0)
self.phase = tk.DoubleVar(value=0.0)
# Kontrol frekuensi
freq_label = tk.Label(
self.control_frame,
text="Frekuensi:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
freq_label.pack(pady=(20, 5))
freq_scale = tk.Scale(
self.control_frame,
from_=0.1,
to=5.0,
resolution=0.1,
orient=tk.HORIZONTAL,
variable=self.frequency,
command=self.on_parameter_change,
length=200,
bg="lightblue"
)
freq_scale.pack(pady=5)
# Label untuk menampilkan nilai
self.freq_value_label = tk.Label(
self.control_frame,
text=f"Nilai: {self.frequency.get()}",
font=("Arial", 10),
bg="lightblue"
)
self.freq_value_label.pack()
def on_parameter_change(self, value=None):
# Update label nilai
self.freq_value_label.config(text=f"Nilai: {self.frequency.get()}")
# Update plot
self.update_plot()
Kemudian panggil method ini di __init__ setelah self.setup_plot():
Dan update method update_plot():
def update_plot(self):
# Clear plot sebelumnya
self.ax.clear()
# Plot data dengan parameter yang bisa diubah
x = np.linspace(0, 10, 100)
freq = self.frequency.get()
y = np.sin(freq * x)
self.ax.plot(x, y, 'b-', linewidth=2)
self.ax.set_title(f"Grafik Sinus (Frekuensi: {freq})", fontsize=16)
self.ax.set_xlabel("X", fontsize=12)
self.ax.set_ylabel("Y", fontsize=12)
self.ax.grid(True, alpha=0.3)
# Refresh canvas
self.canvas.draw()
Tugas: Test aplikasi dan geser slider frekuensi. Amati bagaimana gelombang sinus berubah secara real-time.
Praktikum 2.2: Menambahkan Kontrol Amplitudo dan Phase
Tambahkan kontrol lainnya dalam method setup_controls setelah freq_value_label:
# Kontrol amplitudo
amp_label = tk.Label(
self.control_frame,
text="Amplitudo:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
amp_label.pack(pady=(20, 5))
amp_scale = tk.Scale(
self.control_frame,
from_=0.1,
to=3.0,
resolution=0.1,
orient=tk.HORIZONTAL,
variable=self.amplitude,
command=self.on_parameter_change,
length=200,
bg="lightblue"
)
amp_scale.pack(pady=5)
self.amp_value_label = tk.Label(
self.control_frame,
text=f"Nilai: {self.amplitude.get()}",
font=("Arial", 10),
bg="lightblue"
)
self.amp_value_label.pack()
# Kontrol phase
phase_label = tk.Label(
self.control_frame,
text="Phase:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
phase_label.pack(pady=(20, 5))
phase_scale = tk.Scale(
self.control_frame,
from_=0.0,
to=6.28, # 2π
resolution=0.1,
orient=tk.HORIZONTAL,
variable=self.phase,
command=self.on_parameter_change,
length=200,
bg="lightblue"
)
phase_scale.pack(pady=5)
self.phase_value_label = tk.Label(
self.control_frame,
text=f"Nilai: {self.phase.get()}",
font=("Arial", 10),
bg="lightblue"
)
self.phase_value_label.pack()
Update method on_parameter_change:
def on_parameter_change(self, value=None):
# Update semua label nilai
self.freq_value_label.config(text=f"Nilai: {self.frequency.get()}")
self.amp_value_label.config(text=f"Nilai: {self.amplitude.get()}")
self.phase_value_label.config(text=f"Nilai: {round(self.phase.get(), 2)}")
# Update plot
self.update_plot()
Update method update_plot() untuk menggunakan semua parameter:
def update_plot(self):
# Clear plot sebelumnya
self.ax.clear()
# Plot data dengan parameter yang bisa diubah
x = np.linspace(0, 10, 100)
freq = self.frequency.get()
amp = self.amplitude.get()
phase = self.phase.get()
y = amp * np.sin(freq * x + phase)
self.ax.plot(x, y, 'b-', linewidth=2)
self.ax.set_title(f"y = {amp} × sin({freq}x + {round(phase, 2)})", fontsize=16)
self.ax.set_xlabel("X", fontsize=12)
self.ax.set_ylabel("Y", fontsize=12)
self.ax.grid(True, alpha=0.3)
self.ax.set_ylim(-3.5, 3.5) # Fixed y-axis untuk konsistensi
# Refresh canvas
self.canvas.draw()
Tugas: Test semua kontrol dan amati bagaimana ketiga parameter mengubah gelombang sinus.
2.2 Kontrol Jenis Fungsi dengan Combobox
Combobox memungkinkan pengguna memilih dari daftar opsi yang telah ditentukan. Kita akan menggunakannya untuk memilih jenis fungsi matematika yang akan divisualisasikan.
Praktikum 2.3: Menambahkan Pemilihan Fungsi
Tambahkan di akhir method setup_controls:
# Pemilihan jenis fungsi
func_label = tk.Label(
self.control_frame,
text="Jenis Fungsi:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
func_label.pack(pady=(30, 5))
self.function_type = tk.StringVar(value="sin")
function_combo = ttk.Combobox(
self.control_frame,
textvariable=self.function_type,
values=["sin", "cos", "tan", "exp", "log"],
state="readonly",
width=18
)
function_combo.pack(pady=5)
function_combo.bind("<<ComboboxSelected>>", self.on_function_change)
def on_function_change(self, event=None):
self.update_plot()
Update method update_plot() untuk mendukung berbagai fungsi:
def update_plot(self):
# Clear plot sebelumnya
self.ax.clear()
# Parameter
x = np.linspace(0, 10, 100)
freq = self.frequency.get()
amp = self.amplitude.get()
phase = self.phase.get()
func_type = self.function_type.get()
# Hitung y berdasarkan jenis fungsi
try:
if func_type == "sin":
y = amp * np.sin(freq * x + phase)
title = f"y = {amp} × sin({freq}x + {round(phase, 2)})"
elif func_type == "cos":
y = amp * np.cos(freq * x + phase)
title = f"y = {amp} × cos({freq}x + {round(phase, 2)})"
elif func_type == "tan":
y = amp * np.tan(freq * x + phase)
# Limit y untuk tan agar tidak terlalu ekstrem
y = np.clip(y, -10, 10)
title = f"y = {amp} × tan({freq}x + {round(phase, 2)})"
elif func_type == "exp":
y = amp * np.exp(freq * (x - 5) + phase)
y = np.clip(y, 0, 100) # Limit untuk exp
title = f"y = {amp} × exp({freq}(x-5) + {round(phase, 2)})"
elif func_type == "log":
y = amp * np.log(freq * x + 1) + phase
title = f"y = {amp} × log({freq}x + 1) + {round(phase, 2)}"
except:
# Fallback ke sin jika ada error
y = amp * np.sin(freq * x + phase)
title = f"y = {amp} × sin({freq}x + {round(phase, 2)})"
self.ax.plot(x, y, 'b-', linewidth=2)
self.ax.set_title(title, fontsize=14)
self.ax.set_xlabel("X", fontsize=12)
self.ax.set_ylabel("Y", fontsize=12)
self.ax.grid(True, alpha=0.3)
# Refresh canvas
self.canvas.draw()
Tugas: Test pemilihan berbagai fungsi dan amati perubahannya. Coba kombinasi parameter untuk setiap fungsi.
2.3 Kontrol Warna dan Style
Kita akan menambahkan kontrol untuk mengubah warna garis dan style plot secara interaktif.
Praktikum 2.4: Menambahkan Kontrol Warna
Tambahkan di akhir method setup_controls:
# Pemilihan warna
color_label = tk.Label(
self.control_frame,
text="Warna Garis:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
color_label.pack(pady=(20, 5))
self.line_color = tk.StringVar(value="blue")
color_combo = ttk.Combobox(
self.control_frame,
textvariable=self.line_color,
values=["blue", "red", "green", "orange", "purple", "brown", "pink"],
state="readonly",
width=18
)
color_combo.pack(pady=5)
color_combo.bind("<<ComboboxSelected>>", self.on_style_change)
# Pemilihan line style
style_label = tk.Label(
self.control_frame,
text="Style Garis:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
style_label.pack(pady=(15, 5))
self.line_style = tk.StringVar(value="-")
style_combo = ttk.Combobox(
self.control_frame,
textvariable=self.line_style,
values=["-", "--", "-.", ":", "o-", "s-", "^-"],
state="readonly",
width=18
)
style_combo.pack(pady=5)
style_combo.bind("<<ComboboxSelected>>", self.on_style_change)
# Line width
width_label = tk.Label(
self.control_frame,
text="Ketebalan Garis:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
width_label.pack(pady=(15, 5))
self.line_width = tk.DoubleVar(value=2.0)
width_scale = tk.Scale(
self.control_frame,
from_=0.5,
to=5.0,
resolution=0.5,
orient=tk.HORIZONTAL,
variable=self.line_width,
command=self.on_style_change,
length=200,
bg="lightblue"
)
width_scale.pack(pady=5)
def on_style_change(self, value=None):
self.update_plot()
Update method update_plot() untuk menggunakan style yang dipilih:
def update_plot(self):
# Clear plot sebelumnya
self.ax.clear()
# Parameter
x = np.linspace(0, 10, 100)
freq = self.frequency.get()
amp = self.amplitude.get()
phase = self.phase.get()
func_type = self.function_type.get()
color = self.line_color.get()
style = self.line_style.get()
width = self.line_width.get()
# Hitung y berdasarkan jenis fungsi (sama seperti sebelumnya)
try:
if func_type == "sin":
y = amp * np.sin(freq * x + phase)
title = f"y = {amp} × sin({freq}x + {round(phase, 2)})"
elif func_type == "cos":
y = amp * np.cos(freq * x + phase)
title = f"y = {amp} × cos({freq}x + {round(phase, 2)})"
elif func_type == "tan":
y = amp * np.tan(freq * x + phase)
y = np.clip(y, -10, 10)
title = f"y = {amp} × tan({freq}x + {round(phase, 2)})"
elif func_type == "exp":
y = amp * np.exp(freq * (x - 5) + phase)
y = np.clip(y, 0, 100)
title = f"y = {amp} × exp({freq}(x-5) + {round(phase, 2)})"
elif func_type == "log":
y = amp * np.log(freq * x + 1) + phase
title = f"y = {amp} × log({freq}x + 1) + {round(phase, 2)}"
except:
y = amp * np.sin(freq * x + phase)
title = f"y = {amp} × sin({freq}x + {round(phase, 2)})"
# Plot dengan style yang dipilih
self.ax.plot(x, y, linestyle=style, color=color, linewidth=width)
self.ax.set_title(title, fontsize=14)
self.ax.set_xlabel("X", fontsize=12)
self.ax.set_ylabel("Y", fontsize=12)
self.ax.grid(True, alpha=0.3)
# Refresh canvas
self.canvas.draw()
Tugas: Eksplorasi kombinasi warna, style, dan ketebalan garis yang berbeda untuk setiap fungsi.
Bagian 3: Visualisasi Data Real dan Multiple Plots
Pada bagian ini, kita akan mengembangkan kemampuan aplikasi untuk memvisualisasikan data yang lebih realistis dan menampilkan multiple plots dalam satu canvas.
3.1 Input Data dari File
Kemampuan membaca data dari file eksternal adalah fitur penting dalam aplikasi visualisasi data. Kita akan implementasikan pembacaan file CSV dan visualisasinya.
Praktikum 3.1: Menambahkan Fungsi Baca File
Tambahkan import yang diperlukan di bagian atas file:
import pandas as pd
from tkinter import filedialog
import matplotlib.dates as mdates
from datetime import datetime, timedelta
Tambahkan method baru dalam class DataVisualizer:
def setup_data_controls(self):
# Separator
separator = tk.Frame(self.control_frame, height=2, bg="darkblue")
separator.pack(fill=tk.X, pady=20)
# Label untuk data section
data_label = tk.Label(
self.control_frame,
text="VISUALISASI DATA",
font=("Arial", 14, "bold"),
bg="lightblue"
)
data_label.pack(pady=10)
# Button untuk load data
load_btn = tk.Button(
self.control_frame,
text="Load Data CSV",
font=("Arial", 11, "bold"),
command=self.load_data,
bg="orange",
fg="white",
width=20
)
load_btn.pack(pady=5)
# Button untuk generate sample data
sample_btn = tk.Button(
self.control_frame,
text="Generate Sample Data",
font=("Arial", 11),
command=self.generate_sample_data,
bg="green",
fg="white",
width=20
)
sample_btn.pack(pady=5)
# Info label untuk status data
self.data_info_label = tk.Label(
self.control_frame,
text="Belum ada data dimuat",
font=("Arial", 9),
bg="lightblue",
wraplength=200
)
self.data_info_label.pack(pady=10)
# Variabel untuk menyimpan data
self.current_data = None
def load_data(self):
file_path = filedialog.askopenfilename(
title="Pilih file CSV",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
)
if file_path:
try:
# Baca CSV
self.current_data = pd.read_csv(file_path)
# Update info label
rows, cols = self.current_data.shape
self.data_info_label.config(
text=f"Data dimuat:\n{rows} baris, {cols} kolom\nFile: {file_path.split('/')[-1]}"
)
# Plot data
self.plot_data()
except Exception as e:
messagebox.showerror("Error", f"Gagal membaca file:\n{str(e)}")
def generate_sample_data(self):
# Generate data sample yang menarik
dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D')
# Simulasi data penjualan dengan tren dan noise
trend = np.linspace(100, 200, len(dates))
seasonal = 30 * np.sin(2 * np.pi * np.arange(len(dates)) / 365.25)
noise = np.random.normal(0, 15, len(dates))
sales = trend + seasonal + noise
# Simulasi data temperature
temp_base = 25 + 10 * np.sin(2 * np.pi * np.arange(len(dates)) / 365.25)
temp_noise = np.random.normal(0, 5, len(dates))
temperature = temp_base + temp_noise
# Buat DataFrame
self.current_data = pd.DataFrame({
'Date': dates,
'Sales': sales,
'Temperature': temperature,
'Category_A': sales * 0.6 + np.random.normal(0, 10, len(dates)),
'Category_B': sales * 0.4 + np.random.normal(0, 8, len(dates))
})
# Update info label
rows, cols = self.current_data.shape
self.data_info_label.config(
text=f"Sample data dibuat:\n{rows} baris, {cols} kolom\nData penjualan tahunan"
)
# Plot data
self.plot_data()
def plot_data(self):
if self.current_data is None:
return
# Clear plot
self.ax.clear()
# Deteksi jenis data dan plot accordingly
numeric_columns = self.current_data.select_dtypes(include=[np.number]).columns
date_columns = self.current_data.select_dtypes(include=['datetime64']).columns
if len(date_columns) > 0 and len(numeric_columns) > 0:
# Time series plot
date_col = date_columns[0]
# Plot multiple numeric columns
colors = ['blue', 'red', 'green', 'orange', 'purple']
for i, col in enumerate(numeric_columns[:5]): # Limit to 5 columns
self.ax.plot(
self.current_data[date_col],
self.current_data[col],
color=colors[i % len(colors)],
label=col,
linewidth=2,
marker='o' if len(self.current_data) < 50 else None,
markersize=3
)
self.ax.set_xlabel('Tanggal')
self.ax.set_ylabel('Nilai')
self.ax.set_title('Time Series Data')
self.ax.legend()
# Format tanggal pada x-axis
self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
self.ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
plt.setp(self.ax.xaxis.get_majorticklabels(), rotation=45)
elif len(numeric_columns) >= 2:
# Scatter plot untuk 2 kolom numerik pertama
x_col, y_col = numeric_columns[0], numeric_columns[1]
self.ax.scatter(
self.current_data[x_col],
self.current_data[y_col],
alpha=0.6,
s=50
)
self.ax.set_xlabel(x_col)
self.ax.set_ylabel(y_col)
self.ax.set_title(f'Scatter Plot: {x_col} vs {y_col}')
else:
# Bar plot untuk data kategorikal
if len(numeric_columns) > 0:
col = numeric_columns[0]
data_sample = self.current_data[col].head(20) # Limit to first 20 rows
self.ax.bar(range(len(data_sample)), data_sample)
self.ax.set_xlabel('Index')
self.ax.set_ylabel(col)
self.ax.set_title(f'Bar Plot: {col}')
self.ax.grid(True, alpha=0.3)
self.fig.tight_layout()
self.canvas.draw()
Panggil method ini di __init__ setelah self.setup_controls():
Tugas: Test fungsi "Generate Sample Data" dan amati visualisasi time series yang dihasilkan. Coba juga load file CSV jika tersedia.
Praktikum 3.2: Kontrol Jenis Plot untuk Data
Tambahkan kontrol untuk memilih jenis visualisasi dalam method setup_data_controls:
# Pemilihan jenis plot untuk data
plot_type_label = tk.Label(
self.control_frame,
text="Jenis Plot:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
plot_type_label.pack(pady=(15, 5))
self.plot_type = tk.StringVar(value="line")
plot_type_combo = ttk.Combobox(
self.control_frame,
textvariable=self.plot_type,
values=["line", "scatter", "bar", "histogram", "box"],
state="readonly",
width=18
)
plot_type_combo.pack(pady=5)
plot_type_combo.bind("<<ComboboxSelected>>", self.on_plot_type_change)
def on_plot_type_change(self, event=None):
if self.current_data is not None:
self.plot_data_advanced()
else:
messagebox.showwarning("Peringatan", "Muat data terlebih dahulu!")
def plot_data_advanced(self):
if self.current_data is None:
return
# Clear plot
self.ax.clear()
numeric_columns = self.current_data.select_dtypes(include=[np.number]).columns
date_columns = self.current_data.select_dtypes(include=['datetime64']).columns
plot_type = self.plot_type.get()
if plot_type == "line":
# Line plot
if len(date_columns) > 0 and len(numeric_columns) > 0:
date_col = date_columns[0]
colors = ['blue', 'red', 'green', 'orange', 'purple']
for i, col in enumerate(numeric_columns[:3]):
self.ax.plot(
self.current_data[date_col],
self.current_data[col],
color=colors[i % len(colors)],
label=col,
linewidth=2
)
self.ax.set_xlabel('Tanggal')
self.ax.legend()
elif len(numeric_columns) >= 1:
col = numeric_columns[0]
self.ax.plot(self.current_data[col], linewidth=2)
self.ax.set_xlabel('Index')
self.ax.set_ylabel(col)
elif plot_type == "scatter":
if len(numeric_columns) >= 2:
x_col, y_col = numeric_columns[0], numeric_columns[1]
self.ax.scatter(
self.current_data[x_col],
self.current_data[y_col],
alpha=0.6,
s=50,
c='blue'
)
self.ax.set_xlabel(x_col)
self.ax.set_ylabel(y_col)
elif plot_type == "bar":
if len(numeric_columns) >= 1:
col = numeric_columns[0]
data_sample = self.current_data[col].head(20)
self.ax.bar(range(len(data_sample)), data_sample, color='skyblue')
self.ax.set_xlabel('Index')
self.ax.set_ylabel(col)
elif plot_type == "histogram":
if len(numeric_columns) >= 1:
col = numeric_columns[0]
self.ax.hist(
self.current_data[col].dropna(),
bins=30,
alpha=0.7,
color='lightgreen',
edgecolor='black'
)
self.ax.set_xlabel(col)
self.ax.set_ylabel('Frekuensi')
elif plot_type == "box":
if len(numeric_columns) >= 1:
data_to_plot = [self.current_data[col].dropna() for col in numeric_columns[:5]]
labels = numeric_columns[:5].tolist()
self.ax.boxplot(data_to_plot, labels=labels)
self.ax.set_ylabel('Nilai')
plt.setp(self.ax.xaxis.get_majorticklabels(), rotation=45)
self.ax.set_title(f'{plot_type.title()} Plot')
self.ax.grid(True, alpha=0.3)
self.fig.tight_layout()
self.canvas.draw()
Update method plot_data() untuk menggunakan plot_data_advanced():
Tugas: Generate sample data kemudian test berbagai jenis plot (line, scatter, bar, histogram, box) dan amati perbedaannya.
3.2 Subplot dan Multiple Plots
Subplot memungkinkan kita menampilkan multiple plot dalam satu figure. Ini berguna untuk membandingkan berbagai aspek data secara bersamaan.
Praktikum 3.3: Implementasi Subplot
Tambahkan kontrol untuk subplot dalam method setup_data_controls:
# Checkbox untuk multiple subplot
self.use_subplot = tk.BooleanVar(value=False)
subplot_check = tk.Checkbutton(
self.control_frame,
text="Gunakan Multiple Subplot",
variable=self.use_subplot,
command=self.toggle_subplot,
font=("Arial", 10),
bg="lightblue"
)
subplot_check.pack(pady=10)
def toggle_subplot(self):
if self.current_data is not None:
self.setup_subplot() if self.use_subplot.get() else self.setup_single_plot()
else:
messagebox.showwarning("Peringatan", "Muat data terlebih dahulu!")
self.use_subplot.set(False)
def setup_single_plot(self):
# Reset ke single plot
self.fig.clear()
self.ax = self.fig.add_subplot(111)
self.plot_data_advanced()
def setup_subplot(self):
if self.current_data is None:
return
# Clear figure
self.fig.clear()
numeric_columns = self.current_data.select_dtypes(include=[np.number]).columns
n_cols = len(numeric_columns)
if n_cols < 2:
messagebox.showinfo("Info", "Minimal 2 kolom numerik diperlukan untuk subplot")
self.use_subplot.set(False)
self.setup_single_plot()
return
# Tentukan layout subplot
if n_cols == 2:
rows, cols = 1, 2
elif n_cols == 3:
rows, cols = 2, 2
elif n_cols == 4:
rows, cols = 2, 2
else:
rows, cols = 2, 3
# Limit maksimal 6 subplot
n_plots = min(n_cols, 6)
# Buat subplot
for i in range(n_plots):
ax = self.fig.add_subplot(rows, cols, i+1)
col = numeric_columns[i]
# Plot berbagai jenis untuk setiap subplot
if i % 4 == 0: # Line plot
ax.plot(self.current_data[col], color='blue', linewidth=1.5)
ax.set_title(f'Line: {col}')
elif i % 4 == 1: # Histogram
ax.hist(self.current_data[col].dropna(), bins=20, alpha=0.7, color='green')
ax.set_title(f'Histogram: {col}')
elif i % 4 == 2: # Box plot
ax.boxplot(self.current_data[col].dropna())
ax.set_title(f'Box: {col}')
else: # Scatter dengan kolom pertama
if len(numeric_columns) > 1:
ax.scatter(self.current_data[numeric_columns[0]],
self.current_data[col], alpha=0.6)
ax.set_title(f'Scatter: {col}')
ax.grid(True, alpha=0.3)
self.fig.tight_layout()
self.canvas.draw()
Tugas: Generate sample data, centang "Gunakan Multiple Subplot" dan amati tampilan multiple plot yang berbeda untuk setiap kolom data.
3.3 Real-time Data Updates
Kita akan menambahkan kemampuan untuk mensimulasikan data real-time yang terus diperbarui.
Praktikum 3.4: Simulasi Data Real-time
Tambahkan import untuk threading:
Tambahkan kontrol real-time dalam method setup_data_controls:
# Real-time data simulation
realtime_label = tk.Label(
self.control_frame,
text="Real-time Simulation:",
font=("Arial", 12, "bold"),
bg="lightblue"
)
realtime_label.pack(pady=(20, 5))
self.realtime_active = tk.BooleanVar(value=False)
self.realtime_btn = tk.Button(
self.control_frame,
text="Start Real-time",
font=("Arial", 11),
command=self.toggle_realtime,
bg="red",
fg="white",
width=20
)
self.realtime_btn.pack(pady=5)
# Kecepatan update
speed_label = tk.Label(
self.control_frame,
text="Update Speed (ms):",
font=("Arial", 10),
bg="lightblue"
)
speed_label.pack(pady=(10, 2))
self.update_speed = tk.IntVar(value=500)
speed_scale = tk.Scale(
self.control_frame,
from_=100,
to=2000,
resolution=100,
orient=tk.HORIZONTAL,
variable=self.update_speed,
length=180,
bg="lightblue"
)
speed_scale.pack(pady=5)
# Initialize real-time data
self.realtime_data = []
self.max_points = 50
def toggle_realtime(self):
if not self.realtime_active.get():
# Start real-time
self.realtime_active.set(True)
self.realtime_btn.config(text="Stop Real-time", bg="green")
self.start_realtime_thread()
else:
# Stop real-time
self.realtime_active.set(False)
self.realtime_btn.config(text="Start Real-time", bg="red")
def start_realtime_thread(self):
def update_loop():
while self.realtime_active.get():
# Generate new data point
timestamp = len(self.realtime_data)
value1 = 50 + 20 * np.sin(timestamp * 0.1) + np.random.normal(0, 5)
value2 = 30 + 15 * np.cos(timestamp * 0.15) + np.random.normal(0, 3)
value3 = 40 + 10 * np.sin(timestamp * 0.08) + np.random.normal(0, 4)
self.realtime_data.append({
'timestamp': timestamp,
'sensor1': value1,
'sensor2': value2,
'sensor3': value3
})
# Keep only last max_points
if len(self.realtime_data) > self.max_points:
self.realtime_data.pop(0)
# Update plot in main thread
self.root.after(0, self.update_realtime_plot)
# Sleep based on update speed
time.sleep(self.update_speed.get() / 1000.0)
# Start thread
thread = threading.Thread(target=update_loop, daemon=True)
thread.start()
def update_realtime_plot(self):
if not self.realtime_data:
return
# Convert to arrays for plotting
timestamps = [d['timestamp'] for d in self.realtime_data]
sensor1 = [d['sensor1'] for d in self.realtime_data]
sensor2 = [d['sensor2'] for d in self.realtime_data]
sensor3 = [d['sensor3'] for d in self.realtime_data]
# Clear and plot
if hasattr(self, 'ax'):
self.ax.clear()
self.ax.plot(timestamps, sensor1, 'b-', label='Sensor 1', linewidth=2)
self.ax.plot(timestamps, sensor2, 'r-', label='Sensor 2', linewidth=2)
self.ax.plot(timestamps, sensor3, 'g-', label='Sensor 3', linewidth=2)
self.ax.set_title('Real-time Sensor Data')
self.ax.set_xlabel('Time')
self.ax.set_ylabel('Value')
self.ax.legend()
self.ax.grid(True, alpha=0.3)
# Set axis limits for smooth scrolling effect
if len(timestamps) > 10:
self.ax.set_xlim(timestamps[-self.max_points], timestamps[-1])
self.canvas.draw()
Tugas: Test simulasi real-time dengan mengklik "Start Real-time". Coba ubah kecepatan update dan amati bagaimana data baru terus ditambahkan secara real-time.
Bagian 4: Features Lanjutan dan Interaktivitas
Pada bagian ini, kita akan menambahkan fitur-fitur lanjutan seperti export plot, zoom interaktif, dan annotation untuk membuat aplikasi visualisasi yang lebih professional.
4.1 Export dan Save Functionality
Kemampuan menyimpan plot dalam berbagai format adalah fitur penting dalam aplikasi visualisasi data profesional.
Praktikum 4.1: Implementasi Save Plot
Tambahkan method untuk save plot:
def setup_export_controls(self):
# Separator
export_separator = tk.Frame(self.control_frame, height=2, bg="darkblue")
export_separator.pack(fill=tk.X, pady=20)
# Export section label
export_label = tk.Label(
self.control_frame,
text="EXPORT & SAVE",
font=("Arial", 14, "bold"),
bg="lightblue"
)
export_label.pack(pady=10)
# Save plot button
save_btn = tk.Button(
self.control_frame,
text="Save Plot",
font=("Arial", 11, "bold"),
command=self.save_plot,
bg="purple",
fg="white",
width=20
)
save_btn.pack(pady=5)
# Export format
format_label = tk.Label(
self.control_frame,
text="Format:",
font=("Arial", 10),
bg="lightblue"
)
format_label.pack(pady=(10, 2))
self.export_format = tk.StringVar(value="png")
format_combo = ttk.Combobox(
self.control_frame,
textvariable=self.export_format,
values=["png", "jpg", "pdf", "svg", "eps"],
state="readonly",
width=18
)
format_combo.pack(pady=5)
# DPI setting
dpi_label = tk.Label(
self.control_frame,
text="Resolusi (DPI):",
font=("Arial", 10),
bg="lightblue"
)
dpi_label.pack(pady=(10, 2))
self.dpi_setting = tk.IntVar(value=300)
dpi_scale = tk.Scale(
self.control_frame,
from_=100,
to=600,
resolution=50,
orient=tk.HORIZONTAL,
variable=self.dpi_setting,
length=180,
bg="lightblue"
)
dpi_scale.pack(pady=5)
def save_plot(self):
format_ext = self.export_format.get()
dpi = self.dpi_setting.get()
# File dialog untuk save
file_path = filedialog.asksaveasfilename(
title="Simpan Plot",
defaultextension=f".{format_ext}",
filetypes=[
(f"{format_ext.upper()} files", f"*.{format_ext}"),
("All files", "*.*")
]
)
if file_path:
try:
# Simpan dengan DPI yang dipilih
self.fig.savefig(
file_path,
dpi=dpi,
bbox_inches='tight',
facecolor='white',
edgecolor='none'
)
messagebox.showinfo(
"Berhasil",
f"Plot berhasil disimpan ke:\n{file_path}\nResolusi: {dpi} DPI"
)
except Exception as e:
messagebox.showerror("Error", f"Gagal menyimpan plot:\n{str(e)}")
Panggil method ini di __init__:
Tugas: Buat plot apa saja, lalu coba save dalam berbagai format dan resolusi yang berbeda.
Praktikum 4.2: Export Data ke CSV
Tambahkan dalam method setup_export_controls:
# Export data button
export_data_btn = tk.Button(
self.control_frame,
text="Export Data CSV",
font=("Arial", 11),
command=self.export_data_csv,
bg="brown",
fg="white",
width=20
)
export_data_btn.pack(pady=5)
def export_data_csv(self):
if self.current_data is None:
messagebox.showwarning("Peringatan", "Tidak ada data untuk diekspor!")
return
file_path = filedialog.asksaveasfilename(
title="Export Data ke CSV",
defaultextension=".csv",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
)
if file_path:
try:
self.current_data.to_csv(file_path, index=False)
messagebox.showinfo(
"Berhasil",
f"Data berhasil diekspor ke:\n{file_path}"
)
except Exception as e:
messagebox.showerror("Error", f"Gagal mengekspor data:\n{str(e)}")
Tugas: Generate sample data, lalu export ke CSV dan verifikasi hasilnya.
4.2 Interactive Features dan Annotations
Praktikum 4.3: Click Event untuk Annotation
Tambahkan method untuk handle click events:
def setup_interactive_features(self):
# Enable interactive mode
self.annotations = []
self.interactive_mode = tk.BooleanVar(value=False)
# Interactive checkbox
interactive_check = tk.Checkbutton(
self.control_frame,
text="Mode Interaktif (Click to Annotate)",
variable=self.interactive_mode,
command=self.toggle_interactive,
font=("Arial", 10),
bg="lightblue",
wraplength=200
)
interactive_check.pack(pady=10)
# Clear annotations button
clear_btn = tk.Button(
self.control_frame,
text="Clear Annotations",
font=("Arial", 10),
command=self.clear_annotations,
bg="gray",
fg="white",
width=20
)
clear_btn.pack(pady=5)
def toggle_interactive(self):
if self.interactive_mode.get():
# Connect click event
self.click_cid = self.canvas.mpl_connect('button_press_event', self.on_plot_click)
else:
# Disconnect click event
if hasattr(self, 'click_cid'):
self.canvas.mpl_disconnect(self.click_cid)
def on_plot_click(self, event):
if not self.interactive_mode.get() or event.inaxes != self.ax:
return
# Get click coordinates
x, y = event.xdata, event.ydata
if x is None or y is None:
return
# Create annotation
annotation_text = f'({x:.2f}, {y:.2f})'
annotation = self.ax.annotate(
annotation_text,
xy=(x, y),
xytext=(10, 10),
textcoords='offset points',
bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.7),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')
)
# Store annotation for later removal
self.annotations.append(annotation)
# Redraw canvas
self.canvas.draw()
def clear_annotations(self):
# Remove all annotations
for annotation in self.annotations:
annotation.remove()
self.annotations = []
self.canvas.draw()
Panggil method ini di __init__:
Tugas: Aktifkan mode interaktif, lalu klik berbagai titik pada plot untuk menambahkan annotation. Test juga tombol clear annotations.
4.3 Customization dan Themes
Praktikum 4.4: Theme dan Style Customization
Tambahkan kontrol untuk theme dan styling:
def setup_theme_controls(self):
# Theme section
theme_label = tk.Label(
self.control_frame,
text="TEMA & STYLE",
font=("Arial", 14, "bold"),
bg="lightblue"
)
theme_label.pack(pady=(20, 10))
# Plot style
style_label = tk.Label(
self.control_frame,
text="Plot Style:",
font=("Arial", 10),
bg="lightblue"
)
style_label.pack(pady=(5, 2))
self.plot_style = tk.StringVar(value="default")
style_combo = ttk.Combobox(
self.control_frame,
textvariable=self.plot_style,
values=["default", "ggplot", "seaborn", "classic", "dark_background"],
state="readonly",
width=18
)
style_combo.pack(pady=5)
style_combo.bind("<<ComboboxSelected>>", self.on_style_change)
# Color scheme
color_scheme_label = tk.Label(
self.control_frame,
text="Color Scheme:",
font=("Arial", 10),
bg="lightblue"
)
color_scheme_label.pack(pady=(10, 2))
self.color_scheme = tk.StringVar(value="default")
color_combo = ttk.Combobox(
self.control_frame,
textvariable=self.color_scheme,
values=["default", "viridis", "plasma", "inferno", "cool", "warm"],
state="readonly",
width=18
)
color_combo.pack(pady=5)
color_combo.bind("<<ComboboxSelected>>", self.on_color_scheme_change)
# Grid style
self.show_grid = tk.BooleanVar(value=True)
grid_check = tk.Checkbutton(
self.control_frame,
text="Show Grid",
variable=self.show_grid,
command=self.update_grid,
font=("Arial", 10),
bg="lightblue"
)
grid_check.pack(pady=5)
def on_style_change(self, event=None):
style = self.plot_style.get()
try:
plt.style.use(style)
# Replot to apply new style
if hasattr(self, 'current_data') and self.current_data is not None:
self.plot_data_advanced()
else:
self.update_plot()
except:
messagebox.showerror("Error", f"Style '{style}' tidak tersedia")
self.plot_style.set("default")
plt.style.use("default")
def on_color_scheme_change(self, event=None):
# This will be applied in next plot update
if hasattr(self, 'current_data') and self.current_data is not None:
self.plot_data_advanced()
else:
self.update_plot()
def update_grid(self):
if hasattr(self, 'ax'):
self.ax.grid(self.show_grid.get(), alpha=0.3)
self.canvas.draw()
# Update plot methods to use color scheme
def get_colors(self, n_colors):
scheme = self.color_scheme.get()
if scheme == "default":
return ['blue', 'red', 'green', 'orange', 'purple', 'brown'][:n_colors]
else:
try:
cmap = plt.cm.get_cmap(scheme)
return [cmap(i / max(1, n_colors - 1)) for i in range(n_colors)]
except:
return ['blue', 'red', 'green', 'orange', 'purple', 'brown'][:n_colors]
Update method plot_data_advanced untuk menggunakan color scheme:
# Di awal method plot_data_advanced, ganti bagian colors dengan:
colors = self.get_colors(len(numeric_columns))
# Gunakan colors[i] instead of colors[i % len(colors)]
Panggil method ini di __init__:
Tugas: Test berbagai kombinasi plot style dan color scheme. Coba dark_background style dengan color scheme viridis atau plasma.
4.4 Performance Monitoring dan Status Bar
Praktikum 4.5: Status Bar dan Performance Info
Tambahkan status bar di bagian bawah aplikasi:
def setup_status_bar(self):
# Status bar at bottom
self.status_bar = tk.Frame(self.root, bg="gray", height=30)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
self.status_bar.pack_propagate(False)
# Status label
self.status_label = tk.Label(
self.status_bar,
text="Ready",
bg="gray",
fg="white",
font=("Arial", 10)
)
self.status_label.pack(side=tk.LEFT, padx=10, pady=5)
# Performance info
self.perf_label = tk.Label(
self.status_bar,
text="",
bg="gray",
fg="white",
font=("Arial", 10)
)
self.perf_label.pack(side=tk.RIGHT, padx=10, pady=5)
def update_status(self, message):
self.status_label.config(text=message)
self.root.update_idletasks()
def update_performance_info(self, start_time):
end_time = time.time()
duration = (end_time - start_time) * 1000 # Convert to ms
self.perf_label.config(text=f"Render time: {duration:.1f}ms")
Update beberapa method untuk menggunakan status bar:
# Di awal method plot_data_advanced, tambahkan:
start_time = time.time()
self.update_status("Plotting data...")
# Di akhir method plot_data_advanced, tambahkan:
self.update_performance_info(start_time)
self.update_status("Plot ready")
# Update method load_data untuk menggunakan status:
# Di awal method load_data, tambahkan:
self.update_status("Loading data...")
# Di akhir method load_data (dalam try block), tambahkan:
self.update_status(f"Data loaded: {rows} rows")
Panggil method ini di __init__:
Tugas: Test aplikasi dan perhatikan status bar yang menampilkan informasi status dan waktu render di bagian bawah aplikasi.
Bagian 5: Final Aplication dan Optimisasi
Pada bagian terakhir ini, kita akan menyelesaikan aplikasi dengan menambahkan fitur-fitur finishing touch dan optimisasi performa.
5.1 Menu Bar dan Keyboard Shortcuts
Praktikum 5.1: Implementasi Menu Bar Lengkap
Tambahkan method untuk membuat menu bar:
def create_menu_bar(self):
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# File menu
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Load Data CSV", command=self.load_data, accelerator="Ctrl+O")
file_menu.add_command(label="Generate Sample", command=self.generate_sample_data, accelerator="Ctrl+G")
file_menu.add_separator()
file_menu.add_command(label="Save Plot", command=self.save_plot, accelerator="Ctrl+S")
file_menu.add_command(label="Export Data", command=self.export_data_csv, accelerator="Ctrl+E")
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.on_closing, accelerator="Ctrl+Q")
# View menu
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="View", menu=view_menu)
view_menu.add_checkbutton(label="Show Grid", variable=self.show_grid, command=self.update_grid)
view_menu.add_checkbutton(label="Interactive Mode", variable=self.interactive_mode, command=self.toggle_interactive)
view_menu.add_checkbutton(label="Multiple Subplot", variable=self.use_subplot, command=self.toggle_subplot)
view_menu.add_separator()
view_menu.add_command(label="Clear Annotations", command=self.clear_annotations)
view_menu.add_command(label="Reset View", command=self.reset_view)
# Tools menu
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", menu=tools_menu)
tools_menu.add_checkbutton(label="Real-time Mode", variable=self.realtime_active, command=self.toggle_realtime)
tools_menu.add_separator()
tools_menu.add_command(label="Statistics", command=self.show_statistics)
tools_menu.add_command(label="Data Info", command=self.show_data_info)
# Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="Keyboard Shortcuts", command=self.show_shortcuts)
help_menu.add_command(label="About", command=self.show_about)
def setup_keyboard_shortcuts(self):
# Bind keyboard shortcuts
self.root.bind('<Control-o>', lambda e: self.load_data())
self.root.bind('<Control-g>', lambda e: self.generate_sample_data())
self.root.bind('<Control-s>', lambda e: self.save_plot())
self.root.bind('<Control-e>', lambda e: self.export_data_csv())
self.root.bind('<Control-q>', lambda e: self.on_closing())
self.root.bind('<F5>', lambda e: self.refresh_plot())
self.root.bind('<Escape>', lambda e: self.clear_annotations())
def reset_view(self):
if hasattr(self, 'ax'):
self.ax.relim()
self.ax.autoscale()
self.canvas.draw()
def refresh_plot(self):
if hasattr(self, 'current_data') and self.current_data is not None:
self.plot_data_advanced()
else:
self.update_plot()
def show_statistics(self):
if self.current_data is None:
messagebox.showwarning("Peringatan", "Tidak ada data untuk ditampilkan statistiknya!")
return
numeric_columns = self.current_data.select_dtypes(include=[np.number]).columns
if len(numeric_columns) == 0:
messagebox.showinfo("Info", "Tidak ada kolom numerik dalam data!")
return
# Create statistics window
stats_window = tk.Toplevel(self.root)
stats_window.title("Statistik Data")
stats_window.geometry("600x400")
stats_window.configure(bg="white")
# Text widget with scrollbar
text_frame = tk.Frame(stats_window)
text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
stats_text = tk.Text(text_frame, wrap=tk.WORD, font=("Courier", 10))
scrollbar = tk.Scrollbar(text_frame)
stats_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
stats_text.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=stats_text.yview)
# Generate statistics
stats_info = "STATISTIK DATA\n" + "="*50 + "\n\n"
stats_info += f"Jumlah baris: {len(self.current_data)}\n"
stats_info += f"Jumlah kolom: {len(self.current_data.columns)}\n\n"
for col in numeric_columns:
stats_info += f"KOLOM: {col}\n" + "-"*30 + "\n"
data_col = self.current_data[col].dropna()
stats_info += f"Count: {len(data_col)}\n"
stats_info += f"Mean: {data_col.mean():.4f}\n"
stats_info += f"Std: {data_col.std():.4f}\n"
stats_info += f"Min: {data_col.min():.4f}\n"
stats_info += f"25%: {data_col.quantile(0.25):.4f}\n"
stats_info += f"50%: {data_col.median():.4f}\n"
stats_info += f"75%: {data_col.quantile(0.75):.4f}\n"
stats_info += f"Max: {data_col.max():.4f}\n\n"
stats_text.insert(tk.END, stats_info)
stats_text.config(state=tk.DISABLED)
def show_data_info(self):
if self.current_data is None:
messagebox.showwarning("Peringatan", "Tidak ada data yang dimuat!")
return
rows, cols = self.current_data.shape
numeric_cols = len(self.current_data.select_dtypes(include=[np.number]).columns)
text_cols = len(self.current_data.select_dtypes(include=['object']).columns)
info = f"""INFORMASI DATA:
Dimensi: {rows} baris × {cols} kolom
Kolom numerik: {numeric_cols}
Kolom teks: {text_cols}
KOLOM YANG TERSEDIA:
{', '.join(self.current_data.columns.tolist())}
SAMPLE DATA (5 baris pertama):
{self.current_data.head().to_string()}"""
messagebox.showinfo("Informasi Data", info)
def show_shortcuts(self):
shortcuts = """KEYBOARD SHORTCUTS:
Ctrl+O - Load Data CSV
Ctrl+G - Generate Sample Data
Ctrl+S - Save Plot
Ctrl+E - Export Data CSV
Ctrl+Q - Exit Application
F5 - Refresh Plot
Esc - Clear Annotations
MOUSE INTERACTION:
- Click pada plot (saat Interactive Mode aktif) untuk menambah annotation
- Gunakan toolbar untuk zoom, pan, dan navigasi plot"""
messagebox.showinfo("Keyboard Shortcuts", shortcuts)
def show_about(self):
about_text = """DATA VISUALIZER v1.0
Aplikasi visualisasi data interaktif yang dibuat dengan:
- Python 3.x
- Tkinter (GUI Framework)
- Matplotlib (Plotting Library)
- NumPy & Pandas (Data Processing)
Fitur Utama:
✓ Visualisasi fungsi matematika real-time
✓ Import dan visualisasi data CSV
✓ Multiple plot types (line, scatter, bar, histogram, box)
✓ Real-time data simulation
✓ Interactive annotations
✓ Export plot dalam berbagai format
✓ Multiple themes dan color schemes
Developed for educational purposes."""
messagebox.showinfo("About Data Visualizer", about_text)
def on_closing(self):
if self.realtime_active.get():
self.realtime_active.set(False)
if messagebox.askokcancel("Exit", "Apakah Anda yakin ingin keluar?"):
self.root.destroy()
Update __init__ method untuk memanggil method-method baru:
def __init__(self, root):
self.root = root
self.root.title("Data Visualizer - Aplikasi Visualisasi Data")
self.root.geometry("1200x800") # Slightly larger
self.root.configure(bg="white")
# Setup all components
self.setup_ui()
self.setup_plot()
self.setup_controls()
self.setup_data_controls()
self.setup_export_controls()
self.setup_interactive_features()
self.setup_theme_controls()
self.setup_status_bar()
# Setup menu and shortcuts
self.create_menu_bar()
self.setup_keyboard_shortcuts()
# Handle window closing
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
Tugas: Test semua menu dan keyboard shortcuts. Coba fitur Statistics dan Data Info setelah me-load data.
5.2 Error Handling dan User Experience Improvements
Praktikum 5.2: Enhanced Error Handling
Tambahkan wrapper untuk error handling:
def safe_execute(self, func, error_message="An error occurred"):
"""Wrapper untuk menjalankan fungsi dengan error handling yang baik"""
try:
return func()
except Exception as e:
error_details = f"{error_message}\n\nTechnical details:\n{str(e)}"
messagebox.showerror("Error", error_details)
self.update_status(f"Error: {error_message}")
return None
def validate_data_requirements(self, min_numeric_cols=1):
"""Validasi apakah data memenuhi requirements minimum"""
if self.current_data is None:
messagebox.showwarning("Data Required", "Please load or generate data first!")
return False
numeric_columns = self.current_data.select_dtypes(include=[np.number]).columns
if len(numeric_columns) < min_numeric_cols:
messagebox.showwarning(
"Insufficient Data",
f"At least {min_numeric_cols} numeric column(s) required!"
)
return False
return True
# Update method plot_data_advanced dengan error handling:
def plot_data_advanced(self):
if not self.validate_data_requirements():
return
def _plot():
start_time = time.time()
self.update_status("Plotting data...")
# Clear plot
self.ax.clear()
numeric_columns = self.current_data.select_dtypes(include=[np.number]).columns
date_columns = self.current_data.select_dtypes(include=['datetime64']).columns
plot_type = self.plot_type.get()
colors = self.get_colors(len(numeric_columns))
if plot_type == "line":
if len(date_columns) > 0 and len(numeric_columns) > 0:
date_col = date_columns[0]
for i, col in enumerate(numeric_columns[:3]):
self.ax.plot(
self.current_data[date_col],
self.current_data[col],
color=colors[i],
label=col,
linewidth=2
)
self.ax.set_xlabel('Tanggal')
self.ax.legend()
elif len(numeric_columns) >= 1:
col = numeric_columns[0]
self.ax.plot(self.current_data[col], color=colors[0], linewidth=2)
self.ax.set_xlabel('Index')
self.ax.set_ylabel(col)
elif plot_type == "scatter":
if len(numeric_columns) >= 2:
x_col, y_col = numeric_columns[0], numeric_columns[1]
self.ax.scatter(
self.current_data[x_col],
self.current_data[y_col],
alpha=0.6,
s=50,
c=colors[0]
)
self.ax.set_xlabel(x_col)
self.ax.set_ylabel(y_col)
elif plot_type == "bar":
if len(numeric_columns) >= 1:
col = numeric_columns[0]
data_sample = self.current_data[col].head(20)
self.ax.bar(range(len(data_sample)), data_sample, color=colors[0])
self.ax.set_xlabel('Index')
self.ax.set_ylabel(col)
elif plot_type == "histogram":
if len(numeric_columns) >= 1:
col = numeric_columns[0]
self.ax.hist(
self.current_data[col].dropna(),
bins=30,
alpha=0.7,
color=colors[0],
edgecolor='black'
)
self.ax.set_xlabel(col)
self.ax.set_ylabel('Frekuensi')
elif plot_type == "box":
if len(numeric_columns) >= 1:
data_to_plot = [self.current_data[col].dropna() for col in numeric_columns[:5]]
labels = numeric_columns[:5].tolist()
bp = self.ax.boxplot(data_to_plot, labels=labels, patch_artist=True)
for patch, color in zip(bp['boxes'], colors):
patch.set_facecolor(color)
self.ax.set_ylabel('Nilai')
plt.setp(self.ax.xaxis.get_majorticklabels(), rotation=45)
self.ax.set_title(f'{plot_type.title()} Plot')
if self.show_grid.get():
self.ax.grid(True, alpha=0.3)
self.fig.tight_layout()
self.canvas.draw()
self.update_performance_info(start_time)
self.update_status("Plot ready")
self.safe_execute(_plot, "Failed to plot data")
5.3 Final Application Structure
Berikut adalah struktur lengkap aplikasi final:
# File: data_visualizer_final.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.dates as mdates
import numpy as np
import pandas as pd
import threading
import time
from datetime import datetime, timedelta
class DataVisualizer:
def __init__(self, root):
self.root = root
self.root.title("Data Visualizer - Professional Data Visualization Tool")
self.root.geometry("1200x800")
self.root.configure(bg="white")
# Initialize variables
self.current_data = None
self.annotations = []
self.realtime_data = []
self.max_points = 50
# Setup all components
self.setup_ui()
self.setup_plot()
self.setup_controls()
self.setup_data_controls()
self.setup_export_controls()
self.setup_interactive_features()
self.setup_theme_controls()
self.setup_status_bar()
# Setup menu and shortcuts
self.create_menu_bar()
self.setup_keyboard_shortcuts()
# Handle window closing
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# Initialize with default function plot
self.update_plot()
self.update_status("Ready - Load data or use function generator")
# Semua method yang sudah kita buat di atas...
# (Implementasi lengkap dari semua method di atas)
# Main execution
if __name__ == "__main__":
root = tk.Tk()
app = DataVisualizer(root)
root.mainloop()