From 72282285f84a46b6c6d9ade1c984b8c04f58546a Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 17 Apr 2026 19:50:22 +0200 Subject: add mathematical plotting Signed-off-by: salmonik --- cogs/plot.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 cogs/plot.py (limited to 'cogs/plot.py') diff --git a/cogs/plot.py b/cogs/plot.py new file mode 100644 index 0000000..1516bc1 --- /dev/null +++ b/cogs/plot.py @@ -0,0 +1,128 @@ +# This file is part of Senator Sperma +# Licensed under the GPL-3.0 License +# Copyright (C) 2026 Salmonik @ salmonik@tuta.io +# https://codeberg.org/salmonik/senator-sperma + +import asyncio +import logging +import math +import os +import re + +import discord +from discord.ext import commands + +logger = logging.getLogger(__name__) + +try: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + PLOT_AVAILABLE = True +except ImportError: + PLOT_AVAILABLE = False + logger.warning("matplotlib not available, plot command will be disabled") + + +def parse_expression(expr: str) -> str: + """Convert user expression (x^3) to Python (x**3)""" + expr = expr.replace("^", "**") + expr = re.sub(r"(\d)([x])", r"\1*\2", expr) + expr = re.sub(r"([x])(\d)", r"\1*\2", expr) + return expr + + +class Plot(commands.Cog): + def __init__(self, bot): + self.bot = bot + + def _generate_plot(self, func_expr: str, output_path: str): + """Generate plot (runs in executor)""" + parsed_expr = parse_expression(func_expr) + + x = [i * 0.1 for i in range(-100, 101)] + y = [] + + for xi in x: + try: + result = eval( + parsed_expr, + { + "x": xi, + "math": math, + "sin": math.sin, + "cos": math.cos, + "tan": math.tan, + "sqrt": math.sqrt, + "log": math.log, + "exp": math.exp, + "pi": math.pi, + "e": math.e, + }, + ) + if math.isfinite(result): + y.append(result) + else: + y.append(float("nan")) + except Exception: + y.append(float("nan")) + + fig, ax = plt.subplots(figsize=(10, 6)) + ax.plot(x, y, linewidth=2, color="#00ff00") + ax.set_title(f"y = {func_expr}", fontsize=14) + ax.set_xlabel("x", fontsize=12) + ax.set_ylabel("y", fontsize=12) + ax.grid(True, alpha=0.3) + ax.axhline(y=0, color="white", linewidth=0.5) + ax.axvline(x=0, color="white", linewidth=0.5) + ax.set_facecolor("#1a1a1a") + fig.patch.set_facecolor("#1a1a1a") + + plt.savefig(output_path, dpi=100, bbox_inches="tight") + plt.close() + + @commands.slash_command( + name="funkcja", description="Generuje wykres funkcji matematycznej" + ) + async def plot( + self, + ctx, + funkcja: str = discord.Option( + str, "Funkcja np. x^3, sin(x), x^2 + 2*x - 1", required=True + ), + ): + if not PLOT_AVAILABLE: + await ctx.respond( + "Generowanie wykresów nie jest dostępne (brak matplotlib)", + ephemeral=True, + ) + return + + await ctx.defer() + + temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "temp") + os.makedirs(temp_dir, exist_ok=True) + temp_path = os.path.join( + temp_dir, f"plot_{os.getpid()}_{asyncio.get_event_loop().time()}.png" + ) + + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._generate_plot, funkcja, temp_path) + await ctx.respond(file=discord.File(temp_path, filename="plot.png")) + logger.info(f"Generated plot for function: {funkcja}") + except Exception as e: + logger.error(f"Plot generation error: {e}") + await ctx.respond(f"Błąd generowania wykresu: {str(e)}", ephemeral=True) + finally: + try: + if os.path.exists(temp_path): + os.unlink(temp_path) + except Exception as e: + logger.error(f"Error cleaning up temp file: {e}") + + +def setup(bot): + bot.add_cog(Plot(bot)) -- cgit v1.2.3