Skip to content
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

Improve Plotly.toImage #1939

Merged
merged 16 commits into from
Aug 14, 2017
Merged

Improve Plotly.toImage #1939

merged 16 commits into from
Aug 14, 2017

Conversation

etpinard
Copy link
Contributor

@etpinard etpinard commented Aug 8, 2017

The bulk of this PR is in commit 8866525 (where several improvement to Plotly.toImage are made) and dd003ce (where the setBackground config option is now a little more portable)

With 8866525, Plotly.toImage can now accept data/layout/config (plain) object as in input. Previously, only existing graph divs were supported. This is important for image generation performance.

In details, to generate an image using only official API methods (i.e. none of the Plotly.Snapshot methods), currently one must first call Plotly.newPlot and then pass the gd to Plotly.toImage. To ensure that toImage does not mutate the existing graph div, toImage clones it (i.e. extends the graph's data/layout) and calls Plotly.plot on an off-screen <div>. That is, Plotly.plot is called twice for each generated image. Needless to say, we can do better.

So, now calling toImage as:

Plotly.toImage({
  data: [/* */],
  layout: {/* */},
  config: {/* */}
})
.then(imgData => {})

assumes that the data/layout/config containers are new (i.e. aren't linked to an existing graph) meaning that the containers aren't extended and Plotly.plot is only called once per generated image 🐎

- i.e. not crash when called multiple times
- instead of in `plot_config.js` declaration
- which is currently used in the image server
- + add 'webp' test case
- accept data/layout/config figure (plain) object as input,
  along side existing graph div or (string) id of existing graph div
- use Lib.validate & Lib.coerce to sanatize options
- bypass Snapshot.cloneplot (Lib.extendDeep is really all we need)
- handle 'setBackground` (same as config option)
- add 'imageDataOnly' option to strip 'data:image/' prefix
@@ -404,6 +404,11 @@ function opaqueSetBackground(gd, bgColor) {
setBackground(gd, bgColor);
}

function blendSetBackground(gd, bgColor) {
var blend = Color.combine(bgColor, 'white');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implements this (old) image server logic in plotly.js

Copy link
Collaborator

Choose a reason for hiding this comment

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

doesn't this end up visually equivalent to opaqueSetBackground? And if so can we remove opaqueSetBackground?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RE 'opaque' vs 'blend', they don't generate the same PNG (gm compare generates a diff) but I can't find a case where the diff is detectable to my 👀

So, I guess a can make the new image server use 'opaque' instead of 'blend' and 🔪 blendSetBackground.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 22a598b

reject(new Error('Height and width should be pixel values.'));
}
if(format === 'svg' && imageDataOnly) {
return resolve(svg);
Copy link
Contributor Author

@etpinard etpinard Aug 8, 2017

Choose a reason for hiding this comment

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

Fast pass for svg with imageDataOnly where Snapshot.toSVG gives the correct result.

Note that PDF and EPS images should first be generated as svg and then converted to pdf or eps using another library. In-house PDF and EPS image generation is outside the scope of plotly.js.

Copy link
Collaborator

Choose a reason for hiding this comment

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

is it worth handling imageDataOnly=false here too - and just adding the prefix? Or is it more complicated than that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it shouldn't be too hard. I'll add that in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 4ec3e0f


if(!isBadlySet('format')) {
throw new Error('Image format is not jpeg, png, svg or webp.');
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice job using coerce for this! But isBadlySet is confusing me... it's true if you either haven't set the attribute or if it has a valid input? Wouldn't that be isValidOrImplied or something? Anyway I don't see how you can use the same logic for format (which has a default) as for width and height (which don't).

Dimensions you could just look at fullOpts after coerce to see if you have something that's not undefined (the implicit dflt)...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh I see, it is OK to omit dimensions, then they default to what's in the layout... might be clearer if this was just used as the default width = coerce('width', layout.width)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

might be clearer if this was just used as the default width = coerce('width', layout.width)?

right, but layout.width might not exist at this stage.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, but if it's not in opts and it's not in layout then what width do we get?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We get the dflt from plots/layout_attributes.js

Copy link
Collaborator

Choose a reason for hiding this comment

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

Alright, I don't want to beat a dead horse and it's not a big deal, but I believe it would still work just fine if you coerce with a dflt which might be undefined at this point, then you coerce with the real dflt later on, overriding that undefined.

Anyway my initial comment stands, about isBadlySet being misnamed and confusing logic.

Copy link
Contributor Author

@etpinard etpinard Aug 14, 2017

Choose a reason for hiding this comment

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

isBadlySet -> isImpliedOrValid in 3c42efb

// extend config for static plot
var configImage = Lib.extendFlat({}, config, {
staticPlot: true,
plotGlPixelRatio: 2,
Copy link
Collaborator

Choose a reason for hiding this comment

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

is plotGlPixelRatio something people might want to override? Would it work if they could?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, potentially. Good 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 7565d0b

})
.then(function() {
expect(errors.length).toBe(1);
expect(errors[0]).toBe('Image format is not jpeg, png, svg or webp.');
Copy link
Collaborator

Choose a reason for hiding this comment

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

You're not a fan of expect(function() { ... }).toThrow(...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 43a6150

- and make 'opaque' use Color.combine instead of hard-setting
  _paperdiv.
- 🔪 config assignments to we already get for free
  via staticPlot: true
- make Plotly.toImage bypass svgToImg even when imageDataOnly
  is false.
if(imageDataOnly) {
return resolve(svg);
} else {
return resolve('data:image/svg+xml,' + encodeURIComponent(svg));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

as on

var url = 'data:image/svg+xml,' + encodeURIComponent(svg);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This broke a lot of tests. It made the toImage promise resolved before removing the clone graph div.

Fixed in 816df16

@alexcjohnson
Copy link
Collaborator

Love it. 💃

- so that even 'svg' formats (which bypass svgToImg) clear
  the clone graph div
- no need to remove *all* graph div in toImage test anymore 🎉
@etpinard etpinard added this to the v1.30.0 milestone Aug 14, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants