Multiple traces with a single slider in plotly

Iā€™m trying to create a single slider which controls two traces using Python 3 and Plotly. I am using Spyder although Iā€™ve also tried the same code modified for Jupyter without success.

This is a sample of code:

import numpy as np
#import pandas as pd
#import matplotlib.pyplot as plt

import plotly as py
import plotly.graph_objs as go
#import ipywidgets as widgets

py.offline.init_notebook_mode(connected=True) 



'''
Plot traces
'''
trace1 = [dict(
        type = 'scatter',
        visible = False,
        name='Demand',
        x = np.arange(0,365,1),
        y = np.sin(step*np.arange(0,365,1))) for step in range(0,365,1)]


trace2 = [dict(
        type = 'scatter',
        visible = False,
        name='Demand',
        x = np.arange(0,365,1),
        y = np.sin(step*np.arange(0,365,1)) + 5) for step in range(0,365,1)]


#data = trace2

data = [list(a) for a in zip(trace1, trace2)]


steps = []
for i in range(len(data)):
    step = dict(
        method = 'restyle',  
        args = ['visible', [False] * len(data)],
        label = ""
    )
    step['args'][1][i] = True # Toggle i'th trace to "visible"
    steps.append(step)

sliders = [dict(
    active = 10,#What's happening here?
    currentvalue = {"prefix": "Day: "}, #????
    #pad = {"t": 50},
    steps = steps
)]

#Add styling
layout = go.Layout(sliders=sliders,
    title='Sample slider plot',
    xaxis=dict(
        title=ā€˜Testā€™,
        titlefont=dict(
            family='Courier New, monospace',
            size=18,
            color='#7f7f7f'
        )
    ),
    yaxis=dict(
        title=ā€˜Unitā€™,
        titlefont=dict(
            family='Courier New, monospace',
            size=18,
            color='#7f7f7f'
        )
    )
)


fig = go.Figure(data=data, layout=layout)

py.offline.plot(fig, filename='Test3')

If I plot either of the traces individually i.e.

data = trace1

it works. However, once I combine trace1 and trace2 i.e.

data = [list(a) for a in zip(trace1, trace2)]

I get the following error:

