pandasdataframedatetimeplotlygantt-chart

How Would One Go About Creating A Graph Like This In Plotly/Pandas?


I would like to recreate following image in Plotly and Pandas. but I'm not sure how.

Time In Bed Graph

I have looked into using Gantt Graphs but every time I see one the timeline is going along the x axis and not the y axis as in this picture. Does anyone have any tips?


Solution

  • I don't think Plotly Gantt charts can easily be rotated, but the most important information is the duration of time for each bar. We can think about your graph as a vertical bar chart where each bar is the duration of time, using the starting times as a base (or offset) for each bar.

    I would recommend entering your times in the 24-hour notation so that durations can be calculated correctly.

    You can use the following as a template for the graph you want to create (changing the color scheme, font size, etc as desired). I have left the y-axis labels so that it's easier to understand my code (we can see hour 23 of the day is actually hour -1 inside the figure itself, and hour 0 of the day remains the same), but you can hide the y-axis labels in your final figure.

    import pandas as pd
    import plotly.graph_objects as go
    from datetime import datetime
    
    df = pd.DataFrame({
        'start': ['2024-06-17 00:04', '2024-06-18 00:24', '2024-06-18 23:54', '2024-06-19 23:59'],
        'end': ['2024-06-17 08:18', '2024-06-18 09:05', '2024-06-19 09:02', '2024-06-20 08:13'],
    })
    
    df['start'] = pd.to_datetime(df['start'])
    df['end'] = pd.to_datetime(df['end'])
    df['duration'] = df['end'] - df['start']
    
    fig = go.Figure()
    
    def get_exact_hour(ts):
        ## we want 23:54 to become -1 on the graph
        if ts.hour > 12:
            hour = ts.hour - 24
        else:
            hour = ts.hour
        minute = ts.minute
        return hour + minute/60
    
    def convert_duration_to_hours(td: pd.Timedelta):
        hours = duration.seconds // 3600
        minutes =  (duration.seconds % 3600) / 60
        return hours + minutes/60
    
    for start, end, duration in df.itertuples(index=False):
        day_rounded = start.round('1d') - pd.Timedelta("1D")
        day_name = day_rounded.day_name()
        day_number = day_rounded.day
        day_string = f"{day_name} </br></br> {day_number}"
        start_hour = get_exact_hour(start)
        end_hour = get_exact_hour(end)
        duration_in_hours = convert_duration_to_hours(duration)
    
        fig.add_trace(
            go.Bar(
                x=[day_string],
                y=[duration_in_hours],
                base=[start_hour],
                marker=dict(color='lightblue'),
            )
        )
    
        ## add start and end times text annotations
        start_time_formatted = start.strftime("%I:%M")
        end_time_formatted = end.strftime("%I:%M")
        padding = 0.2
    
        fig.add_trace(
            go.Scatter(
                x=[day_string],
                y=[start_hour-padding],
                mode='text',
                text=[start_time_formatted],
            )
        )
    
        fig.add_trace(
            go.Scatter(
                x=[day_string],
                y=[end_hour+padding],
                mode='text',
                text=[end_time_formatted],
            )
        )
    
    ## prettify the chart
    fig.update_xaxes(type='category')
    fig.update_layout(
        template='plotly_dark',
        title='TIME IN BED',
        barcornerradius=15,
        showlegend=False
    )
    fig.show()
    

    enter image description here