Update subplots column_widths

Hi,
I would like to know if there is a method to update the column_widths attribute of subplots. For instance, let’s say I have a figure initialized as

    fig = make_subplots(
        rows=1, cols=2,
        column_widths=[.6,.4],
        specs=[[{"type": "scatter"}, {"type": "scatter"}]],
        horizontal_spacing=0.01,
        )

is there something like

fig.update_layout(column_widths=[.5,.5])

?

hi @vincentm
Welcome to the community. Good question.

You can uniformly control column width through udpate_traces().

Hi and thanks. This didn’t work for me. To give more context, here are the versions of the plotly and dash libraries I’m using

Python 3.8.10 (default, Mar 15 2022, 12:22:08) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import plotly
>>> import dash
>>> plotly.__version__
'5.7.0'
>>> dash.__version__
'2.1.0'
>>> 

If I do

    fig = make_subplots(
        rows=1, cols=2,
        # column_widths=[split, 1. - split],
        specs=[[{"type": "scatter"}, {"type": "scatter"}]],
        horizontal_spacing=0.01,
        )
    fig.update_traces(column_widths=[split, 1. - split])

where split is in [0,1], the subplot figure is created with split = .5, the default I assume. Then my code adds a bunch of traces to each plots in the figure. If I instead call update_traces just before returning

    fig.update_traces(column_widths=[split, 1. - split])
    return fig

I get an error like

