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

Plotly.js双图层图表:实现自由矩形缩放并保持像素方正

Plotly热图与Image图层的自由矩形缩放实现

问题背景

在单一热图场景下,已有方案可实现以下效果:

  • 方正像素(必填要求)
  • 支持任意矩形形状缩放(不受热图原始宽高比限制)

但当图表同时包含heatmap和image两个图层时,缩放功能会被强制限制在热图的原始宽高比,无法满足自由矩形缩放需求:

缩放受限效果

解决方案

以下是适配多图层场景的实现代码,分别提供Plotly.js 2.22.0和2.26.0版本的适配:

版本1:Plotly.js 2.22.0

<script src="https://cdn.plot.ly/plotly-2.22.0.min.js"></script>
<div id="plot"></div>
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;
}

版本2:Plotly.js 2.26.0

<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
<div id="plot"></div>
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;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 17:57:02