Coder Social home page Coder Social logo

jackmckew / pandas_alive Goto Github PK

View Code? Open in Web Editor NEW
581.0 13.0 99.0 400.54 MB

Create stunning, animated visualisations with Pandas & Matplotlib as easy as calling `df.plot_animated()`

License: MIT License

Python 99.90% Smarty 0.10%
pandas-alive hacktoberfest plotting-in-python animated-visualisations pandas

pandas_alive's Introduction

Pandas_Alive

Animated plotting extension for Pandas with Matplotlib

Inline docs Interrogate Coverage Downloads PyPI version shields.io PyPI license saythanks

Pandas_Alive is intended to provide a plotting backend for animated matplotlib charts for Pandas DataFrames, similar to the already existing Visualization feature of Pandas.

With Pandas_Alive, creating stunning, animated visualisations is as easy as calling:

df.plot_animated()

Example Bar Chart

Table of Contents

Installation

Install with pip install pandas_alive or conda install pandas_alive -c conda-forge

Usage

As this package was inspired by bar_chart_race, the example data set is sourced from there.

Must begin with a pandas DataFrame containing 'wide' data where:

  • Every row represents a single period of time
  • Each column holds the value for a particular category
  • The index contains the time component (optional)

The data below is an example of properly formatted data. It shows total deaths from COVID-19 for the highest 20 countries by date.

Example Data Table

To produce the above visualisation:

  • Check Requirements first to ensure you have the tooling installed!
  • Call plot_animated() on the DataFrame
    • Either specify a file name to write to with df.plot_animated(filename='example.mp4') or use df.plot_animated().get_html5_video to return a HTML5 video
  • Done!

Note on custom figures in notebooks: When setting up custom figures for animations in Matplotlib make sure to use the Figure() syntax and not figure() instance type. The latter causes animations in Matplotlib, and in turn in pandas_alive, to take twice as long to be generated when changing from 'Figure' to 'figure' syntax.

More on 'Figure' vs 'figure' can be found in this SO entry, and this other SO entry.

import pandas_alive

covid_df = pandas_alive.load_dataset()

covid_df.plot_animated(filename='examples/example-barh-chart.gif')

Currently Supported Chart Types

Horizontal Bar Chart Races

import pandas as pd
import pandas_alive

elec_df = pd.read_csv("data/Aus_Elec_Gen_1980_2018.csv",index_col=0,parse_dates=[0],thousands=',')

elec_df.fillna(0).plot_animated('examples/example-electricity-generated-australia.gif',period_fmt="%Y",title='Australian Electricity Generation Sources 1980-2018')

Electricity Example Line Chart

import pandas_alive

covid_df = pandas_alive.load_dataset()

def current_total(values):
    total = values.sum()
    s = f'Total : {int(total)}'
    return {'x': .85, 'y': .2, 's': s, 'ha': 'right', 'size': 11}

covid_df.plot_animated(filename='examples/summary-func-example.gif',period_summary_func=current_total)

Summary Func Example

import pandas as pd
import pandas_alive

elec_df = pd.read_csv("data/Aus_Elec_Gen_1980_2018.csv",index_col=0,parse_dates=[0],thousands=',')

elec_df.fillna(0).plot_animated('examples/fixed-example.gif',period_fmt="%Y",title='Australian Electricity Generation Sources 1980-2018',fixed_max=True,fixed_order=True)

Fixed Example

import pandas_alive

covid_df = pandas_alive.load_dataset()

covid_df.plot_animated(filename='examples/perpendicular-example.gif',perpendicular_bar_func='mean')

Perpendicular Example

Vertical Bar Chart Races

import pandas_alive

covid_df = pandas_alive.load_dataset()

covid_df.plot_animated(filename='examples/example-barv-chart.gif',orientation='v')

Example Barv Chart

Line Charts

With as many lines as data columns in the DataFrame.

import pandas_alive

covid_df = pandas_alive.load_dataset()

covid_df.diff().fillna(0).plot_animated(filename='examples/example-line-chart.gif',kind='line',period_label={'x':0.25,'y':0.9})

Example Line Chart

Bar Charts

Similar to line charts with time as the x-axis.

import pandas_alive

covid_df = pandas_alive.load_dataset()

covid_df.sum(axis=1).fillna(0).plot_animated(filename='examples/example-bar-chart.gif',kind='bar',
        period_label={'x':0.1,'y':0.9},
        enable_progress_bar=True, steps_per_period=2, interpolate_period=True, period_length=200
)

Example Bar Chart

Scatter Charts

import pandas as pd
import pandas_alive

max_temp_df = pd.read_csv(
    "data/Newcastle_Australia_Max_Temps.csv",
    parse_dates={"Timestamp": ["Year", "Month", "Day"]},
)
min_temp_df = pd.read_csv(
    "data/Newcastle_Australia_Min_Temps.csv",
    parse_dates={"Timestamp": ["Year", "Month", "Day"]},
)

merged_temp_df = pd.merge_asof(max_temp_df, min_temp_df, on="Timestamp")

merged_temp_df.index = pd.to_datetime(merged_temp_df["Timestamp"].dt.strftime('%Y/%m/%d'))

keep_columns = ["Minimum temperature (Degree C)", "Maximum temperature (Degree C)"]

merged_temp_df[keep_columns].resample("Y").mean().plot_animated(filename='examples/example-scatter-chart.gif',kind="scatter",title='Max & Min Temperature Newcastle, Australia')

Example Scatter Chart

Pie Charts

import pandas_alive

covid_df = pandas_alive.load_dataset()

covid_df.plot_animated(filename='examples/example-pie-chart.gif',kind="pie",rotatelabels=True,period_label={'x':0,'y':0})

Example Pie Chart

Bubble Charts

Bubble charts are generated from a multi-indexed dataframes. Where the index is the time period (optional) and the axes are defined with x_data_label & y_data_label which should be passed a string in the level 0 column labels.

See an example multi-indexed dataframe at: https://github.com/JackMcKew/pandas_alive/tree/master/data/multi.csv

When you set color_data_label= to a df column name, pandas_alive will automatically add a colorbar.

import pandas_alive

multi_index_df = pd.read_csv("data/multi.csv", header=[0, 1], index_col=0)

multi_index_df.index = pd.to_datetime(multi_index_df.index,dayfirst=True)

map_chart = multi_index_df.plot_animated(
    kind="bubble",
    filename="examples/example-bubble-chart.gif",
    x_data_label="Longitude",
    y_data_label="Latitude",
    size_data_label="Cases",
    color_data_label="Cases",
    vmax=5, steps_per_period=3, interpolate_period=True, period_length=500,
    dpi=100
)

Bubble Chart Example 1

Bubble Chart Example

Bubble Chart Example 2

Jupyter notebook: pendulum_sample.ipynb

Bubble Chart Example

GeoSpatial Charts

GeoSpatial charts can now be animated easily using geopandas!

If using Windows, anaconda is the easiest way to install with all GDAL dependancies.

Must begin with a geopandas GeoDataFrame containing 'wide' data where:

  • Every row represents a single geometry (Point or Polygon).
    • The index contains the geometry label (optional)
  • Each column represents a single period in time.

These can be easily composed by transposing data compatible with the rest of the charts using df = df.T.

GeoSpatial Point Charts

import geopandas
import pandas_alive
import contextily

gdf = geopandas.read_file('data/nsw-covid19-cases-by-postcode.gpkg')
gdf.index = gdf.postcode
gdf = gdf.drop('postcode',axis=1)

map_chart = gdf.plot_animated(filename='examples/example-geo-point-chart.gif',basemap_format={'source':contextily.providers.Stamen.Terrain})

Example Point GeoSpatialChart

Polygon GeoSpatial Charts

Supports GeoDataFrames containing Polygons!

import geopandas
import pandas_alive
import contextily

gdf = geopandas.read_file('data/italy-covid-region.gpkg')
gdf.index = gdf.region
gdf = gdf.drop('region',axis=1)

map_chart = gdf.plot_animated(filename='examples/example-geo-polygon-chart.gif',basemap_format={'source':contextily.providers.Stamen.Terrain})

Example Polygon GeoSpatialChart

Multiple Charts

pandas_alive supports multiple animated charts in a single visualisation.

  • Create a list of all charts to include in animation
  • Use animate_multiple_plots with a filename and the list of charts (this will use matplotlib.subplots)
  • Done!
import pandas_alive

covid_df = pandas_alive.load_dataset()

animated_line_chart = covid_df.diff().fillna(0).plot_animated(kind='line',period_label=False,add_legend=False)

animated_bar_chart = covid_df.plot_animated(n_visible=10)

pandas_alive.animate_multiple_plots('examples/example-bar-and-line-chart.gif',[animated_bar_chart,animated_line_chart],
    enable_progress_bar=True)

Example Bar & Line Chart

Urban Population

import pandas_alive

urban_df = pandas_alive.load_dataset("urban_pop")

animated_line_chart = (
    urban_df.sum(axis=1)
    .pct_change()
    .fillna(method='bfill')
    .mul(100)
    .plot_animated(kind="line", title="Total % Change in Population",period_label=False,add_legend=False)
)

animated_bar_chart = urban_df.plot_animated(n_visible=10,title='Top 10 Populous Countries',period_fmt="%Y")

pandas_alive.animate_multiple_plots('examples/example-bar-and-line-urban-chart.gif',[animated_bar_chart,animated_line_chart],
    title='Urban Population 1977 - 2018', adjust_subplot_top=0.85, enable_progress_bar=True)

