Skip to content

Image and shape clip paths #1453

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 6 commits into from
Mar 9, 2017
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
58 changes: 43 additions & 15 deletions src/components/images/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
module.exports = function draw(gd) {
var fullLayout = gd._fullLayout,
imageDataAbove = [],
imageDataSubplot = [],
imageDataBelow = [];
imageDataSubplot = {},
imageDataBelow = [],
subplot,
i;

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

if(img.visible) {
if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') {
imageDataSubplot.push(img);
subplot = img.xref + img.yref;

var plotinfo = fullLayout._plots[subplot];
if(plotinfo.mainplot) {
subplot = plotinfo.mainplot.id;
}

if(!imageDataSubplot[subplot]) {
imageDataSubplot[subplot] = [];
}
imageDataSubplot[subplot].push(img);
} else if(img.layer === 'above') {
imageDataAbove.push(img);
} else {
Expand Down Expand Up @@ -143,36 +155,52 @@ module.exports = function draw(gd) {
yId = ya ? ya._id : '',
clipAxes = xId + yId;

if(clipAxes) {
thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes);
}
thisImage.call(Drawing.setClipUrl, clipAxes ?
('clip' + fullLayout._uid + clipAxes) :
null
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Needed to unset the clip path if you change from data-referenced to fully paper-referenced.

}

var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
.data(imageDataBelow),
imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image')
.data(imageDataSubplot),
imagesAbove = fullLayout._imageUpperLayer.selectAll('image')
.data(imageDataAbove);

imagesBelow.enter().append('image');
imagesSubplot.enter().append('image');
imagesAbove.enter().append('image');

imagesBelow.exit().remove();
imagesSubplot.exit().remove();
imagesAbove.exit().remove();

imagesBelow.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});
imagesSubplot.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});
imagesAbove.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});

var allSubplots = Object.keys(fullLayout._plots);
for(i = 0; i < allSubplots.length; i++) {
subplot = allSubplots[i];
var subplotObj = fullLayout._plots[subplot];

// filter out overlaid plots (which havd their images on the main plot)
// and gl2d plots (which don't support below images, at least not yet)
if(!subplotObj.imagelayer) continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good 👍


var imagesOnSubplot = subplotObj.imagelayer.selectAll('image')
// even if there are no images on this subplot, we need to run
// enter and exit in case there were previously
.data(imageDataSubplot[subplot] || []);

imagesOnSubplot.enter().append('image');
imagesOnSubplot.exit().remove();

imagesOnSubplot.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});
}
};
4 changes: 0 additions & 4 deletions src/components/rangeslider/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,6 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
}

Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id));

// no need for the bg layer,
// drawBg handles coloring the background
if(isMainPlot) plotinfo.bg.remove();
});
}

