Skip to content

Commit 5940dd1

Browse files
committed
Merge pull request #525 from plotly/plot-images
Feature: Plot layout images 🎉
2 parents 1903061 + 1f409e4 commit 5940dd1

File tree

20 files changed

+888
-18
lines changed

20 files changed

+888
-18
lines changed

src/components/images/attributes.js

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var cartesianConstants = require('../../plots/cartesian/constants');
12+
13+
14+
module.exports = {
15+
_isLinkedToArray: true,
16+
17+
source: {
18+
valType: 'string',
19+
role: 'info',
20+
description: [
21+
'Specifies the URL of the image to be used.',
22+
'The URL must be accessible from the domain where the',
23+
'plot code is run, and can be either relative or absolute.'
24+
25+
].join(' ')
26+
},
27+
28+
layer: {
29+
valType: 'enumerated',
30+
values: ['below', 'above'],
31+
dflt: 'above',
32+
role: 'info',
33+
description: [
34+
'Specifies whether images are drawn below or above traces.',
35+
'When `xref` and `yref` are both set to `paper`,',
36+
'image is drawn below the entire plot area.'
37+
].join(' ')
38+
},
39+
40+
sizex: {
41+
valType: 'number',
42+
role: 'info',
43+
dflt: 0,
44+
description: [
45+
'Sets the image container size horizontally.',
46+
'The image will be sized based on the `position` value.',
47+
'When `xref` is set to `paper`, units are sized relative',
48+
'to the plot width.'
49+
].join(' ')
50+
},
51+
52+
sizey: {
53+
valType: 'number',
54+
role: 'info',
55+
dflt: 0,
56+
description: [
57+
'Sets the image container size vertically.',
58+
'The image will be sized based on the `position` value.',
59+
'When `yref` is set to `paper`, units are sized relative',
60+
'to the plot height.'
61+
].join(' ')
62+
},
63+
64+
sizing: {
65+
valType: 'enumerated',
66+
values: ['fill', 'contain', 'stretch'],
67+
dflt: 'contain',
68+
role: 'info',
69+
description: [
70+
'Specifies which dimension of the image to constrain.'
71+
].join(' ')
72+
},
73+
74+
opacity: {
75+
valType: 'number',
76+
role: 'info',
77+
min: 0,
78+
max: 1,
79+
dflt: 1,
80+
description: 'Sets the opacity of the image.'
81+
},
82+
83+
x: {
84+
valType: 'number',
85+
role: 'info',
86+
dflt: 0,
87+
description: [
88+
'Sets the image\'s x position.',
89+
'When `xref` is set to `paper`, units are sized relative',
90+
'to the plot height.',
91+
'See `xref` for more info'
92+
].join(' ')
93+
},
94+
95+
y: {
96+
valType: 'number',
97+
role: 'info',
98+
dflt: 0,
99+
description: [
100+
'Sets the image\'s y position.',
101+
'When `yref` is set to `paper`, units are sized relative',
102+
'to the plot height.',
103+
'See `yref` for more info'
104+
].join(' ')
105+
},
106+
107+
xanchor: {
108+
valType: 'enumerated',
109+
values: ['left', 'center', 'right'],
110+
dflt: 'left',
111+
role: 'info',
112+
description: 'Sets the anchor for the x position'
113+
},
114+
115+
yanchor: {
116+
valType: 'enumerated',
117+
values: ['top', 'middle', 'bottom'],
118+
dflt: 'top',
119+
role: 'info',
120+
description: 'Sets the anchor for the y position.'
121+
},
122+
123+
xref: {
124+
valType: 'enumerated',
125+
values: [
126+
'paper',
127+
cartesianConstants.idRegex.x.toString()
128+
],
129+
dflt: 'paper',
130+
role: 'info',
131+
description: [
132+
'Sets the images\'s x coordinate axis.',
133+
'If set to a x axis id (e.g. *x* or *x2*), the `x` position',
134+
'refers to an x data coordinate',
135+
'If set to *paper*, the `x` position refers to the distance from',
136+
'the left of plot in normalized coordinates',
137+
'where *0* (*1*) corresponds to the left (right).'
138+
].join(' ')
139+
},
140+
141+
yref: {
142+
valType: 'enumerated',
143+
values: [
144+
'paper',
145+
cartesianConstants.idRegex.y.toString()
146+
],
147+
dflt: 'paper',
148+
role: 'info',
149+
description: [
150+
'Sets the images\'s y coordinate axis.',
151+
'If set to a y axis id (e.g. *y* or *y2*), the `y` position',
152+
'refers to a y data coordinate.',
153+
'If set to *paper*, the `y` position refers to the distance from',
154+
'the bottom of the plot in normalized coordinates',
155+
'where *0* (*1*) corresponds to the bottom (top).'
156+
].join(' ')
157+
}
158+
};

