Skip to content

Commit a8aa194

Browse files
committedApr 18, 2016
Add layout.shapes.layer
* Added functionality to show layer below traces. * The axis references of a 'below'-shape determine the subplots under which a shape is shown. * If both axis references of a 'below'-shape are set to 'paper', then the shape is shown below all the subplots. * Updated `test/image/mocks/shapes.json` to exercise the new functionality. * Updated `test/jasmine/tests/shapes_test.js` to account for the new shape layers.
1 parent e475be3 commit a8aa194

File tree

5 files changed

+210
-45
lines changed

5 files changed

+210
-45
lines changed
 

‎src/components/shapes/attributes.js

+8
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ module.exports = {
3838
].join(' ')
3939
},
4040

41+
layer: {
42+
valType: 'enumerated',
43+
values: ['below', 'above'],
44+
dflt: 'above',
45+
role: 'info',
46+
description: 'Specifies whether shapes are drawn below or above traces.'
47+
},
48+
4149
xref: extendFlat({}, annAttrs.xref, {
4250
description: [
4351
'Sets the shape\'s x coordinate axis.',

‎src/components/shapes/index.js

+71-16
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function handleShapeDefaults(shapeIn, fullLayout) {
3838
return Lib.coerce(shapeIn, shapeOut, shapes.layoutAttributes, attr, dflt);
3939
}
4040

41+
coerce('layer');
4142
coerce('opacity');
4243
coerce('fillcolor');
4344
coerce('line.color');
@@ -171,7 +172,8 @@ function updateAllShapes(gd, opt, value) {
171172
}
172173

173174
function deleteShape(gd, index) {
174-
gd._fullLayout._shapelayer.selectAll('[data-index="' + index + '"]')
175+
getShapeLayer(gd, index)
176+
.selectAll('[data-index="' + index + '"]')
175177
.remove();
176178

177179
gd._fullLayout.shapes.splice(index, 1);
@@ -181,9 +183,9 @@ function deleteShape(gd, index) {
181183
for(var i = index; i < gd._fullLayout.shapes.length; i++) {
182184
// redraw all shapes past the removed one,
183185
// so they bind to the right events
184-
gd._fullLayout._shapelayer
185-
.selectAll('[data-index="' + (i+1) + '"]')
186-
.attr('data-index', String(i));
186+
getShapeLayer(gd, i)
187+
.selectAll('[data-index="' + (i + 1) + '"]')
188+
.attr('data-index', i);
187189
shapes.draw(gd, i);
188190
}
189191
}
@@ -201,10 +203,13 @@ function insertShape(gd, index, newShape) {
201203
gd.layout.shapes = [rule];
202204
}
203205

206+
// there is no need to call shapes.draw(gd, index),
207+
// because updateShape() is called from within shapes.draw()
208+
204209
for(var i = gd._fullLayout.shapes.length - 1; i > index; i--) {
205-
gd._fullLayout._shapelayer
210+
getShapeLayer(gd, i)
206211
.selectAll('[data-index="' + (i - 1) + '"]')
207-
.attr('data-index', String(i));
212+
.attr('data-index', i);
208213
shapes.draw(gd, i);
209214
}
210215
}
@@ -213,7 +218,8 @@ function updateShape(gd, index, opt, value) {
213218
var i;
214219

215220
// remove the existing shape if there is one
216-
gd._fullLayout._shapelayer.selectAll('[data-index="' + index + '"]')
221+
getShapeLayer(gd, index)
222+
.selectAll('[data-index="' + index + '"]')
217223
.remove();
218224

219225
// remember a few things about what was already there,
@@ -288,23 +294,72 @@ function updateShape(gd, index, opt, value) {
288294
gd._fullLayout.shapes[index] = options;
289295

290296
var attrs = {
291-
'data-index': String(index),
297+
'data-index': index,
292298
'fill-rule': 'evenodd',
293299
d: shapePath(gd, options)
294300
},
295301
clipAxes = (options.xref + options.yref).replace(/paper/g, '');
296302

297303
var lineColor = options.line.width ? options.line.color : 'rgba(0,0,0,0)';
298304

299-
var path = gd._fullLayout._shapelayer.append('path')
300-
.attr(attrs)
301-
.style('opacity', options.opacity)
302-
.call(Color.stroke, lineColor)
303-
.call(Color.fill, options.fillcolor)
304-
.call(Drawing.dashLine, options.line.dash, options.line.width);
305+
if(options.layer !== 'below') {
306+
drawShape(gd._fullLayout._shapeUpperLayer);
307+
}
308+
else if(options.xref === 'paper' && options.yref === 'paper') {
309+
drawShape(gd._fullLayout._shapeLowerLayer);
310+
} else {
311+
forEachSubplot(gd, function(plotinfo) {
312+
if(isShapeInSubplot(gd, options, plotinfo.id)) {
313+
drawShape(plotinfo.shapelayer);
314+
}
315+
});
316+
}
317+
318+
return;
319+
320+
function drawShape(shapeLayer) {
321+
var path = shapeLayer.append('path')
322+
.attr(attrs)
323+
.style('opacity', options.opacity)
324+
.call(Color.stroke, lineColor)
325+
.call(Color.fill, options.fillcolor)
326+
.call(Drawing.dashLine, options.line.dash, options.line.width);
327+
328+
if(clipAxes) {
329+
path.call(Drawing.setClipUrl,
330+
'clip' + gd._fullLayout._uid + clipAxes);
331+
}
332+
}
333+
}
334+
335+
function getShapeLayer(gd, index) {
336+
var shape = gd._fullLayout.shapes[index],
337+
shapeLayer = gd._fullLayout._shapeUpperLayer;
338+
339+
if(!shape) {
340+
console.log('getShapeLayer: undefined shape: index', index);
341+
}
342+
else if(shape.layer === 'below') {
343+
shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ?
344+
gd._fullLayout._shapeLowerLayer :
345+
gd._fullLayout._subplotShapeLayer;
346+
}
347+
348+
return shapeLayer;
349+
}
350+
351+
function isShapeInSubplot(gd, shape, subplot) {
352+
var xa = Plotly.Axes.getFromId(gd, subplot, 'x')._id,
353+
ya = Plotly.Axes.getFromId(gd, subplot, 'y')._id;
354+
return shape.layer === 'below' && (xa === shape.xref || ya === shape.yref);
355+
}
356+
357+
function forEachSubplot(gd, fn) {
358+
var plots = gd._fullLayout._plots || {},
359+
subplots = Object.getOwnPropertyNames(plots);
305360

306-
if(clipAxes) {
307-
path.call(Drawing.setClipUrl, 'clip' + gd._fullLayout._uid + clipAxes);
361+
for(var i = 0, n = subplots.length; i < n; i++) {
362+
fn(plots[subplots[i]]);
308363
}
309364
}
310365

‎src/plot_api/plot_api.js

+21-2
Original file line numberDiff line numberDiff line change
@@ -2615,16 +2615,27 @@ function makePlotFramework(gd) {
26152615
fullLayout._draggers = fullLayout._paper.append('g')
26162616
.classed('draglayer', true);
26172617

2618+
// lower shape layer
2619+
// (only for shapes to be drawn below the whole plot)
2620+
fullLayout._shapeLowerLayer = fullLayout._paper.append('g')
2621+
.classed('shapelayer shapelayer-below', true);
2622+
26182623
var subplots = Plotly.Axes.getSubplots(gd);
26192624
if(subplots.join('') !== Object.keys(gd._fullLayout._plots || {}).join('')) {
26202625
makeSubplots(gd, subplots);
26212626
}
26222627

26232628
if(fullLayout._hasCartesian) makeCartesianPlotFramwork(gd, subplots);
26242629

2625-
// single ternary, shape and pie layers for the whole plot
2630+
// single ternary layer for the whole plot
26262631
fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true);
2627-
fullLayout._shapelayer = fullLayout._paper.append('g').classed('shapelayer', true);
2632+
2633+
// upper shape layer
2634+
// (only for shapes to be drawn above the whole plot, including subplots)
2635+
fullLayout._shapeUpperLayer = fullLayout._paper.append('g')
2636+
.classed('shapelayer shapelayer-above', true);
2637+
2638+
// single pie layer for the whole plot
26282639
fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);
26292640

26302641
// fill in image server scrape-svg
@@ -2752,6 +2763,10 @@ function makeCartesianPlotFramwork(gd, subplots) {
27522763
// the plot and containers for overlays
27532764
plotinfo.bg = plotgroup.append('rect')
27542765
.style('stroke-width', 0);
2766+
// shape layer
2767+
// (only for shapes to be drawn below a subplot)
2768+
plotinfo.shapelayer = plotgroup.append('g')
2769+
.classed('shapelayer shapelayer-subplot', true);
27552770
plotinfo.gridlayer = plotgroup.append('g');
27562771
plotinfo.overgrid = plotgroup.append('g');
27572772
plotinfo.zerolinelayer = plotgroup.append('g');
@@ -2800,6 +2815,10 @@ function makeCartesianPlotFramwork(gd, subplots) {
28002815
.style('fill', 'none')
28012816
.classed('crisp', true);
28022817
});
2818+
2819+
// shape layers in subplots
2820+
fullLayout._subplotShapeLayer = fullLayout._paper
2821+
.selectAll('.shapelayer-subplot');
28032822
}
28042823

28052824
// layoutStyles: styling for plot layout elements

‎test/image/mocks/shapes.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@
2121
"margin": {"l":20,"r":20,"top":10,"bottom":10,"pad":0},
2222
"showlegend":false,
2323
"shapes":[
24-
{"xref":"paper","yref":"paper","x0":0,"x1":0.1,"y0":0,"y1":0.1},
24+
{"layer":"below","xref":"paper","yref":"paper","x0":0,"x1":0.1,"y0":0,"y1":0.1},
2525
{"xref":"paper","yref":"paper","path":"M0,0.2V0.3H0.05L0,0.4Q0.1,0.4 0.1,0.3T0.15,0.3C0.1,0.4 0.2,0.4 0.2,0.3S0.15,0.3 0.15,0.2Z","fillcolor":"#4c0"},
2626
{"xref":"paper","yref":"paper","type":"circle","x0":0.23,"x1":0.3,"y0":0.2,"y1":0.4},
2727
{"xref":"paper","yref":"paper","type":"line","x0":0.2,"x1":0.3,"y0":0,"y1":0.1},
28-
{"x0":0.1,"x1":0.4,"y0":1.5,"y1":20,"opacity":0.5,"fillcolor":"#f00","line":{"width":8,"color":"#008","dash":"dashdot"}},
28+
{"layer":"below","x0":0.1,"x1":0.4,"y0":1.5,"y1":20,"opacity":0.5,"fillcolor":"#f00","line":{"width":8,"color":"#008","dash":"dashdot"}},
2929
{"path":"M0.5,3C0.5,9 0.9,9 0.9,3C0.9,1 0.5,1 0.5,3ZM0.6,4C0.6,5 0.66,5 0.66,4ZM0.74,4C0.74,5 0.8,5 0.8,4ZM0.6,3C0.63,2 0.77,2 0.8,3Z","fillcolor":"#fd2","line":{"width":1,"color":"black"}},
30-
{"xref":"x2","yref":"y2","type":"circle","x0":"2000-01-01 02","x1":"2000-01-01 08:30:33.456","y0":0.1,"y1":0.9,"fillcolor":"rgba(0,0,0,0.5)","line":{"color":"rgba(0,255,0,0.5)", "width":5}},
30+
{"layer":"below","xref":"x2","yref":"y2","type":"circle","x0":"2000-01-01 02","x1":"2000-01-01 08:30:33.456","y0":0.1,"y1":0.9,"fillcolor":"rgba(0,0,0,0.5)","line":{"color":"rgba(0,255,0,0.5)", "width":5}},
3131
{"xref":"x2","yref":"y2","path":"M2000-01-01_11:20:45.6,0.2Q2000-01-01_10:00,0.85 2000-01-01_21,0.8Q2000-01-01_22:20,0.15 2000-01-01_11:20:45.6,0.2Z","fillcolor":"rgb(151,73,58)"},
3232
{"yref":"paper","type":"line","x0":0.1,"x1":0.4,"y0":0,"y1":0.4,"line":{"color":"#009","dash":"dot","width":1}},
3333
{"yref":"paper","path":"M0.5,0H1.1L0.8,0.4Z","line":{"width":0},"fillcolor":"#ccd3ff"},
3434
{"xref":"paper","x0":0.1,"x1":0.2,"y0":-1,"y1":3,"fillcolor":"#ccc"},
35-
{"xref":"paper","path":"M0.05,4C0.4,12 -0.1,12 0.25,4Z","fillcolor":"#a66"}
35+
{"layer":"above","xref":"paper","path":"M0.05,4C0.4,12 -0.1,12 0.25,4Z","fillcolor":"#a66"}
3636
]
3737
}
3838
}

‎test/jasmine/tests/shapes_test.js

+106-23
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,114 @@ describe('Test shapes:', function() {
2424

2525
afterEach(destroyGraphDiv);
2626

27-
function countShapeLayers() {
28-
return d3.selectAll('.shapelayer').size();
27+
function countShapesInLowerLayer() {
28+
return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length;
2929
}
3030

31-
function countShapePaths() {
32-
return d3.selectAll('.shapelayer > path').size();
31+
function countShapesInUpperLayer() {
32+
return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length;
3333
}
3434

35-
describe('DOM', function() {
36-
it('has one *shapelayer* node', function() {
37-
expect(countShapeLayers()).toEqual(1);
35+
function countShapesInSubplots() {
36+
return gd._fullLayout.shapes.filter(isShapeInSubplot).length;
37+
}
38+
39+
function isShapeInUpperLayer(shape) {
40+
return shape.layer !== 'below';
41+
}
42+
43+
function isShapeInLowerLayer(shape) {
44+
return (shape.xref === 'paper' && shape.yref === 'paper') &&
45+
!isShapeInUpperLayer(shape);
46+
}
47+
48+
function isShapeInSubplot(shape) {
49+
return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape);
50+
}
51+
52+
function countShapeLowerLayerNodes() {
53+
return d3.selectAll('.shapelayer-below').size();
54+
}
55+
56+
function countShapeUpperLayerNodes() {
57+
return d3.selectAll('.shapelayer-above').size();
58+
}
59+
60+
function countShapeLayerNodesInSubplots() {
61+
return d3.selectAll('.shapelayer-subplot').size();
62+
}
63+
64+
function countSubplots(gd) {
65+
return Object.getOwnPropertyNames(gd._fullLayout._plots || {}).length;
66+
}
67+
68+
function countShapePathsInLowerLayer() {
69+
return d3.selectAll('.shapelayer-below > path').size();
70+
}
71+
72+
function countShapePathsInUpperLayer() {
73+
return d3.selectAll('.shapelayer-above > path').size();
74+
}
75+
76+
function countShapePathsInSubplots() {
77+
return d3.selectAll('.shapelayer-subplot > path').size();
78+
}
79+
80+
describe('*shapeLowerLayer*', function() {
81+
it('has one node', function() {
82+
expect(countShapeLowerLayerNodes()).toEqual(1);
83+
});
84+
85+
it('has as many *path* nodes as shapes in the lower layer', function() {
86+
expect(countShapePathsInLowerLayer())
87+
.toEqual(countShapesInLowerLayer());
88+
});
89+
90+
it('should be able to get relayout', function(done) {
91+
Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
92+
expect(countShapeLowerLayerNodes()).toEqual(1);
93+
expect(countShapePathsInLowerLayer())
94+
.toEqual(countShapesInLowerLayer());
95+
}).then(done);
96+
});
97+
});
98+
99+
describe('*shapeUpperLayer*', function() {
100+
it('has one node', function() {
101+
expect(countShapeUpperLayerNodes()).toEqual(1);
38102
});
39103

40-
it('has as many *path* nodes as there are shapes', function() {
41-
expect(countShapePaths()).toEqual(mock.layout.shapes.length);
104+
it('has as many *path* nodes as shapes in the upper layer', function() {
105+
expect(countShapePathsInUpperLayer())
106+
.toEqual(countShapesInUpperLayer());
42107
});
43108

44109
it('should be able to get relayout', function(done) {
45-
expect(countShapeLayers()).toEqual(1);
46-
expect(countShapePaths()).toEqual(mock.layout.shapes.length);
110+
Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
111+
expect(countShapeUpperLayerNodes()).toEqual(1);
112+
expect(countShapePathsInUpperLayer())
113+
.toEqual(countShapesInUpperLayer());
114+
}).then(done);
115+
});
116+
});
47117

118+
describe('each *subplot*', function() {
119+
it('has one *shapelayer*', function() {
120+
expect(countShapeLayerNodesInSubplots())
121+
.toEqual(countSubplots(gd));
122+
});
123+
124+
it('has as many *path* nodes as shapes in the subplot', function() {
125+
expect(countShapePathsInSubplots())
126+
.toEqual(countShapesInSubplots());
127+
});
128+
129+
it('should be able to get relayout', function(done) {
48130
Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
49-
expect(countShapeLayers()).toEqual(1);
50-
expect(countShapePaths()).toEqual(mock.layout.shapes.length);
131+
expect(countShapeLayerNodesInSubplots())
132+
.toEqual(countSubplots(gd));
133+
expect(countShapePathsInSubplots())
134+
.toEqual(countShapesInSubplots());
51135
}).then(done);
52136
});
53137
});
@@ -75,33 +159,32 @@ describe('Test shapes:', function() {
75159

76160
describe('Plotly.relayout', function() {
77161
it('should be able to add a shape', function(done) {
78-
var pathCount = countShapePaths();
162+
var pathCount = countShapePathsInUpperLayer();
79163
var index = countShapes(gd);
80164
var shape = getRandomShape();
81165

82-
Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() {
83-
expect(countShapeLayers()).toEqual(1);
84-
expect(countShapePaths()).toEqual(pathCount + 1);
166+
Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function()
167+
{
168+
expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1);
85169
expect(getLastShape(gd)).toEqual(shape);
86170
expect(countShapes(gd)).toEqual(index + 1);
87171
}).then(done);
88172
});
89173

90174
it('should be able to remove a shape', function(done) {
91-
var pathCount = countShapePaths();
175+
var pathCount = countShapePathsInUpperLayer();
92176
var index = countShapes(gd);
93177
var shape = getRandomShape();
94178

95-
Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() {
96-
expect(countShapeLayers()).toEqual(1);
97-
expect(countShapePaths()).toEqual(pathCount + 1);
179+
Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function()
180+
{
181+
expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1);
98182
expect(getLastShape(gd)).toEqual(shape);
99183
expect(countShapes(gd)).toEqual(index + 1);
100184
}).then(function() {
101185
Plotly.relayout(gd, 'shapes[' + index + ']', 'remove');
102186
}).then(function() {
103-
expect(countShapeLayers()).toEqual(1);
104-
expect(countShapePaths()).toEqual(pathCount);
187+
expect(countShapePathsInUpperLayer()).toEqual(pathCount);
105188
expect(countShapes(gd)).toEqual(index);
106189
}).then(done);
107190
});

0 commit comments

Comments
 (0)
Please sign in to comment.