You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Plotly.js如何绘制Quiver图/矢量场?求可行解决方案

Plotly.js 实现矢量场(Quiver)的替代方案

Plotly.js 原生确实没有内置的Quiver组件,不过可以通过以下几种方式手动实现:

1. 自定义箭头标记的散点图

利用散点图的自定义marker.path属性,将箭头形状定义为SVG路径,再根据矢量的方向和长度调整标记的角度与尺寸。示例代码:

// 示例数据:网格点坐标及对应矢量
const x = [0, 0, 1, 1];
const y = [0, 1, 0, 1];
const u = [1, 0.5, -0.5, -1]; // x方向分量
const v = [0.5, 1, -1, -0.5]; // y方向分量

// 计算每个矢量的角度(弧度转角度)和长度
const angles = u.map((ui, i) => Math.atan2(v[i], ui) * 180 / Math.PI);
const lengths = u.map((ui, i) => Math.sqrt(ui**2 + v[i]**2) * 20); // 缩放长度适配显示

// 定义箭头SVG路径
const arrowPath = "M 0,0 L 10,5 L 7,0 L 10,-5 Z";

Plotly.newPlot('graph', [{
  type: 'scatter',
  x: x,
  y: y,
  mode: 'markers',
  marker: {
    path: arrowPath,
    angle: angles,
    size: lengths,
    color: '#1f77b4'
  }
}], {
  title: '自定义箭头标记实现矢量场'
});

2. 线段+箭头头部组合

用线段绘制矢量的主体,再在矢量终点添加箭头标记,需要拆分数据为线段的起点-终点对:

const x = [0, 0, 1, 1];
const y = [0, 1, 0, 1];
const u = [1, 0.5, -0.5, -1];
const v = [0.5, 1, -1, -0.5];

// 生成线段数据:每个矢量对应两个点(起点、终点)
const lineX = [];
const lineY = [];
const arrowX = [];
const arrowY = [];
const arrowAngles = [];

x.forEach((xi, i) => {
  const yi = y[i];
  const endX = xi + u[i];
  const endY = yi + v[i];
  lineX.push(xi, endX, null); // null用于分隔不同线段
  lineY.push(yi, endY, null);
  arrowX.push(endX);
  arrowY.push(endY);
  arrowAngles.push(Math.atan2(v[i], u[i]) * 180 / Math.PI);
});

Plotly.newPlot('graph', [
  {
    type: 'scatter',
    x: lineX,
    y: lineY,
    mode: 'lines',
    line: { color: '#1f77b4' }
  },
  {
    type: 'scatter',
    x: arrowX,
    y: arrowY,
    mode: 'markers',
    marker: {
      path: "M 0,0 L 10,5 L 7,0 L 10,-5 Z",
      angle: arrowAngles,
      size: 12,
      color: '#1f77b4'
    }
  }
], {
  title: '线段+箭头头部实现矢量场'
});

3. 直接叠加SVG元素

如果需要更精细的控制,可以在Plotly图表容器中直接插入SVG元素,通过Plotly的坐标转换方法将数据坐标转为像素坐标:

const x = [0, 0, 1, 1];
const y = [0, 1, 0, 1];
const u = [1, 0.5, -0.5, -1];
const v = [0.5, 1, -1, -0.5];

Plotly.newPlot('graph', [{
  type: 'scatter',
  x: x,
  y: y,
  mode: 'markers',
  marker: { size: 0 } // 隐藏原始点,仅用于生成坐标轴范围
}]).then(() => {
  const graphDiv = document.getElementById('graph');
  const svg = graphDiv.querySelector('svg');

  x.forEach((xi, i) => {
    const yi = y[i];
    const startPixel = Plotly.toPixels([xi, yi], graphDiv);
    const endPixel = Plotly.toPixels([xi + u[i], yi + v[i]], graphDiv);
    
    // 绘制矢量线段
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('x1', startPixel[0]);
    line.setAttribute('y1', startPixel[1]);
    line.setAttribute('x2', endPixel[0]);
    line.setAttribute('y2', endPixel[1]);
    line.setAttribute('stroke', '#1f77b4');
    line.setAttribute('stroke-width', 2);
    
    // 绘制箭头头部
    const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
    const angle = Math.atan2(v[i], u[i]);
    const arrowSize = 8;
    const points = [
      `${endPixel[0]},${endPixel[1]}`,
      `${endPixel[0] - arrowSize*Math.cos(angle - Math.PI/6)},${endPixel[1] - arrowSize*Math.sin(angle - Math.PI/6)}`,
      `${endPixel[0] - arrowSize*Math.cos(angle + Math.PI/6)},${endPixel[1] - arrowSize*Math.sin(angle + Math.PI/6)}`
    ].join(' ');
    arrow.setAttribute('points', points);
    arrow.setAttribute('fill', '#1f77b4');
    
    svg.appendChild(line);
    svg.appendChild(arrow);
  });
});

以上三种方案各有优劣:第一种实现简单但矢量长度的缩放需要手动调整;第二种视觉效果更清晰,适合大密度矢量场;第三种灵活性最高,但需要处理坐标映射逻辑。

内容的提问来源于stack exchange,提问作者Carpetfizz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 11:24:36