Skip to content

Commit 8c23c67

Browse files
authored
Merge pull request #1453 from plotly/image-clip-path
Image and shape clip paths
2 parents f3824fb + 521015a commit 8c23c67

File tree

12 files changed

+337
-76
lines changed

12 files changed

+337
-76
lines changed

src/components/images/draw.js

+43-15
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,28 @@ var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
1616
module.exports = function draw(gd) {
1717
var fullLayout = gd._fullLayout,
1818
imageDataAbove = [],
19-
imageDataSubplot = [],
20-
imageDataBelow = [];
19+
imageDataSubplot = {},
20+
imageDataBelow = [],
21+
subplot,
22+
i;
2123

2224
// Sort into top, subplot, and bottom layers
23-
for(var i = 0; i < fullLayout.images.length; i++) {
25+
for(i = 0; i < fullLayout.images.length; i++) {
2426
var img = fullLayout.images[i];
2527

2628
if(img.visible) {
2729
if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') {
28-
imageDataSubplot.push(img);
30+
subplot = img.xref + img.yref;
31+
32+
var plotinfo = fullLayout._plots[subplot];
33+
if(plotinfo.mainplot) {
34+
subplot = plotinfo.mainplot.id;
35+
}
36+
37+
if(!imageDataSubplot[subplot]) {
38+
imageDataSubplot[subplot] = [];
39+
}
40+
imageDataSubplot[subplot].push(img);
2941
} else if(img.layer === 'above') {
3042
imageDataAbove.push(img);
3143
} else {
@@ -143,36 +155,52 @@ module.exports = function draw(gd) {
143155
yId = ya ? ya._id : '',
144156
clipAxes = xId + yId;
145157

146-
if(clipAxes) {
147-
thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes);
148-
}
158+
thisImage.call(Drawing.setClipUrl, clipAxes ?
159+
('clip' + fullLayout._uid + clipAxes) :
160+
null
161+
);
149162
}
150163

151164
var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
152165
.data(imageDataBelow),
153-
imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image')
154-
.data(imageDataSubplot),
155166
imagesAbove = fullLayout._imageUpperLayer.selectAll('image')
156167
.data(imageDataAbove);
157168

158169
imagesBelow.enter().append('image');
159-
imagesSubplot.enter().append('image');
160170
imagesAbove.enter().append('image');
161171

162172
imagesBelow.exit().remove();
163-
imagesSubplot.exit().remove();
164173
imagesAbove.exit().remove();
165174

166175
imagesBelow.each(function(d) {
167176
setImage.bind(this)(d);
168177
applyAttributes.bind(this)(d);
169178
});
170-
imagesSubplot.each(function(d) {
171-
setImage.bind(this)(d);
172-
applyAttributes.bind(this)(d);
173-
});
174179
imagesAbove.each(function(d) {
175180
setImage.bind(this)(d);
176181
applyAttributes.bind(this)(d);
177182
});
183+
184+
var allSubplots = Object.keys(fullLayout._plots);
185+
for(i = 0; i < allSubplots.length; i++) {
186+
subplot = allSubplots[i];
187+
var subplotObj = fullLayout._plots[subplot];
188+
189+
// filter out overlaid plots (which havd their images on the main plot)
190+
// and gl2d plots (which don't support below images, at least not yet)
191+
if(!subplotObj.imagelayer) continue;
192+
193+
var imagesOnSubplot = subplotObj.imagelayer.selectAll('image')
194+
// even if there are no images on this subplot, we need to run
195+
// enter and exit in case there were previously
196+
.data(imageDataSubplot[subplot] || []);
197+
198+
imagesOnSubplot.enter().append('image');
199+
imagesOnSubplot.exit().remove();
200+
201+
imagesOnSubplot.each(function(d) {
202+
setImage.bind(this)(d);
203+
applyAttributes.bind(this)(d);
204+
});
205+
}
178206
};

src/components/rangeslider/draw.js

-4
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,6 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
400400
}
401401

402402
Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id));
403-
404-
// no need for the bg layer,
405-
// drawBg handles coloring the background
406-
if(isMainPlot) plotinfo.bg.remove();
407403
});
408404
}
409405

