Skip to content

Add layout.shapes.layer #439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/components/shapes/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ module.exports = {
].join(' ')
},

layer: {
valType: 'enumerated',
values: ['below', 'above'],
dflt: 'above',
role: 'info',
description: 'Specifies whether shapes are drawn below or above traces.'
},

xref: extendFlat({}, annAttrs.xref, {
description: [
'Sets the shape\'s x coordinate axis.',
Expand Down
92 changes: 73 additions & 19 deletions src/components/shapes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function handleShapeDefaults(shapeIn, fullLayout) {
return Lib.coerce(shapeIn, shapeOut, shapes.layoutAttributes, attr, dflt);
}

coerce('layer');
coerce('opacity');
coerce('fillcolor');
coerce('line.color');
Expand Down Expand Up @@ -171,7 +172,8 @@ function updateAllShapes(gd, opt, value) {
}

function deleteShape(gd, index) {
gd._fullLayout._shapelayer.selectAll('[data-index="' + index + '"]')
getShapeLayer(gd, index)
.selectAll('[data-index="' + index + '"]')
.remove();

gd._fullLayout.shapes.splice(index, 1);
Expand All @@ -181,9 +183,9 @@ function deleteShape(gd, index) {
for(var i = index; i < gd._fullLayout.shapes.length; i++) {
// redraw all shapes past the removed one,
// so they bind to the right events
gd._fullLayout._shapelayer
.selectAll('[data-index="' + (i+1) + '"]')
.attr('data-index', String(i));
getShapeLayer(gd, i)
.selectAll('[data-index="' + (i + 1) + '"]')
.attr('data-index', i);
shapes.draw(gd, i);
}
}
Expand All @@ -201,19 +203,23 @@ function insertShape(gd, index, newShape) {
gd.layout.shapes = [rule];
}

// there is no need to call shapes.draw(gd, index),
// because updateShape() is called from within shapes.draw()

for(var i = gd._fullLayout.shapes.length - 1; i > index; i--) {
gd._fullLayout._shapelayer
getShapeLayer(gd, i)
.selectAll('[data-index="' + (i - 1) + '"]')
.attr('data-index', String(i));
.attr('data-index', i);
shapes.draw(gd, i);
}
}

function updateShape(gd, index, opt, value) {
var i;
var i, n;

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

// remember a few things about what was already there,
Expand All @@ -232,7 +238,7 @@ function updateShape(gd, index, opt, value) {
else if(Lib.isPlainObject(opt)) optionsEdit = opt;

var optionKeys = Object.keys(optionsEdit);
for(i = 0; i < optionsEdit.length; i++) {
for(i = 0; i < optionKeys.length; i++) {
var k = optionKeys[i];
Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
}
Expand Down Expand Up @@ -288,24 +294,72 @@ function updateShape(gd, index, opt, value) {
gd._fullLayout.shapes[index] = options;

var attrs = {
'data-index': String(index),
'data-index': index,
'fill-rule': 'evenodd',
d: shapePath(gd, options)
},
clipAxes = (options.xref + options.yref).replace(/paper/g, '');
clipAxes;

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

var path = gd._fullLayout._shapelayer.append('path')
.attr(attrs)
.style('opacity', options.opacity)
.call(Color.stroke, lineColor)
.call(Color.fill, options.fillcolor)
.call(Drawing.dashLine, options.line.dash, options.line.width);
if(options.layer !== 'below') {
clipAxes = (options.xref + options.yref).replace(/paper/g, '');
drawShape(gd._fullLayout._shapeUpperLayer);
}
else if(options.xref === 'paper' && options.yref === 'paper') {
clipAxes = '';
drawShape(gd._fullLayout._shapeLowerLayer);
}
else {
var plots = gd._fullLayout._plots || {},
subplots = Object.keys(plots),
plotinfo;

for(i = 0, n = subplots.length; i < n; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you very much

plotinfo = plots[subplots[i]];
clipAxes = subplots[i];

if(isShapeInSubplot(gd, options, plotinfo.id)) {
drawShape(plotinfo.shapelayer);
}
}
}

function drawShape(shapeLayer) {
var path = shapeLayer.append('path')
.attr(attrs)
.style('opacity', options.opacity)
.call(Color.stroke, lineColor)
.call(Color.fill, options.fillcolor)
.call(Drawing.dashLine, options.line.dash, options.line.width);

if(clipAxes) {
path.call(Drawing.setClipUrl,
'clip' + gd._fullLayout._uid + clipAxes);
}
}
}

function getShapeLayer(gd, index) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice abstraction here. 🍻

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this thing possible in 3d ???

var shape = gd._fullLayout.shapes[index],
shapeLayer = gd._fullLayout._shapeUpperLayer;

if(clipAxes) {
path.call(Drawing.setClipUrl, 'clip' + gd._fullLayout._uid + clipAxes);
if(!shape) {
console.log('getShapeLayer: undefined shape: index', index);
}
else if(shape.layer === 'below') {
shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ?
gd._fullLayout._shapeLowerLayer :
gd._fullLayout._subplotShapeLayer;
}

return shapeLayer;
}

function isShapeInSubplot(gd, shape, subplot) {
var xa = Plotly.Axes.getFromId(gd, subplot, 'x')._id,
ya = Plotly.Axes.getFromId(gd, subplot, 'y')._id;
return shape.layer === 'below' && (xa === shape.xref || ya === shape.yref);
}

function decodeDate(convertToPx) {
Expand Down
23 changes: 21 additions & 2 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2615,16 +2615,27 @@ function makePlotFramework(gd) {
fullLayout._draggers = fullLayout._paper.append('g')
.classed('draglayer', true);

// lower shape layer
// (only for shapes to be drawn below the whole plot)
fullLayout._shapeLowerLayer = fullLayout._paper.append('g')
.classed('shapelayer shapelayer-below', true);

var subplots = Plotly.Axes.getSubplots(gd);
if(subplots.join('') !== Object.keys(gd._fullLayout._plots || {}).join('')) {
makeSubplots(gd, subplots);
}

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

// single ternary, shape and pie layers for the whole plot
// single ternary layer for the whole plot
fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true);
fullLayout._shapelayer = fullLayout._paper.append('g').classed('shapelayer', true);

// upper shape layer
// (only for shapes to be drawn above the whole plot, including subplots)
fullLayout._shapeUpperLayer = fullLayout._paper.append('g')
.classed('shapelayer shapelayer-above', true);

// single pie layer for the whole plot
fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);

// fill in image server scrape-svg
Expand Down Expand Up @@ -2752,6 +2763,10 @@ function makeCartesianPlotFramwork(gd, subplots) {
// the plot and containers for overlays
plotinfo.bg = plotgroup.append('rect')
.style('stroke-width', 0);
// shape layer
// (only for shapes to be drawn below a subplot)
plotinfo.shapelayer = plotgroup.append('g')
.classed('shapelayer shapelayer-subplot', true);
plotinfo.gridlayer = plotgroup.append('g');
plotinfo.overgrid = plotgroup.append('g');
plotinfo.zerolinelayer = plotgroup.append('g');
Expand Down Expand Up @@ -2800,6 +2815,10 @@ function makeCartesianPlotFramwork(gd, subplots) {
.style('fill', 'none')
.classed('crisp', true);
});

// shape layers in subplots
fullLayout._subplotShapeLayer = fullLayout._paper
.selectAll('.shapelayer-subplot');
}

// layoutStyles: styling for plot layout elements
Expand Down
Binary file added test/image/baselines/shapes_below_traces.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions test/image/mocks/shapes.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@
"margin": {"l":20,"r":20,"top":10,"bottom":10,"pad":0},
"showlegend":false,
"shapes":[
{"xref":"paper","yref":"paper","x0":0,"x1":0.1,"y0":0,"y1":0.1},
{"layer":"below","xref":"paper","yref":"paper","x0":0,"x1":0.1,"y0":0,"y1":0.1},
{"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"},
{"xref":"paper","yref":"paper","type":"circle","x0":0.23,"x1":0.3,"y0":0.2,"y1":0.4},
{"xref":"paper","yref":"paper","type":"line","x0":0.2,"x1":0.3,"y0":0,"y1":0.1},
{"x0":0.1,"x1":0.4,"y0":1.5,"y1":20,"opacity":0.5,"fillcolor":"#f00","line":{"width":8,"color":"#008","dash":"dashdot"}},
{"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"}},
{"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"}},
{"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}},
{"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}},
{"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)"},
{"yref":"paper","type":"line","x0":0.1,"x1":0.4,"y0":0,"y1":0.4,"line":{"color":"#009","dash":"dot","width":1}},
{"yref":"paper","path":"M0.5,0H1.1L0.8,0.4Z","line":{"width":0},"fillcolor":"#ccd3ff"},
{"xref":"paper","x0":0.1,"x1":0.2,"y0":-1,"y1":3,"fillcolor":"#ccc"},
{"xref":"paper","path":"M0.05,4C0.4,12 -0.1,12 0.25,4Z","fillcolor":"#a66"}
{"layer":"above","xref":"paper","path":"M0.05,4C0.4,12 -0.1,12 0.25,4Z","fillcolor":"#a66"}
]
}
}
156 changes: 156 additions & 0 deletions test/image/mocks/shapes_below_traces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"data": [
{
"line": {
"shape": "spline"
},
"y": [
1,
2,
1,
0,
-1,
2,
3,
5
]
},
{
"line": {
"shape": "spline"
},
"xaxis": "x2",
"y": [
7.071067811865475,
10,
7.071067811865475,
0,
-7.071067811865475,
10,
7.0710678118654755,
-7.071067811865475
]
},
{
"line": {
"shape": "spline"
},
"y": [
7.0710678118654755,
6.123233995736766e-16,
7.0710678118654755,
10,
7.0710678118654755,
6.123233995736766e-16,
-7.071067811865475,
-7.071067811865477
],
"yaxis": "y2"
},
{
"line": {
"shape": "spline"
},
"xaxis": "x2",
"y": [
2,
1.6666666666666667,
2,
2.5,
3.3333333333333335,
1.6666666666666667,
1.4285714285714286,
1.1111111111111112
],
"yaxis": "y2"
}
],
"layout": {
"dragmode": "pan",
"shapes": [
{
"fillcolor": "#c7eae5",
"layer": "below",
"type": "rect",
"x0": 3.5,
"x1": 4.5,
"xref": "x",
"y0": 0,
"y1": 1,
"yref": "paper"
},
{
"fillcolor": "#c7eae5",
"layer": "above",
"opacity": 0.5,
"type": "rect",
"x0": 5.5,
"x1": 6.5,
"xref": "x2",
"y0": 0,
"y1": 1,
"yref": "paper"
},
{
"fillcolor": "#f6e8c3",
"layer": "below",
"type": "rect",
"x0": 0,
"x1": 1,
"xref": "paper",
"y0": 0,
"y1": 3,
"yref": "y"
},
{
"fillcolor": "#f6e8c3",
"layer": "above",
"opacity": 0.5,
"type": "rect",
"x0": 0,
"x1": 1,
"xref": "paper",
"y0": 1,
"y1": 4,
"yref": "y2"
},
{
"fillcolor": "#d3d3d3",
"layer": "below",
"type": "rect",
"x0": 0.3,
"x1": 0.7,
"xref": "paper",
"y0": 0.3,
"y1": 0.7,
"yref": "paper"
}
],
"showlegend": false,
"title": "shape shading a region",
"xaxis": {
"domain": [
0,
0.45
]
},
"xaxis2": {
"domain": [
0.55,
1
]
},
"yaxis": {
"domain": [
0,
0.45
]
},
"yaxis2": {
"domain": [
0.55,
1
]
}
}
}
Loading