Skip to content

Commit 03baca7

Browse files
committed
use uirevisions in Plotly.react to preserve ui state
1 parent 4af2bf8 commit 03baca7

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

src/plot_api/plot_api.js

+189
Original file line numberDiff line numberDiff line change
@@ -2362,6 +2362,193 @@ exports._guiRestyle = guiEdit(restyle);
23622362
exports._guiRelayout = guiEdit(relayout);
23632363
exports._guiUpdate = guiEdit(update);
23642364

2365+
// For connecting edited layout attributes to uirevision attrs
2366+
// If no `attr` we use `match[1] + '.uirevision'`
2367+
// Ordered by most common edits first, to minimize our search time
2368+
var layoutUIControlPatterns = [
2369+
{pattern: /^hiddenlabels/, attr: 'legend.uirevision'},
2370+
{pattern: /^((x|y)axis\d*)\.((auto)?range|title)/, autofill: true},
2371+
2372+
// showspikes and modes include those nested inside scenes
2373+
{pattern: /axis\d*\.showspikes$/, attr: 'modebar.uirevision'},
2374+
{pattern: /(hover|drag)mode$/, attr: 'modebar.uirevision'},
2375+
2376+
{pattern: /^(scene\d*)\.camera/},
2377+
{pattern: /^(geo\d*)\.(projection|center)/},
2378+
{pattern: /^(ternary\d*\.[abc]axis)\.(min|title)$/},
2379+
{pattern: /^(polar\d*\.(radial|angular)axis)\./},
2380+
{pattern: /^(mapbox\d*)\.(center|zoom|bearing|pitch)/},
2381+
2382+
{pattern: /^legend\.(x|y)$/, attr: 'editrevision'},
2383+
{pattern: /^(shapes|annotations)/, attr: 'editrevision'},
2384+
{pattern: /^title$/, attr: 'editrevision'}
2385+
];
2386+
2387+
// same for trace attributes: if `attr` is given it's in layout,
2388+
// or with no `attr` we use `trace.uirevision`
2389+
var traceUIControlPatterns = [
2390+
// "visible" includes trace.transforms[i].styles[j].value.visible
2391+
{pattern: /(^|value\.)visible$/, attr: 'legend.uirevision'},
2392+
{pattern: /^dimensions\[\d+\]\.constraintrange/},
2393+
2394+
// below this you must be in editable: true mode
2395+
// TODO: I still put name and title with `trace.uirevision`
2396+
// reasonable or should these be `editrevision`?
2397+
// Also applies to axis titles up in the layout section
2398+
2399+
// "name" also includes transform.styles
2400+
{pattern: /(^|value\.)name$/},
2401+
// including nested colorbar attributes (ie marker.colorbar)
2402+
{pattern: /colorbar\.title$/},
2403+
{pattern: /colorbar\.(x|y)$/, attr: 'editrevision'}
2404+
];
2405+
2406+
function findUIPattern(key, patternSpecs) {
2407+
for(var i = 0; i < patternSpecs.length; i++) {
2408+
var spec = patternSpecs[i];
2409+
var match = key.match(spec.pattern);
2410+
if(match) {
2411+
return {head: match[1], attr: spec.attr, autofill: spec.autofill};
2412+
}
2413+
}
2414+
}
2415+
2416+
// We're finding the new uirevision before supplyDefaults, so do the
2417+
// inheritance manually. Note that only `undefined` inherits - other
2418+
// falsy values are returned.
2419+
function getNewRev(revAttr, container) {
2420+
var newRev = nestedProperty(container, revAttr).get();
2421+
if(newRev !== undefined) return newRev;
2422+
2423+
var parts = revAttr.split('.');
2424+
parts.pop();
2425+
while(parts.length > 1) {
2426+
parts.pop();
2427+
newRev = nestedProperty(container, parts.join('.') + '.uirevision').get();
2428+
if(newRev !== undefined) return newRev;
2429+
}
2430+
2431+
return container.uirevision;
2432+
}
2433+
2434+
function getFullTraceIndexFromUid(uid, fullData) {
2435+
for(var i = 0; i < fullData.length; i++) {
2436+
if(fullData[i]._fullInput.uid === uid) return i;
2437+
}
2438+
return -1;
2439+
}
2440+
2441+
function getTraceIndexFromUid(uid, data, tracei) {
2442+
for(var i = 0; i < data.length; i++) {
2443+
if(data[i].uid === uid) return i;
2444+
}
2445+
// fall back on trace order, but only if user didn't provide a uid for that trace
2446+
return data[tracei].uid ? -1 : tracei;
2447+
}
2448+
2449+
function applyUIRevisions(data, layout, oldFullData, oldFullLayout) {
2450+
var layoutPreGUI = oldFullLayout._preGUI;
2451+
var key, revAttr, oldRev, newRev, match, preGUIVal, newNP, newVal;
2452+
for(key in layoutPreGUI) {
2453+
match = findUIPattern(key, layoutUIControlPatterns);
2454+
if(match) {
2455+
revAttr = match.attr || (match.head + '.uirevision');
2456+
oldRev = nestedProperty(oldFullLayout, revAttr).get();
2457+
newRev = oldRev && getNewRev(revAttr, layout);
2458+
if(newRev && (newRev === oldRev)) {
2459+
preGUIVal = layoutPreGUI[key];
2460+
if(preGUIVal === null) preGUIVal = undefined;
2461+
newNP = nestedProperty(layout, key);
2462+
newVal = newNP.get();
2463+
// TODO: This test for undefined is to account for the case where
2464+
// the value was filled in automatically in gd.layout,
2465+
// like axis.range/autorange. In principle though, if the initial
2466+
// plot had a value and the new plot removed that value, we would
2467+
// want the removal to override the GUI edit and generate a new
2468+
// auto value. But that would require figuring out what value was
2469+
// in gd.layout *before* the auto values were filled in, and
2470+
// storing *that* in preGUI... oh well, for now at least I limit
2471+
// this to attributes that get autofilled, which AFAICT among
2472+
// the GUI-editable attributes is just axis.range/autorange.
2473+
if(newVal === preGUIVal || (match.autofill && newVal === undefined)) {
2474+
newNP.set(nestedProperty(oldFullLayout, key).get());
2475+
continue;
2476+
}
2477+
}
2478+
}
2479+
else {
2480+
Lib.warn('unrecognized GUI edit: ' + key);
2481+
}
2482+
// if we got this far, the new value was accepted as the new starting
2483+
// point (either because it changed or revision changed)
2484+
// so remove it from _preGUI for next time.
2485+
delete layoutPreGUI[key];
2486+
}
2487+
2488+
// Now traces - try to match them up by uid (in case we added/deleted in
2489+
// the middle), then fall back on index.
2490+
// var tracei = -1;
2491+
// for(var fulli = 0; fulli < oldFullData.length; fulli++) {
2492+
var allTracePreGUI = oldFullLayout._tracePreGUI;
2493+
for(var uid in allTracePreGUI) {
2494+
var tracePreGUI = allTracePreGUI[uid];
2495+
var newTrace = null;
2496+
var fullInput;
2497+
for(key in tracePreGUI) {
2498+
// wait until we know we have preGUI values to look for traces
2499+
// but if we don't find both, stop looking at this uid
2500+
if(!newTrace) {
2501+
var fulli = getFullTraceIndexFromUid(uid, oldFullData);
2502+
if(fulli < 0) {
2503+
// Somehow we didn't even have this trace in oldFullData...
2504+
// I guess this could happen with `deleteTraces` or something
2505+
delete allTracePreGUI[uid];
2506+
break;
2507+
}
2508+
var fullTrace = oldFullData[fulli];
2509+
fullInput = fullTrace._fullInput;
2510+
2511+
var newTracei = getTraceIndexFromUid(uid, data, fullInput.index);
2512+
if(newTracei < 0) {
2513+
// No match in new data
2514+
delete allTracePreGUI[uid];
2515+
break;
2516+
}
2517+
newTrace = data[newTracei];
2518+
}
2519+
2520+
match = findUIPattern(key, traceUIControlPatterns);
2521+
if(match) {
2522+
if(match.attr) {
2523+
oldRev = nestedProperty(oldFullLayout, match.attr).get();
2524+
newRev = oldRev && getNewRev(match.attr, layout);
2525+
}
2526+
else {
2527+
oldRev = fullInput.uirevision;
2528+
// inheritance for trace.uirevision is simple, just layout.uirevision
2529+
newRev = newTrace.uirevision;
2530+
if(newRev === undefined) newRev = layout.uirevision;
2531+
}
2532+
2533+
if(newRev && newRev === oldRev) {
2534+
preGUIVal = tracePreGUI[key];
2535+
if(preGUIVal === null) preGUIVal = undefined;
2536+
newNP = nestedProperty(newTrace, key);
2537+
newVal = newNP.get();
2538+
if(newVal === preGUIVal || (match.autofill && newVal === undefined)) {
2539+
newNP.set(nestedProperty(fullInput, key).get());
2540+
continue;
2541+
}
2542+
}
2543+
}
2544+
else {
2545+
Lib.warn('unrecognized GUI edit: ' + key + ' in trace uid ' + uid);
2546+
}
2547+
delete tracePreGUI[key];
2548+
}
2549+
}
2550+
}
2551+
23652552
/**
23662553
* Plotly.react:
23672554
* A plot/update method that takes the full plot state (same API as plot/newPlot)
@@ -2424,6 +2611,8 @@ exports.react = function(gd, data, layout, config) {
24242611
gd.layout = layout || {};
24252612
helpers.cleanLayout(gd.layout);
24262613

2614+
applyUIRevisions(gd.data, gd.layout, oldFullData, oldFullLayout);
2615+
24272616
// "true" skips updating calcdata and remapping arrays from calcTransforms,
24282617
// which supplyDefaults usually does at the end, but we may need to NOT do
24292618
// if the diff (which we haven't determined yet) says we'll recalc

0 commit comments

Comments
 (0)