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

如何让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>

关键修改说明

  1. 记录选区中心:在relayoutHandler中,先保存用户框选的原始范围,计算出选区的X/Y中心坐标。
  2. 以中心为基准扩展范围:当需要调整轴范围以保持方形像素时,不再从原范围两端扩展,而是以选区中心为中点,计算新的轴范围,确保选区始终居中。
  3. 边界处理优化:新增adjustRangeToBounds函数,当扩展后的范围超出数据边界时,整体平移范围,避免截断数据同时尽可能保持选区居中。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 05:05:59