It's doable, but takes a lot more steps than the method you're currently using. One solution to achieving this with plotly is by creating subplots with multidimensional axis, one for Lunch and one for Dinner, with zero space in between so it looks like one single plot.
import plotly.express as px
import pandas as pd
import numpy as np
from pandas.api.types import CategoricalDtype
df = px.data.tips()
#set order for days
days = CategoricalDtype(
['Thur', 'Fri', 'Sat', 'Sun'],
ordered=True
)
df['day'] = df['day'].astype(days)
#sort df
df.sort_values(['time', 'sex', 'day'], inplace=True)
#create framework
fig = make_subplots(rows=1,
cols=2,
shared_yaxes=True,
horizontal_spacing=0,
column_widths=[7/11, 4/11])
#create "Dinner" boxplots
fig.add_trace(go.Box(x=[df['sex'][df['time']=='Dinner'].tolist(), df['day'][df['time']=='Dinner'].tolist()],
y=df['tip'][df['time']=='Dinner'],
boxpoints=False,
pointpos=0,
line=dict(color='gray',
width=1),
fillcolor='white',
showlegend=False),
row=1,
col=1)
#add "Dinner" smokers
fig.add_trace(go.Scatter(x=[df['sex'][(df['time']=='Dinner') & (df['smoker']=='Yes')].tolist(), df['day'][(df['time']=='Dinner') & (df['smoker']=='Yes')].tolist()],
y=df['tip'][(df['time']=='Dinner') & (df['smoker']=='Yes')],
mode='markers',
marker=dict(color='red',
symbol='circle-open',
size=10),
name='Yes'
),
row=1,
col=1)
#add "Dinner" non-smokers
fig.add_trace(go.Scatter(x=[df['sex'][(df['time']=='Dinner') & (df['smoker']=='No')].tolist(), df['day'][(df['time']=='Dinner') & (df['smoker']=='No')].tolist()],
y=df['tip'][(df['time']=='Dinner') & (df['smoker']=='No')],
mode='markers',
marker=dict(color='green',
symbol='cross-thin-open',
size=10),
name='No'
),
row=1,
col=1)
df_mean = df[['sex', 'day', 'tip']][df['time']=='Dinner'].groupby(['sex', 'day']).mean().reset_index().dropna()
#add "Dinner" mean line
fig.add_trace(go.Scatter(x=[df_mean['sex'].tolist(), df_mean['day'].tolist()],
y=df_mean['tip'].tolist(),
showlegend=False,
marker=dict(color='black')
),
row=1,
col=1)
#create "Lunch" boxplots
fig.add_trace(go.Box(x=[df['sex'][df['time']=='Lunch'].tolist(), df['day'][df['time']=='Lunch'].tolist()],
y=df['tip'][df['time']=='Lunch'],
boxpoints=False,
pointpos=0,
line=dict(color='gray',
width=1),
fillcolor='white',
showlegend=False),
row=1,
col=2)
#add "Lunch" smokers
fig.add_trace(go.Scatter(x=[df['sex'][(df['time']=='Lunch') & (df['smoker']=='Yes')].tolist(), df['day'][(df['time']=='Lunch') & (df['smoker']=='Yes')].tolist()],
y=df['tip'][(df['time']=='Lunch') & (df['smoker']=='Yes')],
mode='markers',
marker=dict(color='red',
symbol='circle-open',
size=10),
showlegend=False
),
row=1,
col=2)
#add "Lunch" non-smokers
fig.add_trace(go.Scatter(x=[df['sex'][(df['time']=='Lunch') & (df['smoker']=='No')].tolist(), df['day'][(df['time']=='Lunch') & (df['smoker']=='No')].tolist()],
y=df['tip'][(df['time']=='Lunch') & (df['smoker']=='No')],
mode='markers',
marker=dict(color='green',
symbol='cross-thin-open',
size=10),
showlegend=False
),
row=1,
col=2)
df_mean = df[['sex', 'day', 'tip']][df['time']=='Lunch'].groupby(['sex', 'day']).mean().reset_index().dropna()
#add "Lunch" mean line
fig.add_trace(go.Scatter(x=[df_mean['sex'].tolist(), df_mean['day'].tolist()],
y=df_mean['tip'].tolist(),
showlegend=False,
marker=dict(color='black')
),
row=1,
col=2)
fig.update_xaxes(title='Dinner', col=1)
fig.update_xaxes(title='Lunch', col=2)
fig.update_yaxes(title='tip', col=1)
fig.update_layout(legend_title='Smoker')
fig.show()
