dash-core-components icon indicating copy to clipboard operation
dash-core-components copied to clipboard

Graph component and Plotly: allow high resolution DPI or PDF export in the download

Open davidpham87 opened this issue 5 years ago • 9 comments

The Graph component allows to display buttons and icon on the top right of any graph. The left most icon is download as PNG, but I could not find any document on how to customize the behavior of plotly.downloadPNG, such as exporting to SVG or high quality PNG.

Is there any solution?

Best regards, David

davidpham87 avatar Dec 06 '18 15:12 davidpham87

There is a way, although it's not documented, which is something I should address!

You can use the toImageButtonOptions key in the config object to specify the download format and width/height of the file that the modebar's "download image" button returns: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L153

nicolaskruchten avatar Dec 06 '18 15:12 nicolaskruchten

I've created an issue to get this documented :) https://github.com/plotly/documentation/issues/1198

nicolaskruchten avatar Dec 06 '18 15:12 nicolaskruchten

Thanks a lot for your swift support!

When increasing the resolution of the plot though, do you have to also increase the font size to have the same ratio between legends and the plot?

So I played with the scaled options, and although the axis and the legends are extremely sharp, the lines of my plots are extremely blurry. Would you know why?

davidpham87 avatar Dec 06 '18 15:12 davidpham87

I recently stumbled on this issue and here is my attempt at helping anyone who would like to save high-res images (say for publication, or inclusion in a PDF in general) of their Plotly figures.

Disclosure: I am by no mean an expert of Dash nor Plotly and I may have misinterpreted something from what I read here and there. Do not hesitate to correct my answer...

Plotly Graph Config

Generally speaking, when you want to give Plotly specific arguments about how to present a given graph_objects.Figure, you can use the config argument of the graph_objects.Figure.show() method. This method expect this config argument to be a dictionary with various keys, as described here.

Note: when using Dash, you can also set the config argument of the dcc.Graph() component, as mentioned here in lieu of the above-mentioned show() method.

Scaling Issue

You have basically two means of playing with the scaling: either by setting the width and height of your picture, or by setting its scale to a given value. Below are three images and the corresponding config to show the influence of these parameters.

Example 1: Scale = 1, 640x480px

newplot

config = {
        'toImageButtonOptions': {
            'format': 'png', # one of png, svg, jpeg, webp
            'height': 480,
            'width': 640,
            'scale': 1 # Multiply title/legend/axis/canvas sizes by this factor
        }
    }

This code produces a well-proportioned image, but with a low resolution (640x480px).

Example 2: Scale = 1, 1280x960px

newplot(1)

config = {
        'toImageButtonOptions': {
            'format': 'png', # one of png, svg, jpeg, webp
            'height': 960,
            'width': 1280,
            'scale': 1 # Multiply title/legend/axis/canvas sizes by this factor
        }
    }

In this second example, the width and height parameters were changed. The resolution of the produced image is doubled (1280x960px), but the proportions are not maintained. Id est the size of the markers, text, axis and lines is not scaled and so they appear far too small.

Example 3: Scale = 2, 640x480px

newplot(2)

config = {
        'toImageButtonOptions': {
            'format': 'png', # one of png, svg, jpeg, webp
            'height': 480,
            'width': 640,
            'scale': 2 # Multiply title/legend/axis/canvas sizes by this factor
        }
    }

In this third example, the scale parameter was changed compared to the first example, but the width and height parameters were left untouched. The resolution of the produced image is doubled (1280x960px) but the overall appearance of the graph is the same as on the low-res first example image.


@davidpham87, I did not manage to reproduce your issue (blurry lines). Could you provide a minimum working example? Or did you find a workaround?

e-dervieux avatar Mar 12 '21 09:03 e-dervieux

This example is awesome and works great, but I'm wondering if there's a way to choose the format interactively, via a dropdown menu or something, just to provide anyone making plots with the option to choose jpg, png, or svg. My workaround right now is to default to svg and people can save that image as jpg or png.

tk27182 avatar Apr 20 '21 22:04 tk27182

@tk27182 here is a MWE:

import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_core_components as dcc
from plotly import graph_objects as go
from plotly.subplots import make_subplots

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div("Please select the saving format below:", id="my_div"),
    dcc.Dropdown(id='format_dropdown',
                 options=[
                            {'label': 'JPEG', 'value': 'jpeg'},
                            {'label': 'PNG', 'value': 'png'},
                            {'label': 'SVG', 'value': 'svg'},
                            {'label': 'WebP', 'value': 'webp'}
                         ],
                 value='jpeg'),
    dcc.Graph(id='my_graph'),

])


@app.callback(
    [Output('my_graph', 'figure'),
     Output('my_graph', 'config')],
    [Input('format_dropdown', 'value')]
)
def update_graph(format_value):

    format_value = format_value if format_value is not None else 'png'
    print(format_value)

    fig = make_subplots()
    fig.update_layout(title="A graph")
    fig.update_xaxes(title_text="Time (s)", rangemode="nonnegative")
    fig.update_yaxes(title_text="Some value (unit)", rangemode="nonnegative")

    fig.add_trace(go.Scatter(x=[1, 2, 3],
                             y=[1, 2, 3],
                             name='Some data',
                             mode='lines+markers',
                             marker=dict(color='blue')))

    config = {
        'toImageButtonOptions': {
            'format': format_value,  # one of png, svg, jpeg, webp
        }
    }

    return [fig, config]


if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0', port=5000)

Basically the idea is that you can use the output of a Dropdown or RadioItem component to trigger a callback that changes the config attribute of the targetted graph object. In this simple example, I used a dcc.Dropdown element to allow the user to choose between the four allowed formats (png, svg, jpeg or webp).

You may find more information about the config attribute options here.

e-dervieux avatar Apr 26 '21 08:04 e-dervieux

there is no way, right now, to set the resolution? I would like to allow downloads of publication ready figures. They often need 300dpi, regardless of the image size. using the size is a workaround, but it is not really great.

sorenwacker avatar May 06 '22 01:05 sorenwacker

@sorenwacker you may want to dive into Example 3 in my above comment https://github.com/plotly/dash-core-components/issues/403#issuecomment-797361923. I may be wrong but your statement about the size being only a workaround does not make much sense to me : the dpi is only the resolution of the image (in pixels) divided by the size at which it is physically displayed (in inches).

For instance a 1200x1200 px image displayed in a 4x4" space will be 300 dpi while the same image displayed in a 6x6" will only be 200 dpi. The dpi of an image file does not make much sense per se, it is the way it is physically printed / displayed that matters. You may have a look here for further information.

Therefore, even if most journals ask for 300 dpi, I tend to interpret it as:

  • the picture occupies in the worst case the full width of the paper (say 6-8" for A4 format in the worst case)
  • 300 dpi x 6-8" = 1800-2400 px
  • thus, if you provide picture about 1-2 kpx wide, you are guaranteed to meet the editor's expectations
  • in any case they'll - hopefully - let you know if you don't

If I misinterpreted your comment please let me know, I don't want to seem pedantic nor to appear as a lesson giver, and might have missed something... I hope that the explanations above are clear enough.

e-dervieux avatar May 06 '22 09:05 e-dervieux

@mranvick Of course, number of pixels and size of the image are related. As you have described, there are in fact two degrees of freedom, the size and the number of pixels, which determine the dpi. We can either control the size and number of pixels, or better size and the resolution, which is of more interest to most people. The choice of which two of the three can be controlled can have a big impact on useability. Right now, as I understand, the number of pixels is a fixed value given a certain image size and cannot be changed, in plotly. Therefore, to get a higher resolution, you have to increase the image size. That leads to relatively smaller elements, labels, line width, etc. Then you have to scale up to correct it. Which is quite inconvenient.

In matplotlib, for example, you can simply set the dpi when saving a figure savefig(dpi=300). If you then scale the figure to the intended size, it will have exactly 300 dpi, ready for print. So, it would be great if the figure config (in Dash) would allow a dpi argument that controls the dpi, or as you prefer the number of pixels, independently. That way you can scale up the figure without all the hassle described above.

I think right now the best way to get a high-resolution figure is to export an SVG and use another program, for example Inkscape or Illustrator, to convert it to png.

Also, the way you described requires the user to determine the image size. However, then the images are not responsive any more.

sorenwacker avatar May 06 '22 17:05 sorenwacker