Skip to content

Inconsistent behavior of the scale factor for vector exports that include encapsulated rasters #58

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

Closed
dperdios opened this issue Nov 11, 2020 · 7 comments
Labels
feature something new

Comments

@dperdios
Copy link

dperdios commented Nov 11, 2020

Inconsistent behavior of the scale factor for vector exports that include encapsulated rasters

Thank you so much for your work that brings the amazing features of plotly to static images too. This is extremely useful for publications that still rely on static figures (e.g. scientific journals). I was trying out some vector exports for different kind of plots at it was made clear from the plotly documentation that part of figures with specific plots (e.g. surface and mesh3d) would be “rasterized”.

Apparently, increasing the scale factor when calling write_image for vector formats does not produce encapsulated rasters with a higher resolution as it does for png (see details below). The main problem is that the behavior is not consistent between png and vector formats (tested with 'pdf' and 'svg').

Is there a way to have a consistent behavior?

(Sorry for the long issue message, I am trying to give as much info as possible.)

Image resolution using the scale factor

To (somehow) control the image resolution (only relevant for raster formats or raster components encapsulated in vector formats) the write_image (doc) method provides a scale factor. This works as expected for 'png' format but does not seem to work consistently for vector formats such as 'pdf' and 'svg'.

Different exports (using the kaleido engine) of the same image with 'png', 'pdf', and 'svg' formats and using different scale factors of 1 and 2 are attached. The code to reproduce these exports is detailed below. PNG and PDF files are also attached to this message (SVG is not supported as direct upload).

From a quick inspection of the file sizes, it is clear that 'png' does produce a larger file size (of “higher resolution”) when using a greater scale factor. This is not the case for the vector format (i.e. 'pdf' and 'svg'). Their file sizes do not change when using a greater scale factor (though their dimensions are scaled). I believe that the encapsulated rasters are similar (if not identical) despite the different scale factors. This unfortunately prevents from any high-resolution exports in vector formats (most suitable for print).

Additional information about the package versions and the PDF exports are provided below.

Ideal image resolution control

For all raster formats or rasters encapsulated within vector formats, it would be ideal if one could define a dpi parameter to control the final resolution of raster components. This would be much nicer than playing with a scale factor and dividing the final image by that factor to include it somewhere else (e.g. an image within a PDF).

Apparently, the current resolution for png exports is 72 dpi and 96 dpi for the vector exports. Both values are “default” values for screens (though many screens have much better resolutions nowadays), but clearly insufficient for print (for which 300 dpi and even 600 dpi is often required).

Additional observation

For the vector formats, it appears that not only the surface gets rasterized (again this is fine), but also the axis ticks (and the background). I do know if this is the expected behavior, but it would of course be optimal not to raster all components belonging to the surface plot (in particular fonts). I believe this would be much more complicated to control though.

Code

import os
import plotly.graph_objects as go
import pandas as pd
import itertools

# Read data from a csv
#  https://plotly.com/python/3d-surface-plots/
z_data = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')

# Create figure
fig = go.Figure(data=[go.Surface(z=z_data.values)])

fig.update_layout(
    title='Mt Bruno Elevation',
    autosize=False, width=500, height=500,
    margin=dict(l=65, r=50, b=65, t=90)
)

# Export
export_dir = "images"
if not os.path.isdir(export_dir):
    os.mkdir(path=export_dir)

scale_seq = 1, 2
format_seq = 'png', 'svg', 'pdf'

for scale, fmt in itertools.product(scale_seq, format_seq):
    fig_name = f"export-surface-scale-{scale}.{fmt}"
    file = os.path.join(export_dir, fig_name)
    fig.write_image(file, format=fmt, scale=scale, engine='kaleido')

Additional information

plotly and kaleido versions

plotly==4.12.0
kaleido==0.0.3.post1

png outputs

$ identify -format "format: %m\nfilename: %f\nfile size: %b\npixels: %wx%h\ndpi: %xx%y\n" export-surface-scale-1.png
format: PNG
filename: export-surface-scale-1.png
file size: 95676B
pixels: 500x500
dpi: 72x72
$ identify -format "format: %m\nfilename: %f\nfile size: %b\npixels: %wx%h\ndpi: %xx%y\n" export-surface-scale-2.png
format: PNG
filename: export-surface-scale-2.png
file size: 253429B
pixels: 1000x1000
dpi: 72x72

pdfinfo outputs

From the page size property given in points, it seems like the renderer used a dpi of 96 as it started from a 500px x 500px image (i.e. 500 * 72 / 96 = 375).

