Skip to content

Feature: Plot layout images #520

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

Closed
wants to merge 10 commits into from
158 changes: 158 additions & 0 deletions src/components/images/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var cartesianConstants = require('../../plots/cartesian/constants');


module.exports = {
_isLinkedToArray: true,

source: {
valType: 'string',
role: 'info',
description: [
Copy link
Contributor

@etpinard etpinard May 9, 2016

Choose a reason for hiding this comment

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

so, nothing is blocking users from inputting data:image/svg+xml;base6 as source ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep works fine!

I'm not 100% sure if there's a possibility for XSS here (or if it should be something that we worry about in plotly.js) but I couldn't manage to break anything or set any code to be executed - maybe I'm just not a good enough h4X0r though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool.

Yes, we don't worry about XSS (see PR #100).

Copy link
Contributor

@etpinard etpinard May 9, 2016

Choose a reason for hiding this comment

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

Maybe we should make things more convenient for folks wanting to use base64 strings.

Maybe we could add a sourcetype attribute with possible values 'link', 'png' or even 'svg'?

Copy link
Member

Choose a reason for hiding this comment

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

Personally I like that we use the same spec as HTML img src. Makes it easy to document, explain, and extend.

'Specifies the URL of the image to be used.',
'The URL must be accessible from the domain where the',
'plot code is run, and can be either relative or absolute.'

].join(' ')
},

layer: {
valType: 'enumerated',
values: ['below', 'above'],
dflt: 'above',
role: 'info',
description: [
'Specifies whether images are drawn below or above traces.',
'When `xref` and `yref` are both set to `paper`,',
'image is drawn below the entire plot area.'
].join(' ')
},

width: {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a fan of using width and height for relative dimensions like here.

Maybe we could use domain.x and domain.y, for example

{
  domain: {
    x: [0, 0.5],
    y: [0, 0.5]
  }
}

would be equivalent to

{
   x: 0,
   y: 0,
   width: 0.5,
   height: 0.5
}

cc @cldougl @chriddyp

valType: 'number',
role: 'info',
dflt: 0,
description: [
'Sets the image container width.',
'The image will be sized based on the `position` value.',
'When `xref` is set to `paper`, units are sized relative',
'to the plot width.'
].join(' ')
},

height: {
valType: 'number',
role: 'info',
dflt: 0,
description: [
'Sets the image container height.',
'The image will be sized based on the `position` value.',
'When `yref` is set to `paper`, units are sized relative',
'to the plot height.'
].join(' ')
},

sizing: {
valType: 'enumerated',
values: ['fill', 'contain', 'stretch'],
dflt: 'contain',
role: 'info',
description: [
'Specifies which dimension of the image to constrain.'
].join(' ')
},

opacity: {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe attributes for an optional bordering rectangle would be nice, but let's leave that for the next iteration.

valType: 'number',
role: 'info',
min: 0,
max: 1,
dflt: 1,
description: 'Sets the opacity of the image.'
},

x: {
valType: 'number',
role: 'info',
dflt: 0,
description: [
'Sets the image\'s x position.',
'When `xref` is set to `paper`, units are sized relative',
'to the plot height.',
'See `xref` for more info'
].join(' ')
},

y: {
valType: 'number',
role: 'info',
dflt: 0,
description: [
'Sets the image\'s y position.',
'When `yref` is set to `paper`, units are sized relative',
'to the plot height.',
'See `yref` for more info'
].join(' ')
},

xanchor: {
valType: 'enumerated',
values: ['left', 'center', 'right'],
dflt: 'left',
role: 'info',
description: 'Sets the anchor for the x position'
},

yanchor: {
valType: 'enumerated',
values: ['top', 'middle', 'bottom'],
dflt: 'top',
role: 'info',
description: 'Sets the anchor for the y position.'
},

xref: {
valType: 'enumerated',
values: [
'paper',
cartesianConstants.idRegex.x.toString()
],
dflt: 'paper',
role: 'info',
description: [
'Sets the images\'s x coordinate axis.',
'If set to a x axis id (e.g. *x* or *x2*), the `x` position',
'refers to an x data coordinate',
'If set to *paper*, the `x` position refers to the distance from',
'the left of plot in normalized coordinates',
'where *0* (*1*) corresponds to the left (right).'
].join(' ')
},

yref: {
valType: 'enumerated',
values: [
'paper',
cartesianConstants.idRegex.y.toString()
],
dflt: 'paper',
role: 'info',
description: [
'Sets the images\'s y coordinate axis.',
'If set to a y axis id (e.g. *y* or *y2*), the `y` position',
'refers to a y data coordinate.',
'If set to *paper*, the `y` position refers to the distance from',
'the bottom of the plot in normalized coordinates',
'where *0* (*1*) corresponds to the bottom (top).'
].join(' ')
}
};
65 changes: 65 additions & 0 deletions src/components/images/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var attributes = require('./attributes');


