Accessing line width and line color in figure data

Hi all,

As title says I’m trying to access line width and color the same way I already do for data_series name with ClickData.

First, I noticed that I cannot access line width directly from fig.data with

current_width = fig['data'][clicked_series].get('line', {}).get('width')

Why is that? I get the error that width is not in dict. So the width is only in layout?

Below code works however I cannot figure out the indexing:

@callback(
    Output("selected-series-index", "data"),      
    Output("input-data-series", "value"),  
    Output('line-width-dropdown', 'value'),        
    Output("offcanvas2", "is_open"),    
    Input("graph_analysis", "clickData"),
    State("graph_analysis", "figure"),
    State("offcanvas2", "is_open"),                # State of the offcanvas
    prevent_initial_call=True
)
def update_selected_series_and_name_input(clickData, fig, is_open):
    if clickData:
        clicked_series = clickData['points'][0]['curveNumber']
        series_name = fig['data'][clicked_series]['name']
        current_width = fig['layout']['shapes'][clicked_series]['line']['width']   
        return clicked_series, series_name, current_width, True if not is_open else dash.no_update
    return None, '', '', dash.no_update

While the code work for first selected series, for each next series I get:

Traceback (most recent call last):
line:
    current_width = fig['layout']['shapes'][clicked_series]['line']['width']
IndexError: list index out of range

Could you crate a MRE for that? Without knowing exactly your figure structure/content it’s difficult to troubleshoot.

MRE below, I noticed that the error can be avoided if I manually set widths in advance though this is not a preferred way.

import dash
from dash import dcc, html, Input, Output, State, callback
import plotly.graph_objs as go
import dash_bootstrap_components as dbc
import pandas as pd
import plotly.express as px

# Sample data
df = pd.DataFrame({
    'X': [1, 2, 3] * 3,
    'Y': [4, 5, 6, 7, 8, 9, 10, 11, 12],
    'Series': ['Series 1'] * 3 + ['Series 2'] * 3 + ['Series 3'] * 3
})

# Create the plot with Plotly Express
fig = px.line(df, x='X', y='Y', color='Series', line_shape='linear')

# Initialize the Dash app
app = dash.Dash(__name__)

# Define the app layout
app.layout = html.Div([
    dcc.Graph(
        id='graph_analysis',
        figure=fig
    ),
    dcc.Store(id='selected-series-index'),
    dcc.Input(id='input-data-series'),
    dcc.Dropdown(id='line-width-dropdown', options=[{'label': i, 'value': i} for i in range(1, 11)]),
    dbc.Offcanvas(id='offcanvas2')
])

@callback(
    Output("selected-series-index", "data"),      
    Output("input-data-series", "value"),  
    Output('line-width-dropdown', 'value'),        
    Output("offcanvas2", "is_open"),    
    Input("graph_analysis", "clickData"),
    State("graph_analysis", "figure"),
    State("offcanvas2", "is_open"),
    prevent_initial_call=True
)
def update_selected_series_and_name_input(clickData, fig, is_open):
    if clickData:
        clicked_series = clickData['points'][0]['curveNumber']
        series_name = fig['data'][clicked_series]['name']
        current_width = fig['data'][clicked_series]['line']['width'] 
        return clicked_series, series_name, current_width, True if not is_open else dash.no_update
    return None, '', '', dash.no_update

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

In my actual app I am using this code function for the actual plot:

def line(error_y_mode=None, **kwargs):
    """Extension of `plotly.express.line` to use error bands."""
    ERROR_MODES = {'bar','band','bars','bands', None}
    if error_y_mode not in ERROR_MODES:
        raise ValueError(f"'error_y_mode' must be one of {ERROR_MODES}, received {repr(error_y_mode)}.")
    if error_y_mode in {'bar','bars', None}:
        fig = px.line(**kwargs)
    elif error_y_mode in {'band','bands'}:
        if 'error_y' not in kwargs:
            raise ValueError(f"If you provide argument 'error_y_mode' you must also provide 'error_y'.")
        figure_with_error_bars = px.line(**kwargs)
        fig = px.line(**{arg: val for arg,val in kwargs.items() if arg != 'error_y'})
        for data in figure_with_error_bars.data:
            x = list(data['x'])
            y_upper = list(data['y'] + data['error_y']['array'])
            y_lower = list(data['y'] - data['error_y']['array'] if data['error_y']['arrayminus'] is None else data['y'] - data['error_y']['arrayminus'])
            color = f"rgba({tuple(int(data['line']['color'].lstrip('#')[i:i+2], 16) for i in (0, 2, 4))},.3)".replace('((','(').replace('),',',').replace(' ','')
            fig.add_trace(
                go.Scatter(
                    x = x+x[::-1],
                    y = y_upper+y_lower[::-1],
                    fill = 'toself',
                    fillcolor = color,
                    line = dict(
                        color = 'rgba(255,255,255,0)'
                    ),
                    hoverinfo = "skip",
                    showlegend = False,
                    legendgroup = data['legendgroup'],
                    xaxis = data['xaxis'],
                    yaxis = data['yaxis'],
                )
            )
        # Reorder data as said here: https://stackoverflow.com/a/66854398/8849755
        reordered_data = []
        for i in range(int(len(fig.data)/2)):
            reordered_data.append(fig.data[i+int(len(fig.data)/2)])
            reordered_data.append(fig.data[i])
        fig.data = tuple(reordered_data)
    return fig

I guess using plotly express might be a problem here?

Manually setting width in advance seems to work fine though:

    for i, trace in enumerate(fig.data):
        trace.line.width = 2 

Now I remember that I ran into the same problem a while ago. I did not find any solution other than what you mentioned already: make sure the dictionary exists in the initial figure by specifying the line width beforehand.

Same applies for all kind of stuff, such as linewidth, gridwith, gridcolor,zerolinewidth... for axes.

I guess plotly somehow uses default values for these things if you don’t specify them. However, these default values are not reflected in the figure object- thats why you get the error of a missing key if you try to alter these values in a callback.

It’s actually the same for plotly.express and plotly.graph_objects.

1 Like