Skip to content

Commit 442a337

Browse files
committed
Merge pull request #289 from plotly/delete-last-trace2
A slightly less ambitious take on orphan subplots and 'delete last trace'
2 parents f712283 + 07efdc7 commit 442a337

File tree

18 files changed

+830
-242
lines changed

18 files changed

+830
-242
lines changed

src/plot_api/plot_api.js

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,13 @@ var xmlnsNamespaces = require('../constants/xmlns_namespaces');
5050
Plotly.plot = function(gd, data, layout, config) {
5151
Lib.markTime('in plot');
5252

53-
5453
gd = getGraphDiv(gd);
5554

56-
/*
57-
* Events.init is idempotent and bails early if gd has already been init'd
58-
*/
55+
// Events.init is idempotent and bails early if gd has already been init'd
5956
Events.init(gd);
6057

6158
var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]);
62-
if(okToPlot===false) return Promise.reject();
59+
if(okToPlot === false) return Promise.reject();
6360

6461
// if there's no data or layout, and this isn't yet a plotly plot
6562
// container, log a warning to help plotly.js users debug
@@ -89,22 +86,23 @@ Plotly.plot = function(gd, data, layout, config) {
8986
// complete, and empty out the promise list again.
9087
gd._promises = [];
9188

89+
var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data));
90+
9291
// if there is already data on the graph, append the new data
9392
// if you only want to redraw, pass a non-array for data
94-
var graphwasempty = ((gd.data||[]).length===0 && Array.isArray(data));
9593
if(Array.isArray(data)) {
9694
cleanData(data, gd.data);
9795

98-
if(graphwasempty) gd.data=data;
99-
else gd.data.push.apply(gd.data,data);
96+
if(graphWasEmpty) gd.data = data;
97+
else gd.data.push.apply(gd.data, data);
10098

10199
// for routines outside graph_obj that want a clean tab
102100
// (rather than appending to an existing one) gd.empty
103101
// is used to determine whether to make a new tab
104-
gd.empty=false;
102+
gd.empty = false;
105103
}
106104

107-
if(!gd.layout || graphwasempty) gd.layout = cleanLayout(layout);
105+
if(!gd.layout || graphWasEmpty) gd.layout = cleanLayout(layout);
108106

