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
相关产品推荐
相关产品推荐