Urban Population Bar & Line Chart

Life Expectancy in G7 Countries

import pandas_alive
import pandas as pd

data_raw = pd.read_csv(
    "https://raw.githubusercontent.com/owid/owid-datasets/master/datasets/Long%20run%20life%20expectancy%20-%20Gapminder%2C%20UN/Long%20run%20life%20expectancy%20-%20Gapminder%2C%20UN.csv"
)

list_G7 = [
    "Canada",
    "France",
    "Germany",
    "Italy",
    "Japan",
    "United Kingdom",
    "United States",
]

data_raw = data_raw.pivot(
    index="Year", columns="Entity", values="Life expectancy (Gapminder, UN)"
)

data = pd.DataFrame()
data["Year"] = data_raw.reset_index()["Year"]
for country in list_G7:
    data[country] = data_raw[country].values

data = data.fillna(method="pad")
data = data.fillna(0)
data = data.set_index("Year").loc[1900:].reset_index()

data["Year"] = pd.to_datetime(data.reset_index()["Year"].astype(str))

data = data.set_index("Year")

animated_bar_chart = data.plot_animated(
    period_fmt="%Y",perpendicular_bar_func="mean", period_length=200,fixed_max=True
)

animated_line_chart = data.plot_animated(
    kind="line", period_fmt="%Y", period_length=200,fixed_max=True
)

pandas_alive.animate_multiple_plots(
    "examples/life-expectancy.gif",
    plots=[animated_bar_chart, animated_line_chart],
    title="Life expectancy in G7 countries up to 2015",
    adjust_subplot_left=0.2, adjust_subplot_top=0.9, enable_progress_bar=True
)

Life Expectancy Chart

NSW COVID Visualisation

import geopandas
import pandas as pd
import pandas_alive
import contextily
import matplotlib.pyplot as plt

import urllib.request, json

with urllib.request.urlopen(
    "https://data.nsw.gov.au/data/api/3/action/package_show?id=aefcde60-3b0c-4bc0-9af1-6fe652944ec2"
) as url:
    data = json.loads(url.read().decode())

# Extract url to csv component
covid_nsw_data_url = data["result"]["resources"][0]["url"]

# Read csv from data API url
nsw_covid = pd.read_csv(covid_nsw_data_url)
postcode_dataset = pd.read_csv("data/postcode-data.csv")

# Prepare data from NSW health dataset


nsw_covid = nsw_covid.fillna(9999)
nsw_covid["postcode"] = nsw_covid["postcode"].astype(int)

grouped_df = nsw_covid.groupby(["notification_date", "postcode"]).size()
grouped_df = pd.DataFrame(grouped_df).unstack()
grouped_df.columns = grouped_df.columns.droplevel().astype(str)

grouped_df = grouped_df.fillna(0)
grouped_df.index = pd.to_datetime(grouped_df.index)

cases_df = grouped_df

# Clean data in postcode dataset prior to matching

grouped_df = grouped_df.T
postcode_dataset = postcode_dataset[postcode_dataset['Longitude'].notna()]
postcode_dataset = postcode_dataset[postcode_dataset['Longitude'] != 0]
postcode_dataset = postcode_dataset[postcode_dataset['Latitude'].notna()]
postcode_dataset = postcode_dataset[postcode_dataset['Latitude'] != 0]
postcode_dataset['Postcode'] = postcode_dataset['Postcode'].astype(str)

# Build GeoDataFrame from Lat Long dataset and make map chart
grouped_df['Longitude'] = grouped_df.index.map(postcode_dataset.set_index('Postcode')['Longitude'].to_dict())
grouped_df['Latitude'] = grouped_df.index.map(postcode_dataset.set_index('Postcode')['Latitude'].to_dict())
gdf = geopandas.GeoDataFrame(
    grouped_df, geometry=geopandas.points_from_xy(grouped_df.Longitude, grouped_df.Latitude),crs="EPSG:4326")
gdf = gdf.dropna()

# Prepare GeoDataFrame for writing to geopackage
gdf = gdf.drop(['Longitude','Latitude'],axis=1)
gdf.columns = gdf.columns.astype(str)
gdf['postcode'] = gdf.index
gdf.to_file("data/nsw-covid19-cases-by-postcode.gpkg", layer='nsw-postcode-covid', driver="GPKG")

# Prepare GeoDataFrame for plotting
gdf.index = gdf.postcode
gdf = gdf.drop('postcode',axis=1)
gdf = gdf.to_crs("EPSG:3857") #Web Mercator

map_chart = gdf.plot_animated(basemap_format={'source':contextily.providers.Stamen.Terrain},cmap='cool')

cases_df.to_csv('data/nsw-covid-cases-by-postcode.csv')

from datetime import datetime

bar_chart = cases_df.sum(axis=1).plot_animated(
    kind='line',
    label_events={
        'Ruby Princess Disembark':datetime.strptime("19/03/2020", "%d/%m/%Y"),
        'Lockdown':datetime.strptime("31/03/2020", "%d/%m/%Y")
    },
    fill_under_line_color="blue",
    add_legend=False
)

map_chart.ax.set_title('Cases by Location')

grouped_df = pd.read_csv('data/nsw-covid-cases-by-postcode.csv', index_col=0, parse_dates=[0])

line_chart = (
    grouped_df.sum(axis=1)
    .cumsum()
    .fillna(0)
    .plot_animated(kind="line", period_label=False, title="Cumulative Total Cases", add_legend=False)
)


def current_total(values):
    total = values.sum()
    s = f'Total : {int(total)}'
    return {'x': .85, 'y': .2, 's': s, 'ha': 'right', 'size': 11}

race_chart = grouped_df.cumsum().plot_animated(
    n_visible=5, title="Cases by Postcode", period_label=False,period_summary_func=current_total
)

import time

timestr = time.strftime("%d/%m/%Y")

plots = [bar_chart, line_chart, map_chart, race_chart]

from matplotlib import rcParams

rcParams.update({"figure.autolayout": False})
# make sure figures are `Figure()` instances
figs = plt.Figure()
gs = figs.add_gridspec(2, 3, hspace=0.5)
f3_ax1 = figs.add_subplot(gs[0, :])
f3_ax1.set_title(bar_chart.title)
bar_chart.ax = f3_ax1

f3_ax2 = figs.add_subplot(gs[1, 0])
f3_ax2.set_title(line_chart.title)
line_chart.ax = f3_ax2

f3_ax3 = figs.add_subplot(gs[1, 1])
f3_ax3.set_title(map_chart.title)
map_chart.ax = f3_ax3

f3_ax4 = figs.add_subplot(gs[1, 2])
f3_ax4.set_title(race_chart.title)
race_chart.ax = f3_ax4

timestr = cases_df.index.max().strftime("%d/%m/%Y")
figs.suptitle(f"NSW COVID-19 Confirmed Cases up to {timestr}")

pandas_alive.animate_multiple_plots(
    'examples/nsw-covid.gif',
    plots,
    figs,
    enable_progress_bar=True
)

NSW COVID

Italy COVID Visualisation

import geopandas
import pandas as pd
import pandas_alive
import contextily
import matplotlib.pyplot as plt


region_gdf = geopandas.read_file('data\geo-data\italy-with-regions')
region_gdf.NOME_REG = region_gdf.NOME_REG.str.lower().str.title()
region_gdf = region_gdf.replace('Trentino-Alto Adige/Sudtirol','Trentino-Alto Adige')
region_gdf = region_gdf.replace("Valle D'Aosta/Vallée D'Aoste\r\nValle D'Aosta/Vallée D'Aoste","Valle d'Aosta")

italy_df = pd.read_csv('data\Regional Data - Sheet1.csv',index_col=0,header=1,parse_dates=[0])

italy_df = italy_df[italy_df['Region'] != 'NA']

cases_df = italy_df.iloc[:,:3]
cases_df['Date'] = cases_df.index
pivoted = cases_df.pivot(values='New positives',index='Date',columns='Region')
pivoted.columns = pivoted.columns.astype(str)
pivoted = pivoted.rename(columns={'nan':'Unknown Region'})

cases_gdf = pivoted.T
cases_gdf['geometry'] = cases_gdf.index.map(region_gdf.set_index('NOME_REG')['geometry'].to_dict())

cases_gdf = cases_gdf[cases_gdf['geometry'].notna()]

cases_gdf = geopandas.GeoDataFrame(cases_gdf, crs=region_gdf.crs, geometry=cases_gdf.geometry)

gdf = cases_gdf

map_chart = gdf.plot_animated(basemap_format={'source':contextily.providers.Stamen.Terrain},cmap='viridis')

cases_df = pivoted

from datetime import datetime

bar_chart = cases_df.sum(axis=1).plot_animated(
    kind='line',
    label_events={
        'Schools Close':datetime.strptime("4/03/2020", "%d/%m/%Y"),
        'Phase I Lockdown':datetime.strptime("11/03/2020", "%d/%m/%Y"),
        '1M Global Cases':datetime.strptime("02/04/2020", "%d/%m/%Y"),
        '100k Global Deaths':datetime.strptime("10/04/2020", "%d/%m/%Y"),
        'Manufacturing Reopens':datetime.strptime("26/04/2020", "%d/%m/%Y"),
        'Phase II Lockdown':datetime.strptime("4/05/2020", "%d/%m/%Y"),

    },
    fill_under_line_color="blue",
    add_legend=False
)

