如何让Plotly.js热力图框选缩放后选区居中?
实现Plotly热力图矩形缩放后选区居中的标准UX
现有代码可创建满足以下特性的Plotly热力图:
- 像素为方形
- 支持任意矩形区域缩放(不受热力图宽高比限制)
- 包含多层轨迹(trace)
但当前实现存在问题:框选矩形区域缩放后,选区并未在图表中居中,不符合Matplotlib、Photoshop等软件通用的标准用户体验。标准UX要求:
- 缩放后的选区在x轴方向居中
- 缩放后的选区在y轴方向居中

解决方案
核心调整是在缩放逻辑中以用户框选的选区中心为基准,均匀扩展轴范围,确保缩放后选区始终处于图表中心。修改后的完整代码如下:
JavaScript 代码
const z = Array.from({ length: 50 }, () => Array.from({ length: 20 }, () => Math.floor(Math.random() * 255))); const z2 = [[[255, 0, 0], [0, 255, 255]], [[0, 0, 255], [255, 0, 255]]]; const data = [{ type: "image", z: z2 }, { type: "heatmap", z: z, opacity: 0.3 }]; const layout = { xaxis: { constrain: 'range', constraintoward: 'center', scaleanchor: "y", scaleratio: 1, zeroline: false, showgrid: false }, yaxis: { constrain: 'range', constraintoward: 'center', showgrid: false, zeroline: false } }; Plotly.newPlot('plot', data, layout).then(afterPlot); function afterPlot(gd) { const xrange = gd._fullLayout.xaxis.range; const yrange = gd._fullLayout.yaxis.range; const xrange_init = [...xrange]; const yrange_init = [...yrange]; const zw0 = Math.abs(xrange[1] - xrange[0]); const zh0 = Math.abs(yrange[1] - yrange[0]); const r0 = Number((zw0 / zh0).toPrecision(6)); const update = { 'xaxis.range': xrange, 'yaxis.range': yrange, 'xaxis.scaleanchor': false, 'yaxis.scaleanchor': false }; Plotly.relayout(gd, update); gd.on('plotly_relayout', relayoutHandler); function relayoutHandler(e) { if (e.width || e.height) { return unbindAndReset(gd, relayoutHandler); } if (e['xaxis.autorange'] || e['yaxis.autorange']) { [xrange[0], xrange[1]] = xrange_init; [yrange[0], yrange[1]] = yrange_init; return Plotly.relayout(gd, update); } // 记录用户框选的原始范围及中心 const originalXRange = [...xrange]; const originalYRange = [...yrange]; const xCenter = (originalXRange[0] + originalXRange[1]) / 2; const yCenter = (originalYRange[0] + originalYRange[1]) / 2; const zw1 = Math.abs(xrange[1] - xrange[0]); const zh1 = Math.abs(yrange[1] - yrange[0]); const r1 = Number((zw1 / zh1).toPrecision(6)); if (r1 === r0) { return; } const [xmin, xmax] = getExtremes(gd, 1, 'x'); const [ymin, ymax] = getExtremes(gd, 1, 'y'); if (r1 > r0) { const newYRangeLength = zh1 * r1 / r0; const halfLength = newYRangeLength / 2; // 以原选区Y中心为基准设置新范围 yrange[0] = yCenter - halfLength; yrange[1] = yCenter + halfLength; // 处理超出数据边界的情况 adjustRangeToBounds(yrange, ymin, ymax); } if (r1 < r0) { const newXRangeLength = zw1 * r0 / r1; const halfLength = newXRangeLength / 2; // 以原选区X中心为基准设置新范围 xrange[0] = xCenter - halfLength; xrange[1] = xCenter + halfLength; // 处理超出数据边界的情况 adjustRangeToBounds(xrange, xmin, xmax); } Plotly.relayout(gd, update); } } function unbindAndReset(gd, handler) { gd.removeListener('plotly_relayout', handler); const _layout = { xaxis: {scaleanchor: 'y', scaleratio: 1, autorange: true}, yaxis: {autorange: true} }; return Plotly.relayout(gd, _layout).then(afterPlot); } function getExtremes(gd, traceIndex, axisId) { const extremes = gd._fullData[traceIndex]._extremes[axisId]; return [extremes.min[0].val, extremes.max[0].val]; } // 调整范围,确保不超出数据的最小/最大值 function adjustRangeToBounds(range, min, max) { const reversed = range[0] > range[1]; if (reversed) { [range[0], range[1]] = [range[1], range[0]]; } if (range[0] < min) { const shift = min - range[0]; range[0] += shift; range[1] += shift; } if (range[1] > max) { const shift = max - range[1]; range[0] += shift; range[1] += shift; } if (reversed) { [range[0], range[1]] = [range[1], range[0]]; } }
HTML 代码
<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script> <div id="plot"></div>
关键修改说明
- 记录选区中心:在
relayoutHandler中,先保存用户框选的原始范围,计算出选区的X/Y中心坐标。 - 以中心为基准扩展范围:当需要调整轴范围以保持方形像素时,不再从原范围两端扩展,而是以选区中心为中点,计算新的轴范围,确保选区始终居中。
- 边界处理优化:新增
adjustRangeToBounds函数,当扩展后的范围超出数据边界时,整体平移范围,避免截断数据同时尽可能保持选区居中。
内容的提问来源于stack exchange,提问作者Basj
相关产品推荐
相关产品推荐