src/components/shapes/draw.js

+14-31
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function draw(gd) {
4242
// Remove previous shapes before drawing new in shapes in fullLayout.shapes
4343
fullLayout._shapeUpperLayer.selectAll('path').remove();
4444
fullLayout._shapeLowerLayer.selectAll('path').remove();
45-
fullLayout._shapeSubplotLayer.selectAll('path').remove();
45+
fullLayout._shapeSubplotLayers.selectAll('path').remove();
4646

4747
for(var i = 0; i < fullLayout.shapes.length; i++) {
4848
if(fullLayout.shapes[i].visible) {
@@ -55,8 +55,6 @@ function draw(gd) {
5555
}
5656

5757
function drawOne(gd, index) {
58-
var i, n;
59-
6058
// remove the existing shape if there is one.
6159
// because indices can change, we need to look in all shape layers
6260
gd._fullLayout._paper
@@ -70,28 +68,17 @@ function drawOne(gd, index) {
7068
// TODO: use d3 idioms instead of deleting and redrawing every time
7169
if(!optionsIn || options.visible === false) return;
7270

73-
var clipAxes;
7471
if(options.layer !== 'below') {
75-
clipAxes = (options.xref + options.yref).replace(/paper/g, '');
7672
drawShape(gd._fullLayout._shapeUpperLayer);
7773
}
78-
else if(options.xref === 'paper' && options.yref === 'paper') {
79-
clipAxes = '';
74+
else if(options.xref === 'paper' || options.yref === 'paper') {
8075
drawShape(gd._fullLayout._shapeLowerLayer);
8176
}
8277
else {
83-
var plots = gd._fullLayout._plots || {},
84-
subplots = Object.keys(plots),
85-
plotinfo;
86-
87-
for(i = 0, n = subplots.length; i < n; i++) {
88-
plotinfo = plots[subplots[i]];
89-
clipAxes = subplots[i];
78+
var plotinfo = gd._fullLayout._plots[options.xref + options.yref],
79+
mainPlot = plotinfo.mainplot || plotinfo;
9080

91-
if(isShapeInSubplot(gd, options, plotinfo)) {
92-
drawShape(plotinfo.shapelayer);
93-
}
94-
}
81+
drawShape(mainPlot.shapelayer);
9582
}
9683

9784
function drawShape(shapeLayer) {
@@ -110,10 +97,15 @@ function drawOne(gd, index) {
11097
.call(Color.fill, options.fillcolor)
11198
.call(Drawing.dashLine, options.line.dash, options.line.width);
11299

113-
if(clipAxes) {
114-
path.call(Drawing.setClipUrl,
115-
'clip' + gd._fullLayout._uid + clipAxes);
116-
}
100+
// note that for layer="below" the clipAxes can be different from the
101+
// subplot we're drawing this in. This could cause problems if the shape
102+
// spans two subplots. See https://github.com/plotly/plotly.js/issues/1452
103+
var clipAxes = (options.xref + options.yref).replace(/paper/g, '');
104+
105+
path.call(Drawing.setClipUrl, clipAxes ?
106+
('clip' + gd._fullLayout._uid + clipAxes) :
107+
null
108+
);
117109

118110
if(gd._context.editable) setupDragElement(gd, path, options, index);
119111
}
@@ -271,15 +263,6 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
271263
}
272264
}
273265

274-
function isShapeInSubplot(gd, shape, plotinfo) {
275-
var xa = Axes.getFromId(gd, plotinfo.id, 'x')._id,
276-
ya = Axes.getFromId(gd, plotinfo.id, 'y')._id,
277-
isBelow = shape.layer === 'below',
278-
inSuplotAxis = (xa === shape.xref || ya === shape.yref),
279-
isNotAnOverlaidSubplot = !!plotinfo.shapelayer;
280-
return isBelow && inSuplotAxis && isNotAnOverlaidSubplot;
281-
}
282-
283266
function getPathString(gd, options) {
284267
var type = options.type,
285268
xa = Axes.getFromId(gd, options.xref),

src/plot_api/plot_api.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,7 @@ Plotly.plot = function(gd, data, layout, config) {
308308

309309
// keep reference to shape layers in subplots
310310
var layerSubplot = fullLayout._paper.selectAll('.layer-subplot');
311-
fullLayout._imageSubplotLayer = layerSubplot.selectAll('.imagelayer');
312-
fullLayout._shapeSubplotLayer = layerSubplot.selectAll('.shapelayer');
311+
fullLayout._shapeSubplotLayers = layerSubplot.selectAll('.shapelayer');
313312

314313
// styling separate from drawing
315314
Plots.style(gd);
@@ -2855,11 +2854,20 @@ function makePlotFramework(gd) {
28552854
fullLayout._topdefs = fullLayout._toppaper.append('defs')
28562855
.attr('id', 'topdefs-' + fullLayout._uid);
28572856

2857+
fullLayout._bgLayer = fullLayout._paper.append('g')
2858+
.classed('bglayer', true);
2859+
28582860
fullLayout._draggers = fullLayout._paper.append('g')
28592861
.classed('draglayer', true);
28602862

2861-
// lower shape layer
2862-
// (only for shapes to be drawn below the whole plot)
2863+
// lower shape/image layer - note that this is behind
2864+
// all subplots data/grids but above the backgrounds
2865+
// except inset subplots, whose backgrounds are drawn
2866+
// inside their own group so that they appear above
2867+
// the data for the main subplot
2868+
// lower shapes and images which are fully referenced to
2869+
// a subplot still get drawn within the subplot's group
2870+
// so they will work correctly on insets
28632871
var layerBelow = fullLayout._paper.append('g')
28642872
.classed('layer-below', true);
28652873
fullLayout._imageLowerLayer = layerBelow.append('g')

src/plot_api/subroutines.js

+84-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
'use strict';
1111

12+
var d3 = require('d3');
1213
var Plotly = require('../plotly');
1314
var Registry = require('../registry');
1415
var Plots = require('../plots/plots');
@@ -24,6 +25,21 @@ exports.layoutStyles = function(gd) {
2425
return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd);
2526
};
2627

28+
function overlappingDomain(xDomain, yDomain, domains) {
29+
for(var i = 0; i < domains.length; i++) {
30+
var existingX = domains[i][0],
31+
existingY = domains[i][1];
32+
33+
if(existingX[0] >= xDomain[1] || existingX[1] <= xDomain[0]) {
34+
continue;
35+
}
36+
if(existingY[0] < yDomain[1] && existingY[1] > yDomain[0]) {
37+
return true;
38+
}
39+
}
40+
return false;
41+
}
42+
2743
exports.lsInner = function(gd) {
2844
var fullLayout = gd._fullLayout,
2945
gs = fullLayout._size,
@@ -43,8 +59,73 @@ exports.lsInner = function(gd) {
4359

4460
gd._context.setBackground(gd, fullLayout.paper_bgcolor);
4561

62+
var subplotSelection = fullLayout._paper.selectAll('g.subplot');
63+
64+
// figure out which backgrounds we need to draw, and in which layers
65+
// to put them
66+
var lowerBackgroundIDs = [];
67+
var lowerDomains = [];
68+
subplotSelection.each(function(subplot) {
69+
var plotinfo = fullLayout._plots[subplot];
70+
71+
if(plotinfo.mainplot) {
72+
// mainplot is a reference to the main plot this one is overlaid on
73+
// so if it exists, this is an overlaid plot and we don't need to
74+
// give it its own background
75+
if(plotinfo.bg) {
76+
plotinfo.bg.remove();
77+
}
78+
plotinfo.bg = undefined;
79+
return;
80+
}
81+
82+
var xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
83+
ya = Plotly.Axes.getFromId(gd, subplot, 'y'),
84+
xDomain = xa.domain,
85+
yDomain = ya.domain,
86+
plotgroupBgData = [];
87+
88+
if(overlappingDomain(xDomain, yDomain, lowerDomains)) {
89+
plotgroupBgData = [0];
90+
}
91+
else {
92+
lowerBackgroundIDs.push(subplot);
93+
lowerDomains.push([xDomain, yDomain]);
94+
}
95+
96+
// create the plot group backgrounds now, since
97+
// they're all independent selections
98+
var plotgroupBg = plotinfo.plotgroup.selectAll('.bg')
99+
.data(plotgroupBgData);
100+
101+
plotgroupBg.enter().append('rect')
102+
.classed('bg', true);
103+
104+
plotgroupBg.exit().remove();
105+
106+
plotgroupBg.each(function() {
107+
plotinfo.bg = plotgroupBg;
108+
var pgNode = plotinfo.plotgroup.node();
109+
pgNode.insertBefore(this, pgNode.childNodes[0]);
110+
});
111+
});
112+
113+
// now create all the lower-layer backgrounds at once now that
114+
// we have the list of subplots that need them
115+
var lowerBackgrounds = fullLayout._bgLayer.selectAll('.bg')
116+
.data(lowerBackgroundIDs);
117+
118+
lowerBackgrounds.enter().append('rect')
119+
.classed('bg', true);
120+
121+
lowerBackgrounds.exit().remove();
122+
123+
lowerBackgrounds.each(function(subplot) {
124+
fullLayout._plots[subplot].bg = d3.select(this);
125+
});
126+
46127
var freefinished = [];
47-
fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
128+
subplotSelection.each(function(subplot) {
48129
var plotinfo = fullLayout._plots[subplot],
49130
xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
50131
ya = Plotly.Axes.getFromId(gd, subplot, 'y');
@@ -58,7 +139,8 @@ exports.lsInner = function(gd) {
58139
.call(Drawing.setRect,
59140
xa._offset - gs.p, ya._offset - gs.p,
60141
xa._length + 2 * gs.p, ya._length + 2 * gs.p)
61-
.call(Color.fill, fullLayout.plot_bgcolor);
142+
.call(Color.fill, fullLayout.plot_bgcolor)
143+
.style('stroke-width', 0);
62144
}
63145

64146
// Clip so that data only shows up on the plot area.

src/plots/cartesian/index.js

-3
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,6 @@ function makeSubplotLayer(plotinfo) {
300300
}
301301

302302
if(!plotinfo.mainplot) {
303-
plotinfo.bg = joinLayer(plotgroup, 'rect', 'bg');
304-
plotinfo.bg.style('stroke-width', 0);
305-
306303
var backLayer = joinLayer(plotgroup, 'g', 'layer-subplot');
307304
plotinfo.shapelayer = joinLayer(backLayer, 'g', 'shapelayer');
308305
plotinfo.imagelayer = joinLayer(backLayer, 'g', 'imagelayer');

test/image/baselines/dendrogram.png

52 Bytes
Loading
269 Bytes
Loading

test/image/mocks/layout_image.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
}
1212
],
1313
"layout": {
14+
"plot_bgcolor": "rgba(0,0,0,0)",
1415
"xaxis2": {
1516
"anchor": "y2"
1617
},
@@ -19,7 +20,8 @@
1920
},
2021
"yaxis2": {
2122
"domain": [0.55, 1],
22-
"type": "log"
23+
"type": "log",
24+
"anchor": "x2"
2325
},
2426
"images": [
2527
{

0 commit comments

Comments
 (0)