map_chart.ax.set_title('Cases by Location')

line_chart = (
    cases_df.sum(axis=1)
    .cumsum()
    .fillna(0)
    .plot_animated(kind="line", period_label=False, title="Cumulative Total Cases",add_legend=False)
)


def current_total(values):
    total = values.sum()
    s = f'Total : {int(total)}'
    return {'x': .85, 'y': .1, 's': s, 'ha': 'right', 'size': 11}

race_chart = cases_df.cumsum().plot_animated(
    n_visible=5, title="Cases by Region", period_label=False,period_summary_func=current_total
)

import time

timestr = time.strftime("%d/%m/%Y")

plots = [bar_chart, race_chart, map_chart, line_chart]

# Otherwise titles overlap and adjust_subplot does nothing
from matplotlib import rcParams
from matplotlib.animation import FuncAnimation

rcParams.update({"figure.autolayout": False})
# make sure figures are `Figure()` instances
figs = plt.Figure()
gs = figs.add_gridspec(2, 3, hspace=0.5)
f3_ax1 = figs.add_subplot(gs[0, :])
f3_ax1.set_title(bar_chart.title)
bar_chart.ax = f3_ax1

f3_ax2 = figs.add_subplot(gs[1, 0])
f3_ax2.set_title(race_chart.title)
race_chart.ax = f3_ax2

f3_ax3 = figs.add_subplot(gs[1, 1])
f3_ax3.set_title(map_chart.title)
map_chart.ax = f3_ax3

f3_ax4 = figs.add_subplot(gs[1, 2])
f3_ax4.set_title(line_chart.title)
line_chart.ax = f3_ax4

axes = [f3_ax1, f3_ax2, f3_ax3, f3_ax4]
timestr = cases_df.index.max().strftime("%d/%m/%Y")
figs.suptitle(f"Italy COVID-19 Confirmed Cases up to {timestr}")

pandas_alive.animate_multiple_plots(
    'examples/italy-covid.gif',
    plots,
    figs,
    enable_progress_bar=True
)

Italy COVID

Simple Pendulum Motion

Jupyter notebook: pendulum_sample.ipynb

Bubble Chart Example

HTML 5 Videos

Pandas_Alive supports rendering HTML5 videos through the use of df.plot_animated().get_html5_video(). .get_html5_video saves the animation as an h264 video, encoded in base64 directly into the HTML5 video tag. This respects the rc parameters for the writer as well as the bitrate. This also makes use of the interval to control the speed, and uses the repeat parameter to decide whether to loop.

This is typically used in Jupyter notebooks.

import pandas_alive
from IPython.display import HTML

covid_df = pandas_alive.load_dataset()

animated_html = covid_df.plot_animated().get_html5_video()

HTML(animated_html)

Progress Bars!

Generating animations can take some time, so enable progress bars by installing tqdm with pip install tqdm or conda install tqdm and using the keyword enable_progress_bar=True together with filename=movie file name.

By default Pandas_Alive will create a tqdm progress bar when saving to a file, for the number of frames to animate, and update the progres bar after each frame.

import pandas_alive

covid_df = pandas_alive.load_dataset()
# add a filename=movie.mp4 or movie.gif to save to, in order to see the progress bar in action
covid_df.plot_animated(enable_progress_bar=True)

Example of TQDM in action:

TQDM Example

Future Features

A list of future features that may/may not be developed is:

  • Add to line & scatter charts the ability to plot 'X' vs 'Y', as already implemented with bubble plots.
  • Add option of a colorbar for bubble plots when included in multiple plots. Currently only available for single bubble chart animations.
  • Geographic charts (currently using OSM export image, potential geopandas)
  • Loading bar support (potential tqdm or alive-progress)
  • Potentially support writing to GIF in memory with https://github.com/maxhumber/gif
  • Support custom figures & axes for multiple plots (eg, gridspec)

Tutorials

Find tutorials on how to use Pandas_Alive over at:

Inspiration

The inspiration for this project comes from:

Requirements

If you get an error such as TypeError: 'MovieWriterRegistry' object is not an iterator, this signals there isn't a writer library installed on your machine.

This package utilises the matplotlib.animation function, thus requiring a writer library.

Ensure to have one of the supported tooling software installed prior to use!

If the output file name has an extension of .gif, pandas_alive will write this with PIL in memory.

Documentation

Documentation is provided at https://jackmckew.github.io/pandas_alive/

Contributing

Pull requests are welcome! Please help to cover more and more chart types!

Development

To get started in development, clone a copy of this repository to your PC. This will now enable you to create a Jupyter notebook or a standalone .py file, and import pandas_alive as a local module. Now you can create new chart types in pandas_alive/charts.py or pandas_alive/geocharts.py to build to your hearts content!

For Python packages for a development environment check requirements.txt if using PIP, or py38-pandas_alive.yml if using conda.

If you are using conda and are new to setting up environments for collaboration on projects, here are some notes from a previous contributor using conda: Python set up with conda for project collaboration

If you wish to contribute new Jupyter notebooks with different application examples, please place them in this directory: ./examples/test_notebooks/.

pandas_alive's People

Contributors

actions-user avatar dependabot[bot] avatar giswqs avatar jackmckew avatar owenlamont avatar soundspinning avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pandas_alive's Issues

Relative Links in README

The relative links in the README are causing the documentation page on PyPI to have broken links all through it (especially with the visualisations 😲)

The fix for this would be to append the GitHub url to each of the relative links ☺️ (eg, GitHub.com/pandas_alive/)

Barchart race with negative data cannot show properly, the bar with negative data point cannot show in full.

Describe the bug
With a time series dataset, I have a good portion of data point with negative numbers as shown below. In the rendered bar chart race mp4 file, the bar with negative numbers cannot show in full and trimmed. With all positive numbers, the output is perfect. How could I properly deal with negative numbers when plotting barchart race?

Time Series Data
801010.SI 801030.SI
2022/1/4 0.047342559 -0.008077596
2022/1/5 0.04736944 -0.025241489
2022/1/6 0.038082105 -0.016606611
2022/1/7 0.015604335 -0.033500851
2022/1/10 0.052041332 -0.026741557
2022/1/11 0.025246497 -0.029220364
2022/1/12 0.037713837 -0.014121811
2022/1/13 0.013593648 -0.032819728
2022/1/14 0.00680892 -0.0340062
2022/1/17 0.009693236 -0.027111081
2022/1/18 -0.000276873 -0.027900065
2022/1/19 -0.009540015 -0.04367974
2022/1/20 0.006169156 -0.066530309
2022/1/21 -0.025375526 -0.075732457
2022/1/24 -0.029509801 -0.067858598
2022/1/25 -0.065473694 -0.096090236
2022/1/26 -0.050272572 -0.086760253
2022/1/27 -0.071462749 -0.104737099
2022/1/28 -0.045990452 -0.105897604
2022/2/7 -0.03369515 -0.08711979
2022/2/8 -0.025660463 -0.078520865
2022/2/9 0.002451534 -0.065909109
2022/2/10 0.028961431 -0.061227139
2022/2/11 0.004201477 -0.070694945
2022/2/14 -0.016249476 -0.071849458
2022/2/15 -0.019166048 -0.068753446
2022/2/16 -0.027017946 -0.05653718
2022/2/17 -0.017139232 -0.04423902
2022/2/18 -0.006085825 -0.041314787
2022/2/21 -0.002489167 -0.0375776
2022/2/22 -0.008935196 -0.036105496
2022/2/23 -0.023493866 -0.012891396
2022/2/24 -0.039603561 -0.033650658
2022/2/25 -0.038563272 -0.027248903
2022/2/28 -0.038743374 -0.019093408

Reproduce
DF.plot_animated(filename = 'd:/test.mp4', steps_per_period = 15, figsize = np.array([20, 10]), orientation='v')

Allow flexible placement of period label

It'd be neat to be able to select the placement of the period (i.e. year) label - at present it seems to be fixed to bottom-right or top-right. One option could be to have the period contained in the title of the chart


I think that labelling takes place here:
https://github.com/JackMcKew/pandas-alive/blob/e5f5feda5957362d948a7bd8ad63d927d7e6d50c/pandas_alive/charts.py#L274-L286

which in turn uses self.x_label and self.y_label which are set here

https://github.com/JackMcKew/pandas-alive/blob/e5f5feda5957362d948a7bd8ad63d927d7e6d50c/pandas_alive/charts.py#L128

which come from this function:

https://github.com/JackMcKew/pandas-alive/blob/ac770eecb2173e2d06fa852b0d65c03674f18b98/pandas_alive/charts.py#L169-L176

set colors in pie chart

It seems not possible to specify the colors of pie charts. When I pass a list as kwarg (colors=['red', 'blue',...]) I get the following error:

TypeError: pie() got multiple values for keyword argument 'colors'

It seems that there is a color list named "wedge_color_list" passed internally

658 self.ax.pie( --> 659 wedges.values, labels=wedges.index, colors=wedge_color_list, **self.kwargs 660 )

But apparently I cannot overwrite it with my kwarg.

Is this feature not implemented or am I doing something wrong?

Basemap does not fill consistent extent of plot when plotting `geopandas` features

Describe the bug
When plotting a geopandas animation with a basemap supplied by basemap_format, the basemap extends only to the bounding box of the spatial data. In the example below, this causes a constantly changing basemap region (starting with a square and ending up with a longer rectangle). This is a disconcerting effect which distracts from the animation itself.

test

