const z = Array.from({length: 500}, () => Array.from({length: 100}, () => Math.floor(Math.random() * 255)));
const data = [{
type: 'heatmap',
z: z
}];
const layout = {
xaxis: {
constrain: 'range',
constraintoward: 'center',
scaleanchor: "y",
scaleratio: 1
}
};
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.22.0.min.js"></script>
<div id="plot"></div>