Skip to content

Feature: Plot layout images #525

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 20 commits into from
May 12, 2016
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
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: [
'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(' ')
},

sizex: {
valType: 'number',
role: 'info',
dflt: 0,
description: [
'Sets the image container size horizontally.',
'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(' ')
},

sizey: {
valType: 'number',
role: 'info',
dflt: 0,
description: [
'Sets the image container size vertically.',
'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: {
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(' ')
}
};
64 changes: 64 additions & 0 deletions src/components/images/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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 || !Array.isArray(layoutIn.images)) return;


var containerIn = layoutIn.images,
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('sizex');
coerce('sizey');
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;
}
171 changes: 171 additions & 0 deletions src/components/images/draw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* 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];

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 }
}
};


// Images must be converted to dataURL's for exporting.
function setImage(d) {

var thisImage = d3.select(this);

var imagePromise = new Promise(function(resolve) {

var img = new Image();

// If not set, a `tainted canvas` error is thrown
img.setAttribute('crossOrigin', 'anonymous');
img.onerror = errorHandler;
img.onload = function() {

var canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;

var ctx = canvas.getContext('2d');
ctx.drawImage(this, 0, 0);

var dataURL = canvas.toDataURL('image/png');

thisImage.attr('xlink:href', dataURL);
};


thisImage.on('error', errorHandler);
thisImage.on('load', resolve);

img.src = d.source;

function errorHandler() {
thisImage.remove();
resolve();
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think you got this right. Throwing an error on incorrect source input would be a little bit too much I think.

}
});

gd._promises.push(imagePromise);
}

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.sizex) - xref.l2p(0)) : d.sizex * size.w,
height = yref ? Math.abs(yref.l2p(d.sizey) - yref.l2p(0)) : d.sizey * 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 * size.w) + xOffset,
yPos = (yref ? yref.l2p(d.y) : size.h - d.y * size.h) + yOffset;


// Construct the proper aspectRatio attribute
switch(d.sizing) {
case 'fill':
sizing += ' slice';
break;

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

thisImage.attr({
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);
}


// Required for updating images
function keyFunction(d) {
Copy link
Contributor

Choose a reason for hiding this comment

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

well played.

return d.source;
}


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

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

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

imagesBelow.each(applyAttributes);
imagesSubplot.each(applyAttributes);
imagesAbove.each(applyAttributes);
};
Loading