Expected behavior
I would expect the basemap to fill a consistent portion of the total animation figure boundary regardless of the bounding box of the features currently being plotted. For example, I would expect basemap data to be plotted in the following region (red) for this frame:

image

And the same for this frame:
image

To Reproduce

import geopandas as gpd
import pandas_alive
import matplotlib.pyplot as plt

# Load data from geojson
geojson_data = '{"type": "FeatureCollection", "features": [{"id": "097067", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": 29.867664138574362, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[143.91428035361733, -10.808771846671602], [143.9137014965743, -10.808646407382145], [143.91443129161283, -10.804561791263017], [143.96236371001456, -10.587449471886186], [144.10991715020089, -9.91728134220046], [144.21231262746187, -9.450744536185013], [144.2944827857594, -9.076132366431155], [144.29521051510125, -9.07628886669165], [144.29523218028365, -9.076205683919108], [145.97446706945252, -9.433148866436886], [145.99134034415968, -9.436990148774726], [145.99133594702138, -9.437018782993729], [145.99232425097716, -9.43723617914727], [145.62090338068282, -11.173615301039446], [145.61987686014714, -11.173398906348543], [145.61987422907097, -11.173407125668124], [143.9294347528281, -10.8123877461815], [143.91521160865008, -10.809264697635998], [143.91428035361733, -10.808771846671602]]]}}, {"id": "097068", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": 34.80462667027474}, "geometry": {"type": "Polygon", "coordinates": [[[145.30913286709654, -12.618488141834789], [145.30908816509887, -12.618636760819879], [143.5937201619494, -12.253131086590175], [143.59373662459242, -12.253057353407678], [143.59310322188728, -12.252920277836918], [143.66500595778587, -11.929671534392405], [143.79250716856384, -11.355459745433562], [143.91443129161283, -10.804561791263017], [143.9771131029972, -10.521230534002695], [143.97796222930918, -10.521241450273058], [144.66632855176198, -10.668443156659169], [145.6822774166325, -10.882662748312352], [145.6822619317395, -10.882784457217609], [145.68318245055164, -10.882977063735982], [145.68317652508617, -10.883682567486442], [145.31068656283418, -12.61657260301841], [145.31008296000917, -12.618671141533998], [145.30913286709654, -12.618488141834789]]]}}, {"id": "106067", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": 92.28839450249258, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[130.00806271666028, -10.808760720865454], [130.0073044320351, -10.808596244145392], [130.3851395319774, -9.089332279604136], [130.38782264540103, -9.07711121924026], [130.38822442298596, -9.076193125732111], [130.3888242583388, -9.076322240375415], [130.38883874803957, -9.076258609742117], [132.05353305165664, -9.430073334850485], [132.08442872108205, -9.436578216208934], [132.08481733137484, -9.43684711305887], [132.08570298512865, -9.437038827852035], [132.0860815868322, -9.43730243082185], [131.87158394859458, -10.44177136571256], [131.714698027727, -11.17356427699734], [131.713738611739, -11.173362273028507], [131.71372425234694, -11.17342596785272], [130.0182985513368, -10.81133436941015], [130.0086804754553, -10.809194045053754], [130.00806271666028, -10.808760720865454]]]}}, {"id": "106068", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": 98.16696146567573, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[129.687369968105, -12.253099827309851], [129.68693956478307, -12.253006587207274], [129.68686515987946, -12.252449156388291], [130.07078189641607, -10.521095547832832], [130.07138481711394, -10.521225248185688], [130.0713989313729, -10.521161284256547], [131.7668427010805, -10.880589741703321], [131.77536724471986, -10.882411635180734], [131.7759397037145, -10.88280812613711], [131.7769801761577, -10.88303844343788], [131.6720699834156, -11.37305689458326], [131.40396203206524, -12.618718350117197], [131.40284499882242, -12.618665163767721], [131.40234538945023, -12.61856083707641], [131.40164433320348, -12.61860557077436], [131.39030022919962, -12.616266513995845], [129.72426959785258, -12.26139583430603], [129.68839087404686, -12.253621140241187], [129.687369968105, -12.253099827309851]]]}}, {"id": "106069", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": 90.0498490262895, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[131.09031906885653, -14.063226202407128], [131.09029367272683, -14.063274704204506], [131.03765020539686, -14.052602901169053], [129.36395512506684, -13.696900323826341], [129.3639761094108, -13.696807144556509], [129.36346929931952, -13.69669759152833], [129.47705138442598, -13.190704257754394], [129.75087921720228, -11.965705587393014], [129.75181850149036, -11.965907337086191], [129.75307453209152, -11.965909545418581], [129.7743032611266, -11.970463547062328], [131.45474947098708, -12.325654868016546], [131.46159018238853, -12.327276137523512], [131.46597791310654, -12.328193328688895], [131.46650185157063, -12.328456429080365], [131.13862275554254, -13.846209804786069], [131.09129583639879, -14.063429647818419], [131.09031906885653, -14.063226202407128]]]}}, {"id": "106070", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": 77.97755515771955, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[129.03787166242506, -15.14057647904133], [129.0371310267789, -15.140416752035131], [129.03660280110287, -15.140148834237252], [129.2697625360147, -14.111543683068914], [129.4271915828486, -13.412825333094714], [129.4277414735554, -13.410383007422752], [129.42801460800325, -13.410005068204857], [129.4287371800396, -13.410007018876392], [129.43449488474135, -13.411240024729137], [129.4475478960092, -13.413843101496171], [131.13486430032947, -13.769296117920009], [131.15237124959634, -13.772939057833149], [131.15297580869841, -13.773241034900439], [131.15311046888107, -13.773269050732075], [131.15418986303615, -13.773662198421981], [131.14075284080474, -13.836431724149593], [130.77765486302226, -15.504478735221893], [130.77671066985758, -15.508117096025881], [130.77481300493685, -15.507724215273829], [130.77336076783587, -15.507663107340992], [129.03898567599336, -15.141023485305219], [129.03787166242506, -15.14057647904133]]]}}, {"id": "106071", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": 63.85976153132652, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[128.70693799205378, -16.583328833428812], [128.7063615335003, -16.583204806317543], [128.70628793495285, -16.582647494339074], [128.7272262312719, -16.491689623807975], [128.92936200732544, -15.610284609731272], [129.10180770782844, -14.853703052269678], [129.10253956520535, -14.853859668137844], [129.1025746082386, -14.853770521194136], [130.80112235912682, -15.210041211802244], [130.83776393696496, -15.217606987105952], [130.83875340843235, -15.218098611592525], [130.83987351384636, -15.218328822691154], [130.83410511700342, -15.245787336081994], [130.77657944074852, -15.509502901029913], [130.49536111097373, -16.790369518565225], [130.46326536464557, -16.93537629806664], [130.45941242256035, -16.95200828494311], [130.45871935048214, -16.95204974447574], [130.45758341392596, -16.951816458517886], [130.45678632989294, -16.951857079038106], [130.4426792893811, -16.948974907345626], [128.7069162264245, -16.583423299209823], [128.70693799205378, -16.583328833428812]]]}}, {"id": "106072", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": 66.10098387009742, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[128.37281993295176, -18.025717737654215], [128.37210159203434, -18.025482273993145], [128.37210732271288, -18.024775666040682], [128.3785824234838, -17.996732827730195], [128.44492634595844, -17.711410008247817], [128.5980149795141, -17.051617802822715], [128.7631139274434, -16.335522204930417], [128.77172380006064, -16.298105578375335], [128.77214274018147, -16.297186235995568], [128.7727455865398, -16.297245219079095], [128.7727746464901, -16.297119115367803], [130.52198813879266, -16.662682716363534], [130.52199658945614, -16.66276872836035], [130.5231230969374, -16.662998960565094], [130.5232010688509, -16.663419003191965], [130.52143833518613, -16.67170095360451], [130.51329389590924, -16.709039786371104], [130.13989229854855, -18.39459347901251], [130.13947748614768, -18.39578683727089], [130.13858116422185, -18.395603669883187], [130.13855413808997, -18.395724591840064], [128.74057123526694, -18.10427014531466], [128.37334496813304, -18.02588983591451], [128.3730502972487, -18.02579324765607], [128.37282135402697, -18.025744122856544], [128.37281993295176, -18.025717737654215]]]}}, {"id": "106073", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": 85.8496352819355, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[128.03393005855898, -19.46709620053533], [128.03319701774427, -19.466917631331725], [128.0411186359046, -19.432638185948313], [128.170015494341, -18.886728818558517], [128.43790023330112, -17.741759599204205], [128.4386114421374, -17.739485880629548], [128.43924859367542, -17.73960967228387], [128.43938979276805, -17.739420316922295], [130.20285497843008, -18.106360715030313], [130.20282224703763, -18.10650861174279], [130.20375217084913, -18.106705080073613], [130.20379405873996, -18.10723622646962], [130.20049369499938, -18.12216778524136], [129.81667568458957, -19.83864254781606], [129.81564055964316, -19.838432538149593], [129.81560106657201, -19.838579692756106], [128.4267746878048, -19.550733173239525], [128.03830363316033, -19.46816153699595], [128.03683700508878, -19.467804301818695], [128.03392224386334, -19.467180957218158], [128.03393005855898, -19.46709620053533]]]}}, {"id": "106074", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": 57.33606207184423, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[127.6901682615305, -20.90816558021579], [127.6894307449565, -20.908008517047445], [127.7410861122638, -20.691722128119906], [127.94002639939688, -19.85888403079116], [128.0880851345025, -19.234032899960933], [128.100354454165, -19.182177924304266], [128.10063915325526, -19.181666526880544], [128.10137554192198, -19.18176979667867], [128.10142412802173, -19.1816025974577], [129.8625076381003, -19.545863421597698], [129.87826079293967, -19.549042451842404], [129.87944432959713, -19.549465442082244], [129.8802983211316, -19.549637496922287], [129.88155686480712, -19.550034884195135], [129.4909368947102, -21.280090183367168], [129.4905099019211, -21.28114687048309], [129.48959693884086, -21.28096304732437], [129.4895697987457, -21.281081842451346], [127.69019381394567, -20.908364220951142], [127.6901682615305, -20.90816558021579]]]}}, {"id": "106075", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": 58.707279175196526, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[127.34114528888952, -22.348212816510664], [127.34033947641352, -22.348041851376717], [127.53378227636911, -21.55199643279669], [127.73094203859402, -20.733928184841567], [127.75777254753115, -20.622758899852183], [127.75848636334993, -20.62288384344786], [127.75870868691734, -20.622697870156575], [128.84983760949808, -20.849825432983085], [129.55323445468215, -20.992145758118575], [129.55465399864528, -20.9925011906876], [129.5549248317188, -20.992555401943296], [129.55492556266182, -20.992569184703267], [129.5560505751363, -20.992850859263285], [129.55310934955332, -21.00653785872775], [129.16215311397497, -22.7169482119524], [129.16098940577558, -22.721967341686792], [129.16055678413775, -22.72301896007316], [129.1590646150567, -22.72272080261975], [129.15794300955824, -22.722813041376664], [129.15238659801705, -22.721733511262162], [127.3463026082214, -22.349654647472928], [127.34113796054287, -22.348253240710296], [127.34114528888952, -22.348212816510664]]]}}, {"id": "106076", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": 90.97827204874245, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[126.98642815793356, -23.787417240436994], [126.98576366253165, -23.787276835263736], [126.9858517021366, -23.786045157607916], [127.0756320070483, -23.423401692025163], [127.16002465643142, -23.08186853072201], [127.32763717863409, -22.399767989121404], [127.4084502965044, -22.068665029808106], [127.40996065264466, -22.063289147428105], [127.41069718503257, -22.063444282238244], [127.41075919857425, -22.063364646733827], [129.2128293530146, -22.431960092692993], [129.2236287608398, -22.43411570097817], [129.22484410809412, -22.434546353335264], [129.2257042628138, -22.43471774600892], [129.22683943137775, -22.435119460191604], [128.82635611136965, -24.163904725911475], [128.82471852919613, -24.163839633259133], [128.82330794886622, -24.163560601999684], [128.82122014378703, -24.163358759185645], [128.77575160451974, -24.154356191409487], [127.00299992528518, -23.791233461458624], [126.986428539245, -23.787423623818842], [126.98642815793356, -23.787417240436994]]]}}, {"id": "106077", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": 51.76097217560933, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[128.4830734362999, -25.603982417885124], [128.47465680914965, -25.602548815009257], [126.6262101486508, -25.22653663161378], [126.62539416885063, -25.22613260133475], [126.62498417007059, -25.22604576381197], [126.62442647537591, -25.225769244267987], [126.73311672882598, -24.795259999575375], [126.84043327527253, -24.36800242127999], [126.9457940787507, -23.946405136699195], [127.0561171179157, -23.502962509544826], [127.05801691761354, -23.503361040948743], [127.06269752128252, -23.50406627197822], [128.8830296310101, -23.874307523268705], [128.88889139953113, -23.875664855303064], [128.89264846826404, -23.876411781382124], [128.8932056307203, -23.876679915287173], [128.48716759785052, -25.60462377885277], [128.48670392091807, -25.604694079509695], [128.4830734362999, -25.603982417885124]]]}}, {"id": "106078", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": 2.906772680693301, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[126.2571142497158, -26.66374788705592], [126.25642359409419, -26.66360335144358], [126.26905890636172, -26.61387744785672], [126.35482330278768, -26.28117982680821], [126.50491719115108, -25.69542242489204], [126.67431367060966, -25.028320800210917], [126.69565298611316, -24.943895376250943], [126.69627322603831, -24.94230858461836], [126.69703757686236, -24.942390221151058], [126.69709100308077, -24.942253533516755], [128.55083225419625, -25.316732812230573], [128.55395869065444, -25.317628549138064], [128.55396004469281, -25.31764717781112], [128.55501372824986, -25.317891033350428], [128.14323283054543, -27.04476701546884], [128.14217671233075, -27.044561350887943], [128.14209912443016, -27.04469239880914], [126.25709629034706, -26.66381936038643], [126.2571142497158, -26.66374788705592]]]}}, {"id": "106079", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": 0.0, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[125.8815978430446, -28.10081023223811], [125.8814715179326, -28.100783809081943], [125.8809007751496, -28.100503131656023], [125.88109634922486, -28.099127172209197], [125.98498846969122, -27.70433094498851], [126.14220287670588, -27.103043053385562], [126.26889232419404, -26.614550955462082], [126.32629422697521, -26.392315946149694], [126.3292602220517, -26.381000401836907], [126.32956572479021, -26.380629413360133], [126.33022410284146, -26.380766286841894], [126.33024010255812, -26.380709675833025], [128.1983484720824, -26.755495525373824], [128.21124091471884, -26.758270630116037], [128.21123009595573, -26.75831599705301], [128.2120691938422, -26.75847837541494], [128.21174920266338, -26.7606709287185], [127.79369367645597, -28.484182430406232], [127.792622052959, -28.483975757770022], [127.79260969662117, -28.48402581072313], [125.88159404946401, -28.100828580125523], [125.8815978430446, -28.10081023223811]]]}}, {"id": "106080", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": 0.0, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[125.49803579515043, -29.53638527954869], [125.49723474636403, -29.536143677872268], [125.49933996770574, -29.52769109822313], [125.5643816272063, -29.286473056904146], [125.73037268128414, -28.66663231181407], [125.85469108379819, -28.198640161779554], [125.95537904583762, -27.817637043012212], [125.95609469755655, -27.8177850854751], [125.95621741527195, -27.817587889862743], [127.86277830802561, -28.197767000784598], [127.86276417225548, -28.197825320674944], [127.86385833815248, -28.198035138278346], [127.43930987336212, -29.918115499070623], [127.43831651345312, -29.922029003127264], [127.43800102023889, -29.922539224780962], [127.43663553283898, -29.92227838311532], [127.43228021969394, -29.92172139683128], [125.81094404063334, -29.60082989328373], [125.49884290337567, -29.53662870290104], [125.49827816203533, -29.536458377875807], [125.49803098848835, -29.536407171925028], [125.49803579515043, -29.53638527954869]]]}}, {"id": "106081", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": 0.21476448571705786, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[125.57426062837565, -29.253617323572325], [125.574349077143, -29.253550919296977], [127.24782625218738, -29.586494040926336], [127.4630523628978, -29.627640429033733], [127.50847202487036, -29.63628019757304], [127.50932060699473, -29.636597356375706], [127.50803601807824, -29.64252560969111], [127.07570850662171, -31.359571947585827], [127.07470861163341, -31.35939819703462], [127.07468529375065, -31.359488938633085], [127.06849413750818, -31.358318127767326], [127.06546468451197, -31.357791494769536], [126.72445717305958, -31.292746491223777], [125.11858025004285, -30.97340780990811], [125.10906578501938, -30.971227051612356], [125.10562687190973, -30.97051889279713], [125.10504198325852, -30.970233520551844], [125.2079210246769, -30.596979667630812], [125.37597767586401, -29.982560494967785], [125.53262484962868, -29.404133253727384], [125.57319061317257, -29.25369674772449], [125.57350375164874, -29.253461603047786], [125.57426062837565, -29.253617323572325]]]}}, {"id": "106082", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": 2.089510249952543, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[125.26356512032164, -32.517182999642806], [124.72584148152653, -32.40841365899793], [124.70358681792763, -32.40384594452415], [124.70314375200329, -32.40358572985473], [124.95019213664554, -31.52638144742032], [125.15489016709328, -30.78936217928579], [125.18285794602924, -30.688530492273475], [125.18354737537564, -30.68864563881605], [125.18356373255267, -30.688586277959285], [125.18520119784068, -30.68892183935181], [125.18729749319333, -30.68927190321178], [127.01082579799002, -31.04847859983504], [127.14776627850644, -31.074287134811104], [127.14820914302969, -31.074538937685443], [126.70588639727517, -32.79614640327312], [126.70475652665108, -32.79593516247178], [126.70472743629989, -32.79601754809538], [125.26356512032164, -32.517182999642806]]]}}, {"id": "106083", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": 5.6347921038279996, "2020-01-01T01:29:24.118207Z": null, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[124.29199405071262, -33.836304288713116], [124.29138313053733, -33.83617969840487], [124.29093339463354, -33.83591815121987], [124.3762150184195, -33.54222577941503], [124.54246182418245, -32.965865224041416], [124.77565767754402, -32.14752314197299], [124.78250245747144, -32.12334426747147], [124.78295909876573, -32.122559912597815], [124.78615600532518, -32.12321101713672], [124.80003309470203, -32.12577680096279], [126.74674315943426, -32.505193985613204], [126.77793862852772, -32.511019540266304], [126.77874767639737, -32.51147440925369], [126.77988126679871, -32.51169600349565], [126.329232643493, -34.226732345416536], [126.32795548932708, -34.2314988531149], [126.32765004872024, -34.231879065560214], [126.32638876798609, -34.23164568803275], [126.3253656812196, -34.23172979309885], [124.60986428635145, -33.90080511663455], [124.29256527438815, -33.83663647852371], [124.29199405071262, -33.836304288713116]]]}}, {"id": "106084", "type": "Feature", "properties": {"2020-01-01T01:22:37.127576Z": null, "2020-01-01T01:23:01.056739Z": null, "2020-01-01T01:23:24.985903Z": null, "2020-01-01T01:23:48.919302Z": null, "2020-01-01T01:24:12.852702Z": null, "2020-01-01T01:24:36.790337Z": null, "2020-01-01T01:25:00.723735Z": null, "2020-01-01T01:25:24.661371Z": null, "2020-01-01T01:25:48.599007Z": null, "2020-01-01T01:26:12.536642Z": null, "2020-01-01T01:26:36.478513Z": null, "2020-01-01T01:27:00.424621Z": null, "2020-01-01T01:27:24.374964Z": null, "2020-01-01T01:27:48.321071Z": null, "2020-01-01T01:28:12.262943Z": null, "2020-01-01T01:28:36.213286Z": null, "2020-01-01T01:29:00.167865Z": null, "2020-01-01T01:29:24.118207Z": 3.2299473332710713, "2020-01-02T00:26:59.463817Z": null, "2020-01-02T00:27:23.392980Z": null}, "geometry": {"type": "Polygon", "coordinates": [[[123.86910180561922, -35.267021114348935], [123.86839113187737, -35.26687720618592], [123.86841175701664, -35.26680857290545], [123.86785969785855, -35.26663852984522], [123.90692775069489, -35.13570524051392], [124.12573636203525, -34.39915064570171], [124.36625916295402, -33.57683203902846], [124.37254418380144, -33.55525207329451], [124.37285809184203, -33.55501289166589], [124.37573301665002, -33.55559507817994], [124.38257935086402, -33.55673939872407], [125.99873597863721, -33.872266601585395], [126.39649541327681, -33.946223697993766], [126.40286879403, -33.94739944009135], [126.403325267554, -33.94765295974217], [126.40320256874472, -33.94877136206679], [125.94086269908544, -35.66621359502671], [125.93972441909526, -35.66616494542156], [125.93967373499528, -35.66634984231214], [124.07963933708314, -35.309446800624166], [123.86925928364856, -35.26706961834697], [123.86910180561922, -35.267021114348935]]]}}]}'
gdf_wide = gpd.read_file(geojson_data).set_index('id')
gdf_wide.plot(edgecolor='black')

