为何Matplotlib绘制的函数切线线段长度不一致?
尝试绘制衰减正弦函数图像,并在特定点添加代表梯度的切线线段,但视觉上线段长度各不相同。计算时打印的线段长度为1,但理论上期望线段长度为2(因为向两侧延伸)。
初始绘制代码:
import numpy as np import matplotlib.pyplot as plt # create a decaying sinewave, 8 cycles with decaying amplitude t = np.linspace(0, 8, 1000) decaying_sine = np.sin(t * np.pi) * np.exp(-t * np.pi / 4) plt.plot(t, decaying_sine)
切线绘制函数及调用:
def plot_graph_tangent_at_time(graph, t, t_index, label='slope'): # Get the slope of graph at that location slope = np.gradient(graph, t)[t_index] tangent = np.arctan(1/slope) line_length = 1 line_w = np.sin(tangent) * line_length line_h = np.cos(tangent) * line_length # Add a line to the graph plt.scatter(t[t_index], graph[t_index], color='C1', linewidth=2) plt.plot([t[t_index] - line_w, t[t_index] + line_w], [graph[t_index] - line_h, graph[t_index] + line_h], 'C1--', label=label) # 调用绘制切线 plot_graph_tangent_at_time(decaying_sine, t, 100) plot_graph_tangent_at_time(decaying_sine, t, 165) plot_graph_tangent_at_time(decaying_sine, t, 500) plot_graph_tangent_at_time(decaying_sine, t, 700) plt.legend() plt.show()
添加的长度验证代码:
# recalculate the length of the line line_length = np.sqrt(line_w**2 + line_h**2) print(line_length)
打印结果为1,但期望线段总长度为2,且视觉上各线段长短不一。
1. 视觉长度不一致的根本原因
Matplotlib默认采用非等比例坐标轴,x轴和y轴上的单位长度对应的屏幕像素数不同。你计算的是数学上的欧氏长度,但屏幕显示时,x、y方向的缩放比例不同,导致同样数学长度的线段,视觉上像素长度差异明显。
2. 线段长度计算的逻辑偏差
你打印的line_length = 1是单侧线段的长度:代码中line_w和line_h是从中心点向单方向(左/下)延伸的分量,整个切线是向两侧各延伸一段,所以总数学长度应为2 * np.sqrt(line_w² + line_h²) = 2,你只计算了单侧长度,因此打印结果为1,这是计算逻辑的误解。
3. 修正方案
方案A:强制坐标轴等比例(简单直接)
在plt.show()前添加代码,让x、y轴单位长度的像素占比一致:
plt.gca().set_aspect('equal', adjustable='box')
这样所有数学长度相同的切线,视觉上就会一样长,但可能会拉伸原函数图像,破坏其原本的比例展示。
方案B:根据坐标轴缩放调整线段分量(推荐,不影响原图比例)
修改切线函数,根据当前坐标轴的像素缩放比例计算线段增量,确保所有切线的视觉长度一致:
def plot_graph_tangent_at_time(graph, t, t_index, label='slope', visual_pixel_length=25): ax = plt.gca() slope = np.gradient(graph, t)[t_index] # 获取坐标轴的像素-单位转换比例 x_range = ax.get_xlim()[1] - ax.get_xlim()[0] y_range = ax.get_ylim()[1] - ax.get_ylim()[0] x_pix_per_unit = ax.bbox.width / x_range y_pix_per_unit = ax.bbox.height / y_range # 计算x、y方向的单侧增量,确保视觉长度为设定的像素值 dx = visual_pixel_length / np.sqrt(x_pix_per_unit**2 + (slope * y_pix_per_unit)**2) dy = slope * dx plt.scatter(t[t_index], graph[t_index], color='C1', linewidth=2) plt.plot([t[t_index]-dx, t[t_index]+dx], [graph[t_index]-dy, graph[t_index]+dy], 'C1--', label=label)
其中visual_pixel_length是你想要的切线视觉长度(单位:像素),调整这个值可以统一所有切线的显示长短,且不会改变原函数图像的比例。
方案C:修正数学长度计算逻辑
如果只需要保证切线的数学长度一致(不管视觉),可以简化切线分量的计算,避免用arctan(1/slope)的绕弯方式:
def plot_graph_tangent_at_time(graph, t, t_index, label='slope', half_line_length=1): slope = np.gradient(graph, t)[t_index] # 计算单侧的x、y增量,确保单侧数学长度为half_line_length dx = half_line_length / np.sqrt(1 + slope**2) dy = slope * dx plt.scatter(t[t_index], graph[t_index], color='C1', linewidth=2) plt.plot([t[t_index]-dx, t[t_index]+dx], [graph[t_index]-dy, graph[t_index]+dy], 'C1--', label=label)
这里half_line_length是单侧切线的数学长度,整个切线的总数学长度为2 * half_line_length,若要视觉一致,需搭配方案A的等比例坐标轴设置。
内容的提问来源于stack exchange,提问作者Danny Staple