module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {

if(!layoutIn.images) return;


var containerIn = Array.isArray(layoutIn.images) ?
layoutIn.images : [layoutIn.images],
Copy link
Contributor

Choose a reason for hiding this comment

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

layout.images should only be coerced if it is an array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah ok. I figured as a convenience we could accept a single object.

Strict is good though 👍

containerOut = layoutOut.images = [];


for(var i = 0; i < containerIn.length; i++) {
var image = containerIn[i];

if(!image.source) continue;

var defaulted = imageDefaults(containerIn[i] || {}, containerOut[i] || {}, layoutOut);
containerOut.push(defaulted);
}
};


function imageDefaults(imageIn, imageOut, fullLayout) {

imageOut = imageOut || {};

function coerce(attr, dflt) {
return Lib.coerce(imageIn, imageOut, attributes, attr, dflt);
}

coerce('source');
coerce('layer');
coerce('x');
coerce('y');
coerce('xanchor');
coerce('yanchor');
coerce('width');
coerce('height');
coerce('sizing');
coerce('opacity');

for(var i = 0; i < 2; i++) {
var tdMock = { _fullLayout: fullLayout },
axLetter = ['x', 'y'][i];

// 'paper' is the fallback axref
Axes.coerceRef(imageIn, imageOut, tdMock, axLetter, 'paper');
}

return imageOut;
}
125 changes: 125 additions & 0 deletions src/components/images/draw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var d3 = require('d3');
var Drawing = require('../drawing');
var Axes = require('../../plots/cartesian/axes');

module.exports = function draw(gd) {

var fullLayout = gd._fullLayout,
imageDataAbove = [],
imageDataSubplot = [],
imageDataBelow = [];

if(!fullLayout.images) return;


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

@etpinard etpinard May 9, 2016

Choose a reason for hiding this comment

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

I think you need to filter out fullLayout.images[i] without source or with empty source fields

Copy link
Contributor Author

@mdtusz mdtusz May 9, 2016

Choose a reason for hiding this comment

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

This handles it - nothing is added to fullLayout.images if there's no source.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ha. Nicely done.

Copy link
Contributor

@etpinard etpinard May 9, 2016

Choose a reason for hiding this comment

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

But, that means that a layout with:

images: [{}]

results in no images shown on the graph - which is not consistent with annotations and shapes, but perhaps the better default for images.

Unlike corresponding annotations and shapes attributes, there's no suitable default for source, except maybe the plotly logo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was my thinking - there's no really sensible default for an image. Even the Plotly logo may be overstepping bounds - I'd prefer it to fail silently than show what could be considered an incorrect image.

Copy link
Contributor

@etpinard etpinard May 9, 2016

Choose a reason for hiding this comment

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

there's no really sensible default for an image

I agree 100%.

We should make it clear in the source attribute description that images with with wrong source fields are discarded.


if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') {
imageDataSubplot.push(img);
} else if(img.layer === 'above') {
imageDataAbove.push(img);
} else {
imageDataBelow.push(img);
}
}