$ pdfinfo export-surface-scale-1.pdf
Creator:        Chromium
Producer:       Skia/PDF m83
CreationDate:   Wed Nov 11 16:21:43 2020
ModDate:        Wed Nov 11 16:21:43 2020
Tagged:         no
UserProperties: no
Suspects:       no
Form:           none
JavaScript:     no
Pages:          1
Encrypted:      no
Page size:      375.12 x 375.12 pts
Page rot:       0
File size:      221278 bytes
Optimized:      no
PDF version:    1.4
$ pdfinfo export-surface-scale-2.pdf
Creator:        Chromium
Producer:       Skia/PDF m83
CreationDate:   Wed Nov 11 16:21:47 2020
ModDate:        Wed Nov 11 16:21:47 2020
Tagged:         no
UserProperties: no
Suspects:       no
Form:           none
JavaScript:     no
Pages:          1
Encrypted:      no
Page size:      750 x 750 pts
Page rot:       0
File size:      221264 bytes
Optimized:      no
PDF version:    1.4

export-surface-scale-2.pdf
export-surface-scale-2
export-surface-scale-1.pdf
export-surface-scale-1

@jonmmease
Copy link
Collaborator

Thanks for the kind words, and the detailed report @dperdios. This is definitely something worth looking into.

Plotly.js has a configuration option that I think is relevant here (though I haven't experimented with it yet): plotGlPixelRatio

https://github.com/plotly/plotly.js/blob/98dfdb836e56296361e537f15d885b3916bebbdb/src/plot_api/plot_config.js#L334-L344

Something to try would be to increase this value to 3 or 4 (from default of 2).

For example:

from kaleido.scopes.plotly import PlotlyScope
import plotly.graph_objects as go
scope = PlotlyScope()

fig = dict(
    data=[dict(type="scattergl", y=[1, 3, 2])],
    config=dict(plotGlPixelRatio=4)
)
with open("figure.png", "wb") as f:
    f.write(scope.transform(fig, format="png"))

Does this make a difference for you? If so, we could consider increasing this value when the scale factor is set to > 1.

@dperdios
Copy link
Author

dperdios commented Nov 12, 2020

Thank you for the prompt answer @jonmmease.

It does indeed sound like an interesting configuration option to somehow control de resolution of WebGL components. However, it does not seem to produce any effect (i.e. the exports are identical for 'png', 'pdf', and 'svg'). I suspect that this option is ignored somewhere?

Try to run the following code. It shows that specifying the plotGlPixelRatio does not seem to have any impact when calling full_figure_for_development (useful method to obtain a "full" figure including unspecified attributes). Interestingly, the default value seems to be 2.5 and not 2.

import plotly.io as pio

# 'plotGlPixelRatio' specified
fig = dict(
    data=[dict(type="scattergl", y=[1, 3, 2])],
    config=dict(plotGlPixelRatio=4)
)
full_fig = pio.full_figure_for_development(fig=fig, as_dict=True)
gpr = full_fig['config']['plotGlPixelRatio']
print(f"'plotGlPixelRatio' specified. Obtained 'plotGlPixelRatio': {gpr}")

# 'plotGlPixelRatio' not specified
fig = dict(
    data=[dict(type="scattergl", y=[1, 3, 2])]
)
full_fig = pio.full_figure_for_development(fig=fig, as_dict=True)
gpr = full_fig['config']['plotGlPixelRatio']
print(f"'plotGlPixelRatio' not specified. Obtained 'plotGlPixelRatio': {gpr}")

The output I get from this code:

'plotGlPixelRatio' specified. Obtained 'plotGlPixelRatio': 2.5
'plotGlPixelRatio' not specified. Obtained 'plotGlPixelRatio': 2.5

I do not know if I am missing something? On a more general perspective, the 'png' seems to be consistent for the scale factor. So I guess the issue is probably on the rendering of pdf and svg only (which are apparently the same)?

Also, for general reference, the concept of high dpi seems to exist for WebGL (I honestly do not know anything about web-based rendering).

@jonmmease jonmmease added the feature something new label Nov 12, 2020
@jonmmease
Copy link
Collaborator

Thanks for the investigation. Looks like we'll need to dig in a bit more to figure out where the config data is getting dropped.

@dperdios
Copy link
Author

Thank you very much for considering this as a potential enhancement. Unfortunately, I will not be able to help you on the code-related specifics. I really hope that there will exist, at some point, a way of controlling the resolution exports of encapsulated rasters. This would be a major improvement for static images, especially for "print" content.

@letmaik
Copy link

letmaik commented Feb 7, 2021

I have the same issue (using the parallel coordinates plot) and wasn't even able to achieve a better resolution with pngs. This really needs some attention as the resulting images are otherwise of not much use for papers etc.

@jonmmease
Copy link
Collaborator

Quick update: With the WIP Plotly.js fixes in plotly/plotly.js#5500, we should be able to fix this problem by exposing the Plotly.js plotGlPixelRatio configuration parameter through Kaleido.

@jonmmease
Copy link
Collaborator

The kaleido portion of this was addressed in #76 and released in 0.2.0. For plotly.js plots that support plotGlPixelRatio (mostly the 3D plot types), kaleido will now automatically increase the resolution of the WebGL portions of figures as the scale increases.

See plotly/plotly.js#5500 to follow ongoing work on adding plotGlPixelRatio support to additional plotly.js trace types.

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

No branches or pull requests

3 participants