How to offset the y axis plots on a timeline

Is there a way to update a trace of a timeline figure to align the plotted bar to the top or bottom of its row?

I have an issue where it is plotting multiple bars over the top of each other and the ‘hovertemplate’ only seems to show me the longest (greatest time) plot and if there is another plot within the bigger plot then the mouse hover will not pick it up (no matter how I sort or what sequence I add the traces).

I have tried opacity and width but neither seem to get an easy to read effect.

It would be great to just set an offset on the y axis so I can spread them apart.

fig_1 = px.timeline(df1, 
                    x_start = 'DateDue', 
                    x_end = 'Start Date', 
                    y = 'System', 
                    color = 'ActivityCode',
                    color_discrete_map = color_dict, 
                    text = df1['ActivityCode'] + " " + df1['Percent'].astype(str) + '%',
                    custom_data=['ActivityCode', 'DateDue', 'Start_Date', 'Percent' ],
                    category_orders = {'System': sorted(df1['System'].unique())})


fig1.update_traces(
        hovertemplate=
        "<b>***%{y} %{customdata[0]} REQUIRES Extension***</b><br>"
        "Due: %{customdata[1]|%d-%b-%y}<br>"
        "Scheduled: %{customdata[2]|%d-%b-%y}<br>"
        "Percentage Req: %{customdata[3]}<br>"
)

fig1.update_traces(width= 0.6)

# Update text position to left
fig1.update_traces(textposition='inside')
fig1.update_traces(insidetextanchor="start")

#Adjust position on y axis
fig1.update_yaxes(anchor = "free")
fig1.update_layout(margin=dict(l=0, r=0, t=100, b=0)) #<<<Does not seem to have any effect
fig1.update_yaxes(position = 1) #<<<Does not seem to have any effect
fig1.update_yaxes(side= "top") #<<<Does not seem to have any effect


# Combine the two Gantt charts
for trace in fig1.data:
    fig.add_trace(trace)
	
# there are 2 other figs like this one. I want fig2 plotted in the middle and fig3 on the bottom

Please and Thank you :slight_smile:

Hi @RobboTheGreg ,

Welcome to the community!

If you don’t mind, can you provide us data samples (and your current code) which can be used to reproduce your issues?

It will really big help.

Thanks.

data = {
    'System': ['System 1', 'System 2', 'System 3', 'System 4', 'System 5', 'System 6', 'System 7', 'System 8', 'System 9'],
    'Start Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00'],
    'End Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'is_next_serv': ['yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes'],
    'ActivityCode': ['A', 'A', 'A', 'A', 'A', 'A', 'B', 'C', 'A'],
    'DateDue': ['2024-12-03 00:00:00+00:00', '2024-08-02 00:00:00+00:00', '2026-03-28 00:00:00+00:00', '2026-11-29 00:00:00+00:00', '2028-12-13 00:00:00+00:00', '2027-12-22 00:00:00+00:00', '2024-06-17 00:00:00+00:00', '2024-06-16 00:00:00+00:00', '2026-02-12 00:00:00+00:00'],
    'Interval': [304, 304, 304, 304, 304, 304, 152, 152, 304],
    'Interval_days': [2128, 2128, 2128, 2128, 2128, 2128, 1064, 1064, 2128],
    'Scheduled_Due_Diff': [335, 276, 260, 225, 220, 214, 156, 149, 109],
    'Percent': [15.7, 13, 12.2, 10.6, 10.3, 10.1, 14.7, 14, 5.1],
    'End_Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'Start_Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00']
}

Please let me know if it needs editing.

This is part of a bigger picture so hopefully its still useful. This data would be what I want plotted aligned to the top. Another data set with the same structure would be aligned to the middle and a 3rd data set aligned to the bottom. (or another approach would be to offset x amount from the middle).

I hope all that makes sense!

Thank you!

1 Like

Hi @RobboTheGreg ,

As far as I understand,

You want to spread 3 bar (making as a group) instead of making them overlay.
I have tried use offset, bar width and bargroupgap and below is the result.

import plotly.express as px
import pandas as pd

