Skip to content

Scale image in Plotly.toImage #1979

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 3 commits into from
Sep 11, 2017
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
16 changes: 14 additions & 2 deletions src/plot_api/to_image.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ var attrs = {
'Defaults to the value found in `layout.height`'
].join(' ')
},
scale: {
valType: 'number',
min: 0,
dflt: 1,
description: [
'Sets a scaling for the generated image.',
'If set, all features of a graphs (e.g. text, line width)',
'are scaled, unlike simply setting',
'a bigger *width* and *height*.'
].join(' ')
},
setBackground: {
valType: 'any',
dflt: false,
Expand Down Expand Up @@ -111,6 +122,7 @@ function toImage(gd, opts) {
var format = coerce('format');
var width = coerce('width');
var height = coerce('height');
var scale = coerce('scale');
var setBackground = coerce('setBackground');
var imageDataOnly = coerce('imageDataOnly');

Expand All @@ -128,7 +140,6 @@ function toImage(gd, opts) {
// extend config for static plot
var configImage = Lib.extendFlat({}, config, {
staticPlot: true,
plotGlPixelRatio: config.plotGlPixelRatio || 2,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooops. This should have been in a separate commit. In brief, this line was useless, so 🔪

setBackground: setBackground
});

Expand All @@ -142,7 +153,7 @@ function toImage(gd, opts) {

function convert() {
return new Promise(function(resolve, reject) {
var svg = toSVG(clonedGd);
var svg = toSVG(clonedGd, format, scale);
var width = clonedGd._fullLayout.width;
var height = clonedGd._fullLayout.height;

Expand All @@ -164,6 +175,7 @@ function toImage(gd, opts) {
format: format,
width: width,
height: height,
scale: scale,
canvas: canvas,
svg: svg,
// ask svgToImg to return a Promise
Expand Down
12 changes: 9 additions & 3 deletions src/snapshot/svgtoimg.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ function svgToImg(opts) {
}

var canvas = opts.canvas;
var scale = opts.scale || 1;
var w0 = opts.width || 300;
var h0 = opts.height || 150;
var w1 = scale * w0;
var h1 = scale * h0;

var ctx = canvas.getContext('2d');
var img = new Image();

Expand All @@ -41,16 +47,16 @@ function svgToImg(opts) {
// is not restricted to svg
var url = 'data:image/svg+xml,' + encodeURIComponent(svg);

canvas.height = opts.height || 150;
canvas.width = opts.width || 300;
canvas.width = w1;
canvas.height = h1;

img.onload = function() {
var imgData;

// don't need to draw to canvas if svg
// save some time and also avoid failure on IE
if(format !== 'svg') {
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 0, 0, w1, h1);
}

switch(format) {
Expand Down
20 changes: 14 additions & 6 deletions src/snapshot/tosvg.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,21 @@ function xmlEntityEncode(str) {
return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&');
}

module.exports = function toSVG(gd, format) {
var fullLayout = gd._fullLayout,
svg = fullLayout._paper,
toppaper = fullLayout._toppaper,
i;
module.exports = function toSVG(gd, format, scale) {
var fullLayout = gd._fullLayout;
var svg = fullLayout._paper;
var toppaper = fullLayout._toppaper;
var width = fullLayout.width;
var height = fullLayout.height;
var i;

// make background color a rect in the svg, then revert after scraping
// all other alterations have been dealt with by properly preparing the svg
// in the first place... like setting cursors with css classes so we don't
// have to remove them, and providing the right namespaces in the svg to
// begin with
svg.insert('rect', ':first-child')
.call(Drawing.setRect, 0, 0, fullLayout.width, fullLayout.height)
.call(Drawing.setRect, 0, 0, width, height)
.call(Color.fill, fullLayout.paper_bgcolor);

// subplot-specific to-SVG methods
Expand Down Expand Up @@ -137,6 +139,12 @@ module.exports = function toSVG(gd, format) {
svg.node().setAttributeNS(xmlnsNamespaces.xmlns, 'xmlns', xmlnsNamespaces.svg);
svg.node().setAttributeNS(xmlnsNamespaces.xmlns, 'xmlns:xlink', xmlnsNamespaces.xlink);

if(format === 'svg' && scale) {
svg.attr('width', scale * width);
svg.attr('height', scale * height);
svg.attr('viewBox', '0 0 ' + width + ' ' + height);
}

var s = new window.XMLSerializer().serializeToString(svg.node());
s = htmlEntityDecode(s);
s = xmlEntityEncode(s);
Expand Down
20 changes: 20 additions & 0 deletions test/jasmine/tests/snapshot_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,5 +301,25 @@ describe('Plotly.Snapshot', function() {
.catch(fail)
.then(done);
});

it('should adapt *viewBox* attribute under *scale* option', function(done) {
Plotly.plot(gd, [{
y: [1, 2, 1]
}], {
width: 300,
height: 400
})
.then(function() {
var str = Plotly.Snapshot.toSVG(gd, 'svg', 2.5);
var dom = parser.parseFromString(str, 'image/svg+xml');
var el = dom.getElementsByTagName('svg')[0];

expect(el.getAttribute('width')).toBe('750', 'width');
expect(el.getAttribute('height')).toBe('1000', 'height');
expect(el.getAttribute('viewBox')).toBe('0 0 300 400', 'viewbox');
})
.catch(fail)
.then(done);
});
});
});
33 changes: 33 additions & 0 deletions test/jasmine/tests/toimage_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ var fail = require('../assets/fail_test');
var customMatchers = require('../assets/custom_matchers');
var subplotMock = require('@mocks/multiple_subplots.json');

var FORMATS = ['png', 'jpeg', 'webp', 'svg'];

describe('Plotly.toImage', function() {
'use strict';

Expand All @@ -31,6 +33,19 @@ describe('Plotly.toImage', function() {
});
}

function assertSize(url, width, height) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
expect(img.width).toBe(width, 'image width');
expect(img.height).toBe(height, 'image height');
resolve(url);
};
img.onerror = reject;
img.src = url;
});
}

it('should be attached to Plotly', function() {
expect(Plotly.toImage).toBeDefined();
});
Expand Down Expand Up @@ -109,18 +124,22 @@ describe('Plotly.toImage', function() {

Plotly.plot(gd, fig.data, fig.layout)
.then(function() { return Plotly.toImage(gd, {format: 'png'}); })
.then(function(url) { return assertSize(url, 700, 450); })
.then(function(url) {
expect(url.split('png')[0]).toBe('data:image/');
})
.then(function() { return Plotly.toImage(gd, {format: 'jpeg'}); })
.then(function(url) { return assertSize(url, 700, 450); })
.then(function(url) {
expect(url.split('jpeg')[0]).toBe('data:image/');
})
.then(function() { return Plotly.toImage(gd, {format: 'svg'}); })
.then(function(url) { return assertSize(url, 700, 450); })
.then(function(url) {
expect(url.split('svg')[0]).toBe('data:image/');
})
.then(function() { return Plotly.toImage(gd, {format: 'webp'}); })
.then(function(url) { return assertSize(url, 700, 450); })
.then(function(url) {
expect(url.split('webp')[0]).toBe('data:image/');
})
Expand Down Expand Up @@ -156,6 +175,20 @@ describe('Plotly.toImage', function() {
.then(done);
});

FORMATS.forEach(function(f) {
it('should respond to *scale* option ( format ' + f + ')', function(done) {
var fig = Lib.extendDeep({}, subplotMock);

Plotly.plot(gd, fig.data, fig.layout)
.then(function() { return Plotly.toImage(gd, {format: f, scale: 2}); })
.then(function(url) { return assertSize(url, 1400, 900); })
.then(function() { return Plotly.toImage(gd, {format: f, scale: 0.5}); })
.then(function(url) { return assertSize(url, 350, 225); })
.catch(fail)
.then(done);
});
});

it('should accept data/layout/config figure object as input', function(done) {
var fig = Lib.extendDeep({}, subplotMock);

Expand Down