
本文详解如何通过复用 Plotly 的 drawcircle 模式栏按钮,实现在热力图上单击任意像素即添加可交互的红色标注“图钉”,支持重复点击取消,无需绘制真实圆形,且完全兼容原生事件机制。
本文详解如何通过复用 plotly 的 `drawcircle` 模式栏按钮,实现在热力图上单击任意像素即添加可交互的红色标注“图钉”,支持重复点击取消,无需绘制真实圆形,且完全兼容原生事件机制。
在 Plotly 热力图(type: 'heatmap')交互场景中,除默认的矩形选择(select2d)外,用户常需快速定位并高亮单个数据点——例如调试异常值、标记 ROI 或构建交互式标注工具。但 Plotly 原生不提供“单点钉选”(pixel pinning)模式栏按钮,且当启用 drawcircle 等绘图工具时,plotly_click 事件会被屏蔽,导致无法捕获首次点击坐标。
核心思路:绕过工具模式限制,改用 plotly_click 事件 + 动态注解(annotation)模拟“图钉”行为。
关键在于:仅在用户处于 select 拖拽模式(即未激活任何绘图工具)时响应点击,并动态创建带箭头、背景与信息的注解,视觉上呈现为悬停于像素上方的红色图钉(类似 Google Maps 的 pin 样式)。
以下为完整可运行实现:
<script src="https://cdn.plot.ly/plotly-2.16.2.min.js"></script> <div id="plot" style="width: 600px; height: 500px;"></div>
const z = Array.from({ length: 50 }, () =>
Array.from({ length: 50 }, () => Math.floor(Math.random() * 255))
);
const data = [{
type: 'heatmap',
z: z,
colorscale: 'Viridis',
showscale: true
}];
const layout = {
yaxis: { scaleanchor: 'x', autorange: 'reversed' },
margin: { t: 30, b: 40, l: 40, r: 40 },
annotations: [] // 初始化空注解数组(可选)
};
const config = {
modeBarButtons: [
['zoom2d'],
['zoomIn2d'],
['zoomOut2d'],
['autoScale2d'],
['select2d'],
['drawcircle'] // 复用此按钮,但不实际绘圆
],
displaylogo: false,
displayModeBar: true,
editable: true
};
Plotly.newPlot('plot', data, layout, config).then(() => {
const plot = document.getElementById('plot');
plot.on('plotly_click', function(event) {
// ✅ 仅当处于 select 模式(非 drawcircle/pan 等)时响应
if (plot._fullLayout.dragmode === 'select') {
const point = event.points[0];
if (!point) return;
// 将数据坐标转换为布局坐标(用于 annotation 定位)
const xCoord = point.xaxis.d2l(point.x);
const yCoord = point.yaxis.d2l(point.y);
const newAnnotation = {
x: xCoord,
y: yCoord,
xref: 'x',
yref: 'y',
text: `<b>? Pin</b><br>x: ${point.x}<br>y: ${point.y}<br>z: ${point.z}`,
font: { size: 12, color: '#fff' },
bgcolor: 'rgba(220, 53, 69, 0.95)', // 深红底色,高对比度
bordercolor: '#dc3545',
borderwidth: 2,
borderpad: 6,
showarrow: true,
arrowhead: 6,
arrowsize: 1,
arrowwidth: 2,
ax: 0,
ay: -50, // 箭头向上指向像素中心
opacity: 0.98,
align: 'center'
};
const annotations = plot.layout.annotations || [];
const existingIndex = annotations.findIndex(
ann => ann.x === xCoord && ann.y === yCoord
);
// ? 重复点击同一位置 → 移除该标注(Toggle 行为)
if (existingIndex !== -1) {
Plotly.relayout('plot', `annotations[${existingIndex}]`, 'remove');
return;
}
// ➕ 添加新标注(追加到末尾)
const newIndex = annotations.length;
Plotly.relayout('plot', `annotations[${newIndex}]`, newAnnotation);
}
});
// ? 支持点击已存在标注直接删除(增强 UX)
plot.on('plotly_clickannotation', function(event, data) {
Plotly.relayout('plot', `annotations[${data.index}]`, 'remove');
});
});✅ 关键技术要点说明
- 坐标转换可靠性:使用 point.xaxis.d2l() 和 point.yaxis.d2l() 将数据索引(如 x=12, y=7)精确映射为布局像素坐标,确保标注始终精准锚定目标像素中心。
- 模式感知判断:通过 plot._fullLayout.dragmode === 'select' 过滤掉 drawcircle/pan 等工具激活状态,保证 plotly_click 在用户意图“选择”时才生效。
- 去重与 Toggle 逻辑:检查新点击位置是否已有同坐标标注,避免重复图钉;点击已有图钉则自动移除,符合直觉操作。
- 视觉优化:采用深红 (#dc3545) 高饱和底色 + 白色文字 + 向上箭头,形成醒目的“图钉”效果;opacity 和 borderpad 提升可读性与层次感。
- 事件解耦:plotly_clickannotation 事件独立监听,允许用户直接点击图钉删除,无需切换工具模式。
⚠️ 注意事项
- 此方案不修改 Plotly 内部工具逻辑,完全基于公开 API,兼容 v2.16+ 版本;
- 若需支持多图钉批量管理(如导出坐标列表),建议维护一个外部 pinnedPoints = [] 数组,同步更新 Plotly.relayout 调用;
- 热力图若启用 zsmooth: 'best' 或插值,像素物理尺寸可能变化,但 d2l() 转换仍保证逻辑坐标精准;
- 移动端需额外测试触摸事件兼容性(plotly_click 在现代移动端浏览器中表现良好)。
通过以上实现,你已拥有了一个轻量、稳定、专业级的热力图单像素钉选工具——无需后端、零依赖,开箱即用。