# Basemap
basemap = {
    'url': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    'max_zoom': 19,
    'attribution': 'Map data (c) <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
    'name': 'OpenStreetMap.Mapnik'
}


# Animate
gdf_wide.plot_animated(filename='test.gif',
                       basemap_format={'source': basemap},
                       period_length=1000,
                       edgecolor='black')

Additional context
pandas_alive version '0.2.4'

Reduce repo file size?

The current file size of the repo is 1.64 GB, which is gigantic and takes a while to download. I found this huge file within the .git directory.

1.6G    ./.git/objects/pack/pack-6447cf1a836f2030aae5c189d6b0051224081a89.pack

'period_length' not kept when only changing 'steps_per_period' in gif animation

Hi Jack,

Firstly, many things for sharing this great tool, it worked out of the box for me!
So, I played with a small python (see attached jupyter notebook) with a simple pendulum equation plotting angle, ang_velocity & ang_acceleration. Then I used your tool and got nice gif animations.

However, I noticed that for period_length of 50ms (see last cell in attached notebook), it works fine for 1 & 2 steps_per_period, but when I try 3 or 4 the period seems much longer than 50ms; i.e. slower animation. Would you know why, is it because the period needs to be an integer and something happens depending on number of frames?

Cheers,
Jesus
pendulum_sample.zip

