Python数学可视化技术详解:交互式绘制显函数、隐函数及复杂曲线

Python数学可视化——显函数、隐函数及复杂曲线的交互式绘图技术

一、引言

在科学计算和数据分析中,函数与方程的可视化是理解数学关系和物理现象的重要工具。本文基于Python的Tkinter和Matplotlib库,实现一个功能完善的函数与方程可视化工具,支持显函数、隐函数、特殊曲线(如心形线)及物理场分布(如电势)的交互式绘图,并提供安全的表达式解析、图像保存等功能。

二、核心技术架构

2.1 系统架构与技术选型

  • 界面层:使用Tkinter构建GUI,包含类型选择、表达式输入、预设函数下拉菜单等控件
  • 计算层
  • 显函数:通过np.linspace生成采样点,安全计算函数值
  • 隐函数:基于等高线算法contour绘制等值线
  • 安全机制:通过正则表达式过滤非法字符,限制白名单函数防止代码注入
  • 可视化层:Matplotlib实现图表渲染,支持动态更新和交互式工具条
  • 2.2 安全表达式解析

    def is_valid_expression(expr):
        """验证表达式安全性"""
        allowed_chars = set("0123456789.+-*/()xy^np_sin_cos_tan_exp_sqrt_log_pi_ ")
        invalid_chars = set(expr.replace('.', '').replace('_', '')) - allowed_chars
        if invalid_chars:
            raise ValueError(f"非法字符: {''.join(invalid_chars)}")
        # 括号匹配检查
        stack = []
        for char in expr:
            if char == '(': stack.append(char)
            elif char == ')':
                if not stack: raise ValueError("括号不匹配")
                stack.pop()
        if stack: raise ValueError("括号不匹配")
        return True
    
    def safe_eval(expr, namespace):
        """安全执行表达式"""
        expr = expr.replace('^', '**')  # 替换幂运算符
        allowed_funcs = {
            'np': np, 'sin': np.sin, 'cos': np.cos, 'tan': np.tan,
            'exp': np.exp, 'sqrt': np.sqrt, 'log': np.log, 'pi': np.pi
        }
        safe_globals = {"__builtins__": None}
        safe_locals = {**allowed_funcs, **namespace}
        compiled_code = compile(expr, '<string>', 'eval')
        return eval(compiled_code, safe_globals, safe_locals)
    

    三、显函数可视化

    3.1 核心实现

    def plot_explicit_function(self, f, x_range, title):
        """绘制显函数"""
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.set_facecolor('white')
        
        x = np.linspace(x_range[0], x_range[1], 1000)
        y = np.array([f(xi) for xi in x])  # 逐点计算防止数组错误
        
        ax.plot(x, y, 'b-', linewidth=2.5)
        ax.set_title(title)
        ax.grid(True, linestyle='--', alpha=0.6)
        self.optimize_ticks(ax, x_range, (y.min(), y.max()))
    

    3.2 案例演示

    案例1:三次函数
    # 预设函数定义
    self.explicit_presets = {
        "三次函数": {
            "func": lambda x: x**3 - 3*x,
            "expr": "x**3 - 3*x",
            "x_range": (-2.5, 2.5),
            "title": "三次函数: $y = x^3 - 3x$",
        }
    }
    

    案例2:双曲线
    plot_explicit("1/x", x_range=(-5,5))  # 输入表达式直接绘制
    

    四、隐函数可视化

    4.1 核心实现

    def plot_implicit_equation(self, eq, x_range, y_range):
        """绘制隐函数F(x,y)=0"""
        x = np.linspace(x_range[0], x_range[1], 500)
        y = np.linspace(y_range[0], y_range[1], 500)
        X, Y = np.meshgrid(x, y)
        Z = eq(X, Y)
        
        self.fig.contour(X, Y, Z, levels=[0], colors='red', linewidths=2.5)
        self.fig.contourf(X, Y, Z, alpha=0.6)  # 填充色显示数值分布
        self.fig.colorbar(label='F(x,y)')
    

    4.2 案例演示

    案例1:圆方程
    # 预设隐函数
    self.implicit_presets["圆"] = {
        "eq": lambda x, y: x**2 + y**2 - 4,
        "title": "圆: $x^2 + y^2 = 4$",
    }
    

    案例2:笛卡尔叶形线
    plot_implicit("x**3 + y**3 - 3*x*y", x_range=(-3,3), y_range=(-3,3))
    

    五、特色曲线与物理应用

    5.1 心形线(数学艺术)

    def plot_heart_curve(self):
        """笛卡尔心形线"""
        eq = lambda x,y: (x**2 + y**2 -1)**3 - x**2*y**3
        self.plot_implicit_equation(eq, x_range=(-1.5,1.5), y_range=(-1.5,1.5))
        self.fig.contourf(..., colors='pink', alpha=0.4)  # 填充爱心区域
    

    5.2 电势分布(物理应用)

    def plot_electric_potential(self):
        """点电荷电势分布"""
        charges = [{"x":-1,"y":0,"q":1}, {"x":1,"y":0,"q":-1}]
        x = np.linspace(-2.5,2.5,500)
        y = np.linspace(-2,2,500)
        X,Y = np.meshgrid(x,y)
        
        V = sum(charge['q']/np.sqrt((X-c['x'])**2 + (Y-c['y'])**2) for c in charges)
        self.fig.contourf(X,Y,V, cmap='coolwarm')  # 温度映射显示电势
        self.fig.scatter([c['x']], [c['y']], s=300, c=['red','blue'], marker='+-')
    

    六、交互式GUI设计

    6.1 界面布局

    def __init__(self, root):
        self.root = root
        self.root.geometry("1200x800")
        
        # 左侧控制面板
        left_frame = ttk.LabelFrame(root, text="可视化选项")
        ttk.Radiobutton(left_frame, text="显函数", variable=self.viz_type, value="explicit")
        ttk.Radiobutton(left_frame, text="隐函数", variable=self.viz_type, value="implicit")
        ttk.Radiobutton(left_frame, text="心形线", variable=self.viz_type, value="heart")
        
        # 右侧绘图区域
        self.canvas = FigureCanvasTkAgg(self.fig, master=right_frame)
        self.toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame)  # 集成缩放工具
    

    6.2 动态控件更新

    def update_controls(self):
        """根据选择类型显示对应控件"""
        if self.viz_type.get() == "explicit":
            self.explicit_frame.pack()
            self.update_preset_options(self.explicit_presets.keys())
        elif self.viz_type.get() == "implicit":
            self.implicit_frame.pack()
            self.update_preset_options(self.implicit_presets.keys())
        # 隐藏其他面板
    

    七、高级功能

    7.1 图像保存

    def save_image(self):
        filename = simpledialog.askstring("保存", "文件名")
        if filename:
            self.fig.savefig(f"{filename}.png", dpi=150, bbox_inches="tight")
            messagebox.showinfo("成功", f"保存至: {os.path.abspath(filename)}")
    

    7.2 公式渲染

    def get_function_label(self, expr):
        """生成LaTeX公式"""
        expr = expr.replace('np.sin', '\\sin').replace('**', '^')
        expr = re.sub(r'(\d)/(\d)', r'\\frac{\1}{\2}', expr)  # 自动转换分数
        return f"${expr}$"
    

    八、教学与应用场景

    8.1 教学场景

  • 基础数学:演示函数图像变换(平移、缩放、翻转)
  • 解析几何:对比显式方程与隐式方程的几何意义
  • 高等数学:展示参数方程(如心形线)与极坐标方程
  • 大学物理:可视化电场、磁场等物理场分布
  • 8.2 扩展方向

    1. 支持极坐标绘图
    2. 添加导数/积分可视化
    3. 集成3D绘图功能(使用mpl_toolkits.mplot3d)
    4. 开发数据导入功能(CSV/Excel)

    九、总结

    本文实现的函数可视化工具具备以下特点:

    1. 安全性:通过表达式过滤和白名单机制防止代码注入
    2. 交互性:支持实时切换函数类型、调整参数、缩放图表
    3. 扩展性:预设函数与自定义输入结合,方便扩展新类型
    4. 专业性:支持LaTeX公式渲染、物理场可视化等专业需求

    该工具可广泛应用于数学教学、工程仿真、科学研究等领域,帮助用户快速建立数学表达式与图形之间的直观联系。

    十、完整代码

    import tkinter as tk
    from tkinter import ttk, messagebox, simpledialog
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
    from matplotlib.figure import Figure
    import matplotlib.patches as patches
    from matplotlib import ticker
    from matplotlib.colors import ListedColormap
    import re
    import os
    
    # 设置matplotlib支持中文显示
    plt.rcParams["font.family"] = [
        "SimHei",
        "WenQuanYi Micro Hei",
        "Heiti TC",
        "Arial Unicode MS",
    ]
    plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示问题
    
    
    class FunctionVisualizer:
        def __init__(self, root):
            self.root = root
            self.root.title("函数与方程可视化工具")
            self.root.geometry("1200x800")
            self.root.minsize(1000, 700)
    
            # 设置主题颜色
            self.bg_color = "#f5f5f5"
            self.frame_color = "#ffffff"
            self.button_color = "#3b82f6"
            self.button_text_color = "#ffffff"
    
            # 预设函数分组(显函数/隐函数)
            self.explicit_presets = {
                "三次函数": {
                    "func": lambda x: x**3 - 3 * x,
                    "expr": "x**3 - 3*x",
                    "x_range": (-2.5, 2.5),
                    "title": "三次函数: $y = x^3 - 3x$",
                },
                "双曲线": {
                    "func": lambda x: 1 / x,
                    "expr": "1/x",
                    "x_range": (-5, 5),
                    "title": "双曲线: $y = \\frac{1}{x}$",
                },
                "指数函数": {
                    "func": lambda x: np.exp(x),
                    "expr": "np.exp(x)",
                    "x_range": (-3, 3),
                    "title": "指数函数: $y = e^x$",
                },
            }
    
            self.implicit_presets = {
                "圆": {
                    "eq": lambda x, y: x**2 + y**2 - 4,
                    "expr": "x**2 + y**2 - 4",
                    "x_range": (-3, 3),
                    "y_range": (-3, 3),
                    "title": "圆: $x^2 + y^2 = 4$",
                },
                "椭圆": {
                    "eq": lambda x, y: x**2 / 4 + y**2 / 9 - 1,
                    "expr": "x**2/4 + y**2/9 - 1",
                    "x_range": (-3, 3),
                    "y_range": (-4, 4),
                    "title": "椭圆: $\\frac{x^2}{4} + \\frac{y^2}{9} = 1$",
                },
                "双曲线(隐式)": {
                    "eq": lambda x, y: x**2 - y**2 - 1,
                    "expr": "x**2 - y**2 - 1",
                    "x_range": (-3, 3),
                    "y_range": (-3, 3),
                    "title": "双曲线: $x^2 - y^2 = 1$",
                },
                "笛卡尔叶形线": {
                    "eq": lambda x, y: x**3 + y**3 - 3 * x * y,
                    "expr": "x**3 + y**3 - 3*x*y",
                    "x_range": (-3, 3),
                    "y_range": (-3, 3),
                    "title": "笛卡尔叶形线: $x^3 + y^3 = 3xy$",
                },
            }
    
            # 创建主框架
            main_frame = ttk.Frame(self.root, padding=10)
            main_frame.pack(fill=tk.BOTH, expand=True)
    
            # 创建左侧控制面板
            left_frame = ttk.LabelFrame(
                main_frame, text="函数与方程可视化选项", padding=10, width=375
            )
            left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
            left_frame.pack_propagate(False)  # 固定宽度
    
            # 创建右侧绘图区域
            right_frame = ttk.Frame(main_frame)
            right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
    
            # 创建绘图区域和工具栏容器
            self.plot_frame = ttk.Frame(right_frame)
            self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
    
            # 初始化绘图区域
            self.fig = Figure(figsize=(8, 6), dpi=100)
            self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
            self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
    
            # 添加工具栏
            self.toolbar_frame = ttk.Frame(right_frame, height=40)
            self.toolbar_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
            self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
            self.toolbar.update()
    
            # 添加控制选项
            self.create_controls(left_frame)
    
            # 初始显示
            self.plot_predefined_function()
    
        def create_controls(self, parent):
            """创建控制选项"""
            ttk.Label(parent, text="选择可视化类型:", font=("SimHei", 10, "bold")).pack(
                anchor=tk.W, pady=(0, 10)
            )
    
            # 可视化类型选择
            self.viz_type = tk.StringVar(value="explicit")
            types = [
                ("显函数", "explicit"),
                ("隐函数", "implicit"),
                ("心形线", "heart"),
                ("电势分布", "potential"),
            ]
    
            for text, value in types:
                ttk.Radiobutton(
                    parent,
                    text=text,
                    variable=self.viz_type,
                    value=value,
                    command=self.update_controls,
                ).pack(anchor=tk.W, padx=5, pady=2)
    
            # 预设函数下拉菜单(动态更新选项)
            self.preset_frame = ttk.LabelFrame(parent, text="预设函数", padding=10)
            self.preset_frame.pack(fill=tk.X, pady=10)
    
            # 动态选项变量
            self.preset_functions = tk.StringVar()
            self.preset_combobox = ttk.Combobox(
                self.preset_frame, textvariable=self.preset_functions, width=30
            )
            self.preset_combobox.pack(fill=tk.X, pady=5)
    
            ttk.Button(
                self.preset_frame,
                text="绘制预设函数",
                command=self.plot_predefined_function,
            ).pack(fill=tk.X, pady=5)
    
            # 显函数输入
            self.explicit_frame = ttk.LabelFrame(parent, text="显函数输入", padding=10)
            self.explicit_frame.pack(fill=tk.X, pady=10)
    
            ttk.Label(self.explicit_frame, text="函数表达式 (例如 x**2):").pack(anchor=tk.W)
            self.explicit_entry = ttk.Entry(self.explicit_frame, width=30)
            self.explicit_entry.insert(0, "x**3 - 3*x")
            self.explicit_entry.pack(fill=tk.X, pady=5)
    
            ttk.Label(self.explicit_frame, text="X范围 (min,max):").pack(anchor=tk.W)
            self.x_range_entry = ttk.Entry(self.explicit_frame, width=30)
            self.x_range_entry.insert(0, "-2.5,2.5")
            self.x_range_entry.pack(fill=tk.X, pady=5)
    
            ttk.Button(
                self.explicit_frame, text="绘制显函数", command=self.plot_explicit
            ).pack(fill=tk.X, pady=5)
    
            # 隐函数输入
            self.implicit_frame = ttk.LabelFrame(parent, text="隐函数输入", padding=10)
            self.implicit_frame.pack(fill=tk.X, pady=10)
    
            ttk.Label(self.implicit_frame, text="方程表达式 (例如 x**2 + y**2 - 4):").pack(
                anchor=tk.W
            )
            self.implicit_entry = ttk.Entry(self.implicit_frame, width=30)
            self.implicit_entry.insert(0, "x**3 + y**3 - 3*x*y")
            self.implicit_entry.pack(fill=tk.X, pady=5)
    
            ttk.Label(self.implicit_frame, text="X范围 (min,max):").pack(anchor=tk.W)
            self.implicit_x_range_entry = ttk.Entry(self.implicit_frame, width=30)
            self.implicit_x_range_entry.insert(0, "-3,3")
            self.implicit_x_range_entry.pack(fill=tk.X, pady=5)
    
            ttk.Label(self.implicit_frame, text="Y范围 (min,max):").pack(anchor=tk.W)
            self.implicit_y_range_entry = ttk.Entry(self.implicit_frame, width=30)
            self.implicit_y_range_entry.insert(0, "-3,3")
            self.implicit_y_range_entry.pack(fill=tk.X, pady=5)
    
            ttk.Button(
                self.implicit_frame, text="绘制隐函数", command=self.plot_implicit
            ).pack(fill=tk.X, pady=5)
    
            # 保存图像按钮
            ttk.Button(parent, text="保存图像", command=self.save_image).pack(
                side=tk.BOTTOM, pady=10
            )
    
            # 初始更新控件状态
            self.update_controls()
    
        def update_controls(self):
            """更新控件状态"""
            viz_type = self.viz_type.get()
    
            # 隐藏所有输入面板
            self.preset_frame.pack_forget()
            self.explicit_frame.pack_forget()
            self.implicit_frame.pack_forget()
    
            # 显示对应面板
            if viz_type == "explicit":
                self.explicit_frame.pack(fill=tk.X, pady=10)
                self.update_preset_options(self.explicit_presets.keys())  # 显函数预设
            elif viz_type == "implicit":
                self.implicit_frame.pack(fill=tk.X, pady=10)
                self.update_preset_options(self.implicit_presets.keys())  # 隐函数预设
            elif viz_type == "heart":
                self.plot_heart_curve()
            elif viz_type == "potential":
                self.plot_electric_potential()
    
            # 显示预设框架
            self.preset_frame.pack(fill=tk.X, pady=10)
    
        def update_preset_options(self, options=None):
            """动态更新预设函数选项"""
            if options is None:
                options = []
            self.preset_combobox["values"] = list(options)
            if options:
                self.preset_functions.set(list(options)[0])  # 默认选择第一个
    
        def plot_predefined_function(self):
            """绘制预设函数"""
            viz_type = self.viz_type.get()
            selected = self.preset_functions.get()
    
            self.fig.clear()
            ax = self.fig.add_subplot(111)
            ax.set_facecolor("white")
            self.fig.set_facecolor("white")
    
            if viz_type == "explicit" and selected in self.explicit_presets:
                data = self.explicit_presets[selected]
                self.plot_explicit_function(
                    f=data["func"], x_range=data["x_range"], title=data["title"]
                )
                # 更新显函数输入框
                self.explicit_entry.delete(0, tk.END)
                self.explicit_entry.insert(0, data["expr"])
                self.x_range_entry.delete(0, tk.END)
                self.x_range_entry.insert(0, f"{data['x_range'][0]},{data['x_range'][1]}")
    
            elif viz_type == "implicit" and selected in self.implicit_presets:
                data = self.implicit_presets[selected]
                self.plot_implicit_equation(
                    eq=data["eq"],
                    x_range=data["x_range"],
                    y_range=data["y_range"],
                    title=data["title"],
                )
                # 更新隐函数输入框
                self.implicit_entry.delete(0, tk.END)
                self.implicit_entry.insert(0, data["expr"])
                self.implicit_x_range_entry.delete(0, tk.END)
                self.implicit_x_range_entry.insert(
                    0, f"{data['x_range'][0]},{data['x_range'][1]}"
                )
                self.implicit_y_range_entry.delete(0, tk.END)
                self.implicit_y_range_entry.insert(
                    0, f"{data['y_range'][0]},{data['y_range'][1]}"
                )
    
            self.canvas.draw()
    
        def is_valid_expression(self, expr):
            """验证表达式是否为有效的数学表达式"""
            # 允许的字符:数字、运算符、函数名、xy变量、小数点、括号、空格
            allowed_chars = set("0123456789.+-*/()xy^np_sin_cos_tan_exp_sqrt_log_pi_ ")
            
            # 移除所有允许的字符,检查是否还有剩余
            cleaned = expr.replace('.', '').replace('_', '')
            invalid_chars = set(cleaned) - allowed_chars
            
            if invalid_chars:
                raise ValueError(f"非法字符: {''.join(invalid_chars)}")
            
            # 检查括号匹配
            stack = []
            for char in expr:
                if char == '(':
                    stack.append(char)
                elif char == ')':
                    if not stack:
                        raise ValueError("括号不匹配:缺少左括号")
                    stack.pop()
            
            if stack:
                raise ValueError("括号不匹配:缺少右括号")
            
            return True
    
        def safe_eval(self, expr, namespace):
            """安全地执行表达式计算"""
            try:
                self.is_valid_expression(expr)
                
                # 替换常见函数别名
                expr = expr.replace('^', '**')  # 替换^为**
                
                # 白名单函数和变量
                allowed_funcs = {
                    'np': np,
                    'sin': np.sin,
                    'cos': np.cos,
                    'tan': np.tan,
                    'exp': np.exp,
                    'sqrt': np.sqrt,
                    'log': np.log,
                    'pi': np.pi,
                    'arctan2': np.arctan2,
                }
                
                # 创建安全命名空间
                safe_globals = {"__builtins__": None}
                safe_locals = {**allowed_funcs, **namespace}
                
                # 使用编译后的代码提高安全性
                compiled_code = compile(expr, '<string>', 'eval')
                return eval(compiled_code, safe_globals, safe_locals)
            
            except Exception as e:
                raise ValueError(f"表达式错误: {str(e)}")
    
        def plot_explicit(self):
            """绘制用户输入的显函数"""
            try:
                func_str = self.explicit_entry.get().strip()
                x_range_str = self.x_range_entry.get().strip()
                
                if not func_str or not x_range_str:
                    raise ValueError("请输入函数表达式和X范围")
    
                # 解析x范围
                x_min, x_max = map(float, x_range_str.split(","))
                if x_min >= x_max:
                    raise ValueError("X范围的最小值必须小于最大值")
    
                # 生成x值
                x_vals = np.linspace(x_min, x_max, 1000)
                
                # 安全计算y值(逐个点计算,避免数组错误)
                y_vals = np.zeros_like(x_vals)
                for i, x in enumerate(x_vals):
                    y_vals[i] = self.safe_eval(func_str, {'x': x})
    
                # 绘制函数
                self.plot_explicit_function(
                    f=lambda x: y_vals,
                    x_range=(x_min, x_max),
                    title=f"显函数: $y = {self.get_function_label(func_str)}$",
                )
                self.canvas.draw()
    
            except Exception as e:
                messagebox.showerror("错误", f"绘制显函数时出错: {str(e)}")
    
        def plot_implicit(self):
            """绘制用户输入的隐函数(修复网格点数不匹配问题)"""
            try:
                eq_str = self.implicit_entry.get().strip()
                x_range_str = self.implicit_x_range_entry.get().strip()
                y_range_str = self.implicit_y_range_entry.get().strip()
                
                if not eq_str or not x_range_str or not y_range_str:
                    raise ValueError("请输入完整的方程表达式和范围")
    
                # 解析范围
                x_min, x_max = map(float, x_range_str.split(","))
                y_min, y_max = map(float, y_range_str.split(","))
                
                if x_min >= x_max or y_min >= y_max:
                    raise ValueError("范围的最小值必须小于最大值")
    
                # 创建向量化的方程函数(直接处理数组输入)
                eq = lambda X, Y: self.safe_eval(eq_str, {'x': X, 'y': Y})
    
                # 调用隐函数绘图函数,使用默认分辨率500(与函数内部一致)
                self.plot_implicit_equation(
                    eq=eq,
                    x_range=(x_min, x_max),
                    y_range=(y_min, y_max),
                    title=f"隐函数: ${self.get_function_label(eq_str)} = 0$",
                )
                self.canvas.draw()
    
            except Exception as e:
                messagebox.showerror("错误", f"绘制隐函数时出错: {str(e)}")
    
            except Exception as e:
                messagebox.showerror("错误", f"绘制隐函数时出错: {str(e)}")
    
        def plot_explicit_function(self, f, x_range=(-5, 5), title="显函数图像"):
            """
            绘制显函数 y = f(x) 的图像
    
            参数:
            f: 函数对象
            x_range: x轴范围
            title: 图像标题
            """
            self.fig.clear()
            ax = self.fig.add_subplot(111)
    
            # 设置背景为白色
            ax.set_facecolor("white")
            self.fig.set_facecolor("white")
    
            # 创建网格和样式
            ax.grid(True, linestyle="--", alpha=0.6)
            ax.spines["left"].set_position("zero")
            ax.spines["bottom"].set_position("zero")
            ax.spines["right"].set_visible(False)
            ax.spines["top"].set_visible(False)
    
            # 生成数据
            x = np.linspace(x_range[0], x_range[1], 1000)
            try:
                y = f(x)
            except Exception as e:
                messagebox.showerror("函数错误", f"计算函数值时出错: {str(e)}")
                return
    
            # 绘制函数曲线
            ax.plot(x, y, "b-", linewidth=2.5)
    
            # 设置标题和标签
            ax.set_title(title, fontsize=16, pad=20)
            ax.set_xlabel("x", fontsize=12, labelpad=-10, x=1.02)
            ax.set_ylabel("y", fontsize=12, labelpad=-20, y=1.02, rotation=0)
    
            # 优化坐标轴刻度
            self.optimize_ticks(ax, x_range, (np.min(y), np.max(y)))
    
            self.fig.tight_layout()
    
        def plot_implicit_equation(
            self,
            eq,
            x_range=(-3, 3),
            y_range=(-3, 3),
            resolution=500,
            levels=[0],
            cmap="viridis",
            title="隐函数图像",
        ):
            """
            绘制隐函数 F(x, y) = 0 的图像
    
            参数:
            eq: 函数对象
            x_range, y_range: 绘图范围
            resolution: 网格分辨率
            levels: 绘制等高线的值
            cmap: 颜色映射
            title: 图像标题
            """
            self.fig.clear()
            ax = self.fig.add_subplot(111)
    
            # 设置背景为白色
            ax.set_facecolor("white")
            self.fig.set_facecolor("white")
    
            # 创建网格
            x = np.linspace(x_range[0], x_range[1], resolution)
            y = np.linspace(y_range[0], y_range[1], resolution)
            X, Y = np.meshgrid(x, y)
    
            # 计算方程值
            try:
                Z = eq(X, Y)
            except Exception as e:
                messagebox.showerror("方程错误", f"计算方程值时出错: {str(e)}")
                return
    
            # 绘制等高线 (隐函数曲线)
            contour = ax.contour(X, Y, Z, levels=levels, colors="red", linewidths=2.5)
    
            # 添加填充色显示方程值的变化 (只在需要时)
            if len(levels) > 1:
                ax.contourf(
                    X, Y, Z, levels=np.linspace(Z.min(), Z.max(), 100), cmap=cmap, alpha=0.6
                )
                # 添加颜色条
                cbar = self.fig.colorbar(contour)
                cbar.set_label("F(x, y)", rotation=270, labelpad=20)
    
            # 设置网格和样式
            ax.grid(True, linestyle="--", alpha=0.4)
            ax.set_aspect("equal")
    
            # 设置标题和标签
            ax.set_title(title, fontsize=16, pad=20)
            ax.set_xlabel("x", fontsize=12)
            ax.set_ylabel("y", fontsize=12)
    
            # 添加零线
            ax.axhline(0, color="black", linewidth=0.8, alpha=0.7)
            ax.axvline(0, color="black", linewidth=0.8, alpha=0.7)
    
            # 优化坐标轴刻度
            self.optimize_ticks(ax, x_range, y_range)
    
            self.fig.tight_layout()
    
        def optimize_ticks(self, ax, x_range, y_range):
            """优化坐标轴刻度,避免刻度过于密集"""
            x_min, x_max = x_range
            y_min, y_max = y_range
    
            # 根据数据范围自动设置刻度
            x_span = x_max - x_min
            y_span = y_max - y_min
    
            # 设置合理的刻度间隔
            x_major_locator = ticker.MaxNLocator(nbins=7)
            y_major_locator = ticker.MaxNLocator(nbins=7)
    
            ax.xaxis.set_major_locator(x_major_locator)
            ax.yaxis.set_major_locator(y_major_locator)
    
        def plot_heart_curve(self):
            """绘制心形线"""
            self.fig.clear()
    
            # 创建图像和子图
            ax1 = self.fig.add_subplot(111)
            ax1.set_aspect("equal")
            ax1.set_title("心形线: $(x^2+y^2-1)^3 - x^2y^3 = 0$", fontsize=14)
    
            # 设置背景为白色
            ax1.set_facecolor("white")
            self.fig.set_facecolor("white")
    
            # 定义心形线方程
            def heart_eq(x, y):
                return (x**2 + y**2 - 1) ** 3 - x**2 * y**3
    
            # 生成网格
            x = np.linspace(-1.5, 1.5, 500)
            y = np.linspace(-1.5, 1.5, 500)
            X, Y = np.meshgrid(x, y)
            Z = heart_eq(X, Y)
    
            # 绘制心形线
            contour = ax1.contour(X, Y, Z, levels=[0], colors="red", linewidths=3)
    
            # 填充颜色
            ax1.contourf(X, Y, Z, levels=[-1000, 0], colors=["pink"], alpha=0.4)
    
            # 添加网格和样式
            ax1.grid(True, linestyle="--", alpha=0.3)
            ax1.set_xlim(-1.5, 1.5)
            ax1.set_ylim(-1.5, 1.5)
    
            # 优化坐标轴刻度
            self.optimize_ticks(ax1, (-1.5, 1.5), (-1.5, 1.5))
    
            self.fig.tight_layout()
            self.canvas.draw()
    
        def plot_electric_potential(self):
            """可视化点电荷系统的电势分布"""
            self.fig.clear()
            ax = self.fig.add_subplot(111)
    
            # 设置背景为白色
            ax.set_facecolor("white")
            self.fig.set_facecolor("white")
    
            # 定义两个点电荷的位置和电荷量
            charges = [
                {"x": -1, "y": 0, "q": 1},  # 正电荷
                {"x": 1, "y": 0, "q": -1},  # 负电荷
            ]
    
            # 创建网格
            x = np.linspace(-2.5, 2.5, 500)
            y = np.linspace(-2, 2, 500)
            X, Y = np.meshgrid(x, y)
    
            # 计算电势 (k=1)
            V = np.zeros_like(X)
            for charge in charges:
                r = np.sqrt((X - charge["x"]) ** 2 + (Y - charge["y"]) ** 2)
                V += charge["q"] / r
    
            # 避免除以零
            V = np.nan_to_num(V, posinf=10, neginf=-10)
    
            # 绘制电势等高线 (使用contourf创建填充等高线)
            levels = np.linspace(-10, 10, 21)
            contourf = ax.contourf(X, Y, V, levels=levels, cmap="coolwarm", alpha=0.8)
            contour = ax.contour(X, Y, V, levels=levels, colors="k", linewidths=0.5)
            ax.clabel(contour, inline=True, fontsize=8)
    
            # 绘制电荷位置
            for charge in charges:
                color = "red" if charge["q"] > 0 else "blue"
                marker = "+" if charge["q"] > 0 else "_"
                ax.scatter(
                    charge["x"], charge["y"], s=300, c=color, marker=marker, linewidths=2
                )
                ax.text(
                    charge["x"],
                    charge["y"] + 0.2,
                    f"{charge['q']}q",
                    ha="center",
                    fontsize=12,
                    weight="bold",
                )
    
            # 设置标题和标签
            ax.set_title("两个点电荷的电势分布", fontsize=16, pad=20)
            ax.set_xlabel("x (m)", fontsize=12)
            ax.set_ylabel("y (m)", fontsize=12)
    
            # 添加网格和样式
            ax.set_aspect("equal")
            ax.grid(True, linestyle="--", alpha=0.4)
    
            # 添加坐标轴
            ax.axhline(0, color="k", linewidth=0.8, alpha=0.7)
            ax.axvline(0, color="k", linewidth=0.8, alpha=0.7)
    
            # 添加物理公式
            ax.text(
                1.5,
                1.8,
                r"$V = \sum \frac{kq_i}{r_i}$",
                fontsize=14,
                bbox=dict(facecolor="white", alpha=0.8),
            )
    
            # 添加颜色条
            cbar = self.fig.colorbar(contourf, label="电势 (V)")
    
            # 优化坐标轴刻度
            self.optimize_ticks(ax, (-2.5, 2.5), (-2, 2))
    
            self.fig.tight_layout()
            self.canvas.draw()
    
        def get_function_label(self, func_str):
            """生成函数的LaTeX标签"""
            # 安全检查,防止恶意代码
            if any(
                word in func_str.lower() for word in ["import", "os", "sys", "subprocess"]
            ):
                raise ValueError("检测到不安全的代码")
    
            # 直接使用原始字符串,不再进行转义
            safe_str = func_str
            
            # 替换常见的数学函数
            replacements = {
                r'np\.sin\(([^)]+)\)': r'\sin(\1)',
                r'np\.cos\(([^)]+)\)': r'\cos(\1)',
                r'np\.tan\(([^)]+)\)': r'\tan(\1)',
                r'np\.exp\(([^)]+)\)': r'\exp(\1)',
                r'np\.sqrt\(([^)]+)\)': r'\sqrt{\1}',
                r'np\.log\(([^)]+)\)': r'\ln(\1)',
                r'np\.pi': r'\pi',
                r'\*\*': r'^',
                r'\*': r'\cdot',
            }
    
            # 应用所有替换,捕获可能的正则表达式错误
            for pattern, replacement in replacements.items():
                try:
                    safe_str = re.sub(pattern, replacement, safe_str)
                except re.error as e:
                    continue  # 跳过有问题的替换
    
            # 处理分数 - 更稳健的方法
            if '/' in safe_str:
                # 只替换不包含字母的分数表达式
                if re.search(r'\d+\.?\d*/\d+\.?\d*', safe_str):
                    parts = safe_str.split('/')
                    if len(parts) == 2:
                        numerator = parts[0].strip()
                        denominator = parts[1].strip()
                        safe_str = r'\frac{' + numerator + '}{' + denominator + '}'
    
            return safe_str
    
        def save_image(self):
            """保存当前图像"""
            try:
                filename = simpledialog.askstring(
                    "保存图像", "请输入文件名:", initialvalue="function_plot.png"
                )
                if filename:
                    if not filename.endswith(".png"):
                        filename += ".png"
                    self.fig.savefig(filename, dpi=150, bbox_inches="tight")
                    messagebox.showinfo(
                        "成功", f"图像已保存至: {os.path.abspath(filename)}"
                    )
            except Exception as e:
                messagebox.showerror("保存错误", f"保存图像时出错: {e}")
    
    
    def main():
        root = tk.Tk()
    
        # 设置样式
        style = ttk.Style()
        style.configure("TFrame", background="#f5f5f5")
        style.configure("TLabelframe", background="#ffffff", relief="sunken")
        style.configure(
            "TLabelframe.Label", background="#ffffff", font=("SimHei", 10, "bold")
        )
        style.configure("TButton", padding=5)
    
        # 尝试设置中文字体
        try:
            plt.rcParams["font.family"] = ["SimHei"]
        except:
            try:
                plt.rcParams["font.family"] = ["WenQuanYi Micro Hei"]
            except:
                try:
                    plt.rcParams["font.family"] = ["Heiti TC"]
                except:
                    try:
                        plt.rcParams["font.family"] = ["Arial Unicode MS"]
                    except:
                        plt.rcParams["font.family"] = ["DejaVu Sans", "sans-serif"]
                        print("警告: 未找到中文字体,图表文字可能无法正确显示")
    
        app = FunctionVisualizer(root)
        root.mainloop()
    
    
    if __name__ == "__main__":
        main()
    
    

    作者:灏瀚星空

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python数学可视化技术详解:交互式绘制显函数、隐函数及复杂曲线

    发表回复