Dash - Dynamic layout does not propagate resized graph dimensions until window is resized
Here is how to proceed:
app.py must import:
from dash.dependencies import Input, Output, State, ClientsideFunction
let’s include the below Div somewhere in the Dash layout:
html.Div(id="output-clientside"),
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;
},
};
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"
Now, if you save everything, refresh, everytime you press on your toggleSwitch,it resizes the container, triggers the function, and refreshes the figure.
Given the way it’s done, I assume it must also be possible to run more Javascript functions, the same way, but I didnt check yet.
Hope it will help some
The behavior looks like a Plotly bug to me.
Here is a possible workaround/short-termin solution.
There is a nice library visdcc
which allows callbacks with Javascript. You can install it via
pip install visdcc
Add it to your div
:
visdcc.Run_js(id='javascript'),
and add a callback
@app.callback(
Output('javascript', 'run'),
[Input('rows', 'value'),
Input('columns', 'value')])
def resize(_, __):
return "console.log('resize'); window.dispatchEvent(new Event('resize'));"
Plotly will throw an error in the console after the resize
event (this also happens when the windows is manually resized) but the plots are shown correctly.
Full code
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import visdcc
SIZING = {1: '40vw', 2: '35vw', 3: '23vw'}
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([
visdcc.Run_js(id='javascript'),
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'}
layout = [html.Div(className='row', children=[
html.Div(className=mapping[cols], style={'width': SIZING[cols], 'height': SIZING[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
@app.callback(
Output('javascript', 'run'),
[Input('rows', 'value'),
Input('columns', 'value')])
def resize(_, __):
return "console.log('resize'); window.dispatchEvent(new Event('resize'));"
#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):
style = {
'width': SIZING[cols],
'height': SIZING[cols],
}
fig = {'data': [], 'layout': {}}
return [fig, style]
if __name__ == '__main__':
app.server.run()