<think>我们计划使用Python的Tkinter来创建图形界面,使用matplotlib进行绘图。
功能要求:
1. 读取三个文件,分别包含x, y, z三个方向的速度值。注意:每个文件除了包含坐标外,还包含一个方向的速度值(但根据示例,每个文件有5列:节点编号,x坐标,y坐标,z坐标,以及一个方向的速度值)。因此,三个文件分别提供了x方向速度、y方向速度、z方向速度。
2. 绘制指定平面(如XY平面、XZ平面、YZ平面)的二维速度矢量图。
3. 指定范围(坐标范围)来显示图形。
4. 箭头的颜色代表速度大小(即速度矢量的模),箭头的长度相同(归一化长度,只表示方向,大小用颜色表示)。
5. 可以选择箭头的疏密程度(即每隔几个点取一个点来画箭头)。
步骤:
1. 读取三个文件,将数据合并为一个DataFrame(使用pandas)或合并为三个数组(x,y,z坐标和三个方向的速度)。
注意:三个文件的节点编号应该是对应的,所以我们可以按节点编号合并。
2. 用户选择要绘制的平面(例如XY平面,那么我们就忽略z坐标,用x和y坐标,以及x和y方向的速度,z方向速度忽略。但速度大小是三个方向速度的模)。
3. 用户指定坐标范围(xmin, xmax, ymin, ymax)来过滤数据点。
4. 用户设置疏密程度(例如每隔n个点取一个点),我们可以通过设置步长来实现。
5. 绘制二维矢量图(quiver),设置箭头长度固定(例如单位长度),颜色根据速度大小(sqrt(vx^2+vy^2+vz^2))来映射。
6. 添加颜色条。
7. 使用Tkinter构建界面,包含文件选择、平面选择、范围输入、疏密程度输入、绘图按钮等。
由于数据量可能较大,我们需要注意性能,但这里先以实现功能为主。
我们将按照以下步骤编写代码:
- 导入必要的库:tkinter, matplotlib, pandas, numpy
- 创建主窗口
- 添加文件选择按钮(三个文件)
- 添加平面选择的下拉菜单(XY, XZ, YZ)
- 添加范围输入框(xmin, xmax, ymin, ymax)注意:根据平面不同,范围对应的坐标轴不同
- 添加疏密程度输入框(步长)
- 添加绘图按钮,点击后根据输入绘制图形
注意:由于数据文件格式为CSV(逗号分隔),我们可以用pandas读取。
示例数据格式:
nodenumber, x-coordinate, y-coordinate, z-coordinate, x-velocity
我们读取时注意列名,但列名可能不一致,我们假设第一列为节点编号,第二列为x坐标,第三列为y坐标,第四列为z坐标,第五列为速度值(分别对应x、y、z方向的速度文件)。
但是,三个文件分别代表x,y,z方向的速度,所以每个文件的第五列分别是vx, vy, vz。
因此,我们需要读取三个文件,并合并成一个数据集,包含节点编号(可能不需要),x,y,z坐标(取其中一个文件的坐标即可,因为同一个节点在不同文件中的坐标应该相同),以及三个方向的速度。
步骤:
1. 读取x方向速度文件,得到:节点编号,x坐标,y坐标,z坐标,vx
2. 读取y方向速度文件,得到:节点编号,x坐标,y坐标,z坐标,vy(注意:这里我们只取第五列作为vy,因为坐标应该相同)
3. 读取z方向速度文件,得到:节点编号,x坐标,y坐标,z坐标,vz
然后,我们可以将三个文件合并,只保留一个坐标(比如用x方向文件的坐标),然后分别加上vy和vz列。
但是,为了确保节点对应,我们按节点编号合并。或者,我们也可以按坐标合并(但坐标可能有浮点误差,所以按节点编号更安全)。
因此,我们读取三个文件后,分别提取节点编号和速度列,然后合并到一个DataFrame中。
具体:
df_x = pd.read_csv(file_x, ...)
df_y = pd.read_csv(file_y, ...)
df_z = pd.read_csv(file_z, ...)
# 合并,只保留一个坐标
df = df_x[['nodenumber', 'x-coordinate', 'y-coordinate', 'z-coordinate']].copy()
df['vx'] = df_x['x-velocity']
df['vy'] = df_y['y-velocity'] # 注意:列名可能不同,这里假设列名是固定的。但实际文件中列名可能有空格,我们读取时可能需要处理列名。
观察示例数据,列名有空格,我们可以使用`skipinitialspace=True`来避免列名中的多余空格。
另外,数据中使用了科学计数法,pandas可以自动识别。
计划:
- 读取每个文件时,设置`skipinitialspace=True`,这样列名会去掉多余空格。
- 然后合并。
但是,三个文件中的节点编号应该相同且顺序一致,所以我们可以直接按行合并,但为了安全,还是按节点编号合并。
代码步骤:
1. 读取三个文件,使用pandas,并规范列名(去掉多余空格)。
2. 将三个DataFrame按节点编号合并(merge),但这样会重复坐标列,所以我们可以只保留一个坐标。
3. 或者,我们假设三个文件节点顺序和数量完全一致,那么我们可以直接:
df = df_x[['nodenumber', 'x-coordinate', 'y-coordinate', 'z-coordinate']]
df['vx'] = df_x['x-velocity']
df['vy'] = df_y['y-velocity'] # 这里注意,df_y的列名可能不同,但我们可以用列索引或者列名
df['vz'] = df_z['z-velocity']
4. 但是,为了确保列名正确,我们可以统一列名,或者使用列索引(即第5列)。
由于数据格式一致,我们可以用相同的列索引。但是,为了健壮性,我们假设列名是固定的,即:
第一列:'nodenumber'
第二列:'x'
第三列:'y'
第四列:'z'
第五列:'velocity'
但实际列名有长有短,我们可以读取时指定列名,或者使用位置索引(iloc)。
这里,我们读取时不指定列名,使用默认的列名,然后重新命名列。
另一种方法:读取时指定列名,跳过第一行(如果第一行是标题),或者第一行不是标题?示例数据第一行是标题。
根据示例,第一行是标题,所以我们读取时保留标题,但用skipinitialspace=True去掉多余空格。
然后,我们统一命名列,比如:
df_x.columns = ['nodenumber', 'x', 'y', 'z', 'vx']
df_y.columns = ['nodenumber', 'x', 'y', 'z', 'vy'] # 但是注意,df_y的第五列是vy,所以列名改为vy
df_z.columns = ['nodenumber', 'x', 'y', 'z', 'vz']
然后合并:
df = pd.DataFrame()
df['nodenumber'] = df_x['nodenumber']
df['x'] = df_x['x']
df['y'] = df_x['y']
df['z'] = df_x['z']
df['vx'] = df_x['vx']
df['vy'] = df_y['vy'] # 直接按顺序取,因为节点顺序一致
df['vz'] = df_z['vz']
5. 计算速度大小:speed = np.sqrt(df['vx']**2 + df['vy']**2 + df['vz']**2)
6. 根据用户选择的平面,确定绘图的坐标和速度分量:
平面为XY平面:坐标x,y,速度分量vx, vy
平面为XZ平面:坐标x,z,速度分量vx, vz
平面为YZ平面:坐标y,z,速度分量vy, vz
7. 根据用户输入的范围过滤数据点。
8. 根据疏密程度(步长)来采样数据点。
9. 用matplotlib的quiver画图,设置arrow的长度为固定值(例如1),颜色为速度大小(注意:这里的速度大小是三维速度的模,但也可以根据平面上的速度分量?题目要求颜色代表速度大小,应该是三维速度的模,所以不变)。
10. 添加颜色条。
由于界面构建代码较长,我们将分步骤实现。
注意:Tkinter界面中,我们使用matplotlib的嵌入,使用FigureCanvasTkAgg。
由于代码较长,我们逐步实现。
首先,我们创建一个类来封装应用。
以下为代码框架:
导入模块:
import tkinter as tk
from tkinter import filedialog, ttk
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
然后创建主应用类。
由于代码较长,我们只给出关键部分。</think>以下是一个使用Python编写的脚本,它使用Tkinter创建图形界面,读取三个方向的速度文件,并绘制指定平面和范围内的二维速度矢量图。箭头的颜色代表速度大小,长度固定,且可以调整疏密程度。
```python
import tkinter as tk
from tkinter import ttk, filedialog
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class VelocityVectorPlotter:
def __init__(self, root):
self.root = root
self.root.title("3D Velocity Vector Plotter")
self.root.geometry("1000x800")
# 初始化数据存储
self.data = None
self.plane = "XY"
self.x_range = [0, 1]
self.y_range = [0, 1]
self.density = 1
# 创建UI框架
self.create_widgets()
def create_widgets(self):
# 文件选择区域
file_frame = ttk.LabelFrame(self.root, text="文件选择")
file_frame.pack(fill="x", padx=10, pady=5)
ttk.Button(file_frame, text="选择X方向速度文件",
command=lambda: self.load_file("vx")).grid(row=0, column=0, padx=5, pady=5)
ttk.Button(file_frame, text="选择Y方向速度文件",
command=lambda: self.load_file("vy")).grid(row=0, column=1, padx=5, pady=5)
ttk.Button(file_frame, text="选择Z方向速度文件",
command=lambda: self.load_file("vz")).grid(row=0, column=2, padx=5, pady=5)
# 参数设置区域
param_frame = ttk.LabelFrame(self.root, text="绘图参数")
param_frame.pack(fill="x", padx=10, pady=5)
# 平面选择
ttk.Label(param_frame, text="投影平面:").grid(row=0, column=0, padx=5, pady=5)
self.plane_var = tk.StringVar(value="XY")
plane_combo = ttk.Combobox(param_frame, textvariable=self.plane_var,
values=["XY", "XZ", "YZ"], width=10)
plane_combo.grid(row=0, column=1, padx=5, pady=5)
# X范围
ttk.Label(param_frame, text="X范围 (min:max):").grid(row=0, column=2, padx=5, pady=5)
self.x_min_var = tk.StringVar(value="0.0")
self.x_max_var = tk.StringVar(value="1.0")
ttk.Entry(param_frame, textvariable=self.x_min_var, width=8).grid(row=0, column=3, padx=2)
ttk.Label(param_frame, text=":").grid(row=0, column=4)
ttk.Entry(param_frame, textvariable=self.x_max_var, width=8).grid(row=0, column=5, padx=2)
# Y范围
ttk.Label(param_frame, text="Y范围 (min:max):").grid(row=1, column=2, padx=5, pady=5)
self.y_min_var = tk.StringVar(value="0.0")
self.y_max_var = tk.StringVar(value="1.0")
ttk.Entry(param_frame, textvariable=self.y_min_var, width=8).grid(row=1, column=3, padx=2)
ttk.Label(param_frame, text=":").grid(row=1, column=4)
ttk.Entry(param_frame, textvariable=self.y_max_var, width=8).grid(row=1, column=5, padx=2)
# 密度控制
ttk.Label(param_frame, text="箭头密度 (1-10):").grid(row=0, column=6, padx=5, pady=5)
self.density_var = tk.IntVar(value=5)
density_scale = ttk.Scale(param_frame, from_=1, to=10, variable=self.density_var,
orient="horizontal", length=150)
density_scale.grid(row=0, column=7, padx=5)
# 绘图按钮
ttk.Button(param_frame, text="生成矢量图", command=self.plot_data).grid(
row=0, column=8, padx=10, pady=5, rowspan=2)
# 绘图区域
fig_frame = ttk.Frame(self.root)
fig_frame.pack(fill="both", expand=True, padx=10, pady=5)
self.fig = Figure(figsize=(8, 6), dpi=100)
self.canvas = FigureCanvasTkAgg(self.fig, master=fig_frame)
self.canvas.get_tk_widget().pack(fill="both", expand=True)
# 状态栏
self.status = ttk.Label(self.root, text="请选择三个方向的速度文件", relief="sunken")
self.status.pack(fill="x", padx=10, pady=5, side="bottom")
def load_file(self, direction):
file_path = filedialog.askopenfilename(title=f"选择{direction}方向速度文件")
if not file_path:
return
try:
# 读取CSV文件,跳过空格并处理科学计数法
df = pd.read_csv(file_path, skipinitialspace=True, float_precision="high")
if self.data is None:
self.data = {
"x": df.iloc[:, 1].values,
"y": df.iloc[:, 2].values,
"z": df.iloc[:, 3].values,
"vx": np.zeros(len(df)),
"vy": np.zeros(len(df)),
"vz": np.zeros(len(df))
}
# 根据方向存储速度分量
if direction == "vx":
self.data["vx"] = df.iloc[:, 4].values
elif direction == "vy":
self.data["vy"] = df.iloc[:, 4].values
elif direction == "vz":
self.data["vz"] = df.iloc[:, 4].values
self.status.config(text=f"已加载{direction}方向文件: {file_path.split('/')[-1]}")
except Exception as e:
self.status.config(text=f"错误: {str(e)}")
def plot_data(self):
if self.data is None:
self.status.config(text="错误: 请先加载所有三个方向的速度文件")
return
try:
# 获取参数
self.plane = self.plane_var.get()
self.x_range = [float(self.x_min_var.get()), float(self.x_max_var.get())]
self.y_range = [float(self.y_min_var.get()), float(self.y_max_var.get())]
self.density = int(self.density_var.get())
# 计算速度大小
speed = np.sqrt(self.data["vx"]**2 + self.data["vy"]**2 + self.data["vz"]**2)
# 根据平面选择坐标和速度分量
if self.plane == "XY":
x, y = self.data["x"], self.data["y"]
u, v = self.data["vx"], self.data["vy"]
elif self.plane == "XZ":
x, y = self.data["x"], self.data["z"]
u, v = self.data["vx"], self.data["vz"]
else: # YZ
x, y = self.data["y"], self.data["z"]
u, v = self.data["vy"], self.data["vz"]
# 过滤指定范围内的点
mask = (x >= self.x_range[0]) & (x <= self.x_range[1]) & \
(y >= self.y_range[0]) & (y <= self.y_range[1])
x = x[mask]
y = y[mask]
u = u[mask]
v = v[mask]
speed = speed[mask]
# 根据密度采样
step = max(1, len(x) // (100 * self.density))
indices = np.arange(0, len(x), step)
x = x[indices]
y = y[indices]
u = u[indices]
v = v[indices]
speed = speed[indices]
# 归一化箭头长度(保持方向但长度固定)
norm = np.sqrt(u**2 + v**2)
u_norm = np.where(norm > 0, u/norm * 0.02, 0) # 固定长度0.02
v_norm = np.where(norm > 0, v/norm * 0.02, 0)
# 创建绘图
self.fig.clf()
ax = self.fig.add_subplot(111)
# 绘制矢量图(颜色表示速度大小)
quiver = ax.quiver(x, y, u_norm, v_norm, speed,
cmap="viridis", scale=None, scale_units="inches",
angles="xy", width=0.003, headwidth=3)
# 添加颜色条
cbar = self.fig.colorbar(quiver, ax=ax)
cbar.set_label("速度大小 (m/s)")
# 设置坐标轴标签
ax.set_xlabel(f"{self.plane[0]}-坐标 (m)")
ax.set_ylabel(f"{self.plane[1]}-坐标 (m)")
ax.set_title(f"{self.plane}平面速度矢量图")
ax.set_xlim(self.x_range)
ax.set_ylim(self.y_range)
ax.grid(True, linestyle="--", alpha=0.6)
self.canvas.draw()
self.status.config(text=f"成功绘制 {len(x)} 个箭头 (密度级别: {self.density})")
except Exception as e:
self.status.config(text=f"绘图错误: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = VelocityVectorPlotter(root)
root.mainloop()
```
### 代码解释
1. **GUI界面设计**:
- 使用Tkinter创建主窗口,包含文件选择区、参数设置区、绘图区和状态栏
- 文件选择按钮:分别加载X/Y/Z三个方向的速度文件
- 参数设置:平面选择(XY/XZ/YZ)、坐标范围、箭头密度滑块(1-10级)
- 绘图区域:使用Matplotlib的FigureCanvasTkAgg嵌入Tkinter
2. **数据处理流程**:
- 文件读取:使用Pandas读取CSV文件,处理科学计数法(`float_precision="high"`)
- 数据合并:将三个文件的速度分量合并到统一的数据结构中
- 速度计算:三维速度大小 $|\vec{v}| = \sqrt{v_x^2 + v_y^2 + v_z^2}$
- 平面投影:根据用户选择的平面提取坐标和速度分量
- 范围过滤:仅保留指定坐标范围内的数据点
- 密度控制:按步长采样数据点(密度级别1最密,10最疏)
3. **矢量图绘制**:
- 箭头归一化:固定箭头长度(0.02),只保留方向信息
- 颜色映射:使用Viridis色图表示速度大小
- 动态更新:每次绘图后刷新Canvas并更新状态栏
4. **错误处理**:
- 文件加载错误检测
- 参数格式验证
- 数据范围检查
### 使用说明
1. 点击三个按钮分别加载X/Y/Z方向的速度文件
2. 选择投影平面(默认XY平面)
3. 设置坐标范围(默认0.0-1.0)
4. 调整密度滑块(1=最密,10=最疏)
5. 点击"生成矢量图"按钮绘制图形
6. 颜色条显示速度大小,所有箭头长度相同
### 示例数据格式
程序支持以下格式的CSV文件(列名不重要,按位置解析):
```
nodenumber, x-coordinate, y-coordinate, z-coordinate, x-velocity
1, 9.599838406E-02,-4.000161588E-02,-3.999999911E-02, 0.000000000E+00
2, 9.800061438E-02,-4.000161563E-02,-4.000000000E-02, 0.000000000E+00
...
```