MorrisseyJ
MorrisseyJ

Reputation: 1271

PlotnineAnimation with changing scales throws error: 'The fill scale of plot for frame 1 has different limits from those of the first frame.'

I am trying to use the gganimate (R) equivalent, plotnine (python). I run into problems when one of the plots introduces a new aesthetic scale category, causing the console to throw the error: The fill scale of plot for frame 1 has different limits from those of the first frame.

This appears to be the result of having to produce a generator of all the plots and then stick them together - as per the plotnine documentation (https://plotnine.readthedocs.io/en/stable/generated/plotnine.animation.PlotnineAnimation.html). When I use gganimate() the library is smart enough to know to check all the scales that will be called and print the complete scale accordingly.

I tried using scale_fill_manual() to force a scale onto the first plot as a way to deal with this, but it doesn't work (in R or python).

I am looking for a way around this in plotnine (if there is a way better animation module in python i can learn that if there are suggestions).

Here is a worked example:

First trying in plotnine:

#import modules
import pandas as pd
from plotnine import *
from plotnine.animation import PlotnineAnimation
#create the dataframe
df = pd.DataFrame({'run': [1, 1, 1, 2, 2, 2], 
             'x_pos': [1, 2, 3, 2, 3, 4], 
             'y_pos': [4, 5, 6, 5, 6, 7], 
             'status': ['A', 'B', 'A', 'A', 'B', 'C'] })

#write a function that creates all the plots
def plot(x):
    df2 = df[df['run'] == x]

    p = (ggplot(df2,
               aes(x = 'x_pos', y = 'y_pos', fill = 'status'))
         + geom_point()
    )
    return(p)

#save the plots as a generator as per the plotnine docs (https://plotnine.readthedocs.io/en/stable/generated/plotnine.animation.PlotnineAnimation.html)
plots = (plot(i) for i in range(1, 3))

#create the animation
animation = PlotnineAnimation(plots, interval=100, repeat_delay=500)

Throws the error: PlotnineError: 'The fill scale of plot for frame 1 has different limits from those of the first frame.'

If I do this in R, using gganimate(), everything works fine - I get all three color scales from the start:

library('ggplot2')
library('gganimate')

df <- data.frame(run = c(1, 1, 1, 2, 2, 2), 
                 x_pos = c(1, 2, 3, 2, 3, 4), 
                 y_pos = c(4, 5, 6, 5, 6, 7), 
                 status = c('A', 'B', 'A', 'A', 'B', 'C'))

ggplot(df, aes(x = x_pos, y = y_pos, col = status)) + 
  geom_point() + 
  transition_states(run)

enter image description here

When i try forcing the scale on the first plot it doesn't work. First in R:

library(dplyr)

df %>%
  filter(run == 1) %>%
  ggplot(aes(x = x_pos, y = y_pos, col = status)) + 
  geom_point() + 
  scale_color_manual(labels = c('A', 'B', 'C'), 
                     values = c('red', 'green', 'blue'))

The plot shows with just the two color scales - despite explicitly stating 3 in scale_color_manual():

enter image description here

Then in Python:

#Filter the data frame created above
df2 = df[df['run'] == 1]

#Create the plot - stating scale_fill_manual()
p = (ggplot(df2,
        aes(x = 'x_pos', y = 'y_pos', fill = 'status'))
 + geom_point()
 + scale_fill_manual(labels = ['A', 'B', 'C'], 
                    values = ['red', 'blue', 'green'])
)

#Print the plot
print(p)

Throws errors about the arrays being the wrong length (ValueError: arrays must all be same length), which makes sense: I am asking for more values and colors than are in the filtered dataset. That means that I cannot have the first frame match the second frame, which is the error i am trying to solve.

Anyone know how to make this work with plotnine, the way it does in gganimate()? Further (though less important), any ideas as to why plotnine takes fill for geom_point(), while ggplot2 takes col for geom_point()?

Upvotes: 0

Views: 311

Answers (1)

has2k1
has2k1

Reputation: 2375

In python you should make status a categorical, it is also better to be specific and set the limits of the x and y aesthetics, if you leave it up to the plotting system it may get them wrong.

In Plotnine all the key plotting shapes for geom_point have both the inner area and the stroke around it so you can use fill and color to target either. But this is an intricate detail that would most likely frustrate users, so when you map to the color and not the fill it sets the same colour for both. In between, Plotnine does not recognise the abbreviation col.

import pandas as pd
from plotnine import *
from plotnine.animation import PlotnineAnimation

#create the dataframe
df = pd.DataFrame({'run': [1, 1, 1, 2, 2, 2], 
             'x_pos': [1, 2, 3, 2, 3, 4], 
             'y_pos': [4, 5, 6, 5, 6, 7], 
             'status': ['A', 'B', 'A', 'A', 'B', 'C'] })

# A categorical ensures that each of the sub-dataframes
# can be used to create a scale with the correct limits
df['status'] = pd.Categorical(df['status'])

#write a function that creates all the plots
def plot(x):
    df2 = df[df['run'] == x]

    p = (ggplot(df2,
               aes(x = 'x_pos', y = 'y_pos', color = 'status'))
         + geom_point()
         # Specify the limits for the x and y aesthetics
         + scale_x_continuous(limits=(df.x_pos.min(), df.x_pos.max()))
         + scale_y_continuous(limits=(df.y_pos.min(), df.y_pos.max()))
         + theme(subplots_adjust={'right': 0.85}) # Make space for the legend
        )
    return(p)


plots = (plot(i) for i in range(1, 3))

#create the animation
animation = PlotnineAnimation(plots, interval=1000, repeat_delay=500)
animation

Upvotes: 1

Related Questions