保留方形像素+使用 Plotly.js 进行任意矩形缩放的能力,并绘制 2 层图

问题描述 投票:0回答:1

Zoom on a Plotly heatmap 中,接受的答案提供了一种缩放热图的方法:

  • 有平方像素(必需)
  • 允许任何矩形缩放(并且停留在热图的纵横比上)。

如果我们有一个包含 2 层的 Plotly.js 图:1 个“热图”和 1 个“图像”,这将停止工作:在下面的代码片段中,缩放功能卡在热图的原始长宽比上,我不这样做想要:

如何允许自由矩形缩放?

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: { scaleanchor: "y", scaleratio: 1 } }; 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 = xrange[1] - xrange[0]; const zh0 = yrange[1] - yrange[0]; const r0 = Number((zw0 / zh0).toPrecision(6)); const update = { "xaxis.range": xrange, "yaxis.range": yrange, "xaxis.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 zw1 = xrange[1] - xrange[0]; const zh1 = yrange[1] - yrange[0]; const r1 = Number((zw1 / zh1).toPrecision(6)); if (r1 === r0) { return; } const [xmin, xmax] = getExtremes(gd, 0, "x"); const [ymin, ymax] = getExtremes(gd, 0, "y"); if (r1 > r0) { const extra = ((zh1 * r1) / r0 - zh1) / 2; expandAxisRange(yrange, extra, ymin, ymax); } if (r1 < r0) { const extra = ((zw1 * r0) / r1 - zw1) / 2; expandAxisRange(xrange, extra, xmin, xmax); } Plotly.relayout(gd, update); } } function unbindAndReset(gd, handler) { gd.removeListener("plotly_relayout", handler); return Plotly.relayout(gd, { xaxis: { scaleanchor: "y", scaleratio: 1, autorange: true }, yaxis: { autorange: true } }).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) { let shift = 0; if (range[0] - extra < min) { const out = min - (range[0] - extra); const room = max - (range[1] + extra); shift = out <= room ? out : (out + room) / 2; } else if (range[1] + extra > max) { const out = range[1] + extra - max; const room = range[0] - extra - min; shift = out <= room ? -out : -(out + room) / 2; } range[0] = range[0] - extra + shift; range[1] = range[1] + extra + shift; }
<script src="https://cdn.plot.ly/plotly-2.22.0.min.js"></script>
<div id="plot"></div>


编辑:更新为 2.26.0:

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: { scaleanchor: "y", scaleratio: 1 }, yaxis: { scaleanchor: 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 = xrange[1] - xrange[0]; const zh0 = yrange[1] - yrange[0]; const r0 = Number((zw0 / zh0).toPrecision(6)); const update = { "xaxis.range": xrange, "yaxis.range": yrange, "xaxis.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 zw1 = xrange[1] - xrange[0]; const zh1 = yrange[1] - yrange[0]; const r1 = Number((zw1 / zh1).toPrecision(6)); if (r1 === r0) { return; } const [xmin, xmax] = getExtremes(gd, 0, "x"); const [ymin, ymax] = getExtremes(gd, 0, "y"); if (r1 > r0) { const extra = ((zh1 * r1) / r0 - zh1) / 2; expandAxisRange(yrange, extra, ymin, ymax); } if (r1 < r0) { const extra = ((zw1 * r0) / r1 - zw1) / 2; expandAxisRange(xrange, extra, xmin, xmax); } Plotly.relayout(gd, update); } } function unbindAndReset(gd, handler) { gd.removeListener("plotly_relayout", handler); return Plotly.relayout(gd, { xaxis: { scaleanchor: "y", scaleratio: 1, autorange: true }, yaxis: { autorange: true } }).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) { let shift = 0; if (range[0] - extra < min) { const out = min - (range[0] - extra); const room = max - (range[1] + extra); shift = out <= room ? out : (out + room) / 2; } else if (range[1] + extra > max) { const out = range[1] + extra - max; const room = range[0] - extra - min; shift = out <= room ? -out : -(out + room) / 2; } range[0] = range[0] - extra + shift; range[1] = range[1] + extra + shift; }
<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
<div id="plot"></div>

javascript plotly plotly.js
1个回答
2
投票

文档指出:

默认情况下,当图像显示在子图中时,其 y 轴将反转, 受限于域,它将具有与其 x 相同的比例 轴(即

scaleanchor: 'x'

 + 
scaleratio: 1
 )以便将像素渲染为正方形。

它没有提到的是

我们无法覆盖此行为。无论布局更新期间轴缩放锚点的实际值如何,都会对图像轨迹应用 'yaxis.scaleanchor': 'x'

 约束。因此,在将 
'yaxis.scaleanchor': false 添加到 update
 对象(对于 x 轴)后,来自 
Zoom on a Plotly heatmap
 答案的代码可以工作,但事实并非如此。

[更新]:我做了一个 PR 来修复这个问题,由于 Plotly 团队的积极响应,该问题已被快速合并。因此,从plotly-2.26.0 开始,不再需要选项 1 和 2,设置 'yaxis.scaleanchor': false

 来删除图像跟踪上的 y 轴约束现在可以正常工作。请参阅下面的选项 0。

注意。无论您选择何种选项来规避该问题:

    原始代码无法与默认在图像轨迹的 y 轴上设置的
  • autorange: 'reversed'
     正常工作,现已修复(请参阅下面的 
    diff 和/或完整示例代码)。
  • 代码假设通过增加轴“范围”来应用初始约束(参见
  • constrain
    ),这是默认值 
    除了包含图像轨迹的轴,其中默认值是减小“域”,因此您需要在约束轴上显式设置 constrain: "range"

选项 0 - Plotly 版本 >= 2.26.0 :

来自

Zoom on a Plotly heatmap的代码现在可以正常用于热图和图像轨迹。只需确保在 yaxis: { scaleanchor: false }

 处理程序的 
update
 对象中设置 
afterPlot
 即可。

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: { showgrid: false, zeroline: false } }; Plotly.newPlot('plot', 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.<x|y>axis.range const update = { 'xaxis.range': xrange, 'yaxis.range': yrange, 'xaxis.scaleanchor': false, 'yaxis.scaleanchor': false }; Plotly.relayout(gd, update); // Attach the handler that will do the adjustments after relayout if needed gd.on('plotly_relayout', 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['xaxis.autorange'] || e['yaxis.autorange']) { // Reset zoom range (dblclick or "autoscale" 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't match, expand one of the axis range as necessary const [xmin, xmax] = getExtremes(gd, 0, 'x'); const [ymin, ymax] = getExtremes(gd, 0, 'y'); if (r1 > r0) { const extra = (zh1 * r1/r0 - zh1) / 2; expandAxisRange(yrange, extra, ymin, ymax); } if (r1 < r0) { const extra = (zw1 * r0/r1 - zw1) / 2; expandAxisRange(xrange, extra, xmin, xmax); } Plotly.relayout(gd, update); } } function unbindAndReset(gd, handler) { gd.removeListener('plotly_relayout', 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: '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 expandAxisRange(range, extra, min, max) { const reversed = range[0] > range[1]; if (reversed) { [range[0], range[1]] = [range[1], range[0]]; } let shift = 0; if (range[0] - extra < min) { const out = min - (range[0] - extra); const room = max - (range[1] + extra); shift = out <= room ? out : (out + room) / 2; } else if (range[1] + extra > max) { const out = range[1] + extra - max; const room = range[0] - extra - min; shift = out <= 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]]; } }
<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
<div id="plot"></div>


选项 1 - 布局图像 :

我对

布局图像的看法是错误的:“它不会有用户交互(悬停、平移、缩放等),因此您必须手动应用它们(至少是缩放+平移)”。

事实上,只要图像设置为

xref: 'x'

yref: 'y'
(即指热图轴 id),缩放和平移就可以与热图轨迹的缩放和平移同步。关于悬停交互,我们无法从布局图像中获得悬停事件,但我们仍然可以通过其 
customdata
 属性将一些图像数据添加到热图跟踪中,并使用 
hovertemplate
 在悬停时显示这些自定义数据。

const z = Array.from({ length: 50 }, () => Array.from({ length: 20 }, () => Math.floor(Math.random() * 255))); const z2 = [ [[255, 0, 0, 255], [0, 255, 255, 255], [255, 255, 0, 255]], [[0, 0, 255, 255], [255, 0, 255, 255], [0, 255, 0, 255]] ]; const imgH = z2.length; const imgW = z2[0].length; const imgURL = imgToDataURL(z2); const data = [{ type: 'heatmap', z: z, opacity: 0.3 }]; const layout = { xaxis: { constrain: 'range', constraintoward: 'center', scaleanchor: 'y', scaleratio: 1, zeroline: false, showgrid: false, }, yaxis: { showgrid: false, zeroline: false, autorange: 'reversed' }, images: [{ source: imgURL, layer: 'below', xref: 'x', yref: 'y', x: -0.5, y: -0.5, sizex: imgW, sizey: imgH, xanchor: 'left', yanchor: 'top' }] }; Plotly.newPlot('plot', data, layout).then(afterPlot); // Expects a 2d array of [r, g, b, a] values function imgToDataURL(z) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.height = z.length; canvas.width = z[0].length; const imageData = ctx.createImageData(canvas.width, canvas.height); const pixels = new Uint8ClampedArray(z.flat(2)); imageData.data.set(pixels); ctx.putImageData(imageData, 0, 0); return canvas.toDataURL(); } 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.<x|y>axis.range const update = { 'xaxis.range': xrange, 'yaxis.range': yrange, 'xaxis.scaleanchor': false, 'yaxis.scaleanchor': false }; Plotly.relayout(gd, update); // Attach the handler that will do the adjustments after relayout if needed gd.on('plotly_relayout', 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['xaxis.autorange'] || e['yaxis.autorange']) { // Reset zoom range (dblclick or "autoscale" 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't match, expand one of the axis range as necessary const [xmin, xmax] = getExtremes(gd, 0, 'x'); const [ymin, ymax] = getExtremes(gd, 0, 'y'); if (r1 > r0) { const extra = (zh1 * r1/r0 - zh1) / 2; expandAxisRange(yrange, extra, ymin, ymax); } if (r1 < r0) { const extra = (zw1 * r0/r1 - zw1) / 2; expandAxisRange(xrange, extra, xmin, xmax); } Plotly.relayout(gd, update); } } function unbindAndReset(gd, handler) { gd.removeListener('plotly_relayout', 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: '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 expandAxisRange(range, extra, min, max) { const reversed = range[0] > range[1]; if (reversed) { [range[0], range[1]] = [range[1], range[0]]; } let shift = 0; if (range[0] - extra < min) { const out = min - (range[0] - extra); const room = max - (range[1] + extra); shift = out <= room ? out : (out + room) / 2; } else if (range[1] + extra > max) { const out = range[1] + extra - max; const room = range[0] - extra - min; shift = out <= 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]]; } }
.layer-subplot .imagelayer image {
  image-rendering: pixelated;
}
<script src='https://cdn.plot.ly/plotly-2.22.0.min.js'></script>
<div id='plot'></div>


选项 2 - 补丁 :

涉及的代码

非常小并且易于修补。有一个标志,hasImage(定义于

here
),用于确定是否正在绘制图像轨迹,因此无论实际轴参数如何,都应该应用约束。将其设置为 false :
        handleOneAxDefaults(axIn, axOut, {
            axIds: axIds,
            layoutOut: layoutOut,
            hasImage: false // axHasImage[axName]
        });

由于这部分代码未注入全局范围(即加载 Plotly 后不可能覆盖它),因此您必须在源代码上应用补丁并从中构建您自己的自定义包
(先决条件:git、node.js、npm),顺便说一句,这也可能是一个通过仅包含您需要的跟踪类型来减少包大小的机会,并最终将其托管在某个地方。

© www.soinside.com 2019 - 2024. All rights reserved.