放大 Plotly 热图

huangapple go评论64阅读模式
英文:

Zoom on a Plotly heatmap

问题

目前在Plotly.JS热力图中存在两种“缩放”行为:

  1. 在这里,您可以采用任何矩形形状进行缩放(单击、拖动和释放)。但像素不是正方形,这对某些应用程序来说不合适(宽高比未保持,有时应该保持宽高比):
const z = Array.from({length: 500}, () => Array.from({length: 100}, () => Math.floor(Math.random() * 255)));
Plotly.newPlot('plot', [{type: 'heatmap', z: z}], {});
  1. 在这里,像素是正方形,多亏了{'yaxis': {'scaleanchor': 'x'}},但然后只能使用特定宽高比的矩形形状进行缩放,这有时对用户体验/UI构成了限制:
const z = Array from({length: 500}, () => Array.from({length: 100}, () => Math.floor(Math.random() * 255)));
Plotly.newPlot('plot', [{type: 'heatmap', z: z}], {'yaxis': {'scaleanchor': 'x'}});

问题:如何实现两者兼顾,即您可以绘制任何形状的矩形选择缩放?并保持正方形像素?缩放的对象应该位于图中心(如果需要的话,水平或垂直留有空白)。

英文:

Currently there are 2 "zooming" behaviours in Plotly.JS heatmaps:

  1. Here you can take any rectangular shape for the zoom (click, drag and drop). But then the pixels are not square, which is not ok for some applications (the aspect ratio is not preserved, and sometimes it should be preserved):

    <!-- begin snippet: js hide: false console: true babel: false -->

    <!-- language: lang-js -->

     const z = Array.from({length: 500}, () =&gt; Array.from({length: 100}, () =&gt; Math.floor(Math.random() * 255)));
     Plotly.newPlot(&#39;plot&#39;, [{type: &#39;heatmap&#39;, z: z}], {});
    

    <!-- language: lang-html -->

     &lt;script src=&quot;https://cdn.plot.ly/plotly-2.16.2.min.js&quot;&gt;&lt;/script&gt;
     &lt;div id=&quot;plot&quot;&gt;&lt;/div&gt;
    

    <!-- end snippet -->

  2. Here the pixels are square thanks to {&#39;yaxis&#39;: {&#39;scaleanchor&#39;: &#39;x&#39;}}, but then you can zoom only with a certain aspect ratio rectangular shape, which is sometimes a limiting factor for the UX/UI:

    <!-- begin snippet: js hide: false console: true babel: false -->

    <!-- language: lang-js -->

     const z = Array.from({length: 500}, () =&gt; Array.from({length: 100}, () =&gt; Math.floor(Math.random() * 255)));
     Plotly.newPlot(&#39;plot&#39;, [{type: &#39;heatmap&#39;, z: z}], {&#39;yaxis&#39;: {&#39;scaleanchor&#39;: &#39;x&#39;}});
    

    <!-- language: lang-html -->

     &lt;script src=&quot;https://cdn.plot.ly/plotly-2.16.2.min.js&quot;&gt;&lt;/script&gt;
     &lt;div id=&quot;plot&quot;&gt;&lt;/div&gt;
    

    <!-- end snippet -->

Question: How to have both, i.e. you can draw a rectangle selection zoom of any shape? and keep square-shape pixels? The zoomed object should be centered in the plot (with horizontal or vertical white space if needed).

答案1

得分: 2

以下是翻译好的内容:

一种实现这一目标的方法是最初使用所需的 scaleratio 设置 scaleanchor 约束,以便在绘制图形后,我们可以计算出产生所需的像素到单位比例的受限制缩放范围比例,而不需要太多麻烦。

然后,我们可以移除约束并附加一个 plotly_relayout 事件处理程序,以在需要时进行调整。由于这些调整是通过调用 Plotly.relayout() 来精确进行的,我们通过条件块和仅考虑合理数量的有效数字来防止无限循环来比较范围比例。

如果重新布局后的比例与目标(受限制)比例不匹配,我们通过扩展一个轴范围(而不是缩小另一个轴)来进行调整,以使用户创建的缩放窗口相对于调整后的范围保持居中。

请注意:代码部分未进行翻译。

英文:

One way to do that is to initially set a scaleanchor constraint with the desired scaleratio, so that once the figure is plotted, we can compute the constrained zoom range ratio that produces the desired pixel to unit scaleratio without too much hassle.

Then, we can remove the constraint and attach a plotly_relayout event handler that will do the adjustments when necessary. Since those adjusments are precisely made by calling Plotly.relayout(), we prevent infinite loops with condition blocks and by considering only a reasonable amount of significant digits to compare the range ratios.

If the ratio after relayout don't match the target (contrained) ratio, we adjust it by expanding one of the axis range (rather than shrinking the other), keeping the user-created zoom window centered relative to the adjusted range.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const z = Array.from({length: 500}, () =&gt; Array.from({length: 100}, () =&gt; Math.floor(Math.random() * 255)));

const data = [{
  type: &#39;heatmap&#39;,
  z: z
}];

const layout = {
  xaxis: {
    constrain: &#39;range&#39;,
    constraintoward: &#39;center&#39;,
    scaleanchor: &quot;y&quot;,
    scaleratio: 1
  }
};

Plotly.newPlot(&#39;plot&#39;, data, layout).then(afterPlot);

function afterPlot(gd) {
  // Reference each axis range
  const xrange = gd._fullLayout.xaxis.range;
  const yrange = gd._fullLayout.yaxis.range;

  // Needed when resetting scale
  const xrange_init = [...xrange];
  const yrange_init = [...yrange];

  // Compute the actual zoom range ratio that produces the desired pixel to unit scaleratio
  const zw0 = Math.abs(xrange[1] - xrange[0]);
  const zh0 = Math.abs(yrange[1] - yrange[0]);
  const r0 = Number((zw0 / zh0).toPrecision(6));

  // Now we can remove the scaleanchor constraint
  // Nb. the update object references gd._fullLayout.&lt;x|y&gt;axis.range
  const update = {
    &#39;xaxis.range&#39;: xrange,
    &#39;yaxis.range&#39;: yrange,
    &#39;xaxis.scaleanchor&#39;: false,
    &#39;yaxis.scaleanchor&#39;: false
  };

  Plotly.relayout(gd, update);

  // Attach the handler that will do the adjustments after relayout if needed
  gd.on(&#39;plotly_relayout&#39;, relayoutHandler);

  function relayoutHandler(e) {
    if (e.width || e.height) {
      // The layout aspect ratio probably changed, need to reapply the initial
      // scaleanchor constraint and reset variables
      return unbindAndReset(gd, relayoutHandler);
    }

    if (e[&#39;xaxis.autorange&#39;] || e[&#39;yaxis.autorange&#39;]) {
      // Reset zoom range (dblclick or &quot;autoscale&quot; btn click)
      [xrange[0], xrange[1]] = xrange_init;
      [yrange[0], yrange[1]] = yrange_init;
      return Plotly.relayout(gd, update);
    }

    // Compute zoom range ratio after relayout
    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; // nothing to do
    }

    // ratios don&#39;t match, expand one of the axis range as necessary

    const [xmin, xmax] = getExtremes(gd, 0, &#39;x&#39;);
    const [ymin, ymax] = getExtremes(gd, 0, &#39;y&#39;);

    if (r1 &gt; r0) {
      const extra = (zh1 * r1/r0 - zh1) / 2;
      expandAxisRange(yrange, extra, ymin, ymax);
    }
    if (r1 &lt; r0) {
      const extra = (zw1 * r0/r1 - zw1) / 2;
      expandAxisRange(xrange, extra, xmin, xmax);
    }

    Plotly.relayout(gd, update);
  }
}

function unbindAndReset(gd, handler) {
  gd.removeListener(&#39;plotly_relayout&#39;, handler);

  // Careful here if you want to reuse the original `layout` (eg. could be
  // that you set specific ranges initially) because it has been passed by
  // reference to newPlot() and been modified since then.
  const _layout = {
    xaxis: {scaleanchor: &#39;y&#39;, 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 expandAxisRange(range, extra, min, max) {
  const reversed = range[0] &gt; range[1];
  if (reversed) {
    [range[0], range[1]] = [range[1], range[0]];
  }
  
  let shift = 0;
  if (range[0] - extra &lt; min) {
    const out = min - (range[0] - extra);
    const room = max - (range[1] + extra);
    shift = out &lt;= room ? out : (out + room) / 2;
  }
  else if (range[1] + extra &gt; max) {
    const out = range[1] + extra - max;
    const room = range[0] - extra - min;
    shift = out &lt;= room ? -out : -(out + room) / 2;
  }

  range[0] = range[0] - extra + shift;
  range[1] = range[1] + extra + shift;

  if (reversed) {
    [range[0], range[1]] = [range[1], range[0]];
  }
}

<!-- language: lang-html -->

&lt;script src=&quot;https://cdn.plot.ly/plotly-2.22.0.min.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;plot&quot;&gt;&lt;/div&gt;

<!-- end snippet -->

Nb. In the handler, except when checking if the user just reset the scale, we use references to gd._fullLayout.&lt;x|y&gt;axis.range rather than checking what contains e (the passed-in event object), because the references are always up-to-date and their structure never change, unlike the event parameter that only reflects what was updated. Also, because the update object itself refers these references, it allows to be a bit less verbose and just call Plotly.relayout(gd, update) after modifying the ranges.

答案2

得分: 1

你可以使用layout.xaxislayout.yaxis属性与scaleanchorscaleratio属性。

这里有一个示例代码片段:

        const z = Array.from({length: 500}, () => Array.from({length: 100}, () => Math.floor(Math.random() * 255)));
        
        Plotly.newPlot('plot', [{type: 'heatmap', z: z}], {
          margin: {t: 50}, // 添加一些顶部边距以使热图居中显示
          xaxis: { // 设置x轴属性
            scaleanchor: 'y', // 将比例锚定到y轴
            scaleratio: 1, // 设置比例系数为1以确保像素是正方形的
          },
          yaxis: { // 设置y轴属性
            scaleanchor: 'x', // 将比例锚定到x轴
            scaleratio: 1, // 设置比例系数为1以确保像素是正方形的
          },
          dragmode: 'select', // 启用矩形选择缩放
        });
        
        // 当发生缩放事件时更新图表
        document.getElementById('plot').on('plotly_selected', function(eventData) {
          const xRange = eventData.range.x;
          const yRange = eventData.range.y;
        
          Plotly.relayout('plot', {
            'xaxis.range': xRange, // 更新x轴范围
            'yaxis.range': yRange, // 更新y轴范围
          });
        });

首先,为热图定义z数据,并使用Plotly.newPlot创建图表。我们使用scaleanchor属性设置layout.xaxislayout.yaxis属性,将其设置为相反的轴,并将scaleratio属性设置为1,以确保像素是正方形的。

我们还将dragmode属性设置为'select'以启用矩形选择缩放。

最后,我们为plotly_selected事件添加了事件监听器,以使用Plotly.relayout更新xaxis.rangeyaxis.range属性,确保缩放对象在图表中居中显示,如果需要的话会有水平或垂直的空白空间。

希望这有所帮助。

英文:

You can use the layout.xaxis and layout.yaxis properties with the scaleanchor and scaleratio attributes.

Here's an example code snippet:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

    const z = Array.from({length: 500}, () =&gt; Array.from({length: 100}, () =&gt; Math.floor(Math.random() * 255)));
    
    Plotly.newPlot(&#39;plot&#39;, [{type: &#39;heatmap&#39;, z: z}], {
      margin: {t: 50}, // Add some top margin to center the heatmap
      xaxis: { // Set the x-axis properties
        scaleanchor: &#39;y&#39;, // Set the scale anchor to y-axis
        scaleratio: 1, // Set the scale ratio to 1 for square pixels
      },
      yaxis: { // Set the y-axis properties
        scaleanchor: &#39;x&#39;, // Set the scale anchor to x-axis
        scaleratio: 1, // Set the scale ratio to 1 for square pixels
      },
      dragmode: &#39;select&#39;, // Enable rectangular selection zoom
    });
    
    // Update the plot when a zoom event occurs
    document.getElementById(&#39;plot&#39;).on(&#39;plotly_selected&#39;, function(eventData) {
      const xRange = eventData.range.x;
      const yRange = eventData.range.y;
    
      Plotly.relayout(&#39;plot&#39;, {
        &#39;xaxis.range&#39;: xRange, // Update the x-axis range
        &#39;yaxis.range&#39;: yRange, // Update the y-axis range
      });
    });

<!-- language: lang-html -->

&lt;script src=&quot;https://cdn.plot.ly/plotly-2.16.2.min.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;plot&quot;&gt;&lt;/div&gt;

<!-- end snippet -->

First define the z data for the heatmap and create the plot using Plotly.newPlot. We set the xaxis and yaxis properties with the scaleanchor attribute set to the opposite axis, and the scaleratio attribute set to 1 to ensure square pixels.

We also set the dragmode property to 'select' to enable rectangular selection zoom.

Finally, we add an event listener to the plotly_selected event that updates the xaxis.range and yaxis.range properties to the selected zoom range using Plotly.relayout. This ensures that the zoomed object is centered in the plot with horizontal or vertical white space if needed.

I hope this helps.

huangapple
  • 本文由 发表于 2023年3月20日 23:51:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75792497.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定