- Fix window maximize: replace state("zoomed") with attributes("-zoomed", True) for Linux compatibility
- Add Tooltip class with hover popup showing the Collatz equation description
- Add info "?" button in control bar wired to the tooltip
- Legend was already present; tooltip complements it with equation context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
288 lines
9.4 KiB
Python
288 lines
9.4 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, scrolledtext
|
|
|
|
import matplotlib
|
|
matplotlib.use('TkAgg')
|
|
from matplotlib.figure import Figure
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
|
|
# Catppuccin Mocha palette
|
|
BG = "#1e1e2e"
|
|
SURFACE = "#313244"
|
|
OVERLAY = "#45475a"
|
|
TEXT = "#cdd6f4"
|
|
SUBTEXT = "#a6adc8"
|
|
BLUE = "#89b4fa"
|
|
MAUVE = "#cba6f7"
|
|
GREEN = "#a6e3a1"
|
|
RED = "#f38ba8"
|
|
TEAL = "#94e2d5"
|
|
|
|
|
|
COLLATZ_TOOLTIP = (
|
|
"Collatz Conjecture\n"
|
|
"──────────────────\n"
|
|
"Given any positive integer x, apply repeatedly:\n\n"
|
|
" • x is even → x = x / 2\n"
|
|
" • x is odd → x = 3x + 1\n\n"
|
|
"The conjecture states that no matter which\n"
|
|
"starting value you choose, the sequence will\n"
|
|
"always eventually reach 1."
|
|
)
|
|
|
|
|
|
class Tooltip:
|
|
"""Show a dark tooltip near a widget on hover."""
|
|
|
|
def __init__(self, widget: tk.Widget, text: str):
|
|
self._widget = widget
|
|
self._text = text
|
|
self._win: tk.Toplevel | None = None
|
|
widget.bind("<Enter>", self._show)
|
|
widget.bind("<Leave>", self._hide)
|
|
|
|
def _show(self, _event=None):
|
|
if self._win:
|
|
return
|
|
x = self._widget.winfo_rootx() + self._widget.winfo_width() + 6
|
|
y = self._widget.winfo_rooty()
|
|
self._win = tw = tk.Toplevel(self._widget)
|
|
tw.wm_overrideredirect(True)
|
|
tw.wm_geometry(f"+{x}+{y}")
|
|
tk.Label(
|
|
tw, text=self._text, justify=tk.LEFT,
|
|
bg=OVERLAY, fg=TEXT, font=("Consolas", 10),
|
|
relief=tk.FLAT, bd=0, padx=12, pady=10,
|
|
).pack()
|
|
|
|
def _hide(self, _event=None):
|
|
if self._win:
|
|
self._win.destroy()
|
|
self._win = None
|
|
|
|
|
|
def collatz_sequence(n: int) -> list[int]:
|
|
seq = []
|
|
while n != 1:
|
|
seq.append(n)
|
|
n = n // 2 if n % 2 == 0 else 3 * n + 1
|
|
seq.append(1)
|
|
return seq
|
|
|
|
|
|
class ValuesDialog(tk.Toplevel):
|
|
def __init__(self, parent, values: list[int] | None):
|
|
super().__init__(parent)
|
|
self.title("Collatz Sequence Values")
|
|
self.configure(bg=BG)
|
|
self.geometry("560x320")
|
|
self.resizable(True, True)
|
|
|
|
text = scrolledtext.ScrolledText(
|
|
self, wrap=tk.WORD, bg=SURFACE, fg=TEXT,
|
|
font=("Consolas", 11), insertbackground=TEXT,
|
|
bd=0, padx=12, pady=10,
|
|
)
|
|
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
if values:
|
|
content = ", ".join(str(v) for v in values)
|
|
else:
|
|
content = "Give some value of 'x'"
|
|
text.insert(tk.END, content)
|
|
text.config(state=tk.DISABLED)
|
|
self.focus_set()
|
|
|
|
|
|
class CollatzApp(tk.Tk):
|
|
CHART_TYPES = ["Line", "Scatter", "Bar", "Step"]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.title("Collatz Paradox")
|
|
self.configure(bg=BG)
|
|
self.attributes("-zoomed", True)
|
|
|
|
self.x: int = 10
|
|
self.values: list[int] = []
|
|
self.chart_type = tk.StringVar(value="Line")
|
|
|
|
self._configure_ttk_style()
|
|
self._build_ui()
|
|
self._plot()
|
|
|
|
# ------------------------------------------------------------------ UI --
|
|
|
|
def _configure_ttk_style(self):
|
|
style = ttk.Style()
|
|
style.theme_use("clam")
|
|
style.configure(
|
|
"Dark.TCombobox",
|
|
fieldbackground=OVERLAY,
|
|
background=OVERLAY,
|
|
foreground=TEXT,
|
|
selectbackground=MAUVE,
|
|
selectforeground=BG,
|
|
arrowcolor=TEXT,
|
|
bordercolor=OVERLAY,
|
|
lightcolor=OVERLAY,
|
|
darkcolor=OVERLAY,
|
|
)
|
|
style.map(
|
|
"Dark.TCombobox",
|
|
fieldbackground=[("readonly", OVERLAY)],
|
|
background=[("readonly", OVERLAY)],
|
|
)
|
|
|
|
def _build_ui(self):
|
|
# ── top control bar ────────────────────────────────────────────────
|
|
ctrl = tk.Frame(self, bg=SURFACE, pady=8)
|
|
ctrl.pack(side=tk.TOP, fill=tk.X, padx=0)
|
|
|
|
# x = label + entry
|
|
tk.Label(ctrl, text="x =", bg=SURFACE, fg=TEXT,
|
|
font=("Segoe UI", 12)).pack(side=tk.LEFT, padx=(14, 4))
|
|
|
|
self.entry = tk.Entry(
|
|
ctrl, bg=OVERLAY, fg=TEXT, insertbackground=TEXT,
|
|
font=("Segoe UI", 12), relief=tk.FLAT, width=14,
|
|
disabledbackground=OVERLAY,
|
|
)
|
|
self.entry.pack(side=tk.LEFT, padx=(0, 6), ipady=4)
|
|
self.entry.insert(0, str(self.x))
|
|
self.entry.bind("<Return>", lambda _e: self._plot())
|
|
|
|
# Plot button
|
|
tk.Button(
|
|
ctrl, text="Plot", command=self._plot,
|
|
bg=GREEN, fg=BG, font=("Segoe UI", 11, "bold"),
|
|
relief=tk.FLAT, bd=0, padx=14, pady=4, cursor="hand2",
|
|
activebackground=TEAL, activeforeground=BG,
|
|
).pack(side=tk.LEFT, padx=(0, 20))
|
|
|
|
# Graph type label + combobox
|
|
tk.Label(ctrl, text="Graph Type:", bg=SURFACE, fg=TEXT,
|
|
font=("Segoe UI", 12)).pack(side=tk.LEFT, padx=(0, 6))
|
|
|
|
combo = ttk.Combobox(
|
|
ctrl, textvariable=self.chart_type, values=self.CHART_TYPES,
|
|
state="readonly", width=10, style="Dark.TCombobox",
|
|
font=("Segoe UI", 11),
|
|
)
|
|
combo.pack(side=tk.LEFT, padx=(0, 20), ipady=3)
|
|
combo.bind("<<ComboboxSelected>>", lambda _e: self._plot())
|
|
|
|
# Info button with Collatz equation tooltip
|
|
info_btn = tk.Label(
|
|
ctrl, text=" ? ", bg=OVERLAY, fg=SUBTEXT,
|
|
font=("Segoe UI", 11, "bold"), cursor="question_arrow",
|
|
relief=tk.FLAT, padx=6, pady=4,
|
|
)
|
|
info_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
Tooltip(info_btn, COLLATZ_TOOLTIP)
|
|
|
|
# Right-side buttons
|
|
for label, cmd, color in [
|
|
("Clean Graph", self._clean, MAUVE),
|
|
("Get Values", self._show_values, BLUE),
|
|
]:
|
|
tk.Button(
|
|
ctrl, text=label, command=cmd,
|
|
bg=color, fg=BG, font=("Segoe UI", 11, "bold"),
|
|
relief=tk.FLAT, bd=0, padx=14, pady=4, cursor="hand2",
|
|
activebackground=TEXT, activeforeground=BG,
|
|
).pack(side=tk.RIGHT, padx=(4, 12))
|
|
|
|
# ── matplotlib canvas ──────────────────────────────────────────────
|
|
self.fig = Figure(facecolor=BG, tight_layout=True)
|
|
self.ax = self.fig.add_subplot(111)
|
|
self._style_axes()
|
|
|
|
self.canvas = FigureCanvasTkAgg(self.fig, master=self)
|
|
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Navigation toolbar (zoom / pan / save)
|
|
tb_frame = tk.Frame(self, bg=SURFACE)
|
|
tb_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
|
toolbar = NavigationToolbar2Tk(self.canvas, tb_frame)
|
|
toolbar.config(bg=SURFACE)
|
|
for child in toolbar.winfo_children():
|
|
try:
|
|
child.config(bg=SURFACE, fg=TEXT, activebackground=OVERLAY)
|
|
except tk.TclError:
|
|
pass
|
|
toolbar.update()
|
|
|
|
# --------------------------------------------------------------- axes --
|
|
|
|
def _style_axes(self):
|
|
self.ax.set_facecolor(SURFACE)
|
|
self.ax.tick_params(colors=SUBTEXT, labelsize=10)
|
|
for spine in self.ax.spines.values():
|
|
spine.set_edgecolor(OVERLAY)
|
|
self.ax.set_xlabel("Step", color=SUBTEXT, fontsize=11)
|
|
self.ax.set_ylabel("Value", color=SUBTEXT, fontsize=11)
|
|
self.ax.grid(True, color=OVERLAY, linewidth=0.6, linestyle="--")
|
|
self.fig.patch.set_facecolor(BG)
|
|
|
|
# -------------------------------------------------------------- logic --
|
|
|
|
def _plot(self):
|
|
raw = self.entry.get().strip()
|
|
try:
|
|
val = int(raw)
|
|
if val < 2:
|
|
raise ValueError
|
|
except ValueError:
|
|
self.entry.config(bg=RED)
|
|
return
|
|
|
|
self.entry.config(bg=OVERLAY)
|
|
self.x = val
|
|
self.values = collatz_sequence(self.x)
|
|
steps = len(self.values) - 1
|
|
|
|
self.ax.clear()
|
|
self._style_axes()
|
|
|
|
xs = list(range(len(self.values)))
|
|
ys = self.values
|
|
label = f"Collatz Paradox for x = {self.x}\nNo. of steps to converge at 1: {steps}"
|
|
|
|
chart = self.chart_type.get()
|
|
if chart == "Line":
|
|
self.ax.plot(xs, ys, color=BLUE, linewidth=2.5,
|
|
marker="o", markersize=3, label=label)
|
|
elif chart == "Scatter":
|
|
self.ax.scatter(xs, ys, color=BLUE, s=22, label=label)
|
|
elif chart == "Bar":
|
|
self.ax.bar(xs, ys, color=BLUE, width=0.7, label=label)
|
|
elif chart == "Step":
|
|
self.ax.step(xs, ys, color=BLUE, linewidth=2.5,
|
|
where="mid", label=label)
|
|
|
|
self.ax.set_title(
|
|
f"Collatz Paradox — x = {self.x}", color=TEXT, fontsize=13, pad=10,
|
|
)
|
|
|
|
legend = self.ax.legend(
|
|
facecolor=SURFACE, edgecolor=OVERLAY,
|
|
labelcolor=TEXT, fontsize=10,
|
|
)
|
|
|
|
self.canvas.draw()
|
|
|
|
def _clean(self):
|
|
self.values = []
|
|
self.ax.clear()
|
|
self._style_axes()
|
|
self.canvas.draw()
|
|
|
|
def _show_values(self):
|
|
ValuesDialog(self, self.values if self.values else None)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = CollatzApp()
|
|
app.mainloop()
|