'use strict'; var d3 = require('@plotly/d3'); var Drawing = require('../drawing'); var Axes = require('../../plots/cartesian/axes'); var axisIds = require('../../plots/cartesian/axis_ids'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); module.exports = function draw(gd) { var fullLayout = gd._fullLayout; var imageDataAbove = []; var imageDataSubplot = {}; var imageDataBelow = []; var subplot; var i; // Sort into top, subplot, and bottom layers 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') { subplot = axisIds.ref2id(img.xref) + axisIds.ref2id(img.yref); var plotinfo = fullLayout._plots[subplot]; if(!plotinfo) { // Fall back to _imageLowerLayer in case the requested subplot doesn't exist. // This can happen if you reference the image to an x / y axis combination // that doesn't have any data on it (and layer is below) imageDataBelow.push(img); continue; } 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 { 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); if(this._imgSrc === d.source) { return; } thisImage.attr('xmlns', xmlnsNamespaces.svg); if(d.source && d.source.slice(0, 5) === 'data:') { thisImage.attr('xlink:href', d.source); this._imgSrc = d.source; } else { var imagePromise = new Promise(function(resolve) { var img = new Image(); this.img = img; // 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', {willReadFrequently: true}); ctx.drawImage(this, 0, 0); var dataURL = canvas.toDataURL('image/png'); thisImage.attr('xlink:href', dataURL); // resolve promise in onload handler instead of on 'load' to support IE11 // see https://github.com/plotly/plotly.js/issues/1685 // for more details resolve(); }; thisImage.on('error', errorHandler); img.src = d.source; this._imgSrc = d.source; function errorHandler() { thisImage.remove(); resolve(); } }.bind(this)); gd._promises.push(imagePromise); } } function applyAttributes(d) { var thisImage = d3.select(this); // Axes if specified var xa = Axes.getFromId(gd, d.xref); var ya = Axes.getFromId(gd, d.yref); var xIsDomain = Axes.getRefType(d.xref) === 'domain'; var yIsDomain = Axes.getRefType(d.yref) === 'domain'; var size = fullLayout._size; var width, height; if(xa !== undefined) { width = ((typeof(d.xref) === 'string') && xIsDomain) ? xa._length * d.sizex : Math.abs(xa.l2p(d.sizex) - xa.l2p(0)); } else { width = d.sizex * size.w; } if(ya !== undefined) { height = ((typeof(d.yref) === 'string') && yIsDomain) ? ya._length * d.sizey : Math.abs(ya.l2p(d.sizey) - ya.l2p(0)); } else { height = d.sizey * size.h; } // Offsets for anchor positioning var xOffset = width * anchors.x[d.xanchor].offset; var yOffset = height * anchors.y[d.yanchor].offset; var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; // Final positions var xPos, yPos; if(xa !== undefined) { xPos = ((typeof(d.xref) === 'string') && xIsDomain) ? xa._length * d.x + xa._offset : xa.r2p(d.x) + xa._offset; } else { xPos = d.x * size.w + size.l; } xPos += xOffset; if(ya !== undefined) { yPos = ((typeof(d.yref) === 'string') && yIsDomain) ? // consistent with "paper" yref value, where positive values // move up the page ya._length * (1 - d.y) + ya._offset : ya.r2p(d.y) + ya._offset; } else { yPos = size.h - d.y * size.h + size.t; } yPos += 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 = xa && (Axes.getRefType(d.xref) !== 'domain') ? xa._id : ''; var yId = ya && (Axes.getRefType(d.yref) !== 'domain') ? ya._id : ''; var clipAxes = xId + yId; Drawing.setClipUrl( thisImage, clipAxes ? ('clip' + fullLayout._uid + clipAxes) : null, gd ); } var imagesBelow = fullLayout._imageLowerLayer.selectAll('image') .data(imageDataBelow); var imagesAbove = fullLayout._imageUpperLayer.selectAll('image') .data(imageDataAbove); imagesBelow.enter().append('image'); imagesAbove.enter().append('image'); imagesBelow.exit().remove(); imagesAbove.exit().remove(); imagesBelow.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 have their images on the main plot) // and gl2d plots (which don't support below images, at least not yet) if(!subplotObj.imagelayer) continue; 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); }); } };