Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/plot_api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>}
*/
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;
Expand All @@ -3828,6 +3864,7 @@ exports.restyle = restyle;
exports.setPlotConfig = setPlotConfig;

exports.update = update;
exports.updateImages = updateImages;

exports._guiRelayout = guiEdit(relayout);
exports._guiRestyle = guiEdit(restyle);
Expand Down
88 changes: 88 additions & 0 deletions test/jasmine/tests/image_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
});