Parallelise frame writing to speed up rendering

Given rendering movies with hundreds to thousands of frames can be quite time expensive I was wondering about the possibility of farming the rendering out to multiple processes - maybe using the joblib package.

I know in principle this should work well - but I'm not sure how difficult it will be to integrate with Matplotlib's animation logic. You derive from the FuncAnimation class which looks like it assumes sequential updates and doesn't seem to lend itself to being parallelised. If we used the Writer classes or particularly the FileWriter classes those seem to have more obvious ways to parallelise but even then it might require some tweaks to Matplotlib itself to support rendering frames in parallel - and the refactoring might be too big for pandas_alive.

Would welcome your thoughts, this may be too far out of scope or maybe it should be an issue raised with Matplotlib itself - but pandas_alive case seemed more specialized where we know each frame corresponds to the row of a Dataframe - the general animation case might not have the state of all frames so well defined so I thought I'd run it by you first in the context of pandas_alive.

plot_animated throws lots of unnecessary warnings when data has many columns

Describe the bug
There are two issues here, but it turns out their cause and fix overlap, so I've included them both.

  • Issue 1

plot_animated outputs two warnings with text: "UserWarning: FixedFormatter should only be used together with FixedLocator". Turns out this is a known gotcha when using set_yticklabels or set_xticklabels. Although the warning is innocuous in this case (it's purpose is described here), it's alarming for the user of pandas_alive, and is easy to suppress.

The "fix" is to call ax.set_xticks(ax.get_xticks()) before the call to set_xticklabels, and similarly for the y-axis.

More info here and here.

  • Issue 2

If the data to be plotted has many columns (more than about 60), then plot_animated outputs dozens of warnings with text like:

/usr/local/lib/python3.9/site-packages/matplotlib/backends/backend_agg.py:201: RuntimeWarning: Glyph 157 missing from current font.
font.set_text(s, 0, flags=flags)

where "157" varies from 128 to beyond 157.

This was really hard to pinpoint, but turns out to be because a fake list of column headings is generated when the plot is first created, by iterating through ASCII characters by the number of columns in the data. If there's too many columns, it iterates right off the normal ASCII range, and the standard matplotlib fonts do not have a glyph for ASCII values beyond 127.

To Reproduce
This method is derived from the panda_alive author's article here. In other words, this is a pretty typical to a "my-first-pandas_alive-animation".

import pandas as pd
import matplotlib.pyplot as plt
import pandas_alive
from IPython.display import HTML
import urllib.request, json
from datetime import datetime

NSW_COVID_19_CASES_BY_LOCATION_URL = "https://data.nsw.gov.au/data/api/3/action/package_show?id=aefcde60-3b0c-4bc0-9af1-6fe652944ec2"
with urllib.request.urlopen(NSW_COVID_19_CASES_BY_LOCATION_URL) as url:
    data = json.loads(url.read().decode())

data_url = data["result"]["resources"][0]["url"]
df = pd.read_csv(data_url)

df['lga_name19'].fillna("Unknown", inplace=True)
df['notification_date'] = pd.to_datetime(df['notification_date'])
df_grouped = df.groupby(["notification_date", "lga_name19"]).size()

df_cases = pd.DataFrame(df_grouped).unstack()
df_cases.columns = df_cases.columns.droplevel().astype(str)
df_cases = df_cases.fillna(0)
df_cases.index = pd.to_datetime(df_cases.index)

animated_html = df_cases.plot_animated(n_visible=15)

Expected behavior
Following an introductory tutorial and using the library as intended would not show dozens of warnings. See so many warnings leaves the newbie feeling like they've done something wrong.

Additional context
Here is a patch for pandas_alive/charts.py that fixes the two issues. If this fix is suitable, I can create a PR, or two if you'd like to separate the issues.

214c214
<         fake_cols = [chr(i + 70) for i in range(self.df.shape[1])]
---
>         fake_cols = [chr(i + 70) for i in range(self.n_visible)]
218c218
<             ax.barh(fake_cols, [1] * self.df.shape[1])
---
>             ax.barh(fake_cols, np.ones(len(fake_cols)))
221c221,225
<             ax.set_yticklabels(self.df.columns)
---
>             # Before the labels are set, convince matplotlib not to throw user warning about FixedLocator and FixedFormatter
>             # Added by HR211009, inspired by https://github.com/matplotlib/matplotlib/issues/18848#issuecomment-817098738
>             ax.set_xticks(ax.get_xticks())
>             ax.set_yticks(ax.get_yticks())
>             ax.set_yticklabels(self.df.columns[:len(fake_cols)])
224c228
<             ax.bar(fake_cols, [1] * self.df.shape[1])
---
>             ax.bar(fake_cols, np.ones(len(fake_cols)))
227c231,235
<             ax.set_xticklabels(self.df.columns, ha="right")
---
>             # Before the labels are set, convince matplotlib not to throw user warning about FixedLocator and FixedFormatter
>             # Added by HR211009, inspired by https://github.com/matplotlib/matplotlib/issues/18848#issuecomment-817098738
>             ax.set_xticks(ax.get_xticks())
>             ax.set_yticks(ax.get_yticks())
>             ax.set_xticklabels(self.df.columns[:len(fake_cols)], ha="right")

Install error

While installing the pandas_alive using pip install pandas_alive, following error message shows: ModuleNotFoundError: No module named 'pandas_alive'

when I run first example, got error infomations like that, how should I do? Any help would be greatly appreciated.

MovieWriter imagemagick unavailable; using Pillow instead.
Traceback (most recent call last):
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\matplotlib\animation.py", line 251, in saving
yield self
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\matplotlib\animation.py", line 1137, in save
writer.grab_frame(**savefig_kwargs)
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\matplotlib\animation.py", line 548, in grab_frame
renderer = self.fig.canvas.get_renderer()
AttributeError: 'FigureCanvasBase' object has no attribute 'get_renderer'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:/Users/aaron.hu/PycharmProjects/appsCrawler.py", line 7, in
df.plot_animated(filename='examples/example-barh-chart.gif')
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\pandas_alive\plotting.py", line 606, in call
return plot(self.df, *args, **kwargs)
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\pandas_alive\plotting.py", line 312, in plot
bcr.save(verify_filename(filename))
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\pandas_alive_base_chart.py", line 567, in save
filename, fps=self.fps, dpi=self.dpi, writer="imagemagick"
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\matplotlib\animation.py", line 1137, in save
writer.grab_frame(**savefig_kwargs)
File "C:\Users\aaron.hu\Anaconda\lib\contextlib.py", line 130, in exit
self.gen.throw(type, value, traceback)
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\matplotlib\animation.py", line 253, in saving
self.finish()
File "C:\Users\aaron.hu\Anaconda\lib\site-packages\matplotlib\animation.py", line 553, in finish
self._frames[0].save(
IndexError: list index out of range

Passing `legend=True` to plot_animated on a GeoDataFrame creates one legend for each frame

Describe the bug
Creating a GeoDataFrame using geopandas in a format suitable for plot_animated applies a matplotlib colour map to each frame. In matplotlib, to display the colour map on the graph you turn on the "legend". This can be done by passing legend=True as an argument to the plot method.

pandas_alive permits supplying extra arguments to plot_animated which are passed on to plot. Unfortunately, matplotlib draws the colour map legend as another axis, so when used in plot_animated, each successive frame gets an additional colour map, ruining the effect.

To Reproduce
This is as per the documention, with the addition of legend=True

import geopandas
import pandas_alive
import contextily

gdf = geopandas.read_file('data/italy-covid-region.gpkg')
gdf.index = gdf.region
gdf = gdf.drop('region',axis=1)

map_chart = gdf.plot_animated(filename='examples/example-geo-polygon-chart.gif',basemap_format={'source':contextily.providers.Stamen.Terrain}, legend=True)

Expected behavior
The legend to appear in the animation, as it does for a single call to plot.

Screenshots

Screen Shot 2021-10-10 at 4 22 46 pm

Additional context
The following diff on pandas_alive/geocharts.py fixes the issue:

184a185,191
>         # Added by HR211010. If the user passes "legend=True", only apply to first frame.
>         if i == 0:
>             try:
>                 self.kwargs.pop("legend")
>             except KeyError:
>                 pass
> 

Unable to customise period label position in `geopandas` animations

Describe the bug
#1 adds x_period_label_location, y_period_label_location and append_period_to_title params to .plot_animated to allow users to customise the position of the period label. However, this does not appear to work for animations based on geopandas.GeoDataFrame objects.

To Reproduce

import geopandas
import pandas_alive
import contextily

gdf = geopandas.read_file('data/nsw-covid19-cases-by-postcode.gpkg').iloc[:,[0, 1, -2, -1]]
gdf.index = gdf.postcode
gdf = gdf.drop('postcode',axis=1)

map_chart = gdf.plot_animated(filename='example-geo-point-chart.gif',
                              append_period_to_title=True)
                              
map_chart2 = gdf.plot_animated(filename='example-geo-point-chart.gif',
                              x_period_label_location=0,
                              y_period_label_location=0)                              
                              

But I receive the following error:

/env/lib/python3.6/site-packages/geopandas/geodataframe.py:577: RuntimeWarning: Sequential read of iterator was interrupted. Resetting iterator. This can negatively impact the performance.
  for feature in features_lst:
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-21-0a82dea1cbbe> in <module>
      9 
     10 map_chart = gdf.plot_animated(filename='example-geo-point-chart.gif',
---> 11                               append_period_to_title=True)  #,basemap_format={'source':contextily.providers.Stamen.Terrain})

/env/lib/python3.6/site-packages/pandas_alive/geoplotting.py in geoplot(input_df, filename, kind, interpolate_period, steps_per_period, period_length, period_fmt, figsize, title, fig, cmap, tick_label_size, period_label, period_summary_func, fixed_max, dpi, writer, enable_progress_bar, basemap_format, enable_markersize, scale_markersize, **kwargs)
     76     )
     77     if filename:
---> 78         map_chart.save(verify_filename(filename))
     79     return map_chart

/env/lib/python3.6/site-packages/pandas_alive/_base_chart.py in save(self, filename)
    605                     frames = []
    606                     for i in range(0, num_frames):
--> 607                         self.anim_func(i)
    608                         buffer = io.BytesIO()
    609                         self.fig.savefig(buffer, format="png")

/env/lib/python3.6/site-packages/pandas_alive/geocharts.py in anim_func(self, i)
    211         self.ax.clear()
    212         self.ax.set_axis_off()
--> 213         self.plot_geo_data(i, self.df)
    214         if self.period_fmt:
    215             self.show_period(i)

/env/lib/python3.6/site-packages/pandas_alive/geocharts.py in plot_geo_data(self, i, gdf)
    180             # cmap='Blues',
    181             cmap=self.cmap,
--> 182             **self.kwargs,
    183         )
    184 

