Add Collatz Paradox visualizer — Python/tkinter port of original C# app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
ca607a8ede
236
collatz.py
Normal file
236
collatz.py
Normal file
@ -0,0 +1,236 @@
|
||||
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"
|
||||
|
||||
|
||||
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.state("zoomed")
|
||||
|
||||
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())
|
||||
|
||||
# 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()
|
||||
Loading…
Reference in New Issue
Block a user