Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

[BUG] Graph(style='height=100%') fails when reducing window size on Firefox #605

Closed
orenbenkiki opened this issue Aug 8, 2019 · 18 comments · Fixed by #706
Closed

[BUG] Graph(style='height=100%') fails when reducing window size on Firefox #605

orenbenkiki opened this issue Aug 8, 2019 · 18 comments · Fixed by #706
Assignees
Labels
dash-meta-prioritized dash-type-bug Something isn't working as intended
Milestone

Comments

@orenbenkiki
Copy link

PIP

dash (1.0.2)
dash-bootstrap-components (0.7.0)
dash-core-components (1.0.0)
dash-daq (0.1.7)
dash-html-components (1.0.0)
dash-renderer (1.0.0)
dash-table (4.0.2)

Describe the bug

Writing: Graph(..., style=dict(height='100%'))

Expected behavior

The graph should fill the full height of the div, which will obey normal CSS rules, that is, span all the available space in the layout. It should not cause any overflow, and respond to window resize (also on Firefox ;-).

Actual behavior

  • A scrollbar appears for a small amount of overflow (both Chrome and Firefox). Since the amount is small, it is possible to work around this with overflow=hidden, but it may cause clipping (e.g. if using long labels for the X axis, etc.).

  • Reducing window size does not cause graph resize (Firefox only). That is strange, one would expect that any resize event is handled the same way, regardless if the new size is higher or lower than the original size.

Edit

The following example almost works, except for the two issues above. This threw me off, originally I thought that the feature does not work at all. I edited the issue title and content accordingly.

@orenbenkiki
Copy link
Author

orenbenkiki commented Aug 11, 2019

Here is an example showing the problem:

import dash
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()
app.layout = html.Div(
    id='body',
    style={
        'display': 'flex',
        'flex-flow': 'column',
        'background-color': 'green',
        'height': '100vh',
        'width': '100%'
    },
    children=[
        html.Div(
            id='header',
            children=[
                html.H1('MWE'),
            ]
        ),
        html.Div(
            id='expand',
            style={
                'flex-grow': 1,
                'height': '100%',
                'width': '100%',
                'background-color': 'blue'
            },
            children=[
                dcc.Graph(
                    figure=dict(layout=dict(autosize=True)),
                    style={
                        'height': '100%',
                    },
                ),
            ]
        ),
    ]
)
app.run_server(debug=True)

@orenbenkiki orenbenkiki changed the title [BUG] Graph(style='height=100%') does not work [BUG] Graph(style='height=100%') fails when reducing window size on Firefox Aug 13, 2019
@orenbenkiki
Copy link
Author

orenbenkiki commented Aug 15, 2019

Although the sample example above sort-of-works, in my real-life application it doesn't :-( I have a complex layout using nested flex boxes, specifically a top bar, a side bar, and a main display region which tries to span the remaining space.

Things I have seen:

  • The initial size of the graph is set to 100px * 100px. Any resize event (e.g. if I resize the window) causes the graph to resize to the correct full size.

  • Trying to show a heatmap of 100x * 100px causes "Something went wrong with axis scaling" because the computation of the colorbar scale becomes confused due to the cramped space.

Bottom line: the automatic resize of the graph when the container is at width and height of 100% is too fragile to work for me. I am working around the issue by giving some elements fixed sizes and setting the graph size to something horrible like calc(100vw - 400px). This does work at the cost of using absolute sizes for elements I'd much rather just have their "natural" size :-(

@alexcjohnson alexcjohnson transferred this issue from plotly/dash Aug 16, 2019
@alexcjohnson
Copy link
Collaborator

Moved the issue to the dash-core-components repo where it would be addressed - @antoinerg has some good ideas about how to make this work better 🙏

@alexcjohnson
Copy link
Collaborator

And @orenbenkiki thanks for opening another very clear and helpful issue!

@wbrgss
Copy link
Contributor

wbrgss commented Aug 22, 2019

@orenbenkiki I too have experienced some frustrations using Flexbox layouts with dcc.Graph — there is more work to be done on complex flex layouts. For your simple example, I think this may work:

...
        dcc.Graph(
            figure=dict(layout=dict(autosize=True)),
            config=dict(responsive=True),
            style={
                'height': '100%',
            },
        ),

Behaviour on Firefox without my suggestion (cut-off x-axis)

x-axis-cutoff

With my suggestion (responsive=True)

x-axis-visible

That is, add responsive=True to the config argument in dcc.Graph(). You can thank @antoinerg for that. Related issue plotly/plotly.js#3034 and PR: plotly/plotly.js#3056

Does that solve your problem? The documentation for this could probably be higher-profile, sorry about that. There is a section on it here.

I also want to echo @alexcjohnson that we appreciate your clear and well-defined issues with examples!

@orenbenkiki
Copy link
Author

Possibly these flags do makes things better, but they don't fix my real-world application. Some computation deep in the code looks at "100%" and interprets it as "100px", causing the (very initial) size to be 100 x 100 pixels, which triggers the "something went wrong with axis scaling" error (when computing the colorbar coordinates). I don't have a minimal program to demonstrate this :-(

@Marc-Andre-Rivet Marc-Andre-Rivet added the dash-type-bug Something isn't working as intended label Nov 6, 2019
@Marc-Andre-Rivet Marc-Andre-Rivet added this to the Dash v1.8 milestone Nov 6, 2019
@Marc-Andre-Rivet Marc-Andre-Rivet self-assigned this Nov 19, 2019
@Marc-Andre-Rivet
Copy link
Contributor

Marc-Andre-Rivet commented Nov 19, 2019

Making my way through the comments made above and will provide feedback as I go through them.

I am using:

  • dash==1.6.1 (plotlyjs==1.51.1)
  • Chrome 78 / Firefox 70

A scrollbar appears for a small amount of overflow (both Chrome and Firefox)

This problem is unrelated to the dcc.Graph, while annoying this is normal browser behavior when making a top-level element take 100% of the viewport and is caused by browsers applying a default style to the body element.

In Chrome:

body {
    display: block;
    margin: 8px;
}

Same in Firefox: https://hg.mozilla.org/mozilla-central/file/tip/layout/style/res/html.css#l122

The scrollbar issue can be replicated by simply using an empty div:

<div style='display: flex; flex-flow: column; background-color: green; height: 100vh; width: 100vw' />

There's really only three ways to handle this:

  • explicitly override the body's default margin
body {
    margin: 0`;
}
  • size the top div by taking the margins into consideration (this won't work in all browsers, as they don't all support mixed units)
.my-div {
    height: calc(100vh - 16px);
    width: calc(100vw - 16px);
}
  • update Dash's default style to override the browser defaults for body (reset.css) -- IMO we shouldn't do this

Reducing window size does not cause graph resize (Firefox only)

This seems to be working fine with the versions above. Can you confirm if you still experience this issue?

Although the sample example above sort-of-works, in my real-life application it doesn't :-( I have a complex layout using nested flex boxes

👀 Looking. More to come.

@Marc-Andre-Rivet
Copy link
Contributor

Marc-Andre-Rivet commented Nov 19, 2019

Using a slightly more complex UI I get a graph that can grow but can't get smaller in both Chrome and Firefox.

import dash
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()
app.layout = html.Div(
    style={
        'display': 'flex',
        'flexFlow': 'column',
        'backgroundColor': 'green',
        'height': 'calc(100vh - 16px)',
        'width': 'calc(100vw - 16px)'
    },
    children=[
        html.Div(
            style={
                'backgroundColor': 'blue',
                'flex': '0 0 100px'
            }
        ),
        html.Div(
            style={
                'backgroundColor': 'red',
                'display': 'flex',
                'flex-direction': 'row',
                'flex': '1'
            },
            children=[
                html.Div(
                    style={
                        'backgroundColor': 'yellow',
                        'flex': '0 0 300px'
                    }
                ),
                html.Div(
                    style={
                        'backgroundColor': 'orange',
                        'flex': '1'
                    },
                    children=[
                        dcc.Graph(
                            figure=dict(layout=dict(autosize=True)),
                            style={
                                'height': '100%',
                                'width': '100%'
                            },
                        ),
                    ]
                )
            ]
        )
    ]
)
app.run_server(debug=True)

Adding responsive=True as suggested by @wbrgss improves the behavior and makes it resize consistently when both growing/reducing, down to sizes smaller than 100x100px in both Chrome and Firefox.

The latest versions of plotly.js introduced some fixes to better handle small graphs, so maybe this is why the behavior is better? Or maybe the page structure needs to be more complex, or specific graphs are needs to expose the problem?

@antoinerg Any insight into the latest comments, what would the next steps be?

@antoinerg
Copy link

antoinerg commented Nov 19, 2019

I get a graph that can grow but can't get smaller

That looks a lot like this plotly.js issue plotly/plotly.js#3034 which was fixed by plotly/plotly.js#3056 . What happens when you turn on the configuration option {responsive: true} on the plotly.js figure?

cc @Marc-Andre-Rivet

@Marc-Andre-Rivet
Copy link
Contributor

@antoinerg Too fast! I updated the comment above with responsive=True 😀

@antoinerg
Copy link

antoinerg commented Nov 19, 2019

@Marc-Andre-Rivet By setting responsive=True, plotly.js will change the CSS of the div it draws into as detailed in plotly/plotly.js#3056 . This is necessary to make sure it behaves properly in flex and grid.

However, as of right now, with responsive=True, plotly.js will also add event handlers to the page and trigger the resize operations itself. I think those handlers are unwanted in the context of Dash. Dash should be the one triggering the resize operations and check whether they are all finished before considering the page to be fully loaded. This is really important for dash-snapshots / printing PDF.

To make this reality, I think we may need a new config option in plotly.js to have it add the appropriate CSS on figures but NOT add any javascript event handlers on the page.

@Marc-Andre-Rivet
Copy link
Contributor

Testing this out with a more complex scenario where the elements themselves can be resized without the window being resized. The graph does not react to these changes.

import dash
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output, State

import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash()
app.layout = html.Div(
    style={
        'display': 'flex',
        'flexFlow': 'column',
        'backgroundColor': 'green',
        'height': 'calc(100vh - 16px)',
        'width': 'calc(100vw - 16px)'
    },
    children=[
        html.Div(
            style={
                'backgroundColor': 'blue',
                'flex': '0 0 100px'
            },
            children=[
                html.Button(
                    id='resize',
                    children=['Resize']
                )
            ]
        ),
        html.Div(
            style={
                'backgroundColor': 'red',
                'display': 'flex',
                'flex-direction': 'row',
                'flex': '1'
            },
            children=[
                html.Div(
                    id='menu',
                    style={
                        'backgroundColor': 'yellow',
                        'flex': '0 0 300px'
                    }
                ),
                html.Div(
                    style={
                        'backgroundColor': 'orange',
                        'flex': '1'
                    },
                    children=[
                        dcc.Graph(
                            figure=dict(layout=dict(autosize=True)),
                            config=dict(responsive=True),
                            style={
                                'height': '100%',
                                'width': '100%'
                            },
                        ),
                    ]
                )
            ]
        )
    ]
)

@app.callback(
    Output('menu', 'style'),
    [Input('resize', 'n_clicks')]
)
def resizeContainer(n_clicks):
    if n_clicks is None:
        raise PreventUpdate

    width = (n_clicks % 3) * 300 + 300
    color = 'yellow' if n_clicks % 2 == 0 else 'pink'

    return {
        'backgroundColor': color,
        'flex': '0 0 {}px'.format(width)
    }


app.run_server(debug=True)

@Marc-Andre-Rivet
Copy link
Contributor

Trying out the following strategy:

  • autosize: true + responsive: false applied by default to dcc.Graph -- allows for Plotly.js to be ready for resize events but prevents it from resizing on its own (Dash controls the flow)
  • ResizeObserver / polyfill https://github.com/maslianok/react-resize-detector to trigger Plotly.Plots.resize(target)
  • state driven is-resizing or is-ready class on graph to determine if the graph is ready or in the process of resizing itself -- useful for both tests and usages dependent on the resize/redraw state

@wbrgss
Copy link
Contributor

wbrgss commented Nov 20, 2019

@Marc-Andre-Rivet @antoinerg

Got a demo for you guys, hopefully this will help understand the problem, or at least otherwise educate me on what I'm doing wrong. I'm demonstrating with three ways of describing the figure — px, plotly.graph_objs, and using a dict. I also include a callback to demonstrate how using style={'height': 'inherit'} will still fail when setting a figure via a callback — setting the figure directly as an argument to dcc.Graph appears to work.

dash ==1.6.1
plotly==4.3.0

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
import plotly.graph_objs as go

from dash.dependencies import Input, Output, State, ClientsideFunction

app = dash.Dash(__name__)
server = app.server

iris = px.data.iris()

div_style = {
    # for visual demonstration
    'padding': '10px',
    'background-color': 'red',
    # the key style argument - constrained height containers
    'height': '400px'
}

app.layout = html.Div(children=[

    html.Div(style=div_style,
        children=[
            dcc.Graph(
                figure={
                    'data': [{
                        'x': [1, 2, 3, 4],
                        'y': [5, 4, 3, 6],
                        'line': {'shape': 'spline'}
                    }],
                    'layout': {'autosize': True},
                },
                config={'responsive': True},
            )
        ]
    ),

    # layout spacing, for demo purposes only
    html.Div(style={'height': '200px'}),

    html.Div(style=div_style,
        children=[
            dcc.Dropdown(
                id='flower-dropdown',
                options=[
                    {'label': i, 'value': i} for i in
                    ['Sepal', 'Petal']
                ],
                value='Sepal',
            ),
            dcc.Graph(id='flower-graph',
                # will not even respond to 'height': 'inherit'
                # *when the figure is returned by a callback*

                # not sure if this is px or the callback or both
                config={'responsive': True},
                style={'height': 'inherit'}
            )
        ]
    ),

    html.Div(style={'height': '500px'}),

    html.Div(style=div_style,
        children=[
            dcc.Graph(
                figure={
                    'data': [
                        go.Scatter(
                            x=[1, 2, 3, 4, 5],
                            y=[4, 3, 1, 5, 1],
                            line=go.scatter.Line(shape='spline')
                        )
                    ],
                    'layout': {'autosize': True},
                },
                config={'responsive': True},
                # this will make the graph respond appropriately
                # remove the style argument, and the graph will
                # overflow, despite the 'responsive' flag being set
                style={'height': 'inherit'}
            )
        ]
    ),

    # style arguments for visual demonstration, no affect on graph dimensions
], style={'background-color': 'green', 'height': '300vh'})


@app.callback(
    Output('flower-graph', 'figure'),
    [Input('flower-dropdown', 'value')])
def update_output(val):
    if val == 'Petal':
        x_val = "petal_width"
        y_val = "petal_length"
    else:
        x_val = "sepal_width"
        y_val = "sepal_length"

    fig=px.scatter(iris,
        x=x_val,
        y=y_val,
        color="species")

    fig.update_layout(autosize=True)

    return fig

if __name__ == '__main__':
    app.run_server(debug=True)

@wbrgss
Copy link
Contributor

wbrgss commented Nov 20, 2019

The third example works, but take note of my comments on the style argument — commenting it out will make the graph overflow.

@Marc-Andre-Rivet
Copy link
Contributor

Marc-Andre-Rivet commented Nov 21, 2019

@wbrgss Using the code currently in (early) review #706

For this to work and the graph to be bounded to its container, it needs to be styled appropriately, like any other element. The 1st graph needs, for example:

responsive=True,
style=dict(height='100%', width='100%')

The 2nd graph needs some way to divide the space between itself and the dropdown, this can be achieved for example by adding to the container's style:

dict(dict(display='flex', flexDirection='column'), **div_style)

@sorenwacker
Copy link

It is not yet working for python right?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
dash-meta-prioritized dash-type-bug Something isn't working as intended
Projects
None yet
6 participants