/env/lib/python3.6/site-packages/geopandas/plotting.py in __call__(self, *args, **kwargs)
    923             kind = kwargs.pop("kind", "geo")
    924             if kind == "geo":
--> 925                 return plot_dataframe(data, *args, **kwargs)
    926             if kind in self._pandas_kinds:
    927                 # Access pandas plots

/env/lib/python3.6/site-packages/geopandas/plotting.py in plot_dataframe(df, column, cmap, color, ax, cax, categorical, legend, scheme, k, vmin, vmax, markersize, figsize, legend_kwds, categories, classification_kwds, missing_kwds, aspect, **style_kwds)
    832             markersize=markersize,
    833             cmap=cmap,
--> 834             **style_kwds,
    835         )
    836 

/env/lib/python3.6/site-packages/geopandas/plotting.py in _plot_point_collection(ax, geoms, values, color, cmap, vmin, vmax, marker, markersize, **kwargs)
    301 
    302     if "norm" not in kwargs:
--> 303         collection = ax.scatter(x, y, vmin=vmin, vmax=vmax, cmap=cmap, **kwargs)
    304     else:
    305         collection = ax.scatter(x, y, cmap=cmap, **kwargs)

/env/lib/python3.6/site-packages/matplotlib/__init__.py in inner(ax, data, *args, **kwargs)
   1563     def inner(ax, *args, data=None, **kwargs):
   1564         if data is None:
-> 1565             return func(ax, *map(sanitize_sequence, args), **kwargs)
   1566 
   1567         bound = new_sig.bind(ax, *args, **kwargs)

/env/lib/python3.6/site-packages/matplotlib/cbook/deprecation.py in wrapper(*args, **kwargs)
    356                 f"%(removal)s.  If any parameter follows {name!r}, they "
    357                 f"should be pass as keyword, not positionally.")
--> 358         return func(*args, **kwargs)
    359 
    360     return wrapper

/env/lib/python3.6/site-packages/matplotlib/axes/_axes.py in scatter(self, x, y, s, c, marker, cmap, norm, vmin, vmax, alpha, linewidths, verts, edgecolors, plotnonfinite, **kwargs)
   4430                 )
   4431         collection.set_transform(mtransforms.IdentityTransform())
-> 4432         collection.update(kwargs)
   4433 
   4434         if colors is None:

/env/lib/python3.6/site-packages/matplotlib/artist.py in update(self, props)
   1004 
   1005         with cbook._setattr_cm(self, eventson=False):
-> 1006             ret = [_update_property(self, k, v) for k, v in props.items()]
   1007 
   1008         if len(ret):

/env/lib/python3.6/site-packages/matplotlib/artist.py in <listcomp>(.0)
   1004 
   1005         with cbook._setattr_cm(self, eventson=False):
-> 1006             ret = [_update_property(self, k, v) for k, v in props.items()]
   1007 
   1008         if len(ret):

/env/lib/python3.6/site-packages/matplotlib/artist.py in _update_property(self, k, v)
   1000                 if not callable(func):
   1001                     raise AttributeError('{!r} object has no property {!r}'
-> 1002                                          .format(type(self).__name__, k))
   1003                 return func(v)
   1004 

AttributeError: 'PathCollection' object has no property 'append_period_to_title'

Expected behavior
I would expect to be able to use the x_period_label_location, y_period_label_location and append_period_to_title params in .plot_animated, regardless of whether I provide pandas or geopandas data.

Additional context
pandas_alive version '0.2.4'

error message although writer library is installed

hello, i wanted to try your interesting library, but i always get an error (animated figure does not show up on screen and i also cannot save figure as gif).

errors (when trying to save figure):

with version 0.1.13 i get "RuntimeError: Ensure that a matplotlib writer library is installed, see https://github.com/JackMcKew/pandas_alive/blob/master/README.md#requirements for more details".

with version 0.1.12 i got "'MovieWriterRegistry' object is not an iterator".

im running pandas-alive on windows 10, python 3.7.7, matplotlib 3.2.1, pandas 1.0.3, Pillow 7.1.2.

