pythonpandasmatplotlib

Matplotlib weekly bars are too thin when width<1.0, too thick when width>=1.0


My bar chart

Why are my bars so thin? I have tried setting width to 1 and they go really thick. I'm not sure what else to try. The default thickness is 0.8, is this how it should look?

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
import numpy as np

working_runs = pd.DataFrame(np.random.uniform(1, 2, 210),
                            columns=['distance'],
                            index=pd.date_range('2019-06-01', periods=210, freq='D'))
summed = working_runs['distance'].resample('W').sum()
df = pd.DataFrame(summed)

fig, ax = plt.subplots()

ax.bar(df.index, df.distance)
ax.set_xticks(df.index)
ax.xaxis.set_major_formatter(mdates.DateFormatter("%B %d"))
ax.xaxis.set_minor_formatter(mdates.DateFormatter("%B %d"))
plt.xticks(rotation=90) 

fig = ax.get_figure()

fig.set_figheight(10)
fig.set_figwidth(12)

plt.title('2019 Weekly Running Miles')
plt.ylabel('Distance /m')

fig.savefig("output.png")

I tried changing it like this:

ax.bar(df.index, df.distance,width=1)

0.9 does not look any different and 1.0 looks like this:

thicker lines


Solution

  • I can confirm the weird behaviour, when setting width to something less than 1.0, it seems to be interpreted as a width for one day. When setting it to 1.0 or higher, it gets interpreted as a width for one week.

    It seems to be a problem with how pandas and matplotlib work together.

    A workaround could be to use an edgecolor as in ax.bar(df.index, df.distance, width=1, edgecolor='white') as in:

    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    import pandas as pd
    import numpy as np
    from pandas.plotting import register_matplotlib_converters
    register_matplotlib_converters()
    
    working_runs = pd.DataFrame(np.random.uniform(1, 2, 210),
                                columns=['distance'],
                                index=pd.date_range('2019-06-01', periods=210, freq='D'))
    summed = working_runs['distance'].resample('W').sum()
    df = pd.DataFrame(summed)
    
    fig, ax = plt.subplots()
    ax.bar(df.index, df.distance, width=1, edgecolor='white')
    ax.xaxis.set_major_formatter(mdates.DateFormatter("%B %d"))
    ax.xaxis.set_major_locator(mdates.DayLocator(interval=7))
    ax.autoscale(enable=True, axis='x', tight=True)
    plt.xticks(rotation=90)
    
    plt.title('2019 Weekly Running Miles')
    plt.ylabel('Distance /m')
    plt.show()
    

    example plot

    I experimented with staying in pandas using df.plot.bar(y='distance', width=0.9, ax=ax). Formatting the dates can be accomplished by explicitly converting the index to a list of labels. Also in this case, the plot would look nicer using width=1 and edgecolor='white'.

    import matplotlib.pyplot as plt
    import matplotlib.ticker as mticker
    import pandas as pd
    import numpy as np
    
    working_runs = pd.DataFrame(np.random.uniform(1, 2, 210),
                                columns=['distance'],
                                index=pd.date_range('2019-06-01', periods=210, freq='D'))
    summed = working_runs['distance'].resample('W').sum()
    df = pd.DataFrame(summed)
    
    fig, ax = plt.subplots()
    df.plot.bar(y='distance', width=0.9, ax=ax)
    plt.xticks(range(len(df.index)),
               [t.to_pydatetime().strftime("%b %d") for t in df.index],
               rotation=90)
    plt.title('2019 Weekly Running Miles')
    plt.ylabel('Distance /m')
    plt.show()
    

    example with pandas plot.bar