Traceback (most recent call last):
  File "/home/vincentm/repos/es_app/es/app.py", line 201, in update_graph
    fig = plotly_plot.plot_bs_dos(bsdict=bsdicts, dosdict=dosdicts,
  File "/home/vincentm/repos/es_app/es/plotting.py", line 77, in plot_bs_dos
    fig.update_traces(column_widths=[split, 1. - split])
  File "/home/vincentm/repos/es_app/venv/lib/python3.8/site-packages/plotly/basedatatypes.py", line 1376, in update_traces
    trace.update(patch, overwrite=overwrite, **kwargs)
  File "/home/vincentm/repos/es_app/venv/lib/python3.8/site-packages/plotly/basedatatypes.py", line 5099, in update
    BaseFigure._perform_update(self, kwargs, overwrite=overwrite)
  File "/home/vincentm/repos/es_app/venv/lib/python3.8/site-packages/plotly/basedatatypes.py", line 3887, in _perform_update
    raise err
ValueError: Invalid property specified for object of type plotly.graph_objs.Scatter: 'column'

Did you mean "fill"?

    Valid properties:

Basically, I would like an efficient way to resplit the figure with a callback in a dash app. So I was wondering whether it is possible to just reset column_widths without regenerating the whole figure.

hi @vincentm

Are you having trouble replicating this example code from the docs?

from dash import Dash, dcc, html, Input, Output
from plotly.subplots import make_subplots
import plotly.graph_objects as go

app = Dash(__name__)


app.layout = html.Div([
    html.H4('Live adjustable subplot-width'),
    dcc.Graph(id="graph"),
    html.P("Subplots Width:"),
    dcc.Slider(
        id='slider-width', min=.1, max=.9, 
        value=0.5, step=0.1)
])


@app.callback(
    Output("graph", "figure"), 
    Input("slider-width", "value"))
def customize_width(left_width):
    fig = make_subplots(rows=1, cols=2, 
        column_widths=[left_width, 1 - left_width])

    fig.add_trace(row=1, col=1,
        trace=go.Scatter(x=[1, 2, 3], y=[4, 5, 6])) # replace with your own data source

    fig.add_trace(row=1, col=2,
        trace=go.Scatter(x=[20, 30, 40], y=[50, 60, 70]))
    return fig


app.run_server(debug=True)

You need to update the column_widths inside the make_subplots(). Don’t do it with fig.update_traces()

Hi,

I can replicate this, but in this example, the callback regenerates the figure, doesn’t merely updates it. I was wondering whether there is a quick way to just update column_widths in the case when the figure is more costly to generate.

Thanks.

@vincentm

Changing column_widths implies a fig.layout_update , i.e. for each subplot cell of spec type ‘xy’ or ‘scene’, must be updated the corresponding xaxis domain (for ‘xy’ type), respectively the correspondin scene, x domain. The range of each domain is computed taking into account a few of make_subplots settings . The custom function new_xdomains() , defined below, computes the new ranges for these domains. It is defined such that to be used in subplots like yours, i.e. subplots with specs of type in [‘xy’, ‘scene’], or specs type, the trace name, but not in [‘polar’, ‘ternary’, ‘mapbox’, ‘domain’]. Also no cell pads are allowed in specs definition, and are considered only the default settings:

column_titles=None,
row_titles=None,
x_title=None,
y_title=None`

Any modified settings among those listed above, involves a special treatment.

import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np


def new_xdomains(rows, cols, col_w, horizontal_spacing, specs=None):
    #rows, cols -integers passed to make_subplots()
    
    #col_w - list of new column widths
    #specs - None or list of specs passed to make_subplots()
    #returns new xaxis domain for each subplot cell, corresponding to the new column_width, i.e. col_w
    if specs is None:
        specs = [[{} for c in range(cols)] for r in range(rows)]
    else:
        if len(specs) != rows or len(specs[0]) != cols:
            raise ValueError('bad length of the list specs')
        else:    
            for srow in specs:
                for spec in srow:
                    if spec['type']  in ['polar','ternary','mapbox', 'domain']:
                        raise ValueError('this function works only for spec type "xy", and "scene"')
                    else: continue    
    if len(col_w) != cols:
        raise ValueError('your list of new column widths must have the length f"{col}"')
    else:
        cs = float(sum(col_w))
        
    xd_widths=[] #actual lengths of new columns,ie taking into account horizontal_spacing, too
    for w in col_w:
        xd_widths.append((1 - horizontal_spacing * (cols - 1)) * (w/cs))
  
    #these settings for col_list and row_list are valid when subplot cells
    #are indexed as matrices, i.e (1,1) is the upper left cell
    col_list=range(cols)[::1]
    row_list=range(rows)[::-1]

    left_xdom =  [[sum(xd_widths[:c]) + c * horizontal_spacing for c in col_list] for r in row_list] 
   
    xds= []
    for r, spec_row in enumerate(specs):
        for c, spec in enumerate(spec_row):

            if spec is None:  
                continue

            xl = left_xdom[r][c] 
            xr = left_xdom[r][c] + xd_widths[c]
            
            xds.append([xl, xr])
    return xds   #xds is the list of x domains for  the cells enumerated line by line   (in our example (1,1), (1,2), (2,1), (2,2)

Let us give a general example of subplot with admissible specs:

#vars whose values are used to set    keywords in  make_subplots and to are passed to the function  new_xdomain
specs=[[{"type": "xy"}, {"type": "heatmap"}], 
       [{"type": "scene"}, {}]]
rows=2
cols = 2
horizontal_spacing=0.05
#end  vars definitions
fig = make_subplots(subplot_titles=("Title (1,1)", "Title (1,2)", "Title (2,1)(3D)", "Title (2,2)"),
        rows=rows, cols=cols,
        column_widths=[.4,.6],
        specs=specs,
        horizontal_spacing=horizontal_spacing,
        vertical_spacing=0.08
        )
print(fig.layout)  #print the initial layout to decide what we'll update

fig.add_trace(go.Scatter(x=[1,2,3], y=[-1 , 1.35, 0.7], mode="lines"), 1,1)
fig.add_trace(go.Heatmap(z=np.random.randint(2, 13, (5,5)), showscale=False), 1, 2)
fig.add_trace(go.Surface(x=[1,2,3,4], y=[2,3,4], z= np.random.randint(1, 15, (3, 4)), showscale=False), 2, 1)
fig.add_trace(go.Scatter(x=2*np.random.rand(8), y=1+3*np.random.rand(8), mode="markers"), 2,2)

fig.update_layout(width=770, height=600, showlegend=False)
fig.update_annotations(font_size=12)

This is the initial subplot fig:

new_column_widths = [0.55, 0.45]

xd_lims=new_xdomains(rows, cols, new_column_widths,  horizontal_spacing, specs=specs)
print(xd_lims)
fig.update_layout(xaxis_domain= xd_lims[0], 
                  scene_domain_x=xd_lims[2])
fig.update_layout({'xaxis2': {'domain': xd_lims[1]},
            'xaxis3': {'domain': xd_lims[3]}
           })

The new figure after changing column_widths by new_column_widths:

Conclusion: We cannot update column_widths via fig.update_traces(), but via fig.update_layout() after calling the above auxilliary function, that returns the new xaxes domains.

4 Likes