-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Changes from 8 commits
fa0e49b
3319387
dd15bfd
b94065f
00742e8
cc742fa
7b2859d
8e75905
83592c1
466a770
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(' ') | ||
}, | ||
|
||
width: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(' ') | ||
} | ||
}; |
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], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you need to filter out There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This handles it - nothing is added to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha. Nicely done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But, that means that a images: [{}] results in no images shown on the graph - which is not consistent with Unlike corresponding There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree 100%. We should make it clear in the |
||
|
||
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}; |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should attach the image attributes to |
||
supplyLayoutDefaults: supplyLayoutDefaults | ||
}; |
There was a problem hiding this comment.
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
assource
?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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'
?There was a problem hiding this comment.
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.