From fed59bef9eeb677a63712bbae0ddf0ee65d9f4fa Mon Sep 17 00:00:00 2001 From: Robert Collar Date: Fri, 3 Apr 2026 17:37:26 -0400 Subject: [PATCH 1/2] plot_api: Add Plotly.updateImages() for mid-interaction image updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a lightweight API method that surgically updates layout images without triggering a full relayout or trace redraw. Designed for use during plotly_relayouting (e.g., tiled image pyramids that swap tiles on zoom/pan). The method bypasses the diff/redraw pipeline by directly calling supplyLayoutDefaults and draw for the images component only, avoiding the axrange→drawData code path that causes all traces to flash. --- src/plot_api/index.js | 1 + src/plot_api/plot_api.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/plot_api/index.js b/src/plot_api/index.js index 011ff30ba1c..c4cb2dc1533 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -23,6 +23,7 @@ exports.addFrames = main.addFrames; exports.deleteFrames = main.deleteFrames; exports.animate = main.animate; exports.setPlotConfig = main.setPlotConfig; +exports.updateImages = main.updateImages; var getGraphDiv = require('../lib/dom').getGraphDiv; var eraseActiveShape = require('../components/shapes/draw').eraseActiveShape; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index fd46fdedb0f..81d55248683 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -3806,6 +3806,42 @@ function makePlotFramework(gd) { gd.emit('plotly_framework'); } +/** + * Plotly.updateImages: Surgically update layout images without triggering + * a full relayout or trace redraw. Designed for use during plotly_relayouting + * (e.g., tiled image pyramids that swap tiles on zoom/pan). + * + * @param {HTMLElement|string} gd - plot div or its id + * @param {Array} imageList - new images array (same format as layout.images) + * @returns {Promise} + */ +function updateImages(gd, imageList) { + gd = Lib.getGraphDiv(gd); + + var layout = gd.layout; + var fullLayout = gd._fullLayout; + + // Update user layout + layout.images = imageList; + + // Clear stale _imgIndices on all axes before re-supplying defaults, + // since imageDefaults pushes to these arrays + var axList = Axes.list(gd); + for(var i = 0; i < axList.length; i++) { + axList[i]._imgIndices = []; + } + + // Re-run image defaults only (populates _imgIndices, validates refs) + var supplyImageDefaults = Registry.getComponentMethod('images', 'supplyLayoutDefaults'); + supplyImageDefaults(layout, fullLayout); + + // Draw images only — no trace redraw, no axis processing + var drawImages = Registry.getComponentMethod('images', 'draw'); + drawImages(gd); + + return Plots.previousPromises(gd) || Promise.resolve(gd); +} + exports.animate = animate; exports.addFrames = addFrames; exports.deleteFrames = deleteFrames; @@ -3828,6 +3864,7 @@ exports.restyle = restyle; exports.setPlotConfig = setPlotConfig; exports.update = update; +exports.updateImages = updateImages; exports._guiRelayout = guiEdit(relayout); exports._guiRestyle = guiEdit(restyle); From 5c00ee6acf06d8fd81dfde5fd7cb3412205053ee Mon Sep 17 00:00:00 2001 From: Robert Collar Date: Fri, 3 Apr 2026 17:37:31 -0400 Subject: [PATCH 2/2] test: Add tests for Plotly.updateImages() Cover basic add/remove, no-trace-redraw guarantee (drawData not called), layout state update, and promise return value. --- test/jasmine/tests/image_test.js | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test/jasmine/tests/image_test.js b/test/jasmine/tests/image_test.js index 81fa1d28105..ac42c02afb2 100644 --- a/test/jasmine/tests/image_test.js +++ b/test/jasmine/tests/image_test.js @@ -9,6 +9,7 @@ var d3SelectAll = require('../../strict-d3').selectAll; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var subroutines = require('../../../src/plot_api/subroutines'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; @@ -695,3 +696,90 @@ describe('image hover:', function() { }); }); }); + +describe('Plotly.updateImages', function() { + 'use strict'; + + var gd; + var imgSelector = 'image'; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function countImages() { + return d3SelectAll(imgSelector).size(); + } + + function makeImage(x, y, sizex, sizey) { + return { + source: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', + xref: 'x', + yref: 'y', + x: x || 0, + y: y || 1, + sizex: sizex || 0.5, + sizey: sizey || 0.5, + sizing: 'stretch', + layer: 'below' + }; + } + + it('should add images to an empty plot', function(done) { + Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) + .then(function() { + expect(countImages()).toBe(0); + return Plotly.updateImages(gd, [makeImage()]); + }) + .then(function() { + expect(countImages()).toBe(1); + }) + .then(done, done.fail); + }); + + it('should add, increase, and remove images', function(done) { + Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) + .then(function() { + return Plotly.updateImages(gd, [makeImage(0, 1)]); + }) + .then(function() { + expect(countImages()).toBe(1); + return Plotly.updateImages(gd, [makeImage(0, 1), makeImage(0.5, 1)]); + }) + .then(function() { + expect(countImages()).toBe(2); + return Plotly.updateImages(gd, []); + }) + .then(function() { + expect(countImages()).toBe(0); + }) + .then(done, done.fail); + }); + + it('should not call drawData (no trace redraw)', function(done) { + spyOn(subroutines, 'drawData').and.callThrough(); + + Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) + .then(function() { + subroutines.drawData.calls.reset(); + return Plotly.updateImages(gd, [makeImage()]); + }) + .then(function() { + expect(subroutines.drawData).not.toHaveBeenCalled(); + expect(countImages()).toBe(1); + }) + .then(done, done.fail); + }); + + it('should return a promise', function(done) { + Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) + .then(function() { + var result = Plotly.updateImages(gd, [makeImage()]); + expect(typeof result.then).toBe('function'); + return result; + }) + .then(done, done.fail); + }); +});