以下代码(请参阅保留方形像素+使用 Plotly.js 进行任何矩形缩放的能力,以及具有 2 层的绘图和在 Plotly 热图上缩放)可创建一个 Plotly 热图:
但是,当选择一个矩形区域进行放大时,该区域在缩放后并不居中,而:
如何使用 Plotly.js 实现这个标准的用户体验? (许多具有矩形缩放 UX 的软件的标准配置,例如 Matplotlib、Photoshop 等)
示例:
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 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 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);
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>
事实上,我添加了一个参数(我不应该添加),该参数将绘制的数据可见性置于用户重新居中时绘制的实际缩放形状之前(即,将轴范围“向更多数据”扩展,以便更多数据可见,而不是在缩放窗口周围均匀扩展,即使它包含空白区域)。我知道这可能会出现问题,所以我确保可以轻松丢弃这个范围“转变”,但我完全忘记在之前的答案中提及这一点。
因此,这种情况发生在
expandAxisRange()
函数中,您可以注释或删除涉及 shift
参数的行,这会简化事情,因为您不需要 min
和 max
参数,也不需要 getExtremes()
函数),例如。
function expandAxisRange(range, extra) {
const reversed = range[0] > range[1];
range[0] += reversed ? +extra : -extra;
range[1] += reversed ? -extra : +extra;
}
基于@EricLavault 的答案的即用型代码片段:
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 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;
}
if (r1 > r0) {
const extra = (zh1 * r1/r0 - zh1) / 2;
expandAxisRange(yrange, extra);
}
if (r1 < r0) {
const extra = (zw1 * r0/r1 - zw1) / 2;
expandAxisRange(xrange, extra);
}
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 expandAxisRange(range, extra) {
const reversed = range[0] > range[1];
range[0] += reversed ? +extra : -extra;
range[1] += reversed ? -extra : +extra;
}
<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
<div id="plot"></div>