var anchors = {
x: {
left: { sizing: 'xMin', offset: 0 },
center: { sizing: 'xMid', offset: -1 / 2 },
right: { sizing: 'xMax', offset: -1 }
},
y: {
top: { sizing: 'YMin', offset: 0 },
middle: { sizing: 'YMid', offset: -1 / 2 },
bottom: { sizing: 'YMax', offset: -1 }
}
};


function applyAttributes(d) {

var thisImage = d3.select(this);

// Axes if specified
var xref = Axes.getFromId(gd, d.xref),
yref = Axes.getFromId(gd, d.yref);

var size = fullLayout._size,
width = xref ? Math.abs(xref.l2p(d.width) - xref.l2p(0)) : d.width * size.w,
height = yref ? Math.abs(yref.l2p(d.height) - yref.l2p(0)) : d.width * size.h;

// Offsets for anchor positioning
var xOffset = width * anchors.x[d.xanchor].offset + size.l,
yOffset = height * anchors.y[d.yanchor].offset + size.t;

var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing;

// Final positions
var xPos = (xref ? xref.l2p(d.x) : d.x * width) + xOffset,
yPos = (yref ? yref.l2p(d.y) : -d.y * width) + yOffset;


// Construct the proper aspectRatio attribute
switch(d.sizing) {
case 'fill':
sizing += ' slice';
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.

break;

case 'stretch':
sizing = 'none';
break;
}

thisImage.attr({
href: d.source,
x: xPos,
y: yPos,
width: width,
height: height,
preserveAspectRatio: sizing,
opacity: d.opacity
});


// Set proper clipping on images
var xId = xref ? xref._id : '',
yId = yref ? yref._id : '',
clipAxes = xId + yId;

thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes);
}


var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
.data(imageDataBelow),
Copy link
Contributor

Choose a reason for hiding this comment

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

you'll need to define a key function to ensure that updates in the image data items are properly made.

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(applyAttributes);
imagesSubplot.each(applyAttributes);
imagesAbove.each(applyAttributes);
};
19 changes: 19 additions & 0 deletions src/components/images/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';


var draw = require('./draw');
var supplyLayoutDefaults = require('./defaults');


module.exports = {
draw: draw,
Copy link
Contributor

Choose a reason for hiding this comment

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

you should attach the image attributes to layoutAttributes here and add image': Image, below this line so that Plotly.PlotSchema.get() includes the layout.images attribute in its output.

supplyLayoutDefaults: supplyLayoutDefaults
};
3 changes: 1 addition & 2 deletions src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, axName, coun
containerOut = layoutOut[axName].rangeslider = {};

function coerce(attr, dflt) {
return Lib.coerce(containerIn, containerOut,
attributes, attr, dflt);
return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
}

coerce('bgcolor');
Expand Down
4 changes: 2 additions & 2 deletions src/components/shapes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ shapes.drawAll = function(gd) {
// Remove previous shapes before drawing new in shapes in fullLayout.shapes
fullLayout._shapeUpperLayer.selectAll('path').remove();
fullLayout._shapeLowerLayer.selectAll('path').remove();
fullLayout._subplotShapeLayer.selectAll('path').remove();
fullLayout._shapeSubplotLayer.selectAll('path').remove();

for(var i = 0; i < fullLayout.shapes.length; i++) {
shapes.draw(gd, i);
Expand Down Expand Up @@ -356,7 +356,7 @@ function getShapeLayer(gd, index) {
else if(shape.layer === 'below') {
shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ?
gd._fullLayout._shapeLowerLayer :
gd._fullLayout._subplotShapeLayer;
gd._fullLayout._shapeSubplotLayer;
}

return shapeLayer;
Expand Down
Loading