commit ca607a8ede1beff6fc6a5bf2963cc948f0d992c3 Author: anas-rashid Date: Wed Apr 1 00:29:46 2026 +0200 Add Collatz Paradox visualizer — Python/tkinter port of original C# app Co-Authored-By: Claude Sonnet 4.6 diff --git a/collatz.py b/collatz.py new file mode 100644 index 0000000..c6bc4a2 --- /dev/null +++ b/collatz.py @@ -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("", 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("<>", 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()