The 'data' property is a tuple of trace instances
    that may be specified as:
      - A list or tuple of trace instances
        (e.g. [Scatter(...), Bar(...)])
      - A list or tuple of dicts of string/value properties where:
        - The 'type' property specifies the trace type
            One of: ['area', 'bar', 'barpolar', 'box',
                     'candlestick', 'carpet', 'choropleth', 'cone',
                     'contour', 'contourcarpet', 'heatmap',
                     'heatmapgl', 'histogram', 'histogram2d',
                     'histogram2dcontour', 'mesh3d', 'ohlc',
                     'parcats', 'parcoords', 'pie', 'pointcloud',
                     'sankey', 'scatter', 'scatter3d',
                     'scattercarpet', 'scattergeo', 'scattergl',
                     'scattermapbox', 'scatterpolar',
                     'scatterpolargl', 'scatterternary', 'splom',
                     'streamtube', 'surface', 'table', 'violin']

        - All remaining properties are passed to the constructor of
          the specified trace type

        (e.g. [{'type': 'scatter', ...}, {'type': 'bar, ...}])

Checking type(trace1) shows that each trace is a list of 365 dict objects, which plotly can handle. However, once I combine the traces into the ā€˜dataā€™ object as described above, I get a list of lists, which it cannot.

So my question is: how do I combine trace1 and trace2 into an object that has the right length for the steps of the slider (i.e. 365) and which plotly can deal with - such as a list of dicts or trace instances, rather than a list of lists?

I have looked at plotly - multiple traces using a shared slider variable and also Plot.ly. Using slider control with multiple plots. Havenā€™t had success unfortunately. The first link works if the trace objects are dicts, but not if you have to enclose them in square brackets to make a list for the ā€˜forā€™ loop to work. The second link just adds the trace1 + trace2 to create an object with index length 730 - that is, it joins the two traces successively instead of displaying them together.

Thanks very much in advance for help!

Hi @sumins,

You will need to append all of your traces into a single (730 element) list. The trick is to make the arguments to your slider aware of the ordering of traces. You want the first step to enable traces 0 and 365, the second to enable 1 and 366, etc. Hereā€™s an example with three slider steps and 6 total traces

import plotly.graph_objs as go
from plotly.offline import init_notebook_mode, iplot
init_notebook_mode()

num_steps = 3
trace_list1 = [
    go.Scatter(y=[1, 2, 3], visible=True, line={'color': 'red'}),
    go.Scatter(y=[3, 1, 1.5], visible=False, line={'color': 'red'}),
    go.Scatter(y=[2, 2, 1], visible=False, line={'color': 'red'})
]

trace_list2 = [
    go.Scatter(y=[1, 3, 2], visible=True, line={'color': 'blue'}),
    go.Scatter(y=[1.5, 2, 2.5], visible=False, line={'color': 'blue'}),
    go.Scatter(y=[2.5, 1.2, 2.9], visible=False, line={'color': 'blue'})
]

fig = go.Figure(data=trace_list1+trace_list2)

steps = []
for i in range(num_steps):
    # Hide all traces
    step = dict(
        method = 'restyle',  
        args = ['visible', [False] * len(fig.data)],
    )
    # Enable the two traces we want to see
    step['args'][1][i] = True
    step['args'][1][i+num_steps] = True
    
    # Add step to step list
    steps.append(step)

sliders = [dict(
    steps = steps,
)]

fig.layout.sliders = sliders

iplot(fig, show_link=False)

Hope that helps!
-Jon

1 Like

Hi Jon,
This worked like a dream. I was really struggling. Thank you!

S

1 Like

Hey, thanks to the code snippet, Iā€™ve been able to manage how to build a working slider. Thank you for that ! :+1:

However, Iā€™m now facing a new issue. How can I make my slider aware of which traces have been toggled (by clicking on the tracesā€™ name in the legend) ?

Illustration:
Using your code, if you disable a trace and then update the slider value (by moving it), both traces will be updated and displayed again (and not only the selected one). What can I do in order to keep the trace disabled/invisible (since I donā€™t know which trace will be toggled or not when building the slider) ? I know it is possible using Plotly Express (see this example [when you toggle one or more traces and then you use the slider, these traces will stay disabled]) but I didnā€™t find out how to do this with plotly.graph_objectsā€¦ :confused:

Any help would be welcome :upside_down_face:

1 Like

@acoque,

Could you please explain what trace type are you plotting in each subplot, and what do you intend to update/restyle in each subplot by moving a slider.

Hi @empet,

Of course ! Itā€™s the least I can do.
Basically, Iā€™m plotting absorption spectra juxtaposed to water absorption profiles (for multiple points) - both subplots use ā€œsimpleā€ lines, lines with markers and filled lines. The slider is used to change the wavelength of the absorption profiles (the 2nd subplot) - so it turn specific traces (in)visible (as in jmmeaseā€™s example). At the moment, everything works fine, excepted that when one updates the wavelength (by moving the cursor) it does not keep the ā€œtoggling informationā€ (I donā€™t know if I can say that :confused:), as can be seen on the following gif (I didnā€™t plot any filled lines [=standard deviation] here but it works the same way).

I hope my explanations arenā€™t too messy :thinking:

@acoque

To suggest how you should define the slider steps to keep the toggling information, I need a minimal code with synthetic data, and your actual definition of sliders. My feeling is that in this case buttons are more effective than sliders.

Hi @empet,

Thx for the feedback. Hereā€™s the code:

import pandas as pd
from plotly import graph_objs as go
from plotly.colors import DEFAULT_PLOTLY_COLORS
from plotly.subplots import make_subplots

"""
    Some (useful) parameters of the Dash app's callback
"""
std = True  # plot of the standard deviation
selected_obs = 'a_AC-s'     # what to plot : absorption, reflectance...
blist_selected_points = [True, False]   # simplified (normally depends on which points are selected in a dropdown menu)


"""
    Synthetic data
"""
df_obs_wl = pd.DataFrame([['1', 0, 400, 10, 1], ['1', 0, 500, 8, 0.5],
                         ['1', 0, 600, 6, 0.5], ['1', 0, 700, 7, 1],
                         ['1', 1, 400, 9, 1.5], ['1', 1, 500, 7, 1],
                         ['1', 1, 600, 5, 1], ['1', 1, 700, 6, 1.5],
                         ['1', 2, 400, 8, 2], ['1', 2, 500, 6, 1.5],
                         ['1', 2, 600, 4, 1.5], ['1', 2, 700, 5, 2],
                         ['2', 0, 400, 7.5, 1], ['2', 0, 500, 6.5, 0.5],
                         ['2', 0, 600, 8.5, 0.5], ['2', 0, 700, 10.5, 1],
                         ['2', 1.5, 400, 6.5, 1.5], ['2', 1.5, 500, 5.5, 1],
                         ['2', 1.5, 600, 7.5, 1], ['2', 1.5, 700, 9.5, 1.5],
                         ['2', 2.25, 400, 5.5, 2], ['2', 2.25, 500, 4.5, 1.5],
                         ['2', 2.25, 600, 6.5, 1.5], ['2', 2.25, 700, 8.5, 2]],
                         columns=['point', 'depth', 'wl', 'mean', 'std']
                         ).set_index(['point', 'depth', 'wl'])
df_obs_depth = df_obs_wl.swaplevel('depth', 'wl')

points = df_obs_wl.index.get_level_values('point').unique()
wls = df_obs_depth.index.get_level_values('wl').unique()


"""
    Figure creation
"""
fig = make_subplots(rows=1, cols=2, subplot_titles=('Spectra', 'Profiles'))
for i, point in enumerate(points):
    style = {'name': point, 'legendgroup': point, 'mode': 'lines',
             'line_color': DEFAULT_PLOTLY_COLORS[i % len(DEFAULT_PLOTLY_COLORS)],
             'marker_color': DEFAULT_PLOTLY_COLORS[i % len(DEFAULT_PLOTLY_COLORS)]}
    if not blist_selected_points[i]:
        style['visible'] = 'legendonly'
    depths = df_obs_wl.loc[point].index.get_level_values('depth').unique()
    data_wl = df_obs_wl.loc[point, depths[0]]
    fig.add_trace(go.Scatter(x=data_wl.index.array, y=data_wl['mean'].array, **style), row=1, col=1)
    if std:  # "fill issue" when NaN in y-axis, see https://github.com/plotly/plotly.js/issues/2736
        style_bis = {'name': point, 'legendgroup': point, 'fill': 'toself', 'showlegend': False,
                     'line_color': 'rgba(192, 192, 192, 0)'}
        if not blist_selected_points[i]:
            style_bis['visible'] = 'legendonly'
        fig.add_trace(go.Scatter(x=data_wl.index.to_list() + data_wl.index.to_list()[::-1],
                                 y=(data_wl['mean'] + data_wl['std']).to_list()
                                   + (data_wl['mean'] - data_wl['std']).to_list()[::-1], **style_bis),
                      row=1, col=1)
for i, point in enumerate(points):
    style = {'name': point, 'legendgroup': point, 'mode': 'lines', 'visible': False, 'showlegend': False,
             'line_color': DEFAULT_PLOTLY_COLORS[i % len(DEFAULT_PLOTLY_COLORS)],
             'marker_color': DEFAULT_PLOTLY_COLORS[i % len(DEFAULT_PLOTLY_COLORS)]}
    style_bis = {'name': point, 'legendgroup': point, 'fill': 'toself', 'visible': False, 'showlegend': False,
                 'line_color': 'rgba(192, 192, 192, 0)'}
    for wl in wls:
        data_depth = df_obs_depth.loc[point, wl]
        fig.add_trace(go.Scatter(y=data_depth.index.array, x=data_depth['mean'].array, **style),
                      row=1, col=2)
        if std:  # fill issue when NaN in y-axis, see https://github.com/plotly/plotly.js/issues/2736
            filtered_data_depth = data_depth.dropna()
            fig.add_trace(
                go.Scatter(y=filtered_data_depth.index.to_list() + filtered_data_depth.index.to_list()[::-1],
                           x=(filtered_data_depth['mean'] + filtered_data_depth['std']).to_list()
                             + (filtered_data_depth['mean'] - filtered_data_depth['std']).to_list()[::-1],
                           **style_bis),
                row=1, col=2)
show = {True: True, False: 'legendonly'}
if std:
    for i in range(len(points)):
        fig.data[2 * len(points) + 2 * i * len(wls)].visible = show[blist_selected_points[i]]
        fig.data[2 * len(points) + 2 * i * len(wls) + 1].visible = show[blist_selected_points[i]]
else:
    for i in range(len(points)):
        fig.data[len(points) + i * len(wls)].visible = show[blist_selected_points[i]]

fig.update_xaxes(title_text='λ (nm)', row=1, col=1)
fig.update_xaxes(side='top', title_text=selected_obs, row=1, col=2)
fig.update_yaxes(domain=[0, 0.8], title_text=selected_obs, row=1, col=1)
fig.update_yaxes(autorange='reversed', domain=[0, 0.8], title_text='depth (m)', row=1, col=2)

"""
    Definition of the slider
"""
steps = []
for i, wl in enumerate(wls):
    step = {
        'method': 'restyle',
        'label': wl,
        'args': ['visible', [False] * len(fig.data)]
    }
    """
    Here is the problematic part of the code.
    I'd like to precise that if I replace "show[blist_selected_points[j]]" by True, it still remains problematic
    as it does not keep what I called the "toggling information" (i.e. previous user interaction with the legend).
    What I would need is a way to set True or False according the current state of the legend (which trace are currently
    selected or not).
    """
    if std:
        for j in range(len(points)):
            step['args'][1][2 * j] = show[blist_selected_points[j]]
            step['args'][1][2 * j + 1] = show[blist_selected_points[j]]
            step['args'][1][2 * len(points) + 2 * j * len(wls) + 2 * i] = show[blist_selected_points[j]]
            step['args'][1][2 * len(points) + 2 * j * len(wls) + 2 * i + 1] = show[blist_selected_points[j]]
    else:
        for j in range(len(points)):
            step['args'][1][j] = show[blist_selected_points[j]]
            step['args'][1][len(points) + j * len(wls) + i] = show[blist_selected_points[j]]
    steps.append(step)
sliders = [{
    'active': 0,
    'currentvalue': {'prefix': 'Wavelength: '},
    'pad': {'t': 50},
    'steps': steps
}]

fig.update_layout(margin={'b': 125, 'l': 0, 'r': 0, 't': 25}, template='simple_white', sliders=sliders)

fig.show()

I hope it will help, but my actual definition of the slider (as you can see above) is almost strictly a copy/paste of what wrote jmmease, nothing more (or less :wink:).

I would be interested to know more about your idea of using buttons rather than a slider, because I really donā€™t see what you would have done. I believed a slider was what I needed because I have to handle an unknown amount of points/traces (from 0 to ~60) and I got something like 84 different wavelengths and ~40 depth levels.

Have a nice day,
Arthur

Hi @acoque,

I realized what you expected, but unfortunately each slider step changes the visibility of traces in the initial fig.data. The slider steps are not aware of the clicked legend item. Theoretically it could work if you previously recorded the legend click and based on the corresponding visibility change, to (re)define the step arg. But with plotly.py this isnā€™t possible :frowning:

Yeah, thatā€™s what I suspected. Too badā€¦:cry: Anyway, thanks for you help.