Expand Down
45 changes: 14 additions & 31 deletions src/components/shapes/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function draw(gd) {
// Remove previous shapes before drawing new in shapes in fullLayout.shapes
fullLayout._shapeUpperLayer.selectAll('path').remove();
fullLayout._shapeLowerLayer.selectAll('path').remove();
fullLayout._shapeSubplotLayer.selectAll('path').remove();
fullLayout._shapeSubplotLayers.selectAll('path').remove();

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

function drawOne(gd, index) {
var i, n;

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

var clipAxes;
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 = '';
else if(options.xref === 'paper' || options.yref === 'paper') {
drawShape(gd._fullLayout._shapeLowerLayer);
}
else {
var plots = gd._fullLayout._plots || {},
subplots = Object.keys(plots),
plotinfo;

for(i = 0, n = subplots.length; i < n; i++) {
plotinfo = plots[subplots[i]];
clipAxes = subplots[i];
var plotinfo = gd._fullLayout._plots[options.xref + options.yref],
mainPlot = plotinfo.mainplot || plotinfo;

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

function drawShape(shapeLayer) {
Expand All @@ -110,10 +97,15 @@ function drawOne(gd, index) {
.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);
}
// note that for layer="below" the clipAxes can be different from the
// subplot we're drawing this in. This could cause problems if the shape
// spans two subplots. See https://github.com/plotly/plotly.js/issues/1452
var clipAxes = (options.xref + options.yref).replace(/paper/g, '');

path.call(Drawing.setClipUrl, clipAxes ?
('clip' + gd._fullLayout._uid + clipAxes) :
null
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice way to unset a clip path. 👍

);

if(gd._context.editable) setupDragElement(gd, path, options, index);
}
Expand Down Expand Up @@ -271,15 +263,6 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
}
}

function isShapeInSubplot(gd, shape, plotinfo) {
var xa = Axes.getFromId(gd, plotinfo.id, 'x')._id,
ya = Axes.getFromId(gd, plotinfo.id, 'y')._id,
isBelow = shape.layer === 'below',
inSuplotAxis = (xa === shape.xref || ya === shape.yref),
isNotAnOverlaidSubplot = !!plotinfo.shapelayer;
return isBelow && inSuplotAxis && isNotAnOverlaidSubplot;
}

function getPathString(gd, options) {
var type = options.type,
xa = Axes.getFromId(gd, options.xref),
Expand Down
16 changes: 12 additions & 4 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,7 @@ Plotly.plot = function(gd, data, layout, config) {

// keep reference to shape layers in subplots
var layerSubplot = fullLayout._paper.selectAll('.layer-subplot');
fullLayout._imageSubplotLayer = layerSubplot.selectAll('.imagelayer');
fullLayout._shapeSubplotLayer = layerSubplot.selectAll('.shapelayer');
fullLayout._shapeSubplotLayers = layerSubplot.selectAll('.shapelayer');
Copy link
Contributor

Choose a reason for hiding this comment

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

✏️ nicely done


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

fullLayout._bgLayer = fullLayout._paper.append('g')
.classed('bglayer', true);

fullLayout._draggers = fullLayout._paper.append('g')
.classed('draglayer', true);

// lower shape layer
// (only for shapes to be drawn below the whole plot)
// lower shape/image layer - note that this is behind
// all subplots data/grids but above the backgrounds
// except inset subplots, whose backgrounds are drawn
// inside their own group so that they appear above
// the data for the main subplot
// lower shapes and images which are fully referenced to
// a subplot still get drawn within the subplot's group
// so they will work correctly on insets
var layerBelow = fullLayout._paper.append('g')
.classed('layer-below', true);
fullLayout._imageLowerLayer = layerBelow.append('g')
Expand Down
86 changes: 84 additions & 2 deletions src/plot_api/subroutines.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

'use strict';

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

function overlappingDomain(xDomain, yDomain, domains) {
for(var i = 0; i < domains.length; i++) {
var existingX = domains[i][0],
existingY = domains[i][1];

if(existingX[0] >= xDomain[1] || existingX[1] <= xDomain[0]) {
continue;
}
if(existingY[0] < yDomain[1] && existingY[1] > yDomain[0]) {
return true;
}
}
return false;
}

exports.lsInner = function(gd) {
var fullLayout = gd._fullLayout,
gs = fullLayout._size,
Expand All @@ -43,8 +59,73 @@ exports.lsInner = function(gd) {

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

var subplotSelection = fullLayout._paper.selectAll('g.subplot');

// figure out which backgrounds we need to draw, and in which layers
// to put them
var lowerBackgroundIDs = [];
var lowerDomains = [];
subplotSelection.each(function(subplot) {
var plotinfo = fullLayout._plots[subplot];

if(plotinfo.mainplot) {
// mainplot is a reference to the main plot this one is overlaid on
// so if it exists, this is an overlaid plot and we don't need to
// give it its own background
if(plotinfo.bg) {
plotinfo.bg.remove();
}
plotinfo.bg = undefined;
return;
}

var xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
ya = Plotly.Axes.getFromId(gd, subplot, 'y'),
xDomain = xa.domain,
yDomain = ya.domain,
plotgroupBgData = [];

if(overlappingDomain(xDomain, yDomain, lowerDomains)) {
plotgroupBgData = [0];
}
else {
lowerBackgroundIDs.push(subplot);
lowerDomains.push([xDomain, yDomain]);
}

// create the plot group backgrounds now, since
// they're all independent selections
var plotgroupBg = plotinfo.plotgroup.selectAll('.bg')
.data(plotgroupBgData);

plotgroupBg.enter().append('rect')
.classed('bg', true);

plotgroupBg.exit().remove();

plotgroupBg.each(function() {
plotinfo.bg = plotgroupBg;
var pgNode = plotinfo.plotgroup.node();
pgNode.insertBefore(this, pgNode.childNodes[0]);
});
});

// now create all the lower-layer backgrounds at once now that
// we have the list of subplots that need them
var lowerBackgrounds = fullLayout._bgLayer.selectAll('.bg')
.data(lowerBackgroundIDs);

lowerBackgrounds.enter().append('rect')
.classed('bg', true);

lowerBackgrounds.exit().remove();

lowerBackgrounds.each(function(subplot) {
fullLayout._plots[subplot].bg = d3.select(this);
});

var freefinished = [];
fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
subplotSelection.each(function(subplot) {
var plotinfo = fullLayout._plots[subplot],
xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
ya = Plotly.Axes.getFromId(gd, subplot, 'y');
Expand All @@ -58,7 +139,8 @@ exports.lsInner = function(gd) {
.call(Drawing.setRect,
xa._offset - gs.p, ya._offset - gs.p,
xa._length + 2 * gs.p, ya._length + 2 * gs.p)
.call(Color.fill, fullLayout.plot_bgcolor);
.call(Color.fill, fullLayout.plot_bgcolor)
.style('stroke-width', 0);
}

// Clip so that data only shows up on the plot area.
Expand Down
3 changes: 0 additions & 3 deletions src/plots/cartesian/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,6 @@ function makeSubplotLayer(plotinfo) {
}

if(!plotinfo.mainplot) {
plotinfo.bg = joinLayer(plotgroup, 'rect', 'bg');
plotinfo.bg.style('stroke-width', 0);

var backLayer = joinLayer(plotgroup, 'g', 'layer-subplot');
plotinfo.shapelayer = joinLayer(backLayer, 'g', 'shapelayer');
plotinfo.imagelayer = joinLayer(backLayer, 'g', 'imagelayer');
Expand Down
Binary file modified test/image/baselines/dendrogram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 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.
4 changes: 3 additions & 1 deletion test/image/mocks/layout_image.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
}
],
"layout": {
"plot_bgcolor": "rgba(0,0,0,0)",
"xaxis2": {
"anchor": "y2"
},
Expand All @@ -19,7 +20,8 @@
},
"yaxis2": {
"domain": [0.55, 1],
"type": "log"
"type": "log",
"anchor": "x2"
},
"images": [
{
Expand Down
Loading