Move examples into dedicated folder

The repo is currently a little messy with the examples stored at the root - moving these into an examples folder (and updating the readme accordingly) would help tidy this up a bit.

Line plot index crashing plot

I am unsure as to what changed recently, however the line plot is currently crashing with the error

ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

This is on the ax.set_xlim() call in _base_chart.py

The workaround is to convert any timeseries index to datetimes with to_pydatetime()

Eg: df.index = df.index.to_pydatetime()

What happened when I was working on Line Charts, how can I solve it?

What happened when I was working on Line Charts, how can I solve it?
C:\pandas_alive-main\pandas_alive_base_chart.py:458: UserWarning: FixedFormatter should only be used together with FixedLocator
ax.set_yticklabels(self.df.columns)
C:\pandas_alive-main\pandas_alive_base_chart.py:459: UserWarning: FixedFormatter should only be used together with FixedLocator
ax.set_xticklabels([max_val] * len(ax.get_xticks()))
C:\pandas_alive-main\pandas_alive\charts.py:516: UserWarning: Discarding nonzero nanoseconds in conversion
super().set_x_y_limits(self.df, i, self.ax)
And as long as the beginning of my number is too big, there will be some problems

Include / document / improve single-colour chart options

Particularly when it comes to the bar charts, I can see the merit in making a chart using a single colour for the bars (indeed, I can see the merit in that being the default behaviour). I can't quite work out whether this is possible at present.

I guess from the definition of get_colours() it should be, if you provided cmap as a list containing a single colour?

https://github.com/JackMcKew/pandas-alive/blob/ac770eecb2173e2d06fa852b0d65c03674f18b98/pandas_alive/charts.py#L110-L125

At a minimum it'd be nice to document this if it is possible, and maybe rework it to make it a little easier to just use one colour.

period_summary_func parameter ignored on pie chart

When calling plot_animated() and specifying kind='pie', the period_summary_func parameter appears to be ignored. I tried specifying my own figure and adding my own text with ax.text() but that fails to show up in the pie chart animation as well.

After a little digging, it looks like that's because the text is explicitly removed in charts.py, on lines 633-644:

         for text in self.ax.texts[int(bool(self.period_fmt)) :]:
             text.remove()

This can't be commented out, since it prevents wedge labels from piling up on top of each other as the wedges change. The only thing I could think of to fix it was to add the text created by period_summary_func back in afterward. To do that, I added the following code (which is just copied from lines 551-563 of _base_chart.py) to charts.py after line 650:

        if self.period_summary_func:
            values = self.df.iloc[i]
            text_dict = self.period_summary_func(values)
            if "x" not in text_dict or "y" not in text_dict or "s" not in text_dict:
                name = self.period_summary_func.__name__
                raise ValueError(
                    f"The dictionary returned from `{name}` must contain "
                    '"x", "y", and "s"'
                )
            if len(self.ax.texts) != 2:
                self.ax.text(transform=self.ax.transAxes, **text_dict)
            else:
                self.ax.texts[1].set_text(text_dict["s"])

This works as a quick fix for me, but it's not the most elegant solution, which is why I chose to throw it here instead of creating a PR.

Line Plot connects first & last point when used in Multiple Plots with Summary Func

This peculiar bug only happens when:

  1. period_summary_func is defined on a bar chart race
  2. A line chart is created
  3. They are both plotted with animated_multiple_plots()

Upon output, the line chart will have the first and last points connected by a separate line on all time steps.

Workaround

Don't use period_summary_func on bar chart races with line charts

PRs welcome if you find a cause!

conda-forge recipe request

I wanted to request creating a conda-forge recipe for pandas_alive (given I'd love to use it and I almost exclusively use conda and don't touch PyPi much...)

The advantage to other developers would be we could set ffmpeg and imagemagick as dependencies to be automatically install too (conda-forge has an ffmpeg recipe for all major OSes and and imagemagick recipe for Linux/OSX - I'm trying to ask for a Windows recipe for imagemagick too) so it would make setting up a working environment for pandas_alive really easy. I'd be happy to help work on this if you're open to it.

Thanks again for the excellent package - I've made a lot of matplotlib animations with Pandas data sources and had thought about creating something similar a while back but this way surpasses what I had in mind.

Passing `period_label` to `plot_animated` on a GeoDataFrame has no effect if basemap set.

Describe the bug
Plotting a GeoDataFrame using plot_animated takes an optional period_label parameter that can be used to style the date/time label that appears on the graph in each frame. Except, it turns out, if a contextily basemap is specified, as is done with the basemap_format argument. If so, the period_label parameter is ignored and it is not possible to style or position the period label text.

To Reproduce
This is as per the documention, with the addition of the period_label argument.

import geopandas
import pandas_alive
import contextily

gdf = geopandas.read_file('data/italy-covid-region.gpkg')
gdf.index = gdf.region
gdf = gdf.drop('region',axis=1)

map_chart = gdf.plot_animated(filename='examples/example-geo-polygon-chart.gif',basemap_format={'source':contextily.providers.Stamen.Terrain}, period_label={'foo':'bar'})

Expected behavior
The period_label key-value pairs to be interpreted as they are in the non-geopandas examples in the documentation.

Additional context
The following diff on pandas_alive/geocharts.py fixes the issue. Turns out the problem is that the first frame is determined by the number of strings already added to the axes text (where the period label is set), but adding a basemap sets the axes text to the attribution string for that basemap, so there is always already one string already set. I have elected to leave the attribution text be, so the period label can be positioned elsewhere and styled differently so the attribution still appears as desired. A separate flag could be added to remove the attribution is desired.

253,262c260,265
<             num_texts = len(self.ax.texts)
<             if num_texts == 0:
<                 # first frame
<                 self.ax.text(
<                     s=s,
<                     transform=self.ax.transAxes,
<                     **self.get_period_label(self.period_label),
<                 )
<             else:
<                 self.ax.texts[0].set_text(s)
---
> 
>             self.ax.text(
>                 s=s,
>                 transform=self.ax.transAxes,
>                 **self.get_period_label(self.period_label),
>             )

Not getting the plots to show

When I attempt to plot the animation, I don't get any plots or animations to appear. I have a consistent print of each country, year, and numbers over time.

Can't install. Error in Pillows

Describe the bug
Run pip script and errors from issue with pillows

To Reproduce
Steps to reproduce the behavior:

  1. run install pip script
  2. errors

Expected behavior
install

Tested On
Win/python 3.9.5/pip 21.1.2
macOS/python 3.9.5/pip 21.1.2

Additional context
Add any other context about the problem here.

Installing collected packages: pillow, attrs, pandas-alive Attempting uninstall: pillow Found existing installation: Pillow 8.2.0 Uninstalling Pillow-8.2.0: Successfully uninstalled Pillow-8.2.0 Running setup.py install for pillow ... error ERROR: Command errored out with exit status 1:

how to get decimal point data displayed in bar chart race?

Describe the bug
Hi Jack, I'm trying to plot some decimal point data, but the mp4 or gif always display the number (next to the bars) in integer format, is there a way to solve this?

e.g. When I plot fertility rate data, the actual value is 1.56, but the number gets rounded, and gets displayed as 2 which could be misleading.

Is there a setting in pandas_alive.animate_multiple_plots() function that can tune this?

thanks

Screenshots
test

urllib.error.HTTPERROR: HTTP ERROR 404: Not Found

When i try to run the first example i get this error message:

/home/mauro/Documentos/chart-race-cometa/env/bin/python /home/mauro/Documentos/chart-race-cometa/main.py
Traceback (most recent call last):
File "/home/mauro/Documentos/chart-race-cometa/main.py", line 4, in
df = pd.load_dataset('data/covid19.csv')
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas_alive/base.py", line 28, in load_dataset
return pd.read_csv(
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/util/_decorators.py", line 311, in wrapper
return func(*args, **kwargs)
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/parsers/readers.py", line 586, in read_csv
return _read(filepath_or_buffer, kwds)
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/parsers/readers.py", line 482, in _read
parser = TextFileReader(filepath_or_buffer, **kwds)
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/parsers/readers.py", line 811, in init
self._engine = self._make_engine(self.engine)
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/parsers/readers.py", line 1040, in _make_engine
return mapping[engine](self.f, **self.options) # type: ignore[call-arg]
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/parsers/c_parser_wrapper.py", line 51, in init
self._open_handles(src, kwds)
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/parsers/base_parser.py", line 222, in _open_handles
self.handles = get_handle(
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/common.py", line 608, in get_handle
ioargs = _get_filepath_or_buffer(
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/common.py", line 311, in _get_filepath_or_buffer
with urlopen(req_info) as req:
File "/home/mauro/Documentos/chart-race-cometa/env/lib/python3.8/site-packages/pandas/io/common.py", line 211, in urlopen
return urllib.request.urlopen(*args, **kwargs)
File "/usr/lib/python3.8/urllib/request.py", line 222, in urlopen
return opener.open(url, data, timeout)
File "/usr/lib/python3.8/urllib/request.py", line 531, in open
response = meth(req, response)
File "/usr/lib/python3.8/urllib/request.py", line 640, in http_response
response = self.parent.error(
File "/usr/lib/python3.8/urllib/request.py", line 569, in error
return self._call_chain(*args)
File "/usr/lib/python3.8/urllib/request.py", line 502, in _call_chain
result = func(*args)
File "/usr/lib/python3.8/urllib/request.py", line 649, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found

Any ideia of what it could be?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.