Skip to content
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

Add layout.shapes.layer #439

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
87 changes: 71 additions & 16 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,10 +203,13 @@ 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);
}
}
Expand All @@ -213,7 +218,8 @@ function updateShape(gd, index, opt, value) {
var i;

// 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 Down Expand Up @@ -288,23 +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, '');

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') {
drawShape(gd._fullLayout._shapeUpperLayer);
}
else if(options.xref === 'paper' && options.yref === 'paper') {
drawShape(gd._fullLayout._shapeLowerLayer);
} else {
forEachSubplot(gd, function(plotinfo) {
if(isShapeInSubplot(gd, options, plotinfo.id)) {
drawShape(plotinfo.shapelayer);
}
});
}

return;
Copy link
Contributor

Choose a reason for hiding this comment

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

🐄 no need for this. Save the bytes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will remove it.

I usually write closures at the end of a function and use the return to indicate that there are no more statements hidden after the closures.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand. I'm not saying that it's a bad habit. It's really a matter of consistency with the rest of the source code.


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(!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 forEachSubplot(gd, fn) {
Copy link
Contributor

@etpinard etpinard Apr 18, 2016

Choose a reason for hiding this comment

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

🐄 You may argue that adding forEachSubplot makes things more readable, but scrolling to the bottom of the page to see what a function does is anything but reader-friendly.

Please move the for loop below in the else block on line 310.

var plots = gd._fullLayout._plots || {},
subplots = Object.getOwnPropertyNames(plots);
Copy link
Contributor

Choose a reason for hiding this comment

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

🐄 Agreed. getOwnPropertyNames is safer, but fullLayout._plots should not have anything but own properties.

So, please swap Object.getOwnPropertyNames for Object.keys for consistency with the rest of the code (and to save a few bytes 😃 )


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

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
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"}
]
}
}
129 changes: 106 additions & 23 deletions test/jasmine/tests/shapes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,114 @@ describe('Test shapes:', function() {

afterEach(destroyGraphDiv);

function countShapeLayers() {
return d3.selectAll('.shapelayer').size();
function countShapesInLowerLayer() {
return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length;
}

function countShapePaths() {
return d3.selectAll('.shapelayer > path').size();
function countShapesInUpperLayer() {
return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length;
}

describe('DOM', function() {
it('has one *shapelayer* node', function() {
expect(countShapeLayers()).toEqual(1);
function countShapesInSubplots() {
return gd._fullLayout.shapes.filter(isShapeInSubplot).length;
}

function isShapeInUpperLayer(shape) {
return shape.layer !== 'below';
}

function isShapeInLowerLayer(shape) {
return (shape.xref === 'paper' && shape.yref === 'paper') &&
!isShapeInUpperLayer(shape);
}

function isShapeInSubplot(shape) {
return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape);
}

function countShapeLowerLayerNodes() {
return d3.selectAll('.shapelayer-below').size();
}

function countShapeUpperLayerNodes() {
return d3.selectAll('.shapelayer-above').size();
}

function countShapeLayerNodesInSubplots() {
return d3.selectAll('.shapelayer-subplot').size();
}

function countSubplots(gd) {
return Object.getOwnPropertyNames(gd._fullLayout._plots || {}).length;
Copy link
Contributor

Choose a reason for hiding this comment

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

🐄 Object.keys

}

function countShapePathsInLowerLayer() {
return d3.selectAll('.shapelayer-below > path').size();
}

function countShapePathsInUpperLayer() {
return d3.selectAll('.shapelayer-above > path').size();
}

function countShapePathsInSubplots() {
return d3.selectAll('.shapelayer-subplot > path').size();
}

describe('*shapeLowerLayer*', function() {
it('has one node', function() {
expect(countShapeLowerLayerNodes()).toEqual(1);
});

it('has as many *path* nodes as shapes in the lower layer', function() {
expect(countShapePathsInLowerLayer())
.toEqual(countShapesInLowerLayer());
});

it('should be able to get relayout', function(done) {
Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
expect(countShapeLowerLayerNodes()).toEqual(1);
expect(countShapePathsInLowerLayer())
.toEqual(countShapesInLowerLayer());
}).then(done);
});
});

describe('*shapeUpperLayer*', function() {
it('has one node', function() {
expect(countShapeUpperLayerNodes()).toEqual(1);
});

it('has as many *path* nodes as there are shapes', function() {
expect(countShapePaths()).toEqual(mock.layout.shapes.length);
it('has as many *path* nodes as shapes in the upper layer', function() {
expect(countShapePathsInUpperLayer())
.toEqual(countShapesInUpperLayer());
});

it('should be able to get relayout', function(done) {
expect(countShapeLayers()).toEqual(1);
expect(countShapePaths()).toEqual(mock.layout.shapes.length);
Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
expect(countShapeUpperLayerNodes()).toEqual(1);
expect(countShapePathsInUpperLayer())
.toEqual(countShapesInUpperLayer());
}).then(done);
});
});

describe('each *subplot*', function() {
it('has one *shapelayer*', function() {
expect(countShapeLayerNodesInSubplots())
.toEqual(countSubplots(gd));
});

it('has as many *path* nodes as shapes in the subplot', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

great tests! 🍻

expect(countShapePathsInSubplots())
.toEqual(countShapesInSubplots());
});

it('should be able to get relayout', function(done) {
Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
expect(countShapeLayers()).toEqual(1);
expect(countShapePaths()).toEqual(mock.layout.shapes.length);
expect(countShapeLayerNodesInSubplots())
.toEqual(countSubplots(gd));
expect(countShapePathsInSubplots())
.toEqual(countShapesInSubplots());
}).then(done);
});
});
Expand Down Expand Up @@ -75,33 +159,32 @@ describe('Test shapes:', function() {

describe('Plotly.relayout', function() {
it('should be able to add a shape', function(done) {
var pathCount = countShapePaths();
var pathCount = countShapePathsInUpperLayer();
var index = countShapes(gd);
var shape = getRandomShape();

Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() {
expect(countShapeLayers()).toEqual(1);
expect(countShapePaths()).toEqual(pathCount + 1);
Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function()
{
Copy link
Contributor

@etpinard etpinard Apr 18, 2016

Choose a reason for hiding this comment

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

we should really pick an eslint brace-style.

I'd vote for Stroustrup, I know @mdtusz would vote for 1tbs, but no one on our team would vote for Allman.

🐄 So please, bring up that { on that same line as function().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm trying to keep lines 80 chars or less. Shall I break the line before the function, like this:

            Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(
                function() {
                    expect(countShapePathsInUpperLayer())
                        .toEqual(pathCount + 1);
                    expect(getLastShape(gd)).toEqual(shape);
                    expect(countShapes(gd)).toEqual(index + 1);
                }).then(done);

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah good point. Don't worry about going over 80 characters. I don't think we'll ever enforce that rule in our jasmine test suites.

expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1);
expect(getLastShape(gd)).toEqual(shape);
expect(countShapes(gd)).toEqual(index + 1);
}).then(done);
});

it('should be able to remove a shape', function(done) {
var pathCount = countShapePaths();
var pathCount = countShapePathsInUpperLayer();
var index = countShapes(gd);
var shape = getRandomShape();

Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() {
expect(countShapeLayers()).toEqual(1);
expect(countShapePaths()).toEqual(pathCount + 1);
Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function()
{
Copy link
Contributor

Choose a reason for hiding this comment

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

🐄 again, bring up {

expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1);
expect(getLastShape(gd)).toEqual(shape);
expect(countShapes(gd)).toEqual(index + 1);
}).then(function() {
Plotly.relayout(gd, 'shapes[' + index + ']', 'remove');
}).then(function() {
expect(countShapeLayers()).toEqual(1);
expect(countShapePaths()).toEqual(pathCount);
expect(countShapePathsInUpperLayer()).toEqual(pathCount);
expect(countShapes(gd)).toEqual(index);
}).then(done);
});
Expand Down