src/components/images/defaults.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Axes = require('../../plots/cartesian/axes');
12+
var Lib = require('../../lib');
13+
var attributes = require('./attributes');
14+
15+
16+
module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
17+
18+
if(!layoutIn.images || !Array.isArray(layoutIn.images)) return;
19+
20+
21+
var containerIn = layoutIn.images,
22+
containerOut = layoutOut.images = [];
23+
24+
25+
for(var i = 0; i < containerIn.length; i++) {
26+
var image = containerIn[i];
27+
28+
if(!image.source) continue;
29+
30+
var defaulted = imageDefaults(containerIn[i] || {}, containerOut[i] || {}, layoutOut);
31+
containerOut.push(defaulted);
32+
}
33+
};
34+
35+
36+
function imageDefaults(imageIn, imageOut, fullLayout) {
37+
38+
imageOut = imageOut || {};
39+
40+
function coerce(attr, dflt) {
41+
return Lib.coerce(imageIn, imageOut, attributes, attr, dflt);
42+
}
43+
44+
coerce('source');
45+
coerce('layer');
46+
coerce('x');
47+
coerce('y');
48+
coerce('xanchor');
49+
coerce('yanchor');
50+
coerce('sizex');
51+
coerce('sizey');
52+
coerce('sizing');
53+
coerce('opacity');
54+
55+
for(var i = 0; i < 2; i++) {
56+
var tdMock = { _fullLayout: fullLayout },
57+
axLetter = ['x', 'y'][i];
58+
59+
// 'paper' is the fallback axref
60+
Axes.coerceRef(imageIn, imageOut, tdMock, axLetter, 'paper');
61+
}
62+
63+
return imageOut;
64+
}