data = {
    'System': ['System 1', 'System 2', 'System 3', 'System 4', 'System 5', 'System 6', 'System 7', 'System 8', 'System 9'],
    'Start Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00'],
    'End Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'is_next_serv': ['yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes'],
    'ActivityCode': ['A', 'A', 'A', 'A', 'A', 'A', 'B', 'C', 'A'],
    'DateDue': ['2024-12-03 00:00:00+00:00', '2024-08-02 00:00:00+00:00', '2026-03-28 00:00:00+00:00', '2026-11-29 00:00:00+00:00', '2028-12-13 00:00:00+00:00', '2027-12-22 00:00:00+00:00', '2024-06-17 00:00:00+00:00', '2024-06-16 00:00:00+00:00', '2026-02-12 00:00:00+00:00'],
    'Interval': [304, 304, 304, 304, 304, 304, 152, 152, 304],
    'Interval_days': [2128, 2128, 2128, 2128, 2128, 2128, 1064, 1064, 2128],
    'Scheduled_Due_Diff': [335, 276, 260, 225, 220, 214, 156, 149, 109],
    'Percent': [15.7, 13, 12.2, 10.6, 10.3, 10.1, 14.7, 14, 5.1],
    'End_Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'Start_Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00']
}

data2 = {
    'System': ['System 1', 'System 2', 'System 3', 'System 4', 'System 5', 'System 6', 'System 7', 'System 8', 'System 9'],
    'Start Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00'],
    'End Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'is_next_serv': ['yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes'],
    'ActivityCode': ['A', 'A', 'A', 'A', 'A', 'A', 'B', 'C', 'A'],
    'DateDue': ['2024-12-25 00:00:00+00:00', '2024-08-26 00:00:00+00:00', '2026-04-22 00:00:00+00:00', '2026-12-22 00:00:00+00:00', '2029-01-12 00:00:00+00:00', '2028-01-20 00:00:00+00:00', '2024-07-10 00:00:00+00:00', '2024-07-04 00:00:00+00:00', '2026-03-15 00:00:00+00:00'],
    'Interval': [304, 304, 304, 304, 304, 304, 152, 152, 304],
    'Interval_days': [2128, 2128, 2128, 2128, 2128, 2128, 1064, 1064, 2128],
    'Scheduled_Due_Diff': [335, 276, 260, 225, 220, 214, 156, 149, 109],
    'Percent': [12.7, 12, 11.2, 7.6, 8.3, 10.1, 10.7, 10, 3.1],
    'End_Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'Start_Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00']
}

data3 = {
    'System': ['System 1', 'System 2', 'System 3', 'System 4', 'System 5', 'System 6', 'System 7', 'System 8', 'System 9'],
    'Start Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00'],
    'End Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'is_next_serv': ['yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes'],
    'ActivityCode': ['A', 'A', 'A', 'A', 'A', 'A', 'B', 'C', 'A'],
    'DateDue': ['2025-5-25 00:00:00+00:00', '2025-03-26 00:00:00+00:00', '2026-10-22 00:00:00+00:00', '2027-05-22 00:00:00+00:00', '2029-05-12 00:00:00+00:00', '2028-04-20 00:00:00+00:00', '2024-10-10 00:00:00+00:00', '2024-10-04 00:00:00+00:00', '2026-05-15 00:00:00+00:00'],
    'Interval': [304, 304, 304, 304, 304, 304, 152, 152, 304],
    'Interval_days': [2128, 2128, 2128, 2128, 2128, 2128, 1064, 1064, 2128],
    'Scheduled_Due_Diff': [335, 276, 260, 225, 220, 214, 156, 149, 109],
    'Percent': [2.7, 6, 4.2, 5.6, 6.3, 5.1, 3.7, 5, 1.1],
    'End_Date': ['2026-06-01 00:00:00+00:00', '2025-12-01 00:00:00+00:00', '2027-07-11 00:00:00+00:00', '2028-02-07 00:00:00+00:00', '2030-02-17 00:00:00+00:00', '2029-03-05 00:00:00+00:00', '2025-01-29 00:00:00+00:00', '2025-01-21 00:00:00+00:00', '2026-12-28 00:00:00+00:00'],
    'Start_Date': ['2025-11-03 00:00:00+00:00', '2025-05-05 00:00:00+00:00', '2026-12-13 00:00:00+00:00', '2027-07-12 00:00:00+00:00', '2029-07-21 00:00:00+00:00', '2028-07-23 00:00:00+00:00', '2024-11-20 00:00:00+00:00', '2024-11-12 00:00:00+00:00', '2026-06-01 00:00:00+00:00']
}

df1 = pd.DataFrame(data)
df2 = pd.DataFrame(data2)
df3 = pd.DataFrame(data3)

# bar color 
color_dict_list = [{'A': 'rgba(255, 99, 71,0.5)','B':'rgba(131, 255, 71,0.5)','C':'rgba(131, 152, 255, 0.5)'},{'A': 'rgba(255, 99, 71, 0.8)','B':'rgba(131, 255, 71, 0.8)','C':'rgba(131, 152, 255, 0.8)'},{'A': 'rgba(255, 99, 71,1.0)','B':'rgba(131, 255, 71,1.0)','C':'rgba(131, 152, 255, 1.0)'}]


for idx,df in  enumerate([df3,df2,df1]):
	if idx == 0:
		fig = px.timeline(df, 
		                    x_start = 'DateDue', 
		                    x_end = 'Start Date', 
		                    y = 'System', 
		                    color = 'ActivityCode',
		                    color_discrete_map = color_dict_list[0], 
		                    text = df['ActivityCode'] + " " + df['Percent'].astype(str) + '%',
		                    custom_data=['ActivityCode', 'DateDue', 'Start_Date', 'Percent' ],
		                    category_orders = {'System': sorted(df['System'].unique())})


		fig.update_traces(
		        hovertemplate=
		        "<b>***%{y} %{customdata[0]} REQUIRES Extension***</b><br>"
		        "Due: %{customdata[1]|%d-%b-%y}<br>"
		        "Scheduled: %{customdata[2]|%d-%b-%y}<br>"
		        "Percentage Req: %{customdata[3]}<br>"
		)

		fig.update_traces(width= 0.3,offset=0.3 + (0.3 * (idx-1)))

	else:

		fig1 = px.timeline(df, 
		                    x_start = 'DateDue', 
		                    x_end = 'Start Date', 
		                    y = 'System', 
		                    color = 'ActivityCode',
		                    color_discrete_map = color_dict_list[idx], 
		                    text = df['ActivityCode'] + " " + df['Percent'].astype(str) + '%',
		                    custom_data=['ActivityCode', 'DateDue', 'Start_Date', 'Percent' ],
		                    category_orders = {'System': sorted(df['System'].unique())})


		fig1.update_traces(
		        hovertemplate=
		        "<b>***%{y} %{customdata[0]} REQUIRES Extension***</b><br>"
		        "Due: %{customdata[1]|%d-%b-%y}<br>"
		        "Scheduled: %{customdata[2]|%d-%b-%y}<br>"
		        "Percentage Req: %{customdata[3]}<br>"
		)
		fig1.update_traces(width= 0.3,offset=0.3 + (0.3 * (idx-1)))
		
		# Combine the two Gantt charts
		for trace in fig1.data:
			fig.add_trace(trace)

# Update text position to left
fig.update_traces(textposition='inside')
fig.update_traces(insidetextanchor="start")

#Adjust position on y axis
fig.update_yaxes(anchor = "free")
fig.update_layout(margin=dict(l=0, r=0, t=100, b=0),bargroupgap=1) #<<<Does not seem to have any effect
fig.update_yaxes(position = 1) #<<<Does not seem to have any effect
fig.update_yaxes(side= "right") #<<<Does not seem to have any effect
fig.update_layout(legend=dict(
    yanchor="top",
    y=0.99,
    xanchor="left",
    x=0.92
))
fig.show()

This another (2nd and 3rd) data sets, I generate based on your first dataset.

Is this what you are looking for ?

I looks about right. I will integrate with what I have already and see how it turns out. I will let you know once I have given it a go. Thank you very much for your time!!

OK. so all I needed was this line of code:

fig1.update_traces(width= 0.3,offset=0.3 + (0.3 * (idx-1)))

But to integrate into what all I have already I just had to edit this:

fig1.update_traces(width= .25)

To this

fig1.update_traces(width= .25, offset = 0.2)

Thank you so much! I should have mentioned I am a bit of a novice. I just wasn’t aware of the offset arg, This is exactly what I needed.

Thank you!!

1 Like

Hi @RobboTheGreg ,

Yes, you can play around width and offset properties and just need adjustment based on your current code.

Glad it works for you.
Cheers.

1 Like