109107
// if the user is trying to drag the axes, allow new data and layout
110108
// to come in but don't allow a replot.
@@ -126,23 +124,28 @@ Plotly.plot = function(gd, data, layout, config) {
126124
// so we don't try to re-call Plotly.plot from inside
127125
// legend and colorbar, if margins changed
128126
gd._replotting = true;
129-
var hasData = gd._fullData.length>0;
127+
var hasData = gd._fullData.length > 0;
128+
129+
var subplots = Plotly.Axes.getSubplots(gd).join(''),
130+
oldSubplots = Object.keys(gd._fullLayout._plots || {}).join(''),
131+
hasSameSubplots = (oldSubplots === subplots);
130132

131133
// Make or remake the framework (ie container and axes) if we need to
132134
// note: if they container already exists and has data,
133135
// the new layout gets ignored (as it should)
134136
// but if there's no data there yet, it's just a placeholder...
135137
// then it should destroy and remake the plot
136-
if (hasData) {
137-
var subplots = Plotly.Axes.getSubplots(gd).join(''),
138-
oldSubplots = Object.keys(gd._fullLayout._plots || {}).join('');
139-
140-
if(gd.framework!==makePlotFramework || graphwasempty || (oldSubplots!==subplots)) {
138+
if(hasData) {
139+
if(gd.framework !== makePlotFramework || graphWasEmpty || !hasSameSubplots) {
141140
gd.framework = makePlotFramework;
142141
makePlotFramework(gd);
143142
}
144143
}
145-
else if(graphwasempty) makePlotFramework(gd);
144+
else if(!hasSameSubplots) {
145+
gd.framework = makePlotFramework;
146+
makePlotFramework(gd);
147+
}
148+
else if(graphWasEmpty) makePlotFramework(gd);
146149

147150
var fullLayout = gd._fullLayout;
148151

@@ -160,7 +163,7 @@ Plotly.plot = function(gd, data, layout, config) {
160163
}
161164

162165
// in case it has changed, attach fullData traces to calcdata
163-
for (var i = 0; i < gd.calcdata.length; i++) {
166+
for(var i = 0; i < gd.calcdata.length; i++) {
164167
gd.calcdata[i][0].trace = gd._fullData[i];
165168
}
166169

@@ -2144,8 +2147,12 @@ Plotly.relayout = function relayout(gd, astr, val) {
21442147
undoit[ai] = (pleaf === 'reverse') ? vi : p.get();
21452148

21462149
// check autosize or autorange vs size and range
2147-
if(hw.indexOf(ai)!==-1) { doextra('autosize', false); }
2148-
else if(ai==='autosize') { doextra(hw, undefined); }
2150+
if(hw.indexOf(ai) !== -1) {
2151+
doextra('autosize', false);
2152+
}
2153+
else if(ai === 'autosize') {
2154+
doextra(hw, undefined);
2155+
}
21492156
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
21502157
doextra(ptrunk+'.autorange', false);
21512158
}
@@ -2165,6 +2172,10 @@ Plotly.relayout = function relayout(gd, astr, val) {
21652172
else if(pleaf === 'tickmode') {
21662173
doextra([ptrunk + '.tick0', ptrunk + '.dtick'], undefined);
21672174
}
2175+
else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) {
2176+
docalc = true;
2177+
}
2178+
21682179
// toggling log without autorange: need to also recalculate ranges
21692180
// logical XOR (ie are we toggling log)
21702181
if(pleaf==='type' && ((parentFull.type === 'log') !== (vi === 'log'))) {
@@ -2318,10 +2329,12 @@ Plotly.relayout = function relayout(gd, astr, val) {
23182329
seq.push(function layoutReplot() {
23192330
// force plot() to redo the layout
23202331
gd.layout = undefined;
2332+
23212333
// force it to redo calcdata?
23222334
if(docalc) gd.calcdata = undefined;
2335+
23232336
// replot with the modified layout
2324-
return Plotly.plot(gd,'',layout);
2337+
return Plotly.plot(gd, '', layout);
23252338
});
23262339
}
23272340
else if(ak.length) {

src/plots/cartesian/index.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,8 @@ exports.plot = function(gd) {
100100
}
101101

102102
// finally do all error bars at once
103-
if(fullLayout._hasCartesian) {
104-
ErrorBars.plot(gd, subplotInfo, cdError);
105-
Lib.markTime('done ErrorBars');
106-
}
103+
ErrorBars.plot(gd, subplotInfo, cdError);
104+
Lib.markTime('done ErrorBars');
107105
}
108106

109107
// now draw stuff not on subplots (ie, only pies at the moment)
@@ -114,3 +112,9 @@ exports.plot = function(gd) {
114112
if(cdPie.length) Pie.plot(gd, cdPie);
115113
}
116114
};
115+
116+
exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
117+
if(oldFullLayout._hasPie && !newFullLayout._hasPie) {
118+
oldFullLayout._pielayer.selectAll('g.trace').remove();
119+
}
120+
};

src/plots/cartesian/layout_defaults.js

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,36 @@ var axisIds = require('./axis_ids');
2020

2121

2222
module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
23-
// get the full list of axes already defined
2423
var layoutKeys = Object.keys(layoutIn),
25-
xaList = [],
26-
yaList = [],
24+
xaListCartesian = [],
25+
yaListCartesian = [],
26+
xaListGl2d = [],
27+
yaListGl2d = [],
2728
outerTicks = {},
2829
noGrids = {},
2930
i;
3031

31-
for(i = 0; i < layoutKeys.length; i++) {
32-
var key = layoutKeys[i];
33-
if(constants.xAxisMatch.test(key)) xaList.push(key);
34-
else if(constants.yAxisMatch.test(key)) yaList.push(key);
35-
}
36-
32+
// look for axes in the data
3733
for(i = 0; i < fullData.length; i++) {
38-
var trace = fullData[i],
39-
xaName = axisIds.id2name(trace.xaxis),
34+
var trace = fullData[i];
35+
var listX, listY;
36+
37+
if(Plots.traceIs(trace, 'cartesian')) {
38+
listX = xaListCartesian;
39+
listY = yaListCartesian;
40+
}
41+
else if(Plots.traceIs(trace, 'gl2d')) {
42+
listX = xaListGl2d;
43+
listY = yaListGl2d;
44+
}
45+
else continue;
46+
47+
var xaName = axisIds.id2name(trace.xaxis),
4048
yaName = axisIds.id2name(trace.yaxis);
4149

4250
// add axes implied by traces
43-
if(xaName && xaList.indexOf(xaName) === -1) xaList.push(xaName);
44-
if(yaName && yaList.indexOf(yaName) === -1) yaList.push(yaName);
51+
if(xaName && listX.indexOf(xaName) === -1) listX.push(xaName);
52+
if(yaName && listY.indexOf(yaName) === -1) listY.push(yaName);
4553

4654
// check for default formatting tweaks
4755
if(Plots.traceIs(trace, '2dMap')) {
@@ -55,22 +63,47 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
5563
}
5664
}
5765

58-
function axSort(a,b) {
59-
var aNum = Number(a.substr(5)||1),
60-
bNum = Number(b.substr(5)||1);
61-
return aNum - bNum;
66+
// N.B. Ignore orphan axes (i.e. axes that have no data attached to them)
67+
// if gl3d or geo is present on graph. This is retain backward compatible.
68+
//
69+
// TODO drop this in version 2.0
70+
var ignoreOrphan = (layoutOut._hasGL3D || layoutOut._hasGeo);
71+
72+
if(!ignoreOrphan) {
73+
for(i = 0; i < layoutKeys.length; i++) {
74+
var key = layoutKeys[i];
75+
76+
// orphan layout axes are considered cartesian subplots
77+
78+
if(xaListGl2d.indexOf(key) === -1 &&
79+
xaListCartesian.indexOf(key) === -1 &&
80+
constants.xAxisMatch.test(key)) {
81+
xaListCartesian.push(key);
82+
}
83+
else if(yaListGl2d.indexOf(key) === -1 &&
84+
yaListCartesian.indexOf(key) === -1 &&
85+
constants.yAxisMatch.test(key)) {
86+
yaListCartesian.push(key);
87+
}
88+
}
6289
}
6390

64-
if(layoutOut._hasCartesian || layoutOut._hasGL2D || !fullData.length) {
65-
// make sure there's at least one of each and lists are sorted
66-
if(!xaList.length) xaList = ['xaxis'];
67-
else xaList.sort(axSort);
91+
// make sure that plots with orphan cartesian axes
92+
// are considered 'cartesian'
93+
if(xaListCartesian.length && yaListCartesian.length) {
94+
layoutOut._hasCartesian = true;
95+
}
6896

69-
if(!yaList.length) yaList = ['yaxis'];
70-
else yaList.sort(axSort);
97+
function axSort(a, b) {
98+
var aNum = Number(a.substr(5) || 1),
99+
bNum = Number(b.substr(5) || 1);
100+
return aNum - bNum;
71101
}
72102

73-
xaList.concat(yaList).forEach(function(axName){
103+
var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort),
104+
yaList = yaListCartesian.concat(yaListGl2d).sort(axSort);
105+
106+
xaList.concat(yaList).forEach(function(axName) {
74107
var axLetter = axName.charAt(0),
75108
axLayoutIn = layoutIn[axName] || {},
76109
axLayoutOut = {},
@@ -100,7 +133,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
100133

101134
// so we don't have to repeat autotype unnecessarily,
102135
// copy an autotype back to layoutIn
103-
if(!layoutIn[axName] && axLayoutIn.type!=='-') {
136+
if(!layoutIn[axName] && axLayoutIn.type !== '-') {
104137
layoutIn[axName] = {type: axLayoutIn.type};
105138
}
106139

src/plots/geo/geo.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ function Geo(options, fullLayout) {
5656

5757
this.makeFramework();
5858
this.updateFx(fullLayout.hovermode);
59+
60+
this.traceHash = {};
5961
}
6062

6163
module.exports = Geo;
@@ -152,25 +154,47 @@ function filterData(dataIn) {
152154
}
153155

154156
proto.onceTopojsonIsLoaded = function(geoData, geoLayout) {
155-
var traceData = {};
157+
var i;
156158

157159
this.drawLayout(geoLayout);
158160

159-
for(var i = 0; i < geoData.length; i++) {
161+
var traceHashOld = this.traceHash;
162+
var traceHash = {};
163+
164+
for(i = 0; i < geoData.length; i++) {
160165
var trace = geoData[i];
161166

162-
traceData[trace.type] = traceData[trace.type] || [];
163-
traceData[trace.type].push(trace);
167+
traceHash[trace.type] = traceHash[trace.type] || [];
168+
traceHash[trace.type].push(trace);
164169
}
165170

166-
var traceKeys = Object.keys(traceData);
167-
for(var j = 0; j < traceKeys.length; j++){
168-
var moduleData = traceData[traceKeys[j]];
171+
var moduleNamesOld = Object.keys(traceHashOld);
172+
var moduleNames = Object.keys(traceHash);
173+
174+
// when a trace gets deleted, make sure that its module's
175+
// plot method is called so that it is properly
176+
// removed from the DOM.
177+
for(i = 0; i < moduleNamesOld.length; i++) {
178+
var moduleName = moduleNamesOld[i];
179+
180+
if(moduleNames.indexOf(moduleName) === -1) {
181+
var fakeModule = traceHashOld[moduleName][0];
182+
fakeModule.visible = false;
183+
traceHash[moduleName] = [fakeModule];
184+
}
185+
}
186+
187+
moduleNames = Object.keys(traceHash);
188+
189+
for(i = 0; i < moduleNames.length; i++) {
190+
var moduleData = traceHash[moduleNames[i]];
169191
var _module = moduleData[0]._module;
170192

171193
_module.plot(this, filterData(moduleData), geoLayout);
172194
}
173195

196+
this.traceHash = traceHash;
197+
174198
this.render();
175199
};
176200

src/plots/geo/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,16 @@ exports.plot = function plotGeo(gd) {
6565
geo.plot(fullGeoData, fullLayout, gd._promises);
6666
}
6767
};
68+
69+
exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
70+
var oldGeoKeys = Plots.getSubplotIds(oldFullLayout, 'geo');
71+
72+
for(var i = 0; i < oldGeoKeys.length; i++) {
73+
var oldGeoKey = oldGeoKeys[i];
74+
var oldGeo = oldFullLayout[oldGeoKey]._geo;
75+
76+
if(!newFullLayout[oldGeoKey] && !!oldGeo) {
77+
oldGeo.geoDiv.remove();
78+
}
79+
}
80+
};

src/plots/geo/layout/defaults.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ var supplyGeoAxisLayoutDefaults = require('./axis_defaults');
1717

1818

1919
module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
20-
var geos = Plots.getSubplotIdsInData(fullData, 'geo'),
20+
if(!layoutOut._hasGeo) return;
21+
22+
var geos = Plots.findSubplotIds(fullData, 'geo'),
2123
geosLength = geos.length;
2224

2325
var geoLayoutIn, geoLayoutOut;
@@ -28,7 +30,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
2830

2931
for(var i = 0; i < geosLength; i++) {
3032
var geo = geos[i];
31-
geoLayoutIn = layoutIn[geo] || {};
33+
34+
// geo traces get a layout geo for free!
35+
if(layoutIn[geo]) geoLayoutIn = layoutIn[geo];
36+
else geoLayoutIn = layoutIn[geo] = {};
37+
38+
geoLayoutIn = layoutIn[geo];
3239
geoLayoutOut = {};
3340

3441
coerce('domain.x');

src/plots/gl3d/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ exports.plot = function plotGl3d(gd) {
6767
}
6868
};
6969

70+
exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
71+
var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl3d');
72+
73+
for(var i = 0; i < oldSceneKeys.length; i++) {
74+
var oldSceneKey = oldSceneKeys[i];
75+
76+
if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) {
77+
oldFullLayout[oldSceneKey]._scene.destroy();
78+
}
79+
}
80+
};
81+
7082
// clean scene ids, 'scene1' -> 'scene'
7183
exports.cleanId = function cleanId(id) {
7284
if (!id.match(/^scene[0-9]*$/)) return;

0 commit comments

Comments
 (0)