src/components/images/draw.js

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var d3 = require('d3');
12+
var Drawing = require('../drawing');
13+
var Axes = require('../../plots/cartesian/axes');
14+
15+
module.exports = function draw(gd) {
16+
17+
var fullLayout = gd._fullLayout,
18+
imageDataAbove = [],
19+
imageDataSubplot = [],
20+
imageDataBelow = [];
21+
22+
if(!fullLayout.images) return;
23+
24+
25+
// Sort into top, subplot, and bottom layers
26+
for(var i = 0; i < fullLayout.images.length; i++) {
27+
var img = fullLayout.images[i];
28+
29+
if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') {
30+
imageDataSubplot.push(img);
31+
} else if(img.layer === 'above') {
32+
imageDataAbove.push(img);
33+
} else {
34+
imageDataBelow.push(img);
35+
}
36+
}
37+
38+
39+
var anchors = {
40+
x: {
41+
left: { sizing: 'xMin', offset: 0 },
42+
center: { sizing: 'xMid', offset: -1 / 2 },
43+
right: { sizing: 'xMax', offset: -1 }
44+
},
45+
y: {
46+
top: { sizing: 'YMin', offset: 0 },
47+
middle: { sizing: 'YMid', offset: -1 / 2 },
48+
bottom: { sizing: 'YMax', offset: -1 }
49+
}
50+
};
51+
52+
53+
// Images must be converted to dataURL's for exporting.
54+
function setImage(d) {
55+
56+
var thisImage = d3.select(this);
57+
58+
var imagePromise = new Promise(function(resolve) {
59+
60+
var img = new Image();
61+
62+
// If not set, a `tainted canvas` error is thrown
63+
img.setAttribute('crossOrigin', 'anonymous');
64+
img.onerror = errorHandler;
65+
img.onload = function() {
66+
67+
var canvas = document.createElement('canvas');
68+
canvas.width = this.width;
69+
canvas.height = this.height;
70+
71+
var ctx = canvas.getContext('2d');
72+
ctx.drawImage(this, 0, 0);
73+
74+
var dataURL = canvas.toDataURL('image/png');
75+
76+
thisImage.attr('xlink:href', dataURL);
77+
};
78+
79+
80+
thisImage.on('error', errorHandler);
81+
thisImage.on('load', resolve);
82+
83+
img.src = d.source;
84+
85+
function errorHandler() {
86+
thisImage.remove();
87+
resolve();
88+
}
89+
});
90+
91+
gd._promises.push(imagePromise);
92+
}
93+
94+
function applyAttributes(d) {
95+
96+
var thisImage = d3.select(this);
97+
98+
// Axes if specified
99+
var xref = Axes.getFromId(gd, d.xref),
100+
yref = Axes.getFromId(gd, d.yref);
101+
102+
var size = fullLayout._size,
103+
width = xref ? Math.abs(xref.l2p(d.sizex) - xref.l2p(0)) : d.sizex * size.w,
104+
height = yref ? Math.abs(yref.l2p(d.sizey) - yref.l2p(0)) : d.sizey * size.h;
105+
106+
// Offsets for anchor positioning
107+
var xOffset = width * anchors.x[d.xanchor].offset + size.l,
108+
yOffset = height * anchors.y[d.yanchor].offset + size.t;
109+
110+
var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing;
111+
112+
// Final positions
113+
var xPos = (xref ? xref.l2p(d.x) : d.x * size.w) + xOffset,
114+
yPos = (yref ? yref.l2p(d.y) : size.h - d.y * size.h) + yOffset;
115+
116+
117+
// Construct the proper aspectRatio attribute
118+
switch(d.sizing) {
119+
case 'fill':
120+
sizing += ' slice';
121+
break;
122+
123+
case 'stretch':
124+
sizing = 'none';
125+
break;
126+
}
127+
128+
thisImage.attr({
129+
x: xPos,
130+
y: yPos,
131+
width: width,
132+
height: height,
133+
preserveAspectRatio: sizing,
134+
opacity: d.opacity
135+
});
136+
137+
138+
// Set proper clipping on images
139+
var xId = xref ? xref._id : '',
140+
yId = yref ? yref._id : '',
141+
clipAxes = xId + yId;
142+
143+
thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes);
144+
}
145+
146+
147+
// Required for updating images
148+
function keyFunction(d) {
149+
return d.source;
150+
}
151+
152+
153+
var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
154+
.data(imageDataBelow, keyFunction),
155+
imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image')
156+
.data(imageDataSubplot, keyFunction),
157+
imagesAbove = fullLayout._imageUpperLayer.selectAll('image')
158+
.data(imageDataAbove, keyFunction);
159+
160+
imagesBelow.enter().append('image').each(setImage);
161+
imagesSubplot.enter().append('image').each(setImage);
162+
imagesAbove.enter().append('image').each(setImage);
163+
164+
imagesBelow.exit().remove();
165+
imagesSubplot.exit().remove();
166+
imagesAbove.exit().remove();
167+
168+
imagesBelow.each(applyAttributes);
169+
imagesSubplot.each(applyAttributes);
170+
imagesAbove.each(applyAttributes);
171+
};

0 commit comments

Comments
 (0)