Skip to content

Commit 1c84913

Browse files
committed
Merge pull request plotly#439 from n-riesco/issue-148-implement-shapes-below-traces
Add `layout.shapes.layer`
2 parents 79f3d89 + 81a44ce commit 1c84913

File tree

7 files changed

+404
-46
lines changed

7 files changed

+404
-46
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

+73-19
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,19 +203,23 @@ 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
}
211216

212217
function updateShape(gd, index, opt, value) {
213-
var i;
218+
var i, n;
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,
@@ -232,7 +238,7 @@ function updateShape(gd, index, opt, value) {
232238
else if(Lib.isPlainObject(opt)) optionsEdit = opt;
233239

234240
var optionKeys = Object.keys(optionsEdit);
235-
for(i = 0; i < optionsEdit.length; i++) {
241+
for(i = 0; i < optionKeys.length; i++) {
236242
var k = optionKeys[i];
237243
Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
238244
}
@@ -288,24 +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
},
295-
clipAxes = (options.xref + options.yref).replace(/paper/g, '');
301+
clipAxes;
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+
clipAxes = (options.xref + options.yref).replace(/paper/g, '');
307+
drawShape(gd._fullLayout._shapeUpperLayer);
308+
}
309+
else if(options.xref === 'paper' && options.yref === 'paper') {
310+
clipAxes = '';
311+
drawShape(gd._fullLayout._shapeLowerLayer);
312+
}
313+
else {
314+
var plots = gd._fullLayout._plots || {},
315+
subplots = Object.keys(plots),
316+
plotinfo;
317+
318+
for(i = 0, n = subplots.length; i < n; i++) {
319+
plotinfo = plots[subplots[i]];
320+
clipAxes = subplots[i];
321+
322+
if(isShapeInSubplot(gd, options, plotinfo.id)) {
323+
drawShape(plotinfo.shapelayer);
324+
}
325+
}
326+
}
327+
328+
function drawShape(shapeLayer) {
329+
var path = shapeLayer.append('path')
330+
.attr(attrs)
331+
.style('opacity', options.opacity)
332+
.call(Color.stroke, lineColor)
333+
.call(Color.fill, options.fillcolor)
334+
.call(Drawing.dashLine, options.line.dash, options.line.width);
335+
336+
if(clipAxes) {
337+
path.call(Drawing.setClipUrl,
338+
'clip' + gd._fullLayout._uid + clipAxes);
339+
}
340+
}
341+
}
342+
343+
function getShapeLayer(gd, index) {
344+
var shape = gd._fullLayout.shapes[index],
345+
shapeLayer = gd._fullLayout._shapeUpperLayer;
305346

306-
if(clipAxes) {
307-
path.call(Drawing.setClipUrl, 'clip' + gd._fullLayout._uid + clipAxes);
347+
if(!shape) {
348+
console.log('getShapeLayer: undefined shape: index', index);
349+
}
350+
else if(shape.layer === 'below') {
351+
shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ?
352+
gd._fullLayout._shapeLowerLayer :
353+
gd._fullLayout._subplotShapeLayer;
308354
}
355+
356+
return shapeLayer;
357+
}
358+
359+
function isShapeInSubplot(gd, shape, subplot) {
360+
var xa = Plotly.Axes.getFromId(gd, subplot, 'x')._id,
361+
ya = Plotly.Axes.getFromId(gd, subplot, 'y')._id;
362+
return shape.layer === 'below' && (xa === shape.xref || ya === shape.yref);
309363
}
310364

311365
function decodeDate(convertToPx) {

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
36.2 KB
Loading

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
}
+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
{
2+
"data": [
3+
{
4+
"line": {
5+
"shape": "spline"
6+
},
7+
"y": [
8+
1,
9+
2,
10+
1,
11+
0,
12+
-1,
13+
2,
14+
3,
15+
5
16+
]
17+
},
18+
{
19+
"line": {
20+
"shape": "spline"
21+
},
22+
"xaxis": "x2",
23+
"y": [
24+
7.071067811865475,
25+
10,
26+
7.071067811865475,
27+
0,
28+
-7.071067811865475,
29+
10,
30+
7.0710678118654755,
31+
-7.071067811865475
32+
]
33+
},
34+
{
35+
"line": {
36+
"shape": "spline"
37+
},
38+
"y": [
39+
7.0710678118654755,
40+
6.123233995736766e-16,
41+
7.0710678118654755,
42+
10,
43+
7.0710678118654755,
44+
6.123233995736766e-16,
45+
-7.071067811865475,
46+
-7.071067811865477
47+
],
48+
"yaxis": "y2"
49+
},
50+
{
51+
"line": {
52+
"shape": "spline"
53+
},
54+
"xaxis": "x2",
55+
"y": [
56+
2,
57+
1.6666666666666667,
58+
2,
59+
2.5,
60+
3.3333333333333335,
61+
1.6666666666666667,
62+
1.4285714285714286,
63+
1.1111111111111112
64+
],
65+
"yaxis": "y2"
66+
}
67+
],
68+
"layout": {
69+
"dragmode": "pan",
70+
"shapes": [
71+
{
72+
"fillcolor": "#c7eae5",
73+
"layer": "below",
74+
"type": "rect",
75+
"x0": 3.5,
76+
"x1": 4.5,
77+
"xref": "x",
78+
"y0": 0,
79+
"y1": 1,
80+
"yref": "paper"
81+
},
82+
{
83+
"fillcolor": "#c7eae5",
84+
"layer": "above",
85+
"opacity": 0.5,
86+
"type": "rect",
87+
"x0": 5.5,
88+
"x1": 6.5,
89+
"xref": "x2",
90+
"y0": 0,
91+
"y1": 1,
92+
"yref": "paper"
93+
},
94+
{
95+
"fillcolor": "#f6e8c3",
96+
"layer": "below",
97+
"type": "rect",
98+
"x0": 0,
99+
"x1": 1,
100+
"xref": "paper",
101+
"y0": 0,
102+
"y1": 3,
103+
"yref": "y"
104+
},
105+
{
106+
"fillcolor": "#f6e8c3",
107+
"layer": "above",
108+
"opacity": 0.5,
109+
"type": "rect",
110+
"x0": 0,
111+
"x1": 1,
112+
"xref": "paper",
113+
"y0": 1,
114+
"y1": 4,
115+
"yref": "y2"
116+
},
117+
{
118+
"fillcolor": "#d3d3d3",
119+
"layer": "below",
120+
"type": "rect",
121+
"x0": 0.3,
122+
"x1": 0.7,
123+
"xref": "paper",
124+
"y0": 0.3,
125+
"y1": 0.7,
126+
"yref": "paper"
127+
}
128+
],
129+
"showlegend": false,
130+
"title": "shape shading a region",
131+
"xaxis": {
132+
"domain": [
133+
0,
134+
0.45
135+
]
136+
},
137+
"xaxis2": {
138+
"domain": [
139+
0.55,
140+
1
141+
]
142+
},
143+
"yaxis": {
144+
"domain": [
145+
0,
146+
0.45
147+
]
148+
},
149+
"yaxis2": {
150+
"domain": [
151+
0.55,
152+
1
153+
]
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)