Dynamic layout does not propagate resized graph dimensions until window is resized

Same question posted on SO here: https://stackoverflow.com/q/55462861/8146556

In the sample Dash application below, I am attempting to create a dynamic layout with a variable number of rows and columns. This dynamic grid-style layout will be populated with various graphs that can be modified by dropdowns, etc.

The main issue I have run into thus far pertains to viewport-units and attempting to style the individual graphs appropriately to accommodate the dynamic layout. For example, I am modifying the style of the dcc.Graph() components via viewport-units, where the dimensions (e.g. height and width may be either 35vw or 23vw depending on the number of columns). When I change the number of columns from 3 to 2, for example, the height and width of the dcc.Graph() component are clearly changed, however this change is not reflected in the actual rendered layout until the window is physically resized (see the images below the sample code).

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.config.suppress_callback_exceptions = True

app.layout = html.Div([

    html.Div(className='row', children=[

        html.Div(className='two columns', style={'margin-top': '2%'}, children=[

            html.Div(className='row', style={'margin-top': 30}, children=[

                html.Div(className='six columns', children=[

                    html.H6('Rows'),

                    dcc.Dropdown(
                        id='rows',
                        options=[{
                            'label': i,
                            'value': i
                        } for i in [1,2,3,4]],
                        placeholder='Select number of rows...',
                        clearable=False,
                        value=2
                    ),

                ]),

                html.Div(className='six columns', children=[

                    html.H6('Columns'),

                    dcc.Dropdown(
                        id='columns',
                        options=[{
                            'label': i,
                            'value': i
                        } for i in [1,2,3]],
                        placeholder='Select number of columns...',
                        clearable=False,
                        value=3
                    ),

                ])

            ]),

        ]),

        html.Div(className='ten columns', id='layout-div', style={'border-style': 'solid', 'border-color': 'gray'}, children=[])

    ])

])

@app.callback(
    Output('layout-div', 'children'),
    [Input('rows', 'value'),
    Input('columns', 'value')])
def configure_layout(rows, cols):

    mapping = {1: 'twelve columns', 2: 'six columns', 3: 'four columns', 4: 'three columns'}
    sizing = {1: '40vw', 2: '35vw', 3: '23vw'}

    layout = [html.Div(className='row', children=[

        html.Div(className=mapping[cols], children=[

            dcc.Graph(
                id='test{}'.format(i+1+j*cols),
                config={'displayModeBar': False},
                style={'width': sizing[cols], 'height': sizing[cols]}
            ),

        ]) for i in range(cols)

    ]) for j in range(rows)]

    return layout

#Max layout is 3 X 4
for k in range(1,13):

    @app.callback(
        [Output('test{}'.format(k), 'figure'),
        Output('test{}'.format(k), 'style')],
        [Input('columns', 'value')])
    def create_graph(cols):

        sizing = {1: '40vw', 2: '35vw', 3: '23vw'}

        style = {
            'width': sizing[cols],
            'height': sizing[cols],
        }

        fig = {'data': [], 'layout': {}}

        return [fig, style]

if __name__ == '__main__':
    app.server.run()

Relevant screenshots (Image 1 - page load, Image 2 - change columns to 2):

Is there a solution to this issue ? I’ve the same problem when I update a className.

FOr instance, switching from “three columns” and “nine columns” to six / six. Graph in these div are not resized until I resize the window.

The only workaround I am currently aware of is available via the SO link in my original post

Mmhh… interesting, but there is now a js script by default in the asset folder, which contains

if (!window.dash_clientside) {
  window.dash_clientside = {};
}
window.dash_clientside.clientside = {
  resize: function(value) {
    console.log("resizing..."); // for testing
    setTimeout(function() {
      window.dispatchEvent(new Event("resize"));
      console.log("fired resize");
    }, 500);
    return null;
  }
};

I also noticed that last example in the dash gallery include:
from dash.dependencies import Input, Output, State, ClientsideFunction

and somewhere in the app there is a
html.Div(id=“output-clientside”),

so, I’m wondering if this is related to the issue we are focusing on, and if yes, then, it looks like the vsdcc.js is maybe not necessary?

Will keep digging…

Interesting, curious to see if you are able to uncover anything there. The solution that I referenced was quite a few months ago, thus prior to the dash 1.0 release, which may or may not have addressed this issue.

I also found this, in dash.py:

def clientside_callback(
        self, clientside_function, output, inputs=[], state=[]):
    """
    Create a callback that updates the output by calling a clientside
    (JavaScript) function instead of a Python function.

    Unlike `@app.calllback`, `clientside_callback` is not a decorator:
    it takes a
    `dash.dependencies.ClientsideFunction(namespace, function_name)`
    argument that describes which JavaScript function to call
    (Dash will look for the JavaScript function at
    `window[namespace][function_name]`).

    For example:
    ```
    app.clientside_callback(
        ClientsideFunction('my_clientside_library', 'my_function'),
        Output('my-div' 'children'),
        [Input('my-input', 'value'),
         Input('another-input', 'value')]
    )
    ```

    With this signature, Dash's front-end will call
    `window.my_clientside_library.my_function` with the current
    values of the `value` properties of the components
    `my-input` and `another-input` whenever those values change.

    Include a JavaScript file by including it your `assets/` folder.
    The file can be named anything but you'll need to assign the
    function's namespace to the `window`. For example, this file might
    look like:
    ```
    window.my_clientside_library = {
        my_function: function(input_value_1, input_value_2) {
            return (
                parseFloat(input_value_1, 10) +
                parseFloat(input_value_2, 10)
            );
        }
    }
    ```
    """

And this particular callback, without @, is in the last bunch of dash apps examples available in the dash gallery.

app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="resize"),
    Output("output-clientside", "children"),
    [Input("count_graph", "figure")],
)

So, when Input is updated, the resize function is executed.

Trying to figure out how to force window.dispatchEvent(new Event(‘resize’) to be triggered when i change a className. Will update this post once done

EDIT

Ok, done.

Here is how to proceed:

  1. app.py must import:
    from dash.dependencies import Input, Output, State, ClientsideFunction

  2. let’s include the below Div somewhere in the Dash layout:
    html.Div(id="output-clientside"),

  3. asset folder must include either your own script, or the default script resizing_script.js, which contains:

    if (!window.dash_clientside) {
        window.dash_clientside = {};
    }
    window.dash_clientside.clientside = {
        resize: function(value) {
            console.log("resizing..."); // for testing
            setTimeout(function() {
                window.dispatchEvent(new Event("resize"));
                console.log("fired resize");
            }, 500);
        return null;
        },
    };
    
  4. Among your callbacks, put this one, without @:

    app.clientside_callback(
        ClientsideFunction(namespace="clientside", function_name="resize"),
        Output("output-clientside", "children"),
        [Input("yourGraph_ID", "figure")],
    )    
    

At this point, when you manually resize the window, in your browser, the resize function is triggered.

We aim to achieve the same result, but without manual window resizing. For instance, the trigger could be a className update.

So, we apply the following changes:
Step 1: unchanged
Step 2: unchanged
Step 3: let’s add a “resize2” function inside our javascript file, which takes 2 arguments:

if (!window.dash_clientside) {
  window.dash_clientside = {};
}
window.dash_clientside.clientside = {
  resize: function(value) {
    console.log("resizing..."); // for testing
    setTimeout(function() {
      window.dispatchEvent(new Event("resize"));
      console.log("fired resize");
    }, 500);
    return null;
  },

  resize2: function(value1, value2) {
    console.log("resizingV2..."); // for testing
    setTimeout(function() {
       window.dispatchEvent(new Event("resize"));
       console.log("fired resizeV2");
    }, 500);
    return value2; // for testing
  }
};

Function “resize2” now takes 2 arguments, one for each Input defined in the below callback. It will return the value of “value2” in the Output, specified in this very same callback. You can set it back to “null”, it’s just to illustrate.

Step4: our callback now becomes:

app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="resize2"),
    Output("output-clientside", "children"),
    [Input("yourGraph_ID", "figure"), Input("yourDivContainingYourGraph_ID", "className")],
)    

Finally, you need a button to trigger the event which will change the className of your container.

let’s say your have:

daq.ToggleSwitch(
    id='switchClassName',
    label={
        'label':['Option1', 'Option2'],
    },          
    value=False,                                          
),  

And the following callback:

@app.callback(Output("yourDivContainingYourGraph_ID", "className"), 
              [Input("switchClassName","value")]
              )
def updateClassName(value):
    if value==False:
        return "twelve columns"
    else:
        return "nine columns"

And now, if you save everything, refresh, everytime you press on your toggleSwitch,it resizes the container, triggers the function, and refresh the figure.

Given the way it’s done, I assume it must also be possible to run more Javascript functions, the same way.

Hope it will help some :slight_smile:

4 Likes

This is excellent, thanks so much for the detailed solution. Would you mind adding this as a solution to the same SO question I have linked in my original post? I can post it, but want you to have the opportunity to answer it considering your work on the topic.

Ok, I added the sol on SO too :slight_smile: