Difficulties setting annotation position accurately

I am building an animated map using Plotly Express px.choropleth_mapbox. I use it to generate one PNG file per day (via fig.write_image()) and then I stitch them together to have an animation. That part works just fine.

I want to display the date of each day in a changing position that I calculate based on the date. Basically I calculate the timeframe of all dates in seconds and then divide the difference of the current date to the first date by that value.

It looks like this (the date column is of type datetime64[ns]):

# Get min and max dates of the whole dataset
first_date = df['date'].min()
last_date = df['date'].max()

# Get all unique dates and sort them
dates = df['date'].unique()
dates.sort()

# Loop through the dates
for date in dates:

    # Convert to Pandas datetime
    date = pd.to_datetime(date)

    # Create a new dataframe containing just the rows for the current date
    df_plot = df[df['date'] == date]

    # Calculate position of the date
    total_seconds = (last_date - first_date).total_seconds()
    now_seconds = (date - first_date).total_seconds()
    date_position = 1 - (now_seconds / total_seconds)

    # Start plotting
    fig = px.choropleth_mapbox(
        df_plot,
        ...
    )

    # Show current date in the calculated position
    fig.update_layout(
        annotations=[
            dict(
                xref='paper',
                yref='paper',
                x=1,
                y=date_position,
                showarrow=False,
                text='<b>' + str(date.strftime('%d.%m.%Y')) + '</b>',
            ),
        ]
    )

The original code has much more configurations, but I stripped it down to the necessary items.

This basically works: The date is shown on the upper right side for the first day and then slides down with each following day. But now the strange thing happens: On some days (for example, 19 Nov 2020) the date jumps up a little bit (not very much, but enough to note it clearly).

I guessed there was some error in my calculation. But when I plot the calculated values, they form a straight line with no visible error. Also .describe looks just fine: All calculated values have the same difference to the next and the last one, mean, std etc all make sense and indicate a linear decline of the values.

I also tried calculating the position differently, not using the date value but rather the position of the date in the timeframe, like so:

# Set count to zero
num = 0
# Get total number of unique dates
num_total = len(df['date'].unique())

# Loop through the dates
for date in dates:

    ...

    # Calculate position of the date
    date_position = 1 - (num / num_total)
    # Increase the count by 1
    num += 1

    # Start plotting
    fig = px.choropleth_mapbox(
        df_plot,
        ...
    )

But the result is the same. I even had the calculated value displayed next to the date in the plot and while the values declined, the date still jumped up.

Am I missing something regarding the positioning of annotations? I would be thankful for any hint.

I couldn’t figure out how to edit my own post, so here is an addition. This is what the ‘jump’ looks like in the gif (and in the pngs) with the date and the calculated value:
output

Hi @yotka
welcome to the community.

Could you share the data file with us or fake data tied to a minimal reproducible example, so we can replicate this error locally on our computer?

Thanks for your answer @adamschroeder. Here is the minimal reproducible example. Stripping down the code I noticed I could even reproduce the error in an empty figure:

import pandas as pd
import plotly.graph_objects as go
import datetime as dt
import imageio
import pathlib

# Create folder for export
path = 'export/png/' + str(dt.datetime.now().strftime('%Y%m%d-%H%M%S'))
export_path = pathlib.Path(path)
export_path.mkdir(parents=True, exist_ok=True)

# Create a list for the image paths
image_files = []

# Get min and max dates of the whole dataset (usually this comes from the dataframe)
first_date_total = pd.to_datetime('2020.03.01')
last_date_total = pd.to_datetime('2022.04.30')

# Create date ranges to plot
dates = pd.date_range(start='2020.11.01', end='2020.11.30')

# Loop through dates
for date in dates:

    # Calculate position of the date
    total_seconds = (last_date_total - first_date_total).total_seconds()
    now_seconds = (date - first_date_total).total_seconds()
    date_position = 1 - (now_seconds / total_seconds)

    # Create an empty figure
    fig = go.Figure()

    # Add some info to it
    fig.update_layout(
        autosize=False,
        height=800,
        width=1000,
        annotations=[
            dict(
                xref='paper',
                yref='paper',
                x=1,
                y=date_position,
                showarrow=False,
                text='<b>' + str(date.strftime('%d.%m.%Y')) + '(' + str(round(date_position, 5)) + ')</b>',
            ),
        ]
    )

    # Export fig as PNG
    file = str(export_path) + '/' + date.strftime('%Y-%m-%d') + '-' + '.png'
    fig.write_image(file, width=1000, height=800)
    image_files.append(file)

# Create a list for the images to be stitched together
images = []

# Loop through images from above and append them
for file_name in image_files:
    images.append(imageio.v2.imread(file_name))

# Create an animation
gif_path = str(path) + '/animation.gif'
imageio.mimsave(gif_path, images, fps=7)

You should see the error on Nov 19th 2020. I don’t know why, but it always happens there.

This is the result of the code above:
animation

hi @yotka
I took a look at the code and this looks like a bug to me. I would open an issue to report the bug in the Plotly repo.

Thanks @adamschroeder, I did so:

I updated the author’s code as follows:

fig.update_layout(xaxis_autorange=False, yaxis_autorange=False)

and removed autosize=False
In each annotation I set the text anchors:

yanchor="bottom",
xanchor="right",

Also I added scale=1 in fig.write_image:

fig.write_image(filename, width=1000, height=800, scale=1)

and this the result with fps=5, and font_size=14, just to see that it works:
forum-annot5

2 Likes

Thanks @empet It seems that simply adding

yanchor="bottom",
xanchor="right",

is enough. I don’t really understand why that is necessary, but it solves the problem.

The anchors ensure that the text position with respect to its boundaries is constant in each frame. In your initial animation it was the yanchor that changed at a moment of time.
I added also autorange as False and scale=1 because they can help to plot traces and save the corresponding png, by respecting their